Files
agents-marketplace/docs/supplier/api.py
jelisaveta-talbot cbab81efbd M1-F: end-to-end LLM marketplace lifecycle on Vector testnet
Bonded-escrow marketplace for prompt → response between buyers and
local-LLM suppliers, settling on Vector (UTxO) via Aiken Plutus V3
validators. Lifecycle PostEscrow → Claim → Submit → Accept proven on
Vector testnet 2026-05-06; AcceptEscrow tx 125e3cfe…7dc100d distributes
payment + bonds per the validator's distribution rule.

Workspace layout (pnpm + TypeScript):
  contracts/marketplace/      Aiken validators (advert + escrow) + plutus.json
  packages/shared/            Tx-builders (lucid-evolution), CBOR codecs,
                              chain providers (mock / read-only / live Ogmios)
  supplier/                   OpenAI-compatible HTTP node (Ollama by default;
                              vLLM / llama.cpp / OpenRouter / OpenClaw all
                              swap-in via OLLAMA_URL); 202+poll chat handler
  buyer/                      Express + React SPA; server-side runs the full
                              chain lifecycle via /v1/submit-prompt and
                              /v1/accept; SPA never holds the buyer key
  indexer/                    Chain-follower (extends apex-dashboard pattern),
                              SSE event stream + REST queries
  indexer-ui/                 Operator dashboard (events + suppliers + escrow
                              lookup)
  tests/                      1098 vitest unit + integration tests (10 skipped
                              under M1-F-D-defer for UPLC test plumbing)
  deploy/testnet/             Per-service docker-compose for the Hetzner stack
  docs/EXECUTIVE_SUMMARY.*    Marp deck (PDF + PPTX) — 11 slides, mermaid
                              diagrams pre-rendered for portable export
  docs/CLOUDFLARE_WAF_BYPASS.md  CF Skip-rule runbook for /v1/* paths
  docs/ARCHITECTURE.md        canonical spec

Live demo (testnet, 2026-05-06):
  https://mp-buyers.vector.testnet.apexfusion.org      (buyer SPA)
  https://mp-suppliers.vector.testnet.apexfusion.org   (supplier API)
  https://mp-indexer.vector.testnet.apexfusion.org     (indexer + UI)
2026-05-06 13:27:48 +02:00

187 lines
5.2 KiB
Python

"""
FastAPI router -- HTTP endpoints for the supplier service.
Includes task processing, health checks, and dashboard API.
"""
import logging
import time
from fastapi import APIRouter, HTTPException
from lib.protocol import TaskRequest, TaskResponse, SupplierInfo
from config import get_config
from wallet import get_supplier_agent
from llm import LLMProvider
from task_runner import run_task
import storage
logger = logging.getLogger(__name__)
router = APIRouter()
# Lazy-initialized singletons
_agent = None
_llm_provider = None
_config = None
def _get_agent():
global _agent
if _agent is None:
_agent = get_supplier_agent()
return _agent
def _get_llm_provider():
global _llm_provider
if _llm_provider is None:
config = _get_config()
_llm_provider = LLMProvider(
openrouter_api_key=config.openrouter_api_key,
)
return _llm_provider
def _get_config():
global _config
if _config is None:
_config = get_config()
return _config
@router.post("/task", response_model=TaskResponse)
async def handle_task(task_request: TaskRequest):
"""Receive a task request from a buyer, fulfill it, and settle on-chain."""
config = _get_config()
agent = _get_agent()
llm = _get_llm_provider()
logger.info(f"Received task {task_request.task_id} -- escrow_tx={task_request.escrow_tx}")
start_time = time.time()
response = await run_task(task_request, agent, llm, config)
latency_ms = int((time.time() - start_time) * 1000)
# Save to local storage regardless of success/failure
task_record = {
"task_id": task_request.task_id,
"prompt": task_request.prompt,
"buyer_address": task_request.buyer_address,
"payment_dfm": task_request.payment_dfm,
"escrow_tx": task_request.escrow_tx,
"task_hash": task_request.task_hash,
"model_preference": task_request.model_preference,
"status": response.status,
"response": response.response,
"result_hash": response.result_hash,
"model_used": response.model_used,
"input_tokens": response.input_tokens,
"output_tokens": response.output_tokens,
"claim_tx": response.claim_tx,
"submit_tx": response.submit_tx,
"error": response.error,
"latency_ms": latency_ms,
"completed_at": time.time(),
}
storage.save_task(task_record)
if response.status == "failed":
logger.error(f"Task {task_request.task_id} failed: {response.error}")
raise HTTPException(status_code=500, detail=response.error)
return response
@router.get("/health")
async def health():
"""Health check endpoint."""
config = _get_config()
try:
agent = _get_agent()
address = str(agent.address)
except Exception:
address = "not initialized"
return {
"status": "ok",
"name": config.name,
"address": address,
}
@router.get("/info", response_model=SupplierInfo)
async def info():
"""Return supplier metadata."""
config = _get_config()
try:
agent = _get_agent()
address = str(agent.address)
except Exception:
address = "not initialized"
return SupplierInfo(
name=config.name,
address=address,
endpoint=f"http://localhost:{config.port}",
models=[
"nvidia/nemotron-3-super-120b-a12b",
"nvidia/nemotron-3-nano-30b-a3b",
"anthropic/claude-sonnet-4-6",
"anthropic/claude-haiku-4-5-20251001",
],
)
@router.get("/api/dashboard")
async def dashboard():
"""Return dashboard data: wallet info, stats, and task history."""
config = _get_config()
# Wallet info
wallet_info = {"address": "not initialized", "ada": "0", "lovelace": 0}
try:
agent = _get_agent()
balance = await agent.get_balance()
wallet_info = {
"address": balance.address,
"ada": balance.ada,
"lovelace": balance.lovelace,
}
except Exception as e:
logger.warning(f"Could not fetch balance: {e}")
# Task history
tasks = storage.load_tasks()
# Compute stats
completed_tasks = [t for t in tasks if t.get("status") == "completed"]
total_earnings_dfm = sum(t.get("payment_dfm", 0) for t in completed_tasks)
tasks_completed = len(completed_tasks)
total_tokens = sum(
t.get("input_tokens", 0) + t.get("output_tokens", 0)
for t in completed_tasks
)
avg_latency_ms = 0
if completed_tasks:
latencies = [t.get("latency_ms", 0) for t in completed_tasks if t.get("latency_ms")]
avg_latency_ms = int(sum(latencies) / len(latencies)) if latencies else 0
return {
"wallet": wallet_info,
"stats": {
"total_earnings_dfm": total_earnings_dfm,
"tasks_completed": tasks_completed,
"avg_latency_ms": avg_latency_ms,
"total_tokens": total_tokens,
},
"tasks": list(reversed(tasks)), # Most recent first
"name": config.name,
}
@router.get("/api/tasks")
async def api_tasks():
"""Return task history."""
tasks = storage.load_tasks()
return {"tasks": list(reversed(tasks)), "total": len(tasks)}