fischer-agentkit/src/agentkit/marketplace/auction.py

231 lines
8.0 KiB
Python

"""AuctionHouse - 拍卖机制,基于竞价选择 Agent"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from agentkit.marketplace.wealth import WealthTracker
@dataclass
class Bid:
"""Agent 竞价信息"""
agent_name: str
architecture: str # "react", "rewoo", "plan_exec", "reflexion", "direct"
estimated_steps: int
estimated_cost: float # estimated token cost
confidence: float # 0.0-1.0 confidence in completing the task
payment_offer: float # how much the agent "charges"
capabilities: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.estimated_cost < 0:
raise ValueError(f"estimated_cost must be non-negative, got {self.estimated_cost}")
if not (0.0 <= self.confidence <= 1.0):
raise ValueError(f"confidence must be between 0.0 and 1.0, got {self.confidence}")
if self.payment_offer < 0:
raise ValueError(f"payment_offer must be non-negative, got {self.payment_offer}")
@dataclass
class AuctionResult:
"""拍卖结果"""
winner: Bid | None
all_bids: list[Bid]
selection_reason: str
total_bidders: int
class AuctionHouse:
"""Auction-based agent selection mechanism.
Default disabled. Enable via marketplace.auction_enabled: true in config.
When enabled, Layer 2 routing uses auction instead of capability matching.
"""
def __init__(self, wealth_tracker: WealthTracker | None = None) -> None:
self._wealth_tracker = wealth_tracker or WealthTracker()
async def run_auction(self, task_description: str, bidders: list[Bid]) -> AuctionResult:
"""Run auction among bidders, select winner.
Scoring formula:
score = (confidence / max(estimated_cost, 0.001)) * wealth_factor
wealth_factor = 1.0 + (wealth / 1000.0) # wealth bonus, diminishing returns
"""
if not bidders:
return AuctionResult(
winner=None,
all_bids=[],
selection_reason="No bidders participated",
total_bidders=0,
)
# Filter out bankrupt agents
eligible = [
b for b in bidders
if not self._wealth_tracker.is_bankrupt(b.agent_name)
]
if not eligible:
return AuctionResult(
winner=None,
all_bids=bidders,
selection_reason="All bidders are bankrupt",
total_bidders=len(bidders),
)
# Score each bid
scored: list[tuple[Bid, float]] = []
for bid in eligible:
score = self.score_bid(bid)
scored.append((bid, score))
# Select highest score
scored.sort(key=lambda x: x[1], reverse=True)
winner, winner_score = scored[0]
return AuctionResult(
winner=winner,
all_bids=bidders,
selection_reason=(
f"Agent '{winner.agent_name}' won with score {winner_score:.4f} "
f"(confidence={winner.confidence}, cost={winner.estimated_cost}, "
f"wealth_factor={self._wealth_tracker.get_wealth_factor(winner.agent_name):.4f})"
),
total_bidders=len(bidders),
)
def score_bid(self, bid: Bid) -> float:
"""Calculate bid score without running full auction"""
wealth_factor = self._wealth_tracker.get_wealth_factor(bid.agent_name)
score = (bid.confidence / max(bid.estimated_cost, 0.001)) * wealth_factor
return score
def filter_by_capabilities(
self, bidders: list[Bid], required_capabilities: list[str]
) -> list[Bid]:
"""Filter bidders whose capabilities include ALL required capabilities.
Args:
bidders: List of Bid objects to filter.
required_capabilities: Capabilities that a bidder must ALL possess.
Returns:
List of Bids whose capabilities list includes every required capability.
"""
if not required_capabilities:
return bidders
required_set = {cap.lower() for cap in required_capabilities}
return [
b for b in bidders
if required_set.issubset({cap.lower() for cap in b.capabilities})
]
async def run_vickrey_auction(
self,
task_description: str,
bidders: list[Bid],
required_capabilities: list[str] | None = None,
) -> AuctionResult:
"""Run a Vickrey (second-price sealed-bid) auction.
In a Vickrey auction each bidder submits a sealed bid (their
estimated_cost). The lowest estimated_cost wins, but the winner
*pays* the second-lowest estimated_cost rather than their own.
This is incentive-compatible: agents' dominant strategy is to bid
truthfully.
Steps:
1. Filter by required_capabilities (if provided).
2. Filter out bankrupt agents.
3. If only 1 eligible bidder → wins, pays 0.
4. If 2+ eligible bidders → lowest cost wins, pays second-lowest.
5. Update WealthTracker: winner earns (payment - cost_estimate).
6. Return AuctionResult with selection_reason.
Args:
task_description: Description of the task being auctioned.
bidders: List of Bid objects.
required_capabilities: Optional list of capabilities that bidders
must possess to be eligible.
Returns:
AuctionResult with the winner and Vickrey outcome details.
"""
# 1. Capability filtering
eligible = (
self.filter_by_capabilities(bidders, required_capabilities)
if required_capabilities
else list(bidders)
)
# 2. Filter out bankrupt agents
eligible = [
b for b in eligible
if not self._wealth_tracker.is_bankrupt(b.agent_name)
]
# No bidders at all
if not bidders:
return AuctionResult(
winner=None,
all_bids=bidders,
selection_reason="No bidders participated",
total_bidders=0,
)
# All eligible bidders filtered out (bankrupt or no capabilities)
if not eligible:
return AuctionResult(
winner=None,
all_bids=bidders,
selection_reason="No eligible bidders (bankrupt or missing capabilities)",
total_bidders=len(bidders),
)
# 3. Sort by estimated_cost ascending (lowest cost = best bid)
# Apply minimum cost floor to prevent zero-cost bid manipulation
MIN_COST = 0.001
for b in eligible:
b.estimated_cost = max(b.estimated_cost, MIN_COST)
eligible = sorted(eligible, key=lambda b: b.estimated_cost)
winner = eligible[0]
# 4. Determine payment (second-price rule)
if len(eligible) == 1:
# Single bidder: payment equals their own cost (no profit, no loss)
payment = winner.estimated_cost
else:
payment = eligible[1].estimated_cost
# 5. Update WealthTracker
profit = payment - winner.estimated_cost
self._wealth_tracker.reward(winner.agent_name, profit)
# 6. Build selection_reason
if len(eligible) == 1:
reason = (
f"Vickrey auction: Agent '{winner.agent_name}' won as sole eligible bidder "
f"(cost={winner.estimated_cost}, payment={payment:.4f}, profit=0)"
)
else:
second = eligible[1]
reason = (
f"Vickrey auction: Agent '{winner.agent_name}' won with lowest cost "
f"({winner.estimated_cost}), pays second-lowest cost ({payment}) from "
f"'{second.agent_name}'; profit={profit:.4f}"
)
return AuctionResult(
winner=winner,
all_bids=bidders,
selection_reason=reason,
total_bidders=len(bidders),
)