diff --git a/client/app/api/transactions/route.ts b/client/app/api/transactions/route.ts index c1cbc14a..cc928907 100644 --- a/client/app/api/transactions/route.ts +++ b/client/app/api/transactions/route.ts @@ -1,218 +1,231 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { NextRequest, NextResponse } from 'next/server'; -import { transactionProcessor } from '@/lib/transaction'; -import { LayerswapClient } from '@/lib/layerswap/client'; -import type { BrianResponse, BrianTransactionData } from '@/lib/transaction/types'; - -const BRIAN_API_URL = 'https://api.brianknows.org/api/v0/agent'; -const layerswapClient = new LayerswapClient(process.env.LAYERSWAP_API_KEY || ''); - -async function convertBrianResponseFormat(apiResponse: any): Promise { - const response = apiResponse.result[0]; - - // Construct base response - const brianResponse: BrianResponse = { - solver: response.solver, - action: response.action, - type: response.type, - extractedParams: response.extractedParams, - data: {} as BrianTransactionData - }; - - // Convert data based on action type - switch (response.action) { - case 'swap': - case 'transfer': - brianResponse.data = { - description: response.data?.description || '', - steps: response.data?.steps?.map((step: any) => ({ - contractAddress: step.contractAddress, - entrypoint: step.entrypoint, - calldata: step.calldata - })) || [], - fromToken: response.data?.fromToken, - toToken: response.data?.toToken, - fromAmount: response.data?.fromAmount, - toAmount: response.data?.toAmount, - receiver: response.data?.receiver, - amountToApprove: response.data?.amountToApprove, - gasCostUSD: response.data?.gasCostUSD - }; - break; - - case 'bridge': - brianResponse.data = { - description: '', - steps: [], - bridge: { - sourceNetwork: response.extractedParams.chain, - destinationNetwork: response.extractedParams.dest_chain, - sourceToken: response.extractedParams.token1, - destinationToken: response.extractedParams.token2, - amount: parseFloat(response.extractedParams.amount), - sourceAddress: response.extractedParams.address || '', - destinationAddress: response.extractedParams.address || '' - } - }; - break; - - case 'deposit': - case 'withdraw': - brianResponse.data = { - description: '', - steps: [], - protocol: response.extractedParams.protocol, - fromAmount: response.extractedParams.amount, - toAmount: response.extractedParams.amount, - receiver: response.extractedParams.address || '' - }; - break; - - default: - throw new Error(`Unsupported action type: ${response.action}`); - } - - return brianResponse; -} - -async function getBrianTransactionData(prompt: string, address: string, chainId: string, messages: any[]): Promise { +import { NextResponse, NextRequest } from "next/server"; +import { ChatOpenAI } from "@langchain/openai"; +import { transactionProcessor } from "@/lib/transaction"; +import type { + BrianResponse, + BrianTransactionData, +} from "@/lib/transaction/types"; +import { + TRANSACTION_INTENT_PROMPT, + transactionIntentPromptTemplate, +} from "@/prompts/prompts"; +import { StringOutputParser } from "@langchain/core/output_parsers"; + +const llm = new ChatOpenAI({ + model: "gpt-4", + apiKey: process.env.OPENAI_API_KEY, +}); + +async function getTransactionIntentFromOpenAI( + prompt: string, + address: string, + chainId: string, + messages: any[] +): Promise { try { - const response = await fetch(`${BRIAN_API_URL}/transaction`, { - method: 'POST', - headers: { - 'X-Brian-Api-Key': process.env.BRIAN_API_KEY || '', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prompt, - address, - chainId: chainId.toString(), - messages - }), + const conversationHistory = messages + .map((msg) => `${msg.role}: ${msg.content}`) + .join("\n"); + + const formattedPrompt = await transactionIntentPromptTemplate.format({ + TRANSACTION_INTENT_PROMPT, + prompt, + chainId, + conversationHistory, }); - const data = await response.json(); - console.log('Brian API Response:', JSON.stringify(data, null, 2)); - - // Special handling for intent recognition errors that still have extracted params - if (!response.ok && data.error && data.extractedParams) { - // If we have extracted params, we can still proceed - if (data.extractedParams[0]) { - // Convert to expected format - return { - solver: "Brian-Starknet", - action: data.extractedParams[0].action as 'bridge', - type: "write", - extractedParams: data.extractedParams[0], - data: { - description: '', - steps: [], - bridge: { - sourceNetwork: data.extractedParams[0].chain, - destinationNetwork: data.extractedParams[0].dest_chain, - sourceToken: data.extractedParams[0].token1, - destinationToken: data.extractedParams[0].token2, - amount: parseFloat(data.extractedParams[0].amount), - sourceAddress: address, - destinationAddress: data.extractedParams[0].address - } - } - }; - } - } - - if (!response.ok) { - throw new Error(data.error || `API request failed with status ${response.status}`); - } + const jsonOutputParser = new StringOutputParser(); + const response = await llm.pipe(jsonOutputParser).invoke(formattedPrompt); + const intentData = JSON.parse(response); - if (!data.result?.[0]) { - throw new Error('Invalid response format from Brian API'); + if (!intentData.isTransactionIntent) { + throw new Error("Not a transaction-related prompt"); } - const brianResponse = data.result[0] as BrianResponse; + const intentResponse: BrianResponse = { + solver: intentData.solver || "OpenAI-Intent-Recognizer", + action: intentData.action, + type: "write", + extractedParams: { + action: intentData.extractedParams.action, + token1: intentData.extractedParams.token1 || "", + token2: intentData.extractedParams.token2 || "", + chain: intentData.extractedParams.chain || "", + amount: intentData.extractedParams.amount || "", + protocol: intentData.extractedParams.protocol || "", + address: intentData.extractedParams.address || address, + dest_chain: intentData.extractedParams.dest_chain || "", + destinationChain: intentData.extractedParams.dest_chain || "", + destinationAddress: + intentData.extractedParams.destinationAddress || address, + }, + data: {} as BrianTransactionData, + }; + + switch (intentData.action) { + case "swap": + case "transfer": + intentResponse.data = { + description: intentData.data?.description || "", + steps: + intentData.extractedParams.transaction?.contractAddress || + intentData.extractedParams.transaction?.entrypoint || + intentData.extractedParams.transaction?.calldata + ? [ + { + contractAddress: + intentData.extractedParams.transaction.contractAddress, + entrypoint: + intentData.extractedParams.transaction.entrypoint, + calldata: [ + intentData.extractedParams.destinationAddress || + intentData.extractedParams.address, + intentData.extractedParams.amount, + "0", + ], + }, + ] + : [], + fromToken: { + symbol: intentData.extractedParams.token1 || "", + address: intentData.extractedParams.address || "", + decimals: 1, + }, + toToken: { + symbol: intentData.extractedParams.token2 || "", + address: intentData.extractedParams.address || "", + decimals: 1, + }, + fromAmount: intentData.extractedParams.amount, + toAmount: intentData.extractedParams.amount, + receiver: intentData.extractedParams.address, + amountToApprove: intentData.data?.amountToApprove, + gasCostUSD: intentData.data?.gasCostUSD, + }; + break; + + case "bridge": + intentResponse.data = { + description: "", + steps: [], + bridge: { + sourceNetwork: intentData.extractedParams.chain || "", + destinationNetwork: intentData.extractedParams.dest_chain || "", + sourceToken: intentData.extractedParams.token1 || "", + destinationToken: intentData.extractedParams.token2 || "", + amount: parseFloat(intentData.extractedParams.amount || "0"), + sourceAddress: address, + destinationAddress: + intentData.extractedParams.destinationAddress || address, + }, + }; + break; + + case "deposit": + case "withdraw": + intentResponse.data = { + description: "", + steps: [], + protocol: intentData.extractedParams.protocol || "", + fromAmount: intentData.extractedParams.amount, + toAmount: intentData.extractedParams.amount, + receiver: intentData.extractedParams.address || "", + }; + break; - // Add connected wallet address to params - if (brianResponse.extractedParams) { - brianResponse.extractedParams.connectedAddress = address; + default: + throw new Error(`Unsupported action type: ${intentData.action}`); } - return brianResponse; + return intentResponse; } catch (error) { - console.error('Error fetching transaction data:', error); + console.error("Error fetching transaction intent:", error); throw error; } } - export async function POST(request: NextRequest) { try { const body = await request.json(); - const { prompt, address, messages = [], chainId = '4012' } = body; + const { prompt, address, messages = [], chainId = "4012" } = body; if (!prompt || !address) { return NextResponse.json( - { error: 'Missing required parameters (prompt or address)' }, + { error: "Missing required parameters (prompt or address)" }, { status: 400 } ); } try { - const brianResponse = await getBrianTransactionData(prompt, address, chainId, messages); - console.log('Processed Brian Response:', JSON.stringify(brianResponse, null, 2)); - - // Always pass connected address to handlers - if (brianResponse.extractedParams) { - brianResponse.extractedParams.connectedAddress = address; - } - - const processedTx = await transactionProcessor.processTransaction(brianResponse); - console.log('Processed Transaction:', JSON.stringify(processedTx, null, 2)); + const transactionIntent = await getTransactionIntentFromOpenAI( + prompt, + address, + chainId, + messages + ); + console.log( + "Processed Transaction Intent from OPENAI:", + JSON.stringify(transactionIntent, null, 2) + ); + + const processedTx = await transactionProcessor.processTransaction( + transactionIntent + ); + console.log( + "Processed Transaction:", + JSON.stringify(processedTx, null, 2) + ); - // For deposit and withdraw, always use connected address as receiver - if (['deposit', 'withdraw'].includes(brianResponse.action)) { + if (["deposit", "withdraw"].includes(transactionIntent.action)) { processedTx.receiver = address; } return NextResponse.json({ - result: [{ - data: { - description: processedTx.description, - transaction: { - type: processedTx.action, - data: { - transactions: processedTx.transactions, - fromToken: processedTx.fromToken, - toToken: processedTx.toToken, - fromAmount: processedTx.fromAmount, - toAmount: processedTx.toAmount, - receiver: processedTx.receiver, - gasCostUSD: processedTx.estimatedGas, - solver: processedTx.solver, - protocol: processedTx.protocol, - bridge: processedTx.bridge - } - } + result: [ + { + data: { + description: processedTx.description, + transaction: { + type: processedTx.action, + data: { + transactions: processedTx.transactions, + fromToken: processedTx.fromToken, + toToken: processedTx.toToken, + fromAmount: processedTx.fromAmount, + toAmount: processedTx.toAmount, + receiver: processedTx.receiver, + gasCostUSD: processedTx.estimatedGas, + solver: processedTx.solver, + protocol: processedTx.protocol, + bridge: processedTx.bridge, + }, + }, + }, + conversationHistory: messages, }, - conversationHistory: messages - }] + ], }); } catch (error) { - console.error('Transaction processing error:', error); + console.error("Transaction processing error:", error); return NextResponse.json( - { - error: error instanceof Error ? error.message : 'Transaction processing failed', - details: error instanceof Error ? error.stack : undefined + { + error: + error instanceof Error + ? error.message + : "Transaction processing failed", + details: error instanceof Error ? error.stack : undefined, }, { status: 400 } ); } } catch (error) { - console.error('Request processing error:', error); + console.error("Request processing error:", error); return NextResponse.json( - { error: 'Internal server error' }, + { error: "Internal server error" }, { status: 500 } ); } -} \ No newline at end of file +} diff --git a/client/package-lock.json b/client/package-lock.json index 38396c2e..3e266885 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,7 +14,7 @@ "@brian-ai/sdk": "^0.3.6", "@hookform/resolvers": "^3.10.0", "@langchain/anthropic": "^0.3.11", - "@langchain/core": "^0.3.31", + "@langchain/core": "^0.3.33", "@langchain/langgraph": "^0.2.41", "@langchain/openai": "^0.3.17", "@prisma/client": "^6.2.1", @@ -3687,9 +3687,9 @@ } }, "node_modules/@langchain/core": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.31.tgz", - "integrity": "sha512-Fjy8gdaFjGuAW7+ug1XfQYJR3fyWPpWyydPXOhXfjnThaMnHfhIg9kzA3W7DxiMLgAC2fQsAqyxGJVXajV/07A==", + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.33.tgz", + "integrity": "sha512-gIszaRKWmP1HEgOhJLJaMiTMH8U3W9hG6raWihwpCTb0Ns7owjrmaqmgMa9h3W4/0xriaKfrfFBd6tepKsfxZA==", "license": "MIT", "dependencies": { "@cfworker/json-schema": "^4.0.2", @@ -3697,7 +3697,7 @@ "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.2.8", + "langsmith": ">=0.2.8 <0.4.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", diff --git a/client/package.json b/client/package.json index bb01f62f..11ebb102 100644 --- a/client/package.json +++ b/client/package.json @@ -19,7 +19,7 @@ "@brian-ai/sdk": "^0.3.6", "@hookform/resolvers": "^3.10.0", "@langchain/anthropic": "^0.3.11", - "@langchain/core": "^0.3.31", + "@langchain/core": "^0.3.33", "@langchain/langgraph": "^0.2.41", "@langchain/openai": "^0.3.17", "@prisma/client": "^6.2.1", diff --git a/client/prompts/prompts.js b/client/prompts/prompts.js index ba3d2bcd..d305cea2 100644 --- a/client/prompts/prompts.js +++ b/client/prompts/prompts.js @@ -1,3 +1,5 @@ +import { PromptTemplate } from "@langchain/core/prompts"; + export const ASK_OPENAI_AGENT_PROMPT = ` You are StarkFinder, an expert assistant specializing in the Starknet ecosystem and trading, designed to assist users on our website. You complement BrianAI, the primary knowledge base, by providing additional insights and guidance to users. Your goal is to enhance user understanding and decision-making related to Starknet. @@ -13,3 +15,118 @@ Your responsibilities: 5. Format all responses in Markdown for better readability on the website. NOTE: On the website, always refer to yourself as "StarkFinder." Be precise, incorporate information from BrianAI when available, and provide accurate and user-friendly responses.`; + +export const TRANSACTION_INTENT_PROMPT = ` +You are a blockchain transaction intent recognition system. + +Given a user prompt, analyze and determine if the request involves a blockchain transaction. +Respond ONLY in JSON format with the following structure: +{ + "isTransactionIntent": boolean, + "solver": string, + "action": "swap" | "transfer" | "deposit" | "withdraw" | "bridge", + "type": "write", + "extractedParams": { + "action": string, + "token1": string, + "token2": string, + "chain": string, + "dest_chain": string, + "amount": string, + "protocol": string, + "address": string, + "destinationAddress": string, + "transaction": { + "contractAddress": string, + "entrypoint": string, + "calldata": string[] + } + }, + "data": { + "description": string, + "steps": [ + { + "contractAddress": string, + "entrypoint": string, + "calldata": string[] + } + ] + } +} + +Transaction Analysis Guidelines: +1. Accurately identify the type of transaction from the user's intent +2. Extract precise transaction parameters +3. Include transaction details in the 'transaction' field when applicable +4. Use empty strings for parameters that cannot be determined + +Examples: +1. "Send 0.1 ETH to 0x123..." + - action: "transfer" + - token1: "ETH" + - amount: "0.1" + - address: "0x123..." + - data.steps: { + contractAddress: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + entrypoint: "transfer", + calldata: ["0x123...", "0.1","0"] + } + +2. "Send 0.1 STRK to 0x123..." + - action: "transfer" + - token1: "STRK" + - amount: "0.1" + - address: "0x123..." + - data.steps: { + contractAddress: "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + entrypoint: "transfer", + calldata: ["0x123...", "0.1","0"] + } + +2. "Bridge 50 USDC from Ethereum to Arbitrum" + - action: "bridge" + - token1: "USDC" + - chain: "Ethereum" + - dest_chain: "Arbitrum" + - amount: "50" + - transaction: { + contractAddress: "", + entrypoint: "bridge", + calldata: ["USDC", "50", "Ethereum", "Arbitrum"] + } + +remember to take contract address based on type of token as there are different address for STRK and ETH that i have provided + +Current Context: +- User Prompt: {prompt} +- Connected Chain ID: {chainId} +- Conversation History: {conversationHistory} + +Analyze the intent carefully and provide the most accurate transaction representation possible. +`; + +export const transactionIntentPromptTemplate = new PromptTemplate({ + inputVariables: [ + "TRANSACTION_INTENT_PROMPT", + "prompt", + "chainId", + "conversationHistory", + ], + template: ` + {TRANSACTION_INTENT_PROMPT} + + dditional Context: + Current Chain ID: {chainId} + + Conversation History: + {conversationHistory} + + User Prompt: {prompt} + + IMPORTANT: + - Respond ONLY in JSON format + - Ensure all fields are present + - If no transaction intent is detected, set isTransactionIntent to false + - Use empty strings if a parameter is not applicable +`, +});