Compare commits

..

1 Commits

Author SHA1 Message Date
c390ba07c1 fix: resolve CLI service imports and update blockchain documentation
- Add proper package imports for coordinator-api services
- Fix 6 command modules to use app.services.* with clean path resolution
- Remove brittle path hacks and user-specific fallbacks
- Update blockchain-node README with operational status, API endpoints, and troubleshooting
- Add blockchain section to main README with quick launch and CLI examples
- Remove generated genesis.json from repository (should be ignored)

These changes fix import errors in surveillance, ai-trading, ai-surveillance,
advanced-analytics, regulatory, and enterprise-integration commands, and
document the now-operational Brother Chain (blockchain node).

Co-authored with sibling aitbc instance (coordination via Gitea).
2026-03-15 10:09:48 +00:00
14 changed files with 18 additions and 302 deletions

View File

@@ -1,18 +1,9 @@
# Blockchain Node Configuration
chain_id=ait-devnet
supported_chains=ait-devnet
rpc_bind_host=0.0.0.0
rpc_bind_port=8006
p2p_bind_host=0.0.0.0
p2p_bind_port=7070
proposer_id=aitbc1-proposer
# Gossip backend: use broadcast with Redis for cross-node communication
gossip_backend=broadcast
gossip_broadcast_url=redis://localhost:6379
# Data
db_path=./data/chain.db
CHAIN_ID=ait-devnet
DB_PATH=./data/chain.db
RPC_BIND_HOST=127.0.0.1
RPC_BIND_PORT=8080
P2P_BIND_HOST=0.0.0.0
P2P_BIND_PORT=7070
PROPOSER_KEY=change_me
MINT_PER_UNIT=1000
COORDINATOR_RATIO=0.05

View File

@@ -26,8 +26,6 @@ rich = "^13.7.1"
cryptography = "^46.0.5"
asyncpg = ">=0.29.0"
requests = "^2.32.5"
# Pin starlette to a version with Broadcast (removed in 0.38)
starlette = ">=0.37.2,<0.38.0"
[tool.poetry.extras]
uvloop = ["uvloop"]

View File

@@ -16,7 +16,6 @@ from .mempool import init_mempool
from .metrics import metrics_registry
from .rpc.router import router as rpc_router
from .rpc.websocket import router as websocket_router
from .escrow_routes import router as escrow_router
_app_logger = get_logger("aitbc_chain.app")
@@ -129,12 +128,9 @@ def create_app() -> FastAPI:
allow_headers=["*"],
)
# Include routers
app.include_router(rpc_router, prefix="/rpc", tags=["rpc"])
app.include_router(websocket_router, prefix="/rpc")
app.include_router(escrow_router, prefix="/rpc")
# Metrics and health endpoints
metrics_router = APIRouter()
@metrics_router.get("/metrics", response_class=PlainTextResponse, tags=["metrics"], summary="Prometheus metrics")

View File

@@ -7,9 +7,6 @@ from sqlalchemy import event
from .config import settings
# Import all models to ensure they are registered with SQLModel.metadata
from .models import Block, Transaction, Account, Receipt, Escrow # noqa: F401
_engine = create_engine(f"sqlite:///{settings.db_path}", echo=False)
@event.listens_for(_engine, "connect")
@@ -32,6 +29,3 @@ def init_db() -> None:
def session_scope() -> Session:
with Session(_engine) as session:
yield session
# Expose engine for escrow routes
engine = _engine

View File

@@ -2,14 +2,11 @@ from __future__ import annotations
import asyncio
import json
import warnings
from collections import defaultdict
from contextlib import suppress
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Set
warnings.filterwarnings("ignore", message="coroutine.* was never awaited", category=RuntimeWarning)
try:
from starlette.broadcast import Broadcast
except ImportError: # pragma: no cover - Starlette removed Broadcast in recent versions

View File

@@ -155,12 +155,3 @@ class Account(SQLModel, table=True):
balance: int = 0
nonce: int = 0
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Escrow(SQLModel, table=True):
__tablename__ = "escrow"
job_id: str = Field(primary_key=True)
buyer: str = Field(foreign_key="account.address")
provider: str = Field(foreign_key="account.address")
amount: int
created_at: datetime = Field(default_factory=datetime.utcnow)
released_at: Optional[datetime] = None

View File

@@ -468,7 +468,3 @@ def create_app() -> FastAPI:
app = create_app()
# Register jobs router
from .routers import jobs as jobs_router
app.include_router(jobs_router.router)

