Files
aitbc/cli/aitbc_cli/commands/operations.py
aitbc 51209844c9 Implement 36 missing CLI subcommands for scenarios 04-47
Phase 1: Simple GET Commands (system, resource, edge, config)
- system: restart, status, config
- resource: status, deallocate
- edge: status, balance, transfer
- config: import (already existed)

Phase 2: Exchange & Market Commands
- exchange: order, orders, book, history
- marketplace: bid, bids, ask, asks

Phase 3: AI & GPU Commands
- agent (AI): job, jobs, submit, cancel
- gpu: register, update

Phase 4: Wallet, Hermes, Operations
- wallet: export, import
- hermes: send, receive, peers
- operations: vote, proposal, delegate

Phase 5: Simulate Commands
- simulate: run, status, result

All commands follow common pattern using AITBCHTTPClient for backend API interaction.
2026-05-26 22:03:58 +02:00

448 lines
15 KiB
Python

"""
General operations commands for AITBC CLI (marketplace, AI, agents)
"""
import json
import time
import hashlib
from pathlib import Path
from typing import Optional
import click
from ..utils import error, success, output
from ..utils.wallet import decrypt_private_key
from ..config import get_config
from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR, get_logger
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
logger = get_logger(__name__)
DEFAULT_RPC_URL = "http://localhost:8006"
DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR
@click.group()
def operations():
"""General operations commands"""
pass
# Marketplace operations
@operations.group()
def marketplace():
"""Marketplace operations"""
pass
@marketplace.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
def list_listings(format: str):
"""List marketplace listings"""
try:
http_client = AITBCHTTPClient(base_url="http://localhost:8102", timeout=30)
data = http_client.get("/rpc/marketplace/listings")
listings = data.get("listings", [])
success(f"Marketplace listings: {len(listings)}")
if format == 'json':
click.echo(json.dumps(listings, indent=2))
else:
for listing in listings:
click.echo(f" - {listing.get('name', 'unknown')}: {listing.get('price', 0)} AIT")
except NetworkError as e:
error(f"Error getting marketplace listings: {e}")
except Exception as e:
error(f"Error: {e}")
@marketplace.command()
@click.argument('listing_id')
@click.option('--quantity', type=int, default=1, help='Quantity to purchase')
@click.option('--wallet', help='Wallet name for payment')
def purchase(listing_id: str, quantity: int, wallet: Optional[str]):
"""Purchase from marketplace listing"""
success(f"Purchase {quantity} of listing {listing_id}")
# TODO: Implement actual purchase logic with wallet signing
@marketplace.command()
@click.option('--wallet-name', required=True, help='Seller wallet name')
@click.option('--item-type', required=True, help='Type of item')
@click.option('--price', type=float, required=True, help='Listing price')
@click.option('--description', help='Item description')
def create_listing(wallet_name: str, item_type: str, price: float, description: Optional[str]):
"""Create a marketplace listing"""
try:
# Get wallet address
keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json"
if not keystore_path.exists():
error(f"Wallet '{wallet_name}' not found")
return None
with open(keystore_path) as f:
wallet_data = json.load(f)
address = wallet_data['address']
# Create listing via RPC
listing_config = {
"seller_address": address,
"item_type": item_type,
"price": price,
"description": description or ""
}
try:
http_client = AITBCHTTPClient(base_url="http://localhost:8102", timeout=30)
result = http_client.post("/rpc/marketplace/create", json=listing_config)
success(f"Listing created successfully")
click.echo(f"Item: {item_type}")
click.echo(f"Price: {price} AIT")
click.echo(f"Listing ID: {result.get('listing_id', 'unknown')}")
return result
except NetworkError as e:
error(f"Error creating listing: {e}")
return None
except Exception as e:
error(f"Error: {e}")
return None
except Exception as e:
error(f"Error: {e}")
# AI operations
@operations.group()
def ai():
"""AI operations"""
pass
@ai.command()
@click.option('--wallet-name', required=True, help='Client wallet name')
@click.option('--job-type', required=True, help='Type of AI job')
@click.option('--prompt', required=True, help='AI prompt')
@click.option('--payment', type=float, required=True, help='Payment amount')
@click.option('--model', help='AI model to use')
def submit_job(wallet_name: str, job_type: str, prompt: str, payment: float, model: Optional[str]):
"""Submit an AI job"""
try:
# Get wallet address
keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json"
if not keystore_path.exists():
error(f"Wallet '{wallet_name}' not found")
return None
with open(keystore_path) as f:
wallet_data = json.load(f)
address = wallet_data['address']
# Submit job via coordinator API
job_config = {
"client_address": address,
"job_type": job_type,
"prompt": prompt,
"payment": payment,
"model": model or "default"
}
try:
http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30)
result = http_client.post("/v1/jobs", json=job_config)
success(f"AI job submitted successfully")
click.echo(f"Job ID: {result.get('job_id', 'unknown')}")
click.echo(f"Type: {job_type}")
click.echo(f"Payment: {payment} AIT")
return result
except NetworkError as e:
error(f"Error submitting AI job: {e}")
return None
except Exception as e:
error(f"Error: {e}")
return None
except Exception as e:
error(f"Error: {e}")
@ai.command()
@click.option('--job-id', help='Specific job ID')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
def status(job_id: Optional[str], format: str):
"""Get AI job status"""
try:
http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30)
if job_id:
result = http_client.get(f"/v1/jobs/{job_id}")
success(f"Job status for {job_id}")
else:
result = http_client.get("/v1/jobs")
success(f"All jobs status")
if format == 'json':
click.echo(json.dumps(result, indent=2))
else:
if job_id:
click.echo(f"Status: {result.get('state', 'unknown')}")
click.echo(f"Progress: {result.get('progress', '0%')}")
else:
for job in result.get('jobs', []):
click.echo(f" - {job.get('job_id', 'unknown')}: {job.get('state', 'unknown')}")
except NetworkError as e:
error(f"Error getting AI job status: {e}")
except Exception as e:
error(f"Error: {e}")
@ai.command()
@click.option('--job-id', help='Specific job ID')
def cancel(job_id: Optional[str]):
"""Cancel an AI job"""
if not job_id:
error("Job ID is required")
return
try:
http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30)
result = http_client.post(f"/v1/jobs/{job_id}/cancel")
success(f"AI job {job_id} cancelled")
except NetworkError as e:
error(f"Error cancelling AI job: {e}")
except Exception as e:
error(f"Error: {e}")
# Agent operations
@operations.group()
def agent():
"""Agent operations"""
pass
@agent.command()
@click.option('--agent-id', required=True, help='Agent ID')
@click.option('--status', type=click.Choice(['active', 'inactive', 'busy', 'offline']), default='active', help='Agent status')
def register(agent_id: str, status: str):
"""Register an agent"""
try:
agent_config = {
"agent_id": agent_id,
"status": status
}
http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30)
result = http_client.post("/v1/agents/register", json=agent_config)
success(f"Agent {agent_id} registered with status {status}")
except NetworkError as e:
error(f"Error registering agent: {e}")
except Exception as e:
error(f"Error: {e}")
@agent.command()
@click.option('--status', help='Filter by status')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
def list(status: Optional[str], format: str):
"""List registered agents"""
try:
import requests
coordinator_url = "http://localhost:9001"
query = {}
if status:
query["status"] = status
response = requests.post(f"{coordinator_url}/v1/agents/discover", json=query, timeout=10)
if response.status_code == 200:
data = response.json()
agents = data.get("agents", [])
success(f"Agents: {len(agents)}")
if format == 'json':
click.echo(json.dumps(agents, indent=2))
else:
for agent in agents:
click.echo(f" - {agent.get('agent_id', 'unknown')}: {agent.get('status', 'unknown')} - {agent.get('agent_type', 'unknown')}")
else:
error(f"Error listing agents: {response.status_code}")
except Exception as e:
error(f"Error: {e}")
@agent.command()
@click.argument('agent_id')
def deregister(agent_id: str):
"""Deregister an agent"""
try:
http_client = AITBCHTTPClient(base_url="http://localhost:9001", timeout=30)
result = http_client.post(f"/v1/agents/{agent_id}/deregister")
success(f"Agent {agent_id} deregistered")
except NetworkError as e:
error(f"Error deregistering agent: {e}")
except Exception as e:
error(f"Error: {e}")
@agent.command()
@click.option('--agent', required=True, help='Recipient agent address')
@click.option('--message', required=True, help='Message content')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.option('--password', help='Wallet password')
@click.option('--password-file', help='File containing wallet password')
@click.option('--rpc-url', help='Blockchain RPC URL')
def message(agent: str, message: str, wallet: str, password: Optional[str], password_file: Optional[str], rpc_url: Optional[str]):
"""Send message to agent via blockchain transaction"""
if not rpc_url:
rpc_url = DEFAULT_RPC_URL
# Get password
if password_file:
with open(password_file) as f:
password = f.read().strip()
elif not password:
import getpass
password = getpass.getpass("Enter wallet password: ")
try:
# Decrypt wallet
keystore_path = DEFAULT_KEYSTORE_DIR / f"{wallet}.json"
private_key_hex = decrypt_private_key(keystore_path, password)
private_key_bytes = bytes.fromhex(private_key_hex)
# Get sender address
with open(keystore_path) as f:
keystore_data = json.load(f)
sender_address = keystore_data['address']
# Create transaction with message as payload
priv_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
pub_hex = priv_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
).hex()
# Get chain_id
from ..utils.chain_id import get_chain_id
chain_id = get_chain_id(rpc_url)
# Get actual nonce
try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5)
account_data = http_client.get(f"/rpc/account/{sender_address}")
actual_nonce = account_data.get("nonce", 0)
except Exception:
actual_nonce = 0
tx = {
"type": "TRANSFER",
"chain_id": chain_id,
"from": sender_address,
"nonce": actual_nonce,
"fee": 10,
"payload": {
"recipient": agent,
"amount": 0,
"message": message
}
}
# Sign transaction
tx_string = json.dumps(tx, sort_keys=True)
tx["signature"] = priv_key.sign(tx_string.encode()).hex()
tx["public_key"] = pub_hex
# Submit transaction
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
result = http_client.post("/rpc/transaction", json=tx)
success(f"Message sent successfully")
click.echo(f"From: {sender_address}")
click.echo(f"To: {agent}")
click.echo(f"Content: {message}")
click.echo(f"TX Hash: {result.get('transaction_hash', 'unknown')}")
except Exception as e:
error(f"Error sending message: {e}")
# Governance operations
@operations.group()
def governance():
"""Governance operations"""
pass
@governance.command()
@click.argument('proposal_id')
@click.option('--vote', type=click.Choice(['yes', 'no', 'abstain']), required=True, help='Vote option')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.pass_context
def vote(ctx, proposal_id: str, vote: str, wallet: str):
"""Vote on a governance proposal"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.governance_service_url, timeout=10)
vote_data = {
"proposal_id": proposal_id,
"vote": vote,
"wallet": wallet
}
result = http_client.post("/governance/vote", json=vote_data)
success(f"Vote '{vote}' cast for proposal {proposal_id}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error casting vote: {e}")
@governance.command()
@click.option('--title', required=True, help='Proposal title')
@click.option('--description', required=True, help='Proposal description')
@click.option('--type', default='parameter', help='Proposal type')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.pass_context
def proposal(ctx, title: str, description: str, type: str, wallet: str):
"""Create a governance proposal"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.governance_service_url, timeout=10)
proposal_data = {
"title": title,
"description": description,
"type": type,
"wallet": wallet
}
result = http_client.post("/governance/proposals", json=proposal_data)
success(f"Proposal created: {result.get('proposal_id')}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error creating proposal: {e}")
@governance.command()
@click.argument('to_address')
@click.option('--amount', type=float, required=True, help='Amount to delegate')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.pass_context
def delegate(ctx, to_address: str, amount: float, wallet: str):
"""Delegate voting power to another address"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.governance_service_url, timeout=10)
delegate_data = {
"to_address": to_address,
"amount": amount,
"wallet": wallet
}
result = http_client.post("/governance/delegate", json=delegate_data)
success(f"Delegated {amount} to {to_address}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error delegating: {e}")