Skip to content

Commit

Permalink
integrate transfer and balance smart agent
Browse files Browse the repository at this point in the history
  • Loading branch information
IshShogun committed Feb 17, 2024
1 parent a9ec7eb commit d25119c
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 15 additions & 4 deletions src/backend/services/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand Down
62 changes: 62 additions & 0 deletions src/backend/services/prompts.ts
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions src/frontend/utils/transaction.ts
Original file line number Diff line number Diff line change
@@ -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
}

8 changes: 8 additions & 0 deletions src/frontend/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ModelResponse = {
response: string;
transaction: transactionParams
};

export type transactionParams = {
[key: string]: string
}
16 changes: 16 additions & 0 deletions src/frontend/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
63 changes: 56 additions & 7 deletions src/frontend/views/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AIMessage>();
const [isOllamaBeingPolled, setIsOllamaBeingPolled] = useState(false);
const { ready, sdk, connected, connecting, provider, chainId, account, balance } = useSDK();

useEffect(() => {
window.backendBridge.ollama.onAnswer((response) => {
Expand All @@ -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;
Expand All @@ -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);
Expand Down
Loading

0 comments on commit d25119c

Please sign in to comment.