Compare commits
9 Commits
0a0a8a1e15
...
4c04652291
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c04652291 | |||
| a0d0d22a4a | |||
| ee430ebb49 | |||
| e7af9ac365 | |||
| 3bdada174c | |||
| d29a54e98f | |||
| 8fee73a2ec | |||
| 4c2ada682a | |||
| 6223e0b582 |
@@ -1,9 +1,18 @@
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"address": "ait1faucet000000000000000000000000000000000",
|
||||
"balance": 1000000000,
|
||||
"nonce": 0
|
||||
}
|
||||
],
|
||||
"authorities": [
|
||||
{
|
||||
"address": "ait1devproposer000000000000000000000000000000",
|
||||
"weight": 1
|
||||
}
|
||||
],
|
||||
"chain_id": "ait-devnet",
|
||||
"params": {
|
||||
"base_fee": 10,
|
||||
"coordinator_ratio": 0.05,
|
||||
"fee_per_byte": 1,
|
||||
"mint_per_unit": 1000
|
||||
},
|
||||
"timestamp": 1772895053
|
||||
}
|
||||
@@ -26,6 +26,8 @@ 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"]
|
||||
|
||||
@@ -16,6 +16,7 @@ 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")
|
||||
|
||||
@@ -128,9 +129,12 @@ 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")
|
||||
|
||||
@@ -7,6 +7,9 @@ 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")
|
||||
@@ -29,3 +32,6 @@ def init_db() -> None:
|
||||
def session_scope() -> Session:
|
||||
with Session(_engine) as session:
|
||||
yield session
|
||||
|
||||
# Expose engine for escrow routes
|
||||
engine = _engine
|
||||
|
||||
@@ -2,11 +2,14 @@ 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
|
||||
|
||||
@@ -155,3 +155,12 @@ 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
|
||||
|
||||
@@ -468,3 +468,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
# Register jobs router
|
||||
from .routers import jobs as jobs_router
|
||||
app.include_router(jobs_router.router)
|
||||
|
||||
@@ -426,6 +426,26 @@ 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,
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -15,9 +16,12 @@ 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
|
||||
@@ -64,6 +68,14 @@ 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
|
||||
@@ -75,7 +87,17 @@ def init_db() -> Engine:
|
||||
data_dir = Path(db_path).parent
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SQLModel.metadata.create_all(engine)
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
159
cli/aitbc_cli/commands/ai.py
Normal file
159
cli/aitbc_cli/commands/ai.py
Normal file
@@ -0,0 +1,159 @@
|
||||
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 on‑chain 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()
|
||||
@@ -300,6 +300,34 @@ 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
|
||||
|
||||
@@ -33,8 +33,13 @@ else:
|
||||
|
||||
try:
|
||||
from regulatory_reporting import (
|
||||
generate_sar, generate_compliance_summary, list_reports,
|
||||
regulatory_reporter, ReportType, ReportStatus, RegulatoryBody
|
||||
generate_sar as generate_sar_svc,
|
||||
generate_compliance_summary as generate_compliance_summary_svc,
|
||||
list_reports as list_reports_svc,
|
||||
regulatory_reporter,
|
||||
ReportType,
|
||||
ReportStatus,
|
||||
RegulatoryBody
|
||||
)
|
||||
_import_error = None
|
||||
except ImportError as e:
|
||||
@@ -45,7 +50,7 @@ except ImportError as e:
|
||||
f"Required service module 'regulatory_reporting' could not be imported: {_import_error}. "
|
||||
"Ensure coordinator-api dependencies are installed or set AITBC_SERVICES_PATH."
|
||||
)
|
||||
generate_sar = generate_compliance_summary = list_reports = regulatory_reporter = _missing
|
||||
generate_sar_svc = generate_compliance_summary_svc = list_reports_svc = regulatory_reporter = _missing
|
||||
|
||||
class ReportType:
|
||||
pass
|
||||
@@ -91,7 +96,7 @@ def generate_sar(ctx, user_id: str, activity_type: str, amount: float, descripti
|
||||
}
|
||||
|
||||
# Generate SAR
|
||||
result = asyncio.run(generate_sar([activity]))
|
||||
result = asyncio.run(generate_sar_svc([activity]))
|
||||
|
||||
click.echo(f"\n✅ SAR Report Generated Successfully!")
|
||||
click.echo(f"📋 Report ID: {result['report_id']}")
|
||||
@@ -124,7 +129,7 @@ def compliance_summary(ctx, period_start: str, period_end: str):
|
||||
click.echo(f"📈 Duration: {(end_date - start_date).days} days")
|
||||
|
||||
# Generate compliance summary
|
||||
result = asyncio.run(generate_compliance_summary(
|
||||
result = asyncio.run(generate_compliance_summary_svc(
|
||||
start_date.isoformat(),
|
||||
end_date.isoformat()
|
||||
))
|
||||
@@ -169,7 +174,7 @@ def list(ctx, report_type: str, status: str, limit: int):
|
||||
try:
|
||||
click.echo(f"📋 Regulatory Reports")
|
||||
|
||||
reports = list_reports(report_type, status)
|
||||
reports = list_reports_svc(report_type, status)
|
||||
|
||||
if not reports:
|
||||
click.echo(f"✅ No reports found")
|
||||
@@ -454,7 +459,7 @@ def test(ctx, period_start: str, period_end: str):
|
||||
|
||||
# Test SAR generation
|
||||
click.echo(f"\n📋 Test 1: SAR Generation")
|
||||
result = asyncio.run(generate_sar([{
|
||||
result = asyncio.run(generate_sar_svc([{
|
||||
"id": "test_sar_001",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user_id": "test_user_123",
|
||||
@@ -471,13 +476,13 @@ def test(ctx, period_start: str, period_end: str):
|
||||
|
||||
# Test compliance summary
|
||||
click.echo(f"\n📊 Test 2: Compliance Summary")
|
||||
compliance_result = asyncio.run(generate_compliance_summary(period_start, period_end))
|
||||
compliance_result = asyncio.run(generate_compliance_summary_svc(period_start, period_end))
|
||||
click.echo(f" ✅ Compliance Summary: {compliance_result['report_id']}")
|
||||
click.echo(f" 📈 Overall Score: {compliance_result['overall_score']:.1%}")
|
||||
|
||||
# Test report listing
|
||||
click.echo(f"\n📋 Test 3: Report Listing")
|
||||
reports = list_reports()
|
||||
reports = list_reports_svc()
|
||||
click.echo(f" ✅ Total Reports: {len(reports)}")
|
||||
|
||||
# Test export
|
||||
|
||||
@@ -58,7 +58,16 @@ 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
|
||||
from .commands.enterprise_integration import enterprise_integration_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
|
||||
|
||||
@@ -242,6 +251,7 @@ 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)
|
||||
@@ -258,7 +268,8 @@ cli.add_command(regulatory)
|
||||
cli.add_command(ai_trading)
|
||||
cli.add_command(advanced_analytics_group)
|
||||
cli.add_command(ai_surveillance_group)
|
||||
cli.add_command(enterprise_integration_group)
|
||||
if enterprise_integration_group is not None:
|
||||
cli.add_command(enterprise_integration_group)
|
||||
cli.add_command(explorer)
|
||||
cli.add_command(plugin)
|
||||
load_plugins(cli)
|
||||
|
||||
@@ -20,7 +20,7 @@ genesis:
|
||||
- address: aitbc1genesis
|
||||
balance: '2100000000'
|
||||
type: genesis
|
||||
- address: aitbc1aitbc1_simple
|
||||
- address: aitbc1aitbc1_simple_simple
|
||||
balance: '500'
|
||||
type: gift
|
||||
metadata:
|
||||
|
||||
@@ -118,7 +118,13 @@ dependencies = [
|
||||
"tabulate==0.9.0",
|
||||
"colorama==0.4.6",
|
||||
"python-dotenv==1.0.0",
|
||||
"asyncpg==0.31.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"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
@@ -162,11 +168,12 @@ requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["cli"]
|
||||
include = ["aitbc_cli*"]
|
||||
where = ["cli", "apps/coordinator-api"]
|
||||
include = ["aitbc_cli*", "aitbc*"]
|
||||
|
||||
[tool.setuptools.package-dir]
|
||||
"aitbc_cli" = "cli/aitbc_cli"
|
||||
"aitbc" = "apps/coordinator-api/aitbc"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user