View File

@@ -426,26 +426,6 @@ async def send_payment(
}
@router.delete("/marketplace/gpu/{gpu_id}")
async def delete_gpu(
gpu_id: str,
session: Annotated[Session, Depends(get_session)],
force: bool = Query(default=False, description="Force delete even if GPU is booked")
) -> Dict[str, Any]:
"""Delete (unregister) a GPU from the marketplace."""
gpu = _get_gpu_or_404(session, gpu_id)
if gpu.status == "booked" and not force:
raise HTTPException(
status_code=http_status.HTTP_409_CONFLICT,
detail=f"GPU {gpu_id} is currently booked. Use force=true to delete anyway."
)
session.delete(gpu)
session.commit()
return {"status": "deleted", "gpu_id": gpu_id}
@router.get("/marketplace/gpu/{gpu_id}/reviews")
async def get_gpu_reviews(
gpu_id: str,

View File

@@ -7,7 +7,6 @@ Provides SQLite and PostgreSQL support with connection pooling.
from __future__ import annotations
import os
import logging
from contextlib import contextmanager
from contextlib import asynccontextmanager
from typing import Generator, AsyncGenerator
@@ -16,12 +15,9 @@ from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.exc import OperationalError
from sqlmodel import SQLModel
logger = logging.getLogger(__name__)
from ..config import settings
_engine = None
@@ -68,14 +64,6 @@ def init_db() -> Engine:
"""Initialize database tables and ensure data directory exists."""
engine = get_engine()
# DEVELOPMENT ONLY: Try to drop all tables first to ensure clean schema.
# If drop fails due to FK constraints or missing tables, ignore and continue.
if "sqlite" in str(engine.url):
try:
SQLModel.metadata.drop_all(engine)
except Exception as e:
logger.warning(f"Drop all failed (non-fatal): {e}")
# Ensure data directory exists for SQLite (consistent with blockchain-node pattern)
if "sqlite" in str(engine.url):
db_path = engine.url.database
@@ -87,17 +75,7 @@ def init_db() -> Engine:
data_dir = Path(db_path).parent
data_dir.mkdir(parents=True, exist_ok=True)
# DEVELOPMENT: Try create_all; if OperationalError about existing index, ignore
try:
SQLModel.metadata.create_all(engine)
except OperationalError as e:
if "already exists" in str(e):
logger.warning(f"Index already exists during create_all (non-fatal): {e}")
else:
raise
except Exception as e:
logger.error(f"Unexpected error during create_all: {e}")
raise
return engine

View File

@@ -1,159 +0,0 @@
import os
import subprocess
import sys
import uuid
import click
import uvicorn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
@click.group(name='ai')
def ai_group():
"""AI marketplace commands."""
pass
@ai_group.command()
@click.option('--port', default=8008, show_default=True, help='Port to listen on')
@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 serve(port, model, provider_wallet, marketplace_url):
"""Start AI provider daemon (FastAPI server)."""
click.echo(f"Starting AI provider on port {port}, model {model}, marketplace {marketplace_url}")
app = FastAPI(title="AI Provider")
class JobRequest(BaseModel):
prompt: str
buyer: str # buyer wallet address
amount: int
txid: str | None = None # optional transaction id
class JobResponse(BaseModel):
result: str
model: str
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()
@click.option('--to', required=True, help='Provider host (IP)')
@click.option('--port', default=8008, help='Provider port')
@click.option('--prompt', required=True, help='Prompt to send')
@click.option('--buyer-wallet', 'buyer_wallet', required=True, help='Buyer wallet name (in local wallet store)')
@click.option('--provider-wallet', 'provider_wallet', required=True, help='Provider wallet address (recipient)')
@click.option('--amount', default=1, help='Amount to pay in AITBC')
def request(to, port, prompt, buyer_wallet, provider_wallet, amount):
"""Send a prompt to an AI provider (buyer side) with onchain payment."""
# Helper to get provider balance
def get_balance():
res = subprocess.run([
sys.executable, "-m", "aitbc_cli.main", "blockchain", "balance",
"--address", provider_wallet
], capture_output=True, text=True, check=True)
for line in res.stdout.splitlines():
if "Balance:" in line:
parts = line.split(":")
return float(parts[1].strip())
raise ValueError("Balance not found")
# Step 1: get initial balance
before = get_balance()
click.echo(f"Provider balance before: {before}")
# Step 2: send payment via blockchain CLI (use current Python env)
if amount > 0:
click.echo(f"Sending {amount} AITBC from wallet '{buyer_wallet}' to {provider_wallet}...")
try:
subprocess.run([
sys.executable, "-m", "aitbc_cli.main", "blockchain", "send",
"--from", buyer_wallet,
"--to", provider_wallet,
"--amount", str(amount)
], check=True, capture_output=True, text=True)
click.echo("Payment sent.")
except subprocess.CalledProcessError as e:
raise click.ClickException(f"Blockchain send failed: {e.stderr}")
# Step 3: get new balance
after = get_balance()
click.echo(f"Provider balance after: {after}")
delta = after - before
click.echo(f"Balance delta: {delta}")
# Step 4: call provider
url = f"http://{to}:{port}/job"
payload = {
"prompt": prompt,
"buyer": provider_wallet,
"amount": amount
}
try:
resp = httpx.post(url, json=payload, timeout=30.0)
resp.raise_for_status()
data = resp.json()
click.echo("Result: " + data.get("result", ""))
except httpx.HTTPError as e:
raise click.ClickException(f"Request to provider failed: {e}")
if __name__ == '__main__':
ai_group()

View File

@@ -300,34 +300,6 @@ def pay(ctx, booking_id: str, amount: float, from_wallet: str, to_wallet: str, t
except Exception as e:
error(f"Payment failed: {e}")
@gpu.command()
@click.argument("gpu_id")
@click.option("--force", is_flag=True, help="Force delete even if GPU is booked")
@click.pass_context
def unregister(ctx, gpu_id: str, force: bool):
"""Unregister (delete) a GPU from marketplace"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.delete(
f"{config.coordinator_url}/v1/marketplace/gpu/{gpu_id}",
params={"force": force},
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
result = response.json()
success(f"GPU {gpu_id} unregistered")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to unregister GPU: {response.status_code}")
if response.text:
error(response.text)
except Exception as e:
error(f"Network error: {e}")
@gpu.command()
@click.argument("gpu_id")
@click.pass_context

View File

@@ -58,16 +58,7 @@ from .commands.regulatory import regulatory
from .commands.ai_trading import ai_trading
from .commands.advanced_analytics import advanced_analytics_group
from .commands.ai_surveillance import ai_surveillance_group
# AI provider commands
from .commands.ai import ai_group
# Enterprise integration (optional)
try:
from .commands.enterprise_integration import enterprise_integration_group
except ImportError:
enterprise_integration_group = None
from .commands.explorer import explorer
from .plugins import plugin, load_plugins
@@ -251,7 +242,6 @@ cli.add_command(transfer_control)
cli.add_command(agent)
cli.add_command(multimodal)
cli.add_command(optimize)
cli.add_command(ai_group)
# cli.add_command(openclaw) # Temporarily disabled
cli.add_command(swarm)
cli.add_command(chain)
@@ -268,7 +258,6 @@ cli.add_command(regulatory)
cli.add_command(ai_trading)
cli.add_command(advanced_analytics_group)
cli.add_command(ai_surveillance_group)
if enterprise_integration_group is not None:
cli.add_command(enterprise_integration_group)
cli.add_command(explorer)
cli.add_command(plugin)

View File

@@ -20,7 +20,7 @@ genesis:
- address: aitbc1genesis
balance: '2100000000'
type: genesis
- address: aitbc1aitbc1_simple_simple
- address: aitbc1aitbc1_simple
balance: '500'
type: gift
metadata:

View File

@@ -118,13 +118,7 @@ dependencies = [
"tabulate==0.9.0",
"colorama==0.4.6",
"python-dotenv==1.0.0",
"asyncpg==0.31.0",
# Dependencies for service module imports (coordinator-api services)
"numpy>=1.26.0",
"pandas>=2.0.0",
"aiohttp>=3.9.0",
"fastapi>=0.111.0",
"uvicorn[standard]>=0.30.0"
"asyncpg==0.31.0"
]
classifiers = [
"Development Status :: 4 - Beta",
@@ -168,12 +162,11 @@ requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["cli", "apps/coordinator-api"]
include = ["aitbc_cli*", "aitbc*"]
where = ["cli"]
include = ["aitbc_cli*"]
[tool.setuptools.package-dir]
"aitbc_cli" = "cli/aitbc_cli"
"aitbc" = "apps/coordinator-api/aitbc"
[dependency-groups]
dev = [