ziadmostafa commited on
Commit
e8b4723
·
1 Parent(s): 2cd1e41
Files changed (4) hide show
  1. README.md +11 -0
  2. app.py +29 -3
  3. static/js/main.js +126 -79
  4. utils/ai_helpers.py +99 -22
README.md CHANGED
@@ -30,6 +30,17 @@ NoteGenie is an AI-powered Jupyter notebook generator that uses Google's Gemini
30
  - SECRET_KEY: A secure random string for Flask sessions
31
  - PORT: 7860 (default for Hugging Face Spaces)
32
 
 
 
 
 
 
 
 
 
 
 
 
33
  ## Local Development
34
 
35
  1. Install dependencies:
 
30
  - SECRET_KEY: A secure random string for Flask sessions
31
  - PORT: 7860 (default for Hugging Face Spaces)
32
 
33
+ ### Troubleshooting on Hugging Face Spaces
34
+
35
+ If you encounter issues when running NoteGenie on Hugging Face Spaces, try these steps:
36
+
37
+ 1. **API Key**: Ensure you've entered a valid Google Gemini API key
38
+ 2. **Browser Refresh**: Try completely refreshing the page
39
+ 3. **Shorter Prompts**: Use shorter, more concise prompts (Spaces may have connection timeouts)
40
+ 4. **Space Resources**: Check if your Space has enough resources allocated
41
+ 5. **Clear Cache**: Try clearing your browser cache or using an incognito window
42
+ 6. **Check Logs**: View the Space logs for detailed error information
43
+
44
  ## Local Development
45
 
46
  1. Install dependencies:
app.py CHANGED
@@ -4,9 +4,19 @@ import google.generativeai as genai
4
  import json
5
  import uuid
6
  import os
 
 
7
  from utils.ai_helpers import generate_notebook, stream_notebook_generation, stream_notebook_edit, edit_notebook
8
  from utils.notebook_helpers import format_notebook, extract_notebook_info
9
 
 
 
 
 
 
 
 
 
10
  app = Flask(__name__)
11
  app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "notegenie-secret-key-change-in-production")
12
  app.config["SESSION_TYPE"] = "filesystem"
@@ -16,6 +26,15 @@ app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 30 # 30 days
16
  app.config["SESSION_FILE_DIR"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flask_session")
17
  os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True) # Ensure directory exists
18
 
 
 
 
 
 
 
 
 
 
19
  Session(app)
20
 
21
  # Map front-end model names to API model names
@@ -47,14 +66,17 @@ def set_api_key():
47
  # Store API key in session with permanent flag
48
  session.permanent = True
49
  session["api_key"] = api_key
 
50
 
51
  return jsonify({"success": True})
52
  except Exception as e:
 
53
  return jsonify({"success": False, "message": str(e)}), 400
54
 
55
  @app.route("/generate_notebook", methods=["GET", "POST"])
56
  def generate_notebook_route():
57
  if "api_key" not in session:
 
58
  return jsonify({"success": False, "message": "API key not set"}), 401
59
 
60
  # Handle both GET (for streaming) and POST requests
@@ -74,10 +96,11 @@ def generate_notebook_route():
74
 
75
  # Map the frontend model name to the API model name
76
  api_model_name = get_api_model_name(model_name)
77
-
78
- genai.configure(api_key=session["api_key"])
79
 
80
  try:
 
 
81
  # OPTIMIZATION: If format_only is True, skip the AI call and just format the provided content
82
  if request.method == "POST" and format_only:
83
  # Use client-provided content as is (it's already the AI response)
@@ -92,6 +115,7 @@ def generate_notebook_route():
92
  "description": notebook_info["description"]
93
  })
94
  elif stream:
 
95
  return stream_notebook_generation(prompt, api_model_name)
96
  else:
97
  notebook_content = generate_notebook(prompt, api_model_name)
@@ -105,6 +129,7 @@ def generate_notebook_route():
105
  "description": notebook_info["description"]
106
  })
107
  except Exception as e:
 
108
  return jsonify({"success": False, "message": str(e)}), 500
109
 
