import logging from random import randint from time import time from typing import cast import pandas as pd from geniusweb.actions.Accept import Accept from geniusweb.actions.Action import Action from geniusweb.actions.Offer import Offer from geniusweb.actions.PartyId import PartyId from geniusweb.bidspace.AllBidsList import AllBidsList from geniusweb.inform.ActionDone import ActionDone from geniusweb.inform.Finished import Finished from geniusweb.inform.Inform import Inform from geniusweb.inform.Settings import Settings from geniusweb.inform.YourTurn import YourTurn from geniusweb.issuevalue.Bid import Bid from geniusweb.issuevalue.Domain import Domain from geniusweb.party.Capabilities import Capabilities from geniusweb.party.DefaultParty import DefaultParty from geniusweb.profile.utilityspace.LinearAdditiveUtilitySpace import ( LinearAdditiveUtilitySpace, ) from geniusweb.profileconnection.ProfileConnectionFactory import ( ProfileConnectionFactory, ) from geniusweb.progress.ProgressTime import ProgressTime from geniusweb.references.Parameters import Parameters from tudelft_utilities_logging.ReportToLogger import ReportToLogger from agents.template_agent.utils.opponent_model import OpponentModel # our imports import numpy as np from sklearn import tree from sklearn.preprocessing import label_binarize import random class GEAAgent(DefaultParty): """ Template of a Python geniusweb agent. """ def __init__(self): super().__init__() self.logger: ReportToLogger = self.getReporter() self.domain: Domain = None self.parameters: Parameters = None self.profile: LinearAdditiveUtilitySpace = None self.progress: ProgressTime = None self.me: PartyId = None self.other: str = None self.settings: Settings = None self.storage_dir: str = None self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None self.logger.log(logging.INFO, "party is initialized") # our parameters # collect negitioation data self.dataX = [] self.dataY = [] self.data_len = 0 self.issue_encoder = {} # decision tree and weights self.decision_model = None self.tree_depth = 20 self.orig_opponent_agree_weight = 0.15 self.opponent_agree_weight = self.orig_opponent_agree_weight self.accept_threshold = 0.85 # for heuristic function, not utility. # define self.OPPONENT_ACCEPT = 1 self.OPPONENT_REJECT = -1 # bid dictionaries self.bid_values = {} self.all_issue_values = {} # bid lookup indices # self.lower_threshold = 0 # self.higher_threshold = 200 self.last_bid_utility = 0 self.debug_offer = 0 self.randomn = random.uniform(0, 1) def notifyChange(self, data: Inform): """MUST BE IMPLEMENTED This is the entry point of all interaction with your agent after is has been initialised. How to handle the received data is based on its class type. Args: info (Inform): Contains either a request for action or information. """ # a Settings message is the first message that will be send to your # agent containing all the information about the negotiation session. if isinstance(data, Settings): self.settings = cast(Settings, data) self.me = self.settings.getID() # progress towards the deadline has to be tracked manually through the use of the Progress object self.progress = self.settings.getProgress() self.parameters = self.settings.getParameters() self.storage_dir = self.parameters.get("storage_dir") # the profile contains the preferences of the agent over the domain profile_connection = ProfileConnectionFactory.create( data.getProfile().getURI(), self.getReporter() ) self.profile = profile_connection.getProfile() self.domain = self.profile.getDomain() profile_connection.close() # our code: init issue dictionary self.init_bid_values() # ActionDone informs you of an action (an offer or an accept) # that is performed by one of the agents (including yourself). elif isinstance(data, ActionDone): action = cast(ActionDone, data).getAction() actor = action.getActor() # ignore action if it is our action if actor != self.me: # obtain the name of the opponent, cutting of the position ID. self.other = str(actor).rsplit("_", 1)[0] # process action done by opponent self.opponent_action(action) # YourTurn notifies you that it is your turn to act elif isinstance(data, YourTurn): # execute a turn self.my_turn() # Finished will be send if the negotiation has ended (through agreement or deadline) elif isinstance(data, Finished): self.save_data() # terminate the agent MUST BE CALLED self.logger.log(logging.INFO, "party is terminating:") super().terminate() else: self.logger.log(logging.WARNING, "Ignoring unknown info " + str(data)) def getCapabilities(self) -> Capabilities: """MUST BE IMPLEMENTED Method to indicate to the protocol what the capabilities of this agent are. Leave it as is for the ANL 2022 competition Returns: Capabilities: Capabilities representation class """ return Capabilities( set(["SAOP"]), set(["geniusweb.profile.utilityspace.LinearAdditive"]), ) def send_action(self, action: Action): """Sends an action to the opponent(s) Args: action (Action): action of this agent """ self.getConnection().send(action) # give a description of your agent def getDescription(self) -> str: """MUST BE IMPLEMENTED Returns a description of your agent. 1 or 2 sentences. Returns: str: Agent description """ return "Template agent for the ANL 2022 competition" def opponent_action(self, action): """Process an action that was received from the opponent. Args: action (Action): action of opponent """ # if it is an offer, set the last received bid if isinstance(action, Offer): # create opponent model if it was not yet initialised if self.opponent_model is None: self.opponent_model = OpponentModel(self.domain) bid = cast(Offer, action).getBid() # update opponent model with bid self.opponent_model.update(bid) # set bid as last received self.last_received_bid = bid def my_turn(self): """This method is called when it is our turn. It should decide upon an action to perform and send this action to the opponent. """ # check if the last received offer is good enough if self.accept_condition(self.last_received_bid): # if so, accept the offer action = Accept(self.me, self.last_received_bid) else: # if not, find a bid to propose as counter offer bid = self.find_bid() action = Offer(self.me, bid) self.append_data_and_train_tree(bid, self.OPPONENT_REJECT) # send the action self.send_action(action) def save_data(self): """This method is called after the negotiation is finished. It can be used to store data for learning capabilities. Note that no extensive calculations can be done within this method. Taking too much time might result in your agent being killed, so use it for storage only. """ data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: f.write(data) ########################################################################################### ################################## Example methods below ################################## ########################################################################################### def accept_condition(self, bid: Bid) -> bool: if bid is None: return False # our code # process new bid offer heuristic_score = self.score_bid(bid) objective_utility = self.profile.getUtility(bid) self.append_data_and_train_tree(bid, self.OPPONENT_ACCEPT) if objective_utility < self.last_bid_utility: self.opponent_agree_weight *= 0.99 if objective_utility > self.last_bid_utility: self.opponent_agree_weight /= 0.99 if self.opponent_agree_weight == 0: self.opponent_agree_weight = 0.01 self.last_bid_utility = objective_utility # progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) conditions = [ progress > 0.95 and objective_utility > 0.4, progress > 0.9 and objective_utility > 0.6, objective_utility > 0.7, heuristic_score >= self.accept_threshold, ] return any(conditions) def find_bid(self) -> Bid: # compose a list of all possible bids domain = self.profile.getDomain() all_bids = AllBidsList(domain) best_bid_score = 0.0 best_bid = None # take 500 attempts to find a bid according to a heuristic score for _ in range(500): bid = all_bids.get(randint(0, all_bids.size() - 1)) bid_score = self.score_bid(bid) if bid_score > best_bid_score: best_bid_score, best_bid = bid_score, bid return best_bid def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: ''' Calculate heuristic score for a bid ''' progress = self.progress.get(time() * 1000) our_utility = float(self.profile.getUtility(bid)) time_pressure = 1.0 - progress ** (1 / eps) score = alpha * time_pressure * our_utility opponent_score = self.tree_predict(bid) * self.opponent_agree_weight score += opponent_score return score def tree_predict(self, bid: Bid) -> float: ''' returns acceptance estimation for the other agent ''' bid_data = [] bid_issue_values = bid.getIssueValues() domain_issues = list(bid_issue_values.keys()) domain_issues.sort() for issue in domain_issues: # encode categorical data issue_encoded = label_binarize([str(bid_issue_values[issue])], classes=self.all_issue_values[issue]) # concat current category to X bid_data.extend(issue_encoded.flatten().tolist()) # if the tree is trained, we can use it to predict opponent reaction if self.decision_model is not None: tree_prediction = float(self.decision_model.predict(np.array(bid_data).reshape(1, -1))) return tree_prediction return 0 # no knowledge def append_data_and_train_tree(self, bid: Bid, opponent_accept: int) -> None: ''' appends new bid to negotiation history and retrain model ''' bid_data = [] bid_issue_values = bid.getIssueValues() domain_issues = list(bid_issue_values.keys()) domain_issues.sort() for issue in domain_issues: # encode categorical data issue_encoded = label_binarize([str(bid_issue_values[issue])], classes=self.all_issue_values[issue]) # concat current category to X bid_data.extend(issue_encoded.flatten().tolist()) self.data_len += 1 self.dataX.append(bid_data) self.dataY.append(opponent_accept) # train tree if at least two samples were collected if self.data_len > 2: self.decision_model = tree.DecisionTreeClassifier(criterion="entropy", max_depth=self.tree_depth) self.decision_model.fit(self.dataX, self.dataY) def init_bid_values(self): ''' must be called to binarize labels ''' domain = self.profile.getDomain() domain_issues = domain.getIssues() self.all_issue_values = {} for issue in domain_issues: self.all_issue_values[issue] = [] for value in domain.getValues(issue): self.all_issue_values[issue].append(str(value))