source: ANL2022/procrastin_agent/procrastin_agent.py

Last change on this file was 75, checked in by wouter, 21 months ago

#6 added ANAC2022 parties

File size: 16.5 KB
Line 
1from decimal import Decimal
2import logging
3import json
4from os import path
5from random import randint
6from re import A
7from time import time
8from typing import Optional, cast
9
10from geniusweb.actions.Accept import Accept
11from geniusweb.actions.Action import Action
12from geniusweb.actions.Offer import Offer
13from geniusweb.actions.PartyId import PartyId
14from geniusweb.bidspace.AllBidsList import AllBidsList
15from geniusweb.bidspace.BidsWithUtility import BidsWithUtility
16from geniusweb.bidspace.Interval import Interval
17from geniusweb.inform.ActionDone import ActionDone
18from geniusweb.inform.Finished import Finished
19from geniusweb.inform.Inform import Inform
20from geniusweb.inform.Settings import Settings
21from geniusweb.inform.YourTurn import YourTurn
22from geniusweb.issuevalue.Bid import Bid
23from geniusweb.issuevalue.Domain import Domain
24from geniusweb.party.Capabilities import Capabilities
25from geniusweb.party.DefaultParty import DefaultParty
26from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import (
27 LinearAdditiveUtilitySpace,
28)
29from geniusweb.profileconnection.ProfileConnectionFactory import (
30 ProfileConnectionFactory,
31)
32from geniusweb.progress.ProgressTime import ProgressTime
33from geniusweb.references.Parameters import Parameters
34from numpy import append
35from tudelft_utilities_logging.ReportToLogger import ReportToLogger
36from .utils import opponent_model
37
38from .utils.opponent_model import OpponentModel
39from .utils.time_estimator import TimeEstimator
40from .utils.bid_chooser_2 import BidChooser
41from .utils.strategy_model import StrategyModel
42
43# Some testing flags
44test_use_accept = True
45class ProcrastinAgent(DefaultParty):
46 """
47 The Mild Bunch Team's Python geniusweb agent.
48 """
49
50 def __init__(self):
51 super().__init__()
52 self.logger: ReportToLogger = self.getReporter()
53
54 self.domain: Domain = None
55 self.parameters: Parameters = None
56 self.profile: LinearAdditiveUtilitySpace = None
57 self.progress: ProgressTime = None
58 self.me: PartyId = None
59 self.other: str = None
60 self.settings: Settings = None
61 self.storage_dir: str = None
62 self.strategy_model = None
63
64 self.last_received_bid: Bid = None
65 self.opponent_best_bid: Bid = None
66 self.opp_best_self_util: Bid = None
67 self.opponent_concession_bid: Bid = None
68 self.opp_concession_self_util: Bid = 0.0
69 self.alpha: float = 0.5
70 self.lowest_acceptable: float = 1.0
71 self.opponent_model: OpponentModel = None
72 self.bid_chooser: BidChooser = None
73 self.opponent_data: dict = None
74 self.time_estimator: TimeEstimator = TimeEstimator()
75 self.bids_sent: int = 0
76 self.bids_received: int = 0
77 self.logger.log(logging.INFO, "party is initialized")
78
79 self.test_bids_left = []
80
81 def getCapabilities(self) -> Capabilities:
82 """MUST BE IMPLEMENTED
83 Method to indicate to the protocol what the capabilities of this agent are.
84 Leave it as is for the ANL 2022 competition
85
86 Returns:
87 Capabilities: Capabilities representation class
88 """
89 return Capabilities(
90 set(["SAOP"]),
91 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
92 )
93
94 def getDescription(self) -> str:
95 """MUST BE IMPLEMENTED
96
97 Returns:
98 str: Agent description
99 """
100 return "The Mild Bunch's ProcrastinAgent for the ANL 2022 competition." \
101 " This agent puts off concesssion till the end of the negotiation."
102 " It's developers are also procrastinagents! The Procrastin-A-Team!"
103
104 def send_action(self, action: Action):
105 """Sends an action to the opponent(s)
106
107 Args:
108 action (Action): action of this agent
109 """
110 self.getConnection().send(action)
111
112 def extract_name(self, party: PartyId) -> str:
113 return str(party).rsplit("_", 1)[0]
114
115 def current_time(self) -> float:
116 return self.progress.get(time() * 1000)
117
118 def notifyChange(self, data: Inform):
119 """MUST BE IMPLEMENTED
120 This is the entry point of all interaction with your agent after is has been initialised.
121 How to handle the received data is based on its class type.
122
123 Args:
124 data (Inform): Contains either a request for action or information.
125 """
126
127 # a Settings message is the first message that will be send to your
128 # agent containing all the information about the negotiation session.
129 if isinstance(data, Settings):
130 self.settings = cast(Settings, data)
131 self.me = self.settings.getID()
132
133 # progress towards the deadline has to be tracked manually through the use of the Progress object
134 self.progress = self.settings.getProgress()
135
136 self.parameters = self.settings.getParameters()
137 self.storage_dir = self.parameters.get("storage_dir")
138
139 # the profile contains the preferences of the agent over the domain
140 profile_connection = ProfileConnectionFactory.create(
141 data.getProfile().getURI(), self.getReporter()
142 )
143 self.profile = profile_connection.getProfile()
144 self.domain = self.profile.getDomain()
145 profile_connection.close()
146
147 # ActionDone informs you of an action (an offer or an accept)
148 # that is performed by one of the agents (including yourself).
149 elif isinstance(data, ActionDone):
150 action = cast(ActionDone, data).getAction()
151 actor = action.getActor()
152
153 # ignore action if it is our action
154 if actor != self.me:
155 if self.other is None:
156 # obtain the name of the opponent, cutting of the position ID.
157 self.other = self.extract_name(actor)
158 # now that the name of the opponent is known, we load our stored data about them
159 self.load_data()
160
161 # process action done by opponent
162 self.opponent_action(action)
163 # YourTurn notifies you that it is your turn to act
164 elif isinstance(data, YourTurn):
165 # execute a turn
166 self.my_turn()
167
168 # Finished will be send if the negotiation has ended (through agreement or deadline)
169 elif isinstance(data, Finished):
170 finished = cast(Finished, data)
171 self.save_data(finished)
172 # terminate the agent MUST BE CALLED
173 self.logger.log(logging.INFO, "party is terminating:")
174 super().terminate()
175 else:
176 self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data))
177
178 def opponent_action(self, action: Action):
179 """Process an action that was received from the opponent.
180
181 Args:
182 action (Action): action of opponent
183 """
184 if isinstance(action, Accept):
185 # opponent accepted, no response necessary
186 pass
187
188 # if it is an offer, set the last received bid
189 if isinstance(action, Offer):
190 offer = cast(Offer, action)
191 self.bids_received += 1
192 self.process_opponent_offer(offer)
193
194 def my_turn(self):
195 """This method is called when it is our turn. It should decide upon an action
196 to perform and send this action to the opponent.
197 """
198 # log opponent time
199 self.time_estimator.self_times_add(self.current_time())
200 # check if the last received offer is good enough
201 if self.choose_accept(self.last_received_bid):
202 # if so, accept the offer
203 action = Accept(self.me, self.last_received_bid)
204 else:
205 # if not, find a bid to propose as counter offer
206 bid = self.choose_bid()
207 action = Offer(self.me, bid)
208 self.bids_sent += 1
209
210 # log self time
211 self.time_estimator.opp_times_add(self.current_time())
212 # send the action
213 self.send_action(action)
214
215################################################################################################
216################################### Our Implementation ###################################
217################################################################################################
218
219 def load_data(self):
220 # load_data is called as soon as the opponent is known.
221 # In the very rare case where the opponent never makes an offer, load_data is never called.
222 if not path.exists(f"{self.storage_dir}/{self.other}.json"):
223 # First round
224 new_data = {}
225 new_data["count"] = 0
226 new_data["self_accepts"] = 0
227 new_data["did_accept"] = []
228 new_data["opponent_accepts"] = 0
229 new_data["no_accepts"] = 0
230 new_data["beta_values"] = []
231 new_data["time_factor"] = 1.0
232 new_data["alphas"] = []
233 new_data["alpha_achieved"] = []
234 self.opponent_data = new_data
235 else:
236 # Not first round
237 with open(f"{self.storage_dir}/{self.other}.json", "r") as f:
238 self.opponent_data = json.load(f)
239 self.time_estimator.update_time_factor(self.opponent_data["time_factor"])
240
241 def choose_bid(self) -> Bid:
242 if self.bids_sent <= 5:
243 # Action to take before we have a decent estimate at how many turns are left
244 # Send best bid
245 bid_dict = {}
246 for issue, valueset in self.profile.getUtilities().items():
247 bid_dict[issue] = max(self.domain.getValues(issue), key = lambda v: valueset.getUtility(v))
248 best_bid = Bid(bid_dict)
249 return best_bid
250
251 offers_left = self.time_estimator.turns_left(self.current_time())
252 self.test_bids_left.append(offers_left)
253 return self.bid_chooser.choose_bid(offers_left, self.current_time())
254
255 def process_opponent_offer(self, offer: Offer):
256 """Process an offer that was received from the opponent.
257
258 Args:
259 offer (Offer): offer from opponent
260 """
261 # create opponent model if it was not yet initialised
262 if self.opponent_model is None:
263 self.opponent_model = OpponentModel(self.domain)
264 self.bid_chooser = BidChooser(self.profile, self.opponent_model, 0.5) # TODO update lowest acceptable number
265
266 bid = offer.getBid()
267
268 # set bid as last received
269 self.last_received_bid = bid
270 # update opponent model with bid
271 self.opponent_model.update(bid, self.current_time())
272
273 #set opp_highest_bid to either the first or calculated highest (opp POV) opponent bid
274 update_opponent_best = False
275 if self.opponent_best_bid is None:
276 update_opponent_best = True
277 elif self.opponent_model.get_predicted_utility(bid)[0] > self.opponent_model.get_predicted_utility(self.opponent_best_bid)[0]:
278 #update opp_best_bid if new opponent bid is better for them than previous best
279 update_opponent_best = True
280 if update_opponent_best:
281 self.opponent_best_bid = bid
282 self.opp_best_self_util = float(self.profile.getUtility(bid))
283 if self.strategy_model is None:
284 self.strategy_model = StrategyModel(self.opponent_data["alphas"], self.opponent_data["beta_values"], self.opponent_data["did_accept"])
285 self.alpha = self.strategy_model.max_u(self.opp_best_self_util, 0.5, 1.0, mag = 3)
286 self.lowest_acceptable = self.opp_best_self_util + self.alpha * (1.0 - self.opp_best_self_util)
287 self.bid_chooser.update_lowest_acceptable(self.lowest_acceptable)
288
289 #set opp_concession_bid to either the first or highest (self POV) opponent bid
290 update_opponent_concession = False
291 if self.opponent_concession_bid is None:
292 update_opponent_concession = True
293 elif float(self.profile.getUtility(bid)) > self.opp_concession_self_util:
294 update_opponent_concession = True
295 if update_opponent_concession:
296 self.opponent_concession_bid = bid
297 self.opp_concession_self_util = float(self.profile.getUtility(bid))
298
299 # update bid_chooser
300 self.bid_chooser.update_bid(bid)
301
302 def choose_accept(self, bid: Bid) -> bool:
303 if bid is None:
304 return False
305
306 # very basic approach that accepts if the offer is valued above a certain amount of opponent's highest utility, calculated
307 # through formula t = b + alpha (1.0 - b)
308 # only accepts during last 20 turns or last 1/1000 of the negotiation (or our best was offered)
309 bid_util = float(self.profile.getUtility(bid))
310 time = self.current_time()
311 conditions = [
312 bid_util >= max(self.lowest_acceptable, self.opp_concession_self_util),
313 any([
314 self.time_estimator.turns_left(time) < 20,
315 time > 0.999,
316 bid_util >= 1.0,
317 ]),
318 #test_use_accept, # Tests agent behaviour without accepting
319 ]
320 return all(conditions)
321
322 def save_data(self, finished: Finished):
323 """This method is called after the negotiation is finished. It can be used to store data
324 for learning capabilities. Note that no extensive calculations can be done within this method.
325 Taking too much time might result in your agent being killed, so use it for storage only.
326 """
327 agreements = list(finished.getAgreements().getAgreements().items())
328 save = self.opponent_data
329 save["count"] += 1
330 save["test_bid_pool_size"] = len(self.bid_chooser.bid_pool)
331 save["test_time_list_self"] = self.time_estimator.self_times
332 #save["test_time_list_opp"] = self.time_estimator.opp_times_adj
333 save["test_offers_left"] = self.test_bids_left
334 save["self_diff"] = self.time_estimator.self_diff
335
336 opp_stuff = {"weights": {}}
337 total_weight = 0.0
338 for issue in self.domain.getIssues():
339 opp_stuff[issue] = {}
340 total_weight += self.opponent_model.issue_estimators[issue].weight
341 opp_stuff["weights"][issue] = self.opponent_model.issue_estimators[issue].weight
342 for value in self.domain.getValues(issue):
343 opp_stuff[issue][value.getValue()] = self.opponent_model.issue_estimators[issue].get_value_utility(value)
344 for issue in self.domain.getIssues():
345 opp_stuff["weights"][issue] = self.opponent_model.issue_estimators[issue].weight / total_weight
346 save["opponent_model"] = opp_stuff
347
348 beta = float((self.opp_concession_self_util-self.opp_best_self_util)/(1 - self.opp_best_self_util))
349
350 save["beta_values"].append(beta)
351
352 if not agreements:
353 agreement_bid = None
354 agreement_party = None
355 save["time_factor"] = self.time_estimator.get_new_time_factor(self.test_bids_left, len(self.bid_chooser.bid_pool))
356 save["did_accept"].append(False)
357 else:
358 agreement = agreements[0]
359 agreement_bid = agreement[1]
360 agreement_party = agreement[0]
361 save["did_accept"].append(True)
362 if agreement_party is None:
363 # No agreement was made (or rarely they accepted our first bid)
364 save["no_accepts"] += 1
365 elif self.extract_name(agreement_party) == self.extract_name(self.me):
366 # We sent the agreement
367 save["self_accepts"] += 1
368 elif (self.other is not None) and self.extract_name(agreement_party) == self.other:
369 # They accepted
370 save["opponent_accepts"] += 1
371 else:
372 # Only way I can imagine getting here is if we offered
373 # the first bid and the opponent accepted.
374 save["other_accepts"] = save.get("other_accepts", 0) + 1
375 pass
376
377 if agreement_bid is None:
378 alpha_achieved = 0.0
379 else:
380 alpha_achieved = (float(self.profile.getUtility(agreement_bid)) - self.opp_best_self_util) / (1.0 - self.opp_best_self_util)
381 save["alphas"].append(self.alpha)
382 save["alpha_achieved"].append(alpha_achieved)
383
384 with open(f"{self.storage_dir}/{self.other}.json", "w") as f:
385 json.dump(save, f)
Note: See TracBrowser for help on using the repository browser.