Spaces:
Paused
Paused
rb125 commited on
Commit ·
d2e404a
1
Parent(s): 03c8703
added nextjs dashboard
Browse files- dashboard-next/app/globals.css +1 -0
- dashboard-next/app/layout.tsx +20 -0
- dashboard-next/app/page.tsx +262 -0
- dashboard-next/eslint.config.mjs +18 -0
- dashboard-next/next-env.d.ts +6 -0
- dashboard-next/next.config.ts +14 -0
- dashboard-next/package.json +26 -0
- dashboard-next/postcss.config.mjs +7 -0
- dashboard-next/tsconfig.json +34 -0
- server/api.py +231 -0
dashboard-next/app/globals.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
dashboard-next/app/layout.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import "./globals.css";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "CGAE · Agent Economy",
|
| 6 |
+
description: "Comprehension-Gated Agent Economy — 0G Chain",
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 10 |
+
return (
|
| 11 |
+
<html lang="en">
|
| 12 |
+
<head>
|
| 13 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 14 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
| 16 |
+
</head>
|
| 17 |
+
<body className="antialiased min-h-screen" style={{ fontFamily: "'Inter', system-ui, sans-serif" }}>{children}</body>
|
| 18 |
+
</html>
|
| 19 |
+
);
|
| 20 |
+
}
|
dashboard-next/app/page.tsx
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from "react";
|
| 4 |
+
import {
|
| 5 |
+
AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip,
|
| 6 |
+
ResponsiveContainer, CartesianGrid, Cell, PieChart, Pie,
|
| 7 |
+
} from "recharts";
|
| 8 |
+
|
| 9 |
+
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
| 10 |
+
const POLL_MS = 2000;
|
| 11 |
+
|
| 12 |
+
const VIOLET = "#7c3aed";
|
| 13 |
+
const BLUE = "#2563eb";
|
| 14 |
+
const GREEN = "#059669";
|
| 15 |
+
const RED = "#dc2626";
|
| 16 |
+
const AMBER = "#d97706";
|
| 17 |
+
const TC: Record<number,string> = {0:"#94a3b8",1:"#6366f1",2:"#2563eb",3:"#7c3aed",4:"#d97706",5:"#dc2626"};
|
| 18 |
+
|
| 19 |
+
interface Economy { aggregate_safety:number; active_agents:number; total_balance:number; total_earned:number; contracts_completed:number; contracts_failed:number }
|
| 20 |
+
interface Agent { agent_id:string; model_name:string; strategy:string; current_tier:number; balance:number; total_earned:number; total_penalties:number; contracts_completed:number; contracts_failed:number; status:string; wallet_address?:string; robustness:{cc:number;er:number;as_:number;ih:number}|null }
|
| 21 |
+
interface Trade { round:number; agent:string; task_id:string; task_prompt:string; tier:string; domain:string; passed:boolean; reward:number; penalty:number; token_cost:number; latency_ms:number; output_preview:string; constraints_passed:string[]; constraints_failed:string[] }
|
| 22 |
+
interface Evt { timestamp:number; type:string; agent:string; message:string }
|
| 23 |
+
|
| 24 |
+
function usePoll<T>(url:string,ms:number):T|null{const[d,setD]=useState<T|null>(null);useEffect(()=>{let a=true;const p=()=>{fetch(url).then(r=>r.json()).then(v=>{if(a)setD(v)}).catch(()=>{})};p();const id=setInterval(p,ms);return()=>{a=false;clearInterval(id)}},[url,ms]);return d}
|
| 25 |
+
|
| 26 |
+
/* ---- Atoms ---- */
|
| 27 |
+
const Card=({children,className=""}:{children:React.ReactNode;className?:string})=>(
|
| 28 |
+
<div className={`bg-white/80 backdrop-blur-sm border border-white/60 rounded-2xl shadow-lg shadow-black/[0.03] ${className}`}>{children}</div>);
|
| 29 |
+
|
| 30 |
+
const Stat=({label,value,sub,pulse}:{label:string;value:string;sub?:string;pulse?:boolean})=>(
|
| 31 |
+
<Card className="p-5">
|
| 32 |
+
<div className="flex items-center justify-between mb-1"><p className="text-[11px] uppercase tracking-wider text-slate-400 font-semibold">{label}</p>{pulse&&<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse"/>}</div>
|
| 33 |
+
<p className="text-2xl font-extrabold text-slate-800">{value}</p>{sub&&<p className="text-[11px] text-slate-400 mt-0.5">{sub}</p>}
|
| 34 |
+
</Card>);
|
| 35 |
+
|
| 36 |
+
const TB=({t}:{t:number})=>{const c=TC[t]||"#94a3b8";return<span className="px-2 py-0.5 rounded-full text-[10px] font-bold" style={{background:c+"18",color:c,border:`1px solid ${c}35`}}>T{t}</span>};
|
| 37 |
+
const RB=({l,v}:{l:string;v:number})=>{const p=Math.round(v*100);const c=v>=.65?GREEN:v>=.4?AMBER:RED;return(<div className="flex items-center gap-1.5"><span className="w-5 text-[10px] text-slate-400 font-medium">{l}</span><div className="flex-1 h-1.5 bg-slate-100 rounded-full overflow-hidden"><div className="h-full rounded-full transition-all duration-500" style={{width:`${p}%`,backgroundColor:c}}/></div><span className="w-7 text-right text-[10px] text-slate-500 font-medium">{p}%</span></div>)};
|
| 38 |
+
const Addr=({id}:{id:string})=>(!id||id.length<8?<span className="text-slate-400 font-mono text-[10px]">{id}</span>:<span className="text-slate-400 font-mono text-[10px]">{id.slice(0,6)}…{id.slice(-4)}</span>);
|
| 39 |
+
const tt={backgroundColor:"#fff",border:"1px solid #e2e8f0",borderRadius:12,fontSize:11,color:"#1e293b",boxShadow:"0 4px 12px rgba(0,0,0,0.06)"};
|
| 40 |
+
const StatusDot=({s}:{s:string})=>{const c:Record<string,string>={idle:"#94a3b8",setup:AMBER,running:GREEN,done:BLUE,connecting:"#94a3b8"};return<span className="inline-flex items-center gap-1.5 text-xs text-slate-500 font-medium"><span className="h-2 w-2 rounded-full" style={{backgroundColor:c[s]||"#94a3b8"}}/>{s}</span>};
|
| 41 |
+
|
| 42 |
+
function EventFeed({events}:{events:Evt[]}){
|
| 43 |
+
if(!events.length) return null;
|
| 44 |
+
const last = events.slice(-6).reverse();
|
| 45 |
+
const styles:Record<string,{bg:string;border:string;icon:string;text:string}>={
|
| 46 |
+
BANKRUPTCY:{bg:"#fef2f2",border:"#fecaca",icon:"🚨",text:"text-red-700"},
|
| 47 |
+
DEMOTION:{bg:"#fffbeb",border:"#fde68a",icon:"⚠️",text:"text-amber-700"},
|
| 48 |
+
UPGRADE:{bg:"#ecfdf5",border:"#a7f3d0",icon:"🎉",text:"text-emerald-700"},
|
| 49 |
+
TEST_ETH_TOPUP:{bg:"#eef2ff",border:"#c7d2fe",icon:"💰",text:"text-violet-700"},
|
| 50 |
+
};
|
| 51 |
+
return(
|
| 52 |
+
<div className="space-y-1.5 mb-6">
|
| 53 |
+
<h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2">Protocol Events</h3>
|
| 54 |
+
{last.map((e,i)=>{
|
| 55 |
+
const s=styles[e.type]||{bg:"#eff6ff",border:"#bfdbfe",icon:"📋",text:"text-blue-700"};
|
| 56 |
+
return(<div key={i} className="rounded-xl px-4 py-2.5 text-xs" style={{background:s.bg,border:`1px solid ${s.border}`}}>
|
| 57 |
+
<span className="mr-2">{s.icon}</span><span className={`font-bold mr-2 ${s.text}`}>{e.type}</span><span className="text-slate-600">{e.message}</span>
|
| 58 |
+
</div>);
|
| 59 |
+
})}
|
| 60 |
+
</div>);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function EconomyTab({eco,ts,events}:{eco:Economy|null;ts:any;events:Evt[]}){
|
| 64 |
+
if(!eco)return<div className="flex items-center justify-center h-64 text-slate-400 text-sm">Waiting for first round…</div>;
|
| 65 |
+
const sD=(ts?.safety||[]).map((v:number,i:number)=>({r:i+1,v}));
|
| 66 |
+
const bD=(ts?.balance||[]).map((v:number,i:number)=>({r:i+1,v}));
|
| 67 |
+
const fD=(ts?.rewards||[]).map((v:number,i:number)=>({r:i+1,rw:v,pn:ts?.penalties?.[i]||0}));
|
| 68 |
+
return(<div className="space-y-6">
|
| 69 |
+
<EventFeed events={events}/>
|
| 70 |
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
| 71 |
+
<Stat label="Safety S(P)" value={`${(eco.aggregate_safety*100).toFixed(1)}%`} pulse/>
|
| 72 |
+
<Stat label="Active Agents" value={String(eco.active_agents)}/>
|
| 73 |
+
<Stat label="Total Balance" value={`Ξ ${eco.total_balance.toFixed(4)}`} sub="ETH"/>
|
| 74 |
+
<Stat label="Total Earned" value={`Ξ ${eco.total_earned.toFixed(4)}`} sub="ETH"/>
|
| 75 |
+
<Stat label="Completed" value={String(eco.contracts_completed)} sub={`${eco.contracts_failed} failed`}/>
|
| 76 |
+
<Stat label="Chain" value="0G Galileo" sub="Testnet"/>
|
| 77 |
+
</div>
|
| 78 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
| 79 |
+
<Card className="p-5"><h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-4">Aggregate Safety — Theorem 3</h3>
|
| 80 |
+
<ResponsiveContainer width="100%" height={200}><AreaChart data={sD}>
|
| 81 |
+
<defs><linearGradient id="gS" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor={VIOLET} stopOpacity={.2}/><stop offset="100%" stopColor={VIOLET} stopOpacity={0}/></linearGradient></defs>
|
| 82 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9"/><XAxis dataKey="r" tick={{fontSize:9,fill:"#94a3b8"}}/><YAxis domain={[0.3,1]} tick={{fontSize:9,fill:"#94a3b8"}}/><Tooltip contentStyle={tt}/>
|
| 83 |
+
<Area type="monotone" dataKey="v" stroke={VIOLET} fill="url(#gS)" strokeWidth={2.5} dot={false}/>
|
| 84 |
+
</AreaChart></ResponsiveContainer></Card>
|
| 85 |
+
<Card className="p-5"><h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-4">Circulating ETH</h3>
|
| 86 |
+
<ResponsiveContainer width="100%" height={200}><AreaChart data={bD}>
|
| 87 |
+
<defs><linearGradient id="gB" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor={BLUE} stopOpacity={.2}/><stop offset="100%" stopColor={BLUE} stopOpacity={0}/></linearGradient></defs>
|
| 88 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9"/><XAxis dataKey="r" tick={{fontSize:9,fill:"#94a3b8"}}/><YAxis tick={{fontSize:9,fill:"#94a3b8"}}/><Tooltip contentStyle={tt}/>
|
| 89 |
+
<Area type="monotone" dataKey="v" stroke={BLUE} fill="url(#gB)" strokeWidth={2.5} dot={false}/>
|
| 90 |
+
</AreaChart></ResponsiveContainer></Card>
|
| 91 |
+
<Card className="p-5"><h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-4">Rewards vs Penalties</h3>
|
| 92 |
+
<ResponsiveContainer width="100%" height={200}><AreaChart data={fD}>
|
| 93 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9"/><XAxis dataKey="r" tick={{fontSize:9,fill:"#94a3b8"}}/><YAxis tick={{fontSize:9,fill:"#94a3b8"}}/><Tooltip contentStyle={tt}/>
|
| 94 |
+
<Area type="monotone" dataKey="rw" stroke={GREEN} fill={GREEN} fillOpacity={.1} strokeWidth={2} dot={false} name="Reward"/>
|
| 95 |
+
<Area type="monotone" dataKey="pn" stroke={RED} fill={RED} fillOpacity={.1} strokeWidth={2} dot={false} name="Penalty"/>
|
| 96 |
+
</AreaChart></ResponsiveContainer></Card>
|
| 97 |
+
</div>
|
| 98 |
+
</div>);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function AgentsTab({agents}:{agents:Agent[]}){
|
| 102 |
+
const[sort,setSort]=useState<"earned"|"tier"|"balance">("earned");
|
| 103 |
+
if(!agents.length)return<div className="flex items-center justify-center h-64 text-slate-400">No agents yet…</div>;
|
| 104 |
+
const s=[...agents].sort((a,b)=>sort==="tier"?b.current_tier-a.current_tier||b.total_earned-a.total_earned:sort==="balance"?b.balance-a.balance:b.total_earned-a.total_earned);
|
| 105 |
+
const tierCounts:Record<number,number>={};agents.forEach(a=>{tierCounts[a.current_tier]=(tierCounts[a.current_tier]||0)+1});
|
| 106 |
+
const pieData=Object.entries(tierCounts).map(([t,c])=>({name:`T${t}`,value:c,fill:TC[Number(t)]||"#94a3b8"}));
|
| 107 |
+
const robData=agents.filter(a=>a.robustness).map(a=>({name:a.model_name.length>12?a.model_name.slice(0,12)+"…":a.model_name,CC:Math.round((a.robustness?.cc||0)*100),ER:Math.round((a.robustness?.er||0)*100),AS:Math.round((a.robustness?.as_||0)*100)}));
|
| 108 |
+
return(<div className="space-y-6">
|
| 109 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
| 110 |
+
<Card className="p-5"><h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-4">Tier Distribution</h3>
|
| 111 |
+
<ResponsiveContainer width="100%" height={200}><PieChart><Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" innerRadius={50} outerRadius={80} paddingAngle={3} label={({name,value})=>`${name}: ${value}`} labelLine={false} fontSize={11}>
|
| 112 |
+
{pieData.map((e,i)=><Cell key={i} fill={e.fill} stroke="none"/>)}</Pie><Tooltip contentStyle={tt}/></PieChart></ResponsiveContainer></Card>
|
| 113 |
+
<Card className="p-5"><h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-4">Robustness Profile</h3>
|
| 114 |
+
<ResponsiveContainer width="100%" height={200}><BarChart data={robData} barGap={1} barSize={8}>
|
| 115 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9"/><XAxis dataKey="name" tick={{fontSize:8,fill:"#94a3b8"}} interval={0} angle={-20} textAnchor="end" height={50}/><YAxis domain={[0,100]} tick={{fontSize:9,fill:"#94a3b8"}}/><Tooltip contentStyle={tt}/>
|
| 116 |
+
<Bar dataKey="CC" fill={VIOLET} radius={[2,2,0,0]}/><Bar dataKey="ER" fill={AMBER} radius={[2,2,0,0]}/><Bar dataKey="AS" fill={BLUE} radius={[2,2,0,0]}/>
|
| 117 |
+
</BarChart></ResponsiveContainer></Card>
|
| 118 |
+
</div>
|
| 119 |
+
<div className="flex items-center gap-2">
|
| 120 |
+
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold">Sort</span>
|
| 121 |
+
{(["earned","tier","balance"] as const).map(x=>(<button key={x} onClick={()=>setSort(x)} className={`text-[11px] px-3 py-1 rounded-full border font-semibold capitalize transition-all ${sort===x?"border-violet-300 text-violet-700 bg-violet-50 shadow-sm":"border-slate-200 text-slate-400 hover:text-slate-600 hover:border-slate-300"}`}>{x}</button>))}
|
| 122 |
+
</div>
|
| 123 |
+
<Card className="overflow-hidden">
|
| 124 |
+
<table className="w-full text-sm">
|
| 125 |
+
<thead><tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-100 bg-slate-50/60">
|
| 126 |
+
<th className="px-5 py-3 text-left">Agent</th><th className="px-3 py-3 text-left">Strategy</th><th className="px-3 py-3 text-center">Tier</th>
|
| 127 |
+
<th className="px-3 py-3 text-right">Balance</th><th className="px-3 py-3 text-right">Earned</th><th className="px-3 py-3 text-right">Penalties</th>
|
| 128 |
+
<th className="px-3 py-3 text-center">W/L</th><th className="px-3 py-3 text-left" style={{minWidth:200}}>Robustness</th><th className="px-3 py-3 text-center">⬤</th>
|
| 129 |
+
</tr></thead>
|
| 130 |
+
<tbody>{s.map(a=>(
|
| 131 |
+
<tr key={a.agent_id} className="border-b border-slate-50 hover:bg-violet-50/30 transition-colors">
|
| 132 |
+
<td className="px-5 py-3.5"><div className="font-bold text-slate-800">{a.model_name}</div><Addr id={a.wallet_address||a.agent_id}/></td>
|
| 133 |
+
<td className="px-3 py-3.5 text-slate-500 capitalize text-xs font-medium">{a.strategy}</td>
|
| 134 |
+
<td className="px-3 py-3.5 text-center"><TB t={a.current_tier}/></td>
|
| 135 |
+
<td className="px-3 py-3.5 text-right font-mono text-xs text-slate-700">Ξ {a.balance.toFixed(4)}</td>
|
| 136 |
+
<td className="px-3 py-3.5 text-right font-mono text-xs font-bold text-emerald-600">Ξ {a.total_earned.toFixed(4)}</td>
|
| 137 |
+
<td className="px-3 py-3.5 text-right font-mono text-xs text-red-500">{a.total_penalties.toFixed(4)}</td>
|
| 138 |
+
<td className="px-3 py-3.5 text-center text-xs"><span className="text-emerald-600 font-bold">{a.contracts_completed}</span><span className="text-slate-300 mx-0.5">/</span><span className="text-red-500">{a.contracts_failed}</span></td>
|
| 139 |
+
<td className="px-3 py-3.5">{a.robustness?<div className="space-y-0.5"><RB l="CC" v={a.robustness.cc}/><RB l="ER" v={a.robustness.er}/><RB l="AS" v={a.robustness.as_}/><RB l="IH" v={a.robustness.ih}/></div>:<span className="text-slate-300">—</span>}</td>
|
| 140 |
+
<td className="px-3 py-3.5 text-center"><span className={`inline-block w-2.5 h-2.5 rounded-full ${a.status==="active"?"bg-emerald-500":"bg-slate-300"}`}/></td>
|
| 141 |
+
</tr>))}</tbody>
|
| 142 |
+
</table>
|
| 143 |
+
</Card>
|
| 144 |
+
</div>);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
function TradesTab({trades}:{trades:Trade[]}){
|
| 148 |
+
const[exp,setExp]=useState<Set<number>>(new Set());
|
| 149 |
+
if(!trades.length)return<div className="flex items-center justify-center h-64 text-slate-400">No trades yet…</div>;
|
| 150 |
+
const sorted=[...trades].reverse();
|
| 151 |
+
const tog=(i:number)=>{setExp(p=>{const n=new Set(p);n.has(i)?n.delete(i):n.add(i);return n})};
|
| 152 |
+
const passed=trades.filter(t=>t.passed).length;
|
| 153 |
+
return(<div className="space-y-4">
|
| 154 |
+
<div className="grid grid-cols-3 gap-4">
|
| 155 |
+
<Stat label="Total Trades" value={String(trades.length)}/>
|
| 156 |
+
<Stat label="Passed" value={String(passed)} sub={`${trades.length?(passed/trades.length*100).toFixed(0):"0"}% pass rate`}/>
|
| 157 |
+
<Stat label="Failed" value={String(trades.length-passed)}/>
|
| 158 |
+
</div>
|
| 159 |
+
<div className="space-y-2">
|
| 160 |
+
{sorted.map((t,i)=>{const o=exp.has(i);const tn=parseInt(t.tier.replace("T",""))||0;return(
|
| 161 |
+
<Card key={i} className={`overflow-hidden transition-all ${o?"ring-2 ring-violet-200":""}`}>
|
| 162 |
+
<button onClick={()=>tog(i)} className="w-full flex items-center gap-3 px-5 py-3.5 text-left hover:bg-violet-50/30 transition-colors">
|
| 163 |
+
<span className="text-[10px] text-slate-400 font-mono w-8">R{t.round+1}</span>
|
| 164 |
+
<span className={`text-[10px] font-extrabold w-10 ${t.passed?"text-emerald-600":"text-red-500"}`}>{t.passed?"PASS":"FAIL"}</span>
|
| 165 |
+
<span className="text-xs font-bold w-44 truncate text-slate-800">{t.agent}</span>
|
| 166 |
+
<TB t={tn}/>
|
| 167 |
+
<span className="text-[10px] text-slate-400 w-20 capitalize font-medium">{t.domain}</span>
|
| 168 |
+
<span className="text-[11px] text-slate-500 flex-1 truncate">{t.task_id}</span>
|
| 169 |
+
<span className="text-xs font-mono font-bold w-28 text-right" style={{color:t.passed?GREEN:RED}}>
|
| 170 |
+
{t.passed?`+Ξ ${t.reward.toFixed(4)}`:`-Ξ ${t.penalty.toFixed(4)}`}
|
| 171 |
+
</span>
|
| 172 |
+
<svg className={`w-4 h-4 text-slate-400 transition-transform ${o?"rotate-180":""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/></svg>
|
| 173 |
+
</button>
|
| 174 |
+
{o&&(
|
| 175 |
+
<div className="border-t border-slate-100 px-5 py-4 bg-slate-50/40 space-y-4">
|
| 176 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
|
| 177 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-0.5">Task</p><p className="text-slate-700">{t.task_id}</p></div>
|
| 178 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-0.5">Domain / Tier</p><p className="text-slate-700 capitalize">{t.domain} · <TB t={tn}/></p></div>
|
| 179 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-0.5">Token Cost</p><p className="font-mono text-slate-700">Ξ {t.token_cost.toFixed(6)}</p></div>
|
| 180 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-0.5">Latency</p><p className="text-slate-700">{t.latency_ms.toFixed(0)} ms</p></div>
|
| 181 |
+
</div>
|
| 182 |
+
{(t.constraints_passed.length>0||t.constraints_failed.length>0)&&(
|
| 183 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-1.5">Constraints</p>
|
| 184 |
+
<div className="flex flex-wrap gap-1.5">
|
| 185 |
+
{t.constraints_passed.map((c,j)=><span key={`p${j}`} className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">✓ {c}</span>)}
|
| 186 |
+
{t.constraints_failed.map((c,j)=><span key={`f${j}`} className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-red-50 text-red-600 border border-red-200">✗ {c}</span>)}
|
| 187 |
+
</div></div>)}
|
| 188 |
+
{t.task_prompt&&<div><p className="text-[10px] text-slate-400 font-semibold mb-1.5">Task Prompt</p><pre className="text-[11px] text-slate-600 bg-white rounded-xl p-3.5 overflow-x-auto max-h-48 whitespace-pre-wrap border border-slate-200 shadow-inner">{t.task_prompt}</pre></div>}
|
| 189 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-1.5">Agent Output</p><pre className="text-[11px] text-slate-500 bg-white rounded-xl p-3.5 overflow-x-auto max-h-40 whitespace-pre-wrap border border-slate-200 shadow-inner">{t.output_preview}</pre></div>
|
| 190 |
+
</div>)}
|
| 191 |
+
</Card>);})}
|
| 192 |
+
</div>
|
| 193 |
+
</div>);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function OnChainTab(){
|
| 197 |
+
const contracts = usePoll<any>(`${API}/api/contracts`, 10000);
|
| 198 |
+
const explorer = contracts?.explorer || "https://chainscan-galileo.0g.ai";
|
| 199 |
+
const registry = contracts?.contracts?.CGAERegistry?.address || "";
|
| 200 |
+
const escrow = contracts?.contracts?.CGAEEscrow?.address || "";
|
| 201 |
+
return(<div className="space-y-5">
|
| 202 |
+
<Card className="p-6"><h3 className="text-sm font-bold text-slate-800 mb-4">0G Chain — Galileo Testnet</h3>
|
| 203 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 text-sm">
|
| 204 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-1">CGAERegistry</p><code className="text-xs text-violet-600 break-all font-semibold">{registry||"Not deployed"}</code></div>
|
| 205 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-1">CGAEEscrow</p><code className="text-xs text-violet-600 break-all font-semibold">{escrow||"Not deployed"}</code></div>
|
| 206 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-1">Chain ID</p><p className="text-slate-700">16602</p></div>
|
| 207 |
+
<div><p className="text-[10px] text-slate-400 font-semibold mb-1">Architecture</p><p className="text-slate-500 text-xs">Agent wallets · ETH escrow · Weakest-link gate on-chain · Budget ceiling enforcement</p></div>
|
| 208 |
+
</div>
|
| 209 |
+
{registry&&<div className="mt-5"><a href={`${explorer}/address/${registry}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 text-xs px-4 py-2.5 rounded-xl font-bold text-white bg-gradient-to-r from-violet-600 to-blue-600 hover:from-violet-700 hover:to-blue-700 shadow-md shadow-violet-200 transition-all">View on 0G Explorer ↗</a></div>}
|
| 210 |
+
</Card>
|
| 211 |
+
<Card className="p-6"><h3 className="text-sm font-bold text-slate-800 mb-3">Verification Flow</h3>
|
| 212 |
+
<div className="flex items-center gap-2 text-xs text-slate-500 flex-wrap">
|
| 213 |
+
{["audit_live()","→","[CC, ER, AS, IH]","→","0G Storage","→","Merkle root hash","→","CGAERegistry.certify()","→","Tier assigned"].map((step,i)=>(
|
| 214 |
+
<span key={i} className={i%2===0?"text-slate-700 font-semibold bg-slate-100 px-2 py-0.5 rounded-md":"text-violet-400 font-bold"}>{step}</span>))}
|
| 215 |
+
</div>
|
| 216 |
+
<p className="text-xs text-slate-400 mt-3">Anyone can fetch the root hash from the registry, download from 0G Storage, verify the Merkle proof, and confirm scores match.</p>
|
| 217 |
+
</Card>
|
| 218 |
+
</div>);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
export default function Dashboard(){
|
| 222 |
+
const[tab,setTab]=useState<"economy"|"agents"|"trades"|"onchain">("economy");
|
| 223 |
+
const st=usePoll<{status:string;round:number;total_rounds:number;economy:Economy|null}>(`${API}/api/state`,POLL_MS);
|
| 224 |
+
const ag=usePoll<{agents:Agent[]}>(`${API}/api/agents`,POLL_MS);
|
| 225 |
+
const tr=usePoll<{trades:Trade[]}>(`${API}/api/trades?limit=200`,POLL_MS);
|
| 226 |
+
const ts=usePoll<any>(`${API}/api/timeseries`,POLL_MS);
|
| 227 |
+
const ev=usePoll<{events:Evt[]}>(`${API}/api/events?limit=200`,POLL_MS);
|
| 228 |
+
const status=st?.status||"connecting";
|
| 229 |
+
const round=st?.round||0;
|
| 230 |
+
const totalR=st?.total_rounds||0;
|
| 231 |
+
const tabs=[{id:"economy" as const,l:"📈 Economy"},{id:"agents" as const,l:`🛡️ Agents${ag?.agents?` (${ag.agents.length})`:""}`},{id:"trades" as const,l:`⚡ Trades${tr?.trades?` (${tr.trades.length})`:""}`},{id:"onchain" as const,l:"🔗 On-Chain"}];
|
| 232 |
+
|
| 233 |
+
return(<div className="min-h-screen" style={{background:"linear-gradient(135deg, #ede9fe 0%, #e0f2fe 30%, #faf5ff 60%, #fce7f3 100%)"}}>
|
| 234 |
+
<header className="bg-white/70 backdrop-blur-md border-b border-white/50 px-6 py-3.5 sticky top-0 z-50 shadow-sm">
|
| 235 |
+
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
| 236 |
+
<div className="flex items-center gap-5">
|
| 237 |
+
<div className="flex items-center gap-2.5">
|
| 238 |
+
<div className="w-8 h-8 rounded-xl bg-gradient-to-br from-violet-600 to-blue-600 flex items-center justify-center text-white text-sm font-black shadow-lg shadow-violet-200">◆</div>
|
| 239 |
+
<div><h1 className="text-sm font-extrabold text-slate-800">CGAE</h1><p className="text-[10px] text-slate-400 font-medium -mt-0.5">Agent Economy</p></div>
|
| 240 |
+
</div>
|
| 241 |
+
<div className="hidden sm:flex items-center gap-3 text-xs border-l border-slate-200 pl-5">
|
| 242 |
+
<StatusDot s={status}/>
|
| 243 |
+
{round>0&&<span className="text-slate-500">Round <span className="font-bold text-slate-800">{round}</span>{totalR>0?`/${totalR}`:""}</span>}
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
<a href="https://chainscan-galileo.0g.ai" target="_blank" rel="noopener noreferrer" className="text-[11px] px-4 py-2 rounded-xl font-bold text-white bg-gradient-to-r from-violet-600 to-blue-600 hover:from-violet-700 hover:to-blue-700 shadow-md shadow-violet-200 transition-all">0G Explorer ↗</a>
|
| 247 |
+
</div>
|
| 248 |
+
</header>
|
| 249 |
+
<nav className="bg-white/50 backdrop-blur-md border-b border-white/40 px-6 sticky top-[57px] z-40">
|
| 250 |
+
<div className="max-w-7xl mx-auto flex">
|
| 251 |
+
{tabs.map(t=>(<button key={t.id} onClick={()=>setTab(t.id)} className={`px-5 py-3 text-xs font-bold border-b-2 transition-all ${tab===t.id?"border-violet-500 text-violet-700":"border-transparent text-slate-400 hover:text-slate-600"}`}>{t.l}</button>))}
|
| 252 |
+
</div>
|
| 253 |
+
</nav>
|
| 254 |
+
<main className="max-w-7xl mx-auto px-6 py-6">
|
| 255 |
+
{tab==="economy"&&<EconomyTab eco={st?.economy||null} ts={ts} events={ev?.events||[]}/>}
|
| 256 |
+
{tab==="agents"&&<AgentsTab agents={ag?.agents||[]}/>}
|
| 257 |
+
{tab==="trades"&&<TradesTab trades={tr?.trades||[]}/>}
|
| 258 |
+
{tab==="onchain"&&<OnChainTab/>}
|
| 259 |
+
</main>
|
| 260 |
+
<footer className="text-center text-[10px] text-slate-400 font-medium py-5">CGAE · Comprehension-Gated Agent Economy · ETH OpenAgents 2026</footer>
|
| 261 |
+
</div>);
|
| 262 |
+
}
|
dashboard-next/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
dashboard-next/next-env.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="next" />
|
| 2 |
+
/// <reference types="next/image-types/global" />
|
| 3 |
+
import "./.next/dev/types/routes.d.ts";
|
| 4 |
+
|
| 5 |
+
// NOTE: This file should not be edited
|
| 6 |
+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
dashboard-next/next.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
async rewrites() {
|
| 5 |
+
return [
|
| 6 |
+
{
|
| 7 |
+
source: "/api/:path*",
|
| 8 |
+
destination: "http://localhost:8000/api/:path*",
|
| 9 |
+
},
|
| 10 |
+
];
|
| 11 |
+
},
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export default nextConfig;
|
dashboard-next/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cgae-dashboard",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev --port 3000",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"next": "16.2.4",
|
| 12 |
+
"react": "19.2.4",
|
| 13 |
+
"react-dom": "19.2.4",
|
| 14 |
+
"recharts": "^3.8.1"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@tailwindcss/postcss": "^4",
|
| 18 |
+
"@types/node": "^20",
|
| 19 |
+
"@types/react": "^19",
|
| 20 |
+
"@types/react-dom": "^19",
|
| 21 |
+
"eslint": "^9",
|
| 22 |
+
"eslint-config-next": "16.2.4",
|
| 23 |
+
"tailwindcss": "^4",
|
| 24 |
+
"typescript": "^5"
|
| 25 |
+
}
|
| 26 |
+
}
|
dashboard-next/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
dashboard-next/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "react-jsx",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": [
|
| 26 |
+
"next-env.d.ts",
|
| 27 |
+
"**/*.ts",
|
| 28 |
+
"**/*.tsx",
|
| 29 |
+
".next/types/**/*.ts",
|
| 30 |
+
".next/dev/types/**/*.ts",
|
| 31 |
+
"**/*.mts"
|
| 32 |
+
],
|
| 33 |
+
"exclude": ["node_modules"]
|
| 34 |
+
}
|
server/api.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CGAE Live Economy Server — ETH / 0G Chain
|
| 3 |
+
|
| 4 |
+
Runs the LiveSimulationRunner in a background thread and exposes
|
| 5 |
+
real-time state via REST endpoints for the dashboard.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python -m server.api # default 20 rounds
|
| 9 |
+
python -m server.api --rounds 50
|
| 10 |
+
python -m server.api --rounds -1 # infinite
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
+
import threading
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
from fastapi import FastAPI
|
| 20 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 21 |
+
|
| 22 |
+
app = FastAPI(title="CGAE Live Economy")
|
| 23 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger("cgae.api")
|
| 26 |
+
|
| 27 |
+
_state: dict = {
|
| 28 |
+
"status": "idle",
|
| 29 |
+
"round": 0,
|
| 30 |
+
"total_rounds": 0,
|
| 31 |
+
"economy": None,
|
| 32 |
+
"agents": {},
|
| 33 |
+
"trades": [],
|
| 34 |
+
"events": [],
|
| 35 |
+
"time_series": {"safety": [], "balance": [], "rewards": [], "penalties": []},
|
| 36 |
+
}
|
| 37 |
+
_state_lock = threading.Lock()
|
| 38 |
+
MAX_TRADES = 500
|
| 39 |
+
|
| 40 |
+
DEPLOYED = Path(__file__).resolve().parents[1] / "contracts" / "deployed.json"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _run_economy(num_rounds: int, initial_balance: float):
|
| 44 |
+
import sys, os
|
| 45 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 46 |
+
os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
|
| 47 |
+
|
| 48 |
+
from dotenv import load_dotenv
|
| 49 |
+
load_dotenv(Path(__file__).resolve().parents[1] / ".env", override=True)
|
| 50 |
+
|
| 51 |
+
from server.live_runner import LiveSimulationRunner, LiveSimConfig
|
| 52 |
+
|
| 53 |
+
config = LiveSimConfig(
|
| 54 |
+
num_rounds=num_rounds,
|
| 55 |
+
initial_balance=initial_balance,
|
| 56 |
+
run_live_audit=False,
|
| 57 |
+
self_verify=True,
|
| 58 |
+
max_retries=1,
|
| 59 |
+
test_eth_top_up_threshold=0.05,
|
| 60 |
+
test_eth_top_up_amount=0.3,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
runner = LiveSimulationRunner(config)
|
| 64 |
+
|
| 65 |
+
with _state_lock:
|
| 66 |
+
_state["status"] = "setup"
|
| 67 |
+
_state["total_rounds"] = num_rounds
|
| 68 |
+
|
| 69 |
+
runner.setup()
|
| 70 |
+
|
| 71 |
+
with _state_lock:
|
| 72 |
+
_state["status"] = "running"
|
| 73 |
+
|
| 74 |
+
round_num = 0
|
| 75 |
+
infinite = num_rounds == -1
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
while infinite or round_num < num_rounds:
|
| 79 |
+
runner._reactivate_suspended_agents()
|
| 80 |
+
round_results = runner._run_round(round_num)
|
| 81 |
+
runner._round_summaries.append(round_results)
|
| 82 |
+
step_events = runner.economy.step()
|
| 83 |
+
|
| 84 |
+
safety = runner.economy.aggregate_safety()
|
| 85 |
+
agents_snapshot = {}
|
| 86 |
+
for aid, mname in runner.agent_model_map.items():
|
| 87 |
+
rec = runner.economy.registry.get_agent(aid)
|
| 88 |
+
if not rec:
|
| 89 |
+
continue
|
| 90 |
+
r = rec.current_robustness
|
| 91 |
+
agents_snapshot[aid] = {
|
| 92 |
+
"agent_id": aid,
|
| 93 |
+
"model_name": mname,
|
| 94 |
+
"strategy": _get_strategy(runner, mname),
|
| 95 |
+
"current_tier": rec.current_tier.value,
|
| 96 |
+
"balance": rec.balance,
|
| 97 |
+
"total_earned": rec.total_earned,
|
| 98 |
+
"total_penalties": rec.total_penalties,
|
| 99 |
+
"contracts_completed": rec.contracts_completed,
|
| 100 |
+
"contracts_failed": rec.contracts_failed,
|
| 101 |
+
"status": rec.status.value,
|
| 102 |
+
"wallet_address": rec.wallet_address,
|
| 103 |
+
"robustness": {
|
| 104 |
+
"cc": r.cc, "er": r.er, "as_": r.as_, "ih": r.ih,
|
| 105 |
+
} if r else None,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
trades = []
|
| 109 |
+
for tr in round_results.get("task_results", []):
|
| 110 |
+
trades.append({
|
| 111 |
+
"round": round_num,
|
| 112 |
+
"agent": tr["agent"],
|
| 113 |
+
"task_id": tr["task_id"],
|
| 114 |
+
"task_prompt": tr.get("task_prompt", ""),
|
| 115 |
+
"tier": tr["tier"],
|
| 116 |
+
"domain": tr["domain"],
|
| 117 |
+
"passed": tr["verification"]["overall_pass"],
|
| 118 |
+
"reward": tr["settlement"].get("reward", 0) if tr["settlement"] else 0,
|
| 119 |
+
"penalty": tr["settlement"].get("penalty", 0) if tr["settlement"] else 0,
|
| 120 |
+
"token_cost": tr.get("token_cost_eth", 0),
|
| 121 |
+
"latency_ms": tr.get("latency_ms", 0),
|
| 122 |
+
"output_preview": tr.get("output_preview", ""),
|
| 123 |
+
"constraints_passed": tr["verification"].get("constraints_passed", []),
|
| 124 |
+
"constraints_failed": tr["verification"].get("constraints_failed", []),
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
# Capture protocol events from step
|
| 128 |
+
for aid in step_events.get("agents_demoted", []):
|
| 129 |
+
mname = runner.agent_model_map.get(aid, aid)
|
| 130 |
+
with _state_lock:
|
| 131 |
+
_state["events"].append({"timestamp": runner.economy.current_time, "type": "DEMOTION", "agent": mname, "message": f"{mname} demoted after spot-audit failure"})
|
| 132 |
+
|
| 133 |
+
with _state_lock:
|
| 134 |
+
_state["round"] = round_num + 1
|
| 135 |
+
_state["economy"] = {
|
| 136 |
+
"aggregate_safety": safety,
|
| 137 |
+
"active_agents": len(runner.economy.registry.active_agents),
|
| 138 |
+
"total_balance": sum(a["balance"] for a in agents_snapshot.values()),
|
| 139 |
+
"total_earned": sum(a["total_earned"] for a in agents_snapshot.values()),
|
| 140 |
+
"contracts_completed": sum(a["contracts_completed"] for a in agents_snapshot.values()),
|
| 141 |
+
"contracts_failed": sum(a["contracts_failed"] for a in agents_snapshot.values()),
|
| 142 |
+
}
|
| 143 |
+
_state["agents"] = agents_snapshot
|
| 144 |
+
_state["trades"] = (_state["trades"] + trades)[-MAX_TRADES:]
|
| 145 |
+
_state["time_series"]["safety"].append(safety)
|
| 146 |
+
_state["time_series"]["balance"].append(_state["economy"]["total_balance"])
|
| 147 |
+
_state["time_series"]["rewards"].append(round_results.get("total_reward", 0))
|
| 148 |
+
_state["time_series"]["penalties"].append(round_results.get("total_penalty", 0))
|
| 149 |
+
|
| 150 |
+
round_num += 1
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.exception(f"Economy runner failed: {e}")
|
| 154 |
+
finally:
|
| 155 |
+
with _state_lock:
|
| 156 |
+
_state["status"] = "done"
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _get_strategy(runner, model_name: str) -> str:
|
| 160 |
+
auto = runner.autonomous_agents.get(model_name)
|
| 161 |
+
if auto is None:
|
| 162 |
+
return "unknown"
|
| 163 |
+
return type(auto.strategy).__name__.replace("Strategy", "").lower()
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@app.get("/api/state")
|
| 167 |
+
def get_state():
|
| 168 |
+
with _state_lock:
|
| 169 |
+
return {"status": _state["status"], "round": _state["round"], "total_rounds": _state["total_rounds"], "economy": _state["economy"]}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@app.get("/api/agents")
|
| 173 |
+
def get_agents():
|
| 174 |
+
with _state_lock:
|
| 175 |
+
return {"agents": list(_state["agents"].values())}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
@app.get("/api/trades")
|
| 179 |
+
def get_trades(limit: int = 100):
|
| 180 |
+
with _state_lock:
|
| 181 |
+
return {"trades": _state["trades"][-limit:]}
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@app.get("/api/events")
|
| 185 |
+
def get_events(limit: int = 100):
|
| 186 |
+
with _state_lock:
|
| 187 |
+
return {"events": _state["events"][-limit:]}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@app.get("/api/timeseries")
|
| 191 |
+
def get_timeseries():
|
| 192 |
+
with _state_lock:
|
| 193 |
+
return _state["time_series"]
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@app.get("/api/contracts")
|
| 197 |
+
def get_contracts():
|
| 198 |
+
if DEPLOYED.exists():
|
| 199 |
+
return json.loads(DEPLOYED.read_text())
|
| 200 |
+
return {}
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
_runner_thread = None
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def start_economy(rounds: int = 20, balance: float = 0.5):
|
| 207 |
+
global _runner_thread
|
| 208 |
+
if _runner_thread and _runner_thread.is_alive():
|
| 209 |
+
return
|
| 210 |
+
_runner_thread = threading.Thread(target=_run_economy, args=(rounds, balance), daemon=True)
|
| 211 |
+
_runner_thread.start()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@app.on_event("startup")
|
| 215 |
+
async def on_startup():
|
| 216 |
+
import sys
|
| 217 |
+
rounds = 20
|
| 218 |
+
for i, arg in enumerate(sys.argv):
|
| 219 |
+
if arg == "--rounds" and i + 1 < len(sys.argv):
|
| 220 |
+
rounds = int(sys.argv[i + 1])
|
| 221 |
+
start_economy(rounds=rounds)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
if __name__ == "__main__":
|
| 225 |
+
import uvicorn
|
| 226 |
+
parser = argparse.ArgumentParser()
|
| 227 |
+
parser.add_argument("--rounds", type=int, default=20)
|
| 228 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 229 |
+
args = parser.parse_args()
|
| 230 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
| 231 |
+
uvicorn.run(app, host="0.0.0.0", port=args.port)
|