Files
agents-marketplace/docs/supplier/task_runner.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

162 lines
5.6 KiB
Python

"""
Task runner — orchestrates the full supplier flow from request to on-chain settlement.
"""
import asyncio
import hashlib
import logging
import time
from pycardano import Address as PycAddress
from pycardano.hash import ScriptHash
from lib.blueprint import load_escrow_script
from lib.chain import wait_for_utxo
from lib.constants import TX_PROPAGATION_DELAY
from lib.protocol import TaskRequest, TaskResponse
from escrow_ops import verify_open_utxo, claim_escrow, submit_result
from llm import LLMProvider
from config import SupplierConfig
logger = logging.getLogger(__name__)
async def run_task(
task_request: TaskRequest,
supplier_agent,
llm_provider: LLMProvider,
config: SupplierConfig,
) -> TaskResponse:
"""Execute the full supplier workflow for a task request.
Steps:
1. Load script from blueprint
2. Verify the Open UTxO on-chain
3. Call LLM
4. Claim escrow (Open -> Claimed)
5. Wait for claim UTxO (sleep TX_PROPAGATION_DELAY, then wait_for_utxo)
6. Submit result hash (Claimed -> Submitted)
7. Return TaskResponse with all TX hashes
"""
task_id = task_request.task_id
prompt = task_request.prompt
# Compute hashes
task_hash = hashlib.sha256(f"{task_id}:{prompt}".encode()).hexdigest()
try:
# 1. Load script from blueprint
script, script_hash_str = load_escrow_script()
script_hash_val = ScriptHash(bytes.fromhex(script_hash_str))
script_address = PycAddress(script_hash_val, network=supplier_agent.context.network)
script_addr_str = str(script_address)
logger.info(f"[{task_id}] Script address: {script_addr_str}")
# Extract credentials
supplier_vkh = supplier_agent._wallet.payment_verification_key.hash().payload
buyer_addr = PycAddress.from_primitive(task_request.buyer_address)
buyer_vkh = buyer_addr.payment_part.payload
resolver_addr = PycAddress.from_primitive(config.resolver_address)
resolver_cred = resolver_addr.payment_part.payload
# 2. Verify the Open UTxO on-chain
logger.info(f"[{task_id}] Verifying Open escrow UTxO...")
utxo = await verify_open_utxo(
supplier_agent.context, script_addr_str, task_request.escrow_tx
)
# 3. Call LLM
logger.info(f"[{task_id}] Calling LLM...")
start_time = time.time()
llm_result = await llm_provider.complete(
prompt=prompt,
model_preference=[task_request.model_preference, "any"],
max_tokens=task_request.max_tokens,
temperature=task_request.temperature,
)
latency_ms = int((time.time() - start_time) * 1000)
logger.info(
f"[{task_id}] LLM response: model={llm_result.model}, "
f"tokens={llm_result.input_tokens}+{llm_result.output_tokens}, "
f"latency={latency_ms}ms"
)
response_text = llm_result.text
result_hash = hashlib.sha256(f"{task_id}{response_text}".encode()).hexdigest()
# 4. Claim escrow (Open -> Claimed)
logger.info(f"[{task_id}] Claiming escrow...")
claim_tx = await claim_escrow(
context=supplier_agent.context,
signer=supplier_agent,
utxo=utxo,
script=script,
script_address=script_address,
task_hash=task_hash,
buyer_vkh=buyer_vkh,
supplier_vkh=supplier_vkh,
payment_dfm=task_request.payment_dfm,
resolver_cred=resolver_cred,
deadline_posix_ms=task_request.deadline_posix_ms,
dispute_window_ms=task_request.dispute_window_ms,
)
logger.info(f"[{task_id}] Claim TX: {claim_tx}")
# 5. Wait for claim UTxO
logger.info(f"[{task_id}] Waiting for claim UTxO ({TX_PROPAGATION_DELAY}s)...")
await asyncio.sleep(TX_PROPAGATION_DELAY)
claimed_utxo = await wait_for_utxo(
supplier_agent.context, script_addr_str, claim_tx
)
logger.info(f"[{task_id}] Claim UTxO confirmed")
# 6. Submit result hash (Claimed -> Submitted)
logger.info(f"[{task_id}] Submitting result hash...")
submit_tx = await submit_result(
context=supplier_agent.context,
signer=supplier_agent,
utxo=claimed_utxo,
script=script,
script_address=script_address,
task_hash=task_hash,
buyer_vkh=buyer_vkh,
supplier_vkh=supplier_vkh,
payment_dfm=task_request.payment_dfm,
result_hash_hex=result_hash,
resolver_cred=resolver_cred,
deadline_posix_ms=task_request.deadline_posix_ms,
dispute_window_ms=task_request.dispute_window_ms,
)
logger.info(f"[{task_id}] Submit TX: {submit_tx}")
# 7. Return TaskResponse
return TaskResponse(
task_id=task_id,
status="completed",
response=response_text,
result_hash=result_hash,
model_used=llm_result.model,
input_tokens=llm_result.input_tokens,
output_tokens=llm_result.output_tokens,
claim_tx=claim_tx,
submit_tx=submit_tx,
)
except Exception as e:
logger.error(f"[{task_id}] Task failed: {e}", exc_info=True)
return TaskResponse(
task_id=task_id,
status="failed",
response="",
result_hash="",
model_used="",
input_tokens=0,
output_tokens=0,
claim_tx="",
submit_tx="",
error=str(e),
)