From 3dcc9a42e1bca62216b6c749c48070c6cd7e1962 Mon Sep 17 00:00:00 2001 From: Harald Toth Date: Mon, 31 Mar 2025 14:37:14 +0200 Subject: [PATCH 01/17] Added agent and basic acceptance --- .gitignore | 4 +- agents/team 16 agent/__init__.py | 0 agents/team 16 agent/team 16 agent.py | 257 +++++++++++++++++++ agents/team 16 agent/utils/__init__.py | 0 agents/team 16 agent/utils/opponent_model.py | 121 +++++++++ run.py | 4 +- 6 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 agents/team 16 agent/__init__.py create mode 100755 agents/team 16 agent/team 16 agent.py create mode 100644 agents/team 16 agent/utils/__init__.py create mode 100644 agents/team 16 agent/utils/opponent_model.py diff --git a/.gitignore b/.gitignore index e697725e..87ae50b7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ logs results *.sbatch -*.log \ No newline at end of file +*.log + +.idea \ No newline at end of file diff --git a/agents/team 16 agent/__init__.py b/agents/team 16 agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/team 16 agent/team 16 agent.py b/agents/team 16 agent/team 16 agent.py new file mode 100755 index 00000000..eaba2979 --- /dev/null +++ b/agents/team 16 agent/team 16 agent.py @@ -0,0 +1,257 @@ +import logging +from random import randint +from time import time +from typing import cast + +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 .utils.opponent_model import OpponentModel + + +class TemplateAgent(DefaultParty): + """ + The amazing Python geniusweb agent made by team 16. + """ + + 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") + + 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() + + # 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 "Team 16's agent, the best agent in the tournament!" + + 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) + + # 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 + + # progress of the negotiation session between 0 and 1 (1 is deadline) + progress = self.progress.get(time() * 1000) + + # very basic approach that accepts if the offer is valued above 0.7 and + # 95% of the time towards the deadline has passed + conditions = [ + self.profile.getUtility(bid) > 0.8, + progress > 0.95, + ] + + # First phase, before the soft threshold (0 < progress < 0.6) + if progress < 0.6: + return self.profile.getUtility(bid) >= 0.9 + + # Second phase, before hard threshold (0.6 <= progress < 0.9) + if progress < 0.9: + return self.profile.getUtility(bid) >= 0.75 + + # Third phase, critical phase (0.9 <= progress < 1) + return True + + 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 + + Args: + bid (Bid): Bid to score + alpha (float, optional): Trade-off factor between self interested and + altruistic behaviour. Defaults to 0.95. + eps (float, optional): Time pressure factor, balances between conceding + and Boulware behaviour over time. Defaults to 0.1. + + Returns: + float: score + """ + 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 + + if self.opponent_model is not None: + opponent_utility = self.opponent_model.get_predicted_utility(bid) + opponent_score = (1.0 - alpha * time_pressure) * opponent_utility + score += opponent_score + + return score diff --git a/agents/team 16 agent/utils/__init__.py b/agents/team 16 agent/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/team 16 agent/utils/opponent_model.py b/agents/team 16 agent/utils/opponent_model.py new file mode 100644 index 00000000..14d7456b --- /dev/null +++ b/agents/team 16 agent/utils/opponent_model.py @@ -0,0 +1,121 @@ +from collections import defaultdict + +from geniusweb.issuevalue.Bid import Bid +from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet +from geniusweb.issuevalue.Domain import Domain +from geniusweb.issuevalue.Value import Value + + +class OpponentModel: + def __init__(self, domain: Domain): + self.offers = [] + self.domain = domain + + self.issue_estimators = { + i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() + } + + def update(self, bid: Bid): + # keep track of all bids received + self.offers.append(bid) + + # update all issue estimators with the value that is offered for that issue + for issue_id, issue_estimator in self.issue_estimators.items(): + issue_estimator.update(bid.getValue(issue_id)) + + def get_predicted_utility(self, bid: Bid): + if len(self.offers) == 0 or bid is None: + return 0 + + # initiate + total_issue_weight = 0.0 + value_utilities = [] + issue_weights = [] + + for issue_id, issue_estimator in self.issue_estimators.items(): + # get the value that is set for this issue in the bid + value: Value = bid.getValue(issue_id) + + # collect both the predicted weight for the issue and + # predicted utility of the value within this issue + value_utilities.append(issue_estimator.get_value_utility(value)) + issue_weights.append(issue_estimator.weight) + + total_issue_weight += issue_estimator.weight + + # normalise the issue weights such that the sum is 1.0 + if total_issue_weight == 0.0: + issue_weights = [1 / len(issue_weights) for _ in issue_weights] + else: + issue_weights = [iw / total_issue_weight for iw in issue_weights] + + # calculate predicted utility by multiplying all value utilities with their issue weight + predicted_utility = sum( + [iw * vu for iw, vu in zip(issue_weights, value_utilities)] + ) + + return predicted_utility + + +class IssueEstimator: + def __init__(self, value_set: DiscreteValueSet): + if not isinstance(value_set, DiscreteValueSet): + raise TypeError( + "This issue estimator only supports issues with discrete values" + ) + + self.bids_received = 0 + self.max_value_count = 0 + self.num_values = value_set.size() + self.value_trackers = defaultdict(ValueEstimator) + self.weight = 0 + + def update(self, value: Value): + self.bids_received += 1 + + # get the value tracker of the value that is offered + value_tracker = self.value_trackers[value] + + # register that this value was offered + value_tracker.update() + + # update the count of the most common offered value + self.max_value_count = max([value_tracker.count, self.max_value_count]) + + # update predicted issue weight + # the intuition here is that if the values of the receiverd offers spread out over all + # possible values, then this issue is likely not important to the opponent (weight == 0.0). + # If all received offers proposed the same value for this issue, + # then the predicted issue weight == 1.0 + equal_shares = self.bids_received / self.num_values + self.weight = (self.max_value_count - equal_shares) / ( + self.bids_received - equal_shares + ) + + # recalculate all value utilities + for value_tracker in self.value_trackers.values(): + value_tracker.recalculate_utility(self.max_value_count, self.weight) + + def get_value_utility(self, value: Value): + if value in self.value_trackers: + return self.value_trackers[value].utility + + return 0 + + +class ValueEstimator: + def __init__(self): + self.count = 0 + self.utility = 0 + + def update(self): + self.count += 1 + + def recalculate_utility(self, max_value_count: int, weight: float): + if weight < 1: + mod_value_count = ((self.count + 1) ** (1 - weight)) - 1 + mod_max_value_count = ((max_value_count + 1) ** (1 - weight)) - 1 + + self.utility = mod_value_count / mod_max_value_count + else: + self.utility = 1 diff --git a/run.py b/run.py index 5a294c95..e994acd5 100644 --- a/run.py +++ b/run.py @@ -18,8 +18,8 @@ settings = { "agents": [ { - "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", - "parameters": {"storage_dir": "agent_storage/DreamTeam109Agent"}, + "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, }, { "class": "agents.template_agent.template_agent.TemplateAgent", From 5fc57c85410b6b58b02112283ffd0ba348062d21 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Tue, 1 Apr 2025 11:40:30 +0200 Subject: [PATCH 02/17] changed agent to ours --- agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py | 5 ++++- agents/{team 16 agent => group16_agent}/__init__.py | 0 .../team 16 agent.py => group16_agent/group16_agent.py} | 2 +- agents/{team 16 agent => group16_agent}/utils/__init__.py | 0 .../utils/opponent_model.py | 0 run.py | 8 ++++++-- run_tournament.py | 4 ++-- 7 files changed, 13 insertions(+), 6 deletions(-) rename agents/{team 16 agent => group16_agent}/__init__.py (100%) rename agents/{team 16 agent/team 16 agent.py => group16_agent/group16_agent.py} (99%) rename agents/{team 16 agent => group16_agent}/utils/__init__.py (100%) rename agents/{team 16 agent => group16_agent}/utils/opponent_model.py (100%) diff --git a/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py b/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py index 12b71abc..18acca3f 100644 --- a/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py +++ b/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py @@ -112,6 +112,7 @@ def notifyChange(self, data: Inform): self.profile = profile_connection.getProfile() self.domain = self.profile.getDomain() # compose a list of all possible bids + # RAFA: save all bids as a property self.all_bids = AllBidsList(self.domain) profile_connection.close() @@ -128,6 +129,7 @@ def notifyChange(self, data: Inform): self.other = actor # obtain the name of the opponent, cutting of the position ID. self.other_name = str(actor).rsplit("_", 1)[0] + # RAFA: load any previous data from file and adapt self.attempt_load_data() self.learn_from_past_sessions(self.data_dict["sessions"]) @@ -207,6 +209,7 @@ def opponent_action(self, action): # set bid as last received self.last_received_bid = bid + # RAFA: add opponent_best_bid, if existant update it if self.opponent_best_bid is None: self.opponent_best_bid = bid elif self.profile.getUtility(bid) > self.profile.getUtility(self.opponent_best_bid): @@ -264,7 +267,7 @@ def update_data_dict(self): "progressAtFinish": progress_at_finish, "utilityAtFinish": self.utility_at_finish, "didAccept": self.did_accept, - "isGood": self.utility_at_finish >= self.min_util, + "isGood": self.utility_at_finish >= selfisGood.min_util, "topBidsPercentage": self.top_bids_percentage, "forceAcceptAtRemainingTurns": self.force_accept_at_remaining_turns } diff --git a/agents/team 16 agent/__init__.py b/agents/group16_agent/__init__.py similarity index 100% rename from agents/team 16 agent/__init__.py rename to agents/group16_agent/__init__.py diff --git a/agents/team 16 agent/team 16 agent.py b/agents/group16_agent/group16_agent.py similarity index 99% rename from agents/team 16 agent/team 16 agent.py rename to agents/group16_agent/group16_agent.py index eaba2979..5153b6f9 100755 --- a/agents/team 16 agent/team 16 agent.py +++ b/agents/group16_agent/group16_agent.py @@ -30,7 +30,7 @@ from .utils.opponent_model import OpponentModel -class TemplateAgent(DefaultParty): +class Group16Agent(DefaultParty): """ The amazing Python geniusweb agent made by team 16. """ diff --git a/agents/team 16 agent/utils/__init__.py b/agents/group16_agent/utils/__init__.py similarity index 100% rename from agents/team 16 agent/utils/__init__.py rename to agents/group16_agent/utils/__init__.py diff --git a/agents/team 16 agent/utils/opponent_model.py b/agents/group16_agent/utils/opponent_model.py similarity index 100% rename from agents/team 16 agent/utils/opponent_model.py rename to agents/group16_agent/utils/opponent_model.py diff --git a/run.py b/run.py index e994acd5..ce6878f5 100644 --- a/run.py +++ b/run.py @@ -21,9 +21,13 @@ "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, }, + # { + # "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + # "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + # }, { - "class": "agents.template_agent.template_agent.TemplateAgent", - "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + "class": "agents.group16_agent.group16_agent.Group16Agent", + "parameters": {"storage_dir": "agent_storage/Group16Agent"}, }, ], "profiles": ["domains/domain00/profileA.json", "domains/domain00/profileB.json"], diff --git a/run_tournament.py b/run_tournament.py index 41dd7242..03d59fb9 100644 --- a/run_tournament.py +++ b/run_tournament.py @@ -18,8 +18,8 @@ tournament_settings = { "agents": [ { - "class": "agents.template_agent.template_agent.TemplateAgent", - "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + "class": "agents.group16_agent.group16_agent.Group16Agent", + "parameters": {"storage_dir": "agent_storage/Group16Agent"}, }, { "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", From 617c051bf9b3d2c1a2ac2d23417ed72c4bb938f6 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Thu, 3 Apr 2025 10:32:32 +0200 Subject: [PATCH 03/17] opponent model --- agents/group16_agent/__init__.py | 1 + agents/group16_agent/group16_agent.py | 63 +++++++++- agents/group16_agent/utils/__init__.py | 1 + agents/group16_agent/utils/opponent_model.py | 126 +++++++++++++++++-- 4 files changed, 181 insertions(+), 10 deletions(-) diff --git a/agents/group16_agent/__init__.py b/agents/group16_agent/__init__.py index e69de29b..8b137891 100644 --- a/agents/group16_agent/__init__.py +++ b/agents/group16_agent/__init__.py @@ -0,0 +1 @@ + diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index 5153b6f9..14af9b34 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -152,11 +152,19 @@ def opponent_action(self, action): self.opponent_model = OpponentModel(self.domain) bid = cast(Offer, action).getBid() + + # Get our utility for this bid + our_utility = float(self.profile.getUtility(bid)) - # update opponent model with bid - self.opponent_model.update(bid) + # update opponent model with bid and our utility + self.opponent_model.update(bid, our_utility) + # set bid as last received self.last_received_bid = bid + + # Store best bid if this one has the highest utility for us so far + if hasattr(self.opponent_model, 'best_bid_for_us') and self.opponent_model.best_bid_for_us is not None: + self.opponent_best_bid = self.opponent_model.best_bid_for_us def my_turn(self): """This method is called when it is our turn. It should decide upon an action @@ -179,6 +187,21 @@ def save_data(self): 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. """ + # TODO: Implement data saving for opponent modeling + # - Save opponent type classification (HARDHEADED, CONCEDER, NEUTRAL) + # - Save concession rates for each opponent + # - Save top issues for each opponent identified by get_top_issues() + # - This will help improve bidding/acceptance strategies in future negotiations + # + # Example code: + # if self.opponent_model and hasattr(self.opponent_model, 'get_opponent_type'): + # opponent_data = { + # "type": self.opponent_model.get_opponent_type(), + # "concession_rate": self.opponent_model.get_concession_rate(), + # "top_issues": self.opponent_model.get_top_issues(3) + # } + # # Save this data for later use + data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: f.write(data) @@ -194,6 +217,19 @@ def accept_condition(self, bid: Bid) -> bool: # progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) + # NOTE FOR ACCEPTANCE STRATEGY IMPLEMENTER: + # Use the opponent model to improve acceptance strategy: + # 1. self.opponent_model.get_opponent_type() - Returns opponent strategy type (HARDHEADED, CONCEDER, NEUTRAL) + # 2. self.opponent_model.get_concession_rate() - Get opponent's concession rate + # 3. self.opponent_model.best_bid_for_us - Best bid received (highest utility for us) + # + # Example for using opponent type in acceptance: + # opponent_type = self.opponent_model.get_opponent_type() + # if opponent_type == "HARDHEADED" and progress > 0.8: + # # Accept lower utility bids from hardheaded opponents late in negotiation + # return self.profile.getUtility(bid) >= 0.7 + + # Current basic implementation below: # very basic approach that accepts if the offer is valued above 0.7 and # 95% of the time towards the deadline has passed conditions = [ @@ -213,6 +249,24 @@ def accept_condition(self, bid: Bid) -> bool: return True def find_bid(self) -> Bid: + # NOTE FOR BIDDING STRATEGY IMPLEMENTER: + # Use the opponent model to improve bidding strategy: + # 1. self.opponent_model.get_opponent_type() - Returns opponent type (HARDHEADED, CONCEDER, NEUTRAL) + # 2. self.opponent_model.get_top_issues(3) - Returns top 3 issues important to opponent as [(issue_id, weight),...] + # 3. self.opponent_model.get_predicted_utility(bid) - Estimate opponent's utility for a bid + # 4. self.opponent_model.best_bid_for_us - Best bid received (highest utility for us) + # + # Example of using opponent's top issues to create bids they prefer: + # top_issues = self.opponent_model.get_top_issues(2) # Get top 2 most important issues + # # Then create bids that have good values for the opponent on these important issues + # + # Example of using opponent type to adjust strategy: + # opponent_type = self.opponent_model.get_opponent_type() + # if opponent_type == "CONCEDER": + # # With conceder opponents, we can propose higher utility bids for us + # # ... + + # Current basic implementation below: # compose a list of all possible bids domain = self.profile.getDomain() all_bids = AllBidsList(domain) @@ -227,6 +281,11 @@ def find_bid(self) -> Bid: if bid_score > best_bid_score: best_bid_score, best_bid = bid_score, bid + # RAFA: we're late in the negotiation, consider returning the best bid we received + progress = self.progress.get(time() * 1000) + if progress > 0.95 and hasattr(self.opponent_model, 'best_bid_for_us') and self.opponent_model.best_bid_for_us is not None: + return self.opponent_model.best_bid_for_us + return best_bid def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: diff --git a/agents/group16_agent/utils/__init__.py b/agents/group16_agent/utils/__init__.py index e69de29b..8b137891 100644 --- a/agents/group16_agent/utils/__init__.py +++ b/agents/group16_agent/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/agents/group16_agent/utils/opponent_model.py b/agents/group16_agent/utils/opponent_model.py index 14d7456b..447660fd 100644 --- a/agents/group16_agent/utils/opponent_model.py +++ b/agents/group16_agent/utils/opponent_model.py @@ -1,4 +1,6 @@ from collections import defaultdict +import logging +from typing import Dict, List, Tuple, Optional from geniusweb.issuevalue.Bid import Bid from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet @@ -10,20 +12,73 @@ class OpponentModel: def __init__(self, domain: Domain): self.offers = [] self.domain = domain - + + # Track best bid for us (highest utility for our agent) + self.best_bid_for_us = None + self.best_bid_utility = 0.0 + + # Issue estimators to track opponent preferences self.issue_estimators = { i: IssueEstimator(v) for i, v in domain.getIssuesValues().items() } - - def update(self, bid: Bid): - # keep track of all bids received + + # Track opponent strategy type + self.bid_count = 0 + self.concession_rate = 0.0 + self.repeated_bids = defaultdict(int) + + # Track history of opponent utilities to analyze concession + self.opponent_utilities = [] + + def update(self, bid: Bid, our_utility: float = None): + """Update the opponent model with a new bid + + Args: + bid (Bid): New bid from opponent + our_utility (float, optional): Our utility for this bid + """ + # Track all received bids self.offers.append(bid) - - # update all issue estimators with the value that is offered for that issue + self.bid_count += 1 + + # Count repeated bids (use string representation for hashability) + bid_str = str(bid) + self.repeated_bids[bid_str] += 1 + + # Update best bid for us if applicable + if our_utility is not None and (self.best_bid_for_us is None or our_utility > self.best_bid_utility): + self.best_bid_for_us = bid + self.best_bid_utility = our_utility + + # Update all issue estimators for issue_id, issue_estimator in self.issue_estimators.items(): issue_estimator.update(bid.getValue(issue_id)) - - def get_predicted_utility(self, bid: Bid): + + # Calculate opponent utility and track it + opponent_utility = self.get_predicted_utility(bid) + self.opponent_utilities.append(opponent_utility) + + # Update concession rate if we have at least 2 bids + if len(self.opponent_utilities) >= 2: + # Simple concession rate - average decrease in utility between consecutive bids + decreases = [] + for i in range(1, len(self.opponent_utilities)): + diff = self.opponent_utilities[i-1] - self.opponent_utilities[i] + if diff > 0: # Only count decreases in utility (actual concessions) + decreases.append(diff) + + if decreases: + self.concession_rate = sum(decreases) / len(decreases) + + def get_predicted_utility(self, bid: Bid) -> float: + """Predict the opponent's utility for a bid + + Args: + bid (Bid): Bid to evaluate + + Returns: + float: Predicted utility between 0 and 1 + """ if len(self.offers) == 0 or bid is None: return 0 @@ -55,6 +110,61 @@ def get_predicted_utility(self, bid: Bid): ) return predicted_utility + + def get_opponent_type(self) -> str: + """Identify opponent negotiation strategy type + + Returns: + str: Strategy type (HARDHEADED, CONCEDER, or UNKNOWN) + """ + if self.bid_count < 3: + return "UNKNOWN" + + # Check for hardheaded opponent (low concession rate, many repeated bids) + unique_bids = len(self.repeated_bids) + bid_repetition_ratio = unique_bids / self.bid_count + + if self.concession_rate < 0.02 or bid_repetition_ratio < 0.5: + return "HARDHEADED" + elif self.concession_rate > 0.05: + return "CONCEDER" + else: + return "NEUTRAL" + + def get_concession_rate(self) -> float: + """Get the opponent's concession rate + + Returns: + float: Concession rate (higher means more concessions) + """ + return self.concession_rate + + def get_top_issues(self, n: int = 3) -> List[Tuple[str, float]]: + """Get the top n most important issues for the opponent + + Args: + n (int, optional): Number of issues to return. Defaults to 3. + + Returns: + List[Tuple[str, float]]: List of (issue_id, weight) tuples + """ + # Get normalized issue weights + total_weight = sum(est.weight for est in self.issue_estimators.values()) + + if total_weight == 0: + # Equal weights if no data yet + equal_weight = 1.0 / len(self.issue_estimators) + issues = [(issue_id, equal_weight) for issue_id in self.issue_estimators.keys()] + else: + # Normalize weights + issues = [(issue_id, est.weight / total_weight) + for issue_id, est in self.issue_estimators.items()] + + # Sort by weight and return top n + issues.sort(key=lambda x: x[1], reverse=True) + return issues[:n] + + # TODO: Add methods to save/load opponent data for persistent learning class IssueEstimator: From 88396050da5b01f2625224b556dd56fd73c91fb8 Mon Sep 17 00:00:00 2001 From: Adrien Carton de Wiart Date: Thu, 3 Apr 2025 10:55:38 +0200 Subject: [PATCH 04/17] restarted branch from scrach to not have merge conflicts --- agents/group16_agent/utils/wrapper.py | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 agents/group16_agent/utils/wrapper.py diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py new file mode 100644 index 00000000..f66c2bc6 --- /dev/null +++ b/agents/group16_agent/utils/wrapper.py @@ -0,0 +1,45 @@ +# python objects to store opponent information +import pandas as pd +import logging +from tudelft_utilities_logging.ReportToLogger import ReportToLogger + + +class Opponent: + def __init__(self, result=0, finalUtility=0, offerVariance=[], name=""): + self.result = result + self.finalUtility = finalUtility + self.offerVariance = offerVariance + self.name = name + + def save(self): + save_opponent_data(self) + + def normalize(self): + # not the behaviour, will take all the raw data and make it into statistics + return self.result + + +def get_opponent_data(him, name): + + him.logger.log(logging.INFO, "\n\n\n\n\n\nfunction called as wanted \n\n\n\n\n\n") + file_path = f"saved/{name}.plk" + + try: + opponent = pd.read_pickle(file_path) + if not isinstance(opponent, Opponent): + raise ValueError("Deserialized object is not of type Opponent") + opponent.normalize() + except (FileNotFoundError, ValueError, Exception): + print(f"File not found or invalid data. Creating default Opponent for {name}.") + opponent = Opponent(name=name) + pd.to_pickle(opponent, file_path) + + return opponent + + +def save_opponent_data(opponent): + if isinstance(opponent, Opponent): + file_path = f"saved/{opponent.name}.plk" + pd.to_pickle(opponent, file_path) + else: + print("Non opponent saved") From d7757ce1d2bb5566ef236ce453e1643950f578dd Mon Sep 17 00:00:00 2001 From: Adrien Carton de Wiart Date: Thu, 3 Apr 2025 11:25:48 +0200 Subject: [PATCH 05/17] persisting harlod correctly [my goat] --- agents/group16_agent/group16_agent.py | 5 +++++ agents/group16_agent/utils/wrapper.py | 18 +++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index 5153b6f9..a664f5ce 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -28,6 +28,7 @@ from tudelft_utilities_logging.ReportToLogger import ReportToLogger from .utils.opponent_model import OpponentModel +from .utils import wrapper class Group16Agent(DefaultParty): @@ -50,6 +51,7 @@ def __init__(self): self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None + self.opponent = None self.logger.log(logging.INFO, "party is initialized") def notifyChange(self, data: Inform): @@ -162,6 +164,7 @@ 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. """ + self.opponent = wrapper.get_opponent_data(self,"harold") # check if the last received offer is good enough if self.accept_condition(self.last_received_bid): # if so, accept the offer @@ -179,6 +182,8 @@ def save_data(self): 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. """ + #TODO:Adrien save + wrapper.save_opponent_data(self.opponent) data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: f.write(data) diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py index f66c2bc6..bb480331 100644 --- a/agents/group16_agent/utils/wrapper.py +++ b/agents/group16_agent/utils/wrapper.py @@ -1,7 +1,6 @@ # python objects to store opponent information import pandas as pd -import logging -from tudelft_utilities_logging.ReportToLogger import ReportToLogger +import os class Opponent: @@ -20,10 +19,9 @@ def normalize(self): def get_opponent_data(him, name): - - him.logger.log(logging.INFO, "\n\n\n\n\n\nfunction called as wanted \n\n\n\n\n\n") - file_path = f"saved/{name}.plk" - + file_path = f"agents/group16_agent/utils/saved\{name}.plk" + if not os.path.exists('agents/group16_agent/utils/saved'): + os.makedirs('agents/group16_agent/utils/saved') try: opponent = pd.read_pickle(file_path) if not isinstance(opponent, Opponent): @@ -38,8 +36,14 @@ def get_opponent_data(him, name): def save_opponent_data(opponent): + if(opponent == None): + print("we have a problem opponent is None") + return if isinstance(opponent, Opponent): - file_path = f"saved/{opponent.name}.plk" + print(f"we are saving {opponent.name}") + file_path = f"agents/group16_agent/utils/saved\{opponent.name}.plk" + if not os.path.exists('agents/group16_agent/utils/saved'): + os.makedirs('agents/group16_agent/utils/saved') pd.to_pickle(opponent, file_path) else: print("Non opponent saved") From 942062e5e90871c70a4a5f4535b6e6df538aac0f Mon Sep 17 00:00:00 2001 From: Adrien Carton de Wiart Date: Thu, 3 Apr 2025 11:39:27 +0200 Subject: [PATCH 06/17] calling him only once --- agents/group16_agent/group16_agent.py | 6 +++++- agents/group16_agent/utils/wrapper.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index a664f5ce..4efce971 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -48,6 +48,7 @@ def __init__(self): self.other: str = None self.settings: Settings = None self.storage_dir: str = None + self.got_opponent = False self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None @@ -164,7 +165,9 @@ 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. """ - self.opponent = wrapper.get_opponent_data(self,"harold") + if(not self.got_opponent): + self.opponent = wrapper.get_opponent_data(self.other) + self.got_opponent = True # check if the last received offer is good enough if self.accept_condition(self.last_received_bid): # if so, accept the offer @@ -184,6 +187,7 @@ def save_data(self): """ #TODO:Adrien save wrapper.save_opponent_data(self.opponent) + self.got_opponent = False data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: f.write(data) diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py index bb480331..9d6a4685 100644 --- a/agents/group16_agent/utils/wrapper.py +++ b/agents/group16_agent/utils/wrapper.py @@ -18,7 +18,8 @@ def normalize(self): return self.result -def get_opponent_data(him, name): +def get_opponent_data(name): + file_path = f"agents/group16_agent/utils/saved\{name}.plk" if not os.path.exists('agents/group16_agent/utils/saved'): os.makedirs('agents/group16_agent/utils/saved') From c8fd49b83f2e8f6c4904e543ca74ee35a9fed19f Mon Sep 17 00:00:00 2001 From: Adrien Carton de Wiart Date: Thu, 3 Apr 2025 11:42:09 +0200 Subject: [PATCH 07/17] ignoring the plk files so that we don't save them remotely --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 87ae50b7..8ed001d2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ results *.sbatch *.log +agents/group16_agent/utils/saved/*.plk .idea \ No newline at end of file From e4940283692e46ddf7df3e0ee84b7fb6b754cce9 Mon Sep 17 00:00:00 2001 From: Adrien Carton de Wiart Date: Thu, 3 Apr 2025 12:17:01 +0200 Subject: [PATCH 08/17] fixing path issues --- agents/group16_agent/group16_agent.py | 5 ++--- agents/group16_agent/utils/wrapper.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index 4efce971..d3225f87 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -166,7 +166,7 @@ def my_turn(self): to perform and send this action to the opponent. """ if(not self.got_opponent): - self.opponent = wrapper.get_opponent_data(self.other) + self.opponent = wrapper.get_opponent_data(self.parameters.get("storage_dir"), self.other) self.got_opponent = True # check if the last received offer is good enough if self.accept_condition(self.last_received_bid): @@ -185,8 +185,7 @@ def save_data(self): 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. """ - #TODO:Adrien save - wrapper.save_opponent_data(self.opponent) + wrapper.save_opponent_data(self.parameters.get("storage_dir"), self.opponent) self.got_opponent = False data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py index 9d6a4685..c5188f8b 100644 --- a/agents/group16_agent/utils/wrapper.py +++ b/agents/group16_agent/utils/wrapper.py @@ -18,33 +18,33 @@ def normalize(self): return self.result -def get_opponent_data(name): +def get_opponent_data(savepath, name): - file_path = f"agents/group16_agent/utils/saved\{name}.plk" - if not os.path.exists('agents/group16_agent/utils/saved'): - os.makedirs('agents/group16_agent/utils/saved') + file_path = savepath + name+".plk" + if not os.path.exists(savepath): + os.makedirs(savepath) try: opponent = pd.read_pickle(file_path) if not isinstance(opponent, Opponent): raise ValueError("Deserialized object is not of type Opponent") opponent.normalize() except (FileNotFoundError, ValueError, Exception): - print(f"File not found or invalid data. Creating default Opponent for {name}.") + print(f"File not found or invalid data. Creating default Opponent for {name}. at path: ", savepath) opponent = Opponent(name=name) pd.to_pickle(opponent, file_path) return opponent -def save_opponent_data(opponent): +def save_opponent_data(savepath, opponent): if(opponent == None): print("we have a problem opponent is None") return if isinstance(opponent, Opponent): print(f"we are saving {opponent.name}") - file_path = f"agents/group16_agent/utils/saved\{opponent.name}.plk" - if not os.path.exists('agents/group16_agent/utils/saved'): - os.makedirs('agents/group16_agent/utils/saved') + file_path = savepath + opponent.name + ".plk" + if not os.path.exists(savepath): + os.makedirs(savepath) pd.to_pickle(opponent, file_path) else: print("Non opponent saved") From 89f690419c5f948a04f1bb4b008a0dd70d14ec18 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Thu, 3 Apr 2025 15:02:31 +0200 Subject: [PATCH 09/17] learn from previous sessions --- agents/group16_agent/group16_agent.py | 37 +++------------ agents/group16_agent/utils/opponent_model.py | 47 ++++++++++++++++++++ run.py | 12 ++--- 3 files changed, 59 insertions(+), 37 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index 14af9b34..77a1583d 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -217,54 +217,29 @@ def accept_condition(self, bid: Bid) -> bool: # progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) - # NOTE FOR ACCEPTANCE STRATEGY IMPLEMENTER: + # NOTE # Use the opponent model to improve acceptance strategy: # 1. self.opponent_model.get_opponent_type() - Returns opponent strategy type (HARDHEADED, CONCEDER, NEUTRAL) # 2. self.opponent_model.get_concession_rate() - Get opponent's concession rate # 3. self.opponent_model.best_bid_for_us - Best bid received (highest utility for us) - # - # Example for using opponent type in acceptance: - # opponent_type = self.opponent_model.get_opponent_type() - # if opponent_type == "HARDHEADED" and progress > 0.8: - # # Accept lower utility bids from hardheaded opponents late in negotiation - # return self.profile.getUtility(bid) >= 0.7 - - # Current basic implementation below: + + # very basic approach that accepts if the offer is valued above 0.7 and # 95% of the time towards the deadline has passed conditions = [ self.profile.getUtility(bid) > 0.8, progress > 0.95, ] - - # First phase, before the soft threshold (0 < progress < 0.6) - if progress < 0.6: - return self.profile.getUtility(bid) >= 0.9 - - # Second phase, before hard threshold (0.6 <= progress < 0.9) - if progress < 0.9: - return self.profile.getUtility(bid) >= 0.75 - - # Third phase, critical phase (0.9 <= progress < 1) - return True + return all(conditions) def find_bid(self) -> Bid: - # NOTE FOR BIDDING STRATEGY IMPLEMENTER: + # NOTE # Use the opponent model to improve bidding strategy: # 1. self.opponent_model.get_opponent_type() - Returns opponent type (HARDHEADED, CONCEDER, NEUTRAL) # 2. self.opponent_model.get_top_issues(3) - Returns top 3 issues important to opponent as [(issue_id, weight),...] # 3. self.opponent_model.get_predicted_utility(bid) - Estimate opponent's utility for a bid # 4. self.opponent_model.best_bid_for_us - Best bid received (highest utility for us) - # - # Example of using opponent's top issues to create bids they prefer: - # top_issues = self.opponent_model.get_top_issues(2) # Get top 2 most important issues - # # Then create bids that have good values for the opponent on these important issues - # - # Example of using opponent type to adjust strategy: - # opponent_type = self.opponent_model.get_opponent_type() - # if opponent_type == "CONCEDER": - # # With conceder opponents, we can propose higher utility bids for us - # # ... + # Current basic implementation below: # compose a list of all possible bids diff --git a/agents/group16_agent/utils/opponent_model.py b/agents/group16_agent/utils/opponent_model.py index 447660fd..01d0cb7b 100644 --- a/agents/group16_agent/utils/opponent_model.py +++ b/agents/group16_agent/utils/opponent_model.py @@ -26,6 +26,14 @@ def __init__(self, domain: Domain): self.bid_count = 0 self.concession_rate = 0.0 self.repeated_bids = defaultdict(int) + + # learn from previous mistakes + self.force_accept_at_remaining_turns = 1 + self.force_accept_at_remaining_turns_light = 1 + + self.hard_accept_at_turn_X = 1 + self.soft_accept_at_turn_X = 1 + self.top_bids_percentage = 1/300 # Track history of opponent utilities to analyze concession self.opponent_utilities = [] @@ -166,6 +174,45 @@ def get_top_issues(self, n: int = 3) -> List[Tuple[str, float]]: # TODO: Add methods to save/load opponent data for persistent learning + def learn_from_past_sessions(self, sessions: list[SessionData]): + hard_accept_levels = [0, 0, 1, 1.1] + soft_accept_levels = [0, 1, 1.1] + top_bids_levels = [1 / 300, 1 / 100, 1 / 30] + + # fully failed + failed_sessions_count = 0 + for session in sessions: + if self.did_fail(session): + failed_sessions_count += 1 + + # low utility + low_utility_sessions_count = 0 + for session in sessions: + if self.low_utility(session): + low_utility_sessions_count += 1 + + # hard accept based on previous failed sessions + if failed_sessions_count >= len(hard_accept_levels): + accept_index = len(hard_accept_levels) - 1 + else: + accept_index = failed_sessions_count + self.hard_accept_at_turn_X = hard_accept_levels[accept_index] + # self.force_accept_at_remaining_turns = hard_accept_levels[accept_index] + + # soft + if failed_sessions_count >= len(soft_accept_levels): + light_accept_index = len(soft_accept_levels) - 1 + else: + light_accept_index = failed_sessions_count + self.soft_accept_at_turn_X = soft_accept_levels[light_accept_index] + # self.force_accept_at_remaining_turns_light = soft_accept_levels[light_accept_index] + + # Set top_bids_percentage based on low utility sessions + if low_utility_sessions_count >= len(top_bids_levels): + top_bids_index = len(top_bids_levels) - 1 + else: + top_bids_index = low_utility_sessions_count + self.top_bids_percentage = top_bids_levels[top_bids_index] class IssueEstimator: def __init__(self, value_set: DiscreteValueSet): diff --git a/run.py b/run.py index ce6878f5..96f46bb2 100644 --- a/run.py +++ b/run.py @@ -17,14 +17,14 @@ # You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement settings = { "agents": [ - { - "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", - "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, - }, # { - # "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", - # "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + # "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, # }, + { + "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + }, { "class": "agents.group16_agent.group16_agent.Group16Agent", "parameters": {"storage_dir": "agent_storage/Group16Agent"}, From cd232dba7817d7a5dbbe69a4e1dc1740921f034c Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Thu, 3 Apr 2025 16:59:24 +0200 Subject: [PATCH 10/17] pickle file hooked to opponent model --- agents/group16_agent/group16_agent.py | 51 ++++++++++++++---- agents/group16_agent/utils/opponent_model.py | 10 ++-- agents/group16_agent/utils/wrapper.py | 56 ++++++++++++++++++-- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index da2ebd2c..79039b7d 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -53,6 +53,12 @@ def __init__(self): self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None self.opponent = None + self.opponent_best_bid: Bid = None + + # Session tracking + self.utility_at_finish: float = 0.0 + self.did_accept: bool = False + self.logger.log(logging.INFO, "party is initialized") def notifyChange(self, data: Inform): @@ -104,6 +110,16 @@ def notifyChange(self, data: Inform): # Finished will be send if the negotiation has ended (through agreement or deadline) elif isinstance(data, Finished): + # RAFA: check if agreement reached + agreements = cast(Finished, data).getAgreements() + if len(agreements.getMap()) > 0: + agreed_bid = agreements.getMap()[self.me] + self.utility_at_finish = float(self.profile.getUtility(agreed_bid)) + self.logger.log(logging.INFO, f"Agreement reached with utility: {self.utility_at_finish}") + else: + self.utility_at_finish = 0.0 + self.logger.log(logging.INFO, "No agreement reached") + self.save_data() # terminate the agent MUST BE CALLED self.logger.log(logging.INFO, "party is terminating:") @@ -175,11 +191,17 @@ def my_turn(self): """ if(not self.got_opponent): self.opponent = wrapper.get_opponent_data(self.parameters.get("storage_dir"), self.other) + # Apply opponent learned parameters using OpponentModel's learn_from_past_sessions + if self.opponent_model is not None and self.opponent.sessions: + # Use the existing learn_from_past_sessions method in the OpponentModel class + self.opponent_model.learn_from_past_sessions(self.opponent.sessions) self.got_opponent = True + # 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) + self.did_accept = True else: # if not, find a bid to propose as counter offer bid = self.find_bid() @@ -193,7 +215,16 @@ def save_data(self): 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. """ - wrapper.save_opponent_data(self.parameters.get("storage_dir"), self.opponent) + # One-liner that calls wrapper to handle all session data creation and saving + wrapper.create_and_save_session_data( + opponent=self.opponent, + savepath=self.parameters.get("storage_dir"), + progress=self.progress.get(time() * 1000), + utility_at_finish=self.utility_at_finish, + did_accept=self.did_accept, + opponent_model=self.opponent_model + ) + self.got_opponent = False data = "Data for learning (see README.md)" with open(f"{self.storage_dir}/data.md", "w") as f: @@ -210,20 +241,18 @@ def accept_condition(self, bid: Bid) -> bool: # progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) - # NOTE - # Use the opponent model to improve acceptance strategy: - # 1. self.opponent_model.get_opponent_type() - Returns opponent strategy type (HARDHEADED, CONCEDER, NEUTRAL) - # 2. self.opponent_model.get_concession_rate() - Get opponent's concession rate - # 3. self.opponent_model.best_bid_for_us - Best bid received (highest utility for us) - + # Use learned parameters from opponent model + threshold = 0.95 + if self.opponent_model and hasattr(self.opponent_model, 'force_accept_at_remaining_turns'): + threshold = max(0.85, 1 - 0.3 * self.opponent_model.force_accept_at_remaining_turns) # very basic approach that accepts if the offer is valued above 0.7 and # 95% of the time towards the deadline has passed conditions = [ self.profile.getUtility(bid) > 0.8, - progress > 0.95, + progress > threshold, ] - return all(conditions) + return any(conditions) def find_bid(self) -> Bid: # NOTE @@ -251,8 +280,8 @@ def find_bid(self) -> Bid: # RAFA: we're late in the negotiation, consider returning the best bid we received progress = self.progress.get(time() * 1000) - if progress > 0.95 and hasattr(self.opponent_model, 'best_bid_for_us') and self.opponent_model.best_bid_for_us is not None: - return self.opponent_model.best_bid_for_us + if progress > 0.95 and self.opponent_best_bid is not None: + return self.opponent_best_bid return best_bid diff --git a/agents/group16_agent/utils/opponent_model.py b/agents/group16_agent/utils/opponent_model.py index 01d0cb7b..47025e68 100644 --- a/agents/group16_agent/utils/opponent_model.py +++ b/agents/group16_agent/utils/opponent_model.py @@ -174,7 +174,7 @@ def get_top_issues(self, n: int = 3) -> List[Tuple[str, float]]: # TODO: Add methods to save/load opponent data for persistent learning - def learn_from_past_sessions(self, sessions: list[SessionData]): + def learn_from_past_sessions(self, sessions: list): hard_accept_levels = [0, 0, 1, 1.1] soft_accept_levels = [0, 1, 1.1] top_bids_levels = [1 / 300, 1 / 100, 1 / 30] @@ -196,16 +196,16 @@ def learn_from_past_sessions(self, sessions: list[SessionData]): accept_index = len(hard_accept_levels) - 1 else: accept_index = failed_sessions_count - self.hard_accept_at_turn_X = hard_accept_levels[accept_index] - # self.force_accept_at_remaining_turns = hard_accept_levels[accept_index] + #self.hard_accept_at_turn_X = hard_accept_levels[accept_index] + self.force_accept_at_remaining_turns = hard_accept_levels[accept_index] # soft if failed_sessions_count >= len(soft_accept_levels): light_accept_index = len(soft_accept_levels) - 1 else: light_accept_index = failed_sessions_count - self.soft_accept_at_turn_X = soft_accept_levels[light_accept_index] - # self.force_accept_at_remaining_turns_light = soft_accept_levels[light_accept_index] + #self.soft_accept_at_turn_X = soft_accept_levels[light_accept_index] + self.force_accept_at_remaining_turns_light = soft_accept_levels[light_accept_index] # Set top_bids_percentage based on low utility sessions if low_utility_sessions_count >= len(top_bids_levels): diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py index c5188f8b..f75403a0 100644 --- a/agents/group16_agent/utils/wrapper.py +++ b/agents/group16_agent/utils/wrapper.py @@ -4,14 +4,21 @@ class Opponent: - def __init__(self, result=0, finalUtility=0, offerVariance=[], name=""): + def __init__(self, result=0, finalUtility=0, offerVariance=[], name="", sessions=None): self.result = result self.finalUtility = finalUtility self.offerVariance = offerVariance self.name = name + self.sessions = sessions if sessions is not None else [] - def save(self): - save_opponent_data(self) + def add_session(self, session_data): + """Add a new session data entry to the sessions list""" + if self.sessions is None: + self.sessions = [] + self.sessions.append(session_data) + + def save(self, savepath): + save_opponent_data(savepath, self) def normalize(self): # not the behaviour, will take all the raw data and make it into statistics @@ -19,7 +26,6 @@ def normalize(self): def get_opponent_data(savepath, name): - file_path = savepath + name+".plk" if not os.path.exists(savepath): os.makedirs(savepath) @@ -48,3 +54,45 @@ def save_opponent_data(savepath, opponent): pd.to_pickle(opponent, file_path) else: print("Non opponent saved") + + +def create_and_save_session_data(opponent, savepath, progress, utility_at_finish, did_accept, opponent_model=None): + """Create and save session data for an opponent + + Args: + opponent: The Opponent object to update + savepath: Path to save the opponent data + progress: Current negotiation progress + utility_at_finish: Utility of the final agreement (0 if no agreement) + did_accept: Whether we accepted the opponent's offer + opponent_model: Optional OpponentModel to get current parameters from + """ + if opponent is None: + print("Cannot save session data: opponent is None") + return + + # Get default values + min_util = 0.7 # Threshold for a good deal + top_bids_percentage = 1/300 + force_accept_at_remaining_turns = 1 + + # Get values from opponent model if available + if opponent_model: + top_bids_percentage = getattr(opponent_model, 'top_bids_percentage', top_bids_percentage) + force_accept_at_remaining_turns = getattr(opponent_model, 'force_accept_at_remaining_turns', force_accept_at_remaining_turns) + + # Create session data dictionary + session_data = { + "progressAtFinish": progress, + "utilityAtFinish": utility_at_finish, + "didAccept": did_accept, + "isGood": utility_at_finish >= min_util, + "topBidsPercentage": top_bids_percentage, + "forceAcceptAtRemainingTurns": force_accept_at_remaining_turns + } + + # Add session data to opponent + opponent.add_session(session_data) + + # Save opponent data + save_opponent_data(savepath, opponent) From 26af3861c7cfc4c55fd45c2b987dc519d3400c13 Mon Sep 17 00:00:00 2001 From: rebeccanys Date: Fri, 4 Apr 2025 00:50:00 +0200 Subject: [PATCH 11/17] Basic time based bidding --- agents/group16_agent/group16_agent.py | 96 ++++++++++++++++----------- run.py | 16 +++-- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index d3225f87..c9d8a99f 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -25,6 +25,7 @@ ) from geniusweb.progress.ProgressTime import ProgressTime from geniusweb.references.Parameters import Parameters +from numpy import floor from tudelft_utilities_logging.ReportToLogger import ReportToLogger from .utils.opponent_model import OpponentModel @@ -38,6 +39,9 @@ class Group16Agent(DefaultParty): def __init__(self): super().__init__() + self.find_bid_result = None + self.best_bid = None + self.bids_with_utilities = None self.logger: ReportToLogger = self.getReporter() self.domain: Domain = None @@ -199,6 +203,11 @@ def accept_condition(self, bid: Bid) -> bool: if bid is None: return False + # Keep track of the best bid the opponent made so far + utility = self.profile.getUtility(bid) + if self.best_bid is None or self.profile.getUtility(self.best_bid) < utility: + self.best_bid = bid + # progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) @@ -208,58 +217,65 @@ def accept_condition(self, bid: Bid) -> bool: self.profile.getUtility(bid) > 0.8, progress > 0.95, ] + return all(conditions) - # First phase, before the soft threshold (0 < progress < 0.6) - if progress < 0.6: - return self.profile.getUtility(bid) >= 0.9 - - # Second phase, before hard threshold (0.6 <= progress < 0.9) - if progress < 0.9: - return self.profile.getUtility(bid) >= 0.75 - - # Third phase, critical phase (0.9 <= progress < 1) - return True def find_bid(self) -> Bid: - # compose a list of all possible bids + """ + Determines the next bid to offer. + - Starts by offering bids from the top 1% ranked by utility. + - Expands the bid range dynamically as time progresses, up to the top 20%. + - If time is running out, proposes the best bid received from the opponent. + """ + + # Retrieve all possible bids in the domain domain = self.profile.getDomain() all_bids = AllBidsList(domain) + num_of_bids = all_bids.size() - best_bid_score = 0.0 - best_bid = None + # If bids with utilities haven't been calculated yet, compute them + if self.bids_with_utilities is None: + self.bids_with_utilities = [] - # 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 + # Calculate utility for each bid and store them in a list + for index in range(num_of_bids): + bid = all_bids.get(index) + bid_utility = float(self.profile.getUtility(bid)) + self.bids_with_utilities.append((bid, bid_utility)) - return best_bid + # Sort bids by utility from high to low + self.bids_with_utilities.sort(key=lambda tup: tup[1], reverse=True) - def score_bid(self, bid: Bid, alpha: float = 0.95, eps: float = 0.1) -> float: - """Calculate heuristic score for a bid + # Get the current progress of the negotiation (0 to 1 scale) + progress = self.progress.get(time() * 1000) - Args: - bid (Bid): Bid to score - alpha (float, optional): Trade-off factor between self interested and - altruistic behaviour. Defaults to 0.95. - eps (float, optional): Time pressure factor, balances between conceding - and Boulware behaviour over time. Defaults to 0.1. + # Expand the range of acceptable bids over time (starts at 1% and increases gradually up to 20%) + increasing_percentage = min(0.01 + progress * 0.19, 0.2) + expanded_top_bids = max(5, floor(num_of_bids * increasing_percentage)) - Returns: - float: score - """ - progress = self.progress.get(time() * 1000) + # Dynamically decrease threshold: as time progresses, the threshold lowers, making concessions more likely + #dynamic_threshold = max(0.5, 1 - progress * 0.5) + + # If progress exceeds the threshold, offer the best bid from the opponent + #if progress > dynamic_threshold and self.best_bid is not None: + # return self.best_bid - our_utility = float(self.profile.getUtility(bid)) +#TODO: PUT THIS AT THE BEGINNING AND EXTEND IT FOR THE ENTIRE TIMELINE + MAKE IT A BIT RANDOM SO THAT IT'S UNPREDICTABLE + # Calculate the minimum utility threshold dynamically based on progress + # If the opponent's best bid meets the dynamically decreasing utility requirement, offer it + # Works like this: + # When progress = 0.5, offer opponent's best bid if its utility is > 0.95 + # When progress = 0.7, offer opponent's best bid if its utility is > 0.77 + # When progress = 0.9, offer opponent's best bid if its utility is > 0.59 + min_utility_threshold = max(0.5, 1.4 - 0.9 * progress) - time_pressure = 1.0 - progress ** (1 / eps) - score = alpha * time_pressure * our_utility + if self.best_bid is not None: + best_bid_utility = float(self.profile.getUtility(self.best_bid)) - if self.opponent_model is not None: - opponent_utility = self.opponent_model.get_predicted_utility(bid) - opponent_score = (1.0 - alpha * time_pressure) * opponent_utility - score += opponent_score + if best_bid_utility > min_utility_threshold: + return self.best_bid - return score + # Randomly select a bid from the expanded top bids range + next_bid = randint(0, expanded_top_bids - 1) + self.find_bid_result = self.bids_with_utilities[next_bid][0] + return self.bids_with_utilities[next_bid][0] \ No newline at end of file diff --git a/run.py b/run.py index ce6878f5..c3acebe7 100644 --- a/run.py +++ b/run.py @@ -17,14 +17,18 @@ # You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement settings = { "agents": [ + #{ + # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + # "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, + #}, + #{ + # "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + # "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + #}, { - "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", - "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, + "class": "agents.template_agent.template_agent.TemplateAgent", + "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, }, - # { - # "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", - # "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, - # }, { "class": "agents.group16_agent.group16_agent.Group16Agent", "parameters": {"storage_dir": "agent_storage/Group16Agent"}, From d5f70ff961dd0f196b4add3671b638f69289aa13 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Fri, 4 Apr 2025 13:20:41 +0200 Subject: [PATCH 12/17] inline functions added --- .../ANL2022/dreamteam109_agent/dreamteam109_agent.py | 2 +- agents/group16_agent/utils/opponent_model.py | 6 ++++-- run.py | 12 ++++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py b/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py index 18acca3f..f04a11b7 100644 --- a/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py +++ b/agents/ANL2022/dreamteam109_agent/dreamteam109_agent.py @@ -267,7 +267,7 @@ def update_data_dict(self): "progressAtFinish": progress_at_finish, "utilityAtFinish": self.utility_at_finish, "didAccept": self.did_accept, - "isGood": self.utility_at_finish >= selfisGood.min_util, + "isGood": self.utility_at_finish >= self.min_util, "topBidsPercentage": self.top_bids_percentage, "forceAcceptAtRemainingTurns": self.force_accept_at_remaining_turns } diff --git a/agents/group16_agent/utils/opponent_model.py b/agents/group16_agent/utils/opponent_model.py index 47025e68..1f1fee81 100644 --- a/agents/group16_agent/utils/opponent_model.py +++ b/agents/group16_agent/utils/opponent_model.py @@ -182,13 +182,15 @@ def learn_from_past_sessions(self, sessions: list): # fully failed failed_sessions_count = 0 for session in sessions: - if self.did_fail(session): + # Check if session failed (utility == 0) + if isinstance(session, dict) and session.get("utilityAtFinish", 1) == 0: failed_sessions_count += 1 # low utility low_utility_sessions_count = 0 for session in sessions: - if self.low_utility(session): + # Check if session had low utility (utility < 0.5) + if isinstance(session, dict) and session.get("utilityAtFinish", 1) < 0.5: low_utility_sessions_count += 1 # hard accept based on previous failed sessions diff --git a/run.py b/run.py index 96f46bb2..ce6878f5 100644 --- a/run.py +++ b/run.py @@ -17,14 +17,14 @@ # You need to specify a time deadline (is milliseconds (ms)) we are allowed to negotiate before we end without agreement settings = { "agents": [ - # { - # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", - # "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, - # }, { - "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", - "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, }, + # { + # "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + # "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + # }, { "class": "agents.group16_agent.group16_agent.Group16Agent", "parameters": {"storage_dir": "agent_storage/Group16Agent"}, From d7ada00c15ef10f63cbed79c2977b864a30f10b6 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Fri, 4 Apr 2025 17:57:59 +0200 Subject: [PATCH 13/17] tournament works properly --- agents/group16_agent/group16_agent.py | 24 ++-- run_tournament.py | 162 +++++++++++++------------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index 79039b7d..94c02603 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -189,7 +189,8 @@ 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. """ - if(not self.got_opponent): + # Only try to load opponent data if we know who the opponent is, might be wrong + if self.other is not None and not self.got_opponent: self.opponent = wrapper.get_opponent_data(self.parameters.get("storage_dir"), self.other) # Apply opponent learned parameters using OpponentModel's learn_from_past_sessions if self.opponent_model is not None and self.opponent.sessions: @@ -215,15 +216,18 @@ def save_data(self): 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. """ - # One-liner that calls wrapper to handle all session data creation and saving - wrapper.create_and_save_session_data( - opponent=self.opponent, - savepath=self.parameters.get("storage_dir"), - progress=self.progress.get(time() * 1000), - utility_at_finish=self.utility_at_finish, - did_accept=self.did_accept, - opponent_model=self.opponent_model - ) + # problem with trying to save opponent data if we don't have an opponent response yet + if self.other is not None and self.opponent is not None: + wrapper.create_and_save_session_data( + opponent=self.opponent, + savepath=self.parameters.get("storage_dir"), + progress=self.progress.get(time() * 1000), + utility_at_finish=self.utility_at_finish, + did_accept=self.did_accept, + opponent_model=self.opponent_model + ) + else: + self.logger.log(logging.INFO, "No opponent data to save (opponent unknown or no model created)") self.got_opponent = False data = "Data for learning (see README.md)" diff --git a/run_tournament.py b/run_tournament.py index 03d59fb9..3e106f04 100644 --- a/run_tournament.py +++ b/run_tournament.py @@ -21,30 +21,30 @@ "class": "agents.group16_agent.group16_agent.Group16Agent", "parameters": {"storage_dir": "agent_storage/Group16Agent"}, }, - { - "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", - }, - { - "class": "agents.conceder_agent.conceder_agent.ConcederAgent", - }, - { - "class": "agents.hardliner_agent.hardliner_agent.HardlinerAgent", - }, - { - "class": "agents.linear_agent.linear_agent.LinearAgent", - }, - { - "class": "agents.random_agent.random_agent.RandomAgent", - }, - { - "class": "agents.stupid_agent.stupid_agent.StupidAgent", - }, - { - "class": "agents.CSE3210.agent2.agent2.Agent2", - }, - { - "class": "agents.CSE3210.agent3.agent3.Agent3", - }, + # { + # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", + # }, + # { + # "class": "agents.conceder_agent.conceder_agent.ConcederAgent", + # }, + # { + # "class": "agents.hardliner_agent.hardliner_agent.HardlinerAgent", + # }, + # { + # "class": "agents.linear_agent.linear_agent.LinearAgent", + # }, + # { + # "class": "agents.random_agent.random_agent.RandomAgent", + # }, + # { + # "class": "agents.stupid_agent.stupid_agent.StupidAgent", + # }, + # { + # "class": "agents.CSE3210.agent2.agent2.Agent2", + # }, + # { + # "class": "agents.CSE3210.agent3.agent3.Agent3", + # }, { "class": "agents.CSE3210.agent7.agent7.Agent7", }, @@ -57,63 +57,63 @@ { "class": "agents.CSE3210.agent18.agent18.Agent18", }, - { - "class": "agents.CSE3210.agent19.agent19.Agent19", - }, - { - "class": "agents.CSE3210.agent22.agent22.Agent22", - }, - { - "class": "agents.CSE3210.agent24.agent24.Agent24", - }, - { - "class": "agents.CSE3210.agent25.agent25.Agent25", - }, - { - "class": "agents.CSE3210.agent26.agent26.Agent26", - }, - { - "class": "agents.CSE3210.agent27.agent27.Agent27", - }, - { - "class": "agents.CSE3210.agent29.agent29.Agent29", - }, - { - "class": "agents.CSE3210.agent32.agent32.Agent32", - }, - { - "class": "agents.CSE3210.agent33.agent33.Agent33", - }, - { - "class": "agents.CSE3210.agent41.agent41.Agent41", - }, - { - "class": "agents.CSE3210.agent43.agent43.Agent43", - }, - { - "class": "agents.CSE3210.agent50.agent50.Agent50", - }, - { - "class": "agents.CSE3210.agent52.agent52.Agent52", - }, - { - "class": "agents.CSE3210.agent55.agent55.Agent55", - }, - { - "class": "agents.CSE3210.agent58.agent58.Agent58", - }, - { - "class": "agents.CSE3210.agent61.agent61.Agent61", - }, - { - "class": "agents.CSE3210.agent64.agent64.Agent64", - }, - { - "class": "agents.CSE3210.agent67.agent67.Agent67", - }, - { - "class": "agents.CSE3210.agent68.agent68.Agent68", - }, + # { + # "class": "agents.CSE3210.agent19.agent19.Agent19", + # }, + # { + # "class": "agents.CSE3210.agent22.agent22.Agent22", + # }, + # { + # "class": "agents.CSE3210.agent24.agent24.Agent24", + # }, + # { + # "class": "agents.CSE3210.agent25.agent25.Agent25", + # }, + # { + # "class": "agents.CSE3210.agent26.agent26.Agent26", + # }, + # { + # "class": "agents.CSE3210.agent27.agent27.Agent27", + # }, + # { + # "class": "agents.CSE3210.agent29.agent29.Agent29", + # }, + # { + # "class": "agents.CSE3210.agent32.agent32.Agent32", + # }, + # { + # "class": "agents.CSE3210.agent33.agent33.Agent33", + # }, + # { + # "class": "agents.CSE3210.agent41.agent41.Agent41", + # }, + # { + # "class": "agents.CSE3210.agent43.agent43.Agent43", + # }, + # { + # "class": "agents.CSE3210.agent50.agent50.Agent50", + # }, + # { + # "class": "agents.CSE3210.agent52.agent52.Agent52", + # }, + # { + # "class": "agents.CSE3210.agent55.agent55.Agent55", + # }, + # { + # "class": "agents.CSE3210.agent58.agent58.Agent58", + # }, + # { + # "class": "agents.CSE3210.agent61.agent61.Agent61", + # }, + # { + # "class": "agents.CSE3210.agent64.agent64.Agent64", + # }, + # { + # "class": "agents.CSE3210.agent67.agent67.Agent67", + # }, + # { + # "class": "agents.CSE3210.agent68.agent68.Agent68", + # }, ], "profile_sets": [ ["domains/domain00/profileA.json", "domains/domain00/profileB.json"], From 1f0efb277262eaaefb6871533619ce0818ae03e5 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Sat, 5 Apr 2025 09:12:36 +0200 Subject: [PATCH 14/17] starting point --- agents/group16_agent/group16_agent.py | 13 ++++++------- agents/group16_agent/utils/wrapper.py | 3 +++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index 94c02603..cffe9ec3 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -34,6 +34,10 @@ class Group16Agent(DefaultParty): """ The amazing Python geniusweb agent made by team 16. + Should store general information so that geniuse web works + Should store information about the current exchange that isn't related to the opponent + Opponent model should store information about the opponent + Opponent/Wrapper should store information about the opponent that we want persistent between encounters """ def __init__(self): @@ -53,7 +57,6 @@ def __init__(self): self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None self.opponent = None - self.opponent_best_bid: Bid = None # Session tracking self.utility_at_finish: float = 0.0 @@ -180,10 +183,6 @@ def opponent_action(self, action): # set bid as last received self.last_received_bid = bid - - # Store best bid if this one has the highest utility for us so far - if hasattr(self.opponent_model, 'best_bid_for_us') and self.opponent_model.best_bid_for_us is not None: - self.opponent_best_bid = self.opponent_model.best_bid_for_us def my_turn(self): """This method is called when it is our turn. It should decide upon an action @@ -284,8 +283,8 @@ def find_bid(self) -> Bid: # RAFA: we're late in the negotiation, consider returning the best bid we received progress = self.progress.get(time() * 1000) - if progress > 0.95 and self.opponent_best_bid is not None: - return self.opponent_best_bid + if progress > 0.95 and self.opponent_model is not None and self.opponent_model.best_bid_for_us is not None: + return self.opponent_model.best_bid_for_us return best_bid diff --git a/agents/group16_agent/utils/wrapper.py b/agents/group16_agent/utils/wrapper.py index f75403a0..4818be8e 100644 --- a/agents/group16_agent/utils/wrapper.py +++ b/agents/group16_agent/utils/wrapper.py @@ -4,6 +4,9 @@ class Opponent: + ''' + Used to store opponent information that we would like to stay persistent across sessions/encounters + ''' def __init__(self, result=0, finalUtility=0, offerVariance=[], name="", sessions=None): self.result = result self.finalUtility = finalUtility From 5be94e8ee001748fd696fb61d1aefeac0b6ef6ac Mon Sep 17 00:00:00 2001 From: rebeccanys Date: Sat, 5 Apr 2025 09:35:16 +0200 Subject: [PATCH 15/17] Improved time based bidding --- agents/group16_agent/group16_agent.py | 42 +++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index c9d8a99f..871087a3 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -1,5 +1,5 @@ import logging -from random import randint +from random import randint, uniform, choice from time import time from typing import cast @@ -220,6 +220,8 @@ def accept_condition(self, bid: Bid) -> bool: return all(conditions) + # paper 1: + # paper 2: https://www.researchgate.net/publication/2526436_Multi-Issue_Negotiation_Under_Time_Constraints def find_bid(self) -> Bid: """ Determines the next bid to offer. @@ -228,6 +230,26 @@ def find_bid(self) -> Bid: - If time is running out, proposes the best bid received from the opponent. """ + # Get the current progress of the negotiation (0 to 1 scale) + progress = self.progress.get(time() * 1000) + + # Calculate the minimum utility threshold dynamically based on progress + # If the opponent's best bid meets the dynamically decreasing utility requirement, offer it + # Add randomness and variation to the threshold to make us less predictable + if self.best_bid is not None: + #min_utility_threshold = max(0.5, 1.4 - 0.9 * progress) + random_variation = uniform(-0.02, 0.02) + random_strategy = choice(['linear', 'quadratic']) + if random_strategy == 'linear': + min_utility_threshold = max(0.5, min(1.0, -0.5 * progress + 1 + random_variation)) + else: + min_utility_threshold = max(0.5, min(1.0, -0.5 * (progress ** 2) + 1 + random_variation)) + + best_bid_utility = float(self.profile.getUtility(self.best_bid)) + + if best_bid_utility >= min_utility_threshold: + return self.best_bid + # Retrieve all possible bids in the domain domain = self.profile.getDomain() all_bids = AllBidsList(domain) @@ -246,9 +268,6 @@ def find_bid(self) -> Bid: # Sort bids by utility from high to low self.bids_with_utilities.sort(key=lambda tup: tup[1], reverse=True) - # Get the current progress of the negotiation (0 to 1 scale) - progress = self.progress.get(time() * 1000) - # Expand the range of acceptable bids over time (starts at 1% and increases gradually up to 20%) increasing_percentage = min(0.01 + progress * 0.19, 0.2) expanded_top_bids = max(5, floor(num_of_bids * increasing_percentage)) @@ -260,21 +279,6 @@ def find_bid(self) -> Bid: #if progress > dynamic_threshold and self.best_bid is not None: # return self.best_bid -#TODO: PUT THIS AT THE BEGINNING AND EXTEND IT FOR THE ENTIRE TIMELINE + MAKE IT A BIT RANDOM SO THAT IT'S UNPREDICTABLE - # Calculate the minimum utility threshold dynamically based on progress - # If the opponent's best bid meets the dynamically decreasing utility requirement, offer it - # Works like this: - # When progress = 0.5, offer opponent's best bid if its utility is > 0.95 - # When progress = 0.7, offer opponent's best bid if its utility is > 0.77 - # When progress = 0.9, offer opponent's best bid if its utility is > 0.59 - min_utility_threshold = max(0.5, 1.4 - 0.9 * progress) - - if self.best_bid is not None: - best_bid_utility = float(self.profile.getUtility(self.best_bid)) - - if best_bid_utility > min_utility_threshold: - return self.best_bid - # Randomly select a bid from the expanded top bids range next_bid = randint(0, expanded_top_bids - 1) self.find_bid_result = self.bids_with_utilities[next_bid][0] From 443db3fece540ffad7efbaebd99c745433dadca1 Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Sat, 5 Apr 2025 11:37:14 +0200 Subject: [PATCH 16/17] variance trial --- agents/group16_agent/group16_agent.py | 297 ++++++++++++++++++-------- run.py | 12 +- 2 files changed, 212 insertions(+), 97 deletions(-) diff --git a/agents/group16_agent/group16_agent.py b/agents/group16_agent/group16_agent.py index df2c7d79..002718cd 100755 --- a/agents/group16_agent/group16_agent.py +++ b/agents/group16_agent/group16_agent.py @@ -1,7 +1,8 @@ import logging from random import randint, uniform, choice +from statistics import variance, mean from time import time -from typing import cast +from typing import cast, List from geniusweb.actions.Accept import Accept from geniusweb.actions.Action import Action @@ -34,11 +35,13 @@ class Group16Agent(DefaultParty): """ - The amazing Python geniusweb agent made by team 16. - Should store general information so that geniuse web works - Should store information about the current exchange that isn't related to the opponent - Opponent model should store information about the opponent - Opponent/Wrapper should store information about the opponent that we want persistent between encounters + Group16 agent implementing Gahboninho's strategy with session data integration. + + Key features: + - Uses variance in opponent bids to estimate concession willingness + - Calculates target utility using Ut = Umax - (Umax - Umin) * t formula + - Integrates session data from previous negotiations + - Uses random selection of bids above target utility """ def __init__(self): @@ -58,14 +61,37 @@ def __init__(self): self.storage_dir: str = None self.got_opponent = False + # Tracking bids and utilities self.last_received_bid: Bid = None self.opponent_model: OpponentModel = None self.opponent = None + self.all_bids: AllBidsList = None + + # Gahboninho strategy parameters + self.round_count: int = 0 + self.opponent_utilities: List[float] = [] + self.concession_rate: float = 0.0 + self.opponent_utility_variance: float = 0.0 + self.probing_phase_complete: bool = False + + # Target utility parameters (Gahboninho) + self.max_target_utility: float = 0.95 + self.min_target_utility: float = 0.7 + + # For deadline management (DreamTeam) + self.avg_time_per_round = None + self.round_times = [] + self.last_time = None # Session tracking self.utility_at_finish: float = 0.0 self.did_accept: bool = False + # Parameters from past sessions (DreamTeam) + self.force_accept_at_remaining_turns: float = 1.0 + self.force_accept_at_remaining_turns_light: float = 1.0 + self.top_bids_percentage: float = 1/300 + self.logger.log(logging.INFO, "party is initialized") def notifyChange(self, data: Inform): @@ -95,6 +121,10 @@ def notifyChange(self, data: Inform): ) self.profile = profile_connection.getProfile() self.domain = self.profile.getDomain() + + # Initialize list of all possible bids + self.all_bids = AllBidsList(self.domain) + profile_connection.close() # ActionDone informs you of an action (an offer or an accept) @@ -117,7 +147,7 @@ def notifyChange(self, data: Inform): # Finished will be send if the negotiation has ended (through agreement or deadline) elif isinstance(data, Finished): - # RAFA: check if agreement reached + # Check if agreement reached agreements = cast(Finished, data).getAgreements() if len(agreements.getMap()) > 0: agreed_bid = agreements.getMap()[self.me] @@ -163,7 +193,7 @@ def getDescription(self) -> str: Returns: str: Agent description """ - return "Team 16's agent, the best agent in the tournament!" + return "Team 16's GahboniTeam agent using adaptive concession based on opponent behavior" def opponent_action(self, action): """Process an action that was received from the opponent. @@ -185,20 +215,52 @@ def opponent_action(self, action): # update opponent model with bid and our utility self.opponent_model.update(bid, our_utility) + # Keep track of the best bid the opponent made + if self.best_bid is None or our_utility > float(self.profile.getUtility(self.best_bid)): + self.best_bid = bid + + # Track opponent utility for Gahboninho analysis + opponent_utility = self.opponent_model.get_predicted_utility(bid) + self.opponent_utilities.append(opponent_utility) + # set bid as last received self.last_received_bid = bid + + # After receiving at least 5 bids, calculate concession metrics + if len(self.opponent_utilities) >= 5 and not self.probing_phase_complete: + self.update_concession_metrics() + self.probing_phase_complete = True + # Continue updating metrics as we receive more bids + elif len(self.opponent_utilities) >= 5: + self.update_concession_metrics() 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. """ - # Only try to load opponent data if we know who the opponent is, might be wrong + # Track timing for deadline management + current_time = time() + if self.last_time is not None: + round_time = current_time - self.last_time + self.round_times.append(round_time) + if len(self.round_times) >= 3: + self.avg_time_per_round = mean(self.round_times[-3:]) + self.last_time = current_time + + # Track round count for probing phase + self.round_count += 1 + + # Load opponent data from previous sessions if self.other is not None and not self.got_opponent: self.opponent = wrapper.get_opponent_data(self.parameters.get("storage_dir"), self.other) # Apply opponent learned parameters using OpponentModel's learn_from_past_sessions if self.opponent_model is not None and self.opponent.sessions: - # Use the existing learn_from_past_sessions method in the OpponentModel class + # Use the existing learn_from_past_sessions method self.opponent_model.learn_from_past_sessions(self.opponent.sessions) + # Copy learned parameters to main agent + self.force_accept_at_remaining_turns = self.opponent_model.force_accept_at_remaining_turns + self.force_accept_at_remaining_turns_light = self.opponent_model.force_accept_at_remaining_turns_light + self.top_bids_percentage = self.opponent_model.top_bids_percentage self.got_opponent = True # check if the last received offer is good enough @@ -206,10 +268,12 @@ def my_turn(self): # if so, accept the offer action = Accept(self.me, self.last_received_bid) self.did_accept = True + self.logger.log(logging.INFO, f"Accepting bid with utility: {float(self.profile.getUtility(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.logger.log(logging.INFO, f"Offering bid with utility: {float(self.profile.getUtility(bid))}") # send the action self.send_action(action) @@ -219,7 +283,7 @@ def save_data(self): 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. """ - # problem with trying to save opponent data if we don't have an opponent response yet + # Save opponent data for future sessions if self.other is not None and self.opponent is not None: wrapper.create_and_save_session_data( opponent=self.opponent, @@ -237,108 +301,159 @@ def save_data(self): with open(f"{self.storage_dir}/data.md", "w") as f: f.write(data) - ########################################################################################### - ################################## Example methods below ################################## - ########################################################################################### + def update_concession_metrics(self): + """Update metrics about the opponent's concession behavior + + This implements part of Gahboninho's approach to measure opponent's willingness to concede + based on variance in utilities and concession rate + """ + try: + # Calculate variance in opponent utilities + self.opponent_utility_variance = variance(self.opponent_utilities) + + # Calculate concession rate + # (simple approach: how much did opponent concede on average between consecutive bids) + total_decrease = 0.0 + decreases_count = 0 + + for i in range(1, len(self.opponent_utilities)): + diff = self.opponent_utilities[i-1] - self.opponent_utilities[i] + if diff > 0: # Only count actual concessions + total_decrease += diff + decreases_count += 1 + + self.concession_rate = total_decrease / max(1, decreases_count) + + self.logger.log(logging.INFO, f"Opponent utility variance: {self.opponent_utility_variance}") + self.logger.log(logging.INFO, f"Opponent concession rate: {self.concession_rate}") + + except Exception as e: + self.logger.log(logging.WARNING, f"Error calculating concession metrics: {e}") + + def calculate_target_utility(self, progress: float) -> float: + """Calculate target utility using Gahboninho's formula: + Ut = Umax - (Umax - Umin) * t + + Adjusted based on opponent's concession behavior + """ + # Base values + umax = self.max_target_utility + umin = self.min_target_utility + + # Adjust based on observed concession behavior + # If opponent doesn't concede much (low variance and rate), we should be more willing to concede + if self.opponent_utility_variance < 0.01 or self.concession_rate < 0.02: + # Low concession opponent - be more flexible + concession_factor = 1.1 + umin = max(0.65, umin - 0.05) # Lower our minimum acceptable utility + elif self.opponent_utility_variance > 0.03 or self.concession_rate > 0.05: + # High concession opponent - be more stubborn + concession_factor = 0.8 + umin = min(0.85, umin + 0.05) # Raise our minimum acceptable utility + else: + # Neutral concession behavior + concession_factor = 1.0 + + # Apply Gahboninho's formula with the concession factor + target = umax - (umax - umin) * progress * concession_factor + + # Cap the minimum + return max(target, umin) def accept_condition(self, bid: Bid) -> bool: + """Determine whether to accept opponent's bid + + Uses DreamTeam-style dynamic thresholds and Gahboninho's target utility + """ if bid is None: return False + # Get our utility for this bid + utility = float(self.profile.getUtility(bid)) + # Keep track of the best bid the opponent made so far - utility = self.profile.getUtility(bid) - if self.best_bid is None or self.profile.getUtility(self.best_bid) < utility: + if self.best_bid is None or float(self.profile.getUtility(self.best_bid)) < utility: self.best_bid = bid - # progress of the negotiation session between 0 and 1 (1 is deadline) + # Progress of the negotiation session between 0 and 1 (1 is deadline) progress = self.progress.get(time() * 1000) - - # Use learned parameters from opponent model - threshold = 0.95 - if self.opponent_model and hasattr(self.opponent_model, 'force_accept_at_remaining_turns'): - threshold = max(0.85, 1 - 0.3 * self.opponent_model.force_accept_at_remaining_turns) - - # very basic approach that accepts if the offer is valued above 0.7 and - # 95% of the time towards the deadline has passed + + # Calculate current target utility using Gahboninho's formula + target_utility = self.calculate_target_utility(progress) + + # DreamTeam-style dynamic thresholds + threshold = 0.98 + light_threshold = 0.95 + if self.avg_time_per_round is not None: + # Calculate thresholds based on average round time and remaining time + threshold = 1 - 1000 * self.force_accept_at_remaining_turns * self.avg_time_per_round / self.progress.getDuration() + light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time_per_round / self.progress.getDuration() + + # Accept conditions conditions = [ - self.profile.getUtility(bid) > 0.8, - progress > threshold, + utility > 0.9, # Very good offer + utility >= target_utility, # Meets or exceeds our target + progress > threshold, # Very close to deadline + progress > light_threshold and utility >= target_utility - 0.05 # Approaching deadline with near-target utility ] return any(conditions) def find_bid(self) -> Bid: - # NOTE - # Use the opponent model to improve bidding strategy: - # 1. self.opponent_model.get_opponent_type() - Returns opponent type (HARDHEADED, CONCEDER, NEUTRAL) - # 2. self.opponent_model.get_top_issues(3) - Returns top 3 issues important to opponent as [(issue_id, weight),...] - # 3. self.opponent_model.get_predicted_utility(bid) - Estimate opponent's utility for a bid - # 4. self.opponent_model.best_bid_for_us - Best bid received (highest utility for us) + """Find a bid to offer based on Gahboninho's strategy - - # Current basic implementation below: - """ - Determines the next bid to offer. - - Starts by offering bids from the top 1% ranked by utility. - - Expands the bid range dynamically as time progresses, up to the top 20%. - - If time is running out, proposes the best bid received from the opponent. + - Uses probing in early negotiation + - Uses target utility formula + - Randomly selects bids above target utility + - Considers opponent's best bid in late stages """ - - # Get the current progress of the negotiation (0 to 1 scale) + # Get current progress progress = self.progress.get(time() * 1000) - - # Calculate the minimum utility threshold dynamically based on progress - # If the opponent's best bid meets the dynamically decreasing utility requirement, offer it - # Add randomness and variation to the threshold to make us less predictable - if self.best_bid is not None: - #min_utility_threshold = max(0.5, 1.4 - 0.9 * progress) - random_variation = uniform(-0.02, 0.02) - random_strategy = choice(['linear', 'quadratic']) - if random_strategy == 'linear': - min_utility_threshold = max(0.5, min(1.0, -0.5 * progress + 1 + random_variation)) - else: - min_utility_threshold = max(0.5, min(1.0, -0.5 * (progress ** 2) + 1 + random_variation)) - + + # During probing phase, use high utility bids + if self.round_count < 5: + # Steadily concede during probing phase from 0.95 to 0.9 + target_utility = max(0.9, 0.95 - self.round_count * 0.01) + else: + # After probing phase, use Gahboninho's formula + target_utility = self.calculate_target_utility(progress) + + # If we're near the deadline, consider using opponent's best bid + light_threshold = 0.95 + if self.avg_time_per_round is not None: + light_threshold = 1 - 5000 * self.force_accept_at_remaining_turns_light * self.avg_time_per_round / self.progress.getDuration() + + if progress > light_threshold and self.best_bid is not None: best_bid_utility = float(self.profile.getUtility(self.best_bid)) - - if best_bid_utility >= min_utility_threshold: + if best_bid_utility >= target_utility - 0.1: return self.best_bid - - # Retrieve all possible bids in the domain - domain = self.profile.getDomain() - all_bids = AllBidsList(domain) - num_of_bids = all_bids.size() - - # If bids with utilities haven't been calculated yet, compute them + + # Calculate bids with utilities if not done yet if self.bids_with_utilities is None: self.bids_with_utilities = [] - - # Calculate utility for each bid and store them in a list - for index in range(num_of_bids): - bid = all_bids.get(index) + for index in range(self.all_bids.size()): + bid = self.all_bids.get(index) bid_utility = float(self.profile.getUtility(bid)) self.bids_with_utilities.append((bid, bid_utility)) - - # Sort bids by utility from high to low + + # Sort by utility (highest first) self.bids_with_utilities.sort(key=lambda tup: tup[1], reverse=True) - - # Expand the range of acceptable bids over time (starts at 1% and increases gradually up to 20%) - increasing_percentage = min(0.01 + progress * 0.19, 0.2) - expanded_top_bids = max(5, floor(num_of_bids * increasing_percentage)) - - # Dynamically decrease threshold: as time progresses, the threshold lowers, making concessions more likely - #dynamic_threshold = max(0.5, 1 - progress * 0.5) - - # If progress exceeds the threshold, offer the best bid from the opponent - #if progress > dynamic_threshold and self.best_bid is not None: - # return self.best_bid - - # Randomly select a bid from the expanded top bids range - next_bid = randint(0, expanded_top_bids - 1) - self.find_bid_result = self.bids_with_utilities[next_bid][0] - - # RAFA: we're late in the negotiation, consider returning the best bid we received - progress = self.progress.get(time() * 1000) - if progress > 0.95 and self.opponent_model is not None and self.opponent_model.best_bid_for_us is not None: - return self.opponent_model.best_bid_for_us - + + # Gahboninho approach: Find all bids that meet or exceed target utility + eligible_bids = [] + for bid, utility in self.bids_with_utilities: + if utility >= target_utility: + eligible_bids.append((bid, utility)) + # Limit to a reasonable number for efficiency + if len(eligible_bids) >= 100: + break + + # If we have eligible bids, randomly select one + if eligible_bids: + return choice(eligible_bids)[0] + + # If no eligible bids found, fallback to a utility-based approach with expanding range + top_percentage = max(0.01, min(0.2, self.top_bids_percentage + progress * 0.19)) + expanded_top_bids = max(5, floor(self.all_bids.size() * top_percentage)) + next_bid = randint(0, min(expanded_top_bids, len(self.bids_with_utilities)) - 1) + return self.bids_with_utilities[next_bid][0] \ No newline at end of file diff --git a/run.py b/run.py index c3acebe7..adeed6a6 100644 --- a/run.py +++ b/run.py @@ -21,14 +21,14 @@ # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", # "parameters": {"storage_dir": "agent_storage/BoulwareAgent"}, #}, - #{ - # "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", - # "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, - #}, { - "class": "agents.template_agent.template_agent.TemplateAgent", - "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, }, + # { + # "class": "agents.template_agent.template_agent.TemplateAgent", + # "parameters": {"storage_dir": "agent_storage/TemplateAgent"}, + # }, { "class": "agents.group16_agent.group16_agent.Group16Agent", "parameters": {"storage_dir": "agent_storage/Group16Agent"}, From e220296d6b78828951983fb42e1a2b96fdd8309c Mon Sep 17 00:00:00 2001 From: rafael-alani Date: Sat, 5 Apr 2025 12:05:19 +0200 Subject: [PATCH 17/17] variance seems good --- run_tournament.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/run_tournament.py b/run_tournament.py index 3e106f04..f30ec794 100644 --- a/run_tournament.py +++ b/run_tournament.py @@ -21,6 +21,10 @@ "class": "agents.group16_agent.group16_agent.Group16Agent", "parameters": {"storage_dir": "agent_storage/Group16Agent"}, }, + { + "class": "agents.ANL2022.dreamteam109_agent.dreamteam109_agent.DreamTeam109Agent", + "parameters": {"storage_dir": "agent_storage/ANL2022/DreamTeam109Agent"}, + }, # { # "class": "agents.boulware_agent.boulware_agent.BoulwareAgent", # },