Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Deploy Allerion to the VPS over SSH.
#
# This workflow is INERT until you add the repo secrets below. With no secrets
# set, the single job short-circuits on its first step and exits successfully
# (it does NOT attempt to SSH anywhere).
#
# Add these in GitHub -> Settings -> Secrets and variables -> Actions -> New
# repository secret:
#
# VPS_HOST (required) the VPS hostname or public IP
# VPS_USER (required) the SSH login user on the VPS
# VPS_SSH_KEY (required) a private SSH key authorized on the VPS (PEM)
# VPS_PORT (optional) SSH port; defaults to 22 if unset
#
# CAVEAT: this assumes the repo is already checked out at ~/allerion on the VPS
# (see deploy/DEPLOY.md step 2 — `git clone ... allerion`). The deploy step only
# fetches/pulls and rebuilds; it does not bootstrap a fresh host.

name: Deploy to VPS

on:
workflow_dispatch:
push:
# NOTE: change this to `main` once the deploy branch is merged.
branches:
- claude/nvidia-ai-models-access-mmn5n5
paths:
- "apps/**"
- "deploy/**"

jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Secrets can't be referenced in a job-level `if:`, so we gate here:
# map the secret into an env var and skip the rest of the job when empty.
# This keeps the workflow inert (green, no-op) until secrets are added.
- name: Check that deploy secrets are configured
id: gate
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
if [ -z "$VPS_HOST" ]; then
echo "VPS_HOST secret is not set — skipping deploy (this is expected until you add secrets)."
echo "configured=false" >> "$GITHUB_OUTPUT"
else
echo "configured=true" >> "$GITHUB_OUTPUT"
fi

- name: Deploy over SSH
if: steps.gate.outputs.configured == 'true'
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
# appleboy/ssh-action defaults port to 22 when this is empty.
port: ${{ secrets.VPS_PORT }}
script: |
set -e
cd ~/allerion
git fetch
git checkout claude/nvidia-ai-models-access-mmn5n5
git pull --ff-only
cd deploy
docker compose up -d --build
74 changes: 74 additions & 0 deletions apps/agent-market/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Allerion Agent Market

A functional **agent-to-agent (A2A) commerce** simulation: AI **merchant agents**
sell services (summarize, translate, enrich, copywrite) to AI **buyer agents**.
Buyers discover suppliers, **negotiate price** through alternating offers,
transact through a **settlement ledger**, and the platform takes a **commission
on every deal** — that's the revenue model.

It runs with **zero dependencies and no API key** (deterministic fulfilment).
Point it at any **OpenAI-compatible endpoint** — the Allerion gateway, OpenRouter,
NVIDIA, OpenAI — to fulfil purchased work with a real model.

## Run it

```bash
cd apps/agent-market

# Offline, deterministic — no key needed:
python run_demo.py

# Live fulfilment via a real model (example: OpenRouter):
LLM_BASE_URL=https://openrouter.ai/api \
LLM_API_KEY=sk-or-... \
LLM_MODEL=openai/gpt-4o-mini \
python run_demo.py

# Live via the Allerion gateway (PR #2):
LLM_BASE_URL=https://api.allerion.io LLM_API_KEY=sk-... LLM_MODEL=deepseek-r1 python run_demo.py
```

## Tests

```bash
pip install pytest
pytest -q
```

## How it works

```
buyer.need ─discover→ marketplace ─quotes→ negotiate (alternating offers)
│ deal price within [floor, value]
ledger.settle ──fee──> platform wallet
│ net
merchant wallet ; llm.fulfill() delivers work
```

| Module | Responsibility |
|--------|----------------|
| `protocol.py` | `Service`, `Need`, `Deal`, A2A `Message` types |
| `agents.py` | `MerchantAgent` (quoting, concession) and `BuyerAgent` (ranking, bidding) |
| `marketplace.py` | registry, capability discovery, the negotiation engine |
| `ledger.py` | wallets, settlement, platform commission, money-conservation invariant |
| `llm.py` | service fulfilment — real model or deterministic stub |
| `market.py` | the round loop + analytics (GMV, revenue, P&L) |
| `run_demo.py` | seeds an economy and prints transcript + ledger |

