nice-bill commited on
Commit
3cfece7
·
1 Parent(s): de2540f

Add summary and agent profit methods to supabase client

Browse files
api/supabase_client.py CHANGED
@@ -72,6 +72,13 @@ class MetricsData:
72
  pool_stability: float = 0
73
 
74
 
 
 
 
 
 
 
 
75
  class SupabaseClient:
76
  """Client for interacting with Supabase database."""
77
 
@@ -240,6 +247,39 @@ class SupabaseClient:
240
  "metrics": metrics
241
  }
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  # ==================== UTILITY ====================
244
 
245
  def health_check(self) -> bool:
 
72
  pool_stability: float = 0
73
 
74
 
75
+ @dataclass
76
+ class SummaryData:
77
+ """Data class for run summary."""
78
+ run_id: int
79
+ summary_text: str
80
+
81
+
82
  class SupabaseClient:
83
  """Client for interacting with Supabase database."""
84
 
 
247
  "metrics": metrics
248
  }
249
 
250
+ # ==================== RUN SUMMARIES ====================
251
+
252
+ def save_run_summary(self, data: SummaryData):
253
+ """Save a run summary to database."""
254
+ self.client.table("run_summaries").insert({
255
+ "run_id": data.run_id,
256
+ "summary_text": data.summary_text
257
+ }).execute()
258
+
259
+ def get_run_summary(self, run_id: int) -> Optional[Dict]:
260
+ """Get summary for a specific run."""
261
+ response = self.client.table("run_summaries").select("*").eq("run_id", run_id).execute()
262
+ return response.data[0] if response.data else None
263
+
264
+ def get_all_summaries(self) -> List[Dict]:
265
+ """Get all run summaries."""
266
+ response = self.client.table("run_summaries").select("*").order("run_id", desc=True).execute()
267
+ return response.data
268
+
269
+ # ==================== AGENT PROFITS ====================
270
+
271
+ def get_agent_profits_all_runs(self, agent_name: str) -> List[Dict]:
272
+ """Get profit history for an agent across all runs."""
273
+ # Get the latest state for each run for this agent
274
+ response = self.client.table("agent_states").select("*").eq("agent_name", agent_name).order("run_id").execute()
275
+ return response.data
276
+
277
+ def get_all_agent_names(self) -> List[str]:
278
+ """Get all unique agent names."""
279
+ response = self.client.table("agent_states").select("agent_name").execute()
280
+ names = set(item["agent_name"] for item in response.data)
281
+ return sorted(list(names))
282
+
283
  # ==================== UTILITY ====================
284
 
285
  def health_check(self) -> bool:
core/simulation.py CHANGED
@@ -6,6 +6,7 @@ from dataclasses import dataclass
6
 
7
  from core.agent import Agent
8
  from core.defi_mechanics import Pool
 
9
  from api.supabase_client import (
10
  SupabaseClient, RunData, AgentStateData, PoolStateData, ActionData, MetricsData
11
  )
@@ -93,6 +94,14 @@ class Simulation:
93
  )
94
  )
95
 
 
 
 
 
 
 
 
 
96
  # Update agent learning
97
  for agent in self.agents:
98
  agent.update_learning(self.current_run_number, metrics)
 
6
 
7
  from core.agent import Agent
8
  from core.defi_mechanics import Pool
9
+ from core.summarizer import Summarizer
10
  from api.supabase_client import (
11
  SupabaseClient, RunData, AgentStateData, PoolStateData, ActionData, MetricsData
12
  )
 
94
  )
95
  )
96
 
97
+ # Generate and save run summary
98
+ try:
99
+ summarizer = Summarizer(supabase=self.supabase)
100
+ summary = summarizer.summarize_and_save(self.current_run_id)
101
+ print(f"Generated summary for run {self.current_run_id}")
102
+ except Exception as e:
103
+ print(f"Warning: Failed to generate summary - {e}")
104
+
105
  # Update agent learning
106
  for agent in self.agents:
107
  agent.update_learning(self.current_run_number, metrics)
