ar9avg commited on
Commit
aa3ae1f
·
1 Parent(s): 8c8093f

Surface GEPA optimization: prompt history, live banner, smart retry

Browse files

Backend:
- Add /api/prompt-history endpoint (pareto front + query count + nextOptimizeAt)
- Emit gepa_start/gepa_done SSE events so banner shows during optimization
- Accept previousSql in execute-query to tell LLM to try a different approach

Frontend:
- Retry after marking "wrong" auto-executes with previous SQL as context
(button changes label to "Retry differently")
- gepa_done event refreshes PromptEvolution in right sidebar immediately
- PromptEvolution shows query count + progress bar toward next optimization
- PromptEvolution polls /api/prompt-history every 30s

backend/api/demo.py CHANGED
@@ -119,11 +119,39 @@ async def connect_db(req: ConnectDbRequest):
119
  return {"success": False, "message": message, "tables": [], "dbLabel": get_active_db_label()}
120
 
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  # ─── /api/execute-query ───────────────────────────────────────────
123
 
124
  class ExecuteQueryRequest(BaseModel):
125
  question: str
126
  task_id: str = "simple_queries"
 
 
127
 
128
 
129
  @router.post("/execute-query")
@@ -160,8 +188,17 @@ async def execute_query_stream(req: ExecuteQueryRequest):
160
 
161
  if attempt == 1 or ep.current_sql is None:
162
  system_prompt = BASE_SYSTEM_PROMPT
 
 
 
 
 
 
 
 
163
  user_msg = (
164
- f"Schema:\n{obs.schema_info}\n\nQuestion: {req.question}\n\n"
 
165
  "Write a SQL query to answer this question."
166
  )
167
  else:
@@ -376,12 +413,19 @@ async def execute_query_stream(req: ExecuteQueryRequest):
376
  env._episode.done = True
377
  env._episode.success = success
378
 
379
- # Trigger GEPA if needed
380
  if gepa.should_optimize():
 
381
  try:
382
- await gepa.run_optimization_cycle()
383
- except Exception:
384
- pass
 
 
 
 
 
 
385
 
386
  return EventSourceResponse(event_generator())
387
 
 
119
  return {"success": False, "message": message, "tables": [], "dbLabel": get_active_db_label()}
120
 
121
 
