Auction-Arena-Demo / src /auctioneer_base.py
jiangjiechen's picture
init app
8acb22e
import re
from typing import List, Dict
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.callbacks import get_openai_callback
from pydantic import BaseModel
from collections import defaultdict
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
import random
import inflect
from .bidder_base import Bidder
from .human_bidder import HumanBidder
from .item_base import Item
from .prompt_base import PARSE_BID_INSTRUCTION
p = inflect.engine()
class Auctioneer(BaseModel):
enable_discount: bool = False
items: List[Item] = []
cur_item: Item = None
highest_bidder: Bidder = None
highest_bid: int = -1
bidding_history = defaultdict(list) # history about the bidding war of one item
items_queue: List[Item] = [] # updates when a item is taken.
auction_logs = defaultdict(list) # history about the bidding war of all items
openai_cost = 0
prev_round_max_bid: int = -1
min_bid: int = 0
fail_to_sell = False
min_markup_pct = 0.1
class Config:
arbitrary_types_allowed = True
def init_items(self, items: List[Item]):
for item in items:
# reset discounted price
item.reset_price()
self.items = items
self.items_queue = items.copy()
def summarize_items_info(self):
desc = ''
for item in self.items:
desc += f"- {item.get_desc()}\n"
return desc.strip()
def present_item(self):
cur_item = self.items_queue.pop(0)
self.cur_item = cur_item
return cur_item
def shuffle_items(self):
random.shuffle(self.items)
self.items_queue = self.items.copy()
def record_bid(self, bid_info: dict, bid_round: int):
'''
Save the bidding history for each round, log the highest bidder and highest bidding
'''
# bid_info: {'bidder': xxx, 'bid': xxx, 'raw_msg': xxx}
self.bidding_history[bid_round].append(bid_info)
for hist in self.bidding_history[bid_round]:
if hist['bid'] > 0:
if self.highest_bid < hist['bid']:
self.highest_bid = hist['bid']
self.highest_bidder = hist['bidder']
elif self.highest_bid == hist['bid']:
# random if there's a tie
self.highest_bidder = random.choice([self.highest_bidder, hist['bidder']])
self.auction_logs[f"{self.cur_item.get_desc()}"].append(
{'bidder': bid_info['bidder'],
'bid': bid_info['bid'],
'bid_round': bid_round})
def _biddings_to_string(self, bid_round: int):
'''
Return a string that summarizes the bidding history in a round
'''
# bid_hist_text = '' if bid_round == 0 else f'- {self.highest_bidder}: ${self.highest_bid}\n'
bid_hist_text = ''
for js in self.bidding_history[bid_round]:
if js['bid'] < 0:
bid_hist_text += f"- {js['bidder']} withdrew\n"
else:
bid_hist_text += f"- {js['bidder']}: ${js['bid']}\n"
return bid_hist_text.strip()
def all_bidding_history_to_string(self):
bid_hist_text = ''
for bid_round in self.bidding_history:
bid_hist_text += f"Round {bid_round}:\n{self._biddings_to_string(bid_round)}\n\n"
return bid_hist_text.strip()
def ask_for_bid(self, bid_round: int):
'''
Ask for bid, return the message to be sent to bidders
'''
if self.highest_bidder is None:
if bid_round > 0:
msg = f"Seeing as we've had no takers at the initial price, we're going to lower the starting bid to ${self.cur_item.price} for {self.cur_item.name} to spark some interest! Do I have any takers?"
else:
remaining_items = [self.cur_item.name] + [item.name for item in self.items_queue]
msg = f"Attention, bidders! {len(remaining_items)} item(s) left, they are: {', '.join(remaining_items)}.\n\nNow, please bid on {self.cur_item}. The starting price for bidding for {self.cur_item} is ${self.cur_item.price}. Anyone interested in this item?"
else:
bidding_history = self._biddings_to_string(bid_round - 1)
msg = f"Thank you! This is the {p.ordinal(bid_round)} round of bidding for this item:\n{bidding_history}\n\nNow we have ${self.highest_bid} from {self.highest_bidder.name} for {self.cur_item.name}. The minimum increase over this highest bid is ${int(self.cur_item.price * self.min_markup_pct)}. Do I have any advance on ${self.highest_bid}?"
return msg
def ask_for_rebid(self, fail_msg: str, bid_price: int):
return f"Your bid of ${bid_price} failed, because {fail_msg}: You must reconsider your bid."
def get_hammer_msg(self):
if self.highest_bidder is None:
return f"Since no one bid on {self.cur_item.name}, we'll move on to the next item."
else:
return f"Sold! {self.cur_item} to {self.highest_bidder} at ${self.highest_bid}! The true value for {self.cur_item} is ${self.cur_item.true_value}."# Thus {self.highest_bidder}'s profit by winning this item is ${self.cur_item.true_value - self.highest_bid}."
def check_hammer(self, bid_round: int):
# check if the item is sold
self.fail_to_sell = False
num_bid = self._num_bids_in_round(bid_round)
# highest_bidder has already been updated in record_bid().
# so when num_bid == 0 & highest_bidder is None, it means no one bid on this item
if self.highest_bidder is None:
if num_bid == 0:
# failed to sell, as there is no highest bidder
self.fail_to_sell = True
if self.enable_discount and bid_round < 3:
# lower the starting price by 50%. discoutn only applies to the first 3 rounds
self.cur_item.lower_price(0.5)
is_sold = False
else:
is_sold = True
else:
# won't happen
raise ValueError(f"highest_bidder is None but num_bid is {num_bid}")
else:
if self.prev_round_max_bid < 0 and num_bid == 1:
# only one bidder in the first round
is_sold = True
else:
self.prev_round_max_bid = self.highest_bid
is_sold = self._num_bids_in_round(bid_round) == 0
return is_sold
def _num_bids_in_round(self, bid_round: int):
# check if there is no bid in the current round
cnt = 0
for hist in self.bidding_history[bid_round]:
if hist['bid'] > 0:
cnt += 1
return cnt
def hammer_fall(self):
print(f'* Sold! {self.cur_item} (${self.cur_item.true_value}) goes to {self.highest_bidder} at ${self.highest_bid}.')
self.auction_logs[f"{self.cur_item.get_desc()}"].append({
'bidder': self.highest_bidder,
'bid': f"{self.highest_bid} (${self.cur_item.true_value})", # no need for the first $, as it will be added in the self.log()
'bid_round': 'Hammer price (true value)'})
self.cur_item = None
self.highest_bidder = None
self.highest_bid = -1
self.bidding_history = defaultdict(list)
self.prev_round_max_bid = -1
self.fail_to_sell = False
def end_auction(self):
return len(self.items_queue) == 0
def gather_all_status(self, bidders: List[Bidder]):
status = {}
for bidder in bidders:
status[bidder.name] = {
'profit': bidder.profit,
'items_won': bidder.items_won
}
return status
def parse_bid(self, text: str):
prompt = PARSE_BID_INSTRUCTION.format(response=text)
with get_openai_callback() as cb:
llm = ChatOpenAI(model='gpt-3.5-turbo-0613', temperature=0)
result = llm([HumanMessage(content=prompt)]).content
self.openai_cost += cb.total_cost
bid_number = re.findall(r'\$?\d+', result.replace(',', ''))
# find number in the result
if '-1' in result:
return -1
elif len(bid_number) > 0:
return int(bid_number[-1].replace('$', ''))
else:
print('* Rebid:', text)
return None
def log(self, bidder_personal_reports: list = [], show_model_name=True):
''' example
Apparatus H, starting at $1000.
1st bid:
Bidder 1 (gpt-3.5-turbo-16k-0613): $1200
Bidder 2 (gpt-3.5-turbo-16k-0613): $1100
Bidder 3 (gpt-3.5-turbo-16k-0613): Withdrawn
Bidder 4 (gpt-3.5-turbo-16k-0613): $1200
2nd bid:
Bidder 1 (gpt-3.5-turbo-16k-0613): Withdrawn
Bidder 2 (gpt-3.5-turbo-16k-0613): Withdrawn
Hammer price:
Bidder 4 (gpt-3.5-turbo-16k-0613): $1200
'''
markdown_output = "## Auction Log\n\n"
for i, (item, bids) in enumerate(self.auction_logs.items()):
markdown_output += f"### {i+1}. {item}\n\n"
cur_bid_round = -1
for i, bid in enumerate(bids):
if bid['bid_round'] != cur_bid_round:
cur_bid_round = bid['bid_round']
if isinstance(bid['bid_round'], int):
markdown_output += f"\n#### {p.ordinal(bid['bid_round']+1)} bid:\n\n"
else:
markdown_output += f"\n#### {bid['bid_round']}:\n\n"
bid_price = f"${bid['bid']}" if bid['bid'] != -1 else 'Withdrew'
if isinstance(bid['bidder'], Bidder) or isinstance(bid['bidder'], HumanBidder):
if show_model_name:
markdown_output += f"* {bid['bidder']} ({bid['bidder'].model_name}): {bid_price}\n"
else:
markdown_output += f"* {bid['bidder']}: {bid_price}\n"
else:
markdown_output += f"* None bid\n"
markdown_output += "\n"
if len(bidder_personal_reports) != 0:
markdown_output += f"\n## Personal Report"
for report in bidder_personal_reports:
markdown_output += f"\n\n{report}"
return markdown_output.strip()
def finish_auction(self):
self.auction_logs = defaultdict(list)
self.cur_item = None
self.highest_bidder = None
self.highest_bid = -1
self.bidding_history = defaultdict(list)
self.items_queue = []
self.items = []
self.prev_round_max_bid = -1
self.fail_to_sell = False
self.min_bid = 0