Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Proof of Pizza - Agentic Dominos Ordering #1005

Closed
wants to merge 13 commits into from
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@ai16z/plugin-0g": "workspace:*",
"@ai16z/plugin-aptos": "workspace:*",
"@ai16z/plugin-bootstrap": "workspace:*",
"@ai16z/plugin-dominos": "workspace:*",
"@ai16z/plugin-intiface": "workspace:*",
"@ai16z/plugin-coinbase": "workspace:*",
"@ai16z/plugin-conflux": "workspace:*",
Expand Down
23 changes: 12 additions & 11 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,47 @@ import { SqliteDatabaseAdapter } from "@ai16z/adapter-sqlite";
import { AutoClientInterface } from "@ai16z/client-auto";
import { DirectClientInterface } from "@ai16z/client-direct";
import { DiscordClientInterface } from "@ai16z/client-discord";
import { FarcasterAgentClient } from "@ai16z/client-farcaster";
import { TelegramClientInterface } from "@ai16z/client-telegram";
import { TwitterClientInterface } from "@ai16z/client-twitter";
import { FarcasterAgentClient } from "@ai16z/client-farcaster";
import {
AgentRuntime,
CacheManager,
Character,
Clients,
DbCacheAdapter,
defaultCharacter,
elizaLogger,
FsCacheAdapter,
IAgentRuntime,
ICacheManager,
IDatabaseAdapter,
IDatabaseCacheAdapter,
ModelProviderName,
defaultCharacter,
elizaLogger,
settings,
stringToUuid,
validateCharacterConfig,
} from "@ai16z/eliza";
import { zgPlugin } from "@ai16z/plugin-0g";
import createGoatPlugin from "@ai16z/plugin-goat";
import { bootstrapPlugin } from "@ai16z/plugin-bootstrap";
import { dominosPlugin } from "@ai16z/plugin-dominos";
import createGoatPlugin from "@ai16z/plugin-goat";
// import { intifacePlugin } from "@ai16z/plugin-intiface";
import { aptosPlugin } from "@ai16z/plugin-aptos";
import {
advancedTradePlugin,
coinbaseCommercePlugin,
coinbaseMassPaymentsPlugin,
tradePlugin,
tokenContractPlugin,
tradePlugin,
webhookPlugin,
advancedTradePlugin,
} from "@ai16z/plugin-coinbase";
import { confluxPlugin } from "@ai16z/plugin-conflux";
import { imageGenerationPlugin } from "@ai16z/plugin-image-generation";
import { evmPlugin } from "@ai16z/plugin-evm";
import { flowPlugin } from "@ai16z/plugin-flow";
import { imageGenerationPlugin } from "@ai16z/plugin-image-generation";
import { createNodePlugin } from "@ai16z/plugin-node";
import { solanaPlugin } from "@ai16z/plugin-solana";
import { teePlugin, TEEMode } from "@ai16z/plugin-tee";
import { aptosPlugin, TransferAptosToken } from "@ai16z/plugin-aptos";
import { flowPlugin } from "@ai16z/plugin-flow";
import { TEEMode, teePlugin } from "@ai16z/plugin-tee";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
Expand Down Expand Up @@ -396,6 +396,7 @@ export async function createAgent(
character,
plugins: [
bootstrapPlugin,
dominosPlugin,
getSecret(character, "CONFLUX_CORE_PRIVATE_KEY")
? confluxPlugin
: null,
Expand Down
39 changes: 39 additions & 0 deletions packages/client-twitter/src/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import {
} from "@ai16z/eliza";
import { ClientBase } from "./base";
import { buildConversationThread, sendTweet, wait } from "./utils.ts";
import {
generateText
} from "@ai16z/eliza/src/generation.ts";
import { PizzaAPI } from "./pizza.ts";
import {
pizzaDecisionFooter,
parsePizzaDecisionFromText
} from "@ai16z/eliza/src/parsing.ts";

export const twitterMessageHandlerTemplate =
`
Expand Down Expand Up @@ -282,6 +290,37 @@ export class TwitterInteractionClient {
this.client.saveRequestMessage(message, state);
}

const pizzaCheck = `
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should this directly be added to the twitter client and if so should it be configurable as it seems like overkill to always run

You are checking to see if someone is asking you to order a pizza.
They should explicitly ask for a pizza order.

Here is the tweet they posted:
${currentPost}`
+ pizzaDecisionFooter;

const pizzaCheckResponse = await generateText({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highly recommend using generateObjectV2 which returns a typesafe JSON instead of having the parse the response and hope for the best

Copy link

@den0xR den0xR Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateObjectV2 doesn't work with Groq, it throws "Error: Unknown model at getEncodingNameForModel". Is that a known thing?
in generateText it's hardcoded to gpt-4o

runtime: this.runtime,
context: pizzaCheck,
modelClass: ModelClass.LARGE,
});

console.log("[PIZZA-GEN][INTERACTIONS CLIENT] PIZZA check response: ", pizzaCheckResponse, " ", currentPost);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for CRUSHING this you are the GOAT for doing this! Can we elizaLogger everywhere instead


const pizzaCheckResult = parsePizzaDecisionFromText(pizzaCheckResponse);

console.log("[PIZZA-GEN][INTERACTIONS CLIENT] PIZZA check result:", pizzaCheckResult);

if (pizzaCheckResult === "YES"){
console.log("[PIZZA-GEN][INTERACTIONS CLIENT] PIZZA check result is YES, generating pizza order");

const pizzaAPI = new PizzaAPI(this.runtime);

const result = await pizzaAPI.orderPizza();

console.log("[PIZZA-GEN][INTERACTIONS CLIENT] Order result: ", result);

}

const shouldRespondContext = composeContext({
state,
template:
Expand Down
256 changes: 256 additions & 0 deletions packages/client-twitter/src/pizza.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { IAgentRuntime } from "@ai16z/eliza";

// Types and Interfaces
interface Address {
Street: string;
City: string;
Region: string;
PostalCode: string;
}

interface CustomerInfo {
FirstName: string;
LastName: string;
Email: string;
Phone: string;
}

interface PizzaOption {
[key: string]: {
[key: string]: string;
};
}

interface Product {
Code: string;
Options: PizzaOption;
}

interface Payment {
Type: string;
Amount: number;
CardType: string;
Number: string;
Expiration: string;
SecurityCode: string;
PostalCode: string;
TipAmount: number;
}

interface OrderRequest {
Address: Address;
StoreID: string;
Products: Product[];
OrderChannel: string;
OrderMethod: string;
LanguageCode: string;
ServiceMethod: string;
Payments?: Payment[];
FirstName?: string;
LastName?: string;
Email?: string;
Phone?: string;
}

export class PizzaAPI {
private readonly BASE_URL: string;
private readonly TRACKER_URL: string;

private readonly headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Referer': 'order.dominos.com'
};

private readonly trackerHeaders = {
'dpz-language': 'en',
'dpz-market': 'UNITED_STATES',
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8'
};

constructor(private runtime: IAgentRuntime) {
this.BASE_URL = this.runtime.getSetting('API_BASE_URL') || 'https://order.dominos.com/power';
this.TRACKER_URL = this.runtime.getSetting('API_TRACKER_URL') || 'https://tracker.dominos.com/tracker-presentation-service/v2';
}

// Helper function to get required setting
private getRequiredSetting(name: string): string {
const value = this.runtime.getSetting(name);
if (!value) {
throw new Error(`Required setting ${name} is not configured`);
}
return value;
}

// Function to get customer info from settings
private getCustomerInfo(): CustomerInfo {
return {
FirstName: this.getRequiredSetting('CUSTOMER_FIRST_NAME'),
LastName: this.getRequiredSetting('CUSTOMER_LAST_NAME'),
Email: this.getRequiredSetting('CUSTOMER_EMAIL'),
Phone: this.getRequiredSetting('CUSTOMER_PHONE')
};
}

// Function to get address from settings
private getAddress(): Address {
return {
Street: this.getRequiredSetting('CUSTOMER_STREET'),
City: this.getRequiredSetting('CUSTOMER_CITY'),
Region: this.getRequiredSetting('CUSTOMER_REGION'),
PostalCode: this.getRequiredSetting('CUSTOMER_POSTAL_CODE')
};
}

// Function to get payment info from settings
private getPayment(amount: number): Payment {
return {
Type: 'CreditCard',
Amount: amount,
CardType: this.detectCardType(this.getRequiredSetting('PAYMENT_CARD_NUMBER')),
Number: this.getRequiredSetting('PAYMENT_CARD_NUMBER'),
Expiration: this.getRequiredSetting('PAYMENT_EXPIRATION'),
SecurityCode: this.getRequiredSetting('PAYMENT_CVV'),
PostalCode: this.getRequiredSetting('PAYMENT_POSTAL_CODE'),
TipAmount: parseFloat(this.getRequiredSetting('PAYMENT_TIP_AMOUNT'))
};
}

private detectCardType(cardNumber: string): string {
if (cardNumber.startsWith('4')) return 'VISA';
if (cardNumber.startsWith('5')) return 'MASTERCARD';
if (cardNumber.startsWith('34') || cardNumber.startsWith('37')) return 'AMEX';
if (cardNumber.startsWith('6')) return 'DISCOVER';
return 'UNKNOWN';
}

async findNearestStore(): Promise<any> {
const address = this.getAddress();
const encodedAddress = encodeURIComponent(address.Street);
const encodedCityState = encodeURIComponent(`${address.City}, ${address.Region}`);
const url = `${this.BASE_URL}/store-locator?s=${encodedAddress}&c=${encodedCityState}&type=Delivery`;

const response = await fetch(url, {
method: 'GET',
headers: this.headers
});
return response.json();
}

async getStoreInfo(storeId: string): Promise<any> {
const url = `${this.BASE_URL}/store/${storeId}/profile`;
const response = await fetch(url, {
method: 'GET',
headers: this.headers
});
return response.json();
}

async validateOrder(orderData: OrderRequest): Promise<any> {
const url = `${this.BASE_URL}/validate-order`;
const response = await fetch(url, {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ Order: orderData })
});
return response.json();
}

async priceOrder(orderData: OrderRequest): Promise<any> {
const url = `${this.BASE_URL}/price-order`;
const response = await fetch(url, {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ Order: orderData })
});
return response.json();
}

async placeOrder(orderData: OrderRequest): Promise<any> {
const url = `${this.BASE_URL}/place-order`;
const response = await fetch(url, {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ Order: orderData })
});
return response.json();
}

async trackOrder(): Promise<any> {
const customerPhone = this.getRequiredSetting('CUSTOMER_PHONE');
const url = `${this.TRACKER_URL}/orders?phonenumber=${customerPhone.replace(/\D/g, '')}`;
const response = await fetch(url, {
method: 'GET',
headers: this.trackerHeaders
});
return response.json();
}

async orderPizza() {
try {
// 1. Find nearest store using settings address
const storeResponse = await this.findNearestStore();
console.log('Store Response:', JSON.stringify(storeResponse, null, 2));
const storeId = storeResponse.Stores[0].StoreID;

// 2. Get store info
const storeInfo = await this.getStoreInfo(storeId);
console.log('Store Info:', JSON.stringify(storeInfo, null, 2));

// 3. Create order request
const address = this.getAddress();
const orderRequest: OrderRequest = {
Address: address,
StoreID: storeId,
Products: [{
Code: '14SCREEN',
Options: {
X: { '1/1': '1' },
C: { '1/1': '1' },
}
}],
OrderChannel: 'OLO',
OrderMethod: 'Web',
LanguageCode: 'en',
ServiceMethod: 'Delivery'
};

// 4. Validate order
const validatedOrder = await this.validateOrder(orderRequest);
console.log('Validated Order:', JSON.stringify(validatedOrder, null, 2));

// 5. Price order
const pricedOrder = await this.priceOrder(orderRequest);
console.log('Priced Order:', JSON.stringify(pricedOrder, null, 2));

// 6. Add payment and customer info for final order
const customerInfo = this.getCustomerInfo();
const finalOrder: OrderRequest = {
...orderRequest,
FirstName: customerInfo.FirstName,
LastName: customerInfo.LastName,
Email: customerInfo.Email,
Phone: customerInfo.Phone,
Payments: [this.getPayment(pricedOrder.Order.Amounts.Customer)]
};

// 7. Place order
const placedOrder = await this.placeOrder(finalOrder);
console.log('Placed Order:', JSON.stringify(placedOrder, null, 2));

// 8. Track order
const trackingInfo = await this.trackOrder();
console.log('Tracking Info:', JSON.stringify(trackingInfo, null, 2));

return {
storeInfo,
orderDetails: placedOrder,
tracking: trackingInfo
};
} catch (error) {
console.error('Error ordering pizza:', error);
throw error;
}
}
}
Loading
Loading