110
  @app.route("/prepare_edit_notebook", methods=["POST"])
@@ -197,4 +222,5 @@ def download_notebook():
197
 
198
  if __name__ == "__main__":
199
  port = int(os.environ.get("PORT", 5000))
200
- app.run(host="0.0.0.0", port=port, debug=(os.environ.get("FLASK_ENV") == "development"))
 
 
4
  import json
5
  import uuid
6
  import os
7
+ import logging
8
+ import sys
9
  from utils.ai_helpers import generate_notebook, stream_notebook_generation, stream_notebook_edit, edit_notebook
10
  from utils.notebook_helpers import format_notebook, extract_notebook_info
11
 
12
+ # Configure logging
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
+ stream=sys.stdout
17
+ )
18
+ logger = logging.getLogger('notegenie')
19
+
20
  app = Flask(__name__)
21
  app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "notegenie-secret-key-change-in-production")
22
  app.config["SESSION_TYPE"] = "filesystem"
 
26
  app.config["SESSION_FILE_DIR"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flask_session")
27
  os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True) # Ensure directory exists
28
 
29
+ # Detect if running on Hugging Face Spaces
30
+ IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
31
+ if IS_HUGGINGFACE:
32
+ logger.info("Running on Hugging Face Spaces environment")
33
+ # Ensure sessions work properly on Hugging Face
34
+ app.config["SESSION_COOKIE_SECURE"] = True
35
+ app.config["SESSION_COOKIE_HTTPONLY"] = True
36
+ app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
37
+
38
  Session(app)
39
 
40
  # Map front-end model names to API model names
 
66
  # Store API key in session with permanent flag
67
  session.permanent = True
68
  session["api_key"] = api_key
69
+ logger.info("API key successfully set and validated")
70
 
71
  return jsonify({"success": True})
72
  except Exception as e:
73
+ logger.error(f"API key validation error: {str(e)}")
74
  return jsonify({"success": False, "message": str(e)}), 400
75
 
76
  @app.route("/generate_notebook", methods=["GET", "POST"])
77
  def generate_notebook_route():
78
  if "api_key" not in session:
79
+ logger.warning("Generate notebook request without API key")
80
  return jsonify({"success": False, "message": "API key not set"}), 401
81
 
82
  # Handle both GET (for streaming) and POST requests
 
96
 
97
  # Map the frontend model name to the API model name
98
  api_model_name = get_api_model_name(model_name)
99
+ logger.info(f"Generate notebook with model: {api_model_name}, stream: {stream}")
 
100
 
101
  try:
102
+ genai.configure(api_key=session["api_key"])
103
+
104
  # OPTIMIZATION: If format_only is True, skip the AI call and just format the provided content
105
  if request.method == "POST" and format_only:
106
  # Use client-provided content as is (it's already the AI response)
 
115
  "description": notebook_info["description"]
116
  })
117
  elif stream:
118
+ logger.info("Starting streaming notebook generation")
119
  return stream_notebook_generation(prompt, api_model_name)
120
  else:
121
  notebook_content = generate_notebook(prompt, api_model_name)
 
129
  "description": notebook_info["description"]
130
  })
131
  except Exception as e:
132
+ logger.error(f"Error generating notebook: {str(e)}", exc_info=True)
133
  return jsonify({"success": False, "message": str(e)}), 500
134
 
135
  @app.route("/prepare_edit_notebook", methods=["POST"])
 
222
 
223
  if __name__ == "__main__":
224
  port = int(os.environ.get("PORT", 5000))
225
+ debug_mode = os.environ.get("FLASK_ENV") == "development"
226
+ app.run(host="0.0.0.0", port=port, debug=debug_mode)
static/js/main.js CHANGED
@@ -295,105 +295,152 @@ document.addEventListener('DOMContentLoaded', function() {
295
  }
296
  }, 5000); // Check every 5 seconds
297
 
