source: ANL2022/dreamteam109_agent/dreamteam109_agent.py

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

#6 added ANAC2022 parties

File size: 16.0 KB
Line 
1import datetime
2import json
3import logging
4from math import floor
5from random import randint
6import time
7from decimal import Decimal
8from os import path
9from typing import TypedDict, cast
10
11from geniusweb.actions.Accept import Accept
12from geniusweb.actions.Action import Action
13from geniusweb.actions.Offer import Offer
14from geniusweb.actions.PartyId import PartyId
15from geniusweb.bidspace.AllBidsList import AllBidsList
16from geniusweb.inform.ActionDone import ActionDone
17from geniusweb.inform.Finished import Finished
18from geniusweb.inform.Inform import Inform
19from geniusweb.inform.Settings import Settings
20from geniusweb.inform.YourTurn import YourTurn
21from geniusweb.issuevalue.Bid import Bid
22from geniusweb.issuevalue.Domain import Domain
23from geniusweb.party.Capabilities import Capabilities
24from geniusweb.party.DefaultParty import DefaultParty
25from geniusweb.issuevalue.Value import Value
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 tudelft_utilities_logging.ReportToLogger import ReportToLogger
35from .utils.logger import Logger
36
37from .utils.opponent_model import OpponentModel
38from .utils.utils import bid_to_string
39
40class SessionData(TypedDict):
41 progressAtFinish: float
42 utilityAtFinish: float
43 didAccept: bool
44 isGood: bool
45 topBidsPercentage: float
46 forceAcceptAtRemainingTurns: float
47
48class DataDict(TypedDict):
49 sessions: list[SessionData]
50
51class DreamTeam109Agent(DefaultParty):
52
53 def __init__(self):
54 super().__init__()
55 self.logger: Logger = Logger(self.getReporter(), id(self))
56
57 self.domain: Domain = None
58 self.parameters: Parameters = None
59 self.profile: LinearAdditiveUtilitySpace = None
60 self.progress: ProgressTime = None
61 self.me: PartyId = None
62 self.other: PartyId = None
63 self.other_name: str = None
64 self.settings: Settings = None
65 self.storage_dir: str = None
66
67 self.data_dict: DataDict = None
68
69 self.last_received_bid: Bid = None
70 self.opponent_model: OpponentModel = None
71 self.all_bids: AllBidsList = None
72 self.bids_with_utilities: list[tuple[Bid, float]] = None
73 self.num_of_top_bids: int = 1
74 self.min_util: float = 0.9
75
76 self.round_times: list[Decimal] = []
77 self.last_time = None
78 self.avg_time = None
79 self.utility_at_finish: float = 0
80 self.did_accept: bool = False
81 self.top_bids_percentage: float = 1 / 300
82 self.force_accept_at_remaining_turns: float = 1
83 self.force_accept_at_remaining_turns_light: float = 1
84 self.opponent_best_bid: Bid = None
85 self.logger.log(logging.INFO, "party is initialized")
86
87 def notifyChange(self, data: Inform):
88 """MUST BE IMPLEMENTED
89 This is the entry point of all interaction with your agent after is has been initialised.
90 How to handle the received data is based on its class type.
91
92 Args:
93 info (Inform): Contains either a request for action or information.
94 """
95
96 # a Settings message is the first message that will be send to your
97 # agent containing all the information about the negotiation session.
98 if isinstance(data, Settings):
99 self.settings = cast(Settings, data)
100 self.me = self.settings.getID()
101
102 # progress towards the deadline has to be tracked manually through the use of the Progress object
103 self.progress = self.settings.getProgress()
104
105 self.parameters = self.settings.getParameters()
106 self.storage_dir = self.parameters.get("storage_dir")
107
108 # the profile contains the preferences of the agent over the domain
109 profile_connection = ProfileConnectionFactory.create(
110 data.getProfile().getURI(), self.getReporter()
111 )
112 self.profile = profile_connection.getProfile()
113 self.domain = self.profile.getDomain()
114 # compose a list of all possible bids
115 self.all_bids = AllBidsList(self.domain)
116
117 profile_connection.close()
118
119 # ActionDone informs you of an action (an offer or an accept)
120 # that is performed by one of the agents (including yourself).
121 elif isinstance(data, ActionDone):
122 action = cast(ActionDone, data).getAction()
123 actor = action.getActor()
124
125 # ignore action if it is our action
126 if actor != self.me:
127 if self.other is None:
128 self.other = actor
129 # obtain the name of the opponent, cutting of the position ID.
130 self.other_name = str(actor).rsplit("_", 1)[0]
131 self.attempt_load_data()
132 self.learn_from_past_sessions(self.data_dict["sessions"])
133
134 # process action done by opponent
135 self.opponent_action(action)
136 # YourTurn notifies you that it is your turn to act
137 elif isinstance(data, YourTurn):
138 # execute a turn
139 self.my_turn()
140
141 # Finished will be send if the negotiation has ended (through agreement or deadline)
142 elif isinstance(data, Finished):
143 agreements = cast(Finished, data).getAgreements()
144 if len(agreements.getMap()) > 0:
145 agreed_bid = agreements.getMap()[self.me]
146 self.logger.log(logging.INFO, "agreed_bid = " + bid_to_string(agreed_bid))
147 self.utility_at_finish = float(self.profile.getUtility(agreed_bid))
148 else:
149 self.logger.log(logging.INFO, "no agreed bid (timeout? some agent crashed?)")
150
151 self.update_data_dict()
152 self.save_data()
153
154 # terminate the agent MUST BE CALLED
155 self.logger.log(logging.INFO, "party is terminating")
156 super().terminate()
157 else:
158 self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data))
159
160 def getCapabilities(self) -> Capabilities:
161 """MUST BE IMPLEMENTED
162 Method to indicate to the protocol what the capabilities of this agent are.
163 Leave it as is for the ANL 2022 competition
164
165 Returns:
166 Capabilities: Capabilities representation class
167 """
168 return Capabilities(
169 set(["SAOP"]),
170 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
171 )
172
173 def send_action(self, action: Action):
174 """Sends an action to the opponent(s)
175
176 Args:
177 action (Action): action of this agent
178 """
179 self.getConnection().send(action)
180
181 # give a description of your agent
182 def getDescription(self) -> str:
183 """MUST BE IMPLEMENTED
184 Returns a description of your agent. 1 or 2 sentences.
185
186 Returns:
187 str: Agent description
188 """
189 return "DreamTeam109 agent for the ANL 2022 competition"
190
191 def opponent_action(self, action):
192 """Process an action that was received from the opponent.
193
194 Args:
195 action (Action): action of opponent
196 """
197 # if it is an offer, set the last received bid
198 if isinstance(action, Offer):
199 # create opponent model if it was not yet initialised
200 if self.opponent_model is None:
201 self.opponent_model = OpponentModel(self.domain, self.logger)
202
203 bid = cast(Offer, action).getBid()
204
205 # update opponent model with bid
206 self.opponent_model.update(bid)
207 # set bid as last received
208 self.last_received_bid = bid
209
210 if self.opponent_best_bid is None:
211 self.opponent_best_bid = bid
212 elif self.profile.getUtility(bid) > self.profile.getUtility(self.opponent_best_bid):
213 self.opponent_best_bid = bid
214
215 def my_turn(self):
216 """This method is called when it is our turn. It should decide upon an action
217 to perform and send this action to the opponent.
218 """
219
220 # For calculating average time per round
221 if self.last_time is not None:
222 self.round_times.append(datetime.datetime.now().timestamp() - self.last_time.timestamp())
223 self.avg_time = sum(self.round_times[-3:])/3
224 self.last_time = datetime.datetime.now()
225
226 # check if the last received offer is good enough
227 # if self.accept_condition(self.last_received_bid):
228 if self.accept_condition(self.last_received_bid):
229 self.logger.log(logging.INFO, "accepting bid : " + bid_to_string(self.last_received_bid))
230 # if so, accept the offer
231 action = Accept(self.me, self.last_received_bid)
232 self.did_accept = True
233 else:
234 # if not, find a bid to propose as counter offer
235 bid = self.find_bid()
236 self.logger.log(logging.INFO, "Offering bid : " + bid_to_string(bid))
237 action = Offer(self.me, bid)
238
239 # send the action
240 self.send_action(action)
241
242 def get_data_file_path(self) -> str:
243 return f"{self.storage_dir}/{self.other_name}.json"
244
245 def attempt_load_data(self):
246 if path.exists(self.get_data_file_path()):
247 with open(self.get_data_file_path()) as f:
248 self.data_dict = json.load(f)
249 self.logger.log(logging.INFO, "Loaded previous data about opponent: " + self.other_name)
250 self.logger.log(logging.INFO, "data_dict = " + str(self.data_dict))
251 else:
252 self.logger.log(logging.WARN, "No previous data saved about opponent: " + self.other_name)
253 # initialize an empty data dict
254 self.data_dict = {
255 "sessions": []
256 }
257
258 def update_data_dict(self):
259 # NOTE: We shouldn't do extensive calculations in this method (see note in save_data method)
260
261 progress_at_finish = self.progress.get(time.time() * 1000)
262
263 session_data: SessionData = {
264 "progressAtFinish": progress_at_finish,
265 "utilityAtFinish": self.utility_at_finish,
266 "didAccept": self.did_accept,
267 "isGood": self.utility_at_finish >= self.min_util,
268 "topBidsPercentage": self.top_bids_percentage,
269 "forceAcceptAtRemainingTurns": self.force_accept_at_remaining_turns
270 }
271
272 self.logger.log(logging.INFO, "Updating data dict with session data: " + str(session_data))
273 self.data_dict["sessions"].append(session_data)
274
275 def save_data(self):
276 """This method is called after the negotiation is finished. It can be used to store data
277 for learning capabilities. Note that no extensive calculations can be done within this method.
278 Taking too much time might result in your agent being killed, so use it for storage only.
279 """
280 if self.other_name is None:
281 self.logger.log(logging.WARNING, "Opponent name was not set; skipping save data")
282 else:
283 json_data = json.dumps(self.data_dict, sort_keys=True, indent=4)
284 with open(self.get_data_file_path(), "w") as f:
285 f.write(json_data)
286 self.logger.log(logging.INFO, "Saved data about opponent: " + self.other_name)
287
288 def learn_from_past_sessions(self, sessions: list[SessionData]):
289 accept_levels = [0, 0, 1, 1.1]
290 light_accept_levels = [0, 1, 1.1]
291 top_bids_levels = [1 / 300, 1 / 100, 1 / 30]
292
293 self.force_accept_at_remaining_turns = accept_levels[min(len(accept_levels) - 1, len(list(filter(self.did_fail, sessions))))]
294 self.force_accept_at_remaining_turns_light = light_accept_levels[min(len(light_accept_levels) - 1, len(list(filter(self.did_fail, sessions))))]
295 self.top_bids_percentage = top_bids_levels[min(len(top_bids_levels) - 1, len(list(filter(self.low_utility, sessions))))]
296
297 def did_fail(self, session: SessionData):
298 return session["utilityAtFinish"] == 0
299
300 def low_utility(self, session: SessionData):
301 return session["utilityAtFinish"] < 0.5
302
303 def accept_condition(self, bid: Bid) -> bool:
304 if bid is None:
305 return False
306
307 # progress of the negotiation session between 0 and 1 (1 is deadline)
308 progress = self.progress.get(time.time() * 1000)
309 threshold = 0.98
310 light_threshold = 0.95
311
312 if self.avg_time is not None:
313 threshold = 1 - 1000 * self.force_accept_at_remaining_turns * self.avg_time / self.progress.getDuration()
314 light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time / self.progress.getDuration()
315
316 conditions = [
317 self.profile.getUtility(bid) >= self.min_util,
318 progress >= threshold,
319 progress > light_threshold and self.profile.getUtility(bid) >= self.bids_with_utilities[floor(len(self.bids_with_utilities) / 5) - 1][1]
320 ]
321 return any(conditions)
322
323 def find_bid(self) -> Bid:
324 self.logger.log(logging.INFO, "finding bid...")
325
326 num_of_bids = self.all_bids.size()
327
328 if self.bids_with_utilities is None:
329 self.logger.log(logging.INFO, "calculating bids_with_utilities...")
330 startTime = time.time()
331 self.bids_with_utilities = []
332
333 for index in range(num_of_bids):
334 bid = self.all_bids.get(index)
335 bid_utility = float(self.profile.getUtility(bid))
336 self.bids_with_utilities.append((bid, bid_utility))
337
338 self.bids_with_utilities.sort(key=lambda tup: tup[1], reverse=True)
339
340 endTime = time.time()
341 self.logger.log(logging.INFO, "calculating bids_with_utilities took (in seconds): " + str(endTime - startTime))
342
343 self.num_of_top_bids = max(5, num_of_bids * self.top_bids_percentage)
344
345 if (self.last_received_bid is None):
346 return self.bids_with_utilities[0][0]
347
348 progress = self.progress.get(time.time() * 1000)
349 light_threshold = 0.95
350
351 if self.avg_time is not None:
352 light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time / self.progress.getDuration()
353
354 if (progress > light_threshold):
355 return self.opponent_best_bid
356
357 if (num_of_bids < self.num_of_top_bids):
358 self.num_of_top_bids = num_of_bids / 2
359
360 self.min_util = self.bids_with_utilities[floor(self.num_of_top_bids) - 1][1]
361 self.logger.log(logging.INFO, "min_util = " + str(self.min_util))
362
363 picked_ranking = randint(0, floor(self.num_of_top_bids) - 1)
364
365 return self.bids_with_utilities[picked_ranking][0]
366
367 def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float:
368 """Calculate heuristic score for a bid
369
370 Args:
371 bid (Bid): Bid to score
372 alpha (float, optional): Trade-off factor between self interested and
373 altruistic behaviour. Defaults to 0.95.
374 eps (float, optional): Time pressure factor, balances between conceding
375 and Boulware behaviour over time. Defaults to 0.1.
376
377 Returns:
378 float: score
379 """
380 progress = self.progress.get(time.time() * 1000)
381
382 our_utility = float(self.profile.getUtility(bid))
383
384 time_pressure = 1.0 - progress ** (1 / eps)
385 score = alpha * time_pressure * our_utility
386
387 if self.opponent_model is not None:
388 opponent_utility = self.opponent_model.get_predicted_utility(bid)
389 opponent_score = (1.0 - alpha * time_pressure) * opponent_utility
390 score += opponent_score
391
392 return score
Note: See TracBrowser for help on using the repository browser.