Spaces:
Sleeping
Sleeping
Surface GEPA optimization: prompt history, live banner, smart retry
Browse filesBackend:
- 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
|
|
|
|
| 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 |
-
|
| 384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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) =>
|
| 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(
|
| 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}`)
|