Source code for negmas.gb.negotiators.base

from __future__ import annotations
from abc import abstractmethod
from collections import defaultdict
from typing import TYPE_CHECKING, Generic, TypeVar

from negmas.outcomes.common import ExtendedOutcome
from negmas.preferences.base_ufun import BaseUtilityFunction
from negmas.preferences.preferences import Preferences

from ...negotiators import Controller, Negotiator
from ...outcomes import Outcome
from ..common import GBNMI, GBState, ResponseType, ThreadState
from ...common import NegotiatorMechanismInterface, MechanismState

if TYPE_CHECKING:
    from negmas.sao.common import SAOResponse, SAOState
    from negmas.situated import Agent

__all__ = ["GBNegotiator"]

TNMI = TypeVar("TNMI", bound=NegotiatorMechanismInterface)
TState = TypeVar("TState", bound=MechanismState)


def none_return():
    return None


[docs] class GBNegotiator(Negotiator[GBNMI, GBState], Generic[TNMI, TState]): """ Base class for all GB negotiators. Args: name: Negotiator name parent: Parent controller if any preferences: The preferences of the negotiator ufun: The utility function of the negotiator (overrides preferences if given) owner: The `Agent` that owns the negotiator. Remarks: - The only method that **must** be implemented by any GBNegotiator is `propose`. - The default `respond` method, accepts offers with a utility value no less than whatever `propose` returns with the same mechanism state. """ def __init__( self, preferences: Preferences | None = None, ufun: BaseUtilityFunction | None = None, name: str | None = None, parent: Controller | None = None, owner: Agent | None = None, id: str | None = None, type_name: str | None = None, **kwargs, ): super().__init__( name=name, preferences=preferences, ufun=ufun, parent=parent, owner=owner, id=id, type_name=type_name, **kwargs, ) self.add_capabilities({"respond": True, "propose": True, "max-proposals": 1}) self.__end_negotiation = False self.__received_offer: dict[str | None, Outcome | None] = defaultdict( none_return )
[docs] @abstractmethod def propose( self, state: GBState, dest: str | None = None ) -> Outcome | ExtendedOutcome | None: """Propose an offer or None to refuse. Args: state: `GBState` giving current state of the negotiation. Returns: The outcome being proposed or None to refuse to propose Remarks: - This function guarantees that no agents can propose something with a utility value """
[docs] @abstractmethod def respond(self, state: GBState, source: str | None) -> ResponseType: """Called to respond to an offer. This is the method that should be overriden to provide an acceptance strategy. Args: state: a `GBState` giving current state of the negotiation. Returns: ResponseType: The response to the offer Remarks: - The default implementation never ends the negotiation - The default implementation asks the negotiator to `propose`() and accepts the `offer` if its utility was at least as good as the offer that it would have proposed (and above the reserved value). - Current offer is accessible through state.threads[source].current_offer as long as source != None otherwise it is None """
[docs] def on_partner_proposal( self, state: GBState, partner_id: str, offer: Outcome ) -> None: """ A callback called by the mechanism when a partner proposes something Args: state: `GBState` giving the state of the negotiation when the offer was porposed. partner_id: The ID of the agent who proposed offer: The proposal. Remarks: - Will only be called if `enable_callbacks` is set for the mechanism """
[docs] def on_partner_response( self, state: GBState, partner_id: str, outcome: Outcome, response: ResponseType ) -> None: """ A callback called by the mechanism when a partner responds to some offer Args: state: `GBState` giving the state of the negotiation when the partner responded. partner_id: The ID of the agent who responded outcome: The proposal being responded to. response: The response Remarks: - Will only be called if `enable_callbacks` is set for the mechanism """
[docs] def on_partner_ended(self, partner: str): """ Called when a partner ends the negotiation. Note that the negotiator owning this component may never receive this offer. This is only received if the mechanism is sending notifications on every offer. """
# compatibility with SAOMechanism
[docs] def __call__(self, state: SAOState, dest: str | None = None) -> SAOResponse: """ Called by the mechanism to counter the offer. It just calls `respond_` and `propose_` as needed. Args: state: `SAOState` giving current state of the negotiation. dest: The partner to respond to with a counter offer (for AOP, SAOP, MAOP this can safely be ignored). Returns: Tuple[ResponseType, Outcome]: The response to the given offer with a counter offer if the response is REJECT """ from negmas.sao.common import SAOResponse offer = state.current_offer if self.__end_negotiation: return SAOResponse(ResponseType.END_NEGOTIATION, None) if self.preferences is not None: changes = self.preferences.changes() if changes: self.on_preferences_changed(changes) if offer is None: proposal = self.propose_(state=state) if isinstance(proposal, ExtendedOutcome): return SAOResponse( ResponseType.REJECT_OFFER, proposal.outcome, proposal.data ) return SAOResponse(ResponseType.REJECT_OFFER, proposal) response = self.respond_( state=state, source=state.current_proposer if state.current_proposer else "" ) if response == ResponseType.ACCEPT_OFFER: return SAOResponse(response, offer) if response != ResponseType.REJECT_OFFER: return SAOResponse(response, None) proposal = self.propose_(state=state, dest=dest) if isinstance(proposal, ExtendedOutcome): return SAOResponse( ResponseType.REJECT_OFFER, proposal.outcome, proposal.data ) return SAOResponse(response, proposal)
[docs] def propose_( self, state: SAOState, dest: str | None = None ) -> Outcome | ExtendedOutcome | None: if not self._capabilities["propose"] or self.__end_negotiation: return None return self.propose(state=self._gb_state_from_sao_state(state), dest=dest)
[docs] def respond_(self, state: SAOState, source: str | None = None) -> ResponseType: offer = state.current_offer if self.__end_negotiation: return ResponseType.END_NEGOTIATION self.__received_offer[state.current_proposer] = offer return self.respond(state=self._gb_state_from_sao_state(state), source=source)
def _gb_state_from_sao_state(self, state: SAOState) -> GBState: if isinstance(state, GBState): return state if not self.nmi: raise ValueError("No NMI. Cannot convert SAOState to GBState") threads = { source: ThreadState( new_offer=self.__received_offer.get(state.current_proposer, None) ) for source in self.nmi.negotiator_ids } return GBState( running=state.running, waiting=state.waiting, started=state.started, step=state.step, time=state.time, relative_time=state.relative_time, broken=state.broken, timedout=state.timedout, agreement=state.agreement, results=state.results, n_negotiators=state.n_negotiators, has_error=state.has_error, error_details=state.error_details, threads=threads, ) def _sao_state_from_gb_state(self, state: GBState) -> SAOState: if isinstance(state, SAOState): return state if not self.nmi: raise ValueError("No NMI. Cannot convert SAOState to GBState") # TODO: correct all of this nonsense if state.last_thread: offerer = state.last_thread owner = self.nmi._mechanism._negotiator_map[offerer].owner aid = owner.id if owner else None current_offer = state.threads[state.last_thread].new_offer current_data = state.threads[state.last_thread].new_data current_proposer = None current_proposer_agent = None n_acceptances = len( [ (offerer, _) for _ in state.threads[state.last_thread].new_responses.values() if _ == ResponseType.ACCEPT_OFFER ] ) new_offers = [(offerer, state.threads[state.last_thread].new_offer)] new_data = [(offerer, state.threads[state.last_thread].new_data)] new_offerer_agents = [aid] last_negotiator = None else: current_offer = None current_data = None current_proposer = None current_proposer_agent = None n_acceptances = 0 new_offers = [] new_data = [] new_offerer_agents = [] last_negotiator = None return SAOState( running=state.running, waiting=state.waiting, started=state.started, step=state.step, time=state.time, relative_time=state.relative_time, broken=state.broken, timedout=state.timedout, agreement=state.agreement, results=state.results, n_negotiators=state.n_negotiators, has_error=state.has_error, error_details=state.error_details, current_offer=current_offer, current_data=current_data, current_proposer=current_proposer, current_proposer_agent=current_proposer_agent, n_acceptances=n_acceptances, new_offers=new_offers, new_data=new_data, new_offerer_agents=new_offerer_agents, last_negotiator=last_negotiator, )