From dd399b1d1fafbd9548130142a82ec541dcd6ed8d Mon Sep 17 00:00:00 2001 From: Arun Date: Thu, 24 Apr 2025 13:25:25 +0530 Subject: [PATCH 01/11] alerts,mode switch suggestion ,guide me fix --- src/app/api/chat/morpheusSystemPrompt.ts | 52 ++++++++++++------------ src/app/api/chat/systemPrompt.ts | 44 ++++++++------------ 2 files changed, 43 insertions(+), 53 deletions(-) diff --git a/src/app/api/chat/morpheusSystemPrompt.ts b/src/app/api/chat/morpheusSystemPrompt.ts index 1308ee09..a671d464 100644 --- a/src/app/api/chat/morpheusSystemPrompt.ts +++ b/src/app/api/chat/morpheusSystemPrompt.ts @@ -28,17 +28,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). @@ -120,9 +121,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): @@ -168,7 +169,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.) @@ -342,18 +343,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): @@ -369,16 +368,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): @@ -395,16 +394,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) @@ -606,5 +605,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. -`; - +`; \ No newline at end of file diff --git a/src/app/api/chat/systemPrompt.ts b/src/app/api/chat/systemPrompt.ts index b6dd0b40..e9addc3c 100644 --- a/src/app/api/chat/systemPrompt.ts +++ b/src/app/api/chat/systemPrompt.ts @@ -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,14 +96,13 @@ 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? @@ -343,7 +341,7 @@ export const systemPrompt = ( - + {/* MODIFIED: Added specific prohibitions */} Use ONLY if amount missing/ambiguous. Parse result per decimal protocol. Verify against protocol limits if applicable. Validate input format (numeric string). @@ -627,8 +625,8 @@ export const systemPrompt = ( + {/* --- Follow-up Questions --- */} - {/* Follow up structure unchanged, but application is conditional */} Enhance UX via relevant next steps (Sentinel & Morpheus). Educate on mode capabilities. @@ -639,9 +637,12 @@ export const systemPrompt = ( 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). 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"). @@ -689,24 +690,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). 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) @@ -715,4 +707,4 @@ export const systemPrompt = ( -`; +`; \ No newline at end of file From 56d9f0e060493e0f398438372930dc23c258dedd Mon Sep 17 00:00:00 2001 From: Arun Date: Mon, 28 Apr 2025 20:54:20 +0530 Subject: [PATCH 02/11] tool-fix --- src/app/api/chat/route.ts | 1304 ++++++++++++------------ src/components/shared/MatrixCanvas.tsx | 2 +- 2 files changed, 662 insertions(+), 644 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 921f6e3d..5d21d27d 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,37 +1,44 @@ 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"; -import { xai } from "@ai-sdk/xai"; -//import { openai } from "@ai-sdk/openai"; -import { EVM, createConfig } from "@lifi/sdk"; -import { SupabaseClient, createClient } from "@supabase/supabase-js"; -import { - experimental_createMCPClient as createMCPClient, - generateText, - streamText, - tool, -} from "ai"; -import { createWalletClient, http } from "viem"; -import type { Chain as vChain } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { base, mode } from "viem/chains"; -import { z } from "zod"; - -import { CHAINS } from "@/lib/chains"; -import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; -import { incrementMessageUsage } from "@/lib/userManager"; - -import { systemPrompt } from "./systemPrompt"; -import { UIMessage } from "./tools/types"; - -const supabaseWrite: SupabaseClient = createClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_KEY! -); - +import { anthropic } from "@ai-sdk/anthropic"; + //import { deepseek } from "@ai-sdk/deepseek"; + import { google } from "@ai-sdk/google"; + import { xai } from "@ai-sdk/xai"; + //import { openai } from "@ai-sdk/openai"; + import { EVM, createConfig } from "@lifi/sdk"; + import { SupabaseClient, createClient } from "@supabase/supabase-js"; + import { + experimental_createMCPClient as createMCPClient, + generateText, + streamText, + tool, + } from "ai"; + import { createWalletClient, http } from "viem"; + import type { Chain as vChain } from "viem"; + import { privateKeyToAccount } from "viem/accounts"; + import { base, mode } from "viem/chains"; + import { z } from "zod"; + + + + import { CHAINS } from "@/lib/chains"; + import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; + import { incrementMessageUsage } from "@/lib/userManager"; + + + + import { systemPrompt } from "./systemPrompt"; + import { UIMessage } from "./tools/types"; + + + + + + const supabaseWrite: SupabaseClient = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_KEY! + ); + +// Function to determine if a part is a tool data part function isRawToolDataPart(part: any): boolean { if (!part || typeof part !== "object") return false; return ( @@ -41,8 +48,9 @@ function isRawToolDataPart(part: any): boolean { ); } +// Filter history for LLM to improve performance and reduce noise const filterHistoryForLLM = (history: UIMessage[]): UIMessage[] => { - console.log(">>> Filtering history for LLM input..."); ///Log: Start filtering + console.log(">>> Filtering history for LLM input..."); // Log: Start filtering const filtered = history .map(message => { if (message.role === "user") { @@ -64,7 +72,7 @@ const filterHistoryForLLM = (history: UIMessage[]): UIMessage[] => { ` - Original content empty, attempting reconstruction from parts...` ); - // Reconstruction only if original content is missing/empty + // Reconstruction only if original content is missing/empty finalContent = message.parts .filter(part => { const isToolData = isRawToolDataPart(part); @@ -75,7 +83,7 @@ const filterHistoryForLLM = (history: UIMessage[]): UIMessage[] => { part.type === "text"; const isStringPart = typeof part === "string"; console.log( - ` - Part Analysis: IsString=\{isStringPart\}, IsTextPart\={isTextPart}, IsToolData=${isToolData} | Kept: ${!isToolData && (isStringPart || isTextPart)}` + ` - Part Analysis: IsString=${isStringPart}, IsTextPart=${isTextPart}, IsToolData=${isToolData} | Kept: ${!isToolData && (isStringPart || isTextPart)}` ); return !isToolData && (isStringPart || isTextPart); }) @@ -109,7 +117,7 @@ const filterHistoryForLLM = (history: UIMessage[]): UIMessage[] => { ); } - // Skip if empty + // Skip if empty if (!finalContent) { console.log( ` - Filtering out assistant message (ID: ${message.id}) as it became empty.` @@ -142,609 +150,619 @@ const filterHistoryForLLM = (history: UIMessage[]): UIMessage[] => { ); // Log: End filtering return filtered; }; - -export async function PUT(req: Request) { - const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - - try { - console.log(`[${requestId}] PUT /api/chat - Chat save request`); - - // Parse the request body - const body = await req.json(); - - // Get needed fields, with fallbacks for multiple naming patterns - const id = body.id; - const walletAddress = body.wallet_address || body.address; - const messages = body.messages; - - console.log( - `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` - ); - - // Validate required fields - if (!id) { - return NextResponse.json( - { - error: "Missing required field: id", - }, - { status: 400 } - ); - } - - if (!walletAddress) { - return NextResponse.json( - { - error: "Missing required field: wallet_address", - }, - { status: 400 } - ); - } - - if (!messages || !Array.isArray(messages) || messages.length === 0) { - console.warn(`[${requestId}] Empty or invalid messages array`); - } - - // Generate a title - let title = "New Conversation"; - try { - title = await generateConversationTitle(messages || []); - } catch (titleError) { - console.log("titleError", titleError); - // Fall back to first message or default - title = - messages && messages.length > 0 - ? messages[0]?.content?.slice(0, 80) || "New Conversation" - : "New Conversation"; - } - - // Extract message content - const assistantMessage = messages?.find( - (m: UIMessage) => m.role === "assistant" - ); - const userMessage = messages?.find((m: UIMessage) => m.role === "user"); - - // 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, - }; - - console.log(`[${requestId}] Save data prepared:`, { - id: saveData.id, - wallet_address: saveData.wallet_address, - title_length: saveData.label.length, - message_count: saveData.messages.length, - }); - - // Save to database (simple upsert) - try { - console.log(`[${requestId}] Executing upsert to saved_chats table...`); - const { error } = await supabaseWrite - .from("saved_chats") - .upsert([saveData], { - onConflict: "id", - }); - - if (error) { - console.error(`[${requestId}] Save error:`, error); - return NextResponse.json( - { - error: `Failed to save chat: ${error.message}`, - details: error, - requestId, - }, - { status: 500 } - ); - } - - console.log(`[${requestId}] Chat saved successfully`); - return NextResponse.json({ - success: true, - message: "Chat saved successfully", - requestId, - }); - } catch (error) { - console.error(`[${requestId}] Database error:`, error); - return NextResponse.json( - { - error: `Database error: ${error instanceof Error ? error.message : String(error)}`, - requestId, - }, - { status: 500 } - ); - } - } catch (error) { - console.error(`[${requestId}] Unhandled exception:`, error); - return NextResponse.json( - { - error: "An unexpected error occurred", - details: error instanceof Error ? error.message : String(error), - requestId, - }, - { status: 500 } - ); - } -} - -// Allow streaming responses up to 30 seconds -export const maxDuration = 120; - -const account = privateKeyToAccount( - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" -); // dummy key - -const chains = [base, mode]; - -const client = createWalletClient({ - account, - chain: base, - transport: http(), -}); - -createConfig({ - integrator: "ionic", - providers: [ - EVM({ - getWalletClient: async () => client, - switchChain: async (chainId: number) => - // Switch chain by creating a new wallet client - createWalletClient({ - account, - chain: chains.find(chain => chain.id == chainId) as vChain, - transport: http(), - }), - }), - ], -}); - -// 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( - (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 - ); - const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, - messages: [ - { - role: "user", - content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, - }, - ], - }); - - const cleanTitle = title - .trim() - .replace(/["'\.]/g, "") - .slice(0, 80); - - const finalTitle = - cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; - - // 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" - ); - } -} - -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` - ); - - try { - const body = await req.json(); - const { messages: fullHistory, address, id, searchType } = body; - - console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { - id: id || "N/A", - address: address || "N/A", - searchType: searchType || "N/A", - messageCount: fullHistory?.length || 0, - }); - - 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 ===` - ); - - // Check address requirement based on mode - if (!address && searchType === "sentinel-mode") { - console.error( - `[${postRequestId}] (MODIFIED) Error: User address is required for Sentinel mode.` - ); - return NextResponse.json( - { error: "User address is required for message quota tracking" }, - { status: 400 } - ); - } - - 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" - ) { - console.error( - `[${postRequestId}] (MODIFIED) Morpheus Search error: No valid user message found after filtering.` - ); - return new Response( - JSON.stringify({ - error: "Cannot start Morpheus Search without a valid user message.", - }), - { 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 - ); - - if (rawStream) { - console.log( - `[${postRequestId}] (MODIFIED) Returning RAW stream obtained from morpheusSearch.ts` - ); - return new Response(rawStream, { - headers: { "Content-Type": "text/event-stream" }, - }); - } else { - console.error( - `[${postRequestId}] (MODIFIED) getMorpheusSearchRawStream returned null/undefined.` - ); - throw new Error( - "getMorpheusSearchRawStream did not return a valid ReadableStream." - ); - } - } catch (error) { - console.error( - `[${postRequestId}] (MODIFIED) 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"}`, - }), - { status: 500, headers: { "Content-Type": "application/json" } } - ); - } - } else if (searchType === "sentinel-mode") { - console.log( - `[${postRequestId}] (MODIFIED) Processing Sentinel request for address: ${address}` - ); - - // Check quota - try { - await incrementMessageUsage(supabaseWrite, address!); - console.log( - `[${postRequestId}] (MODIFIED) Quota check passed for address: ${address}` - ); - } 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 } - ); - } - console.error( - `[${postRequestId}] (MODIFIED) Error checking message quota (allowing request):`, - error - ); - // 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(); - - console.log( - `[${postRequestId}] (MODIFIED) Preparing MCP client and tools for Sentinel...` - ); - const mcpClient = await createMCPClient({ - transport: { type: "sse", url: process.env.MATRIX_MCP_URL || "" }, - }); - const matrixMcpTools = await mcpClient.tools(); - console.log( - `[${postRequestId}] (MODIFIED) Sentinel - MCP Tools Received:`, - Object.keys(matrixMcpTools) - ); - - 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("..."), - }), - }), - 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("..."), - }), - }), - 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("..."), - }), - }), - 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}...` - ); - 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...` - ); - const finalAssistantMessage = formatResponseToObject(response); // Convert LLM response - console.log( - `[${postRequestId}] (MODIFIED) Formatted final assistant message (ID: ${finalAssistantMessage.id})` - ); - - const messagesForFinalSave = [ - ...(fullHistory || []), - finalAssistantMessage, - ]; - console.log( - `[${postRequestId}] (MODIFIED) Final message count for saving: ${messagesForFinalSave.length}` - ); - - 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.error( - `[${postRequestId}] (MODIFIED) Error in onFinish save function:`, - error - ); - } - }, - 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( - `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, - error - ); - const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred"; - console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { - error: errorMessage, - }); - return new Response(JSON.stringify({ error: errorMessage }), { - 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) => { - if (item.type === "text") { - return { - type: "text", - text: item.text, - }; - } else if (item.type === "tool-result") { - const invocation = { - type: "tool-invocation", - toolInvocation: { - state: "result", - step: toolInvocationIndex, - toolCallId: item.toolCallId, - toolName: item.toolName, - args: {}, - result: item.result, - }, - }; - toolInvocationIndex++; - return invocation; - } - 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; - }); - - // 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(), - role: "assistant", - content: textContent, - parts, - toolInvocations, - revisionId: Math.random().toString(36).substr(2, 16), - }; -} + + // 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)}`; + + try { + console.log(`[${requestId}] PUT /api/chat - Chat save request`); + + // Parse the request body + const body = await req.json(); + + // Get needed fields, with fallbacks for multiple naming patterns + const id = body.id; + const walletAddress = body.wallet_address || body.address; + const messages = body.messages; + + console.log( + `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` + ); + + // Validate required fields + if (!id) { + return NextResponse.json( + { + error: "Missing required field: id", + }, + { status: 400 } + ); + } + + if (!walletAddress) { + return NextResponse.json( + { + error: "Missing required field: wallet_address", + }, + { status: 400 } + ); + } + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + console.warn(`[${requestId}] Empty or invalid messages array`); + } + + // Generate a title + let title = "New Conversation"; + try { + title = await generateConversationTitle(messages || []); + } catch (titleError) { + console.log("titleError", titleError); + // Fall back to first message or default + title = + messages && messages.length > 0 + ? messages[0]?.content?.slice(0, 80) || "New Conversation" + : "New Conversation"; + } + + // Extract message content + const assistantMessage = messages?.find( + (m: UIMessage) => m.role === "assistant" + ); + const userMessage = messages?.find((m: UIMessage) => m.role === "user"); + + // 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, + }; + + console.log(`[${requestId}] Save data prepared:`, { + id: saveData.id, + wallet_address: saveData.wallet_address, + title_length: saveData.label.length, + message_count: saveData.messages.length, + }); + + // Save to database (simple upsert) + try { + console.log(`[${requestId}] Executing upsert to saved_chats table...`); + const { error } = await supabaseWrite + .from("saved_chats") + .upsert([saveData], { + onConflict: "id", + }); + + if (error) { + console.error(`[${requestId}] Save error:`, error); + return NextResponse.json( + { + error: `Failed to save chat: ${error.message}`, + details: error, + requestId, + }, + { status: 500 } + ); + } + + console.log(`[${requestId}] Chat saved successfully`); + return NextResponse.json({ + success: true, + message: "Chat saved successfully", + requestId, + }); + } catch (error) { + console.error(`[${requestId}] Database error:`, error); + return NextResponse.json( + { + error: `Database error: ${error instanceof Error ? error.message : String(error)}`, + requestId, + }, + { status: 500 } + ); + } + } catch (error) { + console.error(`[${requestId}] Unhandled exception:`, error); + return NextResponse.json( + { + error: "An unexpected error occurred", + details: error instanceof Error ? error.message : String(error), + requestId, + }, + { status: 500 } + ); + } + } + + // Allow streaming responses up to 30 seconds + export const maxDuration = 120; + + const account = privateKeyToAccount( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + ); // dummy key + + const chains = [base, mode]; + + const client = createWalletClient({ + account, + chain: base, + transport: http(), + }); + + createConfig({ + integrator: "ionic", + providers: [ + EVM({ + getWalletClient: async () => client, + switchChain: async (chainId: number) => + // Switch chain by creating a new wallet client + createWalletClient({ + account, + chain: chains.find(chain => chain.id == chainId) as vChain, + transport: http(), + }), + }), + ], + }); + + // 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( + (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 + ); + const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, + messages: [ + { + role: "user", + content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, + }, + ], + }); + + const cleanTitle = title + .trim() + .replace(/["'\.]/g, "") + .slice(0, 80); + + const finalTitle = + cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; + + // 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" + ); + } + } + + export async function POST(req: Request) { + const postRequestId = `post-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + console.log(`[${postRequestId}] POST /api/chat - Request received`); + + try { + const body = await req.json(); + const { messages: fullHistory, address, id, searchType } = body; + + console.log(`[${postRequestId}] Request Details:`, { + id: id || "N/A", + address: address || "N/A", + searchType: searchType || "N/A", + messageCount: fullHistory?.length || 0, + }); + + // Apply message filtering for LLM + console.log(`[${postRequestId}] Filtering message history for LLM...`); + const filteredMessagesForLLM = filterHistoryForLLM(fullHistory || []); + console.log( + `[${postRequestId}] Original message count: ${fullHistory?.length || 0}, Filtered count for LLM: ${filteredMessagesForLLM.length}` + ); + + // Check if we have a user address for normal requests + if (!address && searchType !== "morpheus-search") { + return NextResponse.json( + { + error: "User address is required for message quota tracking", + }, + { status: 400 } + ); + } + + // Handle morpheus-Search requests + if (searchType === "morpheus-search") { + // Check if filtered messages are valid for starting Morpheus + if ( + filteredMessagesForLLM.length === 0 || + filteredMessagesForLLM[filteredMessagesForLLM.length - 1].role !== "user" + ) { + console.error( + `[${postRequestId}] Morpheus Search error: No valid user message found after filtering.` + ); + return new Response( + JSON.stringify({ + error: "Cannot start Morpheus Search without a valid user message.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + try { + console.log( + `[${postRequestId}] route.ts: Calling getMorpheusSearchRawStream with FILTERED message history (${filteredMessagesForLLM.length} messages)...` + ); + const rawStream = await getMorpheusSearchRawStream(filteredMessagesForLLM); + if (rawStream) { + console.log( + `[${postRequestId}] route.ts: Returning RAW stream obtained from morpheusSearch.ts` + ); + return new Response(rawStream, { + headers: { + "Content-Type": "text/event-stream", + }, + }); + } else { + console.error( + `[${postRequestId}] route.ts: getMorpheusSearchRawStream returned null/undefined.` + ); + throw new Error( + "getMorpheusSearchRawStream did not return a valid ReadableStream." + ); + } + } catch (error) { + console.error( + `[${postRequestId}] route.ts: 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" + }`, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + } + // For normal chat requests, check quota and proceed + try { + // This will throw an error if quota is exceeded + await incrementMessageUsage(supabaseWrite, address); + } catch (error: any) { + if (error.message === "Daily message quota exceeded") { + return NextResponse.json( + { + error: + "You have reached your daily message limit. Please try again tomorrow or upgrade to a premium plan.", + }, + { status: 429 } + ); + } + + console.error("Error checking message quota:", error); + // Continue processing if there's an error with quota checking + // This prevents blocking users if the quota system fails + } + + // Define a simple save function for the initial state + const saveChat = async () => { + try { + // Basic data for save + const title = await generateConversationTitle(fullHistory || []); + const saveData = { + id, + wallet_address: address, + label: title, + prompt: fullHistory?.[fullHistory.length - 1]?.content || "", + 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", + }); + } catch (e) { + console.error(`Error in initial save:`, e); + } + }; + + // Try to save the chat at the start to ensure it gets created + await saveChat(); + + const mcpClient = await createMCPClient({ + transport: { + type: "sse", + url: process.env.MATRIX_MCP_URL || "", + }, + }); + + const matrixMcpTools = await mcpClient.tools(); + + console.log("matrixMcpTools", Object.keys(matrixMcpTools)); + + const streamConfig = { + //model: deepseek("deepseek-chat"), + model: anthropic("claude-3-5-sonnet-latest"), + //model: xai("grok-3"), + //model: google('gemini-2.5-pro-exp-03-25'), + //model: anthropic("claude-3-5-haiku-latest"), + //model: google("gemini-2.5-pro-exp-03-25"), + //model: openai.chat("gpt-4o"), + messages: filteredMessagesForLLM, // Use filtered messages for the LLM + tools: { + ...matrixMcpTools, + + // 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 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 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"), + }), + }), + }, + async onFinish(finish: { response: { messages: any[] } }) { + await mcpClient.close(); + const { response } = finish; + try { + // Validate required fields + if (!id || !address) { + return; + } + + // Generate title + let title; + try { + title = await generateConversationTitle(fullHistory || []); + } catch (e) { + console.log("titleError", e); + title = fullHistory?.[0]?.content?.slice(0, 80) || "New Conversation"; + } + + // Format the response + const formattedResponse = formatResponseToObject(response); + + // Create the complete message history for final save + const finalMessages = [...(fullHistory || []), formattedResponse]; + + // Create data for final save + const finalSaveData = { + id, + wallet_address: address, + label: title, + prompt: fullHistory?.[fullHistory.length - 1]?.content || "", + response: formattedResponse.content || "", + messages: finalMessages, + is_favorite: false, + }; + + // Save complete data + await supabaseWrite.from("saved_chats").upsert([finalSaveData], { + onConflict: "id", + }); + } catch (error) { + console.error("Error in onFinish function:", error); + } + }, + system: systemPrompt(address), + maxSteps: 25, + }; + + // Create the result stream + const resultStream = streamText(streamConfig).toDataStreamResponse(); + + return resultStream; + } catch (error) { + console.error("★★★ CRITICAL API ERROR ★★★", error); + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred"; + console.log("Returning ERROR RESPONSE:", { error: errorMessage }); + return new Response( + JSON.stringify({ + error: errorMessage, + }), + { + 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) => { + if (item.type === "text") { + return { + type: "text", + text: item.text, + }; + } else if (item.type === "tool-result") { + const invocation = { + type: "tool-invocation", + toolInvocation: { + state: "result", + step: toolInvocationIndex, + toolCallId: item.toolCallId, + toolName: item.toolName, + args: {}, + result: item.result, + }, + }; + toolInvocationIndex++; + return invocation; + } + 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; + }); + + // 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(), + role: "assistant", + content: textContent, + parts, + toolInvocations, + revisionId: Math.random().toString(36).substr(2, 16), + }; + } \ No newline at end of file diff --git a/src/components/shared/MatrixCanvas.tsx b/src/components/shared/MatrixCanvas.tsx index 8bf6b663..5f2c2005 100644 --- a/src/components/shared/MatrixCanvas.tsx +++ b/src/components/shared/MatrixCanvas.tsx @@ -49,7 +49,7 @@ const MatrixCanvas = () => { const draw = () => { // Black BG for the canvas with opacity to create trail effect const isDarkMode = document.documentElement.classList.contains("dark"); - + // Theme-aware background - dark in dark mode, white in light mode if (isDarkMode) { ctx.fillStyle = "rgba(0, 0, 0, 0.06)"; // Dark mode: black with opacity From 9d1483ddae8628fb0b494f1069fe5a1beb402b39 Mon Sep 17 00:00:00 2001 From: Arun Date: Mon, 28 Apr 2025 20:57:43 +0530 Subject: [PATCH 03/11] tool-fix --- src/app/api/chat/route.ts | 1490 ++++++++++++++++++------------------- 1 file changed, 745 insertions(+), 745 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 5d21d27d..466d062b 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,768 +1,768 @@ import { NextResponse } from "next/server"; -import { anthropic } from "@ai-sdk/anthropic"; - //import { deepseek } from "@ai-sdk/deepseek"; - import { google } from "@ai-sdk/google"; - import { xai } from "@ai-sdk/xai"; - //import { openai } from "@ai-sdk/openai"; - import { EVM, createConfig } from "@lifi/sdk"; - import { SupabaseClient, createClient } from "@supabase/supabase-js"; - import { - experimental_createMCPClient as createMCPClient, - generateText, - streamText, - tool, - } from "ai"; - import { createWalletClient, http } from "viem"; - import type { Chain as vChain } from "viem"; - import { privateKeyToAccount } from "viem/accounts"; - import { base, mode } from "viem/chains"; - import { z } from "zod"; - - - - import { CHAINS } from "@/lib/chains"; - import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; - import { incrementMessageUsage } from "@/lib/userManager"; - - - - import { systemPrompt } from "./systemPrompt"; - import { UIMessage } from "./tools/types"; - - - - - - const supabaseWrite: SupabaseClient = createClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_KEY! - ); +//import { anthropic } from "@ai-sdk/anthropic"; +//import { deepseek } from "@ai-sdk/deepseek"; +import { google } from "@ai-sdk/google"; +import { xai } from "@ai-sdk/xai"; +//import { openai } from "@ai-sdk/openai"; +import { EVM, createConfig } from "@lifi/sdk"; +import { SupabaseClient, createClient } from "@supabase/supabase-js"; +import { + experimental_createMCPClient as createMCPClient, + generateText, + streamText, + tool, +} from "ai"; +import { createWalletClient, http } from "viem"; +import type { Chain as vChain } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { base, mode } from "viem/chains"; +import { z } from "zod"; + + + +import { CHAINS } from "@/lib/chains"; +import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; +import { incrementMessageUsage } from "@/lib/userManager"; + + + +import { systemPrompt } from "./systemPrompt"; +import { UIMessage } from "./tools/types"; + + + + + +const supabaseWrite: SupabaseClient = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_KEY! +); // Function to determine if a part is a tool data part 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) - ); +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) +); } // Filter history for LLM to improve performance and reduce noise 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; +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...` - ); + 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; +}; + +// 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)}`; + + try { + console.log(`[${requestId}] PUT /api/chat - Chat save request`); + + // Parse the request body + const body = await req.json(); + + // Get needed fields, with fallbacks for multiple naming patterns + const id = body.id; + const walletAddress = body.wallet_address || body.address; + const messages = body.messages; + + console.log( + `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` + ); + + // Validate required fields + if (!id) { + return NextResponse.json( + { + error: "Missing required field: id", + }, + { status: 400 } + ); + } + + if (!walletAddress) { + return NextResponse.json( + { + error: "Missing required field: wallet_address", + }, + { status: 400 } + ); + } + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + console.warn(`[${requestId}] Empty or invalid messages array`); + } + + // Generate a title + let title = "New Conversation"; + try { + title = await generateConversationTitle(messages || []); + } catch (titleError) { + console.log("titleError", titleError); + // Fall back to first message or default + title = + messages && messages.length > 0 + ? messages[0]?.content?.slice(0, 80) || "New Conversation" + : "New Conversation"; + } + + // Extract message content + const assistantMessage = messages?.find( + (m: UIMessage) => m.role === "assistant" + ); + const userMessage = messages?.find((m: UIMessage) => m.role === "user"); + + // 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, + }; + + console.log(`[${requestId}] Save data prepared:`, { + id: saveData.id, + wallet_address: saveData.wallet_address, + title_length: saveData.label.length, + message_count: saveData.messages.length, + }); + + // Save to database (simple upsert) + try { + console.log(`[${requestId}] Executing upsert to saved_chats table...`); + const { error } = await supabaseWrite + .from("saved_chats") + .upsert([saveData], { + onConflict: "id", + }); + + if (error) { + console.error(`[${requestId}] Save error:`, error); + return NextResponse.json( + { + error: `Failed to save chat: ${error.message}`, + details: error, + requestId, + }, + { status: 500 } + ); + } + + console.log(`[${requestId}] Chat saved successfully`); + return NextResponse.json({ + success: true, + message: "Chat saved successfully", + requestId, + }); + } catch (error) { + console.error(`[${requestId}] Database error:`, error); + return NextResponse.json( + { + error: `Database error: ${error instanceof Error ? error.message : String(error)}`, + requestId, + }, + { status: 500 } + ); + } + } catch (error) { + console.error(`[${requestId}] Unhandled exception:`, error); + return NextResponse.json( + { + error: "An unexpected error occurred", + details: error instanceof Error ? error.message : String(error), + requestId, + }, + { status: 500 } + ); + } +} + +// Allow streaming responses up to 30 seconds +export const maxDuration = 120; + +const account = privateKeyToAccount( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +); // dummy key + +const chains = [base, mode]; + +const client = createWalletClient({ + account, + chain: base, + transport: http(), +}); + +createConfig({ + integrator: "ionic", + providers: [ + EVM({ + getWalletClient: async () => client, + switchChain: async (chainId: number) => + // Switch chain by creating a new wallet client + createWalletClient({ + account, + chain: chains.find(chain => chain.id == chainId) as vChain, + transport: http(), + }), + }), + ], +}); - // 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(); +// 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( + (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 + ); + const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, + messages: [ + { + role: "user", + content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, + }, + ], + }); + + const cleanTitle = title + .trim() + .replace(/["'\.]/g, "") + .slice(0, 80); + + const finalTitle = + cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; + + // 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" + ); + } +} + +export async function POST(req: Request) { + const postRequestId = `post-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + console.log(`[${postRequestId}] POST /api/chat - Request received`); + + try { + const body = await req.json(); + const { messages: fullHistory, address, id, searchType } = body; + + console.log(`[${postRequestId}] Request Details:`, { + id: id || "N/A", + address: address || "N/A", + searchType: searchType || "N/A", + messageCount: fullHistory?.length || 0, + }); + + // Apply message filtering for LLM + console.log(`[${postRequestId}] Filtering message history for LLM...`); + const filteredMessagesForLLM = filterHistoryForLLM(fullHistory || []); + console.log( + `[${postRequestId}] Original message count: ${fullHistory?.length || 0}, Filtered count for LLM: ${filteredMessagesForLLM.length}` + ); + + // Check if we have a user address for normal requests + if (!address && searchType !== "morpheus-search") { + return NextResponse.json( + { + error: "User address is required for message quota tracking", + }, + { status: 400 } + ); + } + + // Handle morpheus-Search requests + if (searchType === "morpheus-search") { + // Check if filtered messages are valid for starting Morpheus + if ( + filteredMessagesForLLM.length === 0 || + filteredMessagesForLLM[filteredMessagesForLLM.length - 1].role !== "user" + ) { + console.error( + `[${postRequestId}] Morpheus Search error: No valid user message found after filtering.` + ); + return new Response( + JSON.stringify({ + error: "Cannot start Morpheus Search without a valid user message.", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + try { + console.log( + `[${postRequestId}] route.ts: Calling getMorpheusSearchRawStream with FILTERED message history (${filteredMessagesForLLM.length} messages)...` + ); + const rawStream = await getMorpheusSearchRawStream(filteredMessagesForLLM); + if (rawStream) { console.log( - ` - Reconstructed content length: ${finalContent.length}` + `[${postRequestId}] route.ts: Returning RAW stream obtained from morpheusSearch.ts` ); + return new Response(rawStream, { + headers: { + "Content-Type": "text/event-stream", + }, + }); } else { - console.log( - ` - Using original content (length: ${finalContent.length})` + console.error( + `[${postRequestId}] route.ts: getMorpheusSearchRawStream returned null/undefined.` ); - } - - // Skip if empty - if (!finalContent) { - console.log( - ` - Filtering out assistant message (ID: ${message.id}) as it became empty.` + throw new Error( + "getMorpheusSearchRawStream did not return a valid ReadableStream." ); - return null; } + } catch (error) { + console.error( + `[${postRequestId}] route.ts: Error handling raw stream from getMorpheusSearchRawStream:`, + error + ); - console.log( - ` - Keeping simplified assistant message (ID: ${message.id})` + return new Response( + JSON.stringify({ + error: `morpheus-search raw stream error: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + } + // For normal chat requests, check quota and proceed + try { + // This will throw an error if quota is exceeded + await incrementMessageUsage(supabaseWrite, address); + } catch (error: any) { + if (error.message === "Daily message quota exceeded") { + return NextResponse.json( + { + error: + "You have reached your daily message limit. Please try again tomorrow or upgrade to a premium plan.", + }, + { status: 429 } ); + } + + console.error("Error checking message quota:", error); + // Continue processing if there's an error with quota checking + // This prevents blocking users if the quota system fails + } + + // Define a simple save function for the initial state + const saveChat = async () => { + try { + // Basic data for save + const title = await generateConversationTitle(fullHistory || []); + const saveData = { + id, + wallet_address: address, + label: title, + prompt: fullHistory?.[fullHistory.length - 1]?.content || "", + 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", + }); + } catch (e) { + console.error(`Error in initial save:`, e); + } + }; + + // Try to save the chat at the start to ensure it gets created + await saveChat(); + + const mcpClient = await createMCPClient({ + transport: { + type: "sse", + url: process.env.MATRIX_MCP_URL || "", + }, + }); + + const matrixMcpTools = await mcpClient.tools(); + + console.log("matrixMcpTools", Object.keys(matrixMcpTools)); + + const streamConfig = { + //model: deepseek("deepseek-chat"), + //model: anthropic("claude-3-5-sonnet-latest"), + model: xai("grok-3"), + //model: google('gemini-2.5-pro-exp-03-25'), + //model: anthropic("claude-3-5-haiku-latest"), + //model: google("gemini-2.5-pro-exp-03-25"), + //model: openai.chat("gpt-4o"), + messages: filteredMessagesForLLM, // Use filtered messages for the LLM + tools: { + ...matrixMcpTools, + + // 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 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 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"), + }), + }), + }, + async onFinish(finish: { response: { messages: any[] } }) { + await mcpClient.close(); + const { response } = finish; + try { + // Validate required fields + if (!id || !address) { + return; + } + + // Generate title + let title; + try { + title = await generateConversationTitle(fullHistory || []); + } catch (e) { + console.log("titleError", e); + title = fullHistory?.[0]?.content?.slice(0, 80) || "New Conversation"; + } + + // Format the response + const formattedResponse = formatResponseToObject(response); + + // Create the complete message history for final save + const finalMessages = [...(fullHistory || []), formattedResponse]; + + // Create data for final save + const finalSaveData = { + id, + wallet_address: address, + label: title, + prompt: fullHistory?.[fullHistory.length - 1]?.content || "", + response: formattedResponse.content || "", + messages: finalMessages, + is_favorite: false, + }; + + // Save complete data + await supabaseWrite.from("saved_chats").upsert([finalSaveData], { + onConflict: "id", + }); + } catch (error) { + console.error("Error in onFinish function:", error); + } + }, + system: systemPrompt(address), + maxSteps: 25, + }; + + // Create the result stream + const resultStream = streamText(streamConfig).toDataStreamResponse(); + + return resultStream; + } catch (error) { + console.error("★★★ CRITICAL API ERROR ★★★", error); + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred"; + console.log("Returning ERROR RESPONSE:", { error: errorMessage }); + return new Response( + JSON.stringify({ + error: errorMessage, + }), + { + 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) => { + if (item.type === "text") { return { - id: message.id, - role: message.role, - content: finalContent, - createdAt: message.createdAt, - parts: undefined, - toolInvocations: undefined, - mode: undefined, + type: "text", + text: item.text, }; + } else if (item.type === "tool-result") { + const invocation = { + type: "tool-invocation", + toolInvocation: { + state: "result", + step: toolInvocationIndex, + toolCallId: item.toolCallId, + toolName: item.toolName, + args: {}, + result: item.result, + }, + }; + toolInvocationIndex++; + return invocation; } - // Keep other message types if they exist and are valid - console.log( - ` - Keeping other message type (Role: ${message.role}, ID: ${message.id})` - ); - return message; + return null; }) - .filter(message => message !== null) as UIMessage[]; // Remove null messages - console.log( - `<<< History filtering complete. Kept ${filtered.length} messages.` - ); // Log: End filtering - return filtered; -}; - - // 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)}`; - - try { - console.log(`[${requestId}] PUT /api/chat - Chat save request`); - - // Parse the request body - const body = await req.json(); - - // Get needed fields, with fallbacks for multiple naming patterns - const id = body.id; - const walletAddress = body.wallet_address || body.address; - const messages = body.messages; - - console.log( - `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` - ); - - // Validate required fields - if (!id) { - return NextResponse.json( - { - error: "Missing required field: id", - }, - { status: 400 } - ); - } - - if (!walletAddress) { - return NextResponse.json( - { - error: "Missing required field: wallet_address", - }, - { status: 400 } - ); - } - - if (!messages || !Array.isArray(messages) || messages.length === 0) { - console.warn(`[${requestId}] Empty or invalid messages array`); - } - - // Generate a title - let title = "New Conversation"; - try { - title = await generateConversationTitle(messages || []); - } catch (titleError) { - console.log("titleError", titleError); - // Fall back to first message or default - title = - messages && messages.length > 0 - ? messages[0]?.content?.slice(0, 80) || "New Conversation" - : "New Conversation"; - } - - // Extract message content - const assistantMessage = messages?.find( - (m: UIMessage) => m.role === "assistant" - ); - const userMessage = messages?.find((m: UIMessage) => m.role === "user"); - - // 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, - }; - - console.log(`[${requestId}] Save data prepared:`, { - id: saveData.id, - wallet_address: saveData.wallet_address, - title_length: saveData.label.length, - message_count: saveData.messages.length, - }); - - // Save to database (simple upsert) - try { - console.log(`[${requestId}] Executing upsert to saved_chats table...`); - const { error } = await supabaseWrite - .from("saved_chats") - .upsert([saveData], { - onConflict: "id", - }); - - if (error) { - console.error(`[${requestId}] Save error:`, error); - return NextResponse.json( - { - error: `Failed to save chat: ${error.message}`, - details: error, - requestId, - }, - { status: 500 } - ); - } - - console.log(`[${requestId}] Chat saved successfully`); - return NextResponse.json({ - success: true, - message: "Chat saved successfully", - requestId, - }); - } catch (error) { - console.error(`[${requestId}] Database error:`, error); - return NextResponse.json( - { - error: `Database error: ${error instanceof Error ? error.message : String(error)}`, - requestId, - }, - { status: 500 } - ); - } - } catch (error) { - console.error(`[${requestId}] Unhandled exception:`, error); - return NextResponse.json( - { - error: "An unexpected error occurred", - details: error instanceof Error ? error.message : String(error), - requestId, - }, - { status: 500 } - ); - } - } - - // Allow streaming responses up to 30 seconds - export const maxDuration = 120; - - const account = privateKeyToAccount( - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - ); // dummy key - - const chains = [base, mode]; - - const client = createWalletClient({ - account, - chain: base, - transport: http(), - }); - - createConfig({ - integrator: "ionic", - providers: [ - EVM({ - getWalletClient: async () => client, - switchChain: async (chainId: number) => - // Switch chain by creating a new wallet client - createWalletClient({ - account, - chain: chains.find(chain => chain.id == chainId) as vChain, - transport: http(), - }), - }), - ], - }); - - // 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( - (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 - ); - const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, - messages: [ - { - role: "user", - content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, - }, - ], - }); - - const cleanTitle = title - .trim() - .replace(/["'\.]/g, "") - .slice(0, 80); - - const finalTitle = - cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; - - // 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" - ); - } - } - - export async function POST(req: Request) { - const postRequestId = `post-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - console.log(`[${postRequestId}] POST /api/chat - Request received`); - - try { - const body = await req.json(); - const { messages: fullHistory, address, id, searchType } = body; - - console.log(`[${postRequestId}] Request Details:`, { - id: id || "N/A", - address: address || "N/A", - searchType: searchType || "N/A", - messageCount: fullHistory?.length || 0, - }); - - // Apply message filtering for LLM - console.log(`[${postRequestId}] Filtering message history for LLM...`); - const filteredMessagesForLLM = filterHistoryForLLM(fullHistory || []); - console.log( - `[${postRequestId}] Original message count: ${fullHistory?.length || 0}, Filtered count for LLM: ${filteredMessagesForLLM.length}` - ); - - // Check if we have a user address for normal requests - if (!address && searchType !== "morpheus-search") { - return NextResponse.json( - { - error: "User address is required for message quota tracking", - }, - { status: 400 } - ); - } - - // Handle morpheus-Search requests - if (searchType === "morpheus-search") { - // Check if filtered messages are valid for starting Morpheus - if ( - filteredMessagesForLLM.length === 0 || - filteredMessagesForLLM[filteredMessagesForLLM.length - 1].role !== "user" - ) { - console.error( - `[${postRequestId}] Morpheus Search error: No valid user message found after filtering.` - ); - return new Response( - JSON.stringify({ - error: "Cannot start Morpheus Search without a valid user message.", - }), - { status: 400, headers: { "Content-Type": "application/json" } } - ); - } - - try { - console.log( - `[${postRequestId}] route.ts: Calling getMorpheusSearchRawStream with FILTERED message history (${filteredMessagesForLLM.length} messages)...` - ); - const rawStream = await getMorpheusSearchRawStream(filteredMessagesForLLM); - if (rawStream) { - console.log( - `[${postRequestId}] route.ts: Returning RAW stream obtained from morpheusSearch.ts` - ); - return new Response(rawStream, { - headers: { - "Content-Type": "text/event-stream", - }, - }); - } else { - console.error( - `[${postRequestId}] route.ts: getMorpheusSearchRawStream returned null/undefined.` - ); - throw new Error( - "getMorpheusSearchRawStream did not return a valid ReadableStream." - ); - } - } catch (error) { - console.error( - `[${postRequestId}] route.ts: 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" - }`, - }), - { - status: 500, - headers: { - "Content-Type": "application/json", - }, - } - ); - } - } - // For normal chat requests, check quota and proceed - try { - // This will throw an error if quota is exceeded - await incrementMessageUsage(supabaseWrite, address); - } catch (error: any) { - if (error.message === "Daily message quota exceeded") { - return NextResponse.json( - { - error: - "You have reached your daily message limit. Please try again tomorrow or upgrade to a premium plan.", - }, - { status: 429 } - ); - } - - console.error("Error checking message quota:", error); - // Continue processing if there's an error with quota checking - // This prevents blocking users if the quota system fails - } - - // Define a simple save function for the initial state - const saveChat = async () => { - try { - // Basic data for save - const title = await generateConversationTitle(fullHistory || []); - const saveData = { - id, - wallet_address: address, - label: title, - prompt: fullHistory?.[fullHistory.length - 1]?.content || "", - 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", - }); - } catch (e) { - console.error(`Error in initial save:`, e); - } - }; - - // Try to save the chat at the start to ensure it gets created - await saveChat(); - - const mcpClient = await createMCPClient({ - transport: { - type: "sse", - url: process.env.MATRIX_MCP_URL || "", - }, - }); - - const matrixMcpTools = await mcpClient.tools(); - - console.log("matrixMcpTools", Object.keys(matrixMcpTools)); - - const streamConfig = { - //model: deepseek("deepseek-chat"), - model: anthropic("claude-3-5-sonnet-latest"), - //model: xai("grok-3"), - //model: google('gemini-2.5-pro-exp-03-25'), - //model: anthropic("claude-3-5-haiku-latest"), - //model: google("gemini-2.5-pro-exp-03-25"), - //model: openai.chat("gpt-4o"), - messages: filteredMessagesForLLM, // Use filtered messages for the LLM - tools: { - ...matrixMcpTools, - - // 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 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 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"), - }), - }), - }, - async onFinish(finish: { response: { messages: any[] } }) { - await mcpClient.close(); - const { response } = finish; - try { - // Validate required fields - if (!id || !address) { - return; - } - - // Generate title - let title; - try { - title = await generateConversationTitle(fullHistory || []); - } catch (e) { - console.log("titleError", e); - title = fullHistory?.[0]?.content?.slice(0, 80) || "New Conversation"; - } - - // Format the response - const formattedResponse = formatResponseToObject(response); - - // Create the complete message history for final save - const finalMessages = [...(fullHistory || []), formattedResponse]; - - // Create data for final save - const finalSaveData = { - id, - wallet_address: address, - label: title, - prompt: fullHistory?.[fullHistory.length - 1]?.content || "", - response: formattedResponse.content || "", - messages: finalMessages, - is_favorite: false, - }; - - // Save complete data - await supabaseWrite.from("saved_chats").upsert([finalSaveData], { - onConflict: "id", - }); - } catch (error) { - console.error("Error in onFinish function:", error); - } - }, - system: systemPrompt(address), - maxSteps: 25, - }; - - // Create the result stream - const resultStream = streamText(streamConfig).toDataStreamResponse(); - - return resultStream; - } catch (error) { - console.error("★★★ CRITICAL API ERROR ★★★", error); - const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred"; - console.log("Returning ERROR RESPONSE:", { error: errorMessage }); - return new Response( - JSON.stringify({ - error: errorMessage, - }), - { - 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) => { - if (item.type === "text") { - return { - type: "text", - text: item.text, - }; - } else if (item.type === "tool-result") { - const invocation = { - type: "tool-invocation", - toolInvocation: { - state: "result", - step: toolInvocationIndex, - toolCallId: item.toolCallId, - toolName: item.toolName, - args: {}, - result: item.result, - }, - }; - toolInvocationIndex++; - return invocation; - } - 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; - }); - - // 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(), - role: "assistant", - content: textContent, - parts, - toolInvocations, - revisionId: Math.random().toString(36).substr(2, 16), - }; - } \ No newline at end of file + .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; + }); + + // 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(), + role: "assistant", + content: textContent, + parts, + toolInvocations, + revisionId: Math.random().toString(36).substr(2, 16), + }; +} \ No newline at end of file From 5f2a838c60309f05caa99db2393faf245ac73d78 Mon Sep 17 00:00:00 2001 From: Arun Date: Tue, 29 Apr 2025 11:27:22 +0530 Subject: [PATCH 04/11] client tools fix --- src/app/api/chat/route.ts | 587 ++++++++++++++------------------ src/app/api/chat/tools/types.ts | 8 +- src/lib/messageUtils.ts | 50 +++ src/lib/morpheusSearch.ts | 77 +++-- 4 files changed, 355 insertions(+), 367 deletions(-) create mode 100644 src/lib/messageUtils.ts diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 466d062b..34faa97e 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { filterAndSimplifyHistoryForLLM } from "@/lib/messageUtils"; //import { anthropic } from "@ai-sdk/anthropic"; //import { deepseek } from "@ai-sdk/deepseek"; import { google } from "@ai-sdk/google"; @@ -11,6 +12,7 @@ import { generateText, streamText, tool, + CoreMessage } from "ai"; import { createWalletClient, http } from "viem"; import type { Chain as vChain } from "viem"; @@ -18,139 +20,19 @@ import { privateKeyToAccount } from "viem/accounts"; import { base, mode } from "viem/chains"; import { z } from "zod"; - - import { CHAINS } from "@/lib/chains"; import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; import { incrementMessageUsage } from "@/lib/userManager"; - - import { systemPrompt } from "./systemPrompt"; import { UIMessage } from "./tools/types"; - - - - const supabaseWrite: SupabaseClient = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_KEY! ); -// Function to determine if a part is a tool data part -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) -); -} - -// Filter history for LLM to improve performance and reduce noise -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)}`; @@ -198,7 +80,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 @@ -206,21 +88,20 @@ 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:`, { @@ -228,9 +109,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 @@ -268,10 +149,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, }, @@ -280,14 +161,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, @@ -311,82 +190,81 @@ 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); + .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 - 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}] 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, }); - // Apply message filtering for LLM - console.log(`[${postRequestId}] Filtering message history for LLM...`); - const filteredMessagesForLLM = filterHistoryForLLM(fullHistory || []); - console.log( - `[${postRequestId}] Original message count: ${fullHistory?.length || 0}, Filtered count for LLM: ${filteredMessagesForLLM.length}` - ); + // Validate ID early + if (!id) { + console.error(`[${requestId}] Missing required field: id`); + return NextResponse.json({ error: "Missing required field: id" }, { status: 400 }); + } // Check if we have a user address for normal requests if (!address && searchType !== "morpheus-search") { + console.error(`[${requestId}] User address is required for non-Morpheus search`); return NextResponse.json( { error: "User address is required for message quota tracking", @@ -397,39 +275,34 @@ export async function POST(req: Request) { // Handle morpheus-Search requests if (searchType === "morpheus-search") { - // 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}] 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}] route.ts: Calling getMorpheusSearchRawStream with FILTERED message history (${filteredMessagesForLLM.length} messages)...` + `[${requestId}] Calling getMorpheusSearchRawStream with ${originalMessages.length} messages...` ); - const rawStream = await getMorpheusSearchRawStream(filteredMessagesForLLM); + const rawStream = await getMorpheusSearchRawStream(originalMessages); if (rawStream) { - console.log( - `[${postRequestId}] route.ts: Returning RAW stream obtained from morpheusSearch.ts` - ); + console.log(`[${requestId}] Returning RAW stream from morpheusSearch.`); return new Response(rawStream, { - headers: { - "Content-Type": "text/event-stream", - }, + headers: { "Content-Type": "text/event-stream" }, }); } else { console.error( - `[${postRequestId}] route.ts: getMorpheusSearchRawStream returned null/undefined.` + `[${requestId}] getMorpheusSearchRawStream returned null/undefined.` ); throw new Error( "getMorpheusSearchRawStream did not return a valid ReadableStream." @@ -437,10 +310,9 @@ export async function POST(req: Request) { } } catch (error) { console.error( - `[${postRequestId}] route.ts: 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: ${ @@ -449,88 +321,128 @@ export async function POST(req: Request) { }), { status: 500, - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, } ); } } - // For normal chat requests, check quota and proceed + + // --- Quota Check --- try { - // This will throw an error if quota is exceeded 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 to a premium plan.", + "You have reached your daily message limit. Please try again tomorrow or upgrade your plan.", }, - { status: 429 } + { status: 429 } // Too Many Requests ); } - - console.error("Error checking message quota:", error); - // Continue processing if there's an error with quota checking - // This prevents blocking users if the quota system fails + console.error(`[${requestId}] Error checking message quota (continuing anyway):`, error); } - // Define a simple save function for the initial state - const saveChat = async () => { + const saveChatInitial = async () => { try { - // Basic data for save - const title = await generateConversationTitle(fullHistory || []); + const title = await generateConversationTitle(originalMessages || []); + const userMessage = originalMessages?.find((m: UIMessage) => m.role === "user"); const saveData = { id, wallet_address: address, label: title, - prompt: fullHistory?.[fullHistory.length - 1]?.content || "", - response: "Processing...", - messages: [ - ...(fullHistory || []), - { - id: `temp-${Date.now()}`, - role: "assistant", - content: "Processing your request...", - }, - ], + prompt: userMessage?.content || "", + response: "Processing the request...", + messages: originalMessages || [], is_favorite: false, }; - - await supabaseWrite.from("saved_chats").upsert([saveData], { + 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.`); + } } catch (e) { - console.error(`Error in initial save:`, e); + console.error(`[${requestId}] Exception during initial save:`, e); } }; + await saveChatInitial(); - // Try to save the chat at the start to ensure it gets created - await saveChat(); - const mcpClient = await createMCPClient({ - transport: { - type: "sse", - url: process.env.MATRIX_MCP_URL || "", - }, - }); + // 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", + "getSwapBridgeData", + "deposit_withdraw_hyperliquid" + ]); - const matrixMcpTools = await mcpClient.tools(); + const lastMessage = originalMessages?.[originalMessages.length - 1]; + let previousTurnUsedClientTool = false; - console.log("matrixMcpTools", Object.keys(matrixMcpTools)); + 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) + ); + } + } + + let messagesToSendToModel: CoreMessage[]; + + if (previousTurnUsedClientTool) { + console.log(`[${requestId}] Previous assistant turn included a client tool result. Using original messages.`); + 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 || "", + }, + }); + matrixMcpTools = await mcpClient.tools(); + console.log(`[${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: deepseek("deepseek-chat"), //model: anthropic("claude-3-5-sonnet-latest"), - model: xai("grok-3"), - //model: google('gemini-2.5-pro-exp-03-25'), - //model: anthropic("claude-3-5-haiku-latest"), - //model: google("gemini-2.5-pro-exp-03-25"), + 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: filteredMessagesForLLM, // Use filtered messages for the LLM + messages: messagesToSendToModel, tools: { - ...matrixMcpTools, + ...matrixMcpTools, // Include MCP tools safely // Client Tools getDesiredChain: tool({ @@ -620,65 +532,80 @@ export async function POST(req: Request) { }), }), }, - async onFinish(finish: { response: { messages: any[] } }) { - await mcpClient.close(); + 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(); + console.log(`[${requestId}] MCP Client closed.`); + } + const { response } = finish; try { - // Validate required fields if (!id || !address) { + console.error(`[${requestId}] Missing id or address in onFinish. Aborting final save.`); return; } - - // Generate title let title; try { - title = await generateConversationTitle(fullHistory || []); + title = await generateConversationTitle(originalMessages || []); } catch (e) { - console.log("titleError", e); - title = fullHistory?.[0]?.content?.slice(0, 80) || "New Conversation"; + 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 + ]; - // Format the response - const formattedResponse = formatResponseToObject(response); - - // Create the complete message history for final save - const finalMessages = [...(fullHistory || []), formattedResponse]; - - // Create data for final save const finalSaveData = { id, wallet_address: address, label: title, - prompt: fullHistory?.[fullHistory.length - 1]?.content || "", - response: formattedResponse.content || "", - messages: finalMessages, + prompt: originalMessages?.find((m: UIMessage) => m.role === 'user')?.content || "", + response: finalAssistantMessage.content || "", + messages: finalMessagesToSave, is_favorite: false, }; - // Save complete data - await supabaseWrite.from("saved_chats").upsert([finalSaveData], { - onConflict: "id", - }); + 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(`[${requestId}] Error during final save in onFinish:`, finalSaveError); + } else { + console.log(`[${requestId}] Final save successful in onFinish.`); + } + } catch (error) { - console.error("Error in onFinish function:", error); + console.error(`[${requestId}] Error processing in onFinish function:`, error); } }, system: systemPrompt(address), - maxSteps: 25, + maxSteps: 25, + // temperature: 0.7, }; + console.log(`[${requestId}] Starting streamText with Sentinel model: ${streamConfig.model.modelId}`); + const resultStream = await streamText(streamConfig); + return resultStream.toDataStreamResponse(); - // Create the result stream - const resultStream = streamText(streamConfig).toDataStreamResponse(); - - return resultStream; } catch (error) { - console.error("★★★ CRITICAL API ERROR ★★★", error); + console.error(`[${requestId || 'req-unknown'}] ★★★ CRITICAL API ERROR in POST /api/chat ★★★`, error); const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred"; - console.log("Returning ERROR RESPONSE:", { error: errorMessage }); + 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, + requestId: requestId || 'req-unknown', }), { status: 500, @@ -689,80 +616,64 @@ export async function POST(req: Request) { ); } } - -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) => { - if (item.type === "text") { - return { - type: "text", - text: item.text, - }; - } else if (item.type === "tool-result") { - const invocation = { - type: "tool-invocation", - toolInvocation: { - state: "result", - step: toolInvocationIndex, - toolCallId: item.toolCallId, - toolName: item.toolName, - args: {}, - result: item.result, - }, - }; - toolInvocationIndex++; - return invocation; - } - 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, +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 }] }; - toolInvocationIndex++; - return invocation; - }); + } + + let textContent = ""; + const parts: any[] = []; + const toolInvocations: any[] = []; + + if (Array.isArray(assistantMsg.content)) { + assistantMsg.content.forEach((item: any) => { + if (item.type === 'text') { + textContent += item.text; + parts.push({ type: 'text', text: item.text }); + } else if (item.type === 'tool-call') { + } else if (item.type === 'tool-result') { + parts.push({ + type: 'tool-invocation', + toolInvocation: { + state: 'result', + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + } + }); + toolInvocations.push({ + state: 'result', + // step: ??? + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + }); + } + }); + } 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(), - role: "assistant", - content: textContent, - parts, - toolInvocations, - revisionId: Math.random().toString(36).substr(2, 16), - }; + return { + id: assistantMsg.id || `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + createdAt: assistantMsg.createdAt || new Date().toISOString(), + role: 'assistant', + content: textContent, + parts, + toolInvocations, + revisionId: Math.random().toString(36).substr(2, 16), + }; + } \ 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..b8c79195 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,6 @@ type ToolInvocationUIPart = { export type UIMessage = Message & { parts: Array; mode?: "morpheus" | "sentinel"; + revisionId?: string; }; + diff --git a/src/lib/messageUtils.ts b/src/lib/messageUtils.ts new file mode 100644 index 00000000..d3031c9a --- /dev/null +++ b/src/lib/messageUtils.ts @@ -0,0 +1,50 @@ +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; +} \ No newline at end of file diff --git a/src/lib/morpheusSearch.ts b/src/lib/morpheusSearch.ts index 24e0a11b..bc5417ba 100644 --- a/src/lib/morpheusSearch.ts +++ b/src/lib/morpheusSearch.ts @@ -1,23 +1,31 @@ +// src/lib/morpheusSearch.ts + import { google } from "@ai-sdk/google"; import { experimental_createMCPClient as createMCPClient, generateText, streamText, tool, + CoreMessage // Import CoreMessage for the simplified history type } from "ai"; import { z } from "zod"; import { morpheusSystemPrompt } from "@/app/api/chat/morpheusSystemPrompt"; - -import { UIMessage } from "../app/api/chat/tools/types"; +// Adjust path if your types file is located differently +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 +34,14 @@ 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 +50,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 +78,22 @@ 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 +103,14 @@ 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("NeoSearch - Search query:", searchQuery); + console.log(`[${neoSearchRequestId}] NeoSearch - Search query:`, searchQuery); try { + // Use the separate search-enabled model for this tool const searchResponse = await generateText({ model: searchEnabledModel, prompt: searchQuery, @@ -103,7 +120,7 @@ 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 +131,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,33 +140,41 @@ 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)); } } -export { getMorpheusSearchRawStream }; +export { getMorpheusSearchRawStream }; \ No newline at end of file From 23079f8742dd358cc154bb2e05b1e1dace083fbc Mon Sep 17 00:00:00 2001 From: Arun Date: Tue, 29 Apr 2025 11:33:53 +0530 Subject: [PATCH 05/11] revert changes token cuts --- src/app/api/chat/route.ts | 624 ++++++++++++++++---------------------- 1 file changed, 257 insertions(+), 367 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 921f6e3d..fddc56fd 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,3 +1,7 @@ ++1 +-11 +Original file line number Diff line number Diff line change +@@ -1,646 +1,636 @@ import { NextResponse } from "next/server"; //import { anthropic } from "@ai-sdk/anthropic"; @@ -32,117 +36,7 @@ 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; -}; - +// 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)}`; @@ -355,85 +249,63 @@ async function generateConversationTitle( } 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` - ); - try { const body = await req.json(); - const { messages: fullHistory, address, id, searchType } = body; + const { messages, address, id, searchType } = body; - console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { - id: id || "N/A", - address: address || "N/A", - searchType: searchType || "N/A", - messageCount: fullHistory?.length || 0, + console.log("id", id); + console.log("API Request:", { + messages, + address, + searchType, }); - 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 ===` - ); - - // Check address requirement based on mode - if (!address && searchType === "sentinel-mode") { - console.error( - `[${postRequestId}] (MODIFIED) Error: User address is required for Sentinel mode.` - ); + // Check if we have a user address for normal requests + if (!address && searchType !== "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 (!messages || messages.length === 0) { console.error( - `[${postRequestId}] (MODIFIED) Morpheus Search error: No valid user message found after filtering.` + `[API Request - ID: ${id}] 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 + `[API Request - ID: ${id}] route.ts: Calling getMorpheusSearchRawStream with FULL message history (${messages.length} messages)...` ); - + const rawStream = await getMorpheusSearchRawStream(messages); if (rawStream) { console.log( - `[${postRequestId}] (MODIFIED) Returning RAW stream obtained from morpheusSearch.ts` + `[API Request - ID: ${id}] route.ts: Returning RAW stream obtained from morpheusSearch.ts` ); return new Response(rawStream, { - headers: { "Content-Type": "text/event-stream" }, + headers: { + "Content-Type": "text/event-stream", + }, }); } else { console.error( - `[${postRequestId}] (MODIFIED) getMorpheusSearchRawStream returned null/undefined.` + `[API Request - ID: ${id}] route.ts: getMorpheusSearchRawStream returned null/undefined.` ); throw new Error( "getMorpheusSearchRawStream did not return a valid ReadableStream." @@ -441,237 +313,255 @@ export async function POST(req: Request) { } } catch (error) { console.error( - `[${postRequestId}] (MODIFIED) Error handling raw stream from getMorpheusSearchRawStream:`, + `[API Request - ID: ${id}] route.ts: 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", + }, + } + ); + } + } + // For normal chat requests, check quota and proceed (comment ou this section to bypass limit) + try { + // This will throw an error if quota is exceeded + await incrementMessageUsage(supabaseWrite, address); + } catch (error: any) { + if (error.message === "Daily message quota exceeded") { + return NextResponse.json( + { + error: + "You have reached your daily message limit. Please try again tomorrow or upgrade to a premium plan.", + }, + { status: 429 } ); } - } else if (searchType === "sentinel-mode") { - console.log( - `[${postRequestId}] (MODIFIED) Processing Sentinel request for address: ${address}` - ); - // Check quota + console.error("Error checking message quota:", error); + // Continue processing if there's an error with quota checking + // This prevents blocking users if the quota system fails + } + + // Define a simple save function for the initial state + const saveChat = async () => { try { - await incrementMessageUsage(supabaseWrite, address!); - console.log( - `[${postRequestId}] (MODIFIED) Quota check passed for address: ${address}` - ); - } 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 } - ); - } - console.error( - `[${postRequestId}] (MODIFIED) Error checking message quota (allowing request):`, - error - ); - // Decide if you want to block or allow if quota check fails + // Basic data for save + const title = messages[0]?.content?.slice(0, 80) || "New Conversation"; + const saveData = { + id, + wallet_address: address, + label: title, + prompt: messages[messages.length - 1]?.content || "", + response: "Processing...", + messages: [ + ...messages, + { + id: `temp-${Date.now()}`, + role: "assistant", + content: "Processing your request...", + }, + ], + is_favorite: false, + }; + + await supabaseWrite.from("saved_chats").upsert([saveData], { + onConflict: "id", + }); + } catch (e) { + console.error(`Error in initial save:`, e); } + }; - // Initial Save (Uses Full History) - const saveChat = async () => { + // Try to save the chat at the start to ensure it gets created + await saveChat(); + + const mcpClient = await createMCPClient({ + transport: { + type: "sse", + url: process.env.MATRIX_MCP_URL || "", + }, + }); + + const matrixMcpTools = await mcpClient.tools(); + + console.log("matrixMcpTools", Object.keys(matrixMcpTools)); + + const streamConfig = { + //model: deepseek("deepseek-chat"), + //model: anthropic("claude-3-5-sonnet-latest"), + model: xai("grok-3"), + //model: google('gemini-2.5-pro-exp-03-25'), + //model: anthropic("claude-3-5-haiku-latest"), + //model: google("gemini-2.5-pro-exp-03-25"), + //model: openai.chat("gpt-4o"), + messages, + tools: { + ...matrixMcpTools, + + // 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 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 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"), + }), + }), + }, + async onFinish(finish: { response: { messages: any[] } }) { + await mcpClient.close(); + const { response } = finish; try { - console.log( - `[${postRequestId}] (MODIFIED) Attempting initial save for chat ID: ${id}...` - ); - const title = await generateConversationTitle(fullHistory || []); // Use full History - const saveData = { + // Validate required fields + if (!id || !address) { + return; + } + + // Generate title + let title; + try { + title = await generateConversationTitle(messages); + } catch (e) { + console.log("titleError", e); + title = messages[0]?.content?.slice(0, 80) || "New Conversation"; + } + + // Format the response + const formattedResponse = formatResponseToObject(response); + + // Create a placeholder save with just the key fields + const initialSaveData = { 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...", - }, - ], + prompt: messages[messages.length - 1]?.content || "", + response: formattedResponse.content || "", 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 - ); + + // Save without the messages array + await supabaseWrite.from("saved_chats").upsert([initialSaveData], { + onConflict: "id", + }); + } catch (error) { + console.error("Error in onFinish function:", error); } - }; - await saveChat(); + }, + system: systemPrompt(address), + maxSteps: 25, + }; - console.log( - `[${postRequestId}] (MODIFIED) Preparing MCP client and tools for Sentinel...` - ); - const mcpClient = await createMCPClient({ - transport: { type: "sse", url: process.env.MATRIX_MCP_URL || "" }, - }); - const matrixMcpTools = await mcpClient.tools(); - console.log( - `[${postRequestId}] (MODIFIED) Sentinel - MCP Tools Received:`, - Object.keys(matrixMcpTools) - ); + // Create the result stream + const resultStream = streamText(streamConfig).toDataStreamResponse(); - 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("..."), - }), - }), - 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("..."), - }), - }), - 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("..."), - }), - }), - 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}...` - ); - 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...` - ); - const finalAssistantMessage = formatResponseToObject(response); // Convert LLM response - console.log( - `[${postRequestId}] (MODIFIED) Formatted final assistant message (ID: ${finalAssistantMessage.id})` - ); - - const messagesForFinalSave = [ - ...(fullHistory || []), - finalAssistantMessage, - ]; - console.log( - `[${postRequestId}] (MODIFIED) Final message count for saving: ${messagesForFinalSave.length}` - ); - - 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.error( - `[${postRequestId}] (MODIFIED) Error in onFinish save function:`, - error - ); - } - }, - 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" } } - ); - } + return resultStream; } catch (error) { - console.error( - `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, - error - ); + console.error("★★★ CRITICAL API ERROR ★★★", error); const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; - console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { - error: errorMessage, - }); - return new Response(JSON.stringify({ error: errorMessage }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); + console.log("Returning ERROR RESPONSE:", { error: errorMessage }); + return new Response( + JSON.stringify({ + error: errorMessage, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); } } + function formatResponseToObject(response: any) { // Get the flattened content array const flatContent = response.messages From 3d5310e563ac58daabacd7cfaff2931b5c8d6994 Mon Sep 17 00:00:00 2001 From: Arun Date: Tue, 29 Apr 2025 11:34:08 +0530 Subject: [PATCH 06/11] revert changes token cuts --- src/app/api/chat/route.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index fddc56fd..26f20dc8 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,7 +1,4 @@ -+1 --11 -Original file line number Diff line number Diff line change -@@ -1,646 +1,636 @@ + import { NextResponse } from "next/server"; //import { anthropic } from "@ai-sdk/anthropic"; From e4118409b61862084824a248a5631db7c1180a17 Mon Sep 17 00:00:00 2001 From: Arun Date: Tue, 29 Apr 2025 18:33:43 +0530 Subject: [PATCH 07/11] swap fix --- package.json | 1 + pnpm-lock.yaml | 10 ++ src/app/api/chat/route.ts | 4 +- src/app/api/chat/systemPrompt.ts | 134 +++++++++++--------- src/components/chat/example-queries.tsx | 3 +- src/components/chat/tools/lifi-widget.tsx | 33 ++--- src/components/shared/sidebar-tool-view.tsx | 4 +- src/components/shared/tool-header-info.tsx | 2 +- src/constants/tools.ts | 2 +- 9 files changed, 104 insertions(+), 89 deletions(-) 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/route.ts b/src/app/api/chat/route.ts index 34faa97e..12889aad 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -381,7 +381,7 @@ export async function POST(req: Request) { "getDesiredChain", "getAmount", "createPerpsOrder", - "getSwapBridgeData", + "swap_or_bridge", "deposit_withdraw_hyperliquid" ]); @@ -490,7 +490,7 @@ export async function POST(req: Request) { .describe("Time in force for limit orders"), }), }), - getSwapBridgeData: tool({ + swap_or_bridge: tool({ description: "Populates swap and/or bridge transaction data for the LiFi widget", parameters: z.object({ diff --git a/src/app/api/chat/systemPrompt.ts b/src/app/api/chat/systemPrompt.ts index 3eb19bd8..a3175ef5 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.** @@ -110,17 +110,15 @@ export const systemPrompt = ( - - {/* --- 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. + 2. **If amount is missing/ambiguous:** Use \`getAmount\` FIRST to get the amount. **PAUSE** if needed. + 3. **Only then:** Call \`swap_or_bridge\` with all *available* details (pass known tokens, amount, and any specified chains). + 4. **The widget handles prompting the user for missing chain information or confirmation.** Do NOT use \`getDesiredChain\` to ask for the chain beforehand if it wasn't mentioned by the user. + 5. Do NOT perform separate balance/allowance checks before calling \`swap_or_bridge\`; the widget handles these steps. @@ -132,8 +130,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 swap/supply). Data dependencies. @@ -144,8 +142,8 @@ 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\`). + 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**). + 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, *especially* for swaps/bridges as per \`swap_or_bridge_handling\`). Parse user amounts correctly per decimal protocol; pad if needed for raw value, reject if too precise. @@ -165,9 +163,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., "Gathering swap data", "Checking your balance"). @@ -210,7 +208,7 @@ 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. @@ -218,24 +216,23 @@ export const systemPrompt = ( 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. Use this *after* confirming amount if it was initially missing. The widget handles chain selection if not provided. - - + + - Ensure chain(s) and amount are provided before calling. Widget handles balance/allowance checks. + Ensure amount is provided before calling (use \`getAmount\` if needed). Widget handles balance/allowance checks and chain selection if not specified. - {/* Removed getTransactionDataForBridge as getSwapBridgeData covers LiFi */} Advanced token stats/market data (price, volume, etc.). @@ -329,14 +326,14 @@ 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. Used after chain confirmation (if needed for non-swap ops) or directly before \`swap_or_bridge\` if amount is missing. @@ -354,24 +351,23 @@ 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). + 2. **Amount Check:** If the amount is missing or ambiguous, use \`getAmount\` to confirm/get the amount. **PAUSE** for user input if needed. + 3. **Tool Call:** Call \`swap_or_bridge\` with the confirmed amount and any other known details (fromToken, toToken, fromChain, toChain). Use '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' for native ETH addresses. Pass known chain IDs if provided by user, otherwise omit/pass null. The widget will handle prompting for missing chains. + 4. **Present Data:** 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 gathering the data for your swap/bridge. The widget will guide you through the next steps, including network confirmation if needed."). (-> 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. 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 +376,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. 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. @@ -512,17 +508,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:** Check Amount (\`getAmount\` if needed) -> \`swap_or_bridge\`. + 3. Explain sequence logic: **Non-swap/bridge:** Check Balance -> Parse Amt (Human -> Raw) -> On Confirm: Final Tx Tool (Raw Amt). **For swaps/bridges:** Confirm Amount -> Gather Swap Data (Widget handles chain) -> Present Widget. + 4. Describe data synthesis (e.g., using balance result, using parsed raw amount, using confirmed amount for swap). 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, missing swap amount). 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 pre-checks for *amount only*). Update workflow sequences if needed. Generate CoT examples for common use cases. @@ -544,9 +540,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 */} @@ -564,10 +560,30 @@ export const systemPrompt = ( 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)** + {/* 3. Get Chain: Widget handles this now. */} + 3. Get Amount: Call \`getAmount\` with tokenSymbol="USDC". Prompt user: "How much USDC would you like to swap?". **PAUSE**. User responds: "1000". Amount confirmed: "1000". + 4. Gather Swap Data: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), amount='1000'. *Do not specify fromChain/toChain*. + 5. Respond: "Okay, I'm gathering the data to swap 1,000 USDC for ETH. The widget will ask you to confirm the network(s) and details..." (Present widget data) **(Add follow-ups)** + + + {/* Swap Example with Chain Specified, Amount Missing */} + Swap USDC for ETH on Base + + 1. Goal: Swap USDC for ETH on Base (ID 8453). + 2. Identify Missing Info: Amount is missing. + 3. Get Amount: Call \`getAmount\` with tokenSymbol="USDC". Prompt user: "How much USDC would you like to swap on Base?". **PAUSE**. User responds: "500". Amount confirmed: "500". + 4. Gather Swap Data: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), fromChain='8453', toChain='8453', amount='500'. + 5. Respond: "Okay, I'm gathering the data to swap 500 USDC for ETH on Base..." (Present widget data) **(Add follow-ups)** + + + {/* Swap Example with Amount Specified, Chain Missing */} + Swap 100 USDC for ETH + + 1. Goal: Swap 100 USDC for ETH. + 2. Identify Missing Info: Chain is missing. + 3. Parse Amount: User input "100". Human amount: "100". + 4. Gather Swap Data: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), amount='100'. *Do not specify fromChain/toChain*. + 5. Respond: "Okay, I'm gathering the data to swap 100 USDC for ETH. The widget will ask you to confirm the network(s) and details..." (Present widget data) **(Add follow-ups)** @@ -587,9 +603,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,17 +623,17 @@ 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:** Amount Check -> 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 for *amount only*). + 4. Foresee potential failure points (parsing, balance, final tx execution, missing swap amount). 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. + 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 (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). 7. Confirm user understanding of risks if applicable (e.g., health factor warning). @@ -634,7 +650,7 @@ 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). Format: \`\\n\\n*Echoes from the Mainframe…:*\\n\` + numbered list 1-4. Content: 2 Sentinel + 2 Morpheus contextual suggestions. Ensure required blank lines before the header. @@ -713,4 +729,4 @@ export const systemPrompt = ( -`; +`; \ No newline at end of file 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..c1e85810 100644 --- a/src/components/chat/tools/lifi-widget.tsx +++ b/src/components/chat/tools/lifi-widget.tsx @@ -1,22 +1,23 @@ import React, { useCallback, useEffect, useRef } from "react"; - -import { - LiFiWidget, - Route, - RouteExecutionUpdate, - WidgetEvent, - useWidgetEvents, -} from "@lifi/widget"; +import { LiFiWidget, Route, RouteExecutionUpdate, WidgetEvent, useWidgetEvents } from "@lifi/widget"; import { CheckCircle, ExternalLink, XCircle } from "lucide-react"; import { Address, formatUnits, zeroAddress } from "viem"; import { arbitrum } from "viem/chains"; import { base } from "viem/chains"; import { useChainId } from "wagmi"; + + import { Chain, chainIdToName, chainNameToChain } from "@/lib/chains"; + + import { useChat } from "@/contexts/chat-context"; + + + + // New component to display bridge completion details export const BridgeCompletedCard = ({ result }: { result: any }) => { let parsedResult; @@ -27,7 +28,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 +46,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 +59,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 +72,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 +83,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { currency: "USD", }) : "N/A"; - return (
@@ -134,7 +131,6 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => { ); } }; - export const SwapBridgeWidget = ({ toolCallId, fromToken, @@ -154,21 +150,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 +173,6 @@ export const SwapBridgeWidget = ({ BigInt(route.toAmount), route.toToken.decimals ); - handleToolResult({ message: toChain === fromChain ? "Swap Completed" : "Bridge Completed", destinationTxLink, @@ -193,7 +184,6 @@ export const SwapBridgeWidget = ({ }, [handleToolResult, toChain, fromChain] ); - const onRouteExecutionFailed = useCallback( (update: RouteExecutionUpdate) => { console.log("🚀 ~ onRouteExecutionFailed ~ update:", update); @@ -204,14 +194,12 @@ export const SwapBridgeWidget = ({ }, [handleToolResult] ); - useEffect(() => { widgetEvents.on( WidgetEvent.RouteExecutionCompleted, onRouteExecutionCompleted ); widgetEvents.on(WidgetEvent.RouteExecutionFailed, onRouteExecutionFailed); - // Cleanup function return () => { widgetEvents.off( @@ -224,7 +212,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, From 541803c8bcc48e09ea093dec0590deef5c28ad9d Mon Sep 17 00:00:00 2001 From: Arunkumar V <137097109+akv2011@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:37:56 +0530 Subject: [PATCH 08/11] Update route.ts --- src/app/api/chat/route.ts | 1406 ++++++++++++++++++++----------------- 1 file changed, 770 insertions(+), 636 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 26f20dc8..07c31538 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,637 +1,771 @@ - 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"; -import { xai } from "@ai-sdk/xai"; -//import { openai } from "@ai-sdk/openai"; -import { EVM, createConfig } from "@lifi/sdk"; -import { SupabaseClient, createClient } from "@supabase/supabase-js"; -import { - experimental_createMCPClient as createMCPClient, - generateText, - streamText, - tool, -} from "ai"; -import { createWalletClient, http } from "viem"; -import type { Chain as vChain } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { base, mode } from "viem/chains"; -import { z } from "zod"; - -import { CHAINS } from "@/lib/chains"; -import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; -import { incrementMessageUsage } from "@/lib/userManager"; - -import { systemPrompt } from "./systemPrompt"; -import { UIMessage } from "./tools/types"; - -const supabaseWrite: SupabaseClient = createClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_KEY! -); - -// 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)}`; - - try { - console.log(`[${requestId}] PUT /api/chat - Chat save request`); - - // Parse the request body - const body = await req.json(); - - // Get needed fields, with fallbacks for multiple naming patterns - const id = body.id; - const walletAddress = body.wallet_address || body.address; - const messages = body.messages; - - console.log( - `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` - ); - - // Validate required fields - if (!id) { - return NextResponse.json( - { - error: "Missing required field: id", - }, - { status: 400 } - ); - } - - if (!walletAddress) { - return NextResponse.json( - { - error: "Missing required field: wallet_address", - }, - { status: 400 } - ); - } - - if (!messages || !Array.isArray(messages) || messages.length === 0) { - console.warn(`[${requestId}] Empty or invalid messages array`); - } - - // Generate a title - let title = "New Conversation"; - try { - title = await generateConversationTitle(messages || []); - } catch (titleError) { - console.log("titleError", titleError); - // Fall back to first message or default - title = - messages && messages.length > 0 - ? messages[0]?.content?.slice(0, 80) || "New Conversation" - : "New Conversation"; - } - - // Extract message content - const assistantMessage = messages?.find( - (m: UIMessage) => m.role === "assistant" - ); - const userMessage = messages?.find((m: UIMessage) => m.role === "user"); - - // 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, - }; - - console.log(`[${requestId}] Save data prepared:`, { - id: saveData.id, - wallet_address: saveData.wallet_address, - title_length: saveData.label.length, - message_count: saveData.messages.length, - }); - - // Save to database (simple upsert) - try { - console.log(`[${requestId}] Executing upsert to saved_chats table...`); - const { error } = await supabaseWrite - .from("saved_chats") - .upsert([saveData], { - onConflict: "id", - }); - - if (error) { - console.error(`[${requestId}] Save error:`, error); - return NextResponse.json( - { - error: `Failed to save chat: ${error.message}`, - details: error, - requestId, - }, - { status: 500 } - ); - } - - console.log(`[${requestId}] Chat saved successfully`); - return NextResponse.json({ - success: true, - message: "Chat saved successfully", - requestId, - }); - } catch (error) { - console.error(`[${requestId}] Database error:`, error); - return NextResponse.json( - { - error: `Database error: ${error instanceof Error ? error.message : String(error)}`, - requestId, - }, - { status: 500 } - ); - } - } catch (error) { - console.error(`[${requestId}] Unhandled exception:`, error); - return NextResponse.json( - { - error: "An unexpected error occurred", - details: error instanceof Error ? error.message : String(error), - requestId, - }, - { status: 500 } - ); - } -} - -// Allow streaming responses up to 30 seconds -export const maxDuration = 120; - -const account = privateKeyToAccount( - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" -); // dummy key - -const chains = [base, mode]; - -const client = createWalletClient({ - account, - chain: base, - transport: http(), -}); - -createConfig({ - integrator: "ionic", - providers: [ - EVM({ - getWalletClient: async () => client, - switchChain: async (chainId: number) => - // Switch chain by creating a new wallet client - createWalletClient({ - account, - chain: chains.find(chain => chain.id == chainId) as vChain, - transport: http(), - }), - }), - ], -}); - -// 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( - (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 - ); - const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, - messages: [ - { - role: "user", - content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, - }, - ], - }); - - const cleanTitle = title - .trim() - .replace(/["'\.]/g, "") - .slice(0, 80); - - const finalTitle = - cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; - - // 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" - ); - } -} - -export async function POST(req: Request) { - try { - const body = await req.json(); - const { messages, address, id, searchType } = body; - - console.log("id", id); - console.log("API Request:", { - messages, - address, - searchType, - }); - - // Check if we have a user address for normal requests - if (!address && searchType !== "morpheus-search") { - return NextResponse.json( - { - error: "User address is required for message quota tracking", - }, - { status: 400 } - ); - } - - // Handle morpheus-Search requests - if (searchType === "morpheus-search") { - if (!messages || messages.length === 0) { - console.error( - `[API Request - ID: ${id}] Morpheus Search error: Received empty messages array.` - ); - return new Response( - JSON.stringify({ - error: "Cannot start Morpheus Search with empty message history.", - }), - { - status: 400, - headers: { - "Content-Type": "application/json", - }, - } - ); - } - - try { - console.log( - `[API Request - ID: ${id}] route.ts: Calling getMorpheusSearchRawStream with FULL message history (${messages.length} messages)...` - ); - const rawStream = await getMorpheusSearchRawStream(messages); - if (rawStream) { - console.log( - `[API Request - ID: ${id}] route.ts: Returning RAW stream obtained from morpheusSearch.ts` - ); - return new Response(rawStream, { - headers: { - "Content-Type": "text/event-stream", - }, - }); - } else { - console.error( - `[API Request - ID: ${id}] route.ts: getMorpheusSearchRawStream returned null/undefined.` - ); - throw new Error( - "getMorpheusSearchRawStream did not return a valid ReadableStream." - ); - } - } catch (error) { - console.error( - `[API Request - ID: ${id}] route.ts: 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" - }`, - }), - { - status: 500, - headers: { - "Content-Type": "application/json", - }, - } - ); - } - } - // For normal chat requests, check quota and proceed (comment ou this section to bypass limit) - try { - // This will throw an error if quota is exceeded - await incrementMessageUsage(supabaseWrite, address); - } catch (error: any) { - if (error.message === "Daily message quota exceeded") { - return NextResponse.json( - { - error: - "You have reached your daily message limit. Please try again tomorrow or upgrade to a premium plan.", - }, - { status: 429 } - ); - } - - console.error("Error checking message quota:", error); - // Continue processing if there's an error with quota checking - // This prevents blocking users if the quota system fails - } - - // Define a simple save function for the initial state - const saveChat = async () => { - try { - // Basic data for save - const title = messages[0]?.content?.slice(0, 80) || "New Conversation"; - const saveData = { - id, - wallet_address: address, - label: title, - prompt: messages[messages.length - 1]?.content || "", - response: "Processing...", - messages: [ - ...messages, - { - id: `temp-${Date.now()}`, - role: "assistant", - content: "Processing your request...", - }, - ], - is_favorite: false, - }; - - await supabaseWrite.from("saved_chats").upsert([saveData], { - onConflict: "id", - }); - } catch (e) { - console.error(`Error in initial save:`, e); - } - }; - - // Try to save the chat at the start to ensure it gets created - await saveChat(); - - const mcpClient = await createMCPClient({ - transport: { - type: "sse", - url: process.env.MATRIX_MCP_URL || "", - }, - }); - - const matrixMcpTools = await mcpClient.tools(); - - console.log("matrixMcpTools", Object.keys(matrixMcpTools)); - - const streamConfig = { - //model: deepseek("deepseek-chat"), - //model: anthropic("claude-3-5-sonnet-latest"), - model: xai("grok-3"), - //model: google('gemini-2.5-pro-exp-03-25'), - //model: anthropic("claude-3-5-haiku-latest"), - //model: google("gemini-2.5-pro-exp-03-25"), - //model: openai.chat("gpt-4o"), - messages, - tools: { - ...matrixMcpTools, - - // 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 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 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"), - }), - }), - }, - async onFinish(finish: { response: { messages: any[] } }) { - await mcpClient.close(); - const { response } = finish; - try { - // Validate required fields - if (!id || !address) { - return; - } - - // Generate title - let title; - try { - title = await generateConversationTitle(messages); - } catch (e) { - console.log("titleError", e); - title = messages[0]?.content?.slice(0, 80) || "New Conversation"; - } - - // Format the response - const formattedResponse = formatResponseToObject(response); - - // Create a placeholder save with just the key fields - const initialSaveData = { - id, - wallet_address: address, - label: title, - prompt: messages[messages.length - 1]?.content || "", - response: formattedResponse.content || "", - is_favorite: false, - }; - - // Save without the messages array - await supabaseWrite.from("saved_chats").upsert([initialSaveData], { - onConflict: "id", - }); - } catch (error) { - console.error("Error in onFinish function:", error); - } - }, - system: systemPrompt(address), - maxSteps: 25, - }; - - // Create the result stream - const resultStream = streamText(streamConfig).toDataStreamResponse(); - - return resultStream; - } catch (error) { - console.error("★★★ CRITICAL API ERROR ★★★", error); - const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred"; - console.log("Returning ERROR RESPONSE:", { error: errorMessage }); - return new Response( - JSON.stringify({ - error: errorMessage, - }), - { - 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) => { - if (item.type === "text") { - return { - type: "text", - text: item.text, - }; - } else if (item.type === "tool-result") { - const invocation = { - type: "tool-invocation", - toolInvocation: { - state: "result", - step: toolInvocationIndex, - toolCallId: item.toolCallId, - toolName: item.toolName, - args: {}, - result: item.result, - }, - }; - toolInvocationIndex++; - return invocation; - } - 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; - }); - - // 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(), - role: "assistant", - content: textContent, - parts, - toolInvocations, - revisionId: Math.random().toString(36).substr(2, 16), - }; -} + + //import { anthropic } from "@ai-sdk/anthropic"; + //import { anthropic } from "@ai-sdk/anthropic"; + //import { deepseek } from "@ai-sdk/deepseek"; + import { google } from "@ai-sdk/google"; + import { xai } from "@ai-sdk/xai"; + //import { openai } from "@ai-sdk/openai"; + import { EVM, createConfig } from "@lifi/sdk"; + import { SupabaseClient, createClient } from "@supabase/supabase-js"; + import { + experimental_createMCPClient as createMCPClient, + generateText, + streamText, + tool, + } from "ai"; + import { createWalletClient, http } from "viem"; + import type { Chain as vChain } from "viem"; + import { privateKeyToAccount } from "viem/accounts"; + import { base, mode } from "viem/chains"; + import { z } from "zod"; + + import { CHAINS } from "@/lib/chains"; + import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; + import { incrementMessageUsage } from "@/lib/userManager"; + + import { systemPrompt } from "./systemPrompt"; + import { UIMessage } from "./tools/types"; + + const supabaseWrite: SupabaseClient = createClient( + process.env.SUPABASE_URL!, + 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; + 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; + return part.text; + } + // Fallback + // 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; + return null; + } + + console.log( + ` - Keeping simplified assistant message (ID: ${message.id})` + ); + return { + id: message.id, + role: message.role, + content: finalContent, + createdAt: message.createdAt, + createdAt: message.createdAt, + parts: undefined, + toolInvocations: undefined, + mode: 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; + }; + + export async function PUT(req: Request) { + const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + try { + console.log(`[${requestId}] PUT /api/chat - Chat save request`); + + // Parse the request body + const body = await req.json(); + + // Get needed fields, with fallbacks for multiple naming patterns + const id = body.id; + const walletAddress = body.wallet_address || body.address; + const messages = body.messages; + + console.log( + `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` + ); + + // Validate required fields + if (!id) { + return NextResponse.json( + { + error: "Missing required field: id", + }, + { status: 400 } + ); + } + + if (!walletAddress) { + return NextResponse.json( + { + error: "Missing required field: wallet_address", + }, + { status: 400 } + ); + } + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + console.warn(`[${requestId}] Empty or invalid messages array`); + } + + // Generate a title + let title = "New Conversation"; + try { + title = await generateConversationTitle(messages || []); + } catch (titleError) { + console.log("titleError", titleError); + // Fall back to first message or default + title = + messages && messages.length > 0 + ? messages[0]?.content?.slice(0, 80) || "New Conversation" + : "New Conversation"; + } + + // Extract message content + const assistantMessage = messages?.find( + (m: UIMessage) => m.role === "assistant" + ); + const userMessage = messages?.find((m: UIMessage) => m.role === "user"); + + // 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, + }; + + console.log(`[${requestId}] Save data prepared:`, { + id: saveData.id, + wallet_address: saveData.wallet_address, + title_length: saveData.label.length, + message_count: saveData.messages.length, + }); + + // Save to database (simple upsert) + try { + console.log(`[${requestId}] Executing upsert to saved_chats table...`); + const { error } = await supabaseWrite + .from("saved_chats") + .upsert([saveData], { + onConflict: "id", + }); + + if (error) { + console.error(`[${requestId}] Save error:`, error); + return NextResponse.json( + { + error: `Failed to save chat: ${error.message}`, + details: error, + requestId, + }, + { status: 500 } + ); + } + + console.log(`[${requestId}] Chat saved successfully`); + return NextResponse.json({ + success: true, + message: "Chat saved successfully", + requestId, + }); + } catch (error) { + console.error(`[${requestId}] Database error:`, error); + return NextResponse.json( + { + error: `Database error: ${error instanceof Error ? error.message : String(error)}`, + requestId, + }, + { status: 500 } + ); + } + } catch (error) { + console.error(`[${requestId}] Unhandled exception:`, error); + return NextResponse.json( + { + error: "An unexpected error occurred", + details: error instanceof Error ? error.message : String(error), + requestId, + }, + { status: 500 } + ); + } + } + + // Allow streaming responses up to 30 seconds + export const maxDuration = 120; + + const account = privateKeyToAccount( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + ); // dummy key + + const chains = [base, mode]; + + const client = createWalletClient({ + account, + chain: base, + transport: http(), + }); + + createConfig({ + integrator: "ionic", + providers: [ + EVM({ + getWalletClient: async () => client, + switchChain: async (chainId: number) => + // Switch chain by creating a new wallet client + createWalletClient({ + account, + chain: chains.find(chain => chain.id == chainId) as vChain, + transport: http(), + }), + }), + ], + }); + + // 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( + (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 + ); + const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, + messages: [ + { + role: "user", + content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, + }, + ], + }); + + const cleanTitle = title + .trim() + .replace(/["'\.]/g, "") + .slice(0, 80); + + const finalTitle = + cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; + + // 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" + ); + } + } + + 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`); + console.log( + `[${postRequestId}] POST /api/chat (MODIFIED) - Request received` + ); + + try { + const body = await req.json(); + const { messages: fullHistory, address, id, searchType } = body; + + console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { + console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { + id: id || "N/A", + address: address || "N/A", + searchType: searchType || "N/A", + messageCount: fullHistory?.length || 0, + }); + + console.log(`[${postRequestId}] (MODIFIED) Filtering message history for LLM...`); + 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 ===` + ); + + // Check address requirement based on mode + if (!address && searchType === "sentinel-mode") { + console.error( + `[${postRequestId}] (MODIFIED) Error: User address is required for Sentinel mode.` + ); + return NextResponse.json( + { error: "User address is required for message quota tracking" }, + { status: 400 } + ); + } + + if (searchType === "morpheus-search") { + console.log(`[${postRequestId}] (MODIFIED) Processing Morpheus Search request...`); + 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" + ) { + console.error( + `[${postRequestId}] (MODIFIED) Morpheus Search error: No valid user message found after filtering.` + ); + return new Response( + JSON.stringify({ + error: "Cannot start Morpheus Search without a valid user message.", + }), + { 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 + ); + + if (rawStream) { + console.log( + `[${postRequestId}] (MODIFIED) Returning RAW stream obtained from morpheusSearch.ts` + ); + return new Response(rawStream, { + headers: { "Content-Type": "text/event-stream" }, + }); + } else { + console.error( + `[${postRequestId}] (MODIFIED) getMorpheusSearchRawStream returned null/undefined.` + ); + throw new Error( + "getMorpheusSearchRawStream did not return a valid ReadableStream." + ); + } + } catch (error) { + console.error( + `[${postRequestId}] (MODIFIED) 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"}`, + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } + } + else if (searchType === "sentinel-mode") { + } else if (searchType === "sentinel-mode") { + console.log( + `[${postRequestId}] (MODIFIED) Processing Sentinel request for address: ${address}` + ); + + // Check quota + try { + await incrementMessageUsage(supabaseWrite, address!); + console.log( + `[${postRequestId}] (MODIFIED) Quota check passed for address: ${address}` + ); + } 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 } + ); + } + console.error( + `[${postRequestId}] (MODIFIED) Error checking message quota (allowing request):`, + error + ); + // 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); + console.error( + `[${postRequestId}] (MODIFIED) Error in initial save:`, + e + ); + } + }; + await saveChat(); + + + console.log( + `[${postRequestId}] (MODIFIED) Preparing MCP client and tools for Sentinel...` + ); + const mcpClient = await createMCPClient({ + transport: { type: "sse", url: process.env.MATRIX_MCP_URL || "" }, + }); + const matrixMcpTools = await mcpClient.tools(); + console.log( + `[${postRequestId}] (MODIFIED) Sentinel - MCP Tools Received:`, + Object.keys(matrixMcpTools) + ); + + 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("..."), + }), + }), + 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("..."), + }), + }), + 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("..."), + }), + }), + 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}...` + ); + 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...` + ); + const finalAssistantMessage = formatResponseToObject(response); // Convert LLM response + console.log( + `[${postRequestId}] (MODIFIED) Formatted final assistant message (ID: ${finalAssistantMessage.id})` + ); + + const messagesForFinalSave = [ + ...(fullHistory || []), + finalAssistantMessage, + ]; + console.log( + `[${postRequestId}] (MODIFIED) Final message count for saving: ${messagesForFinalSave.length}` + ); + + 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 + 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.error( + `[${postRequestId}] (MODIFIED) Error in onFinish save function:`, + error + ); + } + }, + system: systemPrompt(address!), + 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( + `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, + `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, + error + ); + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred"; + console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { + console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { + error: errorMessage, + }); + return new Response(JSON.stringify({ error: errorMessage }), { + 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) => { + if (item.type === "text") { + return { + type: "text", + text: item.text, + }; + } else if (item.type === "tool-result") { + const invocation = { + type: "tool-invocation", + toolInvocation: { + state: "result", + step: toolInvocationIndex, + toolCallId: item.toolCallId, + toolName: item.toolName, + args: {}, + result: item.result, + }, + }; + toolInvocationIndex++; + return invocation; + } + 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; + }); + + // 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(), + role: "assistant", + content: textContent, + parts, + toolInvocations, + revisionId: Math.random().toString(36).substr(2, 16), + }; + } From 144b26c034dd9d60f8b385916b37cd635bc6e2a9 Mon Sep 17 00:00:00 2001 From: Arun Date: Tue, 29 Apr 2025 23:17:50 +0530 Subject: [PATCH 09/11] formating-fix --- src/app/api/chat/route.ts | 342 +++++++++++++--------- src/app/api/chat/systemPrompt.ts | 2 +- src/app/api/chat/tools/types.ts | 1 - src/components/chat/tools/lifi-widget.tsx | 17 +- src/lib/messageUtils.ts | 67 +++-- src/lib/morpheusSearch.ts | 55 +++- 6 files changed, 300 insertions(+), 184 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 12889aad..99547e21 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { filterAndSimplifyHistoryForLLM } from "@/lib/messageUtils"; + //import { anthropic } from "@ai-sdk/anthropic"; //import { deepseek } from "@ai-sdk/deepseek"; import { google } from "@ai-sdk/google"; @@ -8,11 +8,11 @@ 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, tool, - CoreMessage } from "ai"; import { createWalletClient, http } from "viem"; import type { Chain as vChain } from "viem"; @@ -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"; @@ -92,16 +93,19 @@ export async function PUT(req: Request) { const userMessage = messages?.find((m: UIMessage) => m.role === "user"); const assistantMessage = messages ?.filter((m: UIMessage) => m.role === "assistant") - .pop(); + .pop(); const saveData = { id, wallet_address: walletAddress, label: title, prompt: userMessage?.content || "", - response: typeof assistantMessage?.content === 'string' ? assistantMessage.content : "Processing...", + 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, + is_favorite: body.is_favorite !== undefined ? body.is_favorite : false, }; console.log(`[${requestId}] Save data prepared:`, { @@ -207,7 +211,7 @@ async function generateConversationTitle( return titleCache.get(cacheKey)!; } const { text: title } = await generateText({ - model: google("gemini-2.0-flash-001"), + 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: [ { @@ -215,13 +219,10 @@ async function generateConversationTitle( content: `Request: ${firstUserMessage.content.slice(0, 300)}`, }, ], - maxTokens: 20, + maxTokens: 20, }); - const cleanTitle = title - .trim() - .replace(/["'.]/g, "") - .slice(0, 80); + const cleanTitle = title.trim().replace(/["'.]/g, "").slice(0, 80); const finalTitle = cleanTitle || firstUserMessage.content.slice(0, 80); @@ -232,14 +233,13 @@ async function generateConversationTitle( console.error("Title generation failed:", error); // Fallback logic const firstMessageContent = messages[0]?.content; - return typeof firstMessageContent === 'string' - ? firstMessageContent.slice(0, 80).trim() + (firstMessageContent.length > 80 ? "..." : "") + return typeof firstMessageContent === "string" + ? firstMessageContent.slice(0, 80).trim() + + (firstMessageContent.length > 80 ? "..." : "") : "New Conversation"; } } - - export async function POST(req: Request) { const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; let id: string | undefined; @@ -247,7 +247,7 @@ export async function POST(req: Request) { try { const body = await req.json(); const { messages: originalMessages, address, searchType } = body; - id = body.id; + id = body.id; console.log(`[${requestId}] POST /api/chat - Request received`, { id, @@ -259,12 +259,17 @@ export async function POST(req: Request) { // Validate ID early if (!id) { console.error(`[${requestId}] Missing required field: id`); - return NextResponse.json({ error: "Missing required field: id" }, { status: 400 }); + return NextResponse.json( + { error: "Missing required field: id" }, + { status: 400 } + ); } // Check if we have a user address for normal requests if (!address && searchType !== "morpheus-search") { - console.error(`[${requestId}] User address is required for non-Morpheus search`); + console.error( + `[${requestId}] User address is required for non-Morpheus search` + ); return NextResponse.json( { error: "User address is required for message quota tracking", @@ -296,7 +301,9 @@ export async function POST(req: Request) { ); const rawStream = await getMorpheusSearchRawStream(originalMessages); if (rawStream) { - console.log(`[${requestId}] Returning RAW stream from morpheusSearch.`); + console.log( + `[${requestId}] Returning RAW stream from morpheusSearch.` + ); return new Response(rawStream, { headers: { "Content-Type": "text/event-stream" }, }); @@ -342,13 +349,18 @@ export async function POST(req: Request) { { status: 429 } // Too Many Requests ); } - console.error(`[${requestId}] Error checking message quota (continuing anyway):`, error); + console.error( + `[${requestId}] Error checking message quota (continuing anyway):`, + error + ); } const saveChatInitial = async () => { try { const title = await generateConversationTitle(originalMessages || []); - const userMessage = originalMessages?.find((m: UIMessage) => m.role === "user"); + const userMessage = originalMessages?.find( + (m: UIMessage) => m.role === "user" + ); const saveData = { id, wallet_address: address, @@ -359,9 +371,11 @@ export async function POST(req: Request) { is_favorite: false, }; console.log(`[${requestId}] Performing initial save/upsert...`); - const { error } = await supabaseWrite.from("saved_chats").upsert([saveData], { - onConflict: "id", - }); + const { error } = await supabaseWrite + .from("saved_chats") + .upsert([saveData], { + onConflict: "id", + }); if (error) { console.error(`[${requestId}] Error during initial save:`, error); } else { @@ -371,50 +385,66 @@ export async function POST(req: Request) { console.error(`[${requestId}] Exception during initial save:`, e); } }; - await saveChatInitial(); + await saveChatInitial(); - - // Add filter new clietnt side tools here - console.log(`[${requestId}] Sentinel: Original messages before potentially simplifying:`, JSON.stringify(originalMessages, null, 2)); + // 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" + "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 : []; + 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) + ); - 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) ); - - // Fallback - if (!previousTurnUsedClientTool) { - previousTurnUsedClientTool = invocationsToCheck.some((invocation: any) => - invocation && clientToolNames.has(invocation.toolName) - ); - } + } } let messagesToSendToModel: CoreMessage[]; if (previousTurnUsedClientTool) { - console.log(`[${requestId}] Previous assistant turn included a client tool result. Using original messages.`); - messagesToSendToModel = originalMessages as CoreMessage[]; + console.log( + `[${requestId}] Previous assistant turn included a client tool result. Using original messages.` + ); + 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)); + 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; @@ -427,9 +457,15 @@ export async function POST(req: Request) { }, }); matrixMcpTools = await mcpClient.tools(); - console.log(`[${requestId}] Matrix MCP Tools loaded:`, Object.keys(matrixMcpTools)); + console.log( + `[${requestId}] Matrix MCP Tools loaded:`, + Object.keys(matrixMcpTools) + ); } catch (mcpError) { - console.error(`[${requestId}] Failed to initialize Matrix MCP Client or load tools:`, mcpError); + console.error( + `[${requestId}] Failed to initialize Matrix MCP Client or load tools:`, + mcpError + ); } const streamConfig = { @@ -532,44 +568,65 @@ export async function POST(req: Request) { }), }), }, - 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); + 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(); - console.log(`[${requestId}] MCP Client closed.`); + await mcpClient.close(); + 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.`); + console.error( + `[${requestId}] Missing id or address in onFinish. Aborting final save.` + ); 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"; + 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 + ...(originalMessages || []), + finalAssistantMessage, ]; const finalSaveData = { id, wallet_address: address, label: title, - prompt: originalMessages?.find((m: UIMessage) => m.role === 'user')?.content || "", - response: finalAssistantMessage.content || "", + prompt: + originalMessages?.find((m: UIMessage) => m.role === "user") + ?.content || "", + response: finalAssistantMessage.content || "", messages: finalMessagesToSave, is_favorite: false, }; - console.log(`[${requestId}] Preparing final save data. Message count: ${finalMessagesToSave.length}`); + console.log( + `[${requestId}] Preparing final save data. Message count: ${finalMessagesToSave.length}` + ); const { error: finalSaveError } = await supabaseWrite .from("saved_chats") .upsert([finalSaveData], { @@ -577,35 +634,51 @@ export async function POST(req: Request) { }); if (finalSaveError) { - console.error(`[${requestId}] Error during final save in onFinish:`, finalSaveError); + console.error( + `[${requestId}] Error during final save in onFinish:`, + finalSaveError + ); } else { console.log(`[${requestId}] Final save successful in onFinish.`); } - } catch (error) { - console.error(`[${requestId}] Error processing in onFinish function:`, error); + console.error( + `[${requestId}] Error processing in onFinish function:`, + error + ); } }, system: systemPrompt(address), - maxSteps: 25, + maxSteps: 25, // temperature: 0.7, }; - console.log(`[${requestId}] Starting streamText with Sentinel model: ${streamConfig.model.modelId}`); + console.log( + `[${requestId}] Starting streamText with Sentinel model: ${streamConfig.model.modelId}` + ); const resultStream = await streamText(streamConfig); return resultStream.toDataStreamResponse(); - } catch (error) { - console.error(`[${requestId || 'req-unknown'}] ★★★ CRITICAL API ERROR in POST /api/chat ★★★`, error); + console.error( + `[${requestId || "req-unknown"}] ★★★ CRITICAL API ERROR in POST /api/chat ★★★`, + error + ); const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred in the chat API."; + 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.error( + `[${requestId || "req-unknown"}] Error Stack Trace:`, + error.stack + ); } - console.log(`[${requestId || 'req-unknown'}] Returning ERROR RESPONSE:`, { error: errorMessage }); + console.log(`[${requestId || "req-unknown"}] Returning ERROR RESPONSE:`, { + error: errorMessage, + }); return new Response( JSON.stringify({ error: errorMessage, - requestId: requestId || 'req-unknown', + requestId: requestId || "req-unknown", }), { status: 500, @@ -617,63 +690,66 @@ export async function POST(req: Request) { } } 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 }] - }; + 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') { - textContent += item.text; - parts.push({ type: 'text', text: item.text }); - } else if (item.type === 'tool-call') { - } else if (item.type === 'tool-result') { - parts.push({ - type: 'tool-invocation', - toolInvocation: { - state: 'result', - toolCallId: item.toolCallId, - toolName: item.toolName, - args: item.args || {}, - result: item.result, - } - }); - toolInvocations.push({ - state: 'result', - // step: ??? - toolCallId: item.toolCallId, - toolName: item.toolName, - args: item.args || {}, - result: item.result, - }); - } + let textContent = ""; + const parts: any[] = []; + const toolInvocations: any[] = []; + + if (Array.isArray(assistantMsg.content)) { + assistantMsg.content.forEach((item: any) => { + if (item.type === "text") { + textContent += item.text; + parts.push({ type: "text", text: item.text }); + } else if (item.type === "tool-call") { + } else if (item.type === "tool-result") { + parts.push({ + type: "tool-invocation", + toolInvocation: { + state: "result", + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + }, }); - } else if (typeof assistantMsg.content === 'string') { - // Handle plain string content - textContent = assistantMsg.content; - parts.push({ type: 'text', text: textContent }); - } + toolInvocations.push({ + state: "result", + // step: ??? + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + }); + } + }); + } else if (typeof assistantMsg.content === "string") { + // Handle plain string content + textContent = assistantMsg.content; + parts.push({ type: "text", text: textContent }); + } - return { - id: assistantMsg.id || `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, - createdAt: assistantMsg.createdAt || new Date().toISOString(), - role: 'assistant', - content: textContent, - parts, - toolInvocations, - revisionId: Math.random().toString(36).substr(2, 16), - }; - -} \ No newline at end of file + return { + id: + assistantMsg.id || + `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + createdAt: assistantMsg.createdAt || new Date().toISOString(), + role: "assistant", + content: textContent, + parts, + toolInvocations, + revisionId: Math.random().toString(36).substr(2, 16), + }; +} diff --git a/src/app/api/chat/systemPrompt.ts b/src/app/api/chat/systemPrompt.ts index a3175ef5..bb998f20 100644 --- a/src/app/api/chat/systemPrompt.ts +++ b/src/app/api/chat/systemPrompt.ts @@ -729,4 +729,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 b8c79195..654c18f8 100644 --- a/src/app/api/chat/tools/types.ts +++ b/src/app/api/chat/tools/types.ts @@ -208,4 +208,3 @@ export type UIMessage = Message & { mode?: "morpheus" | "sentinel"; revisionId?: string; }; - diff --git a/src/components/chat/tools/lifi-widget.tsx b/src/components/chat/tools/lifi-widget.tsx index c1e85810..4440dc1f 100644 --- a/src/components/chat/tools/lifi-widget.tsx +++ b/src/components/chat/tools/lifi-widget.tsx @@ -1,23 +1,22 @@ import React, { useCallback, useEffect, useRef } from "react"; -import { LiFiWidget, Route, RouteExecutionUpdate, WidgetEvent, useWidgetEvents } from "@lifi/widget"; + +import { + LiFiWidget, + Route, + RouteExecutionUpdate, + WidgetEvent, + useWidgetEvents, +} from "@lifi/widget"; import { CheckCircle, ExternalLink, XCircle } from "lucide-react"; import { Address, formatUnits, zeroAddress } from "viem"; import { arbitrum } from "viem/chains"; import { base } from "viem/chains"; import { useChainId } from "wagmi"; - - import { Chain, chainIdToName, chainNameToChain } from "@/lib/chains"; - - import { useChat } from "@/contexts/chat-context"; - - - - // New component to display bridge completion details export const BridgeCompletedCard = ({ result }: { result: any }) => { let parsedResult; diff --git a/src/lib/messageUtils.ts b/src/lib/messageUtils.ts index d3031c9a..012f1f49 100644 --- a/src/lib/messageUtils.ts +++ b/src/lib/messageUtils.ts @@ -1,50 +1,65 @@ -import { CoreMessage } from 'ai'; -import { UIMessage } from '@/app/api/chat/tools/types'; +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}`); + 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') { + 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'); + .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() }); + simplifiedHistory.push({ role: "user", content: userText.trim() }); } - } else if (message.role === 'assistant') { - let assistantText = ''; - if (typeof message.content === 'string') { + } 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(''); + .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() }); + simplifiedHistory.push({ + role: "assistant", + content: assistantText.trim(), + }); } } } - console.log(`<<< Simplified history complete. New count: ${simplifiedHistory.length}`); + 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; -} \ No newline at end of file +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 bc5417ba..5df788b1 100644 --- a/src/lib/morpheusSearch.ts +++ b/src/lib/morpheusSearch.ts @@ -1,18 +1,18 @@ // 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, tool, - CoreMessage // Import CoreMessage for the simplified history type } from "ai"; 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 the NEW filter function from your utility file import { filterAndSimplifyHistoryForLLM } from "./messageUtils"; @@ -36,9 +36,16 @@ 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)); + 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 @@ -81,7 +88,10 @@ async function getMorpheusSearchRawStream( // Add other MCP tool schemas specific to Morpheus if needed }, }); - console.log(`[${requestId}] Morpheus MCP tools loaded:`, 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({ @@ -107,7 +117,10 @@ async function getMorpheusSearchRawStream( console.log( `[${neoSearchRequestId}] 🔍 NeoSearch execute FUNCTION IS BEING CALLED! (Using generateText internally)` ); - console.log(`[${neoSearchRequestId}] NeoSearch - Search query:`, searchQuery); + console.log( + `[${neoSearchRequestId}] NeoSearch - Search query:`, + searchQuery + ); try { // Use the separate search-enabled model for this tool @@ -120,7 +133,10 @@ async function getMorpheusSearchRawStream( const metadata = searchResponse.providerMetadata; const googleMetadata = metadata?.google; - console.log(`[${neoSearchRequestId}] NeoSearch successful for query:`, searchQuery); + console.log( + `[${neoSearchRequestId}] NeoSearch successful for query:`, + searchQuery + ); return { searchResults: text, sources: googleMetadata?.sources || [], @@ -144,10 +160,13 @@ async function getMorpheusSearchRawStream( }, temperature: 0.2, maxSteps: 25, // Adjust as needed - onFinish: async (finishArgs: { finishReason: string; usage: object; }) => { // Added type annotation + onFinish: async (finishArgs: { finishReason: string; usage: object }) => { + // Added type annotation // Ensure MCP client is closed when the stream finishes await mcpClient.close(); - console.log(`[${requestId}] Morpheus-Search: stream finished, MCP client closed. Reason: ${finishArgs.finishReason}`); + console.log( + `[${requestId}] Morpheus-Search: stream finished, MCP client closed. Reason: ${finishArgs.finishReason}` + ); }, }); @@ -156,14 +175,15 @@ async function getMorpheusSearchRawStream( if (!rawStream) { // This case should ideally not happen if streamText succeeds - throw new Error("[${requestId}] streamText did not return a ReadableStream body."); + throw new Error( + "[${requestId}] streamText did not return a ReadableStream body." + ); } console.log( `[${requestId}] Morpheus-Search: Successfully obtained raw stream.` ); return rawStream; - } catch (error: unknown) { // Ensure MCP client is closed in case of an error during setup or execution console.error( @@ -171,10 +191,17 @@ async function getMorpheusSearchRawStream( 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)); + 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)); } } -export { getMorpheusSearchRawStream }; \ No newline at end of file +export { getMorpheusSearchRawStream }; From 03f73969f1fb6e8c31a51fd20b6dfa855b48f1c3 Mon Sep 17 00:00:00 2001 From: Arunkumar V <137097109+akv2011@users.noreply.github.com> Date: Tue, 29 Apr 2025 23:18:50 +0530 Subject: [PATCH 10/11] Update route.ts --- src/app/api/chat/route.ts | 1524 ++++++++++++++++++------------------- 1 file changed, 754 insertions(+), 770 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 07c31538..99547e21 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,771 +1,755 @@ 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"; - import { xai } from "@ai-sdk/xai"; - //import { openai } from "@ai-sdk/openai"; - import { EVM, createConfig } from "@lifi/sdk"; - import { SupabaseClient, createClient } from "@supabase/supabase-js"; - import { - experimental_createMCPClient as createMCPClient, - generateText, - streamText, - tool, - } from "ai"; - import { createWalletClient, http } from "viem"; - import type { Chain as vChain } from "viem"; - import { privateKeyToAccount } from "viem/accounts"; - import { base, mode } from "viem/chains"; - import { z } from "zod"; - - import { CHAINS } from "@/lib/chains"; - import { getMorpheusSearchRawStream } from "@/lib/morpheusSearch"; - import { incrementMessageUsage } from "@/lib/userManager"; - - import { systemPrompt } from "./systemPrompt"; - import { UIMessage } from "./tools/types"; - - const supabaseWrite: SupabaseClient = createClient( - process.env.SUPABASE_URL!, - 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; - 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; - return part.text; - } - // Fallback - // 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; - return null; - } - - console.log( - ` - Keeping simplified assistant message (ID: ${message.id})` - ); - return { - id: message.id, - role: message.role, - content: finalContent, - createdAt: message.createdAt, - createdAt: message.createdAt, - parts: undefined, - toolInvocations: undefined, - mode: 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; - }; - - export async function PUT(req: Request) { - const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - - try { - console.log(`[${requestId}] PUT /api/chat - Chat save request`); - - // Parse the request body - const body = await req.json(); - - // Get needed fields, with fallbacks for multiple naming patterns - const id = body.id; - const walletAddress = body.wallet_address || body.address; - const messages = body.messages; - - console.log( - `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` - ); - - // Validate required fields - if (!id) { - return NextResponse.json( - { - error: "Missing required field: id", - }, - { status: 400 } - ); - } - - if (!walletAddress) { - return NextResponse.json( - { - error: "Missing required field: wallet_address", - }, - { status: 400 } - ); - } - - if (!messages || !Array.isArray(messages) || messages.length === 0) { - console.warn(`[${requestId}] Empty or invalid messages array`); - } - - // Generate a title - let title = "New Conversation"; - try { - title = await generateConversationTitle(messages || []); - } catch (titleError) { - console.log("titleError", titleError); - // Fall back to first message or default - title = - messages && messages.length > 0 - ? messages[0]?.content?.slice(0, 80) || "New Conversation" - : "New Conversation"; - } - - // Extract message content - const assistantMessage = messages?.find( - (m: UIMessage) => m.role === "assistant" - ); - const userMessage = messages?.find((m: UIMessage) => m.role === "user"); - - // 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, - }; - - console.log(`[${requestId}] Save data prepared:`, { - id: saveData.id, - wallet_address: saveData.wallet_address, - title_length: saveData.label.length, - message_count: saveData.messages.length, - }); - - // Save to database (simple upsert) - try { - console.log(`[${requestId}] Executing upsert to saved_chats table...`); - const { error } = await supabaseWrite - .from("saved_chats") - .upsert([saveData], { - onConflict: "id", - }); - - if (error) { - console.error(`[${requestId}] Save error:`, error); - return NextResponse.json( - { - error: `Failed to save chat: ${error.message}`, - details: error, - requestId, - }, - { status: 500 } - ); - } - - console.log(`[${requestId}] Chat saved successfully`); - return NextResponse.json({ - success: true, - message: "Chat saved successfully", - requestId, - }); - } catch (error) { - console.error(`[${requestId}] Database error:`, error); - return NextResponse.json( - { - error: `Database error: ${error instanceof Error ? error.message : String(error)}`, - requestId, - }, - { status: 500 } - ); - } - } catch (error) { - console.error(`[${requestId}] Unhandled exception:`, error); - return NextResponse.json( - { - error: "An unexpected error occurred", - details: error instanceof Error ? error.message : String(error), - requestId, - }, - { status: 500 } - ); - } - } - - // Allow streaming responses up to 30 seconds - export const maxDuration = 120; - - const account = privateKeyToAccount( - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - ); // dummy key - - const chains = [base, mode]; - - const client = createWalletClient({ - account, - chain: base, - transport: http(), - }); - - createConfig({ - integrator: "ionic", - providers: [ - EVM({ - getWalletClient: async () => client, - switchChain: async (chainId: number) => - // Switch chain by creating a new wallet client - createWalletClient({ - account, - chain: chains.find(chain => chain.id == chainId) as vChain, - transport: http(), - }), - }), - ], - }); - - // 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( - (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 - ); - const cacheKey = `${firstMessage?.content.slice(0, 100) ?? ""}|${firstMessage?.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.`, - messages: [ - { - role: "user", - content: `Request: ${firstMessage?.content.slice(0, 300) || "New Conversation"}`, - }, - ], - }); - - const cleanTitle = title - .trim() - .replace(/["'\.]/g, "") - .slice(0, 80); - - const finalTitle = - cleanTitle || firstMessage?.content.slice(0, 80) || "New Conversation"; - - // 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" - ); - } - } - - 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`); - console.log( - `[${postRequestId}] POST /api/chat (MODIFIED) - Request received` - ); - - try { - const body = await req.json(); - const { messages: fullHistory, address, id, searchType } = body; - - console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { - console.log(`[${postRequestId}] (MODIFIED) Request Details:`, { - id: id || "N/A", - address: address || "N/A", - searchType: searchType || "N/A", - messageCount: fullHistory?.length || 0, - }); - - console.log(`[${postRequestId}] (MODIFIED) Filtering message history for LLM...`); - 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 ===` - ); - - // Check address requirement based on mode - if (!address && searchType === "sentinel-mode") { - console.error( - `[${postRequestId}] (MODIFIED) Error: User address is required for Sentinel mode.` - ); - return NextResponse.json( - { error: "User address is required for message quota tracking" }, - { status: 400 } - ); - } - - if (searchType === "morpheus-search") { - console.log(`[${postRequestId}] (MODIFIED) Processing Morpheus Search request...`); - 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" - ) { - console.error( - `[${postRequestId}] (MODIFIED) Morpheus Search error: No valid user message found after filtering.` - ); - return new Response( - JSON.stringify({ - error: "Cannot start Morpheus Search without a valid user message.", - }), - { 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 - ); - - if (rawStream) { - console.log( - `[${postRequestId}] (MODIFIED) Returning RAW stream obtained from morpheusSearch.ts` - ); - return new Response(rawStream, { - headers: { "Content-Type": "text/event-stream" }, - }); - } else { - console.error( - `[${postRequestId}] (MODIFIED) getMorpheusSearchRawStream returned null/undefined.` - ); - throw new Error( - "getMorpheusSearchRawStream did not return a valid ReadableStream." - ); - } - } catch (error) { - console.error( - `[${postRequestId}] (MODIFIED) 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"}`, - }), - { status: 500, headers: { "Content-Type": "application/json" } } - ); - } - } - else if (searchType === "sentinel-mode") { - } else if (searchType === "sentinel-mode") { - console.log( - `[${postRequestId}] (MODIFIED) Processing Sentinel request for address: ${address}` - ); - - // Check quota - try { - await incrementMessageUsage(supabaseWrite, address!); - console.log( - `[${postRequestId}] (MODIFIED) Quota check passed for address: ${address}` - ); - } 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 } - ); - } - console.error( - `[${postRequestId}] (MODIFIED) Error checking message quota (allowing request):`, - error - ); - // 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); - console.error( - `[${postRequestId}] (MODIFIED) Error in initial save:`, - e - ); - } - }; - await saveChat(); - - - console.log( - `[${postRequestId}] (MODIFIED) Preparing MCP client and tools for Sentinel...` - ); - const mcpClient = await createMCPClient({ - transport: { type: "sse", url: process.env.MATRIX_MCP_URL || "" }, - }); - const matrixMcpTools = await mcpClient.tools(); - console.log( - `[${postRequestId}] (MODIFIED) Sentinel - MCP Tools Received:`, - Object.keys(matrixMcpTools) - ); - - 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("..."), - }), - }), - 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("..."), - }), - }), - 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("..."), - }), - }), - 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}...` - ); - 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...` - ); - const finalAssistantMessage = formatResponseToObject(response); // Convert LLM response - console.log( - `[${postRequestId}] (MODIFIED) Formatted final assistant message (ID: ${finalAssistantMessage.id})` - ); - - const messagesForFinalSave = [ - ...(fullHistory || []), - finalAssistantMessage, - ]; - console.log( - `[${postRequestId}] (MODIFIED) Final message count for saving: ${messagesForFinalSave.length}` - ); - - 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 - 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.error( - `[${postRequestId}] (MODIFIED) Error in onFinish save function:`, - error - ); - } - }, - system: systemPrompt(address!), - 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( - `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, - `[${postRequestId}] ★★★ CRITICAL API ERROR in POST (MODIFIED) ★★★`, - error - ); - const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred"; - console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { - console.log(`[${postRequestId}] (MODIFIED) Returning ERROR RESPONSE:`, { - error: errorMessage, - }); - return new Response(JSON.stringify({ error: errorMessage }), { - 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) => { - if (item.type === "text") { - return { - type: "text", - text: item.text, - }; - } else if (item.type === "tool-result") { - const invocation = { - type: "tool-invocation", - toolInvocation: { - state: "result", - step: toolInvocationIndex, - toolCallId: item.toolCallId, - toolName: item.toolName, - args: {}, - result: item.result, - }, - }; - toolInvocationIndex++; - return invocation; - } - 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; - }); - - // 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(), - role: "assistant", - content: textContent, - parts, - toolInvocations, - revisionId: Math.random().toString(36).substr(2, 16), - }; - } + +//import { anthropic } from "@ai-sdk/anthropic"; +//import { deepseek } from "@ai-sdk/deepseek"; +import { google } from "@ai-sdk/google"; +import { xai } from "@ai-sdk/xai"; +//import { openai } from "@ai-sdk/openai"; +import { EVM, createConfig } from "@lifi/sdk"; +import { SupabaseClient, createClient } from "@supabase/supabase-js"; +import { + CoreMessage, + experimental_createMCPClient as createMCPClient, + generateText, + streamText, + tool, +} from "ai"; +import { createWalletClient, http } from "viem"; +import type { Chain as vChain } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +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"; + +import { systemPrompt } from "./systemPrompt"; +import { UIMessage } from "./tools/types"; + +const supabaseWrite: SupabaseClient = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_KEY! +); + +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)}`; + + try { + console.log(`[${requestId}] PUT /api/chat - Chat save request`); + + // Parse the request body + const body = await req.json(); + + // Get needed fields, with fallbacks for multiple naming patterns + const id = body.id; + const walletAddress = body.wallet_address || body.address; + const messages = body.messages; + + console.log( + `[${requestId}] Save request for chat id: ${id}, wallet: ${walletAddress}, msg count: ${messages?.length}` + ); + + // Validate required fields + if (!id) { + return NextResponse.json( + { + error: "Missing required field: id", + }, + { status: 400 } + ); + } + + if (!walletAddress) { + return NextResponse.json( + { + error: "Missing required field: wallet_address", + }, + { status: 400 } + ); + } + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + console.warn(`[${requestId}] Empty or invalid messages array`); + } + + // Generate a title + let title = "New Conversation"; + try { + title = await generateConversationTitle(messages || []); + } catch (titleError) { + console.log(`[${requestId}] Title generation error:`, titleError); + // Fall back to first message or default + title = + messages && messages.length > 0 + ? messages[0]?.content?.slice(0, 80) || "New Conversation" + : "New Conversation"; + } + + // 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(); + + const saveData = { + id, + wallet_address: walletAddress, + label: title, + prompt: userMessage?.content || "", + 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:`, { + id: saveData.id, + wallet_address: saveData.wallet_address, + title_length: saveData.label.length, + message_count: saveData.messages.length, + is_favorite: saveData.is_favorite, + }); + + try { + console.log(`[${requestId}] Executing upsert to saved_chats table...`); + const { error } = await supabaseWrite + .from("saved_chats") + .upsert([saveData], { + onConflict: "id", + }); + + if (error) { + console.error(`[${requestId}] Save error:`, error); + return NextResponse.json( + { + error: `Failed to save chat: ${error.message}`, + details: error, + requestId, + }, + { status: 500 } + ); + } + + console.log(`[${requestId}] Chat saved successfully`); + return NextResponse.json({ + success: true, + message: "Chat saved successfully", + requestId, + }); + } catch (error) { + console.error(`[${requestId}] Database error:`, error); + return NextResponse.json( + { + error: `Database error: ${error instanceof Error ? error.message : String(error)}`, + requestId, + }, + { status: 500 } + ); + } + } catch (error) { + console.error(`[${requestId}] Unhandled exception in PUT:`, error); + return NextResponse.json( + { + error: "An unexpected error occurred during save", + details: error instanceof Error ? error.message : String(error), + requestId, + }, + { status: 500 } + ); + } +} + +export const maxDuration = 120; + +const account = privateKeyToAccount( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +); // dummy key +const chains = [base, mode]; // Add other chains if needed + +const client = createWalletClient({ + account, + chain: base, + transport: http(), +}); + +createConfig({ + integrator: "ionic", + providers: [ + EVM({ + getWalletClient: async () => client, + switchChain: async (chainId: number) => + // Switch chain by creating a new wallet client + createWalletClient({ + account, + chain: chains.find(chain => chain.id == chainId) as vChain, + transport: http(), + }), + }), + ], +}); + +const titleCache = new Map(); + +// Title Generation based on the First message +async function generateConversationTitle( + messages: UIMessage[] +): Promise { + try { + const firstUserMessage = messages.find( + (m: UIMessage) => m.role === "user" && m.content?.trim().length > 0 + ); + 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: 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: ${firstUserMessage.content.slice(0, 300)}`, + }, + ], + maxTokens: 20, + }); + + const cleanTitle = title.trim().replace(/["'.]/g, "").slice(0, 80); + + 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); + // 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 requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + let id: string | undefined; + + try { + const body = await req.json(); + const { messages: originalMessages, address, searchType } = body; + id = body.id; + + console.log(`[${requestId}] POST /api/chat - Request received`, { + id, + address, + searchType, + messageCount: originalMessages?.length, + }); + + // Validate ID early + if (!id) { + console.error(`[${requestId}] Missing required field: id`); + return NextResponse.json( + { error: "Missing required field: id" }, + { status: 400 } + ); + } + + // Check if we have a user address for normal requests + if (!address && searchType !== "morpheus-search") { + console.error( + `[${requestId}] User address is required for non-Morpheus search` + ); + return NextResponse.json( + { + error: "User address is required for message quota tracking", + }, + { status: 400 } + ); + } + + // Handle morpheus-Search requests + if (searchType === "morpheus-search") { + if (!originalMessages || originalMessages.length === 0) { + console.error( + `[${requestId}] Morpheus Search error: Received empty messages array.` + ); + return new Response( + JSON.stringify({ + error: "Cannot start Morpheus Search with empty message history.", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + try { + console.log( + `[${requestId}] Calling getMorpheusSearchRawStream with ${originalMessages.length} messages...` + ); + const rawStream = await getMorpheusSearchRawStream(originalMessages); + if (rawStream) { + console.log( + `[${requestId}] Returning RAW stream from morpheusSearch.` + ); + return new Response(rawStream, { + headers: { "Content-Type": "text/event-stream" }, + }); + } else { + console.error( + `[${requestId}] getMorpheusSearchRawStream returned null/undefined.` + ); + throw new Error( + "getMorpheusSearchRawStream did not return a valid ReadableStream." + ); + } + } catch (error) { + console.error( + `[${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" + }`, + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } + } + + // --- 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 + ); + } + + const saveChatInitial = async () => { + try { + const title = await generateConversationTitle(originalMessages || []); + const userMessage = originalMessages?.find( + (m: UIMessage) => m.role === "user" + ); + 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.`); + } + } 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) + ); + } + } + + let messagesToSendToModel: CoreMessage[]; + + if (previousTurnUsedClientTool) { + console.log( + `[${requestId}] Previous assistant turn included a client tool result. Using original messages.` + ); + 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 || "", + }, + }); + matrixMcpTools = await mcpClient.tools(); + console.log( + `[${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: 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 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"), + }), + }), + 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"), + }), + }), + }, + 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(); + 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.` + ); + 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 finalSaveData = { + id, + wallet_address: address, + label: title, + prompt: + originalMessages?.find((m: UIMessage) => m.role === "user") + ?.content || "", + response: finalAssistantMessage.content || "", + messages: finalMessagesToSave, + is_favorite: false, + }; + + 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( + `[${requestId}] Error during final save in onFinish:`, + finalSaveError + ); + } else { + console.log(`[${requestId}] Final save successful in onFinish.`); + } + } 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( + `[${requestId || "req-unknown"}] ★★★ CRITICAL API ERROR in POST /api/chat ★★★`, + error + ); + const errorMessage = + 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, + requestId: requestId || "req-unknown", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } +} +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") { + textContent += item.text; + parts.push({ type: "text", text: item.text }); + } else if (item.type === "tool-call") { + } else if (item.type === "tool-result") { + parts.push({ + type: "tool-invocation", + toolInvocation: { + state: "result", + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + }, + }); + toolInvocations.push({ + state: "result", + // step: ??? + toolCallId: item.toolCallId, + toolName: item.toolName, + args: item.args || {}, + result: item.result, + }); + } + }); + } else if (typeof assistantMsg.content === "string") { + // Handle plain string content + textContent = assistantMsg.content; + parts.push({ type: "text", text: textContent }); + } + + return { + id: + assistantMsg.id || + `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + createdAt: assistantMsg.createdAt || new Date().toISOString(), + role: "assistant", + content: textContent, + parts, + toolInvocations, + revisionId: Math.random().toString(36).substr(2, 16), + }; +} From cd8427a923cbb6c13612af30cf00e0afaa13cb68 Mon Sep 17 00:00:00 2001 From: Arun Date: Tue, 29 Apr 2025 23:37:17 +0530 Subject: [PATCH 11/11] amount fix --- src/app/api/chat/systemPrompt.ts | 123 +++++++++++++++---------------- src/lib/morpheusSearch.ts | 3 +- 2 files changed, 61 insertions(+), 65 deletions(-) diff --git a/src/app/api/chat/systemPrompt.ts b/src/app/api/chat/systemPrompt.ts index bb998f20..398d1caa 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 when known or confirmed + 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.** @@ -114,11 +114,10 @@ export const systemPrompt = ( 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 amount is missing/ambiguous:** Use \`getAmount\` FIRST to get the amount. **PAUSE** if needed. - 3. **Only then:** Call \`swap_or_bridge\` with all *available* details (pass known tokens, amount, and any specified chains). - 4. **The widget handles prompting the user for missing chain information or confirmation.** Do NOT use \`getDesiredChain\` to ask for the chain beforehand if it wasn't mentioned by the user. - 5. Do NOT perform separate balance/allowance checks before calling \`swap_or_bridge\`; 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. @@ -130,8 +129,8 @@ export const systemPrompt = ( Operational vs Analytical Intent. Morpheus query check. Tool needs (single/multi). - 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 swap/supply). + 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. @@ -143,8 +142,9 @@ export const systemPrompt = ( Tool selection & sequencing 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**). - 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, *especially* for swaps/bridges as per \`swap_or_bridge_handling\`). + 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,7 +165,7 @@ export const systemPrompt = ( Format clearly. **Communicate state clearly:** Use bullets ('-', '•'), not numbered lists (except final 4 suggestions). - **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., "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"). @@ -208,30 +208,30 @@ export const systemPrompt = ( - 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. - + 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 (non-swap/bridge). + 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 amount if it was initially missing. The widget handles chain selection if not provided. + 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 amount is provided before calling (use \`getAmount\` if needed). Widget handles balance/allowance checks and chain selection if not specified. + Widget handles balance/allowance checks, chain selection if not specified, and prompting for amount if not provided. Advanced token stats/market data (price, volume, etc.). @@ -326,20 +326,20 @@ export const systemPrompt = ( - 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). + 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 (if needed for non-swap ops) or directly before \`swap_or_bridge\` 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). @@ -351,23 +351,22 @@ export const systemPrompt = ( 1. Check context for chain. - 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\`. + 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. **Amount Check:** If the amount is missing or ambiguous, use \`getAmount\` to confirm/get the amount. **PAUSE** for user input if needed. - 3. **Tool Call:** Call \`swap_or_bridge\` with the confirmed amount and any other known details (fromToken, toToken, fromChain, toChain). Use '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' for native ETH addresses. Pass known chain IDs if provided by user, otherwise omit/pass null. The widget will handle prompting for missing chains. - 4. **Present Data:** 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 gathering the data for your swap/bridge. The widget will guide you through the next steps, including network confirmation if needed."). (-> 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) - Excludes Swaps/Bridges */} 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. + 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**. @@ -377,7 +376,7 @@ export const systemPrompt = ( 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. Determine raw amount (pad). + 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. @@ -392,9 +391,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. @@ -424,7 +423,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. @@ -509,16 +508,16 @@ export const systemPrompt = ( Engage CoT for multi-tool requests, especially spending operations. 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:** Check Amount (\`getAmount\` if needed) -> \`swap_or_bridge\`. - 3. Explain sequence logic: **Non-swap/bridge:** Check Balance -> Parse Amt (Human -> Raw) -> On Confirm: Final Tx Tool (Raw Amt). **For swaps/bridges:** Confirm Amount -> Gather Swap Data (Widget handles chain) -> Present Widget. - 4. Describe data synthesis (e.g., using balance result, using parsed raw amount, using confirmed amount for swap). + 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, tx failure, missing swap amount). + 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 for *amount only*). + 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. @@ -555,35 +554,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: Widget handles this now. */} - 3. Get Amount: Call \`getAmount\` with tokenSymbol="USDC". Prompt user: "How much USDC would you like to swap?". **PAUSE**. User responds: "1000". Amount confirmed: "1000". - 4. Gather Swap Data: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), amount='1000'. *Do not specify fromChain/toChain*. - 5. Respond: "Okay, I'm gathering the data to swap 1,000 USDC for ETH. The widget will ask you to confirm the network(s) and details..." (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 */} + {/* 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 Missing Info: Amount is missing. - 3. Get Amount: Call \`getAmount\` with tokenSymbol="USDC". Prompt user: "How much USDC would you like to swap on Base?". **PAUSE**. User responds: "500". Amount confirmed: "500". - 4. Gather Swap Data: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), fromChain='8453', toChain='8453', amount='500'. - 5. Respond: "Okay, I'm gathering the data to swap 500 USDC for ETH on Base..." (Present widget data) **(Add follow-ups)** + 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 */} + {/* Swap Example with Amount Specified, Chain Missing (Updated Flow) */} Swap 100 USDC for ETH 1. Goal: Swap 100 USDC for ETH. - 2. Identify Missing Info: Chain is missing. - 3. Parse Amount: User input "100". Human amount: "100". - 4. Gather Swap Data: Call \`swap_or_bridge\` with fromToken=USDC (address), toToken='0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' (native ETH), amount='100'. *Do not specify fromChain/toChain*. - 5. Respond: "Okay, I'm gathering the data to swap 100 USDC for ETH. The widget will ask you to confirm the network(s) and details..." (Present widget data) **(Add follow-ups)** + 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)** @@ -623,19 +618,19 @@ export const systemPrompt = ( 1. Decompose user request into discrete steps. - 2. Identify dependencies (e.g., **Non-swap/bridge:** Balance Check (ETH vs ERC20) -> Parse/Pad Amt -> **PAUSE/WAIT** -> Final Tx (Raw Amt). **For swaps:** Amount Check -> 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 for *amount only*). - 4. Foresee potential failure points (parsing, balance, final tx execution, missing swap amount). + 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 if required for the specific operation (prompt if needed, *except* for swaps/bridges handled by the widget). - 3. Confirm amount context (prompt if needed). + 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). @@ -650,7 +645,7 @@ 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 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. Ensure required blank lines before the header. @@ -715,7 +710,7 @@ export const systemPrompt = ( 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. diff --git a/src/lib/morpheusSearch.ts b/src/lib/morpheusSearch.ts index 5df788b1..0b64f92b 100644 --- a/src/lib/morpheusSearch.ts +++ b/src/lib/morpheusSearch.ts @@ -1,7 +1,8 @@ // src/lib/morpheusSearch.ts import { google } from "@ai-sdk/google"; import { - CoreMessage, // Import CoreMessage for the simplified history type + CoreMessage, + // Import CoreMessage for the simplified history type experimental_createMCPClient as createMCPClient, generateText, streamText,