- Add GPU fields (model, memory, count, CUDA version, price, region) to MarketplaceOffer model - Create new marketplace_gpu router for GPU-specific operations - Update offer sync to populate GPU fields from miner capabilities - Move GPU attributes from generic attributes dict to dedicated fields - Update MarketplaceOfferView schema with GPU fields - Expand CLI README with comprehensive documentation and
442 lines
13 KiB
Python
442 lines
13 KiB
Python
"""Simulation commands for AITBC CLI"""
|
|
|
|
import click
|
|
import json
|
|
import time
|
|
import random
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict, Any
|
|
from ..utils import output, error, success
|
|
|
|
|
|
@click.group()
|
|
def simulate():
|
|
"""Run simulations and manage test users"""
|
|
pass
|
|
|
|
|
|
@simulate.command()
|
|
@click.option("--distribute", default="10000,1000",
|
|
help="Initial distribution: client_amount,miner_amount")
|
|
@click.option("--reset", is_flag=True, help="Reset existing simulation")
|
|
@click.pass_context
|
|
def init(ctx, distribute: str, reset: bool):
|
|
"""Initialize test economy"""
|
|
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
|
|
|
if reset:
|
|
success("Resetting simulation...")
|
|
# Reset wallet files
|
|
for wallet_file in ["client_wallet.json", "miner_wallet.json"]:
|
|
wallet_path = home_dir / wallet_file
|
|
if wallet_path.exists():
|
|
wallet_path.unlink()
|
|
|
|
# Parse distribution
|
|
try:
|
|
client_amount, miner_amount = map(float, distribute.split(","))
|
|
except:
|
|
error("Invalid distribution format. Use: client_amount,miner_amount")
|
|
return
|
|
|
|
# Initialize genesis wallet
|
|
genesis_path = home_dir / "genesis_wallet.json"
|
|
if not genesis_path.exists():
|
|
genesis_wallet = {
|
|
"address": "aitbc1genesis",
|
|
"balance": 1000000,
|
|
"transactions": []
|
|
}
|
|
with open(genesis_path, 'w') as f:
|
|
json.dump(genesis_wallet, f, indent=2)
|
|
success("Genesis wallet created")
|
|
|
|
# Initialize client wallet
|
|
client_path = home_dir / "client_wallet.json"
|
|
if not client_path.exists():
|
|
client_wallet = {
|
|
"address": "aitbc1client",
|
|
"balance": client_amount,
|
|
"transactions": [{
|
|
"type": "receive",
|
|
"amount": client_amount,
|
|
"from": "aitbc1genesis",
|
|
"timestamp": time.time()
|
|
}]
|
|
}
|
|
with open(client_path, 'w') as f:
|
|
json.dump(client_wallet, f, indent=2)
|
|
success(f"Client wallet initialized with {client_amount} AITBC")
|
|
|
|
# Initialize miner wallet
|
|
miner_path = home_dir / "miner_wallet.json"
|
|
if not miner_path.exists():
|
|
miner_wallet = {
|
|
"address": "aitbc1miner",
|
|
"balance": miner_amount,
|
|
"transactions": [{
|
|
"type": "receive",
|
|
"amount": miner_amount,
|
|
"from": "aitbc1genesis",
|
|
"timestamp": time.time()
|
|
}]
|
|
}
|
|
with open(miner_path, 'w') as f:
|
|
json.dump(miner_wallet, f, indent=2)
|
|
success(f"Miner wallet initialized with {miner_amount} AITBC")
|
|
|
|
output({
|
|
"status": "initialized",
|
|
"distribution": {
|
|
"client": client_amount,
|
|
"miner": miner_amount
|
|
},
|
|
"total_supply": client_amount + miner_amount
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@simulate.group()
|
|
def user():
|
|
"""Manage test users"""
|
|
pass
|
|
|
|
|
|
@user.command()
|
|
@click.option("--type", type=click.Choice(["client", "miner"]), required=True)
|
|
@click.option("--name", required=True, help="User name")
|
|
@click.option("--balance", type=float, default=100, help="Initial balance")
|
|
@click.pass_context
|
|
def create(ctx, type: str, name: str, balance: float):
|
|
"""Create a test user"""
|
|
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
|
|
|
user_id = f"{type}_{name}"
|
|
wallet_path = home_dir / f"{user_id}_wallet.json"
|
|
|
|
if wallet_path.exists():
|
|
error(f"User {name} already exists")
|
|
return
|
|
|
|
wallet = {
|
|
"address": f"aitbc1{user_id}",
|
|
"balance": balance,
|
|
"transactions": [{
|
|
"type": "receive",
|
|
"amount": balance,
|
|
"from": "aitbc1genesis",
|
|
"timestamp": time.time()
|
|
}]
|
|
}
|
|
|
|
with open(wallet_path, 'w') as f:
|
|
json.dump(wallet, f, indent=2)
|
|
|
|
success(f"Created {type} user: {name}")
|
|
output({
|
|
"user_id": user_id,
|
|
"address": wallet["address"],
|
|
"balance": balance
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@user.command()
|
|
@click.pass_context
|
|
def list(ctx):
|
|
"""List all test users"""
|
|
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
|
|
|
users = []
|
|
for wallet_file in home_dir.glob("*_wallet.json"):
|
|
if wallet_file.name in ["genesis_wallet.json"]:
|
|
continue
|
|
|
|
with open(wallet_file) as f:
|
|
wallet = json.load(f)
|
|
|
|
user_type = "client" if "client" in wallet_file.name else "miner"
|
|
user_name = wallet_file.stem.replace("_wallet", "").replace(f"{user_type}_", "")
|
|
|
|
users.append({
|
|
"name": user_name,
|
|
"type": user_type,
|
|
"address": wallet["address"],
|
|
"balance": wallet["balance"]
|
|
})
|
|
|
|
output({"users": users}, ctx.obj['output_format'])
|
|
|
|
|
|
@user.command()
|
|
@click.argument("user")
|
|
@click.pass_context
|
|
def balance(ctx, user: str):
|
|
"""Check user balance"""
|
|
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
|
wallet_path = home_dir / f"{user}_wallet.json"
|
|
|
|
if not wallet_path.exists():
|
|
error(f"User {user} not found")
|
|
return
|
|
|
|
with open(wallet_path) as f:
|
|
wallet = json.load(f)
|
|
|
|
output({
|
|
"user": user,
|
|
"address": wallet["address"],
|
|
"balance": wallet["balance"]
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@user.command()
|
|
@click.argument("user")
|
|
@click.argument("amount", type=float)
|
|
@click.pass_context
|
|
def fund(ctx, user: str, amount: float):
|
|
"""Fund a test user"""
|
|
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
|
|
|
# Load genesis wallet
|
|
genesis_path = home_dir / "genesis_wallet.json"
|
|
with open(genesis_path) as f:
|
|
genesis = json.load(f)
|
|
|
|
if genesis["balance"] < amount:
|
|
error(f"Insufficient genesis balance: {genesis['balance']}")
|
|
return
|
|
|
|
# Load user wallet
|
|
wallet_path = home_dir / f"{user}_wallet.json"
|
|
if not wallet_path.exists():
|
|
error(f"User {user} not found")
|
|
return
|
|
|
|
with open(wallet_path) as f:
|
|
wallet = json.load(f)
|
|
|
|
# Transfer funds
|
|
genesis["balance"] -= amount
|
|
genesis["transactions"].append({
|
|
"type": "send",
|
|
"amount": -amount,
|
|
"to": wallet["address"],
|
|
"timestamp": time.time()
|
|
})
|
|
|
|
wallet["balance"] += amount
|
|
wallet["transactions"].append({
|
|
"type": "receive",
|
|
"amount": amount,
|
|
"from": genesis["address"],
|
|
"timestamp": time.time()
|
|
})
|
|
|
|
# Save wallets
|
|
with open(genesis_path, 'w') as f:
|
|
json.dump(genesis, f, indent=2)
|
|
|
|
with open(wallet_path, 'w') as f:
|
|
json.dump(wallet, f, indent=2)
|
|
|
|
success(f"Funded {user} with {amount} AITBC")
|
|
output({
|
|
"user": user,
|
|
"amount": amount,
|
|
"new_balance": wallet["balance"]
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@simulate.command()
|
|
@click.option("--jobs", type=int, default=5, help="Number of jobs to simulate")
|
|
@click.option("--rounds", type=int, default=3, help="Number of rounds")
|
|
@click.option("--delay", type=float, default=1.0, help="Delay between operations (seconds)")
|
|
@click.pass_context
|
|
def workflow(ctx, jobs: int, rounds: int, delay: float):
|
|
"""Simulate complete workflow"""
|
|
config = ctx.obj['config']
|
|
|
|
success(f"Starting workflow simulation: {jobs} jobs x {rounds} rounds")
|
|
|
|
for round_num in range(1, rounds + 1):
|
|
click.echo(f"\n--- Round {round_num} ---")
|
|
|
|
# Submit jobs
|
|
submitted_jobs = []
|
|
for i in range(jobs):
|
|
prompt = f"Test job {i+1} (round {round_num})"
|
|
|
|
# Simulate job submission
|
|
job_id = f"job_{round_num}_{i+1}_{int(time.time())}"
|
|
submitted_jobs.append(job_id)
|
|
|
|
output({
|
|
"action": "submit_job",
|
|
"job_id": job_id,
|
|
"prompt": prompt,
|
|
"round": round_num
|
|
}, ctx.obj['output_format'])
|
|
|
|
time.sleep(delay)
|
|
|
|
# Simulate job processing
|
|
for job_id in submitted_jobs:
|
|
# Simulate miner picking up job
|
|
output({
|
|
"action": "job_assigned",
|
|
"job_id": job_id,
|
|
"miner": f"miner_{random.randint(1, 3)}",
|
|
"status": "processing"
|
|
}, ctx.obj['output_format'])
|
|
|
|
time.sleep(delay * 0.5)
|
|
|
|
# Simulate job completion
|
|
earnings = random.uniform(1, 10)
|
|
output({
|
|
"action": "job_completed",
|
|
"job_id": job_id,
|
|
"earnings": earnings,
|
|
"status": "completed"
|
|
}, ctx.obj['output_format'])
|
|
|
|
time.sleep(delay * 0.5)
|
|
|
|
output({
|
|
"status": "completed",
|
|
"total_jobs": jobs * rounds,
|
|
"rounds": rounds
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@simulate.command()
|
|
@click.option("--clients", type=int, default=10, help="Number of clients")
|
|
@click.option("--miners", type=int, default=3, help="Number of miners")
|
|
@click.option("--duration", type=int, default=300, help="Test duration in seconds")
|
|
@click.option("--job-rate", type=float, default=1.0, help="Jobs per second")
|
|
@click.pass_context
|
|
def load_test(ctx, clients: int, miners: int, duration: int, job_rate: float):
|
|
"""Run load test"""
|
|
start_time = time.time()
|
|
end_time = start_time + duration
|
|
job_interval = 1.0 / job_rate
|
|
|
|
success(f"Starting load test: {clients} clients, {miners} miners, {duration}s")
|
|
|
|
stats = {
|
|
"jobs_submitted": 0,
|
|
"jobs_completed": 0,
|
|
"errors": 0,
|
|
"start_time": start_time
|
|
}
|
|
|
|
while time.time() < end_time:
|
|
# Submit jobs
|
|
for client_id in range(clients):
|
|
if time.time() >= end_time:
|
|
break
|
|
|
|
job_id = f"load_test_{stats['jobs_submitted']}_{int(time.time())}"
|
|
stats["jobs_submitted"] += 1
|
|
|
|
# Simulate random job completion
|
|
if random.random() > 0.1: # 90% success rate
|
|
stats["jobs_completed"] += 1
|
|
else:
|
|
stats["errors"] += 1
|
|
|
|
time.sleep(job_interval)
|
|
|
|
# Show progress
|
|
elapsed = time.time() - start_time
|
|
if elapsed % 30 < 1: # Every 30 seconds
|
|
output({
|
|
"elapsed": elapsed,
|
|
"jobs_submitted": stats["jobs_submitted"],
|
|
"jobs_completed": stats["jobs_completed"],
|
|
"errors": stats["errors"],
|
|
"success_rate": stats["jobs_completed"] / max(1, stats["jobs_submitted"]) * 100
|
|
}, ctx.obj['output_format'])
|
|
|
|
# Final stats
|
|
total_time = time.time() - start_time
|
|
output({
|
|
"status": "completed",
|
|
"duration": total_time,
|
|
"jobs_submitted": stats["jobs_submitted"],
|
|
"jobs_completed": stats["jobs_completed"],
|
|
"errors": stats["errors"],
|
|
"avg_jobs_per_second": stats["jobs_submitted"] / total_time,
|
|
"success_rate": stats["jobs_completed"] / max(1, stats["jobs_submitted"]) * 100
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@simulate.command()
|
|
@click.option("--file", required=True, help="Scenario file path")
|
|
@click.pass_context
|
|
def scenario(ctx, file: str):
|
|
"""Run predefined scenario"""
|
|
scenario_path = Path(file)
|
|
|
|
if not scenario_path.exists():
|
|
error(f"Scenario file not found: {file}")
|
|
return
|
|
|
|
with open(scenario_path) as f:
|
|
scenario = json.load(f)
|
|
|
|
success(f"Running scenario: {scenario.get('name', 'Unknown')}")
|
|
|
|
# Execute scenario steps
|
|
for step in scenario.get("steps", []):
|
|
step_type = step.get("type")
|
|
step_name = step.get("name", "Unnamed step")
|
|
|
|
click.echo(f"\nExecuting: {step_name}")
|
|
|
|
if step_type == "submit_jobs":
|
|
count = step.get("count", 1)
|
|
for i in range(count):
|
|
output({
|
|
"action": "submit_job",
|
|
"step": step_name,
|
|
"job_num": i + 1,
|
|
"prompt": step.get("prompt", f"Scenario job {i+1}")
|
|
}, ctx.obj['output_format'])
|
|
|
|
elif step_type == "wait":
|
|
duration = step.get("duration", 1)
|
|
time.sleep(duration)
|
|
|
|
elif step_type == "check_balance":
|
|
user = step.get("user", "client")
|
|
# Would check actual balance
|
|
output({
|
|
"action": "check_balance",
|
|
"user": user
|
|
}, ctx.obj['output_format'])
|
|
|
|
output({
|
|
"status": "completed",
|
|
"scenario": scenario.get('name', 'Unknown')
|
|
}, ctx.obj['output_format'])
|
|
|
|
|
|
@simulate.command()
|
|
@click.argument("simulation_id")
|
|
@click.pass_context
|
|
def results(ctx, simulation_id: str):
|
|
"""Show simulation results"""
|
|
# In a real implementation, this would query stored results
|
|
# For now, return mock data
|
|
output({
|
|
"simulation_id": simulation_id,
|
|
"status": "completed",
|
|
"start_time": time.time() - 3600,
|
|
"end_time": time.time(),
|
|
"duration": 3600,
|
|
"total_jobs": 50,
|
|
"successful_jobs": 48,
|
|
"failed_jobs": 2,
|
|
"success_rate": 96.0
|
|
}, ctx.obj['output_format'])
|