-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Changes from all commits
e425d35
0d33b02
2df768d
a4900a9
d90b614
975c76a
4f2a81a
4ed46f7
76d9e61
f68c66f
2256335
bfe6504
630c4fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,25 @@ | ||
import { SearchMode, Tweet } from "agent-twitter-client"; | ||
import { | ||
composeContext, | ||
generateMessageResponse, | ||
generateShouldRespond, | ||
messageCompletionFooter, | ||
shouldRespondFooter, | ||
Content, | ||
HandlerCallback, | ||
IAgentRuntime, | ||
Memory, | ||
ModelClass, | ||
State, | ||
stringToUuid, | ||
composeContext, | ||
elizaLogger, | ||
generateMessageResponse, | ||
generateShouldRespond, | ||
generateText, | ||
getEmbeddingZeroVector, | ||
messageCompletionFooter, | ||
shouldRespondFooter, | ||
stringToUuid, | ||
parsePizzaDecisionFromText, | ||
pizzaDecisionFooter | ||
} from "@elizaos/core"; | ||
import { SearchMode, Tweet } from "agent-twitter-client"; | ||
import { ClientBase } from "./base"; | ||
import { PizzaAPI } from "./pizza.ts"; | ||
import { buildConversationThread, sendTweet, wait } from "./utils.ts"; | ||
|
||
export const twitterMessageHandlerTemplate = | ||
|
@@ -380,7 +384,40 @@ export class TwitterInteractionClient { | |
|
||
// get usernames into str | ||
const validTargetUsersStr = | ||
this.client.twitterConfig.TWITTER_TARGET_USERS.join(","); | ||
this.client.twitterConfig.TWITTER_TARGET_USERS.join(","); | ||
|
||
|
||
const pizzaCheck = ` | ||
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({ | ||
runtime: this.runtime, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
context: pizzaCheck, | ||
modelClass: ModelClass.LARGE, | ||
}); | ||
|
||
console.log("[PIZZA-GEN][INTERACTIONS CLIENT] PIZZA check response: ", pizzaCheckResponse, " ", currentPost); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
import { IAgentRuntime } from "@elizaos/core"; | ||
|
||
// 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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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