122
+ # ─── /api/prompt-history ─────────────────────────────────────────
123
+
124
+ @router.get("/prompt-history")
125
+ async def get_prompt_history():
126
+ import datetime
127
+ gepa = get_gepa()
128
+ pareto = gepa.get_pareto_front()
129
+ history = [
130
+ {
131
+ "generation": c.generation,
132
+ "prompt": c.system_prompt,
133
+ "score": c.score,
134
+ "summary": c.feedback[0][:200] if c.feedback else "Seed prompt",
135
+ "timestamp": datetime.datetime.utcnow().strftime("%Y-%m-%d"),
136
+ }
137
+ for c in sorted(pareto, key=lambda x: x.generation)
138
+ ]
139
+ return {
140
+ "prompt": gepa.get_current_prompt(),
141
+ "generation": gepa.current_generation,
142
+ "history": history,
143
+ "queryCount": len(gepa.get_history()),
144
+ "nextOptimizeAt": (len(gepa.get_history()) // 4 + 1) * 4,
145
+ }
146
+
147
+
148
  # ─── /api/execute-query ───────────────────────────────────────────
149
 
150
  class ExecuteQueryRequest(BaseModel):
151
  question: str
152
  task_id: str = "simple_queries"
153
+ previousSql: Optional[str] = None # SQL from a prior attempt user marked wrong
154
+ previousFeedback: Optional[str] = None # "wrong" context message
155
 
156
 
157
  @router.post("/execute-query")
 
188
 
189
  if attempt == 1 or ep.current_sql is None:
190
  system_prompt = BASE_SYSTEM_PROMPT
191
+ # Include previous wrong SQL if user retried after marking wrong
192
+ prev_context = ""
193
+ if req.previousSql:
194
+ prev_context = (
195
+ f"\nNOTE: A previous attempt generated the following SQL which was marked INCORRECT:\n"
196
+ f"```sql\n{req.previousSql}\n```\n"
197
+ f"You MUST try a completely different approach.\n"
198
+ )
199
  user_msg = (
200
+ f"Schema:\n{obs.schema_info}\n\nQuestion: {req.question}\n"
201
+ f"{prev_context}\n"
202
  "Write a SQL query to answer this question."
203
  )
204
  else:
 
413
  env._episode.done = True
414
  env._episode.success = success
415
 
416
+ # Trigger GEPA if needed — emit events so frontend shows banner
417
  if gepa.should_optimize():
418
+ yield {"data": json.dumps({"type": "gepa_start"})}
419
  try:
420
+ gepa_result = await gepa.run_optimization_cycle()
421
+ yield {"data": json.dumps({
422
+ "type": "gepa_done",
423
+ "generation": gepa.current_generation,
424
+ "reflection": gepa_result.get("reflection", "")[:300] if gepa_result else "",
425
+ })}
426
+ except Exception as e:
427
+ logger.error("GEPA optimization failed: %s", e)
428
+ yield {"data": json.dumps({"type": "gepa_done", "generation": gepa.current_generation, "reflection": ""})}
429
 
430
  return EventSourceResponse(event_generator())
431
 
frontend/src/components/ChatPanel.tsx CHANGED
@@ -5,7 +5,7 @@ import {
5
  Loader2, MessageSquare, Zap, RefreshCw, Trash2,
6
  } from 'lucide-react'
7
  import { useStore } from '../store/useStore'
8
- import { streamExecuteQuery, submitFeedback } from '../lib/api'
9
  import { ResultsTable } from './ResultsTable'
10
  import type { ChatMessage, AttemptStep } from '../lib/types'
11
 
@@ -202,7 +202,7 @@ function MessageCard({
202
  }: {
203
  msg: ChatMessage
204
  onFeedback: (id: string, correct: boolean) => Promise<void>
205
- onRetry: (q: string) => void
206
  }) {
207
  const [sqlOpen, setSqlOpen] = useState(true)
208
 
@@ -343,11 +343,14 @@ function MessageCard({
343
  )}
344
  {(msg.status === 'done' || msg.status === 'error') && (
345
  <button
346
- onClick={() => onRetry(msg.question)}
 
 
 
347
  className="ml-auto flex items-center gap-1 text-[10px] text-gray-600 hover:text-gray-400 transition-colors"
348
  >
349
  <RefreshCw size={10} />
350
- Retry
351
  </button>
352
  )}
353
  </div>
@@ -392,7 +395,7 @@ export function ChatPanel() {
392
  )
393
 
394
  const execute = useCallback(
395
- async (question: string) => {
396
  if (!question.trim() || isExecuting) return
397
  setIsExecuting(true)
398
 
@@ -412,7 +415,7 @@ export function ChatPanel() {
412
  addMessage(newMsg)
413
 
414
  try {
415
- for await (const event of streamExecuteQuery(question, taskId)) {
416
  if (event.type === 'sql') {
417
  updateMessage(msgId, { sql: event.sql as string })
418
  } else if (event.type === 'sql_chunk') {
@@ -457,6 +460,10 @@ export function ChatPanel() {
457
  setOptimizingBanner(true)
458
  } else if (event.type === 'gepa_done') {
459
  setOptimizingBanner(false)
 
 
 
 
460
  }
461
  }
462
  } catch (err) {
@@ -524,7 +531,7 @@ export function ChatPanel() {
524
  key={msg.id}
525
  msg={msg}
526
  onFeedback={handleFeedback}
527
- onRetry={(q) => { setInput(q); inputRef.current?.focus() }}
528
  />
529
  ))}
530
  <div ref={bottomRef} />
 
5
  Loader2, MessageSquare, Zap, RefreshCw, Trash2,
6
  } from 'lucide-react'
7
  import { useStore } from '../store/useStore'
8
+ import { streamExecuteQuery, submitFeedback, fetchPromptHistory } from '../lib/api'
9
  import { ResultsTable } from './ResultsTable'
10
  import type { ChatMessage, AttemptStep } from '../lib/types'
11
 
 
202
  }: {
203
  msg: ChatMessage
204
  onFeedback: (id: string, correct: boolean) => Promise<void>
205
+ onRetry: (q: string, previousSql?: string) => void
206
  }) {
207
  const [sqlOpen, setSqlOpen] = useState(true)
208
 
 
343
  )}
344
  {(msg.status === 'done' || msg.status === 'error') && (
345
  <button
346
+ onClick={() => onRetry(
347
+ msg.question,
348
+ msg.feedback === 'wrong' ? msg.sql : undefined
349
+ )}
350
  className="ml-auto flex items-center gap-1 text-[10px] text-gray-600 hover:text-gray-400 transition-colors"
351
  >
352
  <RefreshCw size={10} />
353
+ {msg.feedback === 'wrong' ? 'Retry differently' : 'Retry'}
354
  </button>
355
  )}
356
  </div>
 
395
  )
396
 
