Files
aitbc/cli/aitbc_cli/commands/operations.py
aitbc 00bd0e5e5e
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
refactor: update CLI commands to use /v1 API versioning prefix
- Changed monitor.py job endpoints from /jobs to /v1/jobs
- Changed monitor.py miner endpoints from /miners to /v1/miners
- Changed operations.py agent discovery from /agents/discover to /v1/agents/discover
- Changed system.py agent endpoints from /agents/* to /v1/agents/*
- Updated agent status, registration, discovery, and retrieval endpoints
- Aligns CLI with coordinator-api versioning structure for business logic endpoints
2026-05-19 11:42:17 +02:00

360 lines
12 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
from ..utils.wallet import decrypt_private_key
from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
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}")