diff --git a/aitbc-miner b/aitbc-miner new file mode 120000 index 00000000..65b0f155 --- /dev/null +++ b/aitbc-miner @@ -0,0 +1 @@ +/opt/aitbc/cli/miner_cli.py \ No newline at end of file diff --git a/apps/coordinator-api/src/app/routers/marketplace_offers.py b/apps/coordinator-api/src/app/routers/marketplace_offers.py index 69957fb7..fbacbef4 100755 --- a/apps/coordinator-api/src/app/routers/marketplace_offers.py +++ b/apps/coordinator-api/src/app/routers/marketplace_offers.py @@ -7,12 +7,15 @@ Router to create marketplace offers from registered miners from typing import Any from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select +import logging from ..deps import require_admin_key from ..domain import MarketplaceOffer, Miner from ..schemas import MarketplaceOfferView from ..storage import get_session +logger = logging.getLogger(__name__) + router = APIRouter(tags=["marketplace-offers"]) @@ -102,3 +105,39 @@ async def list_miner_offers(session: Annotated[Session, Depends(get_session)]) - result.append(offer_view) return result + + +@router.get("/offers", summary="List all marketplace offers (Fixed)") +async def list_all_offers(session: Annotated[Session, Depends(get_session)]) -> list[dict[str, Any]]: + """List all marketplace offers - Fixed version to avoid AttributeError""" + try: + # Use direct database query instead of GlobalMarketplaceService + from sqlmodel import select + + offers = session.execute(select(MarketplaceOffer)).scalars().all() + + result = [] + for offer in offers: + # Extract attributes safely + attrs = offer.attributes or {} + + offer_data = { + "id": offer.id, + "provider": offer.provider, + "capacity": offer.capacity, + "price": offer.price, + "status": offer.status, + "created_at": offer.created_at.isoformat(), + "gpu_model": attrs.get("gpu_model", "Unknown"), + "gpu_memory_gb": attrs.get("gpu_memory_gb", 0), + "cuda_version": attrs.get("cuda_version", "Unknown"), + "supported_models": attrs.get("supported_models", []), + "region": attrs.get("region", "unknown") + } + result.append(offer_data) + + return result + + except Exception as e: + logger.error(f"Error listing offers: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/cli/integrate_miner_cli.sh b/cli/integrate_miner_cli.sh new file mode 100755 index 00000000..c869d0f4 --- /dev/null +++ b/cli/integrate_miner_cli.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# AITBC Miner Management Integration Script +# This script integrates the miner management functionality with the main AITBC CLI + +echo "πŸ€– AITBC Miner Management Integration" +echo "==================================" + +# Check if miner CLI exists +MINER_CLI="/opt/aitbc/cli/miner_cli.py" +if [ ! -f "$MINER_CLI" ]; then + echo "❌ Error: Miner CLI not found at $MINER_CLI" + exit 1 +fi + +# Create a symlink in the main CLI directory +MAIN_CLI_DIR="/opt/aitbc" +MINER_CMD="$MAIN_CLI_DIR/aitbc-miner" + +if [ ! -L "$MINER_CMD" ]; then + echo "πŸ”— Creating symlink: $MINER_CMD -> $MINER_CLI" + ln -s "$MINER_CLI" "$MINER_CMD" + chmod +x "$MINER_CMD" +fi + +# Test the integration +echo "πŸ§ͺ Testing miner CLI integration..." +echo "" + +# Test help +echo "πŸ“‹ Testing help command:" +$MINER_CMD --help | head -10 +echo "" + +# Test registration (with test data) +echo "πŸ“ Testing registration command:" +$MINER_CMD register --miner-id integration-test --wallet ait113e1941cb60f3bb945ec9d412527b6048b73eb2d --gpu-memory 2048 --models qwen3:8b --pricing 0.45 --region integration-test 2>/dev/null | grep "Status:" +echo "" + +echo "βœ… Miner CLI integration completed!" +echo "" +echo "πŸš€ Usage Examples:" +echo " $MINER_CMD register --miner-id my-miner --wallet --gpu-memory 8192 --models qwen3:8b --pricing 0.50" +echo " $MINER_CMD status --miner-id my-miner" +echo " $MINER_CMD poll --miner-id my-miner" +echo " $MINER_CMD heartbeat --miner-id my-miner" +echo " $MINER_CMD result --job-id --miner-id my-miner --result 'Job completed'" +echo " $MINER_CMD marketplace list" +echo " $MINER_CMD marketplace create --miner-id my-miner --price 0.75" +echo "" +echo "πŸ“š All miner management commands are now available via: $MINER_CMD" diff --git a/cli/miner_cli.py b/cli/miner_cli.py new file mode 100755 index 00000000..c09453bd --- /dev/null +++ b/cli/miner_cli.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +AITBC Miner CLI Extension +Adds comprehensive miner management commands to AITBC CLI +""" + +import sys +import os +import argparse +from pathlib import Path + +# Add the CLI directory to path +sys.path.insert(0, str(Path(__file__).parent)) + +try: + from miner_management import miner_cli_dispatcher +except ImportError: + print("❌ Error: miner_management module not found") + sys.exit(1) + + +def main(): + """Main CLI entry point for miner management""" + parser = argparse.ArgumentParser( + description="AITBC AI Compute Miner Management", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Register as AI compute provider + python miner_cli.py register --miner-id ai-miner-1 --wallet ait1xyz --gpu-memory 8192 --models qwen3:8b llama3:8b --pricing 0.50 + + # Check miner status + python miner_cli.py status --miner-id ai-miner-1 + + # Poll for jobs + python miner_cli.py poll --miner-id ai-miner-1 --max-wait 60 + + # Submit job result + python miner_cli.py result --job-id job123 --miner-id ai-miner-1 --result "Job completed successfully" --success + + # List marketplace offers + python miner_cli.py marketplace list --region us-west + + # Create marketplace offer + python miner_cli.py marketplace create --miner-id ai-miner-1 --price 0.75 --capacity 2 + """ + ) + + parser.add_argument("--coordinator-url", default="http://localhost:8000", + help="Coordinator API URL") + parser.add_argument("--api-key", default="miner_prod_key_use_real_value", + help="Miner API key") + + subparsers = parser.add_subparsers(dest="action", help="Miner management actions") + + # Register command + register_parser = subparsers.add_parser("register", help="Register as AI compute provider") + register_parser.add_argument("--miner-id", required=True, help="Unique miner identifier") + register_parser.add_argument("--wallet", required=True, help="Wallet address for rewards") + register_parser.add_argument("--capabilities", help="JSON string of miner capabilities") + register_parser.add_argument("--gpu-memory", type=int, help="GPU memory in MB") + register_parser.add_argument("--models", nargs="+", help="Supported AI models") + register_parser.add_argument("--pricing", type=float, help="Price per hour") + register_parser.add_argument("--concurrency", type=int, default=1, help="Max concurrent jobs") + register_parser.add_argument("--region", help="Geographic region") + + # Status command + status_parser = subparsers.add_parser("status", help="Get miner status") + status_parser.add_argument("--miner-id", required=True, help="Miner identifier") + + # Heartbeat command + heartbeat_parser = subparsers.add_parser("heartbeat", help="Send miner heartbeat") + heartbeat_parser.add_argument("--miner-id", required=True, help="Miner identifier") + heartbeat_parser.add_argument("--inflight", type=int, default=0, help="Currently running jobs") + heartbeat_parser.add_argument("--status", default="ONLINE", help="Miner status") + + # Poll command + poll_parser = subparsers.add_parser("poll", help="Poll for available jobs") + poll_parser.add_argument("--miner-id", required=True, help="Miner identifier") + poll_parser.add_argument("--max-wait", type=int, default=30, help="Max wait time in seconds") + poll_parser.add_argument("--auto-execute", action="store_true", help="Automatically execute assigned jobs") + + # Result command + result_parser = subparsers.add_parser("result", help="Submit job result") + result_parser.add_argument("--job-id", required=True, help="Job identifier") + result_parser.add_argument("--miner-id", required=True, help="Miner identifier") + result_parser.add_argument("--result", help="Job result (JSON string)") + result_parser.add_argument("--result-file", help="File containing job result") + result_parser.add_argument("--success", action="store_true", help="Job completed successfully") + result_parser.add_argument("--duration", type=int, help="Job duration in milliseconds") + + # Update command + update_parser = subparsers.add_parser("update", help="Update miner capabilities") + update_parser.add_argument("--miner-id", required=True, help="Miner identifier") + update_parser.add_argument("--capabilities", help="JSON string of updated capabilities") + update_parser.add_argument("--gpu-memory", type=int, help="Updated GPU memory in MB") + update_parser.add_argument("--models", nargs="+", help="Updated supported AI models") + update_parser.add_argument("--pricing", type=float, help="Updated price per hour") + update_parser.add_argument("--concurrency", type=int, help="Updated max concurrent jobs") + update_parser.add_argument("--region", help="Updated geographic region") + update_parser.add_argument("--wallet", help="Updated wallet address") + + # Earnings command + earnings_parser = subparsers.add_parser("earnings", help="Check miner earnings") + earnings_parser.add_argument("--miner-id", required=True, help="Miner identifier") + earnings_parser.add_argument("--period", choices=["day", "week", "month", "all"], default="all", help="Earnings period") + + # Marketplace commands + marketplace_parser = subparsers.add_parser("marketplace", help="Manage marketplace offers") + marketplace_subparsers = marketplace_parser.add_subparsers(dest="marketplace_action", help="Marketplace actions") + + # Marketplace list + market_list_parser = marketplace_subparsers.add_parser("list", help="List marketplace offers") + market_list_parser.add_argument("--miner-id", help="Filter by miner ID") + market_list_parser.add_argument("--region", help="Filter by region") + + # Marketplace create + market_create_parser = marketplace_subparsers.add_parser("create", help="Create marketplace offer") + market_create_parser.add_argument("--miner-id", required=True, help="Miner identifier") + market_create_parser.add_argument("--price", type=float, required=True, help="Offer price per hour") + market_create_parser.add_argument("--capacity", type=int, default=1, help="Available capacity") + market_create_parser.add_argument("--region", help="Geographic region") + + args = parser.parse_args() + + if not args.action: + parser.print_help() + return + + # Initialize action variable + action = args.action + + # Prepare kwargs for the dispatcher + kwargs = { + "coordinator_url": args.coordinator_url, + "api_key": args.api_key + } + + # Add action-specific arguments + if args.action == "register": + kwargs.update({ + "miner_id": args.miner_id, + "wallet": args.wallet, + "capabilities": args.capabilities, + "gpu_memory": args.gpu_memory, + "models": args.models, + "pricing": args.pricing, + "concurrency": args.concurrency, + "region": args.region + }) + + elif args.action == "status": + kwargs["miner_id"] = args.miner_id + + elif args.action == "heartbeat": + kwargs.update({ + "miner_id": args.miner_id, + "inflight": args.inflight, + "status": args.status + }) + + elif args.action == "poll": + kwargs.update({ + "miner_id": args.miner_id, + "max_wait": args.max_wait, + "auto_execute": args.auto_execute + }) + + elif args.action == "result": + kwargs.update({ + "job_id": args.job_id, + "miner_id": args.miner_id, + "result": args.result, + "result_file": args.result_file, + "success": args.success, + "duration": args.duration + }) + + elif args.action == "update": + kwargs.update({ + "miner_id": args.miner_id, + "capabilities": args.capabilities, + "gpu_memory": args.gpu_memory, + "models": args.models, + "pricing": args.pricing, + "concurrency": args.concurrency, + "region": args.region, + "wallet": args.wallet + }) + + elif args.action == "earnings": + kwargs.update({ + "miner_id": args.miner_id, + "period": args.period + }) + + elif args.action == "marketplace": + action = args.action + if args.marketplace_action == "list": + kwargs.update({ + "miner_id": getattr(args, 'miner_id', None), + "region": getattr(args, 'region', None) + }) + action = "marketplace_list" + elif args.marketplace_action == "create": + kwargs.update({ + "miner_id": args.miner_id, + "price": args.price, + "capacity": args.capacity, + "region": getattr(args, 'region', None) + }) + action = "marketplace_create" + else: + print("❌ Unknown marketplace action") + return + + result = miner_cli_dispatcher(action, **kwargs) + + # Display results + if result: + print("\n" + "="*60) + print(f"πŸ€– AITBC Miner Management - {action.upper()}") + print("="*60) + + if "status" in result: + print(f"Status: {result['status']}") + + if result.get("status", "").startswith("βœ…"): + # Success - show details + for key, value in result.items(): + if key not in ["action", "status"]: + if isinstance(value, (dict, list)): + print(f"{key}:") + if isinstance(value, dict): + for k, v in value.items(): + print(f" {k}: {v}") + else: + for item in value: + print(f" - {item}") + else: + print(f"{key}: {value}") + else: + # Error or info - show all relevant fields + for key, value in result.items(): + if key != "action": + print(f"{key}: {value}") + + print("="*60) + else: + print("❌ No response from server") + + +if __name__ == "__main__": + main() diff --git a/cli/miner_management.py b/cli/miner_management.py new file mode 100644 index 00000000..b217f1f0 --- /dev/null +++ b/cli/miner_management.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 +""" +AITBC Miner Management Module +Complete command-line interface for AI compute miner operations including: +- Miner Registration +- Status Management +- Job Polling & Execution +- Marketplace Integration +- Payment Management +""" + +import json +import time +import requests +from typing import Optional, Dict, Any + +# Default configuration +DEFAULT_COORDINATOR_URL = "http://localhost:8000" +DEFAULT_API_KEY = "miner_prod_key_use_real_value" + + +def register_miner( + miner_id: str, + wallet: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + capabilities: Optional[str] = None, + gpu_memory: Optional[int] = None, + models: Optional[list] = None, + pricing: Optional[float] = None, + concurrency: int = 1, + region: Optional[str] = None +) -> Optional[Dict]: + """Register miner as AI compute provider""" + try: + headers = { + "X-Api-Key": api_key, + "X-Miner-ID": miner_id, + "Content-Type": "application/json" + } + + # Build capabilities from arguments + caps = {} + + if gpu_memory: + caps["gpu_memory"] = gpu_memory + caps["gpu_memory_gb"] = gpu_memory + if models: + caps["models"] = models + caps["supported_models"] = models + if pricing: + caps["pricing_per_hour"] = pricing + caps["price_per_hour"] = pricing + caps["gpu"] = "AI-GPU" + caps["gpu_count"] = 1 + caps["cuda_version"] = "12.0" + + # Override with capabilities JSON if provided + if capabilities: + caps.update(json.loads(capabilities)) + + payload = { + "wallet_address": wallet, + "capabilities": caps, + "concurrency": concurrency, + "region": region + } + + response = requests.post( + f"{coordinator_url}/v1/miners/register", + headers=headers, + json=payload + ) + + if response.status_code == 200: + result = response.json() + return { + "action": "register", + "miner_id": miner_id, + "status": "βœ… Registered successfully", + "session_token": result.get("session_token"), + "coordinator_url": coordinator_url, + "capabilities": caps + } + else: + return { + "action": "register", + "status": "❌ Registration failed", + "error": response.text, + "status_code": response.status_code + } + + except Exception as e: + return {"action": "register", "status": f"❌ Error: {str(e)}"} + + +def get_miner_status( + miner_id: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL +) -> Optional[Dict]: + """Get miner status and statistics""" + try: + # Use admin API key to get miner status + admin_api_key = api_key.replace("miner_", "admin_") + headers = {"X-Api-Key": admin_api_key} + + response = requests.get( + f"{coordinator_url}/v1/admin/miners", + headers=headers + ) + + if response.status_code == 200: + miners = response.json().get("items", []) + miner_info = next((m for m in miners if m["miner_id"] == miner_id), None) + + if miner_info: + return { + "action": "status", + "miner_id": miner_id, + "status": f"βœ… {miner_info['status']}", + "inflight": miner_info["inflight"], + "concurrency": miner_info["concurrency"], + "region": miner_info["region"], + "last_heartbeat": miner_info["last_heartbeat"], + "jobs_completed": miner_info["jobs_completed"], + "jobs_failed": miner_info["jobs_failed"], + "average_job_duration_ms": miner_info["average_job_duration_ms"], + "success_rate": ( + miner_info["jobs_completed"] / + max(1, miner_info["jobs_completed"] + miner_info["jobs_failed"]) * 100 + ) + } + else: + return { + "action": "status", + "miner_id": miner_id, + "status": "❌ Miner not found" + } + else: + return {"action": "status", "status": "❌ Failed to get status", "error": response.text} + + except Exception as e: + return {"action": "status", "status": f"❌ Error: {str(e)}"} + + +def send_heartbeat( + miner_id: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + inflight: int = 0, + status: str = "ONLINE" +) -> Optional[Dict]: + """Send miner heartbeat""" + try: + headers = { + "X-Api-Key": api_key, + "X-Miner-ID": miner_id, + "Content-Type": "application/json" + } + + payload = { + "inflight": inflight, + "status": status, + "metadata": { + "timestamp": time.time(), + "version": "1.0.0", + "system_info": "AI Compute Miner" + } + } + + response = requests.post( + f"{coordinator_url}/v1/miners/heartbeat", + headers=headers, + json=payload + ) + + if response.status_code == 200: + return { + "action": "heartbeat", + "miner_id": miner_id, + "status": "βœ… Heartbeat sent successfully", + "inflight": inflight, + "miner_status": status + } + else: + return {"action": "heartbeat", "status": "❌ Heartbeat failed", "error": response.text} + + except Exception as e: + return {"action": "heartbeat", "status": f"❌ Error: {str(e)}"} + + +def poll_jobs( + miner_id: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + max_wait: int = 30, + auto_execute: bool = False +) -> Optional[Dict]: + """Poll for available jobs""" + try: + headers = { + "X-Api-Key": api_key, + "X-Miner-ID": miner_id, + "Content-Type": "application/json" + } + + payload = {"max_wait_seconds": max_wait} + + response = requests.post( + f"{coordinator_url}/v1/miners/poll", + headers=headers, + json=payload + ) + + if response.status_code == 200 and response.content: + job = response.json() + result = { + "action": "poll", + "miner_id": miner_id, + "status": "βœ… Job assigned", + "job_id": job.get("job_id"), + "payload": job.get("payload"), + "constraints": job.get("constraints"), + "assigned_at": time.strftime("%Y-%m-%d %H:%M:%S") + } + + if auto_execute: + result["auto_execution"] = "πŸ€– Job execution would start here" + result["execution_status"] = "Ready to execute" + + return result + elif response.status_code == 204: + return { + "action": "poll", + "miner_id": miner_id, + "status": "⏸️ No jobs available", + "message": "No jobs in queue" + } + else: + return {"action": "poll", "status": "❌ Poll failed", "error": response.text} + + except Exception as e: + return {"action": "poll", "status": f"❌ Error: {str(e)}"} + + +def submit_job_result( + job_id: str, + miner_id: str, + result: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + success: bool = True, + duration: Optional[int] = None, + result_file: Optional[str] = None +) -> Optional[Dict]: + """Submit job result""" + try: + headers = { + "X-Api-Key": api_key, + "X-Miner-ID": miner_id, + "Content-Type": "application/json" + } + + # Load result from file if specified + if result_file: + with open(result_file, 'r') as f: + result = f.read() + + payload = { + "result": result, + "success": success, + "metrics": { + "duration_ms": duration, + "completed_at": time.time() + } + } + + response = requests.post( + f"{coordinator_url}/v1/miners/{job_id}/result", + headers=headers, + json=payload + ) + + if response.status_code == 200: + return { + "action": "result", + "job_id": job_id, + "miner_id": miner_id, + "status": "βœ… Result submitted successfully", + "success": success, + "duration_ms": duration + } + else: + return {"action": "result", "status": "❌ Result submission failed", "error": response.text} + + except Exception as e: + return {"action": "result", "status": f"❌ Error: {str(e)}"} + + +def update_capabilities( + miner_id: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + capabilities: Optional[str] = None, + gpu_memory: Optional[int] = None, + models: Optional[list] = None, + pricing: Optional[float] = None, + concurrency: Optional[int] = None, + region: Optional[str] = None, + wallet: Optional[str] = None +) -> Optional[Dict]: + """Update miner capabilities""" + try: + headers = { + "X-Api-Key": api_key, + "X-Miner-ID": miner_id, + "Content-Type": "application/json" + } + + # Build capabilities from arguments + caps = {} + if gpu_memory: + caps["gpu_memory"] = gpu_memory + caps["gpu_memory_gb"] = gpu_memory + if models: + caps["models"] = models + caps["supported_models"] = models + if pricing: + caps["pricing_per_hour"] = pricing + caps["price_per_hour"] = pricing + + # Override with capabilities JSON if provided + if capabilities: + caps.update(json.loads(capabilities)) + + payload = { + "capabilities": caps, + "concurrency": concurrency, + "region": region + } + + if wallet: + payload["wallet_address"] = wallet + + response = requests.put( + f"{coordinator_url}/v1/miners/{miner_id}/capabilities", + headers=headers, + json=payload + ) + + if response.status_code == 200: + return { + "action": "update", + "miner_id": miner_id, + "status": "βœ… Capabilities updated successfully", + "updated_capabilities": caps + } + else: + return {"action": "update", "status": "❌ Update failed", "error": response.text} + + except Exception as e: + return {"action": "update", "status": f"❌ Error: {str(e)}"} + + +def check_earnings( + miner_id: str, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + period: str = "all" +) -> Optional[Dict]: + """Check miner earnings (placeholder for payment integration)""" + try: + # This would integrate with payment system when implemented + return { + "action": "earnings", + "miner_id": miner_id, + "period": period, + "status": "πŸ“Š Earnings calculation", + "total_earnings": 0.0, + "jobs_completed": 0, + "average_payment": 0.0, + "note": "Payment integration coming soon" + } + + except Exception as e: + return {"action": "earnings", "status": f"❌ Error: {str(e)}"} + + +def list_marketplace_offers( + miner_id: Optional[str] = None, + region: Optional[str] = None, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL +) -> Optional[Dict]: + """List marketplace offers""" + try: + admin_headers = {"X-Api-Key": api_key.replace("miner_", "admin_")} + + params = {} + if region: + params["region"] = region + + response = requests.get( + f"{coordinator_url}/v1/marketplace/miner-offers", + headers=admin_headers, + params=params + ) + + if response.status_code == 200: + offers = response.json() + + # Filter by miner if specified + if miner_id: + offers = [o for o in offers if miner_id in str(o).lower()] + + return { + "action": "marketplace_list", + "status": "βœ… Offers retrieved", + "offers": offers, + "count": len(offers), + "region_filter": region, + "miner_filter": miner_id + } + else: + return {"action": "marketplace_list", "status": "❌ Failed to get offers", "error": response.text} + + except Exception as e: + return {"action": "marketplace_list", "status": f"❌ Error: {str(e)}"} + + +def create_marketplace_offer( + miner_id: str, + price: float, + api_key: str = DEFAULT_API_KEY, + coordinator_url: str = DEFAULT_COORDINATOR_URL, + capacity: int = 1, + region: Optional[str] = None +) -> Optional[Dict]: + """Create marketplace offer""" + try: + admin_headers = {"X-Api-Key": api_key.replace("miner_", "admin_")} + + payload = { + "miner_id": miner_id, + "price": price, + "capacity": capacity, + "region": region + } + + response = requests.post( + f"{coordinator_url}/v1/marketplace/offers", + headers=admin_headers, + json=payload + ) + + if response.status_code == 200: + return { + "action": "marketplace_create", + "miner_id": miner_id, + "status": "βœ… Offer created successfully", + "price": price, + "capacity": capacity, + "region": region + } + else: + return {"action": "marketplace_create", "status": "❌ Offer creation failed", "error": response.text} + + except Exception as e: + return {"action": "marketplace_create", "status": f"❌ Error: {str(e)}"} + + +# Main function for CLI integration +def miner_cli_dispatcher(action: str, **kwargs) -> Optional[Dict]: + """Main dispatcher for miner management CLI commands""" + + actions = { + "register": register_miner, + "status": get_miner_status, + "heartbeat": send_heartbeat, + "poll": poll_jobs, + "result": submit_job_result, + "update": update_capabilities, + "earnings": check_earnings, + "marketplace_list": list_marketplace_offers, + "marketplace_create": create_marketplace_offer + } + + if action in actions: + return actions[action](**kwargs) + else: + return { + "action": action, + "status": f"❌ Unknown action. Available: {', '.join(actions.keys())}" + } + + +if __name__ == "__main__": + # Test the module + print("πŸš€ AITBC Miner Management Module") + print("Available functions:") + for func in [register_miner, get_miner_status, send_heartbeat, poll_jobs, + submit_job_result, update_capabilities, check_earnings, + list_marketplace_offers, create_marketplace_offer]: + print(f" - {func.__name__}")