Back to projects

~/projects

Contract-Net Auction Market

multi-agentcontract-netauctionlangchainollamastreamlit

Repo: mohd-vasim/ai-engineering → mas-bidder-agents

When multiple specialized AI agents share a task queue, matching the right agent to the right job is a coordination problem. An agent that's great at code generation will give you terrible poetry — but it might not know that. A contract-net protocol solves this by running an auction: announce the task, collect bids, evaluate them against cost and capability, and award the work.

This project builds the full auction pipeline with LangChain and Ollama, using a local LLM (qwen3.5:4b-mlx) for every agent — no cloud dependency, no API costs.

The Pattern: Contract-Net Protocol

The contract-net protocol is a decentralized task allocation pattern from distributed AI systems. A Solicitor announces a task to all agents, each agent evaluates whether it can handle the work and submits a bid, and the Solicitor scores the bids and awards a contract to the winner. The winner executes, and its performance feeds back into future rounds through a reputation system.

FlowFlow

The Components

ComponentResponsibility
SolicitorRuns the auction end-to-end: announces the task, collects bids, scores them, awards the contract, records the outcome.
OllamaBidderAgentWraps an LLM into the auction protocol. Its evaluate method decides whether to bid; its execute method produces the deliverable.
ReputationRegistryTracks every agent's success/failure record and computes a penalty multiplier that adjusts confidence before scoring. Also persists to disk.
UtilityWeights & utility_scoreConfigurable linear scoring function: α × confidence − β × cost − γ × (ETA / 60). Different weight sets capture different priorities — time-sensitive vs. budget-constrained.
Prompts (EVAL_SYSTEM, EXEC_SYSTEM)Two prompt templates. The evaluation prompt enforces domain filtering with explicit can_handle: false rules. The execution prompt is simpler — just produce the deliverable.

The key design choice: each agent holds its own domain list, not a shared one. This means the same underlying LLM can power multiple agents with different specializations. The solicitor never tells an agent what it can do — the agent's own system prompt and domain list enforce that boundary.

Why Domain Filtering Matters

The first iteration of this system revealed a problem: agents would bid on tasks outside their specialization. A poet-agent would claim it could debug JavaScript with high confidence. The fix was to make the evaluation prompt rigid — explicit language forcing can_handle: false when the task type doesn't match the domain list.

text
# Prompt excerpt
RULE: You must respond with can_handle=false if the task type is NOT in your domain list.
Do NOT assume you can do things outside your specialization.

With this in place, a test of six scenarios (three agents × three task types) passes 5/6 — the one deliberate failure tests whether a coder rejects an ML task, which it currently doesn't. The prompt could still be stricter.

Why a Utility Function

Not all bids are equal. A high-confidence bid with a 4-hour ETA and 15. The utility function lets you encode your priorities:

  • Default weights (α=1, β=1, γ=1): balanced between confidence, cost, and time.
  • Time-sensitive (α=1, β=0.5, γ=5): heavily penalizes slow delivery — a fast-but-expensive agent wins.
  • Budget-conscious (α=1, β=5, γ=0.5): heavily penalizes cost — the cheapest agent wins.

How Reputation Works

A one-shot auction ignores history. The ReputationRegistry turns it into an iterated game by computing a penalty multiplier:

python
penalty = min(1.0, (failures / total) * 2.0)
adjusted_confidence = confidence * (1.0 - penalty)

One failure in three attempts gives a 0.67 penalty, cutting advertised confidence from 0.9 to 0.3. A second failure pushes the penalty to 1.0, zeroing out the agent's effective confidence. The registry persists to reputation.json so the system remembers across sessions.

This design has a deliberate blind spot: a newcomer with no track record gets penalty = 0.0 (no penalty). The system trusts new agents fully on their first task. This is fine for a sandbox but worth hardening for production — a default penalty for unknown agents or a probationary period.

The Auction Flow in Detail

A single auction cycle through the Solicitor.run_auction method:

  1. Announce: The task is described to every agent with its type and constraints.
  2. Bid: Each agent calls the LLM with the evaluation prompt and returns can_handle, confidence, cost_dollars, eta_minutes, reasoning.
  3. Score: The solicitor adjusts confidence with reputation penalties, then scores each bid with utility_score.
  4. Award: The highest-score bid wins. The solicitor calls winner.execute(task).
  5. Record: The result (success/failure) updates the reputation registry. The file is persisted to disk.

If no agent can handle the task, a NoBidsException is raised — prefer a clean error over awarding the contract to the least-unqualified bidder.

Tech Stack

  • LangChain + OllamaChatOllama with format="json" for structured agent output. Every agent call is local, free, and private.
  • Streamlit — interactive dashboard where you configure agents, pick or define tasks, tune utility weights, and watch each auction phase in real time.
  • pandas — bid comparison table (agent × confidence × cost × ETA × utility) rendered as a DataFrame.
  • uv — Python package manager; single command to sync and run.

How to Run

bash
git clone https://github.com/mohd-vasim/ai-engineering.git
cd ai-engineering/mas-bidder-agents
uv sync

# Make sure Ollama is running with qwen3.5:4b-mlx (or swap the model in llm.py)
uv run streamlit run app.py

In the dashboard you can:

  • Add or remove agents with custom domain lists
  • Pick from preset tasks or define your own
  • Tune the utility weights with sliders (α, β, γ)
  • Run the auction and watch each phase unfold — evaluation, scoring, award, execution
  • Inspect the reputation history and bid comparison table

A CLI demo is also available via uv run python -m mas_bidder_agents.demo for a no-GUI run.

What's in the Repo

  • src/mas_bidder_agents/ — the core library:
    • models.pyTask, Bid, ContractResult, UtilityWeights dataclasses.
    • llm.pyget_llm() factory and clean_json() defensive JSON parser.
    • prompts.pyEVAL_SYSTEM, EVAL_HUMAN, EXEC_SYSTEM, EXEC_HUMAN templates.
    • bidder.pyOllamaBidderAgent with evaluate() and execute().
    • utility.pyutility_score() function.
    • reputation.pyReputationRegistry with disk persistence.
    • solicitor.pySolicitor orchestrating the full auction.
    • demo.py — CLI demo with preset agents and tasks.
  • app.py — Streamlit interactive dashboard.
  • tests/ — pytest suite.
  • notebooks/ — Jupyter notebook with the iterative prototyping that led to the final design.
  • docs/ — FDS (Functional Design Specification) documents.

Limitations & What's Next

The linear utility function works for three agents but starts to show cracks at scale: two agents with identical scores, agents padding confidence to win bids they can't complete, or a newcomer with no reputation competing against veteran specialists.

The prompt-based domain filter works for broad categories (code, poetry, data_science) but can't prevent an ML engineer from claiming it can do "data science" on a medical diagnosis task that happens to use ML. Finer-grained domain hierarchies would help.

Possible next iterations:

  • Tie-breaking — secondary criteria for identical utility scores (past volume, delivery variance, domain overlap).
  • Probationary reputation — a penalty for newcomers to prevent confidence-inflation attacks before reputation catches up.
  • Domain hierarchiesdata_science → ml → computer_vision so an agent that specializes in computer_vision can reject a generic data_science task.
  • Multi-round auctions — sealed-bid vs. open-bid variants, or iterative price discovery.
  • Execution verification — a separate judge agent that validates the winner's output before recording success.