Clawdbot commited on
Commit ·
a890f8f
1
Parent(s): 7bec764
Upgrade to Phase 2 (Auction Logic) to save API costs
Browse files- hub/__pycache__/main.cpython-310.pyc +0 -0
- hub/__pycache__/models.cpython-310.pyc +0 -0
- hub/main.py +36 -6
- hub/models.py +5 -0
- node/mep_miner.py +40 -0
- node/test_auction.py +34 -0
hub/__pycache__/main.cpython-310.pyc
CHANGED
|
Binary files a/hub/__pycache__/main.cpython-310.pyc and b/hub/__pycache__/main.cpython-310.pyc differ
|
|
|
hub/__pycache__/models.cpython-310.pyc
CHANGED
|
Binary files a/hub/__pycache__/models.cpython-310.pyc and b/hub/__pycache__/models.cpython-310.pyc differ
|
|
|
hub/main.py
CHANGED
|
@@ -2,7 +2,7 @@ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
|
| 2 |
from typing import Dict, List
|
| 3 |
import uuid
|
| 4 |
|
| 5 |
-
from models import NodeRegistration, TaskCreate, TaskResult, NodeBalance
|
| 6 |
|
| 7 |
app = FastAPI(title="Chronos Protocol L1 Hub", description="The Time Exchange Clearinghouse", version="0.1.1")
|
| 8 |
|
|
@@ -39,15 +39,18 @@ async def submit_task(task: TaskCreate):
|
|
| 39 |
"consumer_id": task.consumer_id,
|
| 40 |
"payload": task.payload,
|
| 41 |
"bounty": task.bounty,
|
| 42 |
-
"status": "
|
| 43 |
-
"target_node": task.target_node
|
|
|
|
| 44 |
}
|
| 45 |
active_tasks[task_id] = task_data
|
| 46 |
|
| 47 |
-
# Target specific node if requested (Direct Message)
|
| 48 |
if task.target_node:
|
| 49 |
if task.target_node in connected_nodes:
|
| 50 |
try:
|
|
|
|
|
|
|
| 51 |
await connected_nodes[task.target_node].send_json({"event": "new_task", "data": task_data})
|
| 52 |
return {"status": "success", "task_id": task_id, "routed_to": task.target_node}
|
| 53 |
except:
|
|
@@ -55,16 +58,43 @@ async def submit_task(task: TaskCreate):
|
|
| 55 |
else:
|
| 56 |
return {"status": "error", "detail": "Target node not currently connected to Hub"}
|
| 57 |
|
| 58 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
for node_id, ws in list(connected_nodes.items()):
|
| 60 |
if node_id != task.consumer_id:
|
| 61 |
try:
|
| 62 |
-
await ws.send_json({"event": "
|
| 63 |
except:
|
| 64 |
pass
|
| 65 |
|
| 66 |
return {"status": "success", "task_id": task_id}
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
@app.post("/tasks/complete")
|
| 69 |
async def complete_task(result: TaskResult):
|
| 70 |
task = active_tasks.get(result.task_id)
|
|
|
|
| 2 |
from typing import Dict, List
|
| 3 |
import uuid
|
| 4 |
|
| 5 |
+
from models import NodeRegistration, TaskCreate, TaskResult, NodeBalance, TaskBid
|
| 6 |
|
| 7 |
app = FastAPI(title="Chronos Protocol L1 Hub", description="The Time Exchange Clearinghouse", version="0.1.1")
|
| 8 |
|
|
|
|
| 39 |
"consumer_id": task.consumer_id,
|
| 40 |
"payload": task.payload,
|
| 41 |
"bounty": task.bounty,
|
| 42 |
+
"status": "bidding",
|
| 43 |
+
"target_node": task.target_node,
|
| 44 |
+
"model_requirement": task.model_requirement
|
| 45 |
}
|
| 46 |
active_tasks[task_id] = task_data
|
| 47 |
|
| 48 |
+
# Target specific node if requested (Direct Message skips bidding)
|
| 49 |
if task.target_node:
|
| 50 |
if task.target_node in connected_nodes:
|
| 51 |
try:
|
| 52 |
+
task_data["status"] = "assigned"
|
| 53 |
+
task_data["provider_id"] = task.target_node
|
| 54 |
await connected_nodes[task.target_node].send_json({"event": "new_task", "data": task_data})
|
| 55 |
return {"status": "success", "task_id": task_id, "routed_to": task.target_node}
|
| 56 |
except:
|
|
|
|
| 58 |
else:
|
| 59 |
return {"status": "error", "detail": "Target node not currently connected to Hub"}
|
| 60 |
|
| 61 |
+
# Phase 2: Broadcast RFC (Request For Compute) to all connected nodes EXCEPT the consumer
|
| 62 |
+
rfc_data = {
|
| 63 |
+
"id": task_id,
|
| 64 |
+
"consumer_id": task.consumer_id,
|
| 65 |
+
"bounty": task.bounty,
|
| 66 |
+
"model_requirement": task.model_requirement
|
| 67 |
+
}
|
| 68 |
for node_id, ws in list(connected_nodes.items()):
|
| 69 |
if node_id != task.consumer_id:
|
| 70 |
try:
|
| 71 |
+
await ws.send_json({"event": "rfc", "data": rfc_data})
|
| 72 |
except:
|
| 73 |
pass
|
| 74 |
|
| 75 |
return {"status": "success", "task_id": task_id}
|
| 76 |
|
| 77 |
+
@app.post("/tasks/bid")
|
| 78 |
+
async def place_bid(bid: TaskBid):
|
| 79 |
+
task = active_tasks.get(bid.task_id)
|
| 80 |
+
if not task:
|
| 81 |
+
raise HTTPException(status_code=404, detail="Task not found or already completed")
|
| 82 |
+
|
| 83 |
+
if task["status"] != "bidding":
|
| 84 |
+
return {"status": "rejected", "detail": "Task already assigned to another node"}
|
| 85 |
+
|
| 86 |
+
# Phase 2 Fast Auction: Accept the first valid bid
|
| 87 |
+
task["status"] = "assigned"
|
| 88 |
+
task["provider_id"] = bid.provider_id
|
| 89 |
+
|
| 90 |
+
# Return the full payload to the winner
|
| 91 |
+
return {
|
| 92 |
+
"status": "accepted",
|
| 93 |
+
"payload": task["payload"],
|
| 94 |
+
"consumer_id": task["consumer_id"],
|
| 95 |
+
"model_requirement": task.get("model_requirement")
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
@app.post("/tasks/complete")
|
| 99 |
async def complete_task(result: TaskResult):
|
| 100 |
task = active_tasks.get(result.task_id)
|
hub/models.py
CHANGED
|
@@ -11,6 +11,11 @@ class TaskCreate(BaseModel):
|
|
| 11 |
payload: str
|
| 12 |
bounty: float
|
| 13 |
target_node: Optional[str] = None # Direct messaging / specific bot targeting
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
class TaskResult(BaseModel):
|
| 16 |
task_id: str
|
|
|
|
| 11 |
payload: str
|
| 12 |
bounty: float
|
| 13 |
target_node: Optional[str] = None # Direct messaging / specific bot targeting
|
| 14 |
+
model_requirement: Optional[str] = None
|
| 15 |
+
|
| 16 |
+
class TaskBid(BaseModel):
|
| 17 |
+
task_id: str
|
| 18 |
+
provider_id: str
|
| 19 |
|
| 20 |
class TaskResult(BaseModel):
|
| 21 |
task_id: str
|
node/mep_miner.py
CHANGED
|
@@ -51,6 +51,8 @@ class MEPMiner:
|
|
| 51 |
|
| 52 |
if data["event"] == "new_task":
|
| 53 |
await self.process_task(data["data"])
|
|
|
|
|
|
|
| 54 |
|
| 55 |
except asyncio.TimeoutError:
|
| 56 |
continue # Keep connection alive
|
|
@@ -61,6 +63,44 @@ class MEPMiner:
|
|
| 61 |
except Exception as e:
|
| 62 |
print(f"[MEP Miner {self.node_id}] WebSocket error: {e}")
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
async def process_task(self, task_data: dict):
|
| 65 |
"""Process a task and earn SECONDS."""
|
| 66 |
task_id = task_data["id"]
|
|
|
|
| 51 |
|
| 52 |
if data["event"] == "new_task":
|
| 53 |
await self.process_task(data["data"])
|
| 54 |
+
elif data["event"] == "rfc":
|
| 55 |
+
await self.handle_rfc(data["data"])
|
| 56 |
|
| 57 |
except asyncio.TimeoutError:
|
| 58 |
continue # Keep connection alive
|
|
|
|
| 63 |
except Exception as e:
|
| 64 |
print(f"[MEP Miner {self.node_id}] WebSocket error: {e}")
|
| 65 |
|
| 66 |
+
async def handle_rfc(self, rfc_data: dict):
|
| 67 |
+
"""Phase 2: Evaluate Request For Compute and submit Bid."""
|
| 68 |
+
task_id = rfc_data["id"]
|
| 69 |
+
bounty = rfc_data["bounty"]
|
| 70 |
+
model = rfc_data.get("model_requirement")
|
| 71 |
+
|
| 72 |
+
# Simple evaluation logic
|
| 73 |
+
if bounty < 0.1 and bounty != 0.0: # Allow 0 for testing DM
|
| 74 |
+
print(f"[MEP Miner {self.node_id}] Ignored RFC {task_id[:8]} (Bounty too low: {bounty})")
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
print(f"[MEP Miner {self.node_id}] Received RFC {task_id[:8]} for {bounty:.6f} SECONDS. Placing bid...")
|
| 78 |
+
|
| 79 |
+
# Place bid
|
| 80 |
+
try:
|
| 81 |
+
resp = requests.post(f"{HUB_URL}/tasks/bid", json={
|
| 82 |
+
"task_id": task_id,
|
| 83 |
+
"provider_id": self.node_id
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
if resp.status_code == 200:
|
| 87 |
+
data = resp.json()
|
| 88 |
+
if data["status"] == "accepted":
|
| 89 |
+
print(f"[MEP Miner {self.node_id}] 🏁 BID WON for task {task_id[:8]}! Processing payload...")
|
| 90 |
+
|
| 91 |
+
# Reconstruct task_data to pass to process_task
|
| 92 |
+
task_data = {
|
| 93 |
+
"id": task_id,
|
| 94 |
+
"payload": data["payload"],
|
| 95 |
+
"bounty": bounty,
|
| 96 |
+
"consumer_id": data["consumer_id"]
|
| 97 |
+
}
|
| 98 |
+
await self.process_task(task_data)
|
| 99 |
+
else:
|
| 100 |
+
print(f"[MEP Miner {self.node_id}] Bid rejected (too slow): {data.get('detail', '')}")
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"[MEP Miner {self.node_id}] Error placing bid: {e}")
|
| 103 |
+
|
| 104 |
async def process_task(self, task_data: dict):
|
| 105 |
"""Process a task and earn SECONDS."""
|
| 106 |
task_id = task_data["id"]
|
node/test_auction.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import websockets
|
| 3 |
+
import json
|
| 4 |
+
import requests
|
| 5 |
+
import uuid
|
| 6 |
+
|
| 7 |
+
HUB_URL = "http://localhost:8000"
|
| 8 |
+
|
| 9 |
+
async def test():
|
| 10 |
+
miner = f'mep-miner-{uuid.uuid4().hex[:6]}'
|
| 11 |
+
requests.post(f'{HUB_URL}/register', json={'pubkey': miner})
|
| 12 |
+
async with websockets.connect(f'ws://localhost:8000/ws/{miner}') as ws:
|
| 13 |
+
consumer = 'test-consumer'
|
| 14 |
+
requests.post(f'{HUB_URL}/register', json={'pubkey': consumer})
|
| 15 |
+
requests.post(f'{HUB_URL}/tasks/submit', json={'consumer_id': consumer, 'payload': 'Test payload', 'bounty': 1.0})
|
| 16 |
+
|
| 17 |
+
msg = await asyncio.wait_for(ws.recv(), timeout=2.0)
|
| 18 |
+
data = json.loads(msg)
|
| 19 |
+
print('Received:', data)
|
| 20 |
+
|
| 21 |
+
if data['event'] == 'rfc':
|
| 22 |
+
task_id = data['data']['id']
|
| 23 |
+
resp = requests.post(f'{HUB_URL}/tasks/bid', json={'task_id': task_id, 'provider_id': miner})
|
| 24 |
+
print('Bid response:', resp.json())
|
| 25 |
+
|
| 26 |
+
complete_resp = requests.post(f'{HUB_URL}/tasks/complete', json={
|
| 27 |
+
'task_id': task_id,
|
| 28 |
+
'provider_id': miner,
|
| 29 |
+
'result_payload': 'Done!'
|
| 30 |
+
})
|
| 31 |
+
print('Complete response:', complete_resp.json())
|
| 32 |
+
|
| 33 |
+
if __name__ == '__main__':
|
| 34 |
+
asyncio.run(test())
|