298
- // Create a new event source
299
- eventSource = new EventSource(`/generate_notebook?${new URLSearchParams({
300
- prompt: prompt,
301
- model: modelName,
302
- stream: true
303
- }).toString()}`);
304
-
305
- eventSource.onmessage = function(event) {
306
- // Update our last-activity timestamp
307
- lastChunkTime = Date.now();
 
 
 
308
 
309
- try {
310
- const data = JSON.parse(event.data);
 
311
 
312
- // Handle errors sent from the server
313
- if (data.error) {
314
- console.error("Server error:", data.error);
315
- updateAiMessage(aiMessageId, `**Error:** ${data.error}`);
316
 
317
- // Try to salvage what we have so far
318
- if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
319
- processNotebookResponse(aiResponseText);
 
320
  }
321
 
322
- eventSource.close();
323
- eventSource = null;
324
- setGeneratingState(false);
325
- clearInterval(connectionTimer);
326
- return;
327
- }
328
-
329
- if (data.chunk) {
330
- aiResponseText += data.chunk;
331
-
332
- // Extract notebook info as soon as it's available
333
- const nameMatch = aiResponseText.match(/NOTEBOOK_NAME:?\s*(.+?)(?:\n|$)/);
334
- const descMatch = aiResponseText.match(/NOTEBOOK_DESCRIPTION:?\s*(.+?)(?:\n|$)/);
335
-
336
- if (nameMatch && nameMatch[1].trim()) {
337
- // Update notebook title immediately when found
338
- notebookTitleEl.textContent = nameMatch[1].trim();
339
  }
340
 
341
- if (descMatch && descMatch[1].trim()) {
342
- // Update AI message to only display the description, not the full response
343
- updateAiMessage(aiMessageId, `**NoteGenie:** ${descMatch[1].trim()}`);
344
- } else {
345
- // Show a simple generating message while waiting for description
346
- updateAiMessage(aiMessageId, "**NoteGenie:** Generating your notebook...");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  }
348
 
349
- // Update preview periodically during streaming
350
- const now = Date.now();
351
- if (now - lastPreviewUpdate > PREVIEW_UPDATE_INTERVAL) {
352
- lastPreviewUpdate = now;
353
 
354
- // Only try to update preview if we have meaningful content
355
- if (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL')) {
356
- updateNotebookPreviewDuringStream(aiResponseText);
357
- }
358
  }
 
 
359
  }
 
 
 
 
360
 
361
- if (data.done) {
 
362
  eventSource.close();
363
  eventSource = null;
364
  clearInterval(connectionTimer);
365
 
366
- // Process the complete response for final rendering
367
- processNotebookResponse(aiResponseText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  setGeneratingState(false);
369
  }
370
- } catch (error) {
371
- console.error('Error parsing event data:', error, event.data);
372
- }
373
- };
374
 
375
- eventSource.onerror = function(err) {
376
- console.error('EventSource error:', err);
377
- eventSource.close();
378
- eventSource = null;
379
- clearInterval(connectionTimer);
380
-
381
- // Check if it's an auth error (most likely API key not set)
382
- if (err.status === 401) {
383
- updateAiMessage(aiMessageId, '**Error: API key not set or invalid.** \n\nPlease click the API Key button in the top right corner to set your Google Gemini API key.');
384
- showApiKeyModal();
385
- } else {
386
- // Try to salvage what we have so far
387
- if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
388
- updateAiMessage(aiMessageId, '**Warning:** Connection issue occurred but I\'ll try to process what I received so far.');
389
- processNotebookResponse(aiResponseText);
390
- } else {
391
- updateAiMessage(aiMessageId, '**Error:** Failed to generate notebook. Please try again.');
392
- }
393
- }
394
-
395
- setGeneratingState(false);
396
- };
397
  }
398
 
399
  function handleEditStreamingResponse(editPrompt, notebook, modelName, aiMessageId) {
 
295
  }
296
  }, 5000); // Check every 5 seconds
297
 