## Economic model
- Each service has a `list_price` (opening ask) and a `floor_price` (walk-away).
- Each buyer need has a `value` (max willingness to pay).
- A deal closes only when the bargaining range overlaps (`floor ≤ value`), at a
price in between — so margins and outcomes vary by agent strategy.
- **Money is conserved**: every settlement moves funds buyer → merchant + a
commission to the platform. `summary()["money_conserved"]` must stay `0.00`
(opening balances are funded from a mint account that nets out).

## Going further
- Swap deterministic agent policies for **LLM-driven negotiation** (have each
agent reason about its next offer via `llm.fulfill`).
- Wire settlement to the **Stripe metered billing** in `infra/ai-gateway/billing`
so agent spend becomes real invoices.
- Expose the marketplace over HTTP so external agents can register and trade.
11 changes: 11 additions & 0 deletions apps/agent-market/agentmarket/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Allerion Agent Market — agent-to-agent (A2A) commerce.

AI merchant agents sell services to AI buyer agents. Buyers discover
suppliers, negotiate price, transact through a settlement ledger, and the
platform takes a commission on every deal. The whole simulation runs with
zero external dependencies; service fulfilment optionally calls a real
OpenAI-compatible endpoint (e.g. the Allerion gateway) when configured.
"""

__all__ = ["__version__"]
__version__ = "0.1.0"
65 changes: 65 additions & 0 deletions apps/agent-market/agentmarket/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""The agents: merchants that sell services and buyers that procure them."""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, List, Optional

from .protocol import Need, Service


@dataclass
class MerchantAgent:
id: str
name: str
services: List[Service] = field(default_factory=list)
concession: float = 0.4 # how fast it moves from ask toward floor (0..1)
sales: int = 0
connector: Optional[Any] = None # gateway connector (see connector.Connector)

def offer(self, service: Service) -> "MerchantAgent":
self.services.append(service)
return self

def quote(self, service: Service) -> float:
return service.list_price

def counter(self, service: Service, current_ask: float, buyer_bid: float) -> float:
"""Concede toward the buyer, but never below the service floor."""
target = max(service.floor_price, current_ask - (current_ask - service.floor_price) * self.concession)
# If the buyer's bid already clears the floor, meet in the middle.
if buyer_bid >= service.floor_price:
return round(max(service.floor_price, min(target, (target + buyer_bid) / 2)), 2)
return round(target, 2)


@dataclass
class BuyerAgent:
id: str
name: str
needs: List[Need] = field(default_factory=list)
quality_weight: float = 0.5 # how much it trades price for quality
concession: float = 0.4
purchases: int = 0
connector: Optional[Any] = None # gateway connector (see connector.Connector)

def score(self, need: Need, service: Service) -> float:
"""Expected utility of a supplier before negotiating: value adjusted
for quality, minus the opening ask. Higher is better."""
perceived_value = need.value * (0.5 + self.quality_weight * service.quality)
return perceived_value - service.list_price

def rank(self, need: Need, services: List[Service]) -> List[Service]:
return sorted(services, key=lambda s: self.score(need, s), reverse=True)

def opening_bid(self, need: Need, service: Service) -> float:
# Lowball but never above what the need is worth.
return round(min(need.value, service.list_price * 0.6), 2)

def raise_bid(self, need: Need, current_bid: float) -> float:
return round(min(need.value, current_bid + (need.value - current_bid) * self.concession), 2)

def accepts(self, need: Need, price: float) -> bool:
return price <= need.value

def next_need(self) -> Optional[Need]:
return self.needs.pop(0) if self.needs else None
44 changes: 44 additions & 0 deletions apps/agent-market/agentmarket/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""The Allerion gateway connector — embedded into every agent.

A Connector is an agent's handle to the Allerion AI gateway (the
OpenAI-compatible endpoint from infra/ai-gateway). Each agent carries its own
Connector, so fulfilment and reasoning route through the shared gateway —
metered, rate-limited, and billable per agent. When no gateway/key is
configured the connector falls back to deterministic stubs, so a whole fleet
runs offline with zero credentials.
"""
from __future__ import annotations

from dataclasses import dataclass

from . import llm


@dataclass
class Connector:
"""Per-agent connection to the gateway."""

agent_id: str
model: str = llm.MODEL
calls: int = 0

@property
def live(self) -> bool:
return llm.live()

def status(self) -> str:
return f"live:{self.model}" if self.live else "stub"

def fulfill(self, capability: str, prompt: str) -> str:
"""Route a unit of work through the gateway on this agent's behalf."""
self.calls += 1
return llm.fulfill(capability, prompt)


