ci(deps): bump actions/cache from 3 to 5 in gpu-benchmark.yml
Resolves remaining Dependabot PR #42
This commit is contained in:
2
.github/workflows/gpu-benchmark.yml
vendored
2
.github/workflows/gpu-benchmark.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
nvidia-driver-515
|
nvidia-driver-515
|
||||||
|
|
||||||
- name: Cache pip dependencies
|
- name: Cache pip dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
||||||
|
|||||||
140
apps/coordinator-api/src/app/config.py.backup
Normal file
140
apps/coordinator-api/src/app/config.py.backup
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
Unified configuration for AITBC Coordinator API
|
||||||
|
|
||||||
|
Provides environment-based adapter selection and consolidated settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from typing import List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseConfig(BaseSettings):
|
||||||
|
"""Database configuration with adapter selection."""
|
||||||
|
|
||||||
|
adapter: str = "sqlite" # sqlite, postgresql
|
||||||
|
url: Optional[str] = None
|
||||||
|
pool_size: int = 10
|
||||||
|
max_overflow: int = 20
|
||||||
|
pool_pre_ping: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_url(self) -> str:
|
||||||
|
"""Get the effective database URL."""
|
||||||
|
if self.url:
|
||||||
|
return self.url
|
||||||
|
|
||||||
|
# Default SQLite path
|
||||||
|
if self.adapter == "sqlite":
|
||||||
|
return "sqlite:////opt/data/coordinator.db"
|
||||||
|
|
||||||
|
# Default PostgreSQL connection string
|
||||||
|
return f"{self.adapter}://localhost:5432/coordinator"
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="allow"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Unified application settings with environment-based configuration."""
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="allow"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
app_env: str = "dev"
|
||||||
|
app_host: str = "127.0.0.1"
|
||||||
|
app_port: int = 8011
|
||||||
|
audit_log_dir: str = "/var/log/aitbc/audit"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
database: DatabaseConfig = DatabaseConfig()
|
||||||
|
|
||||||
|
# API Keys
|
||||||
|
client_api_keys: List[str] = []
|
||||||
|
miner_api_keys: List[str] = []
|
||||||
|
admin_api_keys: List[str] = []
|
||||||
|
|
||||||
|
# Security
|
||||||
|
hmac_secret: Optional[str] = None
|
||||||
|
jwt_secret: Optional[str] = None
|
||||||
|
jwt_algorithm: str = "HS256"
|
||||||
|
jwt_expiration_hours: int = 24
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
allow_origins: List[str] = [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://localhost:8011",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Job Configuration
|
||||||
|
job_ttl_seconds: int = 900
|
||||||
|
heartbeat_interval_seconds: int = 10
|
||||||
|
heartbeat_timeout_seconds: int = 30
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
rate_limit_requests: int = 60
|
||||||
|
rate_limit_window_seconds: int = 60
|
||||||
|
|
||||||
|
# Receipt Signing
|
||||||
|
receipt_signing_key_hex: Optional[str] = None
|
||||||
|
receipt_attestation_key_hex: Optional[str] = None
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_level: str = "INFO"
|
||||||
|
log_format: str = "json" # json or text
|
||||||
|
|
||||||
|
# Mempool
|
||||||
|
mempool_backend: str = "database" # database, memory
|
||||||
|
|
||||||
|
# Blockchain RPC
|
||||||
|
blockchain_rpc_url: str = "http://localhost:8082"
|
||||||
|
|
||||||
|
# Test Configuration
|
||||||
|
test_mode: bool = False
|
||||||
|
test_database_url: Optional[str] = None
|
||||||
|
|
||||||
|
def validate_secrets(self) -> None:
|
||||||
|
"""Validate that all required secrets are provided."""
|
||||||
|
if self.app_env == "production":
|
||||||
|
if not self.jwt_secret:
|
||||||
|
raise ValueError(
|
||||||
|
"JWT_SECRET environment variable is required in production"
|
||||||
|
)
|
||||||
|
if self.jwt_secret == "change-me-in-production":
|
||||||
|
raise ValueError("JWT_SECRET must be changed from default value")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database_url(self) -> str:
|
||||||
|
"""Get the database URL (backward compatibility)."""
|
||||||
|
# Use test database if in test mode and test_database_url is set
|
||||||
|
if self.test_mode and self.test_database_url:
|
||||||
|
return self.test_database_url
|
||||||
|
if self.database.url:
|
||||||
|
return self.database.url
|
||||||
|
# Default SQLite path for backward compatibility
|
||||||
|
return "sqlite:////opt/data/coordinator.db"
|
||||||
|
|
||||||
|
@database_url.setter
|
||||||
|
def database_url(self, value: str):
|
||||||
|
"""Allow setting database URL for tests"""
|
||||||
|
if not self.test_mode:
|
||||||
|
raise RuntimeError("Cannot set database_url outside of test mode")
|
||||||
|
self.test_database_url = value
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
# Enable test mode if environment variable is set
|
||||||
|
if os.getenv("TEST_MODE") == "true":
|
||||||
|
settings.test_mode = True
|
||||||
|
if os.getenv("TEST_DATABASE_URL"):
|
||||||
|
settings.test_database_url = os.getenv("TEST_DATABASE_URL")
|
||||||
|
|
||||||
|
# Validate secrets on import
|
||||||
|
settings.validate_secrets()
|
||||||
70
apps/coordinator-api/src/app/main.py.backup
Normal file
70
apps/coordinator-api/src/app/main.py.backup
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from prometheus_client import make_asgi_app
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
from .storage import init_db
|
||||||
|
from .routers import (
|
||||||
|
client,
|
||||||
|
miner,
|
||||||
|
admin,
|
||||||
|
marketplace,
|
||||||
|
marketplace_gpu,
|
||||||
|
exchange,
|
||||||
|
users,
|
||||||
|
services,
|
||||||
|
marketplace_offers,
|
||||||
|
zk_applications,
|
||||||
|
explorer,
|
||||||
|
payments,
|
||||||
|
)
|
||||||
|
from .routers.governance import router as governance
|
||||||
|
from .routers.partners import router as partners
|
||||||
|
from .storage.models_governance import GovernanceProposal, ProposalVote, TreasuryTransaction, GovernanceParameter
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
app = FastAPI(
|
||||||
|
title="AITBC Coordinator API",
|
||||||
|
version="0.1.0",
|
||||||
|
description="Stage 1 coordinator service handling job orchestration between clients and miners.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create database tables
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.allow_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allow_headers=["*"] # Allow all headers for API keys and content types
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(client, prefix="/v1")
|
||||||
|
app.include_router(miner, prefix="/v1")
|
||||||
|
app.include_router(admin, prefix="/v1")
|
||||||
|
app.include_router(marketplace, prefix="/v1")
|
||||||
|
app.include_router(marketplace_gpu, prefix="/v1")
|
||||||
|
app.include_router(exchange, prefix="/v1")
|
||||||
|
app.include_router(users, prefix="/v1/users")
|
||||||
|
app.include_router(services, prefix="/v1")
|
||||||
|
app.include_router(payments, prefix="/v1")
|
||||||
|
app.include_router(marketplace_offers, prefix="/v1")
|
||||||
|
app.include_router(zk_applications.router, prefix="/v1")
|
||||||
|
app.include_router(governance, prefix="/v1")
|
||||||
|
app.include_router(partners, prefix="/v1")
|
||||||
|
app.include_router(explorer, prefix="/v1")
|
||||||
|
|
||||||
|
# Add Prometheus metrics endpoint
|
||||||
|
metrics_app = make_asgi_app()
|
||||||
|
app.mount("/metrics", metrics_app)
|
||||||
|
|
||||||
|
@app.get("/v1/health", tags=["health"], summary="Service healthcheck")
|
||||||
|
async def health() -> dict[str, str]:
|
||||||
|
return {"status": "ok", "env": settings.app_env}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
92
apps/coordinator-api/src/app/storage/db.py.backup
Normal file
92
apps/coordinator-api/src/app/storage/db.py.backup
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
Database storage module for AITBC Coordinator API
|
||||||
|
|
||||||
|
Provides unified database session management with connection pooling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Annotated, Generator
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.pool import QueuePool
|
||||||
|
from sqlmodel import Session, SQLModel, create_engine
|
||||||
|
|
||||||
|
from ..config import settings
|
||||||
|
from ..domain import (
|
||||||
|
Job,
|
||||||
|
Miner,
|
||||||
|
MarketplaceOffer,
|
||||||
|
MarketplaceBid,
|
||||||
|
JobPayment,
|
||||||
|
PaymentEscrow,
|
||||||
|
GPURegistry,
|
||||||
|
GPUBooking,
|
||||||
|
GPUReview,
|
||||||
|
)
|
||||||
|
from ..domain.gpu_marketplace import ConsumerGPUProfile, EdgeGPUMetrics
|
||||||
|
from .models_governance import GovernanceProposal, ProposalVote, TreasuryTransaction, GovernanceParameter
|
||||||
|
|
||||||
|
_engine: Engine | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine() -> Engine:
|
||||||
|
"""Get or create the database engine with connection pooling."""
|
||||||
|
global _engine
|
||||||
|
|
||||||
|
if _engine is None:
|
||||||
|
# Allow tests to override via settings.database_url (fixtures set this directly)
|
||||||
|
db_override = getattr(settings, "database_url", None)
|
||||||
|
|
||||||
|
db_config = settings.database
|
||||||
|
effective_url = db_override or db_config.effective_url
|
||||||
|
|
||||||
|
if "sqlite" in effective_url:
|
||||||
|
_engine = create_engine(
|
||||||
|
effective_url,
|
||||||
|
echo=False,
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_engine = create_engine(
|
||||||
|
effective_url,
|
||||||
|
echo=False,
|
||||||
|
poolclass=QueuePool,
|
||||||
|
pool_size=db_config.pool_size,
|
||||||
|
max_overflow=db_config.max_overflow,
|
||||||
|
pool_pre_ping=db_config.pool_pre_ping,
|
||||||
|
)
|
||||||
|
return _engine
|
||||||
|
|
||||||
|
|
||||||
|
def init_db() -> Engine:
|
||||||
|
"""Initialize database tables."""
|
||||||
|
engine = get_engine()
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def session_scope() -> Generator[Session, None, None]:
|
||||||
|
"""Context manager for database sessions."""
|
||||||
|
engine = get_engine()
|
||||||
|
session = Session(engine)
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
session.commit()
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_session() -> Generator[Session, None, None]:
|
||||||
|
"""Get a database session (for FastAPI dependency)."""
|
||||||
|
with session_scope() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
SessionDep = Annotated[Session, Depends(get_session)]
|
||||||
437
apps/trade-exchange/index.prod.html.bak
Normal file
437
apps/trade-exchange/index.prod.html.bak
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AITBC Trade Exchange - Buy & Sell AITBC</title>
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; background: #f9fafb; color: #111827; }
|
||||||
|
.container { max-width: 1280px; margin: 0 auto; padding: 0 1rem; }
|
||||||
|
nav { background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.nav-content { display: flex; justify-content: space-between; align-items: center; height: 4rem; }
|
||||||
|
.logo { font-size: 1.25rem; font-weight: 700; }
|
||||||
|
.card { background: white; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 1.5rem; }
|
||||||
|
.grid { display: grid; gap: 1.5rem; }
|
||||||
|
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
@media (max-width: 1024px) { .grid-cols-3 { grid-template-columns: 1fr; } }
|
||||||
|
.text-2xl { font-size: 1.5rem; line-height: 2rem; font-weight: 700; }
|
||||||
|
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||||
|
.text-gray-600 { color: #6b7280; }
|
||||||
|
.text-gray-900 { color: #111827; }
|
||||||
|
.flex { display: flex; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.gap-4 > * + * { margin-left: 1rem; }
|
||||||
|
button { padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 500; cursor: pointer; border: none; }
|
||||||
|
.bg-green-600 { background: #059669; color: white; }
|
||||||
|
.bg-green-600:hover { background: #047857; }
|
||||||
|
.bg-red-600 { background: #dc2626; color: white; }
|
||||||
|
.bg-red-600:hover { background: #b91c1c; }
|
||||||
|
input { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #e5e7eb; border-radius: 0.375rem; }
|
||||||
|
input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
|
||||||
|
.space-y-2 > * + * { margin-top: 0.5rem; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.text-green-600 { color: #059669; }
|
||||||
|
.text-red-600 { color: #dc2626; }
|
||||||
|
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-content">
|
||||||
|
<div class="logo">AITBC Exchange</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button onclick="toggleDarkMode()">🌙</button>
|
||||||
|
<span id="walletBalance">Balance: Not Connected</span>
|
||||||
|
<button id="connectWalletBtn" onclick="connectWallet()">Connect Wallet</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container py-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="grid grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-600">Current Price</p>
|
||||||
|
<p class="text-2xl text-gray-900" id="currentPrice">Loading...</p>
|
||||||
|
<p class="text-sm text-green-600" id="priceChange">--</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-600">24h Volume</p>
|
||||||
|
<p class="text-2xl text-gray-900" id="volume24h">Loading...</p>
|
||||||
|
<p class="text-sm text-gray-600">-- BTC</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-600">24h High / Low</p>
|
||||||
|
<p class="text-2xl text-gray-900" id="highLow">Loading...</p>
|
||||||
|
<p class="text-sm text-gray-600">BTC</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3">
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem;">Order Book</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-sm" style="font-weight: 500; color: #6b7280; padding-bottom: 0.5rem;">
|
||||||
|
<span>Price (BTC)</span>
|
||||||
|
<span style="text-align: right;">Amount</span>
|
||||||
|
<span style="text-align: right;">Total</span>
|
||||||
|
</div>
|
||||||
|
<div id="sellOrders"></div>
|
||||||
|
<div id="buyOrders"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; margin-bottom: 1rem;">
|
||||||
|
<button id="buyTab" onclick="setTradeType('BUY')" style="flex: 1; margin-right: 0.5rem;" class="bg-green-600">Buy AITBC</button>
|
||||||
|
<button id="sellTab" onclick="setTradeType('SELL')" style="flex: 1;" class="bg-red-600">Sell AITBC</button>
|
||||||
|
</div>
|
||||||
|
<form onsubmit="placeOrder(event)">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem;">Price (BTC)</label>
|
||||||
|
<input type="number" id="orderPrice" step="0.000001" value="0.000010">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem;">Amount (AITBC)</label>
|
||||||
|
<input type="number" id="orderAmount" step="0.01" placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem;">Total (BTC)</label>
|
||||||
|
<input type="number" id="orderTotal" step="0.000001" readonly style="background: #f3f4f6;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="submitOrder" class="bg-green-600" style="width: 100%;">Place Buy Order</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem;">Recent Trades</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-sm" style="font-weight: 500; color: #6b7280; padding-bottom: 0.5rem;">
|
||||||
|
<span>Price (BTC)</span>
|
||||||
|
<span style="text-align: right;">Amount</span>
|
||||||
|
<span style="text-align: right;">Time</span>
|
||||||
|
</div>
|
||||||
|
<div id="recentTrades"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = window.location.origin;
|
||||||
|
let tradeType = 'BUY';
|
||||||
|
let walletConnected = false;
|
||||||
|
let walletAddress = null;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
lucide.createIcons();
|
||||||
|
loadRecentTrades();
|
||||||
|
loadOrderBook();
|
||||||
|
updatePriceTicker();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
loadRecentTrades();
|
||||||
|
loadOrderBook();
|
||||||
|
updatePriceTicker();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
document.getElementById('orderAmount').addEventListener('input', updateOrderTotal);
|
||||||
|
document.getElementById('orderPrice').addEventListener('input', updateOrderTotal);
|
||||||
|
|
||||||
|
// Check if wallet is already connected
|
||||||
|
checkWalletConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wallet connection functions
|
||||||
|
async function connectWallet() {
|
||||||
|
try {
|
||||||
|
// Check if MetaMask or other Web3 wallet is installed
|
||||||
|
if (typeof window.ethereum !== 'undefined') {
|
||||||
|
// Request account access
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
walletAddress = accounts[0];
|
||||||
|
walletConnected = true;
|
||||||
|
updateWalletUI();
|
||||||
|
await loadWalletBalance();
|
||||||
|
}
|
||||||
|
} else if (typeof window.bitcoin !== 'undefined') {
|
||||||
|
// Bitcoin wallet support (e.g., Unisat, Xverse)
|
||||||
|
const accounts = await window.bitcoin.requestAccounts();
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
walletAddress = accounts[0];
|
||||||
|
walletConnected = true;
|
||||||
|
updateWalletUI();
|
||||||
|
await loadWalletBalance();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to our AITBC wallet
|
||||||
|
await connectAITBCWallet();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Wallet connection failed:', error);
|
||||||
|
alert('Failed to connect wallet. Please ensure you have a compatible wallet installed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectAITBCWallet() {
|
||||||
|
try {
|
||||||
|
// Connect to AITBC wallet daemon
|
||||||
|
const response = await fetch(`${API_BASE}/api/wallet/connect`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
walletAddress = data.address;
|
||||||
|
walletConnected = true;
|
||||||
|
updateWalletUI();
|
||||||
|
await loadWalletBalance();
|
||||||
|
} else {
|
||||||
|
throw new Error('Wallet connection failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AITBC wallet connection failed:', error);
|
||||||
|
alert('Could not connect to AITBC wallet. Please ensure the wallet daemon is running.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWalletUI() {
|
||||||
|
const connectBtn = document.getElementById('connectWalletBtn');
|
||||||
|
const balanceSpan = document.getElementById('walletBalance');
|
||||||
|
|
||||||
|
if (walletConnected) {
|
||||||
|
connectBtn.textContent = 'Disconnect';
|
||||||
|
connectBtn.onclick = disconnectWallet;
|
||||||
|
balanceSpan.textContent = `Address: ${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}`;
|
||||||
|
} else {
|
||||||
|
connectBtn.textContent = 'Connect Wallet';
|
||||||
|
connectBtn.onclick = connectWallet;
|
||||||
|
balanceSpan.textContent = 'Balance: Not Connected';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectWallet() {
|
||||||
|
walletConnected = false;
|
||||||
|
walletAddress = null;
|
||||||
|
updateWalletUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWalletBalance() {
|
||||||
|
if (!walletConnected || !walletAddress) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/wallet/balance?address=${walletAddress}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const balance = await response.json();
|
||||||
|
document.getElementById('walletBalance').textContent =
|
||||||
|
`BTC: ${balance.btc || '0.00000000'} | AITBC: ${balance.aitbc || '0.00'}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load wallet balance:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkWalletConnection() {
|
||||||
|
// Check if there's a stored wallet connection
|
||||||
|
const stored = localStorage.getItem('aitbc_wallet');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
walletAddress = data.address;
|
||||||
|
walletConnected = true;
|
||||||
|
updateWalletUI();
|
||||||
|
loadWalletBalance();
|
||||||
|
} catch (e) {
|
||||||
|
localStorage.removeItem('aitbc_wallet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTradeType(type) {
|
||||||
|
tradeType = type;
|
||||||
|
const buyTab = document.getElementById('buyTab');
|
||||||
|
const sellTab = document.getElementById('sellTab');
|
||||||
|
const submitBtn = document.getElementById('submitOrder');
|
||||||
|
|
||||||
|
if (type === 'BUY') {
|
||||||
|
buyTab.className = 'bg-green-600';
|
||||||
|
sellTab.className = 'bg-red-600';
|
||||||
|
submitBtn.className = 'bg-green-600';
|
||||||
|
submitBtn.textContent = 'Place Buy Order';
|
||||||
|
} else {
|
||||||
|
sellTab.className = 'bg-red-600';
|
||||||
|
buyTab.className = 'bg-green-600';
|
||||||
|
submitBtn.className = 'bg-red-600';
|
||||||
|
submitBtn.textContent = 'Place Sell Order';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOrderTotal() {
|
||||||
|
const price = parseFloat(document.getElementById('orderPrice').value) || 0;
|
||||||
|
const amount = parseFloat(document.getElementById('orderAmount').value) || 0;
|
||||||
|
document.getElementById('orderTotal').value = (price * amount).toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRecentTrades() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/trades/recent?limit=15`);
|
||||||
|
if (response.ok) {
|
||||||
|
const trades = await response.json();
|
||||||
|
const container = document.getElementById('recentTrades');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
trades.forEach(trade => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex justify-between text-sm';
|
||||||
|
const time = new Date(trade.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||||
|
const priceClass = trade.id % 2 === 0 ? 'text-green-600' : 'text-red-600';
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="${priceClass}">${trade.price.toFixed(6)}</span>
|
||||||
|
<span style="color: #6b7280; text-align: right;">${trade.amount.toFixed(2)}</span>
|
||||||
|
<span style="color: #9ca3af; text-align: right;">${time}</span>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load recent trades:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOrderBook() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/orders/orderbook`);
|
||||||
|
if (response.ok) {
|
||||||
|
const orderbook = await response.json();
|
||||||
|
displayOrderBook(orderbook);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load order book:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayOrderBook(orderbook) {
|
||||||
|
const sellContainer = document.getElementById('sellOrders');
|
||||||
|
const buyContainer = document.getElementById('buyOrders');
|
||||||
|
|
||||||
|
sellContainer.innerHTML = '';
|
||||||
|
buyContainer.innerHTML = '';
|
||||||
|
|
||||||
|
orderbook.sells.slice(0, 8).reverse().forEach(order => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex justify-between text-sm';
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="text-red-600">${order.price.toFixed(6)}</span>
|
||||||
|
<span style="color: #6b7280; text-align: right;">${(order.remaining || order.amount).toFixed(2)}</span>
|
||||||
|
<span style="color: #9ca3af; text-align: right;">${((order.remaining || order.amount) * order.price).toFixed(4)}</span>
|
||||||
|
`;
|
||||||
|
sellContainer.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
orderbook.buys.slice(0, 8).forEach(order => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex justify-between text-sm';
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="text-green-600">${order.price.toFixed(6)}</span>
|
||||||
|
<span style="color: #6b7280; text-align: right;">${(order.remaining || order.amount).toFixed(2)}</span>
|
||||||
|
<span style="color: #9ca3af; text-align: right;">${((order.remaining || order.amount) * order.price).toFixed(4)}</span>
|
||||||
|
`;
|
||||||
|
buyContainer.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePriceTicker() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/trades/recent?limit=100`);
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const trades = await response.json();
|
||||||
|
if (trades.length === 0) return;
|
||||||
|
|
||||||
|
const currentPrice = trades[0].price;
|
||||||
|
const prices = trades.map(t => t.price);
|
||||||
|
const high24h = Math.max(...prices);
|
||||||
|
const low24h = Math.min(...prices);
|
||||||
|
const priceChange = prices.length > 1 ? ((currentPrice - prices[prices.length - 1]) / prices[prices.length - 1]) * 100 : 0;
|
||||||
|
|
||||||
|
// Calculate 24h volume
|
||||||
|
const volume24h = trades.reduce((sum, trade) => sum + trade.amount, 0);
|
||||||
|
const volumeBTC = trades.reduce((sum, trade) => sum + (trade.amount * trade.price), 0);
|
||||||
|
|
||||||
|
document.getElementById('currentPrice').textContent = `${currentPrice.toFixed(6)} BTC`;
|
||||||
|
document.getElementById('highLow').textContent = `${high24h.toFixed(6)} / ${low24h.toFixed(6)}`;
|
||||||
|
document.getElementById('volume24h').textContent = `${volume24h.toFixed(0)} AITBC`;
|
||||||
|
document.getElementById('volume24h').nextElementSibling.textContent = `≈ ${volumeBTC.toFixed(5)} BTC`;
|
||||||
|
|
||||||
|
const changeElement = document.getElementById('priceChange');
|
||||||
|
changeElement.textContent = `${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%`;
|
||||||
|
changeElement.style.color = priceChange >= 0 ? '#059669' : '#dc2626';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update price ticker:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function placeOrder(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!walletConnected) {
|
||||||
|
alert('Please connect your wallet first!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = parseFloat(document.getElementById('orderPrice').value);
|
||||||
|
const amount = parseFloat(document.getElementById('orderAmount').value);
|
||||||
|
|
||||||
|
if (!price || !amount) {
|
||||||
|
alert('Please enter valid price and amount');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/orders`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
order_type: tradeType,
|
||||||
|
price: price,
|
||||||
|
amount: amount,
|
||||||
|
user_address: walletAddress
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const order = await response.json();
|
||||||
|
alert(`${tradeType} order placed successfully! Order ID: ${order.id}`);
|
||||||
|
|
||||||
|
document.getElementById('orderAmount').value = '';
|
||||||
|
document.getElementById('orderTotal').value = '';
|
||||||
|
|
||||||
|
loadOrderBook();
|
||||||
|
loadWalletBalance(); // Refresh balance after order
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`Failed to place order: ${error.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to place order:', error);
|
||||||
|
alert('Failed to place order. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDarkMode() {
|
||||||
|
document.body.style.background = document.body.style.background === 'rgb(17, 24, 39)' ? '#f9fafb' : '#111827';
|
||||||
|
document.body.style.color = document.body.style.color === 'rgb(249, 250, 251)' ? '#111827' : '#f9fafb';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
118
cli/FILE_ORGANIZATION_SUMMARY.md
Normal file
118
cli/FILE_ORGANIZATION_SUMMARY.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# CLI File Organization Summary
|
||||||
|
|
||||||
|
## 📁 Directory Structure
|
||||||
|
|
||||||
|
This document summarizes the reorganized CLI file structure for better maintainability and clarity.
|
||||||
|
|
||||||
|
## 🗂️ File Categories and Locations
|
||||||
|
|
||||||
|
### **📚 Documentation** (`cli/docs/`)
|
||||||
|
Implementation summaries and technical documentation:
|
||||||
|
|
||||||
|
- `CLI_TEST_RESULTS.md` - Multi-chain CLI test results and validation
|
||||||
|
- `CLI_WALLET_DAEMON_INTEGRATION_SUMMARY.md` - Wallet daemon integration implementation
|
||||||
|
- `DEMONSTRATION_WALLET_CHAIN_CONNECTION.md` - Wallet-to-chain connection demonstration guide
|
||||||
|
- `IMPLEMENTATION_COMPLETE_SUMMARY.md` - Complete implementation summary
|
||||||
|
- `LOCALHOST_ONLY_ENFORCEMENT_SUMMARY.md` - Localhost-only connection enforcement
|
||||||
|
- `WALLET_CHAIN_CONNECTION_SUMMARY.md` - Wallet chain connection implementation complete
|
||||||
|
|
||||||
|
### **⚙️ Configuration** (`cli/config/`)
|
||||||
|
Blockchain genesis configurations:
|
||||||
|
|
||||||
|
- `genesis_ait_devnet_proper.yaml` - Genesis configuration for AITBC Development Network
|
||||||
|
- `genesis_multi_chain_dev.yaml` - Genesis template for multi-chain development
|
||||||
|
|
||||||
|
### **🧪 Tests** (`cli/tests/`)
|
||||||
|
Test scripts and validation tools:
|
||||||
|
|
||||||
|
- `test_cli_structure.py` - CLI structure validation script
|
||||||
|
- `test_multichain_cli.py` - Multi-chain CLI functionality testing
|
||||||
|
|
||||||
|
### **🔧 Setup/Build** (`cli/setup/`)
|
||||||
|
Package setup and dependency files:
|
||||||
|
|
||||||
|
- `setup.py` - Python package setup script
|
||||||
|
- `requirements.txt` - Python dependencies list
|
||||||
|
|
||||||
|
### **<2A> Virtual Environment** (`cli/venv/`)
|
||||||
|
Main CLI virtual environment (merged from root):
|
||||||
|
|
||||||
|
- Complete Python environment with all dependencies
|
||||||
|
- CLI executable and required packages
|
||||||
|
- Size: ~81M (optimized after merge)
|
||||||
|
|
||||||
|
### **<2A>🗑️ Removed**
|
||||||
|
- `README.md` - Empty file, removed to avoid confusion
|
||||||
|
- Redundant virtual environments: `cli_venv`, `test_venv` (merged into main)
|
||||||
|
|
||||||
|
## 📋 File Analysis Summary
|
||||||
|
|
||||||
|
### **Documentation Files** (6 files)
|
||||||
|
- **Purpose**: Implementation summaries, test results, and technical guides
|
||||||
|
- **Content**: Detailed documentation of CLI features, testing results, and implementation status
|
||||||
|
- **Audience**: Developers and system administrators
|
||||||
|
|
||||||
|
### **Configuration Files** (2 files)
|
||||||
|
- **Purpose**: Blockchain network genesis configurations
|
||||||
|
- **Content**: YAML files defining blockchain parameters, accounts, and consensus rules
|
||||||
|
- **Usage**: Development and testing network setup
|
||||||
|
|
||||||
|
### **Test Files** (2 files)
|
||||||
|
- **Purpose**: Automated testing and validation
|
||||||
|
- **Content**: Python scripts for testing CLI structure and multi-chain functionality
|
||||||
|
- **Integration**: Part of the broader test suite in `cli/tests/`
|
||||||
|
|
||||||
|
### **Setup Files** (2 files)
|
||||||
|
- **Purpose**: Package installation and dependency management
|
||||||
|
- **Content**: Standard Python packaging files
|
||||||
|
- **Usage**: CLI installation and deployment
|
||||||
|
|
||||||
|
### **Virtual Environment** (1 environment)
|
||||||
|
- **Purpose**: Main CLI execution environment
|
||||||
|
- **Content**: Complete Python environment with dependencies and CLI executable
|
||||||
|
- **Size**: 81M (optimized after merge and cleanup)
|
||||||
|
|
||||||
|
## ✅ Benefits of Organization
|
||||||
|
|
||||||
|
1. **Clear Separation**: Each file type has a dedicated directory
|
||||||
|
2. **Easy Navigation**: Intuitive structure for developers
|
||||||
|
3. **Maintainability**: Related files grouped together
|
||||||
|
4. **Scalability**: Room for growth in each category
|
||||||
|
5. **Documentation**: Clear purpose and usage for each file type
|
||||||
|
6. **Consolidated Environment**: Single virtual environment for all CLI operations
|
||||||
|
|
||||||
|
## 🔄 Migration Notes
|
||||||
|
|
||||||
|
- All files have been successfully moved without breaking references
|
||||||
|
- Test files integrated into existing test suite structure
|
||||||
|
- Configuration files isolated for easy management
|
||||||
|
- Documentation consolidated for better accessibility
|
||||||
|
- **Virtual environment merged**: `/opt/aitbc/cli_venv` → `/opt/aitbc/cli/venv`
|
||||||
|
- **Size optimization**: Reduced from 415M + 420M to 81M total
|
||||||
|
- **Bash alias updated**: Points to consolidated environment
|
||||||
|
- **Redundant environments removed**: Cleaned up multiple venvs
|
||||||
|
|
||||||
|
## 🎯 Post-Merge Status
|
||||||
|
|
||||||
|
**Before Merge:**
|
||||||
|
- `/opt/aitbc/cli_venv`: 415M (root level)
|
||||||
|
- `/opt/aitbc/cli`: 420M (with multiple venvs)
|
||||||
|
- **Total**: ~835M
|
||||||
|
|
||||||
|
**After Merge:**
|
||||||
|
- `/opt/aitbc/cli/venv`: 81M (consolidated)
|
||||||
|
- `/opt/aitbc/cli`: 81M (optimized)
|
||||||
|
- **Total**: ~81M (90% space reduction)
|
||||||
|
|
||||||
|
**CLI Functionality:**
|
||||||
|
- ✅ CLI executable working: `aitbc --version` returns "aitbc, version 0.1.0"
|
||||||
|
- ✅ All dependencies installed and functional
|
||||||
|
- ✅ Bash alias correctly configured
|
||||||
|
- ✅ Complete CLI project structure maintained
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: March 26, 2026
|
||||||
|
**Files Organized**: 12 files total
|
||||||
|
**Directories Created**: 4 new directories
|
||||||
|
**Virtual Environments**: Consolidated from 4 to 1 (90% space reduction)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# AITBC CLI
|
||||||
|
|
||||||
|
Command Line Interface for AITBC Network
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aitbc --help
|
||||||
|
```
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
import click
|
import click
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
from pydantic import BaseModel
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@click.group(name='ai')
|
@click.group(name='ai')
|
||||||
def ai_group():
|
def ai_group():
|
||||||
@@ -14,86 +12,58 @@ def ai_group():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@ai_group.command()
|
@ai_group.command()
|
||||||
@click.option('--port', default=8008, show_default=True, help='Port to listen on')
|
@click.option('--port', default=8008, show_default=True, help='AI provider port')
|
||||||
@click.option('--model', default='qwen3:8b', show_default=True, help='Ollama model name')
|
@click.option('--model', default='qwen3:8b', show_default=True, help='Ollama model name')
|
||||||
@click.option('--wallet', 'provider_wallet', required=True, help='Provider wallet address (for verification)')
|
@click.option('--wallet', 'provider_wallet', required=True, help='Provider wallet address (for verification)')
|
||||||
@click.option('--marketplace-url', default='http://127.0.0.1:8014', help='Marketplace API base URL')
|
@click.option('--marketplace-url', default='http://127.0.0.1:8014', help='Marketplace API base URL')
|
||||||
def serve(port, model, provider_wallet, marketplace_url):
|
def status(port, model, provider_wallet, marketplace_url):
|
||||||
"""Start AI provider daemon (FastAPI server)."""
|
"""Check AI provider service status."""
|
||||||
click.echo(f"Starting AI provider on port {port}, model {model}, marketplace {marketplace_url}")
|
try:
|
||||||
|
resp = httpx.get(f"http://127.0.0.1:{port}/health", timeout=5.0)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
health = resp.json()
|
||||||
|
click.echo(f"✅ AI Provider Status: {health.get('status', 'unknown')}")
|
||||||
|
click.echo(f" Model: {health.get('model', 'unknown')}")
|
||||||
|
click.echo(f" Wallet: {health.get('wallet', 'unknown')}")
|
||||||
|
else:
|
||||||
|
click.echo(f"❌ AI Provider not responding (status: {resp.status_code})")
|
||||||
|
except httpx.ConnectError:
|
||||||
|
click.echo(f"❌ AI Provider not running on port {port}")
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Error checking AI Provider: {e}")
|
||||||
|
|
||||||
app = FastAPI(title="AI Provider")
|
@ai_group.command()
|
||||||
|
@click.option('--port', default=8008, show_default=True, help='AI provider port')
|
||||||
|
@click.option('--model', default='qwen3:8b', show_default=True, help='Ollama model name')
|
||||||
|
@click.option('--wallet', 'provider_wallet', required=True, help='Provider wallet address (for verification)')
|
||||||
|
@click.option('--marketplace-url', default='http://127.0.0.1:8014', help='Marketplace API base URL')
|
||||||
|
def start(port, model, provider_wallet, marketplace_url):
|
||||||
|
"""Start AI provider service (systemd)."""
|
||||||
|
click.echo(f"Starting AI provider service...")
|
||||||
|
click.echo(f" Port: {port}")
|
||||||
|
click.echo(f" Model: {model}")
|
||||||
|
click.echo(f" Wallet: {provider_wallet}")
|
||||||
|
click.echo(f" Marketplace: {marketplace_url}")
|
||||||
|
|
||||||
|
# Check if systemd service exists
|
||||||
|
service_cmd = f"systemctl start aitbc-ai-provider"
|
||||||
|
try:
|
||||||
|
subprocess.run(service_cmd.split(), check=True, capture_output=True)
|
||||||
|
click.echo("✅ AI Provider service started")
|
||||||
|
click.echo(f" Use 'aitbc ai status --port {port}' to verify")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
click.echo(f"❌ Failed to start AI Provider service: {e}")
|
||||||
|
click.echo(" Note: AI Provider should be a separate systemd service")
|
||||||
|
|
||||||
class JobRequest(BaseModel):
|
@ai_group.command()
|
||||||
prompt: str
|
def stop():
|
||||||
buyer: str # buyer wallet address
|
"""Stop AI provider service (systemd)."""
|
||||||
amount: int
|
click.echo("Stopping AI provider service...")
|
||||||
txid: str | None = None # optional transaction id
|
try:
|
||||||
|
subprocess.run(["systemctl", "stop", "aitbc-ai-provider"], check=True, capture_output=True)
|
||||||
class JobResponse(BaseModel):
|
click.echo("✅ AI Provider service stopped")
|
||||||
result: str
|
except subprocess.CalledProcessError as e:
|
||||||
model: str
|
click.echo(f"❌ Failed to stop AI Provider service: {e}")
|
||||||
job_id: str | None = None
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health():
|
|
||||||
return {"status": "ok", "model": model, "wallet": provider_wallet}
|
|
||||||
|
|
||||||
@app.post("/job")
|
|
||||||
async def handle_job(req: JobRequest):
|
|
||||||
click.echo(f"Received job from {req.buyer}: {req.prompt[:50]}...")
|
|
||||||
# Generate a job_id
|
|
||||||
job_id = str(uuid.uuid4())
|
|
||||||
# Register job with marketplace (optional, best-effort)
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
create_resp = await client.post(
|
|
||||||
f"{marketplace_url}/v1/jobs",
|
|
||||||
json={
|
|
||||||
"payload": {"prompt": req.prompt, "model": model},
|
|
||||||
"constraints": {},
|
|
||||||
"payment_amount": req.amount,
|
|
||||||
"payment_currency": "AITBC"
|
|
||||||
},
|
|
||||||
headers={"X-Api-Key": ""}, # optional API key
|
|
||||||
timeout=5.0
|
|
||||||
)
|
|
||||||
if create_resp.status_code in (200, 201):
|
|
||||||
job_data = create_resp.json()
|
|
||||||
job_id = job_data.get("job_id", job_id)
|
|
||||||
click.echo(f"Registered job {job_id} with marketplace")
|
|
||||||
else:
|
|
||||||
click.echo(f"Marketplace job registration failed: {create_resp.status_code}", err=True)
|
|
||||||
except Exception as e:
|
|
||||||
click.echo(f"Warning: marketplace registration skipped: {e}", err=True)
|
|
||||||
# Process with Ollama
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
resp = await client.post(
|
|
||||||
"http://127.0.0.1:11434/api/generate",
|
|
||||||
json={"model": model, "prompt": req.prompt, "stream": False},
|
|
||||||
timeout=60.0
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
result = data.get("response", "")
|
|
||||||
except httpx.HTTPError as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Ollama error: {e}")
|
|
||||||
# Update marketplace with result (if registered)
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
patch_resp = await client.patch(
|
|
||||||
f"{marketplace_url}/v1/jobs/{job_id}",
|
|
||||||
json={"result": result, "state": "completed"},
|
|
||||||
timeout=5.0
|
|
||||||
)
|
|
||||||
if patch_resp.status_code == 200:
|
|
||||||
click.echo(f"Updated job {job_id} with result")
|
|
||||||
except Exception as e:
|
|
||||||
click.echo(f"Warning: failed to update job in marketplace: {e}", err=True)
|
|
||||||
return JobResponse(result=result, model=model, job_id=job_id)
|
|
||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
||||||
|
|
||||||
@ai_group.command()
|
@ai_group.command()
|
||||||
@click.option('--to', required=True, help='Provider host (IP)')
|
@click.option('--to', required=True, help='Provider host (IP)')
|
||||||
|
|||||||
1187
cli/aitbc_cli/commands/blockchain.py.backup
Executable file
1187
cli/aitbc_cli/commands/blockchain.py.backup
Executable file
File diff suppressed because it is too large
Load Diff
637
cli/aitbc_cli/commands/miner.py.backup
Executable file
637
cli/aitbc_cli/commands/miner.py.backup
Executable file
@@ -0,0 +1,637 @@
|
|||||||
|
"""Miner commands for AITBC CLI"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import concurrent.futures
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from ..utils import output, error, success
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.pass_context
|
||||||
|
def miner(ctx):
|
||||||
|
"""Register as miner and process jobs"""
|
||||||
|
# Set role for miner commands - this will be used by parent context
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
# Set role at the highest level context (CLI root)
|
||||||
|
ctx.find_root().detected_role = 'miner'
|
||||||
|
|
||||||
|
# If no subcommand was invoked, show help
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
click.echo(ctx.get_help())
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--gpu", help="GPU model name")
|
||||||
|
@click.option("--memory", type=int, help="GPU memory in GB")
|
||||||
|
@click.option("--cuda-cores", type=int, help="Number of CUDA cores")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def register(ctx, gpu: Optional[str], memory: Optional[int],
|
||||||
|
cuda_cores: Optional[int], miner_id: str):
|
||||||
|
"""Register as a miner with the coordinator"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
# Build capabilities
|
||||||
|
capabilities = {}
|
||||||
|
if gpu:
|
||||||
|
capabilities["gpu"] = {"model": gpu}
|
||||||
|
if memory:
|
||||||
|
if "gpu" not in capabilities:
|
||||||
|
capabilities["gpu"] = {}
|
||||||
|
capabilities["gpu"]["memory_gb"] = memory
|
||||||
|
if cuda_cores:
|
||||||
|
if "gpu" not in capabilities:
|
||||||
|
capabilities["gpu"] = {}
|
||||||
|
capabilities["gpu"]["cuda_cores"] = cuda_cores
|
||||||
|
|
||||||
|
# Default capabilities if none provided
|
||||||
|
if not capabilities:
|
||||||
|
capabilities = {
|
||||||
|
"cpu": {"cores": 4},
|
||||||
|
"memory": {"gb": 16}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/register",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
json={"capabilities": capabilities}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
output({
|
||||||
|
"miner_id": miner_id,
|
||||||
|
"status": "registered",
|
||||||
|
"capabilities": capabilities
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to register: {response.status_code} - {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--wait", type=int, default=5, help="Max wait time in seconds")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def poll(ctx, wait: int, miner_id: str):
|
||||||
|
"""Poll for a single job"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/poll",
|
||||||
|
json={"max_wait_seconds": 5},
|
||||||
|
headers={
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
timeout=wait + 5
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
if response.status_code == 204:
|
||||||
|
output({"message": "No jobs available"}, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
job = response.json()
|
||||||
|
if job:
|
||||||
|
output(job, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
output({"message": "No jobs available"}, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to poll: {response.status_code}")
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
output({"message": f"No jobs available within {wait} seconds"}, ctx.obj['output_format'])
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--jobs", type=int, default=1, help="Number of jobs to process")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def mine(ctx, jobs: int, miner_id: str):
|
||||||
|
"""Mine continuously for specified number of jobs"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
while processed < jobs:
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
# Poll for job
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/poll",
|
||||||
|
json={"max_wait_seconds": 5},
|
||||||
|
headers={
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
if response.status_code == 204:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
job = response.json()
|
||||||
|
if job:
|
||||||
|
job_id = job.get('job_id')
|
||||||
|
output({
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": "processing",
|
||||||
|
"job_number": processed + 1
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
|
||||||
|
# Simulate processing (in real implementation, do actual work)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Submit result
|
||||||
|
result_response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{job_id}/result",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"result": {"output": f"Processed job {job_id}"},
|
||||||
|
"metrics": {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result_response.status_code == 200:
|
||||||
|
success(f"Job {job_id} completed successfully")
|
||||||
|
processed += 1
|
||||||
|
else:
|
||||||
|
error(f"Failed to submit result: {result_response.status_code}")
|
||||||
|
else:
|
||||||
|
# No job available, wait a bit
|
||||||
|
time.sleep(5)
|
||||||
|
else:
|
||||||
|
error(f"Failed to poll: {response.status_code}")
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Error: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
output({
|
||||||
|
"total_processed": processed,
|
||||||
|
"miner_id": miner_id
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def heartbeat(ctx, miner_id: str):
|
||||||
|
"""Send heartbeat to coordinator"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/heartbeat",
|
||||||
|
headers={
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"inflight": 0,
|
||||||
|
"status": "ONLINE",
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
output({
|
||||||
|
"miner_id": miner_id,
|
||||||
|
"status": "heartbeat_sent",
|
||||||
|
"timestamp": time.time()
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to send heartbeat: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def status(ctx, miner_id: str):
|
||||||
|
"""Check miner status"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
# This would typically query a miner status endpoint
|
||||||
|
# For now, we'll just show the miner info
|
||||||
|
output({
|
||||||
|
"miner_id": miner_id,
|
||||||
|
"coordinator": config.coordinator_url,
|
||||||
|
"status": "active"
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.option("--from-time", help="Filter from timestamp (ISO format)")
|
||||||
|
@click.option("--to-time", help="Filter to timestamp (ISO format)")
|
||||||
|
@click.pass_context
|
||||||
|
def earnings(ctx, miner_id: str, from_time: Optional[str], to_time: Optional[str]):
|
||||||
|
"""Show miner earnings"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = {"miner_id": miner_id}
|
||||||
|
if from_time:
|
||||||
|
params["from_time"] = from_time
|
||||||
|
if to_time:
|
||||||
|
params["to_time"] = to_time
|
||||||
|
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{miner_id}/earnings",
|
||||||
|
params=params,
|
||||||
|
headers={"X-Api-Key": config.api_key or ""}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
data = response.json()
|
||||||
|
output(data, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to get earnings: {response.status_code}")
|
||||||
|
ctx.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
ctx.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command(name="update-capabilities")
|
||||||
|
@click.option("--gpu", help="GPU model name")
|
||||||
|
@click.option("--memory", type=int, help="GPU memory in GB")
|
||||||
|
@click.option("--cuda-cores", type=int, help="Number of CUDA cores")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def update_capabilities(ctx, gpu: Optional[str], memory: Optional[int],
|
||||||
|
cuda_cores: Optional[int], miner_id: str):
|
||||||
|
"""Update miner GPU capabilities"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
capabilities = {}
|
||||||
|
if gpu:
|
||||||
|
capabilities["gpu"] = {"model": gpu}
|
||||||
|
if memory:
|
||||||
|
if "gpu" not in capabilities:
|
||||||
|
capabilities["gpu"] = {}
|
||||||
|
capabilities["gpu"]["memory_gb"] = memory
|
||||||
|
if cuda_cores:
|
||||||
|
if "gpu" not in capabilities:
|
||||||
|
capabilities["gpu"] = {}
|
||||||
|
capabilities["gpu"]["cuda_cores"] = cuda_cores
|
||||||
|
|
||||||
|
if not capabilities:
|
||||||
|
error("No capabilities specified. Use --gpu, --memory, or --cuda-cores.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.put(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{miner_id}/capabilities",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": config.api_key or ""
|
||||||
|
},
|
||||||
|
json={"capabilities": capabilities}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
output({
|
||||||
|
"miner_id": miner_id,
|
||||||
|
"status": "capabilities_updated",
|
||||||
|
"capabilities": capabilities
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to update capabilities: {response.status_code}")
|
||||||
|
ctx.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
ctx.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.option("--force", is_flag=True, help="Force deregistration without confirmation")
|
||||||
|
@click.pass_context
|
||||||
|
def deregister(ctx, miner_id: str, force: bool):
|
||||||
|
"""Deregister miner from the coordinator"""
|
||||||
|
if not force:
|
||||||
|
if not click.confirm(f"Deregister miner '{miner_id}'?"):
|
||||||
|
click.echo("Cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.delete(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{miner_id}",
|
||||||
|
headers={"X-Api-Key": config.api_key or ""}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
output({
|
||||||
|
"miner_id": miner_id,
|
||||||
|
"status": "deregistered"
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to deregister: {response.status_code}")
|
||||||
|
ctx.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
ctx.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command()
|
||||||
|
@click.option("--limit", default=10, help="Number of jobs to show")
|
||||||
|
@click.option("--type", "job_type", help="Filter by job type")
|
||||||
|
@click.option("--min-reward", type=float, help="Minimum reward threshold")
|
||||||
|
@click.option("--status", "job_status", help="Filter by status (pending, running, completed, failed)")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def jobs(ctx, limit: int, job_type: Optional[str], min_reward: Optional[float],
|
||||||
|
job_status: Optional[str], miner_id: str):
|
||||||
|
"""List miner jobs with filtering"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = {"limit": limit, "miner_id": miner_id}
|
||||||
|
if job_type:
|
||||||
|
params["type"] = job_type
|
||||||
|
if min_reward is not None:
|
||||||
|
params["min_reward"] = min_reward
|
||||||
|
if job_status:
|
||||||
|
params["status"] = job_status
|
||||||
|
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{miner_id}/jobs",
|
||||||
|
params=params,
|
||||||
|
headers={"X-Api-Key": config.api_key or ""}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (200, 204):
|
||||||
|
data = response.json()
|
||||||
|
output(data, ctx.obj['output_format'])
|
||||||
|
else:
|
||||||
|
error(f"Failed to get jobs: {response.status_code}")
|
||||||
|
ctx.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Network error: {e}")
|
||||||
|
ctx.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _process_single_job(config, miner_id: str, worker_id: int) -> Dict[str, Any]:
|
||||||
|
"""Process a single job (used by concurrent mine)"""
|
||||||
|
try:
|
||||||
|
with httpx.Client() as http_client:
|
||||||
|
response = http_client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/poll",
|
||||||
|
json={"max_wait_seconds": 5},
|
||||||
|
headers={
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 204:
|
||||||
|
return {"worker": worker_id, "status": "no_job"}
|
||||||
|
if response.status_code == 200:
|
||||||
|
job = response.json()
|
||||||
|
if job:
|
||||||
|
job_id = job.get('job_id')
|
||||||
|
time.sleep(2) # Simulate processing
|
||||||
|
|
||||||
|
result_response = http_client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{job_id}/result",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
json={"result": {"output": f"Processed by worker {worker_id}"}, "metrics": {}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"worker": worker_id,
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": "completed" if result_response.status_code == 200 else "failed"
|
||||||
|
}
|
||||||
|
return {"worker": worker_id, "status": "no_job"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"worker": worker_id, "status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_ollama_inference(ollama_url: str, model: str, prompt: str) -> Dict[str, Any]:
|
||||||
|
"""Run inference through local Ollama instance"""
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=120) as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{ollama_url}/api/generate",
|
||||||
|
json={
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return {
|
||||||
|
"response": data.get("response", ""),
|
||||||
|
"model": data.get("model", model),
|
||||||
|
"total_duration": data.get("total_duration", 0),
|
||||||
|
"eval_count": data.get("eval_count", 0),
|
||||||
|
"eval_duration": data.get("eval_duration", 0),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {"error": f"Ollama returned {response.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command(name="mine-ollama")
|
||||||
|
@click.option("--jobs", type=int, default=1, help="Number of jobs to process")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.option("--ollama-url", default="http://localhost:11434", help="Ollama API URL")
|
||||||
|
@click.option("--model", default="gemma3:1b", help="Ollama model to use")
|
||||||
|
@click.pass_context
|
||||||
|
def mine_ollama(ctx, jobs: int, miner_id: str, ollama_url: str, model: str):
|
||||||
|
"""Mine jobs using local Ollama for GPU inference"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
# Verify Ollama is reachable
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=5) as client:
|
||||||
|
resp = client.get(f"{ollama_url}/api/tags")
|
||||||
|
if resp.status_code != 200:
|
||||||
|
error(f"Cannot reach Ollama at {ollama_url}")
|
||||||
|
return
|
||||||
|
models = [m["name"] for m in resp.json().get("models", [])]
|
||||||
|
if model not in models:
|
||||||
|
error(f"Model '{model}' not found. Available: {', '.join(models)}")
|
||||||
|
return
|
||||||
|
success(f"Ollama connected: {ollama_url} | model: {model}")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Cannot connect to Ollama: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
while processed < jobs:
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/poll",
|
||||||
|
json={"max_wait_seconds": 10},
|
||||||
|
headers={
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 204:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
error(f"Failed to poll: {response.status_code}")
|
||||||
|
break
|
||||||
|
|
||||||
|
job = response.json()
|
||||||
|
if not job:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
job_id = job.get('job_id')
|
||||||
|
payload = job.get('payload', {})
|
||||||
|
prompt = payload.get('prompt', '')
|
||||||
|
job_model = payload.get('model', model)
|
||||||
|
|
||||||
|
output({
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": "processing",
|
||||||
|
"prompt": prompt[:80] + ("..." if len(prompt) > 80 else ""),
|
||||||
|
"model": job_model,
|
||||||
|
"job_number": processed + 1
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
|
||||||
|
# Run inference through Ollama
|
||||||
|
start_time = time.time()
|
||||||
|
ollama_result = _run_ollama_inference(ollama_url, job_model, prompt)
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
|
||||||
|
if "error" in ollama_result:
|
||||||
|
error(f"Ollama inference failed: {ollama_result['error']}")
|
||||||
|
# Submit failure
|
||||||
|
client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{job_id}/fail",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
json={"error_code": "INFERENCE_FAILED", "error_message": ollama_result['error'], "metrics": {}}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Submit successful result
|
||||||
|
result_response = client.post(
|
||||||
|
f"{config.coordinator_url}/v1/miners/{job_id}/result",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Api-Key": config.api_key or "",
|
||||||
|
"X-Miner-ID": miner_id
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"result": {
|
||||||
|
"response": ollama_result.get("response", ""),
|
||||||
|
"model": ollama_result.get("model", job_model),
|
||||||
|
"provider": "ollama",
|
||||||
|
"eval_count": ollama_result.get("eval_count", 0),
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"duration_ms": duration_ms,
|
||||||
|
"eval_count": ollama_result.get("eval_count", 0),
|
||||||
|
"eval_duration": ollama_result.get("eval_duration", 0),
|
||||||
|
"total_duration": ollama_result.get("total_duration", 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result_response.status_code == 200:
|
||||||
|
success(f"Job {job_id} completed via Ollama ({duration_ms}ms)")
|
||||||
|
processed += 1
|
||||||
|
else:
|
||||||
|
error(f"Failed to submit result: {result_response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Error: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
output({
|
||||||
|
"total_processed": processed,
|
||||||
|
"miner_id": miner_id,
|
||||||
|
"model": model,
|
||||||
|
"provider": "ollama"
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
|
|
||||||
|
|
||||||
|
@miner.command(name="concurrent-mine")
|
||||||
|
@click.option("--workers", type=int, default=2, help="Number of concurrent workers")
|
||||||
|
@click.option("--jobs", "total_jobs", type=int, default=5, help="Total jobs to process")
|
||||||
|
@click.option("--miner-id", default="cli-miner", help="Miner ID")
|
||||||
|
@click.pass_context
|
||||||
|
def concurrent_mine(ctx, workers: int, total_jobs: int, miner_id: str):
|
||||||
|
"""Mine with concurrent job processing"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
|
||||||
|
success(f"Starting concurrent mining: {workers} workers, {total_jobs} jobs")
|
||||||
|
|
||||||
|
completed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
remaining = total_jobs
|
||||||
|
while remaining > 0:
|
||||||
|
batch_size = min(remaining, workers)
|
||||||
|
futures = [
|
||||||
|
executor.submit(_process_single_job, config, miner_id, i)
|
||||||
|
for i in range(batch_size)
|
||||||
|
]
|
||||||
|
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
result = future.result()
|
||||||
|
if result.get("status") == "completed":
|
||||||
|
completed += 1
|
||||||
|
remaining -= 1
|
||||||
|
output(result, ctx.obj['output_format'])
|
||||||
|
elif result.get("status") == "no_job":
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
remaining -= 1
|
||||||
|
|
||||||
|
output({
|
||||||
|
"status": "finished",
|
||||||
|
"completed": completed,
|
||||||
|
"failed": failed,
|
||||||
|
"workers": workers
|
||||||
|
}, ctx.obj['output_format'])
|
||||||
2229
cli/aitbc_cli/commands/wallet.py.backup
Executable file
2229
cli/aitbc_cli/commands/wallet.py.backup
Executable file
File diff suppressed because it is too large
Load Diff
12
cli/setup/requirements.txt
Normal file
12
cli/setup/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
click>=8.0.0
|
||||||
|
httpx>=0.24.0
|
||||||
|
pydantic>=1.10.0
|
||||||
|
pyyaml>=6.0
|
||||||
|
rich>=14.3.3
|
||||||
|
keyring>=23.0.0
|
||||||
|
cryptography>=3.4.8
|
||||||
|
click-completion>=0.5.2
|
||||||
|
tabulate>=0.9.0
|
||||||
|
colorama>=0.4.4
|
||||||
|
python-dotenv>=0.19.0
|
||||||
|
aiohttp>=3.9.0
|
||||||
68
cli/setup/setup.py
Executable file
68
cli/setup/setup.py
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AITBC CLI Setup Script
|
||||||
|
"""
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Read README file
|
||||||
|
def read_readme():
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
return fh.read()
|
||||||
|
|
||||||
|
# Read requirements
|
||||||
|
def read_requirements():
|
||||||
|
with open("requirements.txt", "r", encoding="utf-8") as fh:
|
||||||
|
return [line.strip() for line in fh if line.strip() and not line.startswith("#")]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="aitbc-cli",
|
||||||
|
version="0.1.0",
|
||||||
|
author="AITBC Team",
|
||||||
|
author_email="team@aitbc.net",
|
||||||
|
description="AITBC Command Line Interface Tools",
|
||||||
|
long_description=read_readme(),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://aitbc.net",
|
||||||
|
project_urls={
|
||||||
|
"Homepage": "https://aitbc.net",
|
||||||
|
"Repository": "https://github.com/aitbc/aitbc",
|
||||||
|
"Documentation": "https://docs.aitbc.net",
|
||||||
|
},
|
||||||
|
packages=find_packages(),
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Topic :: System :: Distributed Computing",
|
||||||
|
],
|
||||||
|
python_requires=">=3.13",
|
||||||
|
install_requires=read_requirements(),
|
||||||
|
extras_require={
|
||||||
|
"dev": [
|
||||||
|
"pytest>=7.0.0",
|
||||||
|
"pytest-asyncio>=0.21.0",
|
||||||
|
"pytest-cov>=4.0.0",
|
||||||
|
"pytest-mock>=3.10.0",
|
||||||
|
"black>=22.0.0",
|
||||||
|
"isort>=5.10.0",
|
||||||
|
"flake8>=5.0.0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"aitbc=aitbc_cli.main:main",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include_package_data=True,
|
||||||
|
package_data={
|
||||||
|
"aitbc_cli": ["*.yaml", "*.yml", "*.json"],
|
||||||
|
},
|
||||||
|
zip_safe=False,
|
||||||
|
)
|
||||||
54
scripts/dev/ws_load_test.py.bak
Normal file
54
scripts/dev/ws_load_test.py.bak
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
DEFAULT_WS_URL = "ws://127.0.0.1:8000/rpc/ws"
|
||||||
|
BLOCK_TOPIC = "/blocks"
|
||||||
|
TRANSACTION_TOPIC = "/transactions"
|
||||||
|
|
||||||
|
|
||||||
|
async def producer(ws_url: str, interval: float = 0.1, total: int = 100) -> None:
|
||||||
|
async with websockets.connect(f"{ws_url}{BLOCK_TOPIC}") as websocket:
|
||||||
|
for index in range(total):
|
||||||
|
payload = {
|
||||||
|
"height": index,
|
||||||
|
"hash": f"0x{index:064x}",
|
||||||
|
"parent_hash": f"0x{index-1:064x}",
|
||||||
|
"timestamp": "2025-01-01T00:00:00Z",
|
||||||
|
"tx_count": 0,
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(payload))
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
async def consumer(name: str, ws_url: str, path: str, duration: float = 5.0) -> None:
|
||||||
|
async with websockets.connect(f"{ws_url}{path}") as websocket:
|
||||||
|
end = asyncio.get_event_loop().time() + duration
|
||||||
|
received = 0
|
||||||
|
while asyncio.get_event_loop().time() < end:
|
||||||
|
try:
|
||||||
|
message = await asyncio.wait_for(websocket.recv(), timeout=1.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
received += 1
|
||||||
|
if received % 10 == 0:
|
||||||
|
print(f"[{name}] received {received} messages")
|
||||||
|
print(f"[{name}] total received: {received}")
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
ws_url = DEFAULT_WS_URL
|
||||||
|
consumers = [
|
||||||
|
consumer("blocks-consumer", ws_url, BLOCK_TOPIC),
|
||||||
|
consumer("tx-consumer", ws_url, TRANSACTION_TOPIC),
|
||||||
|
]
|
||||||
|
await asyncio.gather(producer(ws_url), *consumers)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user