Files
aitbc/cli/aitbc_cli/commands/operations.py
aitbc 3897bcbf24
Some checks failed
CLI Tests / test-cli (push) Failing after 4s
Deploy to Testnet / deploy-testnet (push) Successful in 1m40s
Documentation Validation / validate-docs (push) Failing after 12s
Documentation Validation / validate-policies-strict (push) Successful in 4s
Integration Tests / test-service-integration (push) Successful in 2m42s
Package Tests / Python package - aitbc-agent-sdk (push) Failing after 34s
Package Tests / Python package - aitbc-core (push) Successful in 27s
Package Tests / Python package - aitbc-crypto (push) Successful in 13s
Package Tests / Python package - aitbc-sdk (push) Successful in 16s
Package Tests / JavaScript package - aitbc-sdk-js (push) Successful in 8s
Package Tests / JavaScript package - aitbc-token (push) Successful in 18s
Python Tests / test-python (push) Failing after 50s
Security Scanning / security-scan (push) Failing after 43s
Multi-Node Stress Testing / stress-test (push) Successful in 12s
Cross-Node Transaction Testing / transaction-test (push) Successful in 9s
refactor: move version to separate module and improve logging
- Created aitbc/_version.py with centralized version definition
- Updated aitbc/__init__.py to import __version__ from _version module
- Updated constants.py to use __version__ for PACKAGE_VERSION
- Replaced print() calls with logger in decorators.py, events.py, queue_manager.py, and state.py
- Added logger initialization using get_logger(__name__) in config.py, decorators.py, events.py, queue_manager.py, and state.py
- Added cli/commands
2026-05-11 20:12:01 +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}/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}")