def attach(agents) -> int:
"""Embed a fresh Connector into each agent. Returns the count wired."""
n = 0
for a in agents:
a.connector = Connector(agent_id=a.id)
n += 1
return n
74 changes: 74 additions & 0 deletions apps/agent-market/agentmarket/ledger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Settlement ledger: wallets, transactions, and the platform's cut.

Money is conserved: every settlement moves funds from a buyer to a merchant
and a commission to the platform (house) wallet — no money is created or
destroyed. `total_money()` is the invariant the tests assert on.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List

from .protocol import Deal


class InsufficientFunds(Exception):
pass


@dataclass
class Entry:
round: int
debit: str # who paid
credit: str # who received
amount: float
memo: str


@dataclass
class Ledger:
house_id: str = "allerion"
fee_rate: float = 0.10 # platform commission on every deal
balances: Dict[str, float] = field(default_factory=dict)
entries: List[Entry] = field(default_factory=list)

def open_account(self, agent_id: str, opening_balance: float = 0.0) -> None:
self.balances.setdefault(agent_id, 0.0)
if opening_balance:
# Opening balances are funded from a mint account so the live
# economy still conserves money after seeding.
self.balances[agent_id] += opening_balance
self.balances.setdefault("__mint__", 0.0)
self.balances["__mint__"] -= opening_balance

def balance(self, agent_id: str) -> float:
return self.balances.get(agent_id, 0.0)

def settle(self, deal: Deal) -> Deal:
"""Move `deal.price` buyer→merchant, skimming `fee_rate` to the house."""
if self.balance(deal.buyer_id) < deal.price:
raise InsufficientFunds(
f"{deal.buyer_id} has ${self.balance(deal.buyer_id):,.2f}, "
f"needs ${deal.price:,.2f}"
)
fee = round(deal.price * self.fee_rate, 2)
net = round(deal.price - fee, 2)

self.balances[deal.buyer_id] -= deal.price
self.balances.setdefault(deal.merchant_id, 0.0)
self.balances[deal.merchant_id] += net
self.balances.setdefault(self.house_id, 0.0)
self.balances[self.house_id] += fee

self.entries.append(Entry(deal.round, deal.buyer_id, deal.merchant_id, net,
f"{deal.capability} ({deal.service_id})"))
self.entries.append(Entry(deal.round, deal.buyer_id, self.house_id, fee,
f"commission {self.fee_rate:.0%}"))
deal.fee = fee
return deal

def total_money(self) -> float:
return round(sum(self.balances.values()), 2)

def platform_revenue(self) -> float:
return round(self.balance(self.house_id), 2)
58 changes: 58 additions & 0 deletions apps/agent-market/agentmarket/llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Service fulfilment.

If LLM_BASE_URL and LLM_API_KEY are set, fulfilment calls a real
OpenAI-compatible endpoint (point it at the Allerion gateway, OpenRouter,
NVIDIA, OpenAI, etc.). Otherwise it returns a deterministic stub so the whole
market runs offline with no keys.
"""
from __future__ import annotations

import os
import textwrap

BASE_URL = os.environ.get("LLM_BASE_URL", "").rstrip("/")
API_KEY = os.environ.get("LLM_API_KEY", "")
MODEL = os.environ.get("LLM_MODEL", "deepseek-r1")


def live() -> bool:
return bool(BASE_URL and API_KEY)


def _stub(capability: str, prompt: str) -> str:
body = prompt or f"(no prompt supplied for {capability})"
return textwrap.shorten(
f"[stub:{capability}] delivered work for → {body}", width=160, placeholder=" …"
)


def fulfill(capability: str, prompt: str) -> str:
"""Return the work product for one purchased service unit."""
if not live():
return _stub(capability, prompt)

# Use the standard library so live mode needs no extra dependency.
import json
import urllib.request

system = f"You are a specialist agent providing the '{capability}' service. Be concise."
payload = json.dumps({
"model": MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": prompt or capability},
],
"max_tokens": 120,
}).encode()
req = urllib.request.Request(
f"{BASE_URL}/v1/chat/completions",
data=payload,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read())
return data["choices"][0]["message"]["content"].strip()
except Exception as e: # noqa: BLE001 — never let fulfilment crash the market
return f"[fulfilment error via {MODEL}: {e}]"
Loading