Skip to content

Commit

Permalink
feat: enhance DePIN plugin with prediction and betting actions
Browse files Browse the repository at this point in the history
- Uncomment and activate weather, prediction, and betting actions
- Update list predictions formatting with more concise date display
- Modify place bet action to simplify blockchain interaction
- Add automatic token allowance approval before placing bets
- Remove unnecessary transaction hash extraction logic
- Streamline blockchain helper functions for bet placement
  • Loading branch information
nicky-ru committed Feb 7, 2025
1 parent a825667 commit 2550d46
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/client-discord/src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ If {{agentName}} is conversing with a user and they have not asked to stop, it i
# INSTRUCTIONS: Choose the option that best describes {{agentName}}'s response to the last message. Ignore messages if they are addressed to someone else.
` + shouldRespondFooter;

export const discordVoiceHandlerTemplate = `"You are {{agentName}}, an AI assistant capable of engaging in voice conversations.\n\nYour personality and background are as follows:\n\n<bio>\n{{bio}}\n</bio>\n\nRecent conversation context:\n<recent_messages>\n{{recentMessages}}\n</recent_messages>\n\nYou can perform the following actions:\n<actions>\n{{actions}}\n</actions>\n\nFollow these guidelines:\n1. Start with a very short (1-2 word) opening.\n2. Maintain a conversational tone consistent with your bio.\n3. Keep the response concise and suitable for voice output.\n\nFormat your response as plain text, don't include any analysis. If an action is needed, append it after a \"||\" delimiter.\n\nExample format (do not use this content):\nHello! I'd be happy to help you with your account balance. Let me check that for you right away. || CONTINUE\n\nNow, provide your response based on the context given."`;
export const discordVoiceHandlerTemplate = `"You are {{agentName}}, an AI assistant capable of engaging in voice conversations.\n\nYour personality and background are as follows:\n\n<bio>\n{{bio}}\n</bio>\n\nRecent conversation context:\n<recent_messages>\n{{recentMessages}}\n</recent_messages>\n\nYou can perform the following actions:\n<actions>\n{{actions}}\n</actions>\n\nFollow these guidelines:\n1. Start with a very short (1-2 word) opening.\n2. Maintain a conversational tone consistent with your bio.\n3. Keep the response concise and suitable for voice output.\n\nFormat your response as plain text, don't include any analysis. If an action is needed, append it after a \"||\" delimiter.\n\nExample format (do not use this content):\nHello! I'd be happy to help you with your account balance. Let me check that for you right away. || CONTINUE\n\nNow, provide your response based on the context given and one of the actions: {{actionNames}}."`;

export const discordMessageHandlerTemplate =
// {{goals}}
Expand Down
68 changes: 66 additions & 2 deletions packages/client-discord/src/voice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,9 @@ export class VoiceManager extends EventEmitter {
const { roomId } = message;
let fullResponse = "";
let firstResponseSent = false;
let action = "NONE"; // Default action

elizaLogger.debug("context: ", context);

try {
const stream = await client.messages.stream({
Expand All @@ -748,15 +751,24 @@ export class VoiceManager extends EventEmitter {

// Helper function to process a complete text block
const processTextBlock = async (text: string): Promise<void> => {
// Check for action delimiter
const parts = text.split("||");
const processText = parts[0].trim();
if (parts.length > 1) {
action = parts[1].trim();
}

console.time("textToSpeech");
const responseStream = await this.runtime
.getService<ISpeechService>(ServiceType.SPEECH_GENERATION)
.generate(this.runtime, text);
.generate(this.runtime, processText);
console.timeEnd("textToSpeech");

if (responseStream) {
if (!firstResponseSent) {
elizaLogger.debug(`First response sent at ms: ${Date.now()}`);
elizaLogger.debug(
`First response sent at ms: ${Date.now()}`
);
firstResponseSent = true;
}
console.time("audioPlayback");
Expand Down Expand Up @@ -814,6 +826,7 @@ export class VoiceManager extends EventEmitter {
source: "discord",
user: this.runtime.character.name,
inReplyTo: message.id,
action: action, // Add action to the content
},
roomId,
embedding: getEmbeddingZeroVector(),
Expand All @@ -823,6 +836,55 @@ export class VoiceManager extends EventEmitter {
await this.runtime.messageManager.createMemory(responseMemory);
state = await this.runtime.updateRecentMessageState(state);
await this.runtime.evaluate(message, state);

// Process actions if needed
const callback: HandlerCallback = async (content: Content) => {
const newResponseMemory: Memory = {
id: stringToUuid(
message.id + "-voice-response-" + Date.now()
),
agentId: this.runtime.agentId,
userId: this.runtime.agentId,
content: {
...content,
user: this.runtime.character.name,
inReplyTo: message.id,
},
roomId,
embedding: getEmbeddingZeroVector(),
};

if (newResponseMemory.content.text?.trim()) {
await this.runtime.messageManager.createMemory(
newResponseMemory
);
state =
await this.runtime.updateRecentMessageState(state);

const responseStream = await this.runtime
.getService<ISpeechService>(
ServiceType.SPEECH_GENERATION
)
.generate(this.runtime, content.text);

if (responseStream) {
await this.playAudioStream(
userId,
responseStream as Readable
);
}

}
return [newResponseMemory];
};

await this.runtime.processActions(
message,
[responseMemory],
state,
callback
);
this.runtime.evaluate(message, state, true);
}
console.timeEnd("saveResponseMemory");

Expand Down Expand Up @@ -866,6 +928,7 @@ export class VoiceManager extends EventEmitter {
channel: BaseGuildVoiceChannel,
state: State
): Promise<boolean> {
return true;
if (userId === this.client.user?.id) return false;
const lowerMessage = message.toLowerCase();
const botName = this.client.user.username.toLowerCase();
Expand All @@ -876,6 +939,7 @@ export class VoiceManager extends EventEmitter {

if (
lowerMessage.includes(botName as string) ||
lowerMessage.includes("agent") ||
lowerMessage.includes(characterName) ||
lowerMessage.includes(
this.client.user?.tag.toLowerCase() as string
Expand Down
13 changes: 6 additions & 7 deletions packages/plugin-depin/src/actions/listPredictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from "@elizaos/core";

const formatPrediction = (prediction: any) => {
const deadline = new Date(prediction.deadline).toLocaleString();
const deadline = new Date(prediction.deadline).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});

return `"${prediction.statement}" (Due: ${deadline})`;
};
Expand Down Expand Up @@ -67,14 +71,9 @@ export const listPredictions: Action = {
)
.join("\n");

const betFooter = `
You can bet on them by saying
"PREPARE A BET FOR PREDICTION <number>, <amount> $SENTAI, <outcome>, <your_wallet_address>"
`;

if (callback) {
callback({
text: `🎯 Here are the active predictions:\n${formattedPredictions}\n\n${betFooter}`,
text: `🎯 Here are the active predictions:\n${formattedPredictions}`,
inReplyTo: message.id,
});
}
Expand Down
47 changes: 25 additions & 22 deletions packages/plugin-depin/src/actions/placeBet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ import {
} from "@elizaos/core";

import {
approveAllowance,
placeBet as executeBlockchainBet,
getNetwork,
} from "../helpers/blockchain";

const extractTxHash = (text: string): string | null => {
// Match Ethereum/IoTeX transaction hash pattern
const regex = /0x[a-fA-F0-9]{64}/;
const match = text.match(regex);
return match ? match[0] : null;
};
// const extractTxHash = (text: string): string | null => {
// // Match Ethereum/IoTeX transaction hash pattern
// const regex = /0x[a-fA-F0-9]{64}/;
// const match = text.match(regex);
// return match ? match[0] : null;
// };

export const placeBet: Action = {
name: "PLACE_BET",
Expand All @@ -35,7 +36,7 @@ export const placeBet: Action = {
{
user: "user",
content: {
text: "BET 123 APPROVED: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
text: "PLACE BET ON THE PREDICTION 1, 100 $SENTAI, it will rain tomorrow in London",
},
},
{
Expand All @@ -55,16 +56,16 @@ export const placeBet: Action = {
callback?: HandlerCallback
): Promise<boolean> => {
try {
const txHash = extractTxHash(message.content.text);
if (!txHash) {
if (callback) {
callback({
text: "I couldn't find a valid transaction hash in your message. Please provide the approval transaction hash.",
inReplyTo: message.id,
});
}
return false;
}
// const txHash = extractTxHash(message.content.text);
// if (!txHash) {
// if (callback) {
// callback({
// text: "I couldn't find a valid transaction hash in your message. Please provide the approval transaction hash.",
// inReplyTo: message.id,
// });
// }
// return false;
// }

const betParams = await extractBetParamsFromContext(
runtime,
Expand All @@ -83,6 +84,12 @@ export const placeBet: Action = {

const network = getNetwork();

await approveAllowance(
runtime,
network,
Number(betParams.amount)
);

if (callback) {
callback({
text: "Processing your bet...",
Expand All @@ -96,13 +103,12 @@ export const placeBet: Action = {
betParams.predictionId,
betParams.outcome,
betParams.amount,
betParams.bettor as `0x${string}`,
network
);

if (callback) {
callback({
text: `Your bet has been placed successfully!\n\nPrediction: "${betParams.statement}"\nYour bet: ${betParams.outcome ? "Yes" : "No"}\nAmount: ${betResult.betAmount}\nBettor: ${betResult.bettor}\nTransaction: ${betResult.hash}`,
text: `Your bet has been placed successfully!\n\nPrediction: "${betParams.statement}"\nYour bet: ${betParams.outcome ? "Yes" : "No"}\nAmount: ${betResult.betAmount}}`,
inReplyTo: message.id,
});
}
Expand Down Expand Up @@ -137,7 +143,6 @@ export const placeBet: Action = {
};

interface BetParams {
bettor: `0x${string}`;
amount: string;
outcome: boolean;
predictionId: number;
Expand Down Expand Up @@ -211,14 +216,12 @@ Deadline: Sat Jan 18 2025 12:00:00
<recent_messages_example>
- BET ON PREDICTION 1, 100 $SENTAI, true, 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
- BetID: 123, 1. 0x742d35Cc6634C0532925a3b844Bc454e4438f44e, 100 $SENTAI, true. Tx data: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
- BET 123 APPROVED: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
</recent_messages_example>
<response>
{
"reasoning": "The user is betting that it will rain tomorrow in London with 100 $SENTAI and he approved the bet.",
"statement": "It will rain tomorrow in London",
"bettor": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"amount": "100",
"outcome": true,
"predictionId": 1
Expand Down
74 changes: 68 additions & 6 deletions packages/plugin-depin/src/helpers/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export const placeBet = async (
predictionId: number,
outcome: boolean,
amount: string,
bettor: `0x${string}`,
network: SupportedChain
) => {
const walletProvider = await initWalletProvider(runtime);
Expand All @@ -151,7 +150,7 @@ export const placeBet = async (

const betAmount = await getBetAmount(
amountInWei,
bettor,
account,
account,
publicClient
);
Expand All @@ -162,13 +161,13 @@ export const placeBet = async (
abi: predictionAbi,
account,
functionName: "placeBetForAccount",
args: [bettor, BigInt(predictionId), outcome, betAmount],
args: [account, BigInt(predictionId), outcome, betAmount],
});

const data = encodeFunctionData({
abi: predictionAbi,
functionName: "placeBetForAccount",
args: [bettor, BigInt(predictionId), outcome, betAmount],
args: [account, BigInt(predictionId), outcome, betAmount],
});

const walletClient = walletProvider.getWalletClient(network);
Expand All @@ -193,7 +192,7 @@ export const placeBet = async (
return {
hash,
predictionId,
bettor,
account,
betAmount: formatUnits(betAmount, decimals),
outcome,
};
Expand All @@ -202,6 +201,66 @@ export const placeBet = async (
}
};

export const approveAllowance = async (
runtime: IAgentRuntime,
network: SupportedChain,
amount: number
) => {
const walletProvider = await initWalletProvider(runtime);
const publicClient = walletProvider.getPublicClient(network);
const account = walletProvider.getAddress();

const decimals = await getDecimals(
runtime,
process.env.PREDICTION_TOKEN as `0x${string}`,
network
);
const amountInWei = parseUnits(amount.toString(), decimals);

await publicClient.simulateContract({
address: process.env.PREDICTION_TOKEN as `0x${string}`,
abi: erc20Abi,
account,
functionName: "approve",
args: [
process.env.BINARY_PREDICTION_CONTRACT_ADDRESS as `0x${string}`,
amountInWei,
],
});

const data = encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
process.env.BINARY_PREDICTION_CONTRACT_ADDRESS as `0x${string}`,
amountInWei,
],
});

const walletClient = walletProvider.getWalletClient(network);
// @ts-ignore
const request = await walletClient.prepareTransactionRequest({
to: process.env.PREDICTION_TOKEN as `0x${string}`,
data,
account: walletClient.account,
});
// @ts-ignore
const serializedTransaction = await walletClient.signTransaction(request);
const hash = await walletClient.sendRawTransaction({
serializedTransaction,
});

const receipt = await publicClient.waitForTransactionReceipt({
hash,
});

if (receipt.status === "success") {
return hash;
} else {
throw new Error("Allowance approval failed");
}
};

export const genTxDataForAllowance = async (
runtime: IAgentRuntime,
network: SupportedChain,
Expand Down Expand Up @@ -234,7 +293,10 @@ const getBetAmount = async (
address: process.env.PREDICTION_TOKEN as `0x${string}`,
abi: erc20Abi,
functionName: "allowance",
args: [bettor, process.env.BINARY_PREDICTION_CONTRACT_ADDRESS as `0x${string}`],
args: [
bettor,
process.env.BINARY_PREDICTION_CONTRACT_ADDRESS as `0x${string}`,
],
})) as bigint;

if (allowance <= BigInt(0)) {
Expand Down
Loading

0 comments on commit 2550d46

Please sign in to comment.