source: ANL2022/gea_agent/gea_agent.py

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

#6 added ANAC2022 parties

File size: 12.5 KB
Line 
1import logging
2from random import randint
3from time import time
4from typing import cast
5
6import pandas as pd
7from geniusweb.actions.Accept import Accept
8from geniusweb.actions.Action import Action
9from geniusweb.actions.Offer import Offer
10from geniusweb.actions.PartyId import PartyId
11from geniusweb.bidspace.AllBidsList import AllBidsList
12from geniusweb.inform.ActionDone import ActionDone
13from geniusweb.inform.Finished import Finished
14from geniusweb.inform.Inform import Inform
15from geniusweb.inform.Settings import Settings
16from geniusweb.inform.YourTurn import YourTurn
17from geniusweb.issuevalue.Bid import Bid
18from geniusweb.issuevalue.Domain import Domain
19from geniusweb.party.Capabilities import Capabilities
20from geniusweb.party.DefaultParty import DefaultParty
21from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import (
22 LinearAdditiveUtilitySpace,
23)
24from geniusweb.profileconnection.ProfileConnectionFactory import (
25 ProfileConnectionFactory,
26)
27from geniusweb.progress.ProgressTime import ProgressTime
28from geniusweb.references.Parameters import Parameters
29from tudelft_utilities_logging.ReportToLogger import ReportToLogger
30
31from agents.template_agent.utils.opponent_model import OpponentModel
32
33# our imports
34import numpy as np
35from sklearn import tree
36from sklearn.preprocessing import label_binarize
37import random
38
39
40class GEAAgent(DefaultParty):
41 """
42 Template of a Python geniusweb agent.
43 """
44
45 def __init__(self):
46 super().__init__()
47 self.logger: ReportToLogger = self.getReporter()
48
49 self.domain: Domain = None
50 self.parameters: Parameters = None
51 self.profile: LinearAdditiveUtilitySpace = None
52 self.progress: ProgressTime = None
53 self.me: PartyId = None
54 self.other: str = None
55 self.settings: Settings = None
56 self.storage_dir: str = None
57
58 self.last_received_bid: Bid = None
59 self.opponent_model: OpponentModel = None
60 self.logger.log(logging.INFO, "party is initialized")
61
62 # our parameters
63 # collect negitioation data
64 self.dataX = []
65 self.dataY = []
66 self.data_len = 0
67 self.issue_encoder = {}
68
69 # decision tree and weights
70 self.decision_model = None
71 self.tree_depth = 20
72 self.orig_opponent_agree_weight = 0.15
73 self.opponent_agree_weight = self.orig_opponent_agree_weight
74 self.accept_threshold = 0.85 # for heuristic function, not utility.
75
76 # define
77 self.OPPONENT_ACCEPT = 1
78 self.OPPONENT_REJECT = -1
79
80 # bid dictionaries
81 self.bid_values = {}
82 self.all_issue_values = {}
83
84 # bid lookup indices
85 # self.lower_threshold = 0
86 # self.higher_threshold = 200
87
88 self.last_bid_utility = 0
89 self.debug_offer = 0
90
91 self.randomn = random.uniform(0, 1)
92
93 def notifyChange(self, data: Inform):
94 """MUST BE IMPLEMENTED
95 This is the entry point of all interaction with your agent after is has been initialised.
96 How to handle the received data is based on its class type.
97
98 Args:
99 info (Inform): Contains either a request for action or information.
100 """
101
102 # a Settings message is the first message that will be send to your
103 # agent containing all the information about the negotiation session.
104 if isinstance(data, Settings):
105 self.settings = cast(Settings, data)
106 self.me = self.settings.getID()
107
108 # progress towards the deadline has to be tracked manually through the use of the Progress object
109 self.progress = self.settings.getProgress()
110
111 self.parameters = self.settings.getParameters()
112 self.storage_dir = self.parameters.get("storage_dir")
113
114 # the profile contains the preferences of the agent over the domain
115 profile_connection = ProfileConnectionFactory.create(
116 data.getProfile().getURI(), self.getReporter()
117 )
118 self.profile = profile_connection.getProfile()
119 self.domain = self.profile.getDomain()
120 profile_connection.close()
121
122 # our code: init issue dictionary
123 self.init_bid_values()
124
125 # ActionDone informs you of an action (an offer or an accept)
126 # that is performed by one of the agents (including yourself).
127 elif isinstance(data, ActionDone):
128 action = cast(ActionDone, data).getAction()
129 actor = action.getActor()
130
131 # ignore action if it is our action
132 if actor != self.me:
133 # obtain the name of the opponent, cutting of the position ID.
134 self.other = str(actor).rsplit("_", 1)[0]
135
136 # process action done by opponent
137 self.opponent_action(action)
138 # YourTurn notifies you that it is your turn to act
139 elif isinstance(data, YourTurn):
140 # execute a turn
141 self.my_turn()
142
143 # Finished will be send if the negotiation has ended (through agreement or deadline)
144 elif isinstance(data, Finished):
145 self.save_data()
146 # terminate the agent MUST BE CALLED
147 self.logger.log(logging.INFO, "party is terminating:")
148 super().terminate()
149 else:
150 self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data))
151
152 def getCapabilities(self) -> Capabilities:
153 """MUST BE IMPLEMENTED
154 Method to indicate to the protocol what the capabilities of this agent are.
155 Leave it as is for the ANL 2022 competition
156
157 Returns:
158 Capabilities: Capabilities representation class
159 """
160 return Capabilities(
161 set(["SAOP"]),
162 set(["geniusweb.profile.utilityspace.LinearAdditive"]),
163 )
164
165 def send_action(self, action: Action):
166 """Sends an action to the opponent(s)
167
168 Args:
169 action (Action): action of this agent
170 """
171 self.getConnection().send(action)
172
173 # give a description of your agent
174 def getDescription(self) -> str:
175 """MUST BE IMPLEMENTED
176 Returns a description of your agent. 1 or 2 sentences.
177
178 Returns:
179 str: Agent description
180 """
181 return "Template agent for the ANL 2022 competition"
182
183 def opponent_action(self, action):
184 """Process an action that was received from the opponent.
185
186 Args:
187 action (Action): action of opponent
188 """
189 # if it is an offer, set the last received bid
190 if isinstance(action, Offer):
191 # create opponent model if it was not yet initialised
192 if self.opponent_model is None:
193 self.opponent_model = OpponentModel(self.domain)
194
195 bid = cast(Offer, action).getBid()
196
197 # update opponent model with bid
198 self.opponent_model.update(bid)
199 # set bid as last received
200 self.last_received_bid = bid
201
202 def my_turn(self):
203 """This method is called when it is our turn. It should decide upon an action
204 to perform and send this action to the opponent.
205 """
206
207 # check if the last received offer is good enough
208 if self.accept_condition(self.last_received_bid):
209 # if so, accept the offer
210 action = Accept(self.me, self.last_received_bid)
211 else:
212 # if not, find a bid to propose as counter offer
213 bid = self.find_bid()
214 action = Offer(self.me, bid)
215 self.append_data_and_train_tree(bid, self.OPPONENT_REJECT)
216
217 # send the action
218 self.send_action(action)
219
220 def save_data(self):
221 """This method is called after the negotiation is finished. It can be used to store data
222 for learning capabilities. Note that no extensive calculations can be done within this method.
223 Taking too much time might result in your agent being killed, so use it for storage only.
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 ###########################################################################################
230 ################################## Example methods below ##################################
231 ###########################################################################################
232
233 def accept_condition(self, bid: Bid) -> bool:
234 if bid is None:
235 return False
236
237 # our code
238 # process new bid offer
239 heuristic_score = self.score_bid(bid)
240 objective_utility = self.profile.getUtility(bid)
241
242 self.append_data_and_train_tree(bid, self.OPPONENT_ACCEPT)
243
244 if objective_utility < self.last_bid_utility:
245 self.opponent_agree_weight *= 0.99
246 if objective_utility > self.last_bid_utility:
247 self.opponent_agree_weight /= 0.99
248
249 if self.opponent_agree_weight == 0:
250 self.opponent_agree_weight = 0.01
251
252 self.last_bid_utility = objective_utility
253
254 # progress of the negotiation session between 0 and 1 (1 is deadline)
255 progress = self.progress.get(time() * 1000)
256
257 conditions = [
258 progress > 0.95 and objective_utility > 0.4,
259 progress > 0.9 and objective_utility > 0.6,
260 objective_utility > 0.7,
261 heuristic_score >= self.accept_threshold,
262 ]
263 return any(conditions)
264
265 def find_bid(self) -> Bid:
266 # compose a list of all possible bids
267 domain = self.profile.getDomain()
268 all_bids = AllBidsList(domain)
269
270 best_bid_score = 0.0
271 best_bid = None
272
273 # take 500 attempts to find a bid according to a heuristic score
274 for _ in range(500):
275 bid = all_bids.get(randint(0, all_bids.size() - 1))
276 bid_score = self.score_bid(bid)
277 if bid_score > best_bid_score:
278 best_bid_score, best_bid = bid_score, bid
279
280 return best_bid
281
282 def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float:
283 ''' Calculate heuristic score for a bid '''
284 progress = self.progress.get(time() * 1000)
285
286 our_utility = float(self.profile.getUtility(bid))
287
288 time_pressure = 1.0 - progress ** (1 / eps)
289 score = alpha * time_pressure * our_utility
290
291 opponent_score = self.tree_predict(bid) * self.opponent_agree_weight
292 score += opponent_score
293
294 return score
295
296
297 def tree_predict(self, bid: Bid) -> float:
298 ''' returns acceptance estimation for the other agent '''
299 bid_data = []
300 bid_issue_values = bid.getIssueValues()
301 domain_issues = list(bid_issue_values.keys())
302 domain_issues.sort()
303 for issue in domain_issues:
304 # encode categorical data
305 issue_encoded = label_binarize([str(bid_issue_values[issue])], classes=self.all_issue_values[issue])
306 # concat current category to X
307 bid_data.extend(issue_encoded.flatten().tolist())
308
309 # if the tree is trained, we can use it to predict opponent reaction
310 if self.decision_model is not None:
311 tree_prediction = float(self.decision_model.predict(np.array(bid_data).reshape(1, -1)))
312 return tree_prediction
313
314 return 0 # no knowledge
315
316 def append_data_and_train_tree(self, bid: Bid, opponent_accept: int) -> None:
317 ''' appends new bid to negotiation history and retrain model '''
318 bid_data = []
319 bid_issue_values = bid.getIssueValues()
320 domain_issues = list(bid_issue_values.keys())
321 domain_issues.sort()
322 for issue in domain_issues:
323 # encode categorical data
324 issue_encoded = label_binarize([str(bid_issue_values[issue])], classes=self.all_issue_values[issue])
325 # concat current category to X
326 bid_data.extend(issue_encoded.flatten().tolist())
327
328 self.data_len += 1
329 self.dataX.append(bid_data)
330 self.dataY.append(opponent_accept)
331
332 # train tree if at least two samples were collected
333
334 if self.data_len > 2:
335 self.decision_model = tree.DecisionTreeClassifier(criterion="entropy", max_depth=self.tree_depth)
336 self.decision_model.fit(self.dataX, self.dataY)
337
338 def init_bid_values(self):
339 ''' must be called to binarize labels '''
340 domain = self.profile.getDomain()
341 domain_issues = domain.getIssues()
342 self.all_issue_values = {}
343 for issue in domain_issues:
344 self.all_issue_values[issue] = []
345 for value in domain.getValues(issue):
346 self.all_issue_values[issue].append(str(value))
Note: See TracBrowser for help on using the repository browser.