298
+ // Add a retry mechanism
299
+ let retryCount = 0;
300
+ const MAX_RETRIES = 2;
301
+
302
+ function createEventSource() {
303
+ // Create a new event source with a unique timestamp to prevent caching
304
+ const timestamp = Date.now();
305
+ eventSource = new EventSource(`/generate_notebook?${new URLSearchParams({
306
+ prompt: prompt,
307
+ model: modelName,
308
+ stream: true,
309
+ t: timestamp // Add timestamp to prevent caching
310
+ }).toString()}`);
311
 
312
+ eventSource.onmessage = function(event) {
313
+ // Update our last-activity timestamp
314
+ lastChunkTime = Date.now();
315
 
316
+ try {
317
+ const data = JSON.parse(event.data);
 
 
318
 
319
+ // Handle heartbeat messages (for Hugging Face)
320
+ if (data.heartbeat !== undefined) {
321
+ console.log(`Heartbeat received: ${data.heartbeat}`);
322
+ return; // Just a keepalive, no content to process
323
  }
324
 
325
+ // Handle errors sent from the server
326
+ if (data.error) {
327
+ console.error("Server error:", data.error);
328
+ updateAiMessage(aiMessageId, `**Error:** ${data.error}`);
329
+
330
+ // Try to salvage what we have so far
331
+ if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
332
+ processNotebookResponse(aiResponseText);
333
+ }
334
+
335
+ eventSource.close();
336
+ eventSource = null;
337
+ setGeneratingState(false);
338
+ clearInterval(connectionTimer);
339
+ return;
 
 
340
  }
341
 
342
+ if (data.chunk) {
343
+ aiResponseText += data.chunk;
344
+
345
+ // Extract notebook info as soon as it's available
346
+ const nameMatch = aiResponseText.match(/NOTEBOOK_NAME:?\s*(.+?)(?:\n|$)/);
347
+ const descMatch = aiResponseText.match(/NOTEBOOK_DESCRIPTION:?\s*(.+?)(?:\n|$)/);
348
+
349
+ if (nameMatch && nameMatch[1].trim()) {
350
+ // Update notebook title immediately when found
351
+ notebookTitleEl.textContent = nameMatch[1].trim();
352
+ }
353
+
354
+ if (descMatch && descMatch[1].trim()) {
355
+ // Update AI message to only display the description, not the full response
356
+ updateAiMessage(aiMessageId, `**NoteGenie:** ${descMatch[1].trim()}`);
357
+ } else {
358
+ // Show a simple generating message while waiting for description
359
+ updateAiMessage(aiMessageId, "**NoteGenie:** Generating your notebook...");
360
+ }
361
+
362
+ // Update preview periodically during streaming
363
+ const now = Date.now();
364
+ if (now - lastPreviewUpdate > PREVIEW_UPDATE_INTERVAL) {
365
+ lastPreviewUpdate = now;
366
+
367
+ // Only try to update preview if we have meaningful content
368
+ if (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL')) {
369
+ updateNotebookPreviewDuringStream(aiResponseText);
370
+ }
371
+ }
372
  }
373
 
374
+ if (data.done) {
375
+ eventSource.close();
376
+ eventSource = null;
377
+ clearInterval(connectionTimer);
378
 
379
+ // Process the complete response for final rendering
380
+ processNotebookResponse(aiResponseText);
381
+ setGeneratingState(false);
 
382
  }
383
+ } catch (error) {
384
+ console.error('Error parsing event data:', error, event.data);
385
  }
386
+ };
387
+
388
+ eventSource.onerror = function(err) {
389
+ console.error('EventSource error:', err);
390
 
391
+ // Don't retry if we're already disconnected
392
+ if (eventSource.readyState === 2) {
393
  eventSource.close();
394
  eventSource = null;
395
  clearInterval(connectionTimer);
396
 
397
+ if (retryCount < MAX_RETRIES) {
398
+ retryCount++;
399
+ console.log(`Retrying connection (${retryCount}/${MAX_RETRIES})...`);
400
+ updateAiMessage(aiMessageId, `**NoteGenie:** Connection issue, retrying... (${retryCount}/${MAX_RETRIES})`);
401
+
402
+ // Wait a moment before retrying
403
+ setTimeout(() => {
404
+ createEventSource();
405
+ }, 2000);
406
+ return;
407
+ }
408
+
409
+ // Check if it's an auth error (most likely API key not set)
410
+ if (err.status === 401) {
411
+ updateAiMessage(aiMessageId, '**Error: API key not set or invalid.** \n\nPlease click the API Key button in the top right corner to set your Google Gemini API key.');
412
+ showApiKeyModal();
413
+ } else {
414
+ // Try to salvage what we have so far
415
+ if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
416
+ updateAiMessage(aiMessageId, '**Warning:** Connection issue occurred but I\'ll try to process what I received so far.');
417
+ processNotebookResponse(aiResponseText);
418
+ } else {
419
+ // Provide more detailed error message for Hugging Face users
420
+ let errorMsg = '**Error:** Failed to generate notebook.';
421
+
422
+ // Check if we're on Hugging Face (URL check)
423
+ if (window.location.hostname.includes('huggingface.co') ||
424
+ window.location.hostname.includes('hf.space')) {
425
+ errorMsg += ' This might be due to Hugging Face Space limitations. Try: \n\n' +
426
+ '1. Refreshing the page \n' +
427
+ '2. Using a shorter prompt \n' +
428
+ '3. Re-entering your API key';
429
+ } else {
430
+ errorMsg += ' Please try again.';
431
+ }
432
+
433
+ updateAiMessage(aiMessageId, errorMsg);
434
+ }
435
+ }
436
+
437
  setGeneratingState(false);
438
  }
