source: ANL2022/rg_agent/rg_agent.py

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

#6 added ANAC2022 parties

File size: 11.0 KB
Line 
1import logging
2import numpy as np
3
4from random import randint
5from time import time
6from typing import cast
7
8from geniusweb.actions.Accept import Accept
9from geniusweb.actions.Action import Action
10from geniusweb.actions.Offer import Offer
11from geniusweb.actions.PartyId import PartyId
12from geniusweb.bidspace.AllBidsList import AllBidsList
13from geniusweb.inform.ActionDone import ActionDone
14from geniusweb.inform.Finished import Finished
15from geniusweb.inform.Inform import Inform
16from geniusweb.inform.Settings import Settings
17from geniusweb.inform.YourTurn import YourTurn
18from geniusweb.issuevalue.Bid import Bid
19from geniusweb.issuevalue.Domain import Domain
20from geniusweb.party.Capabilities import Capabilities
21from geniusweb.party.DefaultParty import DefaultParty
22from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import (
23 LinearAdditiveUtilitySpace,
24)
25from geniusweb.profileconnection.ProfileConnectionFactory import (
26 ProfileConnectionFactory,
27)
28from geniusweb.progress.ProgressTime import ProgressTime
29from geniusweb.references.Parameters import Parameters
30from tudelft_utilities_logging.ReportToLogger import ReportToLogger
31
32from agents.template_agent.utils.opponent_model import OpponentModel
33
34
35class RGAgent(DefaultParty):
36 """
37 @brief: Raviv-Gavriely negotiation agent of a Python GeniusWeb agent.
38 """
39
40 def __init__(self) -> None:
41 super().__init__()
42 self.logger: ReportToLogger = self.getReporter()
43
44 self.domain: Domain = None
45 self.parameters: Parameters = None
46 self.profile: LinearAdditiveUtilitySpace = None
47 self.progress: ProgressTime = None
48 self.me: PartyId = None
49 self.other: str = None
50 self.settings: Settings = None
51 self.storage_dir: str = None
52
53 self.last_received_bid: Bid = None
54 self.opponent_model: OpponentModel = None
55 self.logger.log(logging.INFO, "party is initialized")
56
57 # Our parameters:
58 self.max_acceptance_threshold = 0.9 # From optimal
59 self.min_acceptance_threshold = 0.5 # From optimal
60 self.compromising_factor = 4 # Higher value means compromise later
61 self.bids_to_consider = 800
62 self.optimal_bid = None
63 self.best_opponent_bid = None
64 self.all_previous_bids = []
65
66 def notifyChange(self, data: Inform) -> None:
67 """
68 @brief: Notify on a change.
69
70 This is the entry point of all interaction with your agent after is has been initialized.
71 How to handle the received data is based on its class type.
72
73 @param info: Contains either a request for action or information.
74
75 @return: None.
76 """
77
78 # A Settings message is the first message that will be send to your
79 # agent containing all the information about the negotiation session.
80 if isinstance(data, Settings):
81 self.settings = cast(Settings, data)
82 self.me = self.settings.getID()
83
84 # Progress towards the deadline has to be tracked manually through the use of the Progress object
85 self.progress = self.settings.getProgress()
86
87 self.parameters = self.settings.getParameters()
88 self.storage_dir = self.parameters.get("storage_dir")
89
90 # The profile contains the preferences of the agent over the domain
91 profile_connection = ProfileConnectionFactory.create(
92 data.getProfile().getURI(), self.getReporter()
93 )
94 self.profile = profile_connection.getProfile()
95 self.domain = self.profile.getDomain()
96 # Calculate best bid and threshold:
97 all_bids = AllBidsList(self.profile.getDomain())
98 optimal_bid = (None, 0)
99 for current_bid in all_bids:
100 current_bid_utility = self.profile.getUtility(current_bid)
101 if current_bid_utility > optimal_bid[1]:
102 optimal_bid = (current_bid, current_bid_utility)
103 self.optimal_bid = optimal_bid[0]
104 self.max_acceptance_threshold *= float(optimal_bid[1])
105 self.min_acceptance_threshold *= float(optimal_bid[1])
106
107 profile_connection.close()
108
109 # ActionDone informs you of an action (an offer or an accept)
110 # that is performed by one of the agents (including yourself).
111 elif isinstance(data, ActionDone):
112 action = cast(ActionDone, data).getAction()
113 actor = action.getActor()
114
115 # Ignore action if it is our action
116 if actor != self.me:
117 # Obtain the name of the opponent, cutting of the position ID.
118 self.other = str(actor).rsplit("_", 1)[0]
119
120 # Process action done by opponent
121 self.opponent_action(action)
122 # YourTurn notifies you that it is your turn to act
123 elif isinstance(data, YourTurn):
124 # Execute a turn
125 self.my_turn()
126
127 # Finished will be send if the negotiation has ended (through agreement or deadline)
128 elif isinstance(data, Finished):
129 self.save_data()
130 # terminate the agent MUST BE CALLED
131 self.logger.log(logging.INFO, "party is terminating:")
132 super().terminate()
133 else:
134 self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data))
135
136 def getCapabilities(self) -> Capabilities:
137 """
138 @brief: Returns the capability of the agent.
139
140 Method to indicate to the protocol what the capabilities of this agent are.
141
142 @return: Capabilities representation class.
143 """
144 return Capabilities(
145 set(["SAOP"]),
146 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
147 )
148
149 def send_action(self, action: Action) -> None:
150 """
151 @brief: Sends an action to the opponent(s).
152
153 @param action: action of this agent
154
155 @return: None.
156 """
157 self.getConnection().send(action)
158
159 def getDescription(self) -> str:
160 """
161 @brief: Returns a description of your agent.
162
163 @return: Agent description.
164 """
165 return "Raviv-Gavriely description for the ANL 2022 competition"
166
167 def opponent_action(self, action: Action) -> None:
168 """
169 @brief: Process an action that was received from the opponent.
170
171 @param: Action of opponent.
172
173 @return: None.
174 """
175 # If it is an offer, set the last received bid:
176 if isinstance(action, Offer):
177 # Create opponent model if it was not yet initialized
178 if self.opponent_model is None:
179 self.opponent_model = OpponentModel(self.domain)
180
181 bid = cast(Offer, action).getBid()
182
183 # Update opponent model with bid
184 self.opponent_model.update(bid)
185 # Set bid as last received
186 self.last_received_bid = bid
187 # Keep track of all the bids and which bid is the best
188 self.all_previous_bids.append(bid)
189 if self.best_opponent_bid is None:
190 self.best_opponent_bid = bid
191 if self.profile.getUtility(bid) > self.profile.getUtility(self.best_opponent_bid):
192 self.best_opponent_bid = bid
193
194 def my_turn(self) -> None:
195 """
196 @brief: My turn.
197
198 This method is called when it is our turn. It should decide upon an action
199 to perform and send this action to the opponent.
200
201 @return: None.
202 """
203 # Check if the last received offer is good enough
204 if self.accept_condition(self.last_received_bid):
205 # If so, accept the offer
206 action = Accept(self.me, self.last_received_bid)
207 else:
208 # If not, find a bid to propose as counter offer
209 bid = self.find_bid()
210 action = Offer(self.me, bid)
211
212 # Send the action
213 self.send_action(action)
214
215 def save_data(self) -> None:
216 """
217 @brief: Saves data.
218
219 This method is called after the negotiation is finished. It can be used to store data
220 for learning capabilities. Note that no extensive calculations can be done within this method.
221 Taking too much time might result in your agent being killed, so use it for storage only.
222
223 @return: None.
224 """
225 data = "Data for learning (see README.md)"
226 with open(f"{self.storage_dir}/data.md", "w") as f:
227 f.write(data)
228
229 def accept_condition(self, bid: Bid) -> bool:
230 """
231 @brief: Accept the given bid.
232
233 @return: Boolean indicator if to accept/reject the bid.
234 """
235 if bid is None:
236 return False
237
238 # Progress of the negotiation session between 0 and 1 (1 is deadline)
239 progress = self.progress.get(time() * 1000)
240
241 acceptance_threshold = -np.exp(self.compromising_factor * progress)
242 acceptance_threshold /= np.exp(self.compromising_factor) - 1 # scale to 1
243 acceptance_threshold *= self.max_acceptance_threshold - self.min_acceptance_threshold
244 acceptance_threshold += self.max_acceptance_threshold
245
246 return self.profile.getUtility(bid) >= acceptance_threshold
247
248 def find_bid(self) -> Bid:
249 """
250 @brief: Finds the bid.
251
252 @return: The chosen bid.
253 """
254 # Compose a list of all possible bids
255 domain = self.profile.getDomain()
256 all_bids = AllBidsList(domain)
257
258 best_bid_score = 0.0
259 best_bid = None
260
261 # Take X attempts to find a bid according to a heuristic score
262 for _ in range(self.bids_to_consider):
263 bid = all_bids.get(randint(0, all_bids.size() - 1))
264 bid_score = self.score_bid(bid)
265 if bid_score > best_bid_score:
266 best_bid_score, best_bid = bid_score, bid
267 if self.accept_condition(best_bid):
268 return best_bid
269 else:
270 return self.optimal_bid
271
272 def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float:
273 """
274 @brief: Calculate heuristic score for a bid.
275
276 @param bid: Bid to score
277 @param alpha: Trade-off factor between self interested and
278 altruistic behavior. Defaults to 0.95.
279 @param eps: Time pressure factor, balances between conceding
280 and Boulware behavior over time. Defaults to 0.1.
281
282 Returns:
283 float: score
284 """
285 progress = self.progress.get(time() * 1000)
286
287 our_utility = float(self.profile.getUtility(bid))
288
289 time_pressure = 1.0 - progress ** (1 / eps)
290 score = alpha * time_pressure * our_utility
291
292 if self.opponent_model is not None:
293 opponent_utility = self.opponent_model.get_predicted_utility(bid)
294 opponent_score = (1.0 - alpha * time_pressure) * opponent_utility
295 score += opponent_score
296
297 return score
Note: See TracBrowser for help on using the repository browser.