397
  const execute = useCallback(
398
+ async (question: string, previousSql?: string) => {
399
  if (!question.trim() || isExecuting) return
400
  setIsExecuting(true)
401
 
 
415
  addMessage(newMsg)
416
 
417
  try {
418
+ for await (const event of streamExecuteQuery(question, taskId, previousSql)) {
419
  if (event.type === 'sql') {
420
  updateMessage(msgId, { sql: event.sql as string })
421
  } else if (event.type === 'sql_chunk') {
 
460
  setOptimizingBanner(true)
461
  } else if (event.type === 'gepa_done') {
462
  setOptimizingBanner(false)
463
+ // Refresh prompt history in right sidebar
464
+ fetchPromptHistory()
465
+ .then((data) => useStore.getState().setPromptData(data))
466
+ .catch(() => { /* noop */ })
467
  }
468
  }
469
  } catch (err) {
 
531
  key={msg.id}
532
  msg={msg}
533
  onFeedback={handleFeedback}
534
+ onRetry={(q, prevSql) => void execute(q, prevSql)}
535
  />
536
  ))}
537
  <div ref={bottomRef} />
frontend/src/components/PromptEvolution.tsx CHANGED
@@ -21,11 +21,18 @@ export function PromptEvolution() {
21
  const prompt = currentPrompt || SEED_PROMPT
22
  const generation = promptGeneration
23
 
 
 
 
24
  const loadHistory = async () => {
25
  setLoading(true)
26
  try {
27
  const data = await fetchPromptHistory()
28
  setPromptData(data)
 
 
 
 
29
  } catch {
30
  // noop
31
  } finally {
@@ -35,6 +42,9 @@ export function PromptEvolution() {
35
 
36
  useEffect(() => {
37
  void loadHistory()
 
 
 
38
  // eslint-disable-next-line react-hooks/exhaustive-deps
39
  }, [])
40
 
@@ -65,6 +75,22 @@ export function PromptEvolution() {
65
  )}
66
  </button>
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  <AnimatePresence>
69
  {expanded && (
70
  <motion.div
 
21
  const prompt = currentPrompt || SEED_PROMPT
22
  const generation = promptGeneration
23
 
24
+ const [queryCount, setQueryCount] = useState(0)
25
+ const [nextAt, setNextAt] = useState(4)
26
+
27
  const loadHistory = async () => {
28
  setLoading(true)
29
  try {
30
  const data = await fetchPromptHistory()
31
  setPromptData(data)
32
+ if ((data as Record<string, unknown>).queryCount !== undefined) {
33
+ setQueryCount((data as Record<string, unknown>).queryCount as number)
34
+ setNextAt((data as Record<string, unknown>).nextOptimizeAt as number)
35
+ }
36
  } catch {
37
  // noop
38
  } finally {
 
42
 
43
  useEffect(() => {
44
  void loadHistory()
45
+ // Poll for updates every 30s
46
+ const interval = setInterval(() => void loadHistory(), 30000)
47
+ return () => clearInterval(interval)
48
  // eslint-disable-next-line react-hooks/exhaustive-deps
49
  }, [])
50
 
 
75
  )}
76
  </button>
77
 
78
+ {/* Progress toward next optimization */}
79
+ {queryCount > 0 && (
80
+ <div className="flex flex-col gap-1">
81
+ <div className="flex items-center justify-between text-[9px] text-gray-600">
82
+ <span>{queryCount} queries processed</span>
83
+ <span>{nextAt - queryCount} until next optimization</span>
84
+ </div>
85
+ <div className="h-1 bg-white/5 rounded-full overflow-hidden">
86
+ <div
87
+ className="h-full rounded-full bg-violet-500/50 transition-all duration-500"
88
+ style={{ width: `${((queryCount % 4) / 4) * 100}%` }}
89
+ />
90
+ </div>
91
+ </div>
92
+ )}
93
+
94
  <AnimatePresence>
95
  {expanded && (
96
  <motion.div
frontend/src/lib/api.ts CHANGED
@@ -29,12 +29,15 @@ async function* parseSSE(response: Response): AsyncGenerator<SSEEvent> {
29
 
30
  export async function* streamExecuteQuery(
31
  question: string,
32
- taskId: string
 
33
  ): AsyncGenerator<SSEEvent> {
 
 
34
  const res = await fetch(`${BASE_URL}/api/execute-query`, {
35
  method: 'POST',
36
  headers: { 'Content-Type': 'application/json' },
37
- body: JSON.stringify({ question, task_id: taskId }),
38
  })
39
  if (!res.ok) {
40
  throw new Error(`HTTP ${res.status}: ${res.statusText}`)
 
29
 
30
  export async function* streamExecuteQuery(
31
  question: string,
32
+ taskId: string,
33
+ previousSql?: string,
34
  ): AsyncGenerator<SSEEvent> {
35
+ const body: Record<string, unknown> = { question, task_id: taskId }
36
+ if (previousSql) body.previousSql = previousSql
37
  const res = await fetch(`${BASE_URL}/api/execute-query`, {
38
  method: 'POST',
39
  headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(body),
41
  })
42
  if (!res.ok) {
43
  throw new Error(`HTTP ${res.status}: ${res.statusText}`)