JasonGross commited on
Commit
de2cfbf
·
1 Parent(s): 1be2e4c

WIP on prompting

Browse files
Files changed (1) hide show
  1. app/main.py +349 -230
app/main.py CHANGED
@@ -80,6 +80,91 @@ def truncate_prompt(text: str, max_length: int = 70) -> str:
80
  return text[:max_length]
81
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  async def get_llm_stream(
84
  model_name: str, system_prompt: str, messages: list, max_tokens: int
85
  ):
@@ -129,28 +214,19 @@ async def get_llm_stream(
129
 
130
  async def stream_and_save_new_response(
131
  prompt_id: str,
 
132
  model_name: str,
133
  system_prompt: str,
134
  messages: list,
135
  max_tokens: int,
136
  ):
137
  """
138
- Calls LLM stream with provided parameters, yields chunks for the client,
139
- and saves the full response along with model info to the `responses` table.
140
  """
141
  full_response = ""
142
  error_occurred = False
143
-
144
- # --- Prepare arguments dictionary for saving ---
145
- # Construct this based on the arguments *received* by the function
146
- model_arguments_to_save = {
147
- "model": model_name,
148
- "max_tokens": max_tokens,
149
- "system": system_prompt,
150
- "messages": messages,
151
- # Add other relevant parameters if they were passed (e.g., temperature)
152
- }
153
- # --------------------------------------------
154
 
155
  try:
156
  # Pass received arguments directly to the LLM stream function
@@ -158,7 +234,7 @@ async def stream_and_save_new_response(
158
  model_name, system_prompt, messages, max_tokens
159
  ):
160
  if isinstance(chunk, str) and chunk.startswith('data: {"error":'):
161
- yield chunk # Propagate error SSE event
162
  logger.warning(
163
  f"LLM Stream Error reported for prompt_id '{prompt_id}': {chunk}"
164
  )
@@ -176,25 +252,41 @@ async def stream_and_save_new_response(
176
  return
177
 
178
  # --- Save the new response to the `responses` table --- #
179
- # Note: model_name and model_arguments are now saved in the prompts table
180
  if supabase and full_response:
181
- logger.info(f"Attempting to save new response for prompt_id: '{prompt_id}'")
 
 
182
  try:
183
- insert_resp = (
184
  supabase.table("responses")
185
  .insert(
186
  {
187
  "prompt_id": prompt_id,
 
188
  "response_text": full_response,
189
- # "model_name": model_name, # Removed: Belongs in prompts table
190
- # "model_arguments": arguments_json, # Removed: Belongs in prompts table
191
- }
192
  )
193
  .execute()
194
  )
195
- logger.info(
196
- f"Successfully saved new response for prompt_id: '{prompt_id}'"
197
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  except Exception as e:
200
  logger.exception(
@@ -204,6 +296,25 @@ async def stream_and_save_new_response(
204
  yield f"data: {json.dumps({'error': 'Failed to save new response.'})}\n\n"
205
  error_occurred = True
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  if not error_occurred:
208
  logger.info(
209
  f"Successfully streamed and saved new response for prompt_id: '{prompt_id}'"
@@ -271,202 +382,128 @@ async def ask_paul_graham(request: Request, prompt: str = Form(...)):
271
  )
272
  # ---------------------------
273
 
274
- truncated_prompt = truncate_prompt(short_description)
275
- logger.info(f"Using truncated prompt_text for lookup: '{truncated_prompt}'")
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
  try:
278
- # Check if prompt_text exists in `prompts` table
279
- prompt_resp = (
 
 
 
 
 
 
280
  supabase.table("prompts")
281
- .select("prompt_id, view_count")
282
- .eq("prompt_text", truncated_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  .limit(1)
284
  .execute()
285
  )
286
- existing_prompt = prompt_resp.data
287
 
288
- if existing_prompt:
289
- # --- Prompt Exists ---
290
- prompt_data = existing_prompt[0]
291
- prompt_id = prompt_data["prompt_id"]
292
- current_views = prompt_data["view_count"]
293
  logger.info(
294
- f"Prompt exists (ID: {prompt_id}). Incrementing view count and fetching latest response."
295
  )
296
 
297
- # Increment view count
298
  try:
299
- supabase.table("prompts").update({"view_count": current_views + 1}).eq(
300
- "prompt_id", prompt_id
 
301
  ).execute()
302
- logger.info(
303
- f"Incremented view count for prompt_id '{prompt_id}' to {current_views + 1}"
304
- )
305
  except Exception as e:
306
- logger.error(
307
- f"Error updating view count for prompt_id '{prompt_id}'", exc_info=e
 
308
  )
309
 
310
- # Fetch the latest response text
311
- latest_response_resp = (
312
- supabase.table("responses")
313
- .select("response_text")
314
- .eq("prompt_id", prompt_id)
315
- .order("response_created_at", desc=True)
316
- .limit(1)
317
- .execute()
 
 
 
318
  )
319
 
320
- if latest_response_resp.data:
321
- latest_response_text = latest_response_resp.data[0]["response_text"]
322
- logger.info(
323
- f"Found latest response for prompt_id '{prompt_id}'. Streaming it back."
324
- )
325
-
326
- # Stream the cached/latest response
327
- async def stream_latest_cached():
328
- chunk_size = 20
329
- for i in range(0, len(latest_response_text), chunk_size):
330
- chunk = latest_response_text[i : i + chunk_size]
331
- yield f"data: {json.dumps({'text': chunk})}\n\n"
332
- await asyncio.sleep(0.01)
333
- yield f"data: {json.dumps({'end': True})}\n\n"
334
-
335
- return StreamingResponse(
336
- stream_latest_cached(), media_type="text/event-stream"
337
- )
338
- else:
339
- logger.error(f"Prompt '{prompt_id}' exists, but no responses found!")
340
-
341
- # Option: Generate a new response for this existing prompt?
342
- # For now, return error. Could call stream_and_save_new_response(prompt_id, truncated_prompt) here instead.
343
- async def no_resp_stream():
344
- yield f"data: {json.dumps({'error': 'Found prompt but no responses available.'})}\n\n"
345
-
346
- return StreamingResponse(
347
- no_resp_stream(), media_type="text/event-stream", status_code=404
348
- )
349
-
350
  else:
351
- # --- Prompt Does Not Exist ---
 
 
352
  logger.info(
353
- f"Prompt_text '{truncated_prompt}' does not exist. Creating new prompt entry."
 
 
 
 
 
 
 
 
 
 
 
 
354
  )
355
- try:
356
- # Insert new prompt
357
- model_name = "claude-3-5-sonnet-20240620"
358
- system_prompt = "You are an AI assistant that writes essays in the style of Paul Graham. Focus on insights about startups, technology, programming, and contrarian thinking. Be concise and clear."
359
- max_tokens = 2048
360
- messages = [
361
- {
362
- "role": "user",
363
- "content": f"Write a Paul Graham essay about {short_description}", # Use full description for the LLM
364
- }
365
- ]
366
- # ----------------------------- #
367
-
368
- # Insert new prompt with model details
369
- insert_prompt_resp = (
370
- supabase.table("prompts")
371
- .insert(
372
- {
373
- "prompt_text": truncated_prompt,
374
- "short_description": short_description,
375
- "view_count": 1,
376
- "model_name": model_name, # Add model name here
377
- "model_arguments": messages[0][
378
- "content"
379
- ], # Add arguments here (Adjust based on desired format)
380
- }
381
- )
382
- .execute()
383
- )
384
-
385
- if insert_prompt_resp.data:
386
- new_prompt_id = insert_prompt_resp.data[0]["prompt_id"]
387
- logger.info(
388
- f"Successfully inserted new prompt with ID: {new_prompt_id}"
389
- )
390
-
391
- # Generate, stream, and save the first response (including model info)
392
- return StreamingResponse(
393
- stream_and_save_new_response(
394
- new_prompt_id,
395
- model_name,
396
- system_prompt,
397
- messages,
398
- max_tokens,
399
- ),
400
- media_type="text/event-stream",
401
- )
402
- else:
403
- logger.error(
404
- f"Failed to insert new prompt '{truncated_prompt}'. Response: {insert_prompt_resp}"
405
- )
406
- raise Exception("Failed to create new prompt entry.")
407
-
408
- except Exception as e:
409
- # Handle potential race condition on prompt_text unique constraint
410
- if "duplicate key value violates unique constraint" in str(
411
- e
412
- ) and "prompts_prompt_text_key" in str(e):
413
- logger.warning(
414
- f"Race condition? Prompt_text '{truncated_prompt}' inserted between check/insert. Recovering."
415
- )
416
- recover_resp = (
417
- supabase.table("prompts")
418
- .select("prompt_id")
419
- .eq("prompt_text", truncated_prompt)
420
- .limit(1)
421
- .execute()
422
- )
423
- if recover_resp.data:
424
- recovered_prompt_id = recover_resp.data[0]["prompt_id"]
425
- logger.info(
426
- f"Recovered prompt_id: {recovered_prompt_id}. Generating new response for existing prompt."
427
- )
428
-
429
- # --- Define LLM Parameters (Race Condition Recovery) --- #
430
- model_name = "claude-3-5-sonnet-20240620"
431
- system_prompt = "You are an AI assistant that writes essays in the style of Paul Graham. Focus on insights about startups, technology, programming, and contrarian thinking. Be concise and clear."
432
- max_tokens = 2048
433
- messages = [
434
- {
435
- "role": "user",
436
- "content": f"Write a Paul Graham essay about {short_description}", # Use full description
437
- }
438
- ]
439
- # ----------------------------------------------------- #
440
-
441
- # Generate a new response and save it, linked to the recovered prompt_id
442
- # NOTE: We don't update the prompt record here as it already exists.
443
- # The model details used for *this specific response* generation are saved
444
- # in the responses table by stream_and_save_new_response.
445
- return StreamingResponse(
446
- stream_and_save_new_response(
447
- recovered_prompt_id,
448
- model_name,
449
- system_prompt,
450
- messages,
451
- max_tokens,
452
- ),
453
- media_type="text/event-stream",
454
- )
455
- else:
456
- logger.error(
457
- f"Race condition recovery failed for prompt_text '{truncated_prompt}'."
458
- )
459
- raise Exception("Failed to create or recover prompt entry.")
460
- else:
461
- logger.exception(
462
- f"Error inserting new prompt with text '{truncated_prompt}'",
463
- exc_info=e,
464
- )
465
- raise e # Re-raise other exceptions
466
 
467
  except Exception as e:
468
  logger.exception(
469
- f"Error processing /ask request for prompt_text '{truncated_prompt}'",
470
  exc_info=e,
471
  )
472
 
@@ -480,7 +517,7 @@ async def ask_paul_graham(request: Request, prompt: str = Form(...)):
480
 
481
  @app.get("/essays", response_class=JSONResponse)
482
  async def get_essays(sort_by: str = "time", order: str = "desc"):
483
- """Fetches the list of saved prompts, returning short_description."""
484
  logger.info(f"Received /essays request. Sort by: {sort_by}, Order: {order}")
485
  if not supabase:
486
  logger.error("Supabase client not available for /essays request.")
@@ -488,49 +525,131 @@ async def get_essays(sort_by: str = "time", order: str = "desc"):
488
  content={"error": "Database connection not available."}, status_code=503
489
  )
490
 
491
- valid_sort_by = {
 
 
 
 
 
492
  "time": "created_at",
493
- "views": "view_count",
494
- "alpha": "short_description",
495
  }
496
- valid_order = {"asc": True, "desc": False}
497
- sort_column = valid_sort_by.get(sort_by, "created_at")
498
- ascending = valid_order.get(order, False)
499
- descending = not ascending # Calculate descending flag
500
 
501
  try:
502
- # Query the `prompts` table
503
- response = (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  supabase.table("prompts")
505
- .select(
506
- "short_description, created_at, view_count", count=CountMethod.exact
507
- ) # Keep count="exact" for now, monitor Supabase docs if needed
508
- .order(
509
- sort_column, desc=descending
510
- ) # Use desc parameter instead of ascending
511
  .execute()
512
  )
513
-
514
- logger.info(f"Fetched {response.count} prompts from database.")
515
- prompts_data = []
516
- if response.data:
517
- for row in response.data:
518
- created_at_iso = (
519
- row["created_at"].isoformat() if row.get("created_at") else None
520
- )
521
- prompts_data.append(
522
- {
523
- "prompt": row.get("short_description"), # Use short_description
524
- "created_at": created_at_iso,
525
- "view_count": row.get("view_count"),
526
- }
527
- )
528
- return JSONResponse(content=prompts_data)
529
- else:
530
  return JSONResponse(content=[])
531
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  except Exception as e:
533
- logger.exception("Error fetching prompts from Supabase", exc_info=e)
534
  return JSONResponse(
535
  content={"error": "Failed to fetch prompts."}, status_code=500
536
  )
 
80
  return text[:max_length]
81
 
82
 
83
+ async def get_or_create_model_params(
84
+ model_name: str, system_prompt: str, max_tokens: int, upsert_first: bool = False
85
+ ) -> str:
86
+ """Finds existing model parameters or creates them, returning the params_id."""
87
+ if not supabase:
88
+ logger.error("Supabase client not available for get_or_create_model_params")
89
+ raise HTTPException(
90
+ status_code=503, detail="Database connection not available."
91
+ )
92
+
93
+ params_to_find_or_insert = {
94
+ "model_name": model_name,
95
+ "system_prompt": system_prompt,
96
+ "max_tokens": max_tokens,
97
+ }
98
+ # Define the columns that form the unique constraint for conflict resolution
99
+ conflict_columns = "model_name, system_prompt, max_tokens"
100
+
101
+ if not upsert_first:
102
+ try:
103
+ select_resp = (
104
+ supabase.table("model_params")
105
+ .select("params_id")
106
+ .match(params_to_find_or_insert)
107
+ .limit(1)
108
+ .execute()
109
+ )
110
+ if select_resp.data:
111
+ params_id = select_resp.data[0]["params_id"]
112
+ return params_id
113
+ except Exception as e:
114
+ logger.warning("Error during model_params select", exc_info=e)
115
+ logger.warning(
116
+ f"Could not find model_params: {params_to_find_or_insert}. Creating new one."
117
+ )
118
+
119
+ logger.info(f"Upserting model_params: {params_to_find_or_insert}")
120
+ upsert_result = None
121
+ try:
122
+ upsert_result = (
123
+ supabase.table("model_params")
124
+ .upsert(
125
+ params_to_find_or_insert,
126
+ on_conflict=conflict_columns,
127
+ returning="representation", # type: ignore
128
+ ignore_duplicates=False, # Ensure we get the existing row if conflict
129
+ )
130
+ .execute()
131
+ )
132
+
133
+ if upsert_result.data and len(upsert_result.data) > 0:
134
+ params_id = upsert_result.data[0]["params_id"]
135
+ logger.info(f"Found or created model_params with ID: {params_id}")
136
+ return params_id
137
+ except Exception as e:
138
+ logger.exception("Error during model_params upsert", exc_info=e)
139
+ logger.error(
140
+ f"Upsert failed or did not return data for model_params: {params_to_find_or_insert}. Result: {upsert_result}"
141
+ )
142
+ return handle_model_params_error(params_to_find_or_insert)
143
+
144
+
145
+ def handle_model_params_error(params_to_find_or_insert):
146
+ """Handle errors in model_params operations with helpful debugging SQL."""
147
+ # Suggest SQL that could be run manually to debug/fix the issue
148
+ suggested_sql = f"""
149
+ -- Check if the record exists:
150
+ SELECT * FROM model_params
151
+ WHERE model_name = '{params_to_find_or_insert['model_name']}'
152
+ AND system_prompt = '{params_to_find_or_insert['system_prompt']}'
153
+ AND max_tokens = {params_to_find_or_insert['max_tokens']};
154
+
155
+ -- If not found, try inserting manually:
156
+ INSERT INTO model_params (model_name, system_prompt, max_tokens)
157
+ VALUES ('{params_to_find_or_insert['model_name']}',
158
+ '{params_to_find_or_insert['system_prompt']}',
159
+ {params_to_find_or_insert['max_tokens']})
160
+ RETURNING params_id;
161
+ """
162
+ logger.error(f"Suggested SQL to run manually: {suggested_sql}")
163
+ raise HTTPException(
164
+ status_code=500, detail="Failed to get or create model parameters."
165
+ )
166
+
167
+
168
  async def get_llm_stream(
169
  model_name: str, system_prompt: str, messages: list, max_tokens: int
170
  ):
 
214
 
215
  async def stream_and_save_new_response(
216
  prompt_id: str,
217
+ params_id: str,
218
  model_name: str,
219
  system_prompt: str,
220
  messages: list,
221
  max_tokens: int,
222
  ):
223
  """
224
+ Calls LLM stream, yields chunks, saves the full response to `responses`
225
+ linking prompt_id and params_id, and records the initial view in `view_counts`.
226
  """
227
  full_response = ""
228
  error_occurred = False
229
+ new_response_id = None
 
 
 
 
 
 
 
 
 
 
230
 
231
  try:
232
  # Pass received arguments directly to the LLM stream function
 
234
  model_name, system_prompt, messages, max_tokens
235
  ):
236
  if isinstance(chunk, str) and chunk.startswith('data: {"error":'):
237
+ yield chunk
238
  logger.warning(
239
  f"LLM Stream Error reported for prompt_id '{prompt_id}': {chunk}"
240
  )
 
252
  return
253
 
254
  # --- Save the new response to the `responses` table --- #
 
255
  if supabase and full_response:
256
+ logger.info(
257
+ f"Attempting to save new response for prompt_id: '{prompt_id}', params_id: '{params_id}'"
258
+ )
259
  try:
260
+ response_insert_result = (
261
  supabase.table("responses")
262
  .insert(
263
  {
264
  "prompt_id": prompt_id,
265
+ "params_id": params_id,
266
  "response_text": full_response,
267
+ },
268
+ returning="representation", # type: ignore
 
269
  )
270
  .execute()
271
  )
272
+
273
+ if response_insert_result.data and len(response_insert_result.data) > 0:
274
+ inserted_row = response_insert_result.data[0]
275
+ if "response_id" in inserted_row:
276
+ new_response_id = inserted_row["response_id"]
277
+ logger.info(
278
+ f"Successfully saved new response (ID: {new_response_id}) for prompt_id: '{prompt_id}'"
279
+ )
280
+ else:
281
+ logger.error(
282
+ f"'response_id' not found in returned data for prompt {prompt_id}"
283
+ )
284
+ error_occurred = True
285
+ else:
286
+ logger.error(
287
+ f"Failed to insert response or get representation for prompt {prompt_id}. Result: {response_insert_result}"
288
+ )
289
+ error_occurred = True
290
 
291
  except Exception as e:
292
  logger.exception(
 
296
  yield f"data: {json.dumps({'error': 'Failed to save new response.'})}\n\n"
297
  error_occurred = True
298
 
299
+ # --- Record the initial view in `view_counts` --- #
300
+ if supabase and new_response_id and not error_occurred:
301
+ try:
302
+ logger.info(
303
+ f"Recording initial view for response_id: {new_response_id}"
304
+ )
305
+ supabase.table("view_counts").insert(
306
+ {"response_id": new_response_id}
307
+ ).execute()
308
+ logger.info(
309
+ f"Successfully recorded initial view for response_id: {new_response_id}"
310
+ )
311
+ except Exception as e:
312
+ logger.exception(
313
+ f"Failed to record initial view for response_id {new_response_id}",
314
+ exc_info=e,
315
+ )
316
+
317
+ # --- Send End Event --- #
318
  if not error_occurred:
319
  logger.info(
320
  f"Successfully streamed and saved new response for prompt_id: '{prompt_id}'"
 
382
  )
383
  # ---------------------------
384
 
385
+ # --- Determine Model Parameters --- #
386
+ # (Define these based on your logic - fixed for now)
387
+ model_name = "claude-3-5-sonnet-20240620"
388
+ system_prompt = "You are an AI assistant that writes essays in the style of Paul Graham. Focus on insights about startups, technology, programming, and contrarian thinking. Be concise and clear."
389
+ max_tokens = 3500 # GPT 2 token statistics on PG essays as of 2025-04-14
390
+ # Mean: 3284.29, Median: 2052, Mode: 3292
391
+ # Min: 104, Max: 17718, SD: 3086.28
392
+ prompt_text = f"Write a Paul Graham essay about {short_description}"
393
+ messages = [
394
+ {
395
+ "role": "user",
396
+ "content": prompt_text, # Use full description for the LLM
397
+ }
398
+ ]
399
+ # --------------------------------- #
400
 
401
  try:
402
+ # --- Get or Create Model Params ID --- #
403
+ params_id = await get_or_create_model_params(
404
+ model_name, system_prompt, max_tokens
405
+ )
406
+ # ------------------------------------- #
407
+
408
+ # --- Find or Create Prompt based on short_description --- #
409
+ prompt_upsert_result = (
410
  supabase.table("prompts")
411
+ .upsert(
412
+ {"short_description": short_description, "prompt_text": prompt_text},
413
+ on_conflict="prompt_text",
414
+ returning="representation", # type: ignore
415
+ ignore_duplicates=False,
416
+ )
417
+ .execute()
418
+ )
419
+
420
+ if not prompt_upsert_result.data or len(prompt_upsert_result.data) == 0:
421
+ logger.error(
422
+ f"Failed to upsert prompt for description: {short_description}"
423
+ )
424
+ raise HTTPException(
425
+ status_code=500, detail="Failed to find or create prompt."
426
+ )
427
+
428
+ prompt_info = prompt_upsert_result.data[0]
429
+ prompt_id = prompt_info["prompt_id"]
430
+ prompt_created_at = prompt_info[
431
+ "created_at"
432
+ ] # Example of getting other info if needed
433
+
434
+ # Determine if the prompt was newly inserted or if it already existed
435
+ # This logic might need refinement based on exact upsert behavior / timestamps
436
+ # A simple check: if created_at is very recent? Or compare count before/after?
437
+ # For now, let's assume if we *found* a response below, the prompt existed.
438
+
439
+ # --- Check for Existing Response --- #
440
+ # Fetch the latest response for this prompt_id (regardless of params_id used to create it)
441
+ latest_response_resp = (
442
+ supabase.table("responses")
443
+ .select("response_id, response_text")
444
+ .eq("prompt_id", prompt_id)
445
+ .order("response_created_at", desc=True)
446
  .limit(1)
447
  .execute()
448
  )
 
449
 
450
+ if latest_response_resp.data:
451
+ # --- Prompt Existed and has a Response --- #
452
+ latest_response = latest_response_resp.data[0]
453
+ latest_response_id = latest_response["response_id"]
454
+ latest_response_text = latest_response["response_text"]
455
  logger.info(
456
+ f"Found existing prompt (ID: {prompt_id}) and latest response (ID: {latest_response_id}). Streaming cached response."
457
  )
458
 
459
+ # Record View
460
  try:
461
+ logger.info(f"Recording view for response_id: {latest_response_id}")
462
+ supabase.table("view_counts").insert(
463
+ {"response_id": latest_response_id}
464
  ).execute()
 
 
 
465
  except Exception as e:
466
+ logger.exception(
467
+ f"Failed to record view for response_id {latest_response_id}",
468
+ exc_info=e,
469
  )
470
 
471
+ # Stream the cached/latest response
472
+ async def stream_latest_cached():
473
+ chunk_size = 20
474
+ for i in range(0, len(latest_response_text), chunk_size):
475
+ chunk = latest_response_text[i : i + chunk_size]
476
+ yield f"data: {json.dumps({'text': chunk})}\\n\\n"
477
+ await asyncio.sleep(0.01)
478
+ yield f"data: {json.dumps({'end': True})}\n\n"
479
+
480
+ return StreamingResponse(
481
+ stream_latest_cached(), media_type="text/event-stream"
482
  )
483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  else:
485
+ # --- Prompt was Newly Created OR Existed but has NO responses --- #
486
+ # This happens if the upsert created the prompt, OR if the prompt existed
487
+ # but its previous responses were deleted (or never created).
488
  logger.info(
489
+ f"Prompt (ID: {prompt_id}) is new or has no existing responses. Generating new response with params_id {params_id}."
490
+ )
491
+ # Generate, stream, and save the first response for this prompt using current params
492
+ return StreamingResponse(
493
+ stream_and_save_new_response(
494
+ prompt_id, # The ID from the upsert
495
+ params_id, # The ID for the *current* model params
496
+ model_name,
497
+ system_prompt,
498
+ messages,
499
+ max_tokens,
500
+ ),
501
+ media_type="text/event-stream",
502
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
  except Exception as e:
505
  logger.exception(
506
+ f"Error processing /ask request for description '{short_description}'",
507
  exc_info=e,
508
  )
509
 
 
517
 
518
  @app.get("/essays", response_class=JSONResponse)
519
  async def get_essays(sort_by: str = "time", order: str = "desc"):
520
+ """Fetches the list of saved prompts and their total view counts."""
521
  logger.info(f"Received /essays request. Sort by: {sort_by}, Order: {order}")
522
  if not supabase:
523
  logger.error("Supabase client not available for /essays request.")
 
525
  content={"error": "Database connection not available."}, status_code=503
526
  )
527
 
528
+ # --- Sorting Logic --- #
529
+ # Note: Sorting by 'views' requires the aggregated count
530
+ # We handle sorting *after* fetching and aggregation for simplicity here.
531
+ # For large datasets, doing sorting in the DB might be better if possible
532
+ # with Supabase function calls or views.
533
+ sort_column_map = {
534
  "time": "created_at",
535
+ "alpha": "prompt",
536
+ "views": "view_count", # We'll use this key after aggregation
537
  }
538
+ sort_key = sort_column_map.get(sort_by, "created_at")
539
+ reverse_sort = order == "desc"
540
+ # --------------------- #
 
541
 
542
  try:
543
+ # --- Query Prompts and Aggregate View Counts --- #
544
+ # This requires joining prompts -> responses -> view_counts
545
+ # Using supabase-py directly for joins/counts can be tricky.
546
+ # An RPC function in Supabase is often the cleaner/more performant way.
547
+ # --- Option 1: Using RPC (Recommended) --- #
548
+ # Assumes you create a SQL function `get_prompts_with_views()` in Supabase:
549
+ # CREATE OR REPLACE FUNCTION get_prompts_with_views()
550
+ # RETURNS TABLE(prompt_id UUID, short_description TEXT, created_at TIMESTAMPTZ, view_count BIGINT)
551
+ # LANGUAGE sql
552
+ # AS $$
553
+ # SELECT
554
+ # p.prompt_id,
555
+ # p.short_description,
556
+ # p.created_at,
557
+ # count(vc.view_id)::BIGINT as view_count
558
+ # FROM prompts p
559
+ # -- Join to find *any* response for the prompt
560
+ # LEFT JOIN responses r ON p.prompt_id = r.prompt_id
561
+ # -- Join views related to those responses
562
+ # LEFT JOIN view_counts vc ON r.response_id = vc.response_id
563
+ # GROUP BY p.prompt_id, p.short_description, p.created_at;
564
+ # $$;
565
+ #
566
+ # response = supabase.rpc('get_prompts_with_views', {}).execute()
567
+ # logger.info(f"Fetched {len(response.data)} prompts via RPC.")
568
+ # prompts_data = response.data # Already contains view_count
569
+
570
+ # --- Option 2: Attempting with supabase-py (Less Ideal/More Complex) --- #
571
+ # Fetch all prompts first
572
+ prompts_resp = (
573
  supabase.table("prompts")
574
+ .select("prompt_id, short_description, created_at")
 
 
 
 
 
575
  .execute()
576
  )
577
+ if not prompts_resp.data:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  return JSONResponse(content=[])
579
 
580
+ prompts_map = {p["prompt_id"]: p for p in prompts_resp.data}
581
+ prompt_ids = list(prompts_map.keys())
582
+
583
+ # Fetch response IDs linked to these prompts
584
+ responses_ids_resp = (
585
+ supabase.table("responses")
586
+ .select("response_id")
587
+ .in_("prompt_id", prompt_ids)
588
+ .execute()
589
+ )
590
+ response_ids = (
591
+ [r["response_id"] for r in responses_ids_resp.data]
592
+ if responses_ids_resp.data
593
+ else []
594
+ )
595
+
596
+ # Fetch view counts for these response IDs
597
+ views_resp = (
598
+ supabase.table("view_counts")
599
+ .select("response_id, view_id")
600
+ .in_("response_id", response_ids)
601
+ .execute()
602
+ )
603
+
604
+ views_per_response: dict[str, int] = {} # Type hint added
605
+ if views_resp.data:
606
+ for view in views_resp.data:
607
+ resp_id = view["response_id"]
608
+ views_per_response[resp_id] = views_per_response.get(resp_id, 0) + 1
609
+
610
+ # Fetch responses to link prompts to view counts
611
+ responses_linking_resp = (
612
+ supabase.table("responses")
613
+ .select("prompt_id, response_id")
614
+ .in_("prompt_id", prompt_ids)
615
+ .execute()
616
+ )
617
+
618
+ views_per_prompt = {pid: 0 for pid in prompt_ids}
619
+ if responses_linking_resp.data:
620
+ for resp in responses_linking_resp.data:
621
+ prompt_id = resp["prompt_id"]
622
+ response_id = resp["response_id"]
623
+ views_per_prompt[prompt_id] += views_per_response.get(response_id, 0)
624
+
625
+ # Combine data
626
+ final_data = []
627
+ for pid, prompt_info in prompts_map.items():
628
+ created_at_iso = (
629
+ prompt_info["created_at"].isoformat()
630
+ if prompt_info.get("created_at")
631
+ else None
632
+ )
633
+ final_data.append(
634
+ {
635
+ "prompt": prompt_info.get("short_description"),
636
+ "created_at": created_at_iso,
637
+ "view_count": views_per_prompt.get(pid, 0),
638
+ }
639
+ )
640
+ logger.info(f"Processed {len(final_data)} prompts with aggregated views.")
641
+ # -------------------------------------------------- #
642
+
643
+ # Sort results in Python
644
+ final_data.sort(
645
+ key=lambda x: x.get(sort_key) or (0 if sort_key == "view_count" else " "),
646
+ reverse=reverse_sort,
647
+ )
648
+
649
+ return JSONResponse(content=final_data)
650
+
651
  except Exception as e:
652
+ logger.exception("Error fetching prompts/views from Supabase", exc_info=e)
653
  return JSONResponse(
654
  content={"error": "Failed to fetch prompts."}, status_code=500
655
  )