Customization¶
negmas-llm is designed to be highly customizable. You can override various methods to control how information is presented to the LLM and how responses are parsed.
Customization Points¶
The LLMNegotiator class provides several methods you can override:
| Method | Purpose |
|---|---|
format_outcome_space() |
Format the negotiation outcome space |
format_own_ufun() |
Format your utility function |
format_partner_ufun() |
Format the partner's utility function |
format_nmi_info() |
Format negotiation mechanism metadata |
format_state() |
Format the complete negotiation state |
format_response_instructions() |
Define the expected JSON response format |
build_system_prompt() |
Build the complete system prompt |
build_user_message() |
Build the user message for each round |
_parse_llm_response() |
Parse LLM responses into actions |
Example: Custom System Prompt¶
The simplest customization is providing a custom system prompt:
from negmas_llm import OpenAINegotiator
negotiator = OpenAINegotiator(
model="gpt-4o",
system_prompt="""You are a tough negotiator who never accepts the first offer.
Always counter with a better deal for yourself.
Respond in JSON format with response_type, outcome, and text fields.""",
ufun=my_utility_function,
)
Example: Custom Formatting Methods¶
For more control, create a subclass and override specific methods:
from negmas_llm import OpenAINegotiator
from negmas.sao import SAOState
class VerboseNegotiator(OpenAINegotiator):
"""A negotiator that provides more detailed state information."""
def format_state(self, state: SAOState, offer=None) -> str:
"""Add extra context to state formatting."""
base_state = super().format_state(state, offer)
# Add custom analysis
extra_info = []
extra_info.append("## Strategic Analysis")
extra_info.append("")
# Time pressure indicator
if state.relative_time > 0.8:
extra_info.append("**WARNING**: Running low on time!")
elif state.relative_time > 0.5:
extra_info.append("**Note**: Past the halfway point.")
# Utility analysis if we have an offer
if offer is not None and self.ufun is not None:
utility = self.ufun(offer)
reserved = self.reserved_value
if utility < reserved:
extra_info.append(
f"**Alert**: This offer ({utility:.3f}) is BELOW "
f"your reservation value ({reserved:.3f})!"
)
elif utility > reserved * 1.5:
extra_info.append(
f"**Opportunity**: This offer provides good value "
f"({utility:.3f} vs reserved {reserved:.3f})."
)
extra_info.append("")
return base_state + "\n".join(extra_info)
Example: Custom Response Format¶
Change the expected response format for your LLM:
import json
import re
from negmas.sao import ResponseType, SAOState
from negmas.outcomes import Outcome
from typing import Any
from negmas_llm import AnthropicNegotiator
class XMLResponseNegotiator(AnthropicNegotiator):
"""A negotiator that expects XML-formatted responses."""
def format_response_instructions(self) -> str:
return """\
## Response Format
Respond in XML format:
<response>
<action>accept|reject|end</action>
<offer>value1, value2, ...</offer>
<reasoning>your explanation</reasoning>
</response>
"""
def _parse_llm_response(
self, response_text: str, state: SAOState
) -> tuple[ResponseType, Outcome | None, str | None, dict[str, Any] | None]:
"""Parse XML response into negotiation actions."""
# Extract action
action_match = re.search(r"<action>(\w+)</action>", response_text)
action = action_match.group(1).lower() if action_match else "reject"
response_type_map = {
"accept": ResponseType.ACCEPT_OFFER,
"reject": ResponseType.REJECT_OFFER,
"end": ResponseType.END_NEGOTIATION,
}
response_type = response_type_map.get(action, ResponseType.REJECT_OFFER)
# Extract offer
outcome = None
offer_match = re.search(r"<offer>([^<]+)</offer>", response_text)
if offer_match:
values = [v.strip() for v in offer_match.group(1).split(",")]
# Convert to appropriate types based on outcome space
outcome = tuple(self._convert_values(values))
# Extract reasoning
reasoning = None
reasoning_match = re.search(r"<reasoning>([^<]+)</reasoning>", response_text)
if reasoning_match:
reasoning = reasoning_match.group(1).strip()
return response_type, outcome, reasoning, None
def _convert_values(self, values: list[str]) -> list[Any]:
"""Convert string values to appropriate types."""
converted = []
for v in values:
try:
# Try integer first
converted.append(int(v))
except ValueError:
try:
# Then float
converted.append(float(v))
except ValueError:
# Keep as string
converted.append(v)
return converted
Example: Domain-Specific Negotiator¶
Create a negotiator specialized for a specific domain:
from negmas_llm import GeminiNegotiator
from negmas.sao import SAOState
class RealEstateNegotiator(GeminiNegotiator):
"""A negotiator specialized for real estate deals."""
def __init__(self, property_type: str = "residential", **kwargs):
super().__init__(**kwargs)
self.property_type = property_type
def build_system_prompt(self, state: SAOState) -> str:
base_prompt = super().build_system_prompt(state)
domain_context = f"""
## Domain Context: Real Estate ({self.property_type.title()})
You are negotiating a real estate transaction. Keep in mind:
- Market conditions and comparable sales
- Inspection contingencies and closing timelines
- Financing considerations
- Typical negotiation patterns in real estate
Be professional and build rapport while advocating for your interests.
"""
return base_prompt + domain_context
def format_own_ufun(self, state: SAOState) -> str:
base_ufun = super().format_own_ufun(state)
# Add domain-specific interpretation
interpretation = """
### Interpretation for Real Estate
- **Price**: Lower is better for buyers, higher for sellers
- **Closing Date**: Flexibility may be valuable to either party
- **Contingencies**: More contingencies favor buyers, fewer favor sellers
"""
return base_ufun + interpretation
Example: Negotiator with Memory¶
Create a negotiator that maintains strategic memory across rounds:
from negmas_llm import OpenAINegotiator
from negmas.sao import SAOState
from negmas.outcomes import Outcome
class StrategicMemoryNegotiator(OpenAINegotiator):
"""A negotiator that tracks patterns and adjusts strategy."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.offer_history: list[tuple[str, Outcome | None, float | None]] = []
self.concession_pattern: list[float] = []
def build_user_message(
self, state: SAOState, offer: Outcome | None, source: str | None
) -> str:
base_message = super().build_user_message(state, offer, source)
# Add strategic memory context
memory_context = ["## Strategic Memory"]
memory_context.append("")
if self.offer_history:
memory_context.append("### Offer History")
for i, (who, hist_offer, utility) in enumerate(self.offer_history[-5:]):
util_str = f" (utility: {utility:.3f})" if utility else ""
memory_context.append(f"{i+1}. {who}: {hist_offer}{util_str}")
memory_context.append("")
if len(self.concession_pattern) >= 2:
trend = self.concession_pattern[-1] - self.concession_pattern[-2]
if trend > 0:
memory_context.append(
"**Trend**: Opponent is making concessions (offers improving)"
)
elif trend < 0:
memory_context.append(
"**Trend**: Opponent is hardening (offers getting worse)"
)
else:
memory_context.append("**Trend**: Opponent is holding steady")
memory_context.append("")
# Track current offer
if offer is not None:
utility = self.ufun(offer) if self.ufun else None
self.offer_history.append((source or "opponent", offer, utility))
if utility is not None:
self.concession_pattern.append(utility)
return base_message + "\n".join(memory_context)
def on_negotiation_start(self, state) -> None:
super().on_negotiation_start(state)
self.offer_history = []
self.concession_pattern = []
Tips for Customization¶
-
Start Simple: Begin with a custom system prompt before creating subclasses.
-
Test Incrementally: Override one method at a time and verify behavior.
-
Preserve Base Behavior: Call
super()methods when you want to extend rather than replace functionality. -
Consider Token Limits: Be mindful of context length when adding information to prompts.
-
Handle Errors Gracefully: The
_parse_llm_responsemethod should always return a valid response, even if parsing fails. -
Use Type Hints: Maintain type hints in your subclasses for better IDE support and error catching.