core/summarizer.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run summarizer agent using MiniMax LLM."""
2
+
3
+ from typing import Dict, List, Any
4
+ from api.minimax_client import MiniMaxClient
5
+ from api.supabase_client import SupabaseClient, SummaryData
6
+
7
+
8
+ class Summarizer:
9
+ """Agent that generates detailed summaries of simulation runs."""
10
+
11
+ def __init__(self, supabase: SupabaseClient = None):
12
+ self.supabase = supabase
13
+ self.minimax = MiniMaxClient()
14
+
15
+ def generate_summary(self, run_id: int) -> str:
16
+ """Generate a detailed summary for a run."""
17
+ # Get run data
18
+ run_detail = self.supabase.get_run_detail(run_id)
19
+ metrics = run_detail.get("metrics", {})
20
+ actions = run_detail.get("actions", [])
21
+ agent_states = run_detail.get("agent_states", [])
22
+ pool_states = run_detail.get("pool_states", [])
23
+
24
+ # Get run info
25
+ runs = self.supabase.get_all_runs()
26
+ run_info = next((r for r in runs if r["id"] == run_id), {})
27
+ run_number = run_info.get("run_number", run_id)
28
+
29
+ # Analyze data
30
+ agent_performance = self._analyze_agents(agent_states)
31
+ action_distribution = self._analyze_actions(actions)
32
+ market_events = self._analyze_market_events(actions, pool_states)
33
+
34
+ # Build prompt
35
+ prompt = self._build_summary_prompt(
36
+ run_number=run_number,
37
+ metrics=metrics,
38
+ agent_performance=agent_performance,
39
+ action_distribution=action_distribution,
40
+ market_events=market_events
41
+ )
42
+
43
+ # Generate summary using LLM
44
+ response = self.minimax.complete(prompt, max_tokens=1024)
45
+
46
+ return response.strip()
47
+
48
+ def _analyze_agents(self, agent_states: List[Dict]) -> List[Dict]:
49
+ """Analyze agent performance."""
50
+ # Get latest state for each agent
51
+ latest_by_agent = {}
52
+ for state in agent_states:
53
+ agent = state["agent_name"]
54
+ if agent not in latest_by_agent or state["turn"] > latest_by_agent[agent]["turn"]:
55
+ latest_by_agent[agent] = state
56
+
57
+ performance = []
58
+ for agent, state in latest_by_agent.items():
59
+ performance.append({
60
+ "name": agent,
61
+ "profit": state.get("profit", 0),
62
+ "strategy": state.get("strategy", "unknown"),
63
+ "final_tokens": f"{state.get('token_a_balance', 0):.0f}A / {state.get('token_b_balance', 0):.0f}B"
64
+ })
65
+
66
+ # Sort by profit descending
67
+ performance.sort(key=lambda x: x["profit"], reverse=True)
68
+ return performance
69
+
70
+ def _analyze_actions(self, actions: List[Dict]) -> Dict[str, int]:
71
+ """Count action types."""
72
+ distribution = {}
73
+ for action in actions:
74
+ action_type = action.get("action_type", "unknown")
75
+ distribution[action_type] = distribution.get(action_type, 0) + 1
76
+ return distribution
77
+
78
+ def _analyze_market_events(self, actions: List[Dict], pool_states: List[Dict]) -> List[str]:
79
+ """Identify notable market events."""
80
+ events = []
81
+
82
+ # Find significant pool changes
83
+ if len(pool_states) >= 2:
84
+ first_reserve = pool_states[0]
85
+ last_reserve = pool_states[-1]
86
+
87
+ if first_reserve and last_reserve:
88
+ reserve_a_change = last_reserve["reserve_a"] - first_reserve["reserve_a"]
89
+ reserve_b_change = last_reserve["reserve_b"] - first_reserve["reserve_b"]
90
+
91
+ if abs(reserve_a_change) > 100 or abs(reserve_b_change) > 100:
92
+ events.append(f"Pool shifted: A {reserve_a_change:+.0f}, B {reserve_b_change:+.0f}")
93
+
94
+ # Count alliances
95
+ alliances = [a for a in actions if a.get("action_type") == "propose_alliance"]
96
+ if len(alliances) > 3:
97
+ events.append(f"{len(alliances)} alliance proposals made")
98
+
99
+ # Count trades
100
+ trades = [a for a in actions if a.get("action_type") == "swap"]
101
+ if len(trades) > 5:
102
+ events.append(f"{len(trades)} swap transactions executed")
103
+
104
+ return events[:5] # Limit to top 5 events
105
+
106
+ def _build_summary_prompt(
107
+ self,
108
+ run_number: int,
109
+ metrics: Dict,
110
+ agent_performance: List[Dict],
111
+ action_distribution: Dict[str, int],
112
+ market_events: List[str]
113
+ ) -> str:
114
+ """Build the summary prompt for the LLM."""
115
+
116
+ top_agents = agent_performance[:3] if agent_performance else []
117
+ bottom_agents = agent_performance[-2:] if len(agent_performance) > 2 else []
118
+
119
+ prompt = f"""Generate a detailed summary of this DeFi agent simulation run.
120
+
121
+ ## Run {run_number} Summary
122
+
123
+ ### Overall Metrics
124
+ - Gini Coefficient: {metrics.get('gini_coefficient', 0):.4f} (0=equal, 1=unequal)
125
+ - Average Agent Profit: {metrics.get('avg_agent_profit', 0):.2f}
126
+ - Cooperation Rate: {metrics.get('cooperation_rate', 0):.1f}%
127
+ - Pool Stability: {metrics.get('pool_stability', 0):.0f}
128
+
129
+ ### Agent Performance (ranked by profit)
130
+ """
131
+
132
+ for i, agent in enumerate(top_agents, 1):
133
+ prompt += f"{i}. {agent['name']}: {agent['profit']:+.2f} profit ({agent['strategy']})\n"
134
+
135
+ if bottom_agents and bottom_agents != top_agents:
136
+ prompt += "\nBottom performers:\n"
137
+ for agent in bottom_agents:
138
+ prompt += f"- {agent['name']}: {agent['profit']:+.2f} ({agent['strategy']})\n"
139
+
140
+ prompt += f"\n### Action Distribution\n"
141
+ for action_type, count in sorted(action_distribution.items(), key=lambda x: -x[1]):
142
+ prompt += f"- {action_type}: {count}\n"
143
+
144
+ if market_events:
145
+ prompt += "\n### Notable Market Events\n"
146
+ for event in market_events:
147
+ prompt += f"- {event}\n"
148
+
149
+ prompt += """
150
+ ### Analysis
151
+ Write a 2-3 paragraph analysis covering:
152
+ 1. Overall market behavior and whether agents cooperated or competed
153
+ 2. Notable strategy patterns and their effectiveness
154
+ 3. Key insights about the DeFi market dynamics
155
+
156
+ Keep the tone informative and analytical. Use markdown formatting for readability.
157
+ """
158
+
159
+ return prompt
160
+
161
+ def summarize_and_save(self, run_id: int) -> Dict:
162
+ """Generate summary and save to database."""
163
+ summary_text = self.generate_summary(run_id)
164
+
165
+ # Save to database
166
+ summary_data = SummaryData(run_id=run_id, summary_text=summary_text)
167
+ self.supabase.save_run_summary(summary_data)
168
+
169
+ return {
170
+ "run_id": run_id,
171
+ "summary": summary_text
172
+ }
frontend/src/App.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import { useState, useEffect, useRef } from 'react'
2
- import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
3
 
4
  const getApiBase = () => {
5
  const url = import.meta.env.VITE_API_URL || ''
@@ -14,11 +14,17 @@ function App() {
14
  const [activeTab, setActiveTab] = useState('dashboard')
15
  const [terminalLogs, setTerminalLogs] = useState([])
16
  const [isTerminalConnected, setIsTerminalConnected] = useState(false)
 
 
 
 
17
 
18
  useEffect(() => {
19
  fetchRuns()
20
  fetchTrends()
21
  fetchLatestRunForTerminal()
 
 
22
  }, [])
23
 
24
  const fetchRuns = async () => {
@@ -45,6 +51,61 @@ function App() {
45
  }
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  const fetchLatestRunForTerminal = async () => {
49
  try {
50
  const res = await fetch(`${API_BASE}/runs`)
@@ -148,6 +209,16 @@ function App() {
148
  >
149
  Dashboard
150
  </button>
 
 
 
 
 
 
 
 
 
 
151
  <button
152
  onClick={() => setActiveTab('terminal')}
153
  className={`pb-3 px-4 text-sm font-medium border-b-2 transition-colors ${
@@ -309,6 +380,78 @@ function App() {
309
  </section>
310
  )}
311
  </>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  ) : (
313
  /* Terminal Tab - Retro Style */
314
  <div className="bg-slate-800 border-slate-700 rounded-lg border p-6">
 
1
  import { useState, useEffect, useRef } from 'react'
2
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
3
 
4
  const getApiBase = () => {
5
  const url = import.meta.env.VITE_API_URL || ''
 
14
  const [activeTab, setActiveTab] = useState('dashboard')
15
  const [terminalLogs, setTerminalLogs] = useState([])
16
  const [isTerminalConnected, setIsTerminalConnected] = useState(false)
17
+ const [summaries, setSummaries] = useState([])
18
+ const [agents, setAgents] = useState([])
19
+ const [selectedAgent, setSelectedAgent] = useState(null)
20
+ const [agentProfitData, setAgentProfitData] = useState([])
21
 
22
  useEffect(() => {
23
  fetchRuns()
24
  fetchTrends()
25
  fetchLatestRunForTerminal()
26
+ fetchSummaries()
27
+ fetchAgents()
28
  }, [])
29
 
30
  const fetchRuns = async () => {
 
51
  }
52
  }
53
 
54
+ const fetchSummaries = async () => {
55
+ try {
56
+ const res = await fetch(`${API_BASE}/summaries`)
57
+ if (res.ok) {
58
+ const data = await res.json()
59
+ setSummaries(data.summaries || [])
60
+ }
61
+ } catch (e) {
62
+ console.error('Failed to fetch summaries:', e)
63
+ }
64
+ }
65
+
66
+ const fetchAgents = async () => {
67
+ try {
68
+ const res = await fetch(`${API_BASE}/agents`)
69
+ if (res.ok) {
70
+ const data = await res.json()
71
+ setAgents(data.agents || [])
72
+ if (data.agents?.length > 0) {
73
+ setSelectedAgent(data.agents[0])
74
+ }
75
+ }
76
+ } catch (e) {
77
+ console.error('Failed to fetch agents:', e)
78
+ }
79
+ }
80
+
81
+ const fetchAgentProfits = async (agentName) => {
82
+ if (!agentName) return
83
+ try {
84
+ const res = await fetch(`${API_BASE}/agents/${agentName}/profits`)
85
+ if (res.ok) {
86
+ const data = await res.json()
87
+ // Group by run to show profit trajectory
88
+ const grouped = {}
89
+ data.data?.forEach(item => {
90
+ const runKey = `Run ${item.run}`
91
+ if (!grouped[runKey]) {
92
+ grouped[runKey] = { run: item.run, name: runKey }
93
+ }
94
+ grouped[runKey].profit = item.profit
95
+ })
96
+ setAgentProfitData(Object.values(grouped))
97
+ }
98
+ } catch (e) {
99
+ console.error('Failed to fetch agent profits:', e)
100
+ }
101
+ }
102
+
103
+ useEffect(() => {
104
+ if (selectedAgent) {
105
+ fetchAgentProfits(selectedAgent)
106
+ }
107
+ }, [selectedAgent])
108
+
109
  const fetchLatestRunForTerminal = async () => {
110
  try {
111
  const res = await fetch(`${API_BASE}/runs`)
 
209
  >
210
  Dashboard
211
  </button>
212
+ <button
213
+ onClick={() => setActiveTab('summaries')}
214
+ className={`pb-3 px-4 text-sm font-medium border-b-2 transition-colors ${
215
+ activeTab === 'summaries'
216
+ ? 'border-green-500 text-white'
217
+ : 'border-transparent text-slate-400 hover:text-white'
218
+ }`}
219
+ >
220
+ Summaries
221
+ </button>
222
  <button
223
  onClick={() => setActiveTab('terminal')}
224
  className={`pb-3 px-4 text-sm font-medium border-b-2 transition-colors ${
 
380
  </section>
381
  )}
382
  </>
383
+ ) : activeTab === 'summaries' ? (
384
+ /* Summaries Tab */
385
+ <div className="space-y-8">
386
+ {/* Agent Profit Chart */}
387
+ <section className="bg-slate-800 border-slate-700 rounded-lg border p-6">
388
+ <div className="flex justify-between items-center mb-4">
389
+ <h2 className="text-lg font-medium text-white">Agent Profit Over Time</h2>
390
+ <select
391
+ value={selectedAgent || ''}
392
+ onChange={(e) => setSelectedAgent(e.target.value)}
393
+ className="px-4 py-2 rounded bg-slate-700 text-white border border-slate-600"
394
+ >
395
+ {agents.map(agent => (
396
+ <option key={agent} value={agent}>{agent}</option>
397
+ ))}
398
+ </select>
399
+ </div>
400
+ <div className="h-[300px]">
401
+ <ResponsiveContainer width="100%" height="100%">
402
+ <LineChart data={agentProfitData}>
403
+ <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
404
+ <XAxis dataKey="name" stroke="#94a3b8" />
405
+ <YAxis stroke="#94a3b8" />
406
+ <Tooltip
407
+ contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155' }}
408
+ labelStyle={{ color: '#fff' }}
409
+ />
410
+ <Line
411
+ type="monotone"
412
+ dataKey="profit"
413
+ stroke="#22c55e"
414
+ strokeWidth={2}
415
+ dot={{ fill: '#22c55e' }}
416
+ name="Profit"
417
+ />
418
+ </LineChart>
419
+ </ResponsiveContainer>
420
+ </div>
421
+ </section>
422
+
423
+ {/* Run Summaries */}
424
+ <section className="bg-slate-800 border-slate-700 rounded-lg border p-6">
425
+ <div className="flex justify-between items-center mb-4">
426
+ <h2 className="text-lg font-medium text-white">Run Summaries</h2>
427
+ <button
428
+ onClick={() => fetchSummaries()}
429
+ className="px-4 py-2 rounded bg-slate-700 text-white hover:bg-slate-600 text-sm"
430
+ >
431
+ Refresh
432
+ </button>
433
+ </div>
434
+ {summaries.length === 0 ? (
435
+ <p className="text-slate-400">No summaries yet. Summaries are generated after each run.</p>
436
+ ) : (
437
+ <div className="space-y-6">
438
+ {summaries.map((summary) => (
439
+ <div key={summary.id} className="border-b border-slate-700 pb-6 last:border-0 last:pb-0">
440
+ <div className="flex justify-between items-start mb-2">
441
+ <h3 className="text-white font-medium">Run {summary.run_id}</h3>
442
+ <span className="text-sm text-slate-500">
443
+ {new Date(summary.created_at).toLocaleString()}
444
+ </span>
445
+ </div>
446
+ <div className="text-slate-300 text-sm whitespace-pre-wrap">
447
+ {summary.summary_text}
448
+ </div>
449
+ </div>
450
+ ))}
451
+ </div>
452
+ )}
453
+ </section>
454
+ </div>
455
  ) : (
456
  /* Terminal Tab - Retro Style */
457
  <div className="bg-slate-800 border-slate-700 rounded-lg border p-6">
supabase/migrations/001_add_run_summaries.sql ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Migration: Add run_summaries table
2
+ -- Run this in Supabase SQL Editor
3
+
4
+ CREATE TABLE IF NOT EXISTS run_summaries (
5
+ id BIGSERIAL PRIMARY KEY,
6
+ run_id BIGINT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
7
+ summary_text TEXT NOT NULL,
8
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9
+ );
10
+
11
+ -- Index for fast lookups
12
+ CREATE INDEX IF NOT EXISTS idx_run_summaries_run_id ON run_summaries(run_id);
13
+
14
+ -- Enable RLS (Row Level Security)
15
+ ALTER TABLE run_summaries ENABLE ROW LEVEL SECURITY;
16
+
17
+ -- Policy to allow read access
18
+ CREATE POLICY IF NOT EXISTS "Allow public read access" ON run_summaries
19
+ FOR SELECT USING (true);
20
+
21
+ -- Policy to allow insert access (for authenticated users)
22
+ CREATE POLICY IF NOT EXISTS "Allow authenticated insert" ON run_summaries
23
+ FOR INSERT WITH CHECK (auth.role() = 'authenticated');
web/app.py CHANGED
@@ -9,6 +9,7 @@ import json
9
  from api.supabase_client import SupabaseClient
10
  from core.simulation import Simulation
11
  from core.analyzer import Analyzer
 
12
 
13
  app = FastAPI(
14
  title="DeFi Agents API",
@@ -218,6 +219,92 @@ def get_arms_race_analysis(run_id: int):
218
  raise HTTPException(status_code=500, detail=str(e))
219
 
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  def run_server():
222
  """Run the FastAPI server."""
223
  import uvicorn
 
9
  from api.supabase_client import SupabaseClient
10
  from core.simulation import Simulation
11
  from core.analyzer import Analyzer
12
+ from core.summarizer import Summarizer
13
 
14
  app = FastAPI(
15
  title="DeFi Agents API",
 
219
  raise HTTPException(status_code=500, detail=str(e))
220
 
221
 
222
+ # ==================== Summary Endpoints ====================
223
+
224
+ @app.get("/api/runs/{run_id}/summary")
225
+ def get_run_summary(run_id: int):
226
+ """Get summary for a specific run."""
227
+ if not supabase:
228
+ raise HTTPException(status_code=503, detail="Supabase not configured")
229
+
230
+ try:
231
+ summary = supabase.get_run_summary(run_id)
232
+ if not summary:
233
+ return {"run_id": run_id, "summary": None, "message": "No summary generated yet"}
234
+ return summary
235
+ except Exception as e:
236
+ raise HTTPException(status_code=500, detail=str(e))
237
+
238
+
239
+ @app.get("/api/summaries")
240
+ def get_all_summaries():
241
+ """Get all run summaries."""
242
+ if not supabase:
243
+ raise HTTPException(status_code=503, detail="Supabase not configured")
244
+
245
+ try:
246
+ summaries = supabase.get_all_summaries()
247
+ return {"summaries": summaries}
248
+ except Exception as e:
249
+ raise HTTPException(status_code=500, detail=str(e))
250
+
251
+
252
+ @app.post("/api/runs/{run_id}/generate-summary")
253
+ def generate_run_summary(run_id: int):
254
+ """Generate and save a summary for a run."""
255
+ if not supabase:
256
+ raise HTTPException(status_code=503, detail="Supabase not configured")
257
+
258
+ try:
259
+ summarizer = Summarizer(supabase=supabase)
260
+ result = summarizer.summarize_and_save(run_id)
261
+ return result
262
+ except Exception as e:
263
+ raise HTTPException(status_code=500, detail=str(e))
264
+
265
+
266
+ # ==================== Agent Profit Endpoints ====================
267
+
268
+ @app.get("/api/agents")
269
+ def get_all_agents():
270
+ """Get all unique agent names."""
271
+ if not supabase:
272
+ raise HTTPException(status_code=503, detail="Supabase not configured")
273
+
274
+ try:
275
+ agents = supabase.get_all_agent_names()
276
+ return {"agents": agents}
277
+ except Exception as e:
278
+ raise HTTPException(status_code=500, detail=str(e))
279
+
280
+
281
+ @app.get("/api/agents/{agent_name}/profits")
282
+ def get_agent_profits(agent_name: str):
283
+ """Get profit history for an agent across all runs."""
284
+ if not supabase:
285
+ raise HTTPException(status_code=503, detail="Supabase not configured")
286
+
287
+ try:
288
+ profits = supabase.get_agent_profits_all_runs(agent_name)
289
+
290
+ # Format for chart: [{run: 1, profit: 0, turn: 0}, ...]
291
+ chart_data = []
292
+ for p in profits:
293
+ chart_data.append({
294
+ "run": p["run_id"],
295
+ "turn": p["turn"],
296
+ "profit": p["profit"],
297
+ "strategy": p.get("strategy", "unknown")
298
+ })
299
+
300
+ return {
301
+ "agent_name": agent_name,
302
+ "data": chart_data
303
+ }
304
+ except Exception as e:
305
+ raise HTTPException(status_code=500, detail=str(e))
306
+
307
+
308
  def run_server():
309
  """Run the FastAPI server."""
310
  import uvicorn