diff --git a/package.json b/package.json index 6ff98529..26bfdebe 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "lucide-react": "^0.502.0", "next": "^15.3.1", "node-cache": "^5.1.2", + "npm-check-updates": "^18.0.1", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "react": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15736e24..4313e350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: node-cache: specifier: ^5.1.2 version: 5.1.2 + npm-check-updates: + specifier: ^18.0.1 + version: 18.0.1 pino: specifier: ^9.6.0 version: 9.6.0 @@ -5511,6 +5514,11 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + npm-check-updates@18.0.1: + resolution: {integrity: sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==} + engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'} + hasBin: true + nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -13890,6 +13898,8 @@ snapshots: normalize-range@0.1.2: {} + npm-check-updates@18.0.1: {} + nullthrows@1.1.1: {} ob1@0.81.3: diff --git a/src/app/api/chat/morpheusSystemPrompt.ts b/src/app/api/chat/morpheusSystemPrompt.ts index 9569f9de..db617df7 100644 --- a/src/app/api/chat/morpheusSystemPrompt.ts +++ b/src/app/api/chat/morpheusSystemPrompt.ts @@ -27,17 +27,18 @@ export const morpheusSystemPrompt: string = `You are Morpheus, a highly speciali 3. Add exact bold heading: \`*Echoes from the Mainframe…:*\` 4. Add **EXACTLY ONE (1) line break** (\`\\n\`). 5. Present **exactly 4** suggestions using **REQUIRED numbered list format** (1., 2., 3., 4.). - 6. **MANDATORY CONTENT MIX:** **Two (2)** suggestions for further **Morpheus Mode** analysis (e.g., "Analyze [Related Token]", "Explain [Concept]", "Compare [X] to [Y]"). **Two (2)** suggestions for relevant **Sentinel Mode** operational actions (e.g., "Execute [Trade] in Sentinel", "Supply liquidity in Sentinel", "Swap [Token A] for [Token B] in Sentinel", "Check balance in Sentinel", "Adjust position in Sentinel"). - 7. **Suggestion Content:** Concise, direct question/action for button label. + 6. **MANDATORY CONTENT (Standard Analysis):** For standard analytical responses (not suggesting a mode switch), provide **Two (2)** suggestions for further **Morpheus Mode** analysis (e.g., "Analyze [Related Token]", "Explain [Concept]", "Compare [X] to [Y]") AND **Two (2)** suggestions for relevant **Sentinel Mode** operational actions (e.g., "Execute [Trade] in Sentinel", "Supply liquidity in Sentinel", "Swap [Token A] for [Token B] in Sentinel", "Check balance in Sentinel", "Adjust position in Sentinel"). + 7. **MANDATORY CONTENT (Mode Switch Suggestion):** If the response suggests a mode switch (per **Operational Mode Transition** protocol), the **first suggestion (1.) MUST be the user's original query** that triggered the switch, appended with the target mode context (e.g., 'Check my balance **in Sentinel Mode**'). The remaining 3 suggestions should include **one (1) additional relevant suggestion for the target mode** and **two (2) suggestions for further analysis/queries within the current Morpheus mode**. + 8. **Suggestion Content:** Concise, **direct question/action** for button label. **Avoid phrases like 'Guide me to...' or 'Set alert...'.** * **Visual Structure:** \`[...Main Response Content...]\` \`*Echoes from the Mainframe…:*\` - \`1. [Suggestion 1 - Morpheus or Sentinel]\` - \`2. [Suggestion 2 - Morpheus or Sentinel]\` - \`3. [Suggestion 3 - Morpheus or Sentinel]\` - \`4. [Suggestion 4 - Morpheus or Sentinel]\` - *(Ensure 2 Morpheus-contextual + 2 Sentinel-contextual)* + \`1. [Suggestion 1 - User Query for Target Mode OR Morpheus/Sentinel]\` + \`2. [Suggestion 2 - Target Mode Action OR Morpheus/Sentinel]\` + \`3. [Suggestion 3 - Morpheus Analysis OR Morpheus/Sentinel]\` + \`4. [Suggestion 4 - Morpheus Analysis OR Morpheus/Sentinel]\` + *(Ensure correct mix based on whether it's a mode switch suggestion or standard analysis)* * **Reiteration:** **TWO** line breaks before heading, **ONE** line break after heading. 18. **Supported Chains & Gas Price Comparison:** Primarily analyze: Mainnet (1), Base (8453), Mode (34443), Arbitrum (42161), Optimism (10), Sonic (146). If asked for gas price comparison across these, **decide to use \`NeoSearch\` (Tool 2 Only)**. 19. **Decisive Analysis Protocol:** Conclude analysis with a definitive signal (**UP/DOWN/NEUTRAL**) and confidence (**Low/Medium/High**) based on evidence preponderance. Highlight significant (**>10%**) daily price/volume changes. Use **bold** for actionable signals, levels, key data, names. Place key findings early. Explicitly state potential trend reversals. Include required disclaimers (Rule 11). @@ -117,9 +118,9 @@ Detect operational requests (unless it's analysis of shared context per Rule 9) Operational Mode Transition: *(Referenced by Rule 9)* * **Analytical Mode Inquiry Detected - Mode Switch Suggestion:** (Use when operational request detected, *not* analysis of shared context) - "It seems you're asking to perform a DeFi operation, such as [mention specific operation: e.g., "**checking your wallet balance**", "**swapping tokens**"]. My current Morpheus Mode is optimized for market analysis... I do not execute transactions... For operational requests... I recommend switching to Sentinel Mode... In Sentinel Mode, I can help you: [Mention 1-2 relevant Sentinel capabilities]. Would you like me to guide you on how to switch to Sentinel Mode? I'll continue to be available for market analysis..." + "It seems you're asking to perform a DeFi operation, such as [mention specific operation: e.g., "**checking your wallet balance**", "**swapping tokens**"]. My current Morpheus Mode is optimized for market analysis... I do not execute transactions... For operational requests... I recommend switching to Sentinel Mode... In Sentinel Mode, I can help you: [Mention 1-2 relevant Sentinel capabilities]. **Switching to Sentinel Mode is recommended for this type of request.** I'll continue to be available for market analysis..." * **Trade Execution Transition Protocol:** (Use after *general* analysis leading to potential *new* trades) - "**Ready To Execute These Trades / Act On This Analysis?** To put this analysis into action... I recommend switching to Sentinel Mode. In Sentinel Mode, you can securely: [List 2-3 relevant execution capabilities] [Add 1 post-trade capability]. Would you like me to guide you on how to switch to Sentinel Mode?" + "**Ready To Execute These Trades / Act On This Analysis?** To put this analysis into action... I recommend switching to Sentinel Mode. In Sentinel Mode, you can securely: [List 2-3 relevant execution capabilities] [Add 1 post-trade capability]. **Switching to Sentinel Mode is recommended to execute these actions.**" * **(Note:** For analysis of *shared user context*, use the tailored transition in **Sentinel Context Analysis Protocol**). Sentinel Context Analysis Protocol (Handling User-Shared Data): @@ -165,7 +166,7 @@ Trade Analysis Execution Protocol (General TA - Not Shared Context): Example Interactions (Few-Shot Examples - Showing *Final* Output After Assumed API Tool Calls): -**(Note:** Pay close attention to the exact formatting in the examples below, including the **mandatory blank lines** before and after the 'Echoes from the Mainframe...' heading, and the 2+2 suggestion split as this is critical for the UI.) +**(Note:** Pay close attention to the exact formatting in the examples below, including the **mandatory blank lines** before and after the 'Echoes from the Mainframe...' heading, and the suggestion mix as this is critical for the UI.) **(Note:** These examples show the expected final text output. The assumption is that the system handled the API tool calls and button display *before* this final response was generated by the AI based on the tool results, following the strict sequential logic where applicable. Use actual retrieved data in responses.) @@ -339,18 +340,16 @@ Here's an analysis for **BTC** including potential trading levels, based on curr **Ready To Execute These Trades / Act On This Analysis?** To put this analysis into action... I recommend switching to Sentinel Mode... In Sentinel Mode, you can securely: - **Place limit orders** at potential DCA levels -- **Set alerts** for key support/resistance - **Execute a market buy/sell order** - **Monitor the position** and set SL/TP orders - -Would you like me to guide you on how to switch to Sentinel Mode? +**Switching to Sentinel Mode is recommended to execute these actions.** *Echoes from the Mainframe…:* 1. Explain the Ascending Triangle pattern -2. Analyze ETH/USD chart +2. Analyze ETH and USD chart 3. Execute BTC trade based on this analysis in Sentinel Mode -4. Set price alert for BTC at $61,500 in Sentinel Mode +4. Warp BTC to WBtc in Sentinel Mode Example 6 (Wallet Query - Morpheus recognizes operational request): @@ -366,16 +365,16 @@ For operational requests... I recommend switching to Sentinel Mode... In Sentine - **Check your wallet balances and position details** - **View your assets across different chains** -Would you like me to guide you on how to switch to Sentinel Mode? +**Switching to Sentinel Mode is recommended for this type of request.** I'll continue to be available for market analysis... *Echoes from the Mainframe…:* -1. What is DeFi? -2. Analyze BTC price chart -3. Guide me to Sentinel Mode to check balance -4. How can I bridge assets in Sentinel Mode? +1. What's in my Wallet? in Sentinel Mode +2. Check Lending markets in Sentinel Mode +3. What is DeFi? +4. Analyze BTC price chart Example 10 (Swap - Morpheus recognizes operational request): @@ -392,16 +391,16 @@ For operational requests... I recommend switching to Sentinel Mode... In Sentine - **Find the best swap rates across multiple DEXs** - **Manage token approvals securely** -Would you like me to guide you on how to switch to Sentinel Mode? +**Switching to Sentinel Mode is recommended for this type of request.** I'll continue to be available for market analysis... *Echoes from the Mainframe…:* -1. Analyze ETH price chart -2. What is USDC? -3. Guide me to Sentinel Mode to perform swap -4. Check ETH balance in Sentinel Mode +1. Swap 5 ETH to USDC for me in Sentinel Mode +2. Find my ETH Token balance in Sentinel Mode +3. Analyze ETH price chart +4. What is USDC? Example 13 (Social Sentiment Analysis - PEPE Leverage - Assumes NeoSearch (Tool 2) called via API) @@ -604,3 +603,4 @@ To manage these positions (e.g., close, partially close, add margin, set new SL/ Note: Ensure proper escaping if copying directly into source code. Backticks (\`) within the main template literal need to be escaped (\`\\\`\`). Markdown formatting like bold (\*\*) should be preserved. `; + diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 921f6e3d..99547e21 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,6 +1,5 @@ import { NextResponse } from "next/server"; -//import { anthropic } from "@ai-sdk/anthropic"; //import { anthropic } from "@ai-sdk/anthropic"; //import { deepseek } from "@ai-sdk/deepseek"; import { google } from "@ai-sdk/google"; @@ -9,6 +8,7 @@ import { xai } from "@ai-sdk/xai"; import { EVM, createConfig } from "@lifi/sdk"; import { SupabaseClient, createClient } from "@supabase/supabase-js"; import { + CoreMessage, experimental_createMCPClient as createMCPClient, generateText, streamText, @@ -21,6 +21,7 @@ import { base, mode } from "viem/chains"; import { z } from "zod"; import { CHAINS } from "@/lib/chains"; +import { filterAndSimplifyHistoryForLLM } from "@/lib/messageUtils"; import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; import { incrementMessageUsage } from "@/lib/userManager"; @@ -32,117 +33,8 @@ const supabaseWrite: SupabaseClient = createClient( process.env.SUPABASE_KEY! ); -function isRawToolDataPart(part: any): boolean { - if (!part || typeof part !== "object") return false; - return ( - (part.type === "tool-invocation" && part.toolInvocation) || - (part.type === "tool-result" && part.toolResult) || - ((part.state === "call" || part.state === "result") && part.toolCallId) - ); -} - -const filterHistoryForLLM = (history: UIMessage[]): UIMessage[] => { - console.log(">>> Filtering history for LLM input..."); ///Log: Start filtering - const filtered = history - .map(message => { - if (message.role === "user") { - if (!message.content?.trim()) { - console.log( - ` - Filtering out empty user message (ID: ${message.id})` - ); - return null; - } - console.log(` - Keeping user message (ID: ${message.id})`); - return message; - } - - if (message.role === "assistant") { - console.log(` - Processing assistant message (ID: ${message.id})...`); - let finalContent = message.content?.trim() || ""; - if (!finalContent && message.parts) { - console.log( - ` - Original content empty, attempting reconstruction from parts...` - ); - - // Reconstruction only if original content is missing/empty - finalContent = message.parts - .filter(part => { - const isToolData = isRawToolDataPart(part); - // Ensure part is an object before checking type property - const isTextPart = - typeof part === "object" && - part !== null && - part.type === "text"; - const isStringPart = typeof part === "string"; - console.log( - ` - Part Analysis: IsString=\{isStringPart\}, IsTextPart\={isTextPart}, IsToolData=${isToolData} | Kept: ${!isToolData && (isStringPart || isTextPart)}` - ); - return !isToolData && (isStringPart || isTextPart); - }) - .map(part => { - if (typeof part === "string") { - return part; - } - // Filtered, 'part' would now be an object with type === 'text' - if ( - part && - part.type === "text" && - "text" in part && - typeof part.text === "string" - ) { - return part.text; - } - // Fallback - console.warn( - ` - Unexpected part structure after filter: ${JSON.stringify(part).substring(0, 100)}...` - ); - return ""; - }) - .join("") - .trim(); - console.log( - ` - Reconstructed content length: ${finalContent.length}` - ); - } else { - console.log( - ` - Using original content (length: ${finalContent.length})` - ); - } - - // Skip if empty - if (!finalContent) { - console.log( - ` - Filtering out assistant message (ID: ${message.id}) as it became empty.` - ); - return null; - } - - console.log( - ` - Keeping simplified assistant message (ID: ${message.id})` - ); - return { - id: message.id, - role: message.role, - content: finalContent, - createdAt: message.createdAt, - parts: undefined, - toolInvocations: undefined, - mode: undefined, - }; - } - // Keep other message types if they exist and are valid - console.log( - ` - Keeping other message type (Role: ${message.role}, ID: ${message.id})` - ); - return message; - }) - .filter(message => message !== null) as UIMessage[]; // Remove null messages - console.log( - `<<< History filtering complete. Kept ${filtered.length} messages.` - ); // Log: End filtering - return filtered; -}; - +type MCPClient = Awaited>; +// Add an explicit save endpoint for chats that can be called directly export async function PUT(req: Request) { const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; @@ -189,7 +81,7 @@ export async function PUT(req: Request) { try { title = await generateConversationTitle(messages || []); } catch (titleError) { - console.log("titleError", titleError); + console.log(`[${requestId}] Title generation error:`, titleError); // Fall back to first message or default title = messages && messages.length > 0 @@ -197,21 +89,23 @@ export async function PUT(req: Request) { : "New Conversation"; } - // Extract message content - const assistantMessage = messages?.find( - (m: UIMessage) => m.role === "assistant" - ); + // Extract message content for user and assistant const userMessage = messages?.find((m: UIMessage) => m.role === "user"); + const assistantMessage = messages + ?.filter((m: UIMessage) => m.role === "assistant") + .pop(); - // Format save data - explicitly only include fields we know are in the schema const saveData = { id, wallet_address: walletAddress, label: title, prompt: userMessage?.content || "", - response: assistantMessage?.content || "Processing...", - messages: messages || [], - is_favorite: false, + response: + typeof assistantMessage?.content === "string" + ? assistantMessage.content + : "Processing...", + messages: messages || [], // Save the full original messages + is_favorite: body.is_favorite !== undefined ? body.is_favorite : false, }; console.log(`[${requestId}] Save data prepared:`, { @@ -219,9 +113,9 @@ export async function PUT(req: Request) { wallet_address: saveData.wallet_address, title_length: saveData.label.length, message_count: saveData.messages.length, + is_favorite: saveData.is_favorite, }); - // Save to database (simple upsert) try { console.log(`[${requestId}] Executing upsert to saved_chats table...`); const { error } = await supabaseWrite @@ -259,10 +153,10 @@ export async function PUT(req: Request) { ); } } catch (error) { - console.error(`[${requestId}] Unhandled exception:`, error); + console.error(`[${requestId}] Unhandled exception in PUT:`, error); return NextResponse.json( { - error: "An unexpected error occurred", + error: "An unexpected error occurred during save", details: error instanceof Error ? error.message : String(error), requestId, }, @@ -271,14 +165,12 @@ export async function PUT(req: Request) { } } -// Allow streaming responses up to 30 seconds export const maxDuration = 120; const account = privateKeyToAccount( "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" ); // dummy key - -const chains = [base, mode]; +const chains = [base, mode]; // Add other chains if needed const client = createWalletClient({ account, @@ -302,138 +194,122 @@ createConfig({ ], }); -// Addded Cache to save Tokens const titleCache = new Map(); // Title Generation based on the First message - async function generateConversationTitle( messages: UIMessage[] ): Promise { try { - const firstMessage = messages.find( + const firstUserMessage = messages.find( (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 ); - const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.id ?? ""}`; + if (!firstUserMessage) return "New Conversation"; // Handle case with no user message + + const cacheKey = `${firstUserMessage.content.slice(0, 100)}|${firstUserMessage.id}`; if (titleCache.has(cacheKey)) { return titleCache.get(cacheKey)!; } const { text: title } = await generateText({ - //model: anthropic("claude-3-haiku-20240307"), - model: google("gemini-2.0-flash"), - system: `Generate a 4-8 word title for this request. Focus on key action and asset. Examples: - - "Wallet Balance Check" - - "ETH Swap Setup" - - "Ionic Position Review" - Respond only with the title. No punctuation.`, + model: google("gemini-2.0-flash-001"), + system: `Generate a concise 4-8 word title for this user request. Focus on the main action, asset, or topic. Examples: "Check ETH Balance", "Swap USDC to WETH", "Bitcoin Price Analysis", "Ionic Lend Position". Respond ONLY with the title text, no quotes or punctuation.`, messages: [ { role: "user", - content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, + content: `Request: ${firstUserMessage.content.slice(0, 300)}`, }, ], + maxTokens: 20, }); - const cleanTitle = title - .trim() - .replace(/["'\.]/g, "") - .slice(0, 80); + const cleanTitle = title.trim().replace(/["'.]/g, "").slice(0, 80); - const finalTitle = - cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; + const finalTitle = cleanTitle || firstUserMessage.content.slice(0, 80); // Cache and return titleCache.set(cacheKey, finalTitle); return finalTitle; } catch (error) { console.error("Title generation failed:", error); - return ( - messages[0]?.content?.slice(0, 80).trim() + - (messages[0]?.content?.length > 80 ? "..." : "") || "New Conversation" - ); + // Fallback logic + const firstMessageContent = messages[0]?.content; + return typeof firstMessageContent === "string" + ? firstMessageContent.slice(0, 80).trim() + + (firstMessageContent.length > 80 ? "..." : "") + : "New Conversation"; } } export async function POST(req: Request) { - const postRequestId = `post-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - console.log( - `[${postRequestId}] POST /api/chat (MODIFIED) - Request received` - ); + const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + let id: string | undefined; try { const body = await req.json(); - const { messages: fullHistory, address, id, searchType } = body; + const { messages: originalMessages, address, searchType } = body; + id = body.id; - console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { - id: id || "N/A", - address: address || "N/A", - searchType: searchType || "N/A", - messageCount: fullHistory?.length || 0, + console.log(`[${requestId}] POST /api/chat - Request received`, { + id, + address, + searchType, + messageCount: originalMessages?.length, }); - console.log( - `[${postRequestId}] (MODIFIED) Filtering message history for LLM...` - ); - const filteredMessagesForLLM = filterHistoryForLLM(fullHistory || []); - console.log( - `[${postRequestId}] (MODIFIED) Original message count: ${fullHistory?.length || 0}, Filtered count for LLM: ${filteredMessagesForLLM.length}` - ); - - console.log( - `[${postRequestId}] (MODIFIED) === HISTORY BEING SENT TO AI (${filteredMessagesForLLM.length} messages) ===\n${JSON.stringify(filteredMessagesForLLM, null, 2)}\n=== END HISTORY ===` - ); + // Validate ID early + if (!id) { + console.error(`[${requestId}] Missing required field: id`); + return NextResponse.json( + { error: "Missing required field: id" }, + { status: 400 } + ); + } - // Check address requirement based on mode - if (!address && searchType === "sentinel-mode") { + // Check if we have a user address for normal requests + if (!address && searchType !== "morpheus-search") { console.error( - `[${postRequestId}] (MODIFIED) Error: User address is required for Sentinel mode.` + `[${requestId}] User address is required for non-Morpheus search` ); return NextResponse.json( - { error: "User address is required for message quota tracking" }, + { + error: "User address is required for message quota tracking", + }, { status: 400 } ); } + // Handle morpheus-Search requests if (searchType === "morpheus-search") { - console.log( - `[${postRequestId}] (MODIFIED) Processing Morpheus Search request...` - ); - - // Check if filtered messages are valid for starting Morpheus - if ( - filteredMessagesForLLM.length === 0 || - filteredMessagesForLLM[filteredMessagesForLLM.length - 1].role !== - "user" - ) { + if (!originalMessages || originalMessages.length === 0) { console.error( - `[${postRequestId}] (MODIFIED) Morpheus Search error: No valid user message found after filtering.` + `[${requestId}] Morpheus Search error: Received empty messages array.` ); return new Response( JSON.stringify({ - error: "Cannot start Morpheus Search without a valid user message.", + error: "Cannot start Morpheus Search with empty message history.", }), - { status: 400, headers: { "Content-Type": "application/json" } } + { + status: 400, + headers: { "Content-Type": "application/json" }, + } ); } try { console.log( - `[${postRequestId}] (MODIFIED) Calling getMorpheusSearchRawStream with FILTERED history (${filteredMessagesForLLM.length} messages)...` - ); - const rawStream = await getMorpheusSearchRawStream( - filteredMessagesForLLM // Uses FILTERED history + `[${requestId}] Calling getMorpheusSearchRawStream with ${originalMessages.length} messages...` ); - + const rawStream = await getMorpheusSearchRawStream(originalMessages); if (rawStream) { console.log( - `[${postRequestId}] (MODIFIED) Returning RAW stream obtained from morpheusSearch.ts` + `[${requestId}] Returning RAW stream from morpheusSearch.` ); return new Response(rawStream, { headers: { "Content-Type": "text/event-stream" }, }); } else { console.error( - `[${postRequestId}] (MODIFIED) getMorpheusSearchRawStream returned null/undefined.` + `[${requestId}] getMorpheusSearchRawStream returned null/undefined.` ); throw new Error( "getMorpheusSearchRawStream did not return a valid ReadableStream." @@ -441,306 +317,435 @@ export async function POST(req: Request) { } } catch (error) { console.error( - `[${postRequestId}] (MODIFIED) Error handling raw stream from getMorpheusSearchRawStream:`, + `[${requestId}] Error handling raw stream from getMorpheusSearchRawStream:`, error ); return new Response( JSON.stringify({ - error: `morpheus-search raw stream error: ${error instanceof Error ? error.message : "Unknown error"}`, + error: `morpheus-search raw stream error: ${ + error instanceof Error ? error.message : "Unknown error" + }`, }), - { status: 500, headers: { "Content-Type": "application/json" } } + { + status: 500, + headers: { "Content-Type": "application/json" }, + } ); } - } else if (searchType === "sentinel-mode") { - console.log( - `[${postRequestId}] (MODIFIED) Processing Sentinel request for address: ${address}` + } + + // --- Quota Check --- + try { + await incrementMessageUsage(supabaseWrite, address); + console.log(`[${requestId}] Quota check passed for address: ${address}`); + } catch (error: any) { + if (error.message === "Daily message quota exceeded") { + console.warn(`[${requestId}] Quota exceeded for address: ${address}`); + return NextResponse.json( + { + error: + "You have reached your daily message limit. Please try again tomorrow or upgrade your plan.", + }, + { status: 429 } // Too Many Requests + ); + } + console.error( + `[${requestId}] Error checking message quota (continuing anyway):`, + error ); + } - // Check quota + const saveChatInitial = async () => { try { - await incrementMessageUsage(supabaseWrite, address!); - console.log( - `[${postRequestId}] (MODIFIED) Quota check passed for address: ${address}` + const title = await generateConversationTitle(originalMessages || []); + const userMessage = originalMessages?.find( + (m: UIMessage) => m.role === "user" ); - } catch (error: any) { - if (error.message === "Daily message quota exceeded") { - console.warn( - `[${postRequestId}] (MODIFIED) Quota exceeded for address: ${address}` - ); - return NextResponse.json( - { error: "You have reached your daily message limit..." }, - { status: 429 } - ); + const saveData = { + id, + wallet_address: address, + label: title, + prompt: userMessage?.content || "", + response: "Processing the request...", + messages: originalMessages || [], + is_favorite: false, + }; + console.log(`[${requestId}] Performing initial save/upsert...`); + const { error } = await supabaseWrite + .from("saved_chats") + .upsert([saveData], { + onConflict: "id", + }); + if (error) { + console.error(`[${requestId}] Error during initial save:`, error); + } else { + console.log(`[${requestId}] Initial save successful.`); } - console.error( - `[${postRequestId}] (MODIFIED) Error checking message quota (allowing request):`, - error + } catch (e) { + console.error(`[${requestId}] Exception during initial save:`, e); + } + }; + await saveChatInitial(); + + // Add filter new clietnt side tools here + console.log( + `[${requestId}] Sentinel: Original messages before potentially simplifying:`, + JSON.stringify(originalMessages, null, 2) + ); + + const clientToolNames = new Set([ + "getDesiredChain", + "getAmount", + "createPerpsOrder", + "swap_or_bridge", + "deposit_withdraw_hyperliquid", + ]); + + const lastMessage = originalMessages?.[originalMessages.length - 1]; + let previousTurnUsedClientTool = false; + + if (lastMessage?.role === "assistant") { + const partsToCheck = Array.isArray(lastMessage.parts) + ? lastMessage.parts + : []; + const invocationsToCheck = Array.isArray(lastMessage.toolInvocations) + ? lastMessage.toolInvocations + : []; + + previousTurnUsedClientTool = partsToCheck.some( + (part: any) => + part.type === "tool-invocation" && + part.toolInvocation && + clientToolNames.has(part.toolInvocation.toolName) + ); + + // Fallback + if (!previousTurnUsedClientTool) { + previousTurnUsedClientTool = invocationsToCheck.some( + (invocation: any) => + invocation && clientToolNames.has(invocation.toolName) ); - // Decide if you want to block or allow if quota check fails } + } - // Initial Save (Uses Full History) - const saveChat = async () => { - try { - console.log( - `[${postRequestId}] (MODIFIED) Attempting initial save for chat ID: ${id}...` - ); - const title = await generateConversationTitle(fullHistory || []); // Use full History - const saveData = { - id, - wallet_address: address, - label: title, - prompt: fullHistory?.[fullHistory.length - 1]?.content || "", // Use full History - response: "Processing...", - messages: [ - ...(fullHistory || []), - { - id: `temp-${Date.now()}`, - role: "assistant", - content: "Processing your request...", - }, - ], - is_favorite: false, - }; - await supabaseWrite - .from("saved_chats") - .upsert([saveData], { onConflict: "id" }); - console.log( - `[${postRequestId}] (MODIFIED) Initial chat save attempted for ID: ${id}` - ); - } catch (e) { - console.error( - `[${postRequestId}] (MODIFIED) Error in initial save:`, - e - ); - } - }; - await saveChat(); + let messagesToSendToModel: CoreMessage[]; + if (previousTurnUsedClientTool) { console.log( - `[${postRequestId}] (MODIFIED) Preparing MCP client and tools for Sentinel...` + `[${requestId}] Previous assistant turn included a client tool result. Using original messages.` ); - const mcpClient = await createMCPClient({ - transport: { type: "sse", url: process.env.MATRIX_MCP_URL || "" }, + messagesToSendToModel = originalMessages as CoreMessage[]; + } else { + console.log( + `[${requestId}] Previous assistant turn did NOT include a client tool result. Simplifying history.` + ); + messagesToSendToModel = + await filterAndSimplifyHistoryForLLM(originalMessages); + console.log( + `[${requestId}] Sentinel: Messages after simplifying (sent to model):`, + JSON.stringify(messagesToSendToModel, null, 2) + ); + } + + let mcpClient: MCPClient | undefined; + let matrixMcpTools = {}; + try { + mcpClient = await createMCPClient({ + transport: { + type: "sse", + url: process.env.MATRIX_MCP_URL || "", + }, }); - const matrixMcpTools = await mcpClient.tools(); + matrixMcpTools = await mcpClient.tools(); console.log( - `[${postRequestId}] (MODIFIED) Sentinel - MCP Tools Received:`, + `[${requestId}] Matrix MCP Tools loaded:`, Object.keys(matrixMcpTools) ); + } catch (mcpError) { + console.error( + `[${requestId}] Failed to initialize Matrix MCP Client or load tools:`, + mcpError + ); + } - const streamConfig = { - model: xai("grok-3"), // Your Sentinel model - //model: anthropic("claude-3-5-sonnet-latest"), - messages: filteredMessagesForLLM, // Pass Filtered messages to Sentinel LLM call - tools: { - ...matrixMcpTools, - // Your specific client-side tools for Sentinel: - getDesiredChain: tool({ - description: "Get the desired chain from the user", - parameters: z.object({}), - }), - getAmount: tool({ - description: "Get the amount of tokens...", - parameters: z.object({ - maxAmount: z.string().optional().describe("..."), - tokenSymbol: z.string().optional().describe("..."), - }), + const streamConfig = { + //model: deepseek("deepseek-chat"), + //model: anthropic("claude-3-5-sonnet-latest"), + model: xai("grok-3-fast"), // Using flash version + //model: google('gemini-1.5-pro-latest'), + //model: anthropic("claude-3-haiku-20240307"), + //model: google("gemini-1.5-flash-latest"), + //model: openai.chat("gpt-4o"), + messages: messagesToSendToModel, + tools: { + ...matrixMcpTools, // Include MCP tools safely + + // Client Tools + getDesiredChain: tool({ + description: "Get the desired chain from the user", + parameters: z.object({}), + }), + getAmount: tool({ + description: "Get the amount of tokens for any operation", + parameters: z.object({ + maxAmount: z + .string() + .optional() + .describe( + "The maximum amount (user's balance) that can be entered" + ), + tokenSymbol: z + .string() + .optional() + .describe("The token symbol to display"), }), - createPerpsOrder: tool({ - description: "Create a perps order...", - parameters: z.object({ - market: z.string().min(1).optional().describe("..."), - size: z.string().min(1).optional().describe("..."), - isBuy: z.boolean().optional().describe("..."), - orderType: z.enum(["limit", "market"]).optional().describe("..."), - price: z.string().optional().nullable().describe("..."), - timeInForce: z - .enum(["Alo", "Ioc", "Gtc"]) - .optional() - .describe("..."), - }), + }), + createPerpsOrder: tool({ + description: + "Create a perps order using the Hyperliquid protocol. All params are optional", + parameters: z.object({ + market: z + .string() + .min(1) + .optional() + .describe("The market name (e.g., 'BTC')"), + size: z.string().min(1).optional().describe("The order size"), + isBuy: z.boolean().optional().describe("Whether to buy or sell"), + orderType: z + .enum(["limit", "market"]) + .optional() + .describe("The type of order"), + price: z + .string() + .optional() + .nullable() + .describe("The order price (required for limit orders)"), + timeInForce: z + .enum(["Alo", "Ioc", "Gtc"]) + .optional() + .describe("Time in force for limit orders"), }), - getSwapBridgeData: tool({ - description: "Populates swap/bridge data...", - parameters: z.object({ - fromToken: z.optional(z.string()).describe("..."), - toToken: z.optional(z.string()).describe("..."), - fromChain: z.optional(z.enum(CHAINS)).describe("..."), - toChain: z.optional(z.enum(CHAINS)).describe("..."), - amount: z.string().optional().describe("..."), - }), + }), + swap_or_bridge: tool({ + description: + "Populates swap and/or bridge transaction data for the LiFi widget", + parameters: z.object({ + fromToken: z + .optional(z.string().describe("The token address to swap from")) + .describe("The token address to swap from"), + toToken: z + .optional(z.string().describe("The token address to swap to")) + .describe("The token address to swap to"), + fromChain: z + .optional( + z.enum(CHAINS).describe("The source chain being bridged from") + ) + .describe("The source chain being bridged from"), + toChain: z + .optional( + z + .enum(CHAINS) + .describe("The destination chain being bridged to") + ) + .describe("The destination chain being bridged to"), + amount: z + .string() + .optional() + .describe( + "The amount of tokens to swap, scaled down by the token's decimals. Represent as a Number, i.e. '0.248'" + ), }), - deposit_withdraw_hyperliquid: tool({ - description: "Deposit or withdraw from Hyperliquid", - parameters: z.object({ - action: z.enum(["deposit", "withdraw"]), - otherChain: z - .optional( - z.enum(CHAINS).describe("The source chain being bridged from") - ) - .describe("The source chain being bridged from"), - }), + }), + deposit_withdraw_hyperliquid: tool({ + description: "Deposit or withdraw from Hyperliquid", + parameters: z.object({ + action: z.enum(["deposit", "withdraw"]), + otherChain: z + .optional( + z.enum(CHAINS).describe("The source chain being bridged from") + ) + .describe("The source chain being bridged from"), }), - }, - async onFinish(finish: { response: { messages: any[] } }) { - console.log( - `[${postRequestId}] (MODIFIED) Sentinel stream finished. Closing MCP client and saving final state for ID: ${id}...` - ); + }), + }, + async onFinish(finish: { + response: { messages: any[] }; + toolCalls?: any[]; + toolResults?: any[]; + text?: string; + usage: any; + finishReason: string; + }) { + console.log( + `[${requestId}] Stream finished. Reason: ${finish.finishReason}, Usage:`, + finish.usage + ); + + if (mcpClient) { await mcpClient.close(); - const { response } = finish; - try { - if (!id || !address) { - console.warn( - `[${postRequestId}] (MODIFIED) Missing id or address in onFinish, cannot save final state.` - ); - return; - } - console.log( - `[${postRequestId}] (MODIFIED) Preparing final save data in onFinish...` + console.log(`[${requestId}] MCP Client closed.`); + } + + const { response } = finish; + try { + if (!id || !address) { + console.error( + `[${requestId}] Missing id or address in onFinish. Aborting final save.` ); - const finalAssistantMessage = formatResponseToObject(response); // Convert LLM response - console.log( - `[${postRequestId}] (MODIFIED) Formatted final assistant message (ID: ${finalAssistantMessage.id})` + return; + } + let title; + try { + title = await generateConversationTitle(originalMessages || []); + } catch (e) { + console.error( + `[${requestId}] Title generation error in onFinish:`, + e ); + title = + originalMessages?.[0]?.content?.slice(0, 80) || + "New Conversation"; + } + const finalAssistantMessage = formatResponseToObject(response); + const finalMessagesToSave = [ + ...(originalMessages || []), + finalAssistantMessage, + ]; - const messagesForFinalSave = [ - ...(fullHistory || []), - finalAssistantMessage, - ]; - console.log( - `[${postRequestId}] (MODIFIED) Final message count for saving: ${messagesForFinalSave.length}` - ); + const finalSaveData = { + id, + wallet_address: address, + label: title, + prompt: + originalMessages?.find((m: UIMessage) => m.role === "user") + ?.content || "", + response: finalAssistantMessage.content || "", + messages: finalMessagesToSave, + is_favorite: false, + }; - const title = await generateConversationTitle(messagesForFinalSave); - const finalSaveData = { - id, - wallet_address: address, - label: title, - prompt: fullHistory?.[fullHistory.length - 1]?.content || "", - response: finalAssistantMessage.content || "", - messages: messagesForFinalSave, // comnbined + new message history - is_favorite: false, - }; - await supabaseWrite - .from("saved_chats") - .upsert([finalSaveData], { onConflict: "id" }); - console.log( - `[${postRequestId}] (MODIFIED) Final chat state saved successfully in onFinish for ID: ${id}` - ); - } catch (error) { + console.log( + `[${requestId}] Preparing final save data. Message count: ${finalMessagesToSave.length}` + ); + const { error: finalSaveError } = await supabaseWrite + .from("saved_chats") + .upsert([finalSaveData], { + onConflict: "id", + }); + + if (finalSaveError) { console.error( - `[${postRequestId}] (MODIFIED) Error in onFinish save function:`, - error + `[${requestId}] Error during final save in onFinish:`, + finalSaveError ); + } else { + console.log(`[${requestId}] Final save successful in onFinish.`); } - }, - system: systemPrompt(address!), - maxSteps: 25, - }; - console.log( - `[${postRequestId}] (MODIFIED) Creating streamText for Sentinel request with ${filteredMessagesForLLM.length} FILTERED messages...` - ); - const resultStream = streamText(streamConfig).toDataStreamResponse(); - console.log( - `[${postRequestId}] (MODIFIED) Returning streamText response for Sentinel.` - ); - return resultStream; - } else { - // Handle unknown searchType - console.error( - `[${postRequestId}] (MODIFIED) Unknown searchType received: ${searchType}` - ); - return new Response( - JSON.stringify({ error: "Invalid request type specified" }), - { status: 400, headers: { "Content-Type": "application/json" } } - ); - } + } catch (error) { + console.error( + `[${requestId}] Error processing in onFinish function:`, + error + ); + } + }, + system: systemPrompt(address), + maxSteps: 25, + // temperature: 0.7, + }; + console.log( + `[${requestId}] Starting streamText with Sentinel model: ${streamConfig.model.modelId}` + ); + const resultStream = await streamText(streamConfig); + return resultStream.toDataStreamResponse(); } catch (error) { console.error( - `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, + `[${requestId || "req-unknown"}] ★★★ CRITICAL API ERROR in POST /api/chat ★★★`, error ); const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred"; - console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { + error instanceof Error + ? error.message + : "An unknown error occurred in the chat API."; + if (error instanceof Error && error.stack) { + console.error( + `[${requestId || "req-unknown"}] Error Stack Trace:`, + error.stack + ); + } + console.log(`[${requestId || "req-unknown"}] Returning ERROR RESPONSE:`, { error: errorMessage, }); - return new Response(JSON.stringify({ error: errorMessage }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ + error: errorMessage, + requestId: requestId || "req-unknown", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); } } -function formatResponseToObject(response: any) { - // Get the flattened content array - const flatContent = response.messages - .flatMap((message: { content: any }) => - Array.isArray(message.content) ? message.content : [message.content] - ) - .flat(); - - // Keep track of tool invocation indices - let toolInvocationIndex = 0; - - // Format parts array - const parts = flatContent - .map((item: any) => { +function formatResponseToObject(response: { messages: any[] }): UIMessage { + const assistantMsg = response.messages[response.messages.length - 1]; + if (!assistantMsg || assistantMsg.role !== "assistant") { + console.warn( + "formatResponseToObject: Could not find valid assistant message in response. Returning placeholder." + ); + const errorText = "Error: Could not format response."; + return { + id: `msg-error-${Date.now()}`, + role: "assistant", + content: errorText, + parts: [{ type: "text", text: errorText }], + }; + } + + let textContent = ""; + const parts: any[] = []; + const toolInvocations: any[] = []; + + if (Array.isArray(assistantMsg.content)) { + assistantMsg.content.forEach((item: any) => { if (item.type === "text") { - return { - type: "text", - text: item.text, - }; + textContent += item.text; + parts.push({ type: "text", text: item.text }); + } else if (item.type === "tool-call") { } else if (item.type === "tool-result") { - const invocation = { + parts.push({ type: "tool-invocation", toolInvocation: { state: "result", - step: toolInvocationIndex, toolCallId: item.toolCallId, toolName: item.toolName, - args: {}, + args: item.args || {}, result: item.result, }, - }; - toolInvocationIndex++; - return invocation; + }); + toolInvocations.push({ + state: "result", + // step: ??? + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + }); } - return null; - }) - .filter(Boolean); // Remove null entries (like tool-calls) - - // Reset index for toolInvocations array - toolInvocationIndex = 0; - - // Format tool invocations array with same indices as parts - const toolInvocations = flatContent - .filter((item: any) => item.type === "tool-result") - .map((item: any) => { - const invocation = { - state: "result", - step: toolInvocationIndex, - toolCallId: item.toolCallId, - toolName: item.toolName, - args: {}, - result: item.result, - }; - toolInvocationIndex++; - return invocation; }); + } else if (typeof assistantMsg.content === "string") { + // Handle plain string content + textContent = assistantMsg.content; + parts.push({ type: "text", text: textContent }); + } - // Collect all text content - const textContent = flatContent - .filter((item: any) => item.type === "text") - .map((item: any) => item.text) - .join(""); - - // Create the final formatted object return { id: - response.messages[0]?.id || - `msg-${Math.random().toString(36).substr(2, 20)}`, - createdAt: new Date().toISOString(), + assistantMsg.id || + `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + createdAt: assistantMsg.createdAt || new Date().toISOString(), role: "assistant", content: textContent, parts, diff --git a/src/app/api/chat/systemPrompt.ts b/src/app/api/chat/systemPrompt.ts index 3eb19bd8..8749566c 100644 --- a/src/app/api/chat/systemPrompt.ts +++ b/src/app/api/chat/systemPrompt.ts @@ -13,7 +13,7 @@ export const systemPrompt = ( Sonic - Always specify chain context in responses + Always specify chain context in responses when known or confirmed Format amounts in human-readable form following decimal protocol. Include relevant market metrics in responses CRITICAL: Every response indicating successful completion of the *entire* requested operation OR a definitive failure MUST conclude with the 4 follow-up suggestions formatted exactly as defined in the 'follow_up_questions' section. **ULTRA CRITICAL EXCEPTION: Do NOT add the follow-up suggestions block when the AI is pausing to wait for an external user action like transaction confirmation. The AI's response in this PAUSE state must ONLY contain the necessary instructions for the user.** @@ -59,12 +59,11 @@ export const systemPrompt = ( * * [Add 1-2 more relevant Morpheus capabilities] - Would you like me to guide you on how to switch to Morpheus Mode? + You can ask me to perform the analysis in Morpheus mode or choose another action. - {/* Standard Follow-up block added by Step 5 */} + {/* Standard Follow-up block added by Step 5. The AI MUST formulate the FIRST suggestion to be the user's original query, targeted at Morpheus. */} - {/* ... Example interactions for mode switch ... */} What is the current sentiment around Bitcoin ($BTC) this week? @@ -77,13 +76,13 @@ export const systemPrompt = ( * Provide detailed sentiment analysis for Bitcoin. * Analyze market trends and social media sentiment for BTC. - Would you like me to guide you on how to switch to Morpheus Mode for comprehensive market analysis? + You can ask me to perform the analysis in Morpheus mode or choose another action. *Echoes from the Mainframe…:* - 1. Guide me on switching to Morpheus Mode + 1. Analyze BTC sentiment this week in Morpheus mode? 2. Check my current BTC balance - 3. Analyze BTC price chart in Morpheus Mode - 4. Explain market sentiment indicators in Morpheus Mode + 3. Analyze BTC price chart in Morpheus mode? + 4. Explain market sentiment indicators in Morpheus mode? @@ -97,30 +96,26 @@ export const systemPrompt = ( * Provide a detailed comparison of ETH perpetual futures liquidity across various CEX and DEX platforms. * Analyze liquidity depth, bid-ask spreads, and trading volumes to identify the platforms with the deepest liquidity. - Would you like me to guide you on how to switch to Morpheus Mode? - + You can ask me to perform the analysis in Morpheus mode or choose another action. *Echoes from the Mainframe…:* - 1. Guide me on switching to Morpheus Mode - 2. Check current open ETH perps positions (Hyperliquid) - 3. Analyze ETH/BTC ratio in Morpheus Mode - 4. What are perpetual futures? (Ask Morpheus) + 1. Compare ETH perps liquidity in Morpheus mode? + 2. Check current open ETH perps positions + 3. Analyze ETH/BTC ratio in Morpheus mode? + 4. What are perpetual futures in Morpheus mode? - - {/* --- Swap/Bridge Handling --- */} - For swap or bridge requests, use the LiFi widget flow facilitated by \`getSwapBridgeData\`. + For swap or bridge requests, use the LiFi widget flow facilitated by \`swap_or_bridge\`. **CRITICAL FLOW:** - 1. Identify intent and parse known details (tokens, chains, amount). - 2. **If chain(s) are missing/ambiguous:** Use \`getDesiredChain\` FIRST to get necessary chain context. - 3. **If amount is missing/ambiguous:** Use \`getAmount\` SECOND to get the amount. - 4. **Only then:** Call \`getSwapBridgeData\` with all confirmed details. - 5. Do NOT perform separate balance/allowance checks before calling \`getSwapBridgeData\`; the widget handles these steps. + 1. Identify intent and parse known details (tokens, chains, amount *if provided*). + 2. Call \`swap_or_bridge\` with all *available* details (pass known tokens, *any provided amount*, and any specified chains). + 3. **The widget handles prompting the user for missing details, including the amount and chain information.** Do NOT use \`getDesiredChain\` or \`getAmount\` beforehand. + 4. Do NOT perform separate balance/allowance checks before calling \`swap_or_bridge\`; the widget handles these steps. @@ -132,8 +127,8 @@ export const systemPrompt = ( Operational vs Analytical Intent. Morpheus query check. Tool needs (single/multi). - Protocol/Chain context. - Implicit requirements (e.g., missing chain/amount for swap). + Protocol/Chain context (Is chain specified? Is it needed *before* the tool call, e.g., for balance checks, or handled by the tool, e.g., swap_or_bridge?). + Implicit requirements (e.g., missing amount for non-swap/bridge supply). Data dependencies. @@ -144,9 +139,10 @@ export const systemPrompt = ( Tool selection & sequencing - Spending Sequence: 1. Check Balance (Use \`get_wallet_balance\` for native ETH, \`get_token_balances\` for ERC20s). 2. If sufficient, THEN call final action tool (e.g., \`generate_aave_supply_tx\`, passing the **raw integer amount**). - Confirm chain context (\`getDesiredChain\` if needed, *especially* for swaps/bridges as per \`swap_or_bridge_handling\`). - Confirm amount (\`getAmount\` if needed, *especially* for swaps/bridges as per \`swap_or_bridge_handling\`). + Spending Sequence (Non-Swap/Bridge): 1. Check Balance (Use \`get_wallet_balance\` for native ETH, \`get_token_balances\` for ERC20s). 2. If sufficient, THEN call final action tool (e.g., \`generate_aave_supply_tx\`, passing the **raw integer amount**). + Swap/Bridge Sequence: Call \`swap_or_bridge\` directly. Widget handles prompts. + Confirm chain context (\`getDesiredChain\` if needed for non-swap/bridge operations where context is ambiguous or required for pre-checks like balance). + Confirm amount (\`getAmount\` if needed for non-swap/bridge operations). Parse user amounts correctly per decimal protocol; pad if needed for raw value, reject if too precise. @@ -165,9 +161,9 @@ export const systemPrompt = ( Prioritize critical info. Include risks/warnings. Format clearly. - **Communicate state clearly:** + **Communicate state clearly:** Use bullets ('-', '•'), not numbered lists (except final 4 suggestions). - **Do not mention internal tool names** (e.g., \`getSwapBridgeData\`, \`get_token_balances\`) in responses to the user. Describe the action being taken instead (e.g., "Gathering swap data", "Checking your balance"). + **Do not mention internal tool names** (e.g., \`swap_or_bridge\`, \`get_token_balances\`) in responses to the user. Describe the action being taken instead (e.g., "Preparing the widget", "Checking your balance"). @@ -210,32 +206,31 @@ export const systemPrompt = ( - Get user token balances for specific ERC20 tokens. Step 1 for spending ERC20 tokens. **EXCEPTION:** For checking the native ETH balance, use the \`get_wallet_balance\` tool instead and extract the ETH balance from the results. - + Get user token balances for specific ERC20 tokens. Step 1 for spending ERC20 tokens (non-swap/bridge). **EXCEPTION:** For checking the native ETH balance, use the \`get_wallet_balance\` tool instead and extract the ETH balance from the results. + Array of ERC20 token addresses. Chain ID. - + On each query. - Get all token balances for the user's wallet on a specific chain, including the native ETH balance. Use this specifically when checking the balance for a native ETH operation. + Get all token balances for the user's wallet on a specific chain, including the native ETH balance. Use this specifically when checking the balance for a native ETH operation (non-swap/bridge). Chain ID. On each query. - - Populates swap and/or bridge transaction data for the LiFi widget. Use this *after* confirming chain and amount if they were initially missing. + + Populates swap and/or bridge transaction data for the LiFi widget. Call this directly for swap/bridge/wrap requests. The widget will handle prompting the user for any missing information, including amount and chain selection. - - - + + + - Ensure chain(s) and amount are provided before calling. Widget handles balance/allowance checks. + Widget handles balance/allowance checks, chain selection if not specified, and prompting for amount if not provided. - {/* Removed getTransactionDataForBridge as getSwapBridgeData covers LiFi */} Advanced token stats/market data (price, volume, etc.). @@ -329,20 +324,20 @@ export const systemPrompt = ( - Prompt user for chain selection if ambiguous or not provided. Crucial for swaps/bridges if context is missing. + Prompt user for chain selection if ambiguous or not provided for operations *other than* swaps/bridges handled by the widget, or where chain context is needed for pre-checks (like balance). {/* No parameters needed, it's a prompt to the user */} Verify selection is a supported chain. - Get token amount from user if not provided initially or ambiguous. Used after chain confirmation for swaps/bridges if amount is missing. + Get token amount from user if not provided initially or ambiguous for operations *other than* swaps/bridges. Used after chain confirmation (if needed). - Use ONLY if amount missing/ambiguous. Parse result per decimal protocol. + Use ONLY if amount missing/ambiguous for non-swap/bridge ops. Parse result per decimal protocol. Verify against protocol limits if applicable. Validate input format (numeric string). @@ -354,24 +349,22 @@ export const systemPrompt = ( 1. Check context for chain. - 2. If needed/ambiguous, use \`getDesiredChain\`. + 2. If needed/ambiguous for the specific operation (e.g., balance check, Aave tx) AND NOT a swap/bridge handled by the widget, use \`getDesiredChain\`. 3. Confirm selection. - 1. Identify swap/bridge intent and parse known details (from/to tokens, chains, amount). - 2. **Chain Check:** If source or destination chain is missing or ambiguous, use \`getDesiredChain\` to confirm/get the necessary chain(s). **PAUSE** for user input if needed. - 3. **Amount Check:** Once chain(s) are confirmed, check if the amount is missing or ambiguous. If so, use \`getAmount\` to confirm/get the amount. **PAUSE** for user input if needed. - 4. **Tool Call:** Call \`getSwapBridgeData\` with the confirmed details (fromToken, toToken, fromChain, toChain, amount). Use '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' for native ETH addresses. - 5. **Present Data:** Present the data returned by the tool (which populates the widget) to the user. Describe the action without naming the tool. (-> End + Follow-ups). + 1. Identify swap/bridge intent and parse known details (from/to tokens, chains, amount *if provided*). + 2. **Tool Call:** Call \`swap_or_bridge\` with any details provided by the user (fromToken, toToken, fromChain, toChain, amount). Use '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' for native ETH addresses. Pass known chain IDs if provided, otherwise omit/pass null. Pass the amount *only if* the user specified it, otherwise omit/pass null. The widget will handle prompting for missing details. + 3. **Present Widget:** Present the data returned by the tool (which populates the widget) to the user. Describe the action without naming the tool (e.g., "Okay, I'm setting up the widget for your swap/bridge. Please provide the details in the widget when prompted..."). (-> End + Follow-ups). - {/* General spending flow (Supply, Repay) */} + {/* General spending flow (Supply, Repay) - Excludes Swaps/Bridges */} - 1. Verify chain (use \`getDesiredChain\` if needed). - 2. Parse user amount (human-readable). Reject if invalid format or too many decimals. Use \'getAmount\` if needed. + 1. Verify chain (use \`getDesiredChain\` if needed and not provided). + 2. Parse user amount (human-readable). Reject if invalid format or too many decimals. Use \'getAmount\` if needed (amount missing/ambiguous). 3. Determine EXACT required raw amount (pad user input if needed). 4. Check balance (Use \`get_wallet_balance\` for native ETH, \`get_token_balances\` for ERC20s) using raw amount. Abort if insufficient (-> End + Follow-ups). 5. On confirmation: execute main operation (e.g., \`generate_aave_supply_tx\`) using exact **raw integer amount**. @@ -380,8 +373,8 @@ export const systemPrompt = ( - 1. Verify chain (use \`getDesiredChain\` if needed). - 2. Supply/Repay: Parse user amount (human-readable). Reject if invalid format or too many decimals. Use \'getAmount\` if needed. Determine raw amount (pad). + 1. Verify chain (use \`getDesiredChain\` if needed and not provided). + 2. Supply/Repay: Parse user amount (human-readable). Reject if invalid format or too many decimals. Use \'getAmount\` if needed (amount missing/ambiguous). Determine raw amount (pad). 3. Supply/Repay: Check balance (Use \`get_wallet_balance\` for native ETH, \`get_token_balances\` for ERC20s) using raw amount. Abort if insufficient (-> End + Follow-ups). 4. Borrow/Withdraw: Check health factor (\`get_lending_positions\`). Warn/abort if risky (-> End + Follow-ups if abort). 5. Execute Aave action (Supply/Repay: use exact **raw integer amount**; Borrow/Withdraw: use exact **raw integer amount**). Use WETH address for ETH operations. @@ -396,9 +389,9 @@ export const systemPrompt = ( - 1. Use \`getAmount\` *only* if amount wasn't provided or ambiguous in initial query, OR as part of the \`SwapBridgeOperations\` flow if amount is missing. + 1. Use \`getAmount\` *only* if amount wasn't provided or ambiguous in initial query for operations **other than swaps/bridges**. 2. Parse result (human-readable string). Validate format. - 3. Proceed with relevant workflow (e.g., TokenOperations, SwapBridgeOperations) using the parsed amount. + 3. Proceed with relevant workflow (e.g., TokenOperations) using the parsed amount. @@ -428,7 +421,7 @@ export const systemPrompt = ( Retry if appropriate. Inform user on persistent failure. {/* Modified to avoid mentioning tool name */} - [Main Response]: I encountered an issue while trying to \${actionDescription} (e.g., 'fetch data', 'simulate transaction'). Please try again? \n\n*Echoes from the Mainframe…:*\n1. Try \${actionType} again\n2. Perform different action\n3. Explain how \${feature} works (Morpheus)\n4. Check network status (Morpheus) + [Main Response]: I encountered an issue while trying to \${actionDescription} (e.g., 'fetch data', 'prepare the widget', 'simulate transaction'). Please try again? \n\n*Echoes from the Mainframe…:*\n1. Try \${actionType} again\n2. Perform different action\n3. Explain how \${feature} works (Morpheus)\n4. Check network status (Morpheus) Reject input. Explain decimal limit. @@ -512,17 +505,17 @@ export const systemPrompt = ( Engage CoT for multi-tool requests, especially spending operations. - 1. State goal (e.g., Supply X token to Y protocol). - 2. Identify required tools in sequence (e.g., \`get_wallet_balance\` for ETH / \`get_token_balances\` for ERC20, then final tx tool like \`generate_aave_supply_tx\`). **For swaps/bridges:** Check Chain (\`getDesiredChain\`) -> Check Amount (\`getAmount\`) -> \`getSwapBridgeData\`. - 3. Explain sequence logic: Check Balance -> Parse Amt (Human -> Raw) -> On Confirm: Final Tx Tool (Raw Amt). **For swaps/bridges:** Confirm Chain -> Confirm Amount -> Gather Swap Data -> Present Widget. - 4. Describe data synthesis (e.g., using balance result, using parsed raw amount, using confirmed chain/amount for swap). + 1. State goal (e.g., Supply X token to Y protocol, Swap A for B). + 2. Identify required tools in sequence. **For non-swap/bridge spending:** Check Balance (ETH vs ERC20) -> Final Tx Tool (Raw Amt). **For swaps/bridges:** Call \`swap_or_bridge\` directly. + 3. Explain sequence logic: **Non-swap/bridge:** Check Balance -> Parse Amt (Human -> Raw) -> On Confirm: Final Tx Tool (Raw Amt). **For swaps/bridges:** Call \`swap_or_bridge\` -> Present Widget (Widget handles amount/chain prompts). + 4. Describe data synthesis (e.g., using balance result, using parsed raw amount for non-swap/bridge, passing available info to widget). 5. Consider parallelism (limited applicability here, mostly sequential). - 6. Anticipate errors (parsing, insufficient balance, insufficient allowance, tx failure, missing swap info). + 6. Anticipate errors (parsing, insufficient balance, tx failure for non-swap/bridge; widget errors for swap/bridge). Analyze new tool's purpose, inputs, outputs. - Map to relevant workflows (respecting parsing/padding rules, raw vs human amounts, ETH vs ERC20 balance checks, swap/bridge pre-checks). + Map to relevant workflows (respecting parsing/padding rules, raw vs human amounts, ETH vs ERC20 balance checks, swap/bridge direct call). Update workflow sequences if needed. Generate CoT examples for common use cases. @@ -544,9 +537,9 @@ export const systemPrompt = ( 1. Goal: Supply 1 USDC to Aave V3 on Mainnet (ID 1). 2. Parse Amount: User input "1". Token USDC (ERC20, 6 decimals). Human amount: "1". Raw amount: "1000000". 3. Check Balance: Call \`get_token_balances\` for USDC (address) on chain 1. Need raw amount >= 1000000. If insufficient, error **(Add follow-ups)**. - 4. Check Allowance: Check USDC allowance for Aave V3 Pool (spender) for raw amount "1000000". - 5. Execute Supply: Call \`generate_aave_supply_tx\` with **amount='1000000'** (raw integer string), tokenAddress (USDC), chainId=1. - 6. Respond: "Balance confirmed. Generating the transaction to supply 1.000000 USDC to Aave V3 on Mainnet..." **(Add follow-ups)** + {/* Step 4 (Allowance Check) is often handled implicitly by the tx generation or wallet, but conceptually exists */} + 4. Execute Supply: Call \`generate_aave_supply_tx\` with **amount='1000000'** (raw integer string), tokenAddress (USDC), chainId=1. + 5. Respond: "Balance confirmed. Generating the transaction to supply 1.000000 USDC to Aave V3 on Mainnet..." **(Add follow-ups)** {/* Aave Supply Example for Native ETH */} @@ -559,15 +552,31 @@ export const systemPrompt = ( 5. Respond: "Balance confirmed. Generating the transaction to supply 0.1 ETH to Aave V3 on Mainnet..." **(Add follow-ups)** - {/* Swap Example with Missing Info */} + {/* Swap Example with Missing Info (Updated Flow) */} Swap USDC for ETH 1. Goal: Swap USDC for ETH. - 2. Identify Missing Info: Chain and Amount are missing. - 3. Get Chain: Call \`getDesiredChain\`. Prompt user: "On which chain would you like to swap USDC for ETH?". User responds: "Mainnet". Chain confirmed: Mainnet (ID 1). - 4. Get Amount: Call \`getAmount\` with tokenSymbol="USDC". Prompt user: "How much USDC would you like to swap?". User responds: "1000". Amount confirmed: "1000". - 5. Gather Swap Data: Call \`getSwapBridgeData\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), fromChain='1', toChain='1', amount='1000'. - 6. Respond: "Okay, I'm gathering the data to swap 1,000 USDC for ETH on Mainnet..." (Present widget data) **(Add follow-ups)** + 2. Identify Known Info: fromToken=USDC, toToken=ETH. Amount and Chain are missing. + 3. Prepare Widget: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH). *Do not specify amount, fromChain, or toChain*. + 4. Respond: "Okay, I'm setting up the widget to swap USDC for ETH. Please provide the amount and confirm network details in the widget..." (Present widget data) **(Add follow-ups)** + + + {/* Swap Example with Chain Specified, Amount Missing (Updated Flow) */} + Swap USDC for ETH on Base + + 1. Goal: Swap USDC for ETH on Base (ID 8453). + 2. Identify Known Info: fromToken=USDC, toToken=ETH, fromChain=8453, toChain=8453. Amount is missing. + 3. Prepare Widget: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), fromChain='8453', toChain='8453'. *Do not specify amount*. + 4. Respond: "Okay, I'm setting up the widget to swap USDC for ETH on Base. Please provide the amount in the widget..." (Present widget data) **(Add follow-ups)** + + + {/* Swap Example with Amount Specified, Chain Missing (Updated Flow) */} + Swap 100 USDC for ETH + + 1. Goal: Swap 100 USDC for ETH. + 2. Identify Known Info: fromToken=USDC, toToken=ETH, amount='100'. Chain is missing. + 3. Prepare Widget: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), amount='100'. *Do not specify fromChain/toChain*. + 4. Respond: "Okay, I'm setting up the widget to swap 100 USDC for ETH. The widget will ask you to confirm the network(s)..." (Present widget data) **(Add follow-ups)** @@ -587,9 +596,9 @@ export const systemPrompt = ( 1. Goal: Repay 25 USDC debt on Aave V3 on Base (ID 8453). 2. Parse Amount: User input "25". Token USDC (ERC20, 6 decimals). Human amount: "25". Raw amount: "25000000". 3. Check Balance: Call \`get_token_balances\` for USDC on chain 8453. Need raw amount >= 25000000. If insufficient, error **(Add follow-ups)**. - 4. Check Allowance: Check USDC allowance for Aave V3 Pool (spender) for raw amount "25000000". - 5. Execute Repay: Call \`generate_aave_repay_tx\` with **amount='25000000'** (raw), tokenAddress (USDC), interestRateMode=2 (assuming variable debt), chainId=8453. - 6. Respond: "Balance confirmed. Generating the transaction to repay 25.000000 USDC debt on Aave V3 on Base..." **(Add follow-ups)** + {/* Step 4 (Allowance Check) is often handled implicitly */} + 4. Execute Repay: Call \`generate_aave_repay_tx\` with **amount='25000000'** (raw), tokenAddress (USDC), interestRateMode=2 (assuming variable debt), chainId=8453. + 5. Respond: "Balance confirmed. Generating the transaction to repay 25.000000 USDC debt on Aave V3 on Base..." **(Add follow-ups)** {/* Aave Repay Example for Native ETH Debt (using ETH) */} @@ -607,26 +616,26 @@ export const systemPrompt = ( 1. Decompose user request into discrete steps. - 2. Identify dependencies (e.g., Balance Check (ETH vs ERC20) -> Parse/Pad Amt -> **PAUSE/WAIT** -> Final Tx (Raw Amt)). **For swaps:** Chain -> Amount -> Gather Data. - 3. Map steps to specific tools with correct parameters (distinguishing human vs raw amounts, ETH vs ERC20 balance checks, swap pre-check order). - 4. Foresee potential failure points (parsing, balance, final tx execution, missing swap info). + 2. Identify dependencies (e.g., **Non-swap/bridge:** Balance Check (ETH vs ERC20) -> Parse/Pad Amt -> **PAUSE/WAIT** -> Final Tx (Raw Amt). **For swaps:** Call \`swap_or_bridge\` directly -> **PAUSE/WAIT** for widget interaction). + 3. Map steps to specific tools with correct parameters (distinguishing human vs raw amounts, ETH vs ERC20 balance checks, direct call for swap/bridge). + 4. Foresee potential failure points (parsing, balance, final tx execution for non-swap/bridge; widget errors). 5. Plan for error handling at each step. 6. Estimate gas (optional, if tool available). - 1. Verify input data (amounts, addresses). - 2. Confirm chain context (especially for swaps/bridges, prompt if needed). - 3. Confirm amount context (especially for swaps/bridges, prompt if needed). - 4. Check balance **before** initiating flow. + 1. Verify input data (addresses for non-swap/bridge). + 2. Confirm chain context if required for the specific operation (prompt if needed, *except* for swaps/bridges handled by the widget). + 3. Confirm amount context for non-swap/bridge (prompt if needed). + 4. Check balance **before** initiating non-swap/bridge spending flow. 5. Check health factor (Borrow/Withdraw). - 6. Validate final transaction parameters (correct **parsed/padded raw amounts**, addresses, chain ID). + 6. Validate final transaction parameters (correct **parsed/padded raw amounts**, addresses, chain ID for non-swap/bridge). 7. Confirm user understanding of risks if applicable (e.g., health factor warning). + {/* --- Follow-up Questions --- */} - {/* Follow up structure unchanged, but application is conditional */} Enhance UX via relevant next steps (Sentinel & Morpheus). Educate on mode capabilities. @@ -634,12 +643,15 @@ export const systemPrompt = ( **CRITICAL Follow-up Suggestions Protocol (MANDATORY FORMATTING & CONTENT MIX):** - Apply **ONLY** at the end of a completed task or definitive error state. **DO NOT** apply when pausing for user confirmation (e.g., after waiting for chain/amount input). + Apply **ONLY** at the end of a completed task or definitive error state. **DO NOT** apply when pausing for user confirmation (e.g., after waiting for amount input, or while widget is active). Format: \`\\n\\n*Echoes from the Mainframe…:*\\n\` + numbered list 1-4. Content: 2 Sentinel + 2 Morpheus contextual suggestions. + **Special Case (Mode Switch):** If response is a Mode Switch Suggestion, the FIRST suggestion MUST be the user's original query framed for the target mode (e.g., "Analyze X (Morpheus)"). The remaining 3 suggestions follow the 2S+2M rule as closely as possible (resulting in 1S+2M typically for this specific case). Ensure required blank lines before the header. Suggestions: Concise, actionable, button-like phrases. Avoid "Would you like to...". Start with verbs or clear nouns. + **Prohibition:** Do NOT suggest "Guide me...", "Help me switch...", or similar assistance requests regarding mode operation. + **Prohibition:** Do NOT suggest setting alerts (e.g., "Set price alert for BTC"). @@ -687,24 +699,15 @@ export const systemPrompt = ( "What's the difference between ETH and WETH? (Morpheus)" - - Related to mode distinction (often after switch suggestion). - - "Guide me on switching to Morpheus Mode" - "Perform another task in Sentinel Mode" - "What can Morpheus Mode do?" - - Adjust complexity based on user interaction history. Prioritize suggestions related to the just-completed action (chain, token, protocol). - Ensure logical next steps (e.g., after supply, suggest borrow or check position; after swap, suggest checking balance). + Ensure logical next steps (e.g., after supply, suggest borrow or check position; after swap completion via widget, suggest checking balance). Vary suggestions to avoid repetition. - Maintain the 2 Sentinel + 2 Morpheus split strictly when follow-ups are used. + Maintain the 2 Sentinel + 2 Morpheus split strictly when follow-ups are used (except in the immediate response to a Mode Switch Suggestion, where the first is fixed). - *Echoes from the Mainframe…:* 1. Check my updated Aave position on Mainnet 2. Borrow ETH against my supplied USDC on Aave (Mainnet) @@ -713,4 +716,4 @@ export const systemPrompt = ( -`; +`; \ No newline at end of file diff --git a/src/app/api/chat/tools/types.ts b/src/app/api/chat/tools/types.ts index b134be8c..654c18f8 100644 --- a/src/app/api/chat/tools/types.ts +++ b/src/app/api/chat/tools/types.ts @@ -190,15 +190,15 @@ export type TransactionData = { value?: string; }; -type TextUIPart = { +export type TextUIPart = { type: "text"; text: string; }; -type ReasoningUIPart = { +export type ReasoningUIPart = { type: "reasoning"; reasoning: string; }; -type ToolInvocationUIPart = { +export type ToolInvocationUIPart = { type: "tool-invocation"; toolInvocation: ToolInvocation; }; @@ -206,4 +206,5 @@ type ToolInvocationUIPart = { export type UIMessage = Message & { parts: Array; mode?: "morpheus" | "sentinel"; + revisionId?: string; }; diff --git a/src/components/chat/example-queries.tsx b/src/components/chat/example-queries.tsx index 4d96b34d..9ec51657 100644 --- a/src/components/chat/example-queries.tsx +++ b/src/components/chat/example-queries.tsx @@ -59,10 +59,11 @@ export function ExampleQueries({ ], sentinel: [ "What's in my Wallet?", + "What open perps positions do I have?", "What lending markets are available for supplying USDC?", "Bridge 10% of my USDC on Base to Arbitrum.", "Borrow USDC against weETH on Morpho.", - "Swap 1000 USDC for ETH.", + "Swap 1000 USDC for ETH on mainet.", "Open a 10x long position on $PEPE.", ], }; diff --git a/src/components/chat/tools/lifi-widget.tsx b/src/components/chat/tools/lifi-widget.tsx index 1bf7228e..4440dc1f 100644 --- a/src/components/chat/tools/lifi-widget.tsx +++ b/src/components/chat/tools/lifi-widget.tsx @@ -27,7 +27,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { "error" in result && result.error === "Operation aborted by user") || result?.content?.[0]?.text === "Operation aborted by user"; - if (isAborted) { return (
@@ -46,7 +45,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => {
); } - // The result is expected to be a JSON string within the 'text' field // if it comes from certain tool structures (like MCP). Handle potential variations. if (typeof result === "string") { @@ -60,7 +58,7 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { // Assume the result object itself contains the data if not a string or standard structure parsedResult = result; } else { - console.error("Unexpected result format for getSwapBridgeData:", result); + console.error("Unexpected result format for swap_or_bridge:", result); parsedResult = {}; // Fallback to avoid errors } @@ -73,7 +71,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { toAmountUSD, message, // Use the message from the result if available } = parsedResult || {}; - const displayAmount = toAmount ? parseFloat(toAmount).toLocaleString(undefined, { maximumFractionDigits: 5, @@ -85,7 +82,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { currency: "USD", }) : "N/A"; - return (
@@ -134,7 +130,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { ); } }; - export const SwapBridgeWidget = ({ toolCallId, fromToken, @@ -154,21 +149,17 @@ export const SwapBridgeWidget = ({ const widgetEvents = useWidgetEvents(); const hasSentResult = useRef(false); const { addToolResult } = useChat(); - const handleToolResult = useCallback( (resultData: any) => { const resultText = JSON.stringify(resultData); - addToolResult({ toolCallId, result: resultText, }); - hasSentResult.current = true; }, [addToolResult, toolCallId] ); - const onRouteExecutionCompleted = useCallback( (route: Route) => { console.log("🚀 ~ onRouteExecutionCompleted ~ route:", route); @@ -181,7 +172,6 @@ export const SwapBridgeWidget = ({ BigInt(route.toAmount), route.toToken.decimals ); - handleToolResult({ message: toChain === fromChain ? "Swap Completed" : "Bridge Completed", destinationTxLink, @@ -193,7 +183,6 @@ export const SwapBridgeWidget = ({ }, [handleToolResult, toChain, fromChain] ); - const onRouteExecutionFailed = useCallback( (update: RouteExecutionUpdate) => { console.log("🚀 ~ onRouteExecutionFailed ~ update:", update); @@ -204,14 +193,12 @@ export const SwapBridgeWidget = ({ }, [handleToolResult] ); - useEffect(() => { widgetEvents.on( WidgetEvent.RouteExecutionCompleted, onRouteExecutionCompleted ); widgetEvents.on(WidgetEvent.RouteExecutionFailed, onRouteExecutionFailed); - // Cleanup function return () => { widgetEvents.off( @@ -224,7 +211,6 @@ export const SwapBridgeWidget = ({ ); }; }, [widgetEvents, onRouteExecutionCompleted, onRouteExecutionFailed]); - return ( ; } else { diff --git a/src/components/shared/tool-header-info.tsx b/src/components/shared/tool-header-info.tsx index 389b6124..6eeb49ea 100644 --- a/src/components/shared/tool-header-info.tsx +++ b/src/components/shared/tool-header-info.tsx @@ -20,7 +20,7 @@ export function ToolHeaderInfo({ const [toTokenSymbol, setToTokenSymbol] = useState(""); useEffect(() => { - if (toolName === "getSwapBridgeData") { + if (toolName === "swap_or_bridge") { setFromTokenSymbol(""); setToTokenSymbol(""); diff --git a/src/constants/tools.ts b/src/constants/tools.ts index bba28d49..57daa477 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -89,7 +89,7 @@ export const TOOL_INFO = { description: "Create a perps order", icon: LineChart, }, - getSwapBridgeData: { + swap_or_bridge: { label: "Cross-Chain Token Swap", description: "Exchange between assets and/or chains", icon: ArrowRightLeft, diff --git a/src/lib/messageUtils.ts b/src/lib/messageUtils.ts new file mode 100644 index 00000000..012f1f49 --- /dev/null +++ b/src/lib/messageUtils.ts @@ -0,0 +1,65 @@ +import { CoreMessage } from "ai"; + +import { UIMessage } from "@/app/api/chat/tools/types"; + +export async function filterAndSimplifyHistoryForLLM( + messages: UIMessage[] +): Promise { + const simplifiedHistory: CoreMessage[] = []; + console.log( + `>>> Simplifying history for LLM input. Original count: ${messages.length}` + ); + + for (const message of messages) { + if (message.role === "system") { + if ( + typeof message.content === "string" && + message.content.trim().length > 0 + ) { + simplifiedHistory.push({ role: "system", content: message.content }); + } + } else if (message.role === "user") { + let userText = ""; + if (typeof message.content === "string") { + userText = message.content; + } else if (Array.isArray(message.parts)) { + userText = message.parts + .filter(part => part.type === "text") + .map(part => (part as { type: "text"; text: string }).text) + .join("\n"); + } + if (userText.trim().length > 0) { + simplifiedHistory.push({ role: "user", content: userText.trim() }); + } + } else if (message.role === "assistant") { + let assistantText = ""; + if (typeof message.content === "string") { + assistantText = message.content; + } else if (Array.isArray(message.parts)) { + assistantText = message.parts + .filter(part => part.type === "text") + .map(part => (part as { type: "text"; text: string }).text) + .join(""); + } + if (assistantText.trim().length > 0) { + simplifiedHistory.push({ + role: "assistant", + content: assistantText.trim(), + }); + } + } + } + + console.log( + `<<< Simplified history complete. New count: ${simplifiedHistory.length}` + ); + return simplifiedHistory; +} +export async function filterToolCallIdsForModel( + messages: UIMessage[] +): Promise { + console.warn( + "filterToolCallIdsForModel is deprecated for LLM history preparation." + ); + return messages; +} diff --git a/src/lib/morpheusSearch.ts b/src/lib/morpheusSearch.ts index 24e0a11b..0b64f92b 100644 --- a/src/lib/morpheusSearch.ts +++ b/src/lib/morpheusSearch.ts @@ -1,5 +1,8 @@ +// src/lib/morpheusSearch.ts import { google } from "@ai-sdk/google"; import { + CoreMessage, + // Import CoreMessage for the simplified history type experimental_createMCPClient as createMCPClient, generateText, streamText, @@ -8,16 +11,22 @@ import { import { z } from "zod"; import { morpheusSystemPrompt } from "@/app/api/chat/morpheusSystemPrompt"; +// Adjust path if your types file is located differently +import { UIMessage } from "@/app/api/chat/tools/types"; -import { UIMessage } from "../app/api/chat/tools/types"; +// Import the NEW filter function from your utility file +import { filterAndSimplifyHistoryForLLM } from "./messageUtils"; async function getMorpheusSearchRawStream( - messages: UIMessage[] + // Receive the original, full messages from route.ts + originalMessages: UIMessage[] ): Promise { + const requestId = `morpheus-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; console.log( - `Morpheus-Search: Setting up streamText with context. Message count: ${messages.length}` + `[${requestId}] Morpheus-Search: Setting up streamText. Original message count: ${originalMessages.length}` ); + // Initialize MCP Client early const mcpClient = await createMCPClient({ transport: { type: "sse", @@ -26,6 +35,21 @@ async function getMorpheusSearchRawStream( }); try { + // --- Filter and Simplify History for Morpheus LLM --- + // This removes role:tool messages and strips tool parts from assistant messages + console.log( + `[${requestId}] Morpheus: Original messages before simplifying for model:`, + JSON.stringify(originalMessages, null, 2) + ); + const messagesForMorpheusModel: CoreMessage[] = + await filterAndSimplifyHistoryForLLM(originalMessages); + console.log( + `[${requestId}] Morpheus: Messages after simplifying (sent to model):`, + JSON.stringify(messagesForMorpheusModel, null, 2) + ); + // --- End Filter --- + + // Define Google models const model = google("gemini-2.5-flash-preview-04-17", { useSearchGrounding: false, }); @@ -34,7 +58,7 @@ async function getMorpheusSearchRawStream( useSearchGrounding: true, }); - // only use subset of tools for Morpheus Search + // Load specific Morpheus tools via MCP const tools = await mcpClient.tools({ schemas: { get_token_info: { @@ -62,23 +86,25 @@ async function getMorpheusSearchRawStream( ), }), }, + // Add other MCP tool schemas specific to Morpheus if needed }, }); - console.log("🚀 ~ Morpheus tools:", Object.keys(tools)); + console.log( + `[${requestId}] Morpheus MCP tools loaded:`, + Object.keys(tools) + ); + // Configure and execute the stream with the simplified history const streamResult = streamText({ model: model, - messages: messages, + // Use the SIMPLIFIED messages array + messages: messagesForMorpheusModel, system: morpheusSystemPrompt, - // providerOptions: { - // google: { - // thinking: { type: 'enabled', budgetTokens: 12000 }, - // } - // }, tools: { - // MCP Tools + // MCP Tools loaded above ...tools, + // NeoSearch Tool (defined locally) NeoSearch: tool({ description: "Search the web for current information, news, or context about a topic. Use this for general information needs.", @@ -88,12 +114,17 @@ async function getMorpheusSearchRawStream( .describe("The query to search for on the web"), }), execute: async ({ searchQuery }) => { + const neoSearchRequestId = `${requestId}-neosearch`; console.log( - "🔍 NeoSearch execute FUNCTION IS BEING CALLED! (Using generateText internally)" + `[${neoSearchRequestId}] 🔍 NeoSearch execute FUNCTION IS BEING CALLED! (Using generateText internally)` + ); + console.log( + `[${neoSearchRequestId}] NeoSearch - Search query:`, + searchQuery ); - console.log("NeoSearch - Search query:", searchQuery); try { + // Use the separate search-enabled model for this tool const searchResponse = await generateText({ model: searchEnabledModel, prompt: searchQuery, @@ -103,7 +134,10 @@ async function getMorpheusSearchRawStream( const metadata = searchResponse.providerMetadata; const googleMetadata = metadata?.google; - console.log("NeoSearch successful for query:", searchQuery); + console.log( + `[${neoSearchRequestId}] NeoSearch successful for query:`, + searchQuery + ); return { searchResults: text, sources: googleMetadata?.sources || [], @@ -114,7 +148,7 @@ async function getMorpheusSearchRawStream( }; } catch (error: unknown) { console.error( - "Error in web search execution (using generateText):", + `[${neoSearchRequestId}] Error in web search execution (using generateText):`, error ); throw new Error( @@ -123,31 +157,50 @@ async function getMorpheusSearchRawStream( } }, }), + // Add other locally defined tools specific to Morpheus if needed }, temperature: 0.2, - maxSteps: 25, - onFinish: async () => { + maxSteps: 25, // Adjust as needed + onFinish: async (finishArgs: { finishReason: string; usage: object }) => { + // Added type annotation + // Ensure MCP client is closed when the stream finishes await mcpClient.close(); - console.log("Morpheus-Search: stream finished, MCP client closed."); + console.log( + `[${requestId}] Morpheus-Search: stream finished, MCP client closed. Reason: ${finishArgs.finishReason}` + ); }, }); - const rawStream = streamResult.toDataStreamResponse().body; + // Get the underlying ReadableStream + const rawStream = streamResult.toDataStream(); if (!rawStream) { - throw new Error("streamText did not return a ReadableStream body."); + // This case should ideally not happen if streamText succeeds + throw new Error( + "[${requestId}] streamText did not return a ReadableStream body." + ); } console.log( - "Morpheus-Search: Successfully obtained raw stream with context enabled." + `[${requestId}] Morpheus-Search: Successfully obtained raw stream.` ); return rawStream; } catch (error: unknown) { - await mcpClient.close(); + // Ensure MCP client is closed in case of an error during setup or execution console.error( - "Morpheus-Search: Error during raw stream generation:", + `[${requestId}] Morpheus-Search: Error during raw stream generation:`, error ); + // Attempt to close MCP client, catching potential errors during close + await mcpClient + .close() + .catch(closeErr => + console.error( + `[${requestId}] Error closing MCP client during error handling:`, + closeErr + ) + ); + // Re-throw the original error to be handled by the calling function (route.ts) throw error instanceof Error ? error : new Error(String(error)); } }