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
- 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
360 lines
12 KiB
Python
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}")
|
|
|