From d25119c66abe9e103f673f9751107e5b22211282 Mon Sep 17 00:00:00 2001 From: Ismael Bencharef <66883069+IshShogun@users.noreply.github.com> Date: Sat, 17 Feb 2024 01:24:20 +0000 Subject: [PATCH] integrate transfer and balance smart agent --- package.json | 1 + src/backend/services/ollama.ts | 19 ++++-- src/backend/services/prompts.ts | 62 +++++++++++++++++++ src/frontend/utils/transaction.ts | 99 +++++++++++++++++++++++++++++++ src/frontend/utils/types.ts | 8 +++ src/frontend/utils/utils.ts | 16 +++++ src/frontend/views/chat.tsx | 63 +++++++++++++++++--- yarn.lock | 38 ++++++++++++ 8 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 src/backend/services/prompts.ts create mode 100644 src/frontend/utils/transaction.ts create mode 100644 src/frontend/utils/types.ts create mode 100644 src/frontend/utils/utils.ts diff --git a/package.json b/package.json index 4b7e488..5a0f13f 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "check-disk-space": "^3.4.0", "electron-squirrel-startup": "^1.0.0", "electron-store": "^8.1.0", + "ethers": "^6.11.1", "ollama": "^0.4.3", "qrcode": "^1.5.3", "react-loader-spinner": "^6.1.6", diff --git a/src/backend/services/ollama.ts b/src/backend/services/ollama.ts index a12ed03..c5b61e1 100644 --- a/src/backend/services/ollama.ts +++ b/src/backend/services/ollama.ts @@ -3,6 +3,7 @@ import { Ollama, ProgressResponse } from 'ollama'; import { execFile, ChildProcess } from 'child_process'; import fs from 'fs'; import { isDev, sendOllamaStatusToRenderer } from '..'; +import { MOR_PROMPT } from './prompts'; // events import { IpcMainChannel, OllamaChannel } from '../../events'; @@ -144,15 +145,25 @@ export const getOllamaExecutableAndAppDataPath = ( }; export const askOllama = async (model: string, message: string) => { + //const engineeredPrompt = `${MOR_PROMPT}\n\nUser: ${message}\n\nResponse:` return await ollama.chat({ model, messages: [ - { - role: 'user', - content: `${message}`, + { + role: 'system', + content: MOR_PROMPT }, - ], + { + role: 'user', + content: ` Now answer the following question: ${message}. Response:` + } + ] + }); + /*return await ollama.generate({ + model: model, + prompt: engineeredPrompt + })*/ }; export const getOrPullModel = async (model: string) => { diff --git a/src/backend/services/prompts.ts b/src/backend/services/prompts.ts new file mode 100644 index 0000000..58142b0 --- /dev/null +++ b/src/backend/services/prompts.ts @@ -0,0 +1,62 @@ +export const MOR_PROMPT = +`###System: +You are MORPHEUS, an AI assistant, but you prefer to be called a SmartAgent. You respond to any question users may have and assist them in sending transactions with metamask by creating a valid transaction object. + +Respond in a valid JSON to be consumed by an application following this pattern: +{"response", "your response goes here", "transaction", "user transaction object goes here"}. +Only respond with the JSON, NEVER provide any text outside of the json. Your respond only in a valid JSON, nothing else. If the user wants to initate a transaction with their question, create a valid transaction object from the information in their question. Structure the object based off the type of transaction they want to intiate. + +For Transfer transactions create a transaction object following this pattern: +{"type": "Transfer", "targetAddress", "target address goes here", "ethAmount", "amount of eth to transfer goes here"} + +For Balance transactions create a transaction object following this pattern: +{"type": "Balance"} + +Here are examples on how to create the transaction object from the user's question: +###Examples: +Example 1: +Question: "transfer 43 eth to 0x223738a369F0804c091e13740D26D1269294bc1b", //User is initiating a transfer transaction with their question. +Response: "{ + response: "Of course! The transaction details are prepared for you. Please double-check the parameters before confirming on Metamask.", + transaction: { + "type": "transfer", + "targetAddress": "0x223738a369F0804c091e13740D26D1269294bc1b", + "ethAmount": "43" + } +}" + +Example 3: +Question: "Hey Morpheus, whats my balance" +Response: "{ + response: "Your balance is: ", + transaction: { + "type": "Balance" + } +}" + +Example 4: +Question: "Why is the sky blue" //the user's question does not initiate a transaction, leave the transaction field empty. +Response: "{ + response: "The sky is blue because of a thing called Rayleigh scattering. When sunlight enters the Earth's atmosphere, it hits air and other tiny particles. This light is made of many colors. Blue light scatters more because it travels as shorter, smaller waves. So, when we look up, we see more blue light than other colors.", + transaction: {} +}" + + + +Example 5: +Question: "What is stETH" //the user's question does not initiate a transaction, leave the transaction field empty. +Response: "{ + response: "stETH stands for staked Ether. It's a type of cryptocurrency. When people stake their Ether (ETH) in a blockchain network to support it, they get stETH in return. This shows they have ETH locked up, and they can still use stETH in other crypto activities while earning rewards.", + transaction: {} +}" +`; + +const errorHandling = `If a question is initiating a buy or transfer transaction and the user doesn't specify an amount in ETH. Gently decline to send the transaction +and request the amount to buy or transfer (depending on their transaction type) in ethereum. + +If a question is initiating a sell transaction and the user doesn't specify an amount in tokens. Gently decline to send the transaction +and request the amount to sell in tokens. `; +//TODO: allow for staking MOR and swap tokens +//TODO: use RAG to include a database to tokenAddresses and symbols +//TODO: include chat history +//TODO: include error handling in prompt diff --git a/src/frontend/utils/transaction.ts b/src/frontend/utils/transaction.ts new file mode 100644 index 0000000..9f76381 --- /dev/null +++ b/src/frontend/utils/transaction.ts @@ -0,0 +1,99 @@ +import { ethers } from "ethers"; +import { SDKProvider } from "@metamask/sdk"; +import { transactionParams } from "./types"; + +export const isTransactionIntiated = (transaction: transactionParams) => { + return !(Object.keys(transaction).length === 0); +} + +export const buildTransaction = (transaction: transactionParams, account: string | undefined, gasPrice: string, provider: SDKProvider | undefined) => { + const transactionType = transaction.type.toLowerCase(); + + let tx: any + switch (transactionType) { + case "transfer": + tx = buildTransferTransaction(transaction, account, gasPrice); + break; + default: + throw Error(`Transaction of type ${transactionType} is not yet supported`); + } + //returned wrapped call with method for metamask with transaction params + return { + "method": "eth_sendTransaction", + "params": [tx] + } + } + +const buildTransferTransaction = (transaction: transactionParams, account: string | undefined, gasPrice: any) => { + return { + from: account, + to: transaction.targetAddress, + gas: "0x76c0", //for more complex tasks estimate this from metamast + gasPrice: gasPrice, + value: '0x' + ethers.parseEther(transaction.ethAmount).toString(16), + data: "0x000000" + } +} + +//TODO: take chain ID to get arb balance or w/e chain +const formatWalletBalance = (balanceWeiHex: string) => { + const balanceBigInt = BigInt(balanceWeiHex) + const balance = ethers.formatUnits(balanceBigInt, "ether"); + return parseFloat(balance).toFixed(2) + " " + "ETH"; +} + +export const handleBalanceRequest = async (provider: SDKProvider | undefined, account: string | undefined, response: string) => { + const blockNumber = await provider?.request({ + "method": "eth_blockNumber", + "params": [] + }); + + const balanceWeiHex = await provider?.request({ + "method": "eth_getBalance", + "params": [ + account, + blockNumber + ] + }); + if(typeof balanceWeiHex === 'string'){ + return response + " " + formatWalletBalance(balanceWeiHex); + } else { + console.error('Failed to retrieve a valid balance.'); + throw Error('Invalid Balance Recieved from MetaMask.') + } +} + +const estimateGasWithOverHead = (estimatedGasMaybe: string) => { + const estimatedGas = parseInt(estimatedGasMaybe, 16); + //console.log("Gas Limit: " + estimatedGas) + const gasLimitWithOverhead = Math.ceil(estimatedGas * 5); + return "0x" + gasLimitWithOverhead.toString(16); +} + +export const handleTransactionRequest = async (provider: SDKProvider | undefined, transaction: transactionParams, account: string | undefined) => { + const gasPrice = await provider?.request({ + "method": "eth_gasPrice", + "params": [] + }); + //Sanity Check + if(typeof gasPrice !== 'string'){ + console.error('Failed to retrieve a valid gasPrice'); + throw new Error('Invalid gasPrice received'); + } + let builtTx = buildTransaction(transaction, account, gasPrice, provider); + + let estimatedGas = await provider?.request({ + "method": "eth_estimateGas", + "params": [builtTx] + }); + //Sanity Check + if(typeof estimatedGas !== 'string'){ + console.error('Failed to estimate Gas with metamask'); + throw new Error('Invalid gasPrice received'); + } + + const gasLimitWithOverhead = estimateGasWithOverHead(estimatedGas) + builtTx.params[0].gas = gasLimitWithOverhead; // Update the transaction with the new gas limit in hex + return builtTx +} + diff --git a/src/frontend/utils/types.ts b/src/frontend/utils/types.ts new file mode 100644 index 0000000..e5118fd --- /dev/null +++ b/src/frontend/utils/types.ts @@ -0,0 +1,8 @@ +export type ModelResponse = { + response: string; + transaction: transactionParams +}; + +export type transactionParams = { + [key: string]: string +} \ No newline at end of file diff --git a/src/frontend/utils/utils.ts b/src/frontend/utils/utils.ts new file mode 100644 index 0000000..7ddaa85 --- /dev/null +++ b/src/frontend/utils/utils.ts @@ -0,0 +1,16 @@ +import { ModelResponse } from "./types"; + +export function parseResponse(jsonString: string){ + // Assert the type of the parsed object. + const parsed = JSON.parse(jsonString); + + if (isModelResponse(parsed)) { + return { response: parsed.response, transaction: parsed.transaction }; + } else { + throw new Error("Invalid ModelResponse format"); + } +} + +function isModelResponse(object: any): object is ModelResponse { + return 'response' in object && 'transaction' in object; +} \ No newline at end of file diff --git a/src/frontend/views/chat.tsx b/src/frontend/views/chat.tsx index 15bb1d0..18b404b 100644 --- a/src/frontend/views/chat.tsx +++ b/src/frontend/views/chat.tsx @@ -8,12 +8,18 @@ import { AIMessage } from '../types'; import { OllamaChannel } from './../../events'; import { useAIMessagesContext } from '../contexts'; +import { isTransactionIntiated, handleBalanceRequest, handleTransactionRequest } from '../utils/transaction'; +import { useSDK } from '@metamask/sdk-react'; +import {parseResponse} from '../utils/utils' +import { transactionParams } from '../utils/types'; + const ChatView = (): JSX.Element => { const [selectedModel, setSelectedModel] = useState('mistral'); const [dialogueEntries, setDialogueEntries] = useAIMessagesContext(); const [inputValue, setInputValue] = useState(''); const [currentQuestion, setCurrentQuestion] = useState(); const [isOllamaBeingPolled, setIsOllamaBeingPolled] = useState(false); + const { ready, sdk, connected, connecting, provider, chainId, account, balance } = useSDK(); useEffect(() => { window.backendBridge.ollama.onAnswer((response) => { @@ -30,6 +36,51 @@ const ChatView = (): JSX.Element => { }; }); + //Function to update dialogue entries + const updateDialogueEntries = (question: string, message: string) => { + setCurrentQuestion(undefined); + setDialogueEntries([ + ...dialogueEntries, + { question: question, answer: message, answered: true }, + ]); + } + +const processResponse = async (question: string, response: string, transaction: transactionParams) => { + if (!isTransactionIntiated(transaction)){ + updateDialogueEntries(question, response); //no additional logic in this case + return; + } + + //Sanity Checks: + if(!account || !provider){ + const errorMessage = `Error: Please connect to metamask` + updateDialogueEntries(question, errorMessage); + return; + } + + if (transaction.type.toLowerCase() === "balance") { + let message: string; + try { + message = await handleBalanceRequest(provider, account, response); + } catch (error){ + message = `Error: Failed to retrieve a valid balance from Metamask, try reconnecting.` + } + updateDialogueEntries(question, message); + } else { + try { + const builtTx = await handleTransactionRequest(provider, transaction, account); + updateDialogueEntries(question, response); + console.log("from: " + builtTx.params[0].from); + await provider?.request(builtTx); + } catch (error){ + const badTransactionMessage = "Error: There was an error sending your transaction, if the transaction type is balance or transfer please reconnect to metamask" + updateDialogueEntries(question, badTransactionMessage); + } + } + +} + + const handleQuestionAsked = async (question: string) => { if (isOllamaBeingPolled) { return; @@ -45,17 +96,15 @@ const ChatView = (): JSX.Element => { setIsOllamaBeingPolled(true); - const response = await window.backendBridge.ollama.question({ + + const inference = await window.backendBridge.ollama.question({ model: selectedModel, query: question, }); - if (response) { - setCurrentQuestion(undefined); - setDialogueEntries([ - ...dialogueEntries, - { question: question, answer: response.message.content, answered: true }, - ]); + if (inference) { + const { response, transaction } = parseResponse(inference.message.content) + await processResponse(question, response, transaction); } setIsOllamaBeingPolled(false); diff --git a/yarn.lock b/yarn.lock index 593f2ab..bcbb9d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,11 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -3271,6 +3276,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + "@types/node@^12.12.54": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" @@ -4169,6 +4179,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + aes-js@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" @@ -6527,6 +6542,19 @@ ethers@^5.6.9: "@ethersproject/web" "5.7.1" "@ethersproject/wordlists" "5.7.0" +ethers@^6.11.1: + version "6.11.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.11.1.tgz#96aae00b627c2e35f9b0a4d65c7ab658259ee6af" + integrity sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + eventemitter2@^6.4.5, eventemitter2@^6.4.7: version "6.4.9" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" @@ -11516,6 +11544,11 @@ tslib@1.14.1, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" @@ -12212,6 +12245,11 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^7.4.5, ws@^7.4.6, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"