439
+ };
440
+ }
 
 
441
 
442
+ // Initial event source creation
443
+ createEventSource();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  }
445
 
446
  function handleEditStreamingResponse(editPrompt, notebook, modelName, aiMessageId) {
utils/ai_helpers.py CHANGED
@@ -2,6 +2,14 @@ import google.generativeai as genai
2
  from flask import Response, stream_with_context
3
  import json
4
  import time
 
 
 
 
 
 
 
 
5
 
6
  def craft_notebook_prompt(user_prompt):
7
  """Enhance the user prompt with instructions for generating a well-structured Jupyter notebook."""
@@ -90,9 +98,13 @@ def generate_notebook(user_prompt, model_name="gemini-2.0-pro-exp-02-05"):
90
  model = genai.GenerativeModel(model_name)
91
 
92
  enhanced_prompt = craft_notebook_prompt(user_prompt)
93
- response = model.generate_content(enhanced_prompt)
94
 
95
- return response.text
 
 
 
 
 
96
 
97
  def edit_notebook(edit_request, notebook_json, model_name="gemini-2.0-pro-exp-02-05"):
98
  """Edit an existing notebook based on user request."""
@@ -110,25 +122,39 @@ def stream_notebook_generation(user_prompt, model_name="gemini-2.0-pro-exp-02-05
110
 
111
  def generate():
112
  try:
 
113
  response = model.generate_content(enhanced_prompt, stream=True)
114
 
115
  # Send a notification that streaming has started
116
  yield f"data: {json.dumps({'chunk': 'Starting notebook generation...'})}\n\n"
117
 
 
 
 
 
118
  for chunk in response:
 
 
 
 
 
 
 
119
  try:
120
  # More robust empty chunk detection
121
  if not hasattr(chunk, 'parts') or not chunk.parts:
122
  # Skip this empty chunk and continue
123
- print("Warning: Empty chunk received (no parts)")
124
  continue
125
 
126
  # First try the standard text property
127
  try:
128
  if hasattr(chunk, 'text') and chunk.text:
129
  yield f"data: {json.dumps({'chunk': chunk.text})}\n\n"
 
130
  continue # If we successfully got text, continue to next chunk
131
- except (AttributeError, IndexError):
 
132
  # If accessing text property fails, we'll try extracting from parts
133
  pass
134
 
@@ -137,29 +163,50 @@ def stream_notebook_generation(user_prompt, model_name="gemini-2.0-pro-exp-02-05
137
  # Extract text from part using different approaches
138
  if hasattr(part, 'text') and part.text:
139
  yield f"data: {json.dumps({'chunk': part.text})}\n\n"
 
140
  elif isinstance(part, dict) and 'text' in part:
141
  yield f"data: {json.dumps({'chunk': part['text']})}\n\n"
 
142
  elif hasattr(part, 'string_value'):
143
  yield f"data: {json.dumps({'chunk': part.string_value})}\n\n"
 
144
 
145
  except (AttributeError, IndexError, TypeError) as e:
146
  # Log the error but continue - don't break the stream
147
- print(f"Error processing chunk: {e}, chunk structure: {repr(chunk)[:200]}")
148
  continue
149
 
150
- # Briefly pause to prevent overwhelming the client
151
  time.sleep(0.01)
152
 
 
 
 
 
153
  yield f"data: {json.dumps({'done': True})}\n\n"
 
154
 
155
  except Exception as e:
156
  # Send error to client and close stream
157
  error_message = f"Error generating notebook: {str(e)}"
158
- print(error_message)
159
  yield f"data: {json.dumps({'error': error_message})}\n\n"
160
  yield f"data: {json.dumps({'done': True})}\n\n"
161
 
162
- return Response(stream_with_context(generate()), content_type="text/event-stream")
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  def stream_notebook_edit(edit_request, notebook_json, model_name="gemini-2.0-pro-exp-02-05"):
165
  """Stream notebook editing responses from Gemini API."""
@@ -168,53 +215,83 @@ def stream_notebook_edit(edit_request, notebook_json, model_name="gemini-2.0-pro
168
 
169
  def generate():
170
  try:
 
171
  response = model.generate_content(enhanced_prompt, stream=True)
172
 
173
  # Send a notification that editing has started
174
  yield f"data: {json.dumps({'chunk': 'Starting notebook edit...'})}\n\n"
175
 
 
 
 
 
176
  for chunk in response:
 
 
 
 
 
 
 
 
177
  try:
178
  # More robust empty chunk detection
179
  if not hasattr(chunk, 'parts') or not chunk.parts:
180
- # Skip this empty chunk and continue
181
- print("Warning: Empty chunk received (no parts)")
182
  continue
183
 
184
- # First try the standard text property
185
  try:
186
  if hasattr(chunk, 'text') and chunk.text:
187
  yield f"data: {json.dumps({'chunk': chunk.text})}\n\n"
188
- continue # If we successfully got text, continue to next chunk
189
- except (AttributeError, IndexError):
190
- # If accessing text property fails, we'll try extracting from parts
 
191
  pass
192
 
193
- # If we're here, we couldn't get text directly, try to extract from parts
194
  for part in chunk.parts:
195
- # Extract text from part using different approaches
196
  if hasattr(part, 'text') and part.text:
197
  yield f"data: {json.dumps({'chunk': part.text})}\n\n"
 
198
  elif isinstance(part, dict) and 'text' in part:
199
  yield f"data: {json.dumps({'chunk': part['text']})}\n\n"
 
200
  elif hasattr(part, 'string_value'):
201
  yield f"data: {json.dumps({'chunk': part.string_value})}\n\n"
 
202
 
203
  except (AttributeError, IndexError, TypeError) as e:
204
- # Log the error but continue - don't break the stream
205
- print(f"Error processing chunk: {e}, chunk structure: {repr(chunk)[:200]}")
206
  continue
207
 
208
- # Briefly pause to prevent overwhelming the client
209
  time.sleep(0.01)
210
 
 
 
 
 
211
  yield f"data: {json.dumps({'done': True})}\n\n"
 
212
 
213
  except Exception as e:
214
- # Send error to client and close stream
215
  error_message = f"Error editing notebook: {str(e)}"
216
- print(error_message)
217
  yield f"data: {json.dumps({'error': error_message})}\n\n"
218
  yield f"data: {json.dumps({'done': True})}\n\n"
219
 
220
- return Response(stream_with_context(generate()), content_type="text/event-stream")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from flask import Response, stream_with_context
3
  import json
4
  import time
5
+ import logging
6
+ import os
7
+
8
+ # Get logger from app
9
+ logger = logging.getLogger('notegenie')
10
+
11
+ # Check if we're running on Hugging Face Spaces
12
+ IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
13
 
14
  def craft_notebook_prompt(user_prompt):
15
  """Enhance the user prompt with instructions for generating a well-structured Jupyter notebook."""
 
98
  model = genai.GenerativeModel(model_name)
99
 
100
  enhanced_prompt = craft_notebook_prompt(user_prompt)
 
101
 
102
+ try:
103
+ response = model.generate_content(enhanced_prompt)
104
+ return response.text
105
+ except Exception as e:
106
+ logger.error(f"Error generating notebook content: {str(e)}", exc_info=True)
107
+ raise
108
 
109
  def edit_notebook(edit_request, notebook_json, model_name="gemini-2.0-pro-exp-02-05"):
110
  """Edit an existing notebook based on user request."""
 
122
 
123
  def generate():
124
  try:
125
+ logger.info(f"Starting streaming generation with model {model_name}")
126
  response = model.generate_content(enhanced_prompt, stream=True)
127
 
128
  # Send a notification that streaming has started
129
  yield f"data: {json.dumps({'chunk': 'Starting notebook generation...'})}\n\n"
130
 
131
+ # Heartbeat counter for Hugging Face compatibility
132
+ heartbeat_counter = 0
133
+ last_send_time = time.time()
134
+
135
  for chunk in response:
136
+ # Send heartbeat on Hugging Face to prevent connection timeout
137
+ current_time = time.time()
138
+ if IS_HUGGINGFACE and (current_time - last_send_time > 10): # Send heartbeat every 10 seconds
139
+ yield f"data: {json.dumps({'heartbeat': heartbeat_counter})}\n\n"
140
+ heartbeat_counter += 1
141
+ last_send_time = current_time
142
+
143
  try:
144
  # More robust empty chunk detection
145
  if not hasattr(chunk, 'parts') or not chunk.parts:
146
  # Skip this empty chunk and continue
147
+ logger.warning("Empty chunk received (no parts)")
148
  continue
149
 
150
  # First try the standard text property
151
  try:
152
  if hasattr(chunk, 'text') and chunk.text:
153
  yield f"data: {json.dumps({'chunk': chunk.text})}\n\n"
154
+ last_send_time = time.time()
155
  continue # If we successfully got text, continue to next chunk
156
+ except (AttributeError, IndexError) as e:
157
+ logger.warning(f"Error accessing text property: {e}")
158
  # If accessing text property fails, we'll try extracting from parts
159
  pass
160
 
 
163
  # Extract text from part using different approaches
164
  if hasattr(part, 'text') and part.text:
165
  yield f"data: {json.dumps({'chunk': part.text})}\n\n"
166
+ last_send_time = time.time()
167
  elif isinstance(part, dict) and 'text' in part:
168
  yield f"data: {json.dumps({'chunk': part['text']})}\n\n"
169
+ last_send_time = time.time()
170
  elif hasattr(part, 'string_value'):
171
  yield f"data: {json.dumps({'chunk': part.string_value})}\n\n"
172
+ last_send_time = time.time()
173
 
174
  except (AttributeError, IndexError, TypeError) as e:
175
  # Log the error but continue - don't break the stream
176
+ logger.error(f"Error processing chunk: {e}, chunk structure: {repr(chunk)[:200]}")
177
  continue
178
 
179
+ # Very brief pause to prevent overwhelming the client
180
  time.sleep(0.01)
181
 
182
+ # Final heartbeat for Hugging Face before completion
183
+ if IS_HUGGINGFACE:
184
+ yield f"data: {json.dumps({'heartbeat': 'final'})}\n\n"
185
+
186
  yield f"data: {json.dumps({'done': True})}\n\n"
187
+ logger.info("Streaming generation completed successfully")
188
 
189
  except Exception as e:
190
  # Send error to client and close stream
191
  error_message = f"Error generating notebook: {str(e)}"
192
+ logger.error(error_message, exc_info=True)
193
  yield f"data: {json.dumps({'error': error_message})}\n\n"
194
  yield f"data: {json.dumps({'done': True})}\n\n"
195
 
196
+ # Set appropriate headers for Hugging Face compatibility
197
+ headers = None
198
+ if IS_HUGGINGFACE:
199
+ headers = {
200
+ 'X-Accel-Buffering': 'no', # Disable proxy buffering
201
+ 'Cache-Control': 'no-cache',
202
+ 'Connection': 'keep-alive'
203
+ }
204
+
205
+ return Response(
206
+ stream_with_context(generate()),
207
+ content_type="text/event-stream",
208
+ headers=headers
209
+ )
210
 
211
  def stream_notebook_edit(edit_request, notebook_json, model_name="gemini-2.0-pro-exp-02-05"):
212
  """Stream notebook editing responses from Gemini API."""
 
215
 
216
  def generate():
217
  try:
218
+ logger.info(f"Starting streaming edit with model {model_name}")
219
  response = model.generate_content(enhanced_prompt, stream=True)
220
 
221
  # Send a notification that editing has started
222
  yield f"data: {json.dumps({'chunk': 'Starting notebook edit...'})}\n\n"
223
 
224
+ # Heartbeat counter for Hugging Face compatibility
225
+ heartbeat_counter = 0
226
+ last_send_time = time.time()
227
+
228
  for chunk in response:
229
+ # Send heartbeat on Hugging Face to prevent connection timeout
230
+ current_time = time.time()
231
+ if IS_HUGGINGFACE and (current_time - last_send_time > 10): # Send heartbeat every 10 seconds
232
+ yield f"data: {json.dumps({'heartbeat': heartbeat_counter})}\n\n"
233
+ heartbeat_counter += 1
234
+ last_send_time = current_time
235
+
236
+ # Process chunk similar to generation function
237
  try:
238
  # More robust empty chunk detection
239
  if not hasattr(chunk, 'parts') or not chunk.parts:
240
+ logger.warning("Empty chunk received (no parts)")
 
241
  continue
242
 
243
+ # Try standard text property first
244
  try:
245
  if hasattr(chunk, 'text') and chunk.text:
246
  yield f"data: {json.dumps({'chunk': chunk.text})}\n\n"
247
+ last_send_time = time.time()
248
+ continue
249
+ except (AttributeError, IndexError) as e:
250
+ logger.warning(f"Error accessing text property: {e}")
251
  pass
252
 
253
+ # Try parts extraction
254
  for part in chunk.parts:
 
255
  if hasattr(part, 'text') and part.text:
256
  yield f"data: {json.dumps({'chunk': part.text})}\n\n"
257
+ last_send_time = time.time()
258
  elif isinstance(part, dict) and 'text' in part:
259
  yield f"data: {json.dumps({'chunk': part['text']})}\n\n"
260
+ last_send_time = time.time()
261
  elif hasattr(part, 'string_value'):
262
  yield f"data: {json.dumps({'chunk': part.string_value})}\n\n"
263
+ last_send_time = time.time()
264
 
265
  except (AttributeError, IndexError, TypeError) as e:
266
+ logger.error(f"Error processing chunk: {e}, chunk structure: {repr(chunk)[:200]}")
 
267
  continue
268
 
 
269
  time.sleep(0.01)
270
 
271
+ # Final heartbeat for Hugging Face before completion
272
+ if IS_HUGGINGFACE:
273
+ yield f"data: {json.dumps({'heartbeat': 'final'})}\n\n"
274
+
275
  yield f"data: {json.dumps({'done': True})}\n\n"
276
+ logger.info("Streaming edit completed successfully")
277
 
278
  except Exception as e:
 
279
  error_message = f"Error editing notebook: {str(e)}"
280
+ logger.error(error_message, exc_info=True)
281
  yield f"data: {json.dumps({'error': error_message})}\n\n"
282
  yield f"data: {json.dumps({'done': True})}\n\n"
283
 
284
+ # Set appropriate headers for Hugging Face compatibility
285
+ headers = None
286
+ if IS_HUGGINGFACE:
287
+ headers = {
288
+ 'X-Accel-Buffering': 'no', # Disable proxy buffering
289
+ 'Cache-Control': 'no-cache',
290
+ 'Connection': 'keep-alive'
291
+ }
292
+
293
+ return Response(
294
+ stream_with_context(generate()),
295
+ content_type="text/event-stream",
296
+ headers=headers
297
+ )