ziadmostafa commited on
Commit
0d48ff6
·
1 Parent(s): 0213c17

Replace with updated NoteGenie version

Browse files
.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .env
6
+ .venv
7
+ env/
8
+ venv/
9
+ ENV/
10
+ .idea/
11
+ .vscode/
12
+ .git/
13
+ flask_session/
14
+ *.log
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Ignore Flask session data
2
+ /flask_session/
3
+
4
+ # Ignore Python cache files
5
+ /utils/__pycache__/
Dockerfile CHANGED
@@ -12,8 +12,10 @@ COPY . .
12
  # Create necessary directories
13
  RUN mkdir -p flask_session
14
 
15
- # Set permissions for flask_session directory
16
- RUN chmod -R 777 flask_session
 
 
17
 
18
  # Set environment variables
19
  ENV FLASK_APP=app.py
 
12
  # Create necessary directories
13
  RUN mkdir -p flask_session
14
 
15
+ # Set permissions for flask_session directory and touch API key file
16
+ RUN chmod -R 777 flask_session && \
17
+ touch api_key.txt && \
18
+ chmod 666 api_key.txt
19
 
20
  # Set environment variables
21
  ENV FLASK_APP=app.py
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: NoteGenie
3
- emoji: 📔
4
  colorFrom: red
5
  colorTo: pink
6
  sdk: docker
@@ -30,17 +30,6 @@ 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
- ### 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:
@@ -74,12 +63,6 @@ docker run -p 7860:7860 -e SECRET_KEY=your_secret_key notegenie
74
 
75
  NoteGenie requires a Google Gemini API key. Users can set their own API key in the web interface.
76
 
77
- ## API Key Security
78
-
79
- * NoteGenie requires each user to provide their own Google Gemini API key
80
- * API keys are stored only in the user's browser session and are never shared between users
81
- * For security reasons, there is no "default" API key - each user must provide their own
82
-
83
  ## License
84
 
85
  [MIT License](LICENSE)
 
1
  ---
2
  title: NoteGenie
3
+ emoji: 👁
4
  colorFrom: red
5
  colorTo: pink
6
  sdk: docker
 
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:
 
63
 
64
  NoteGenie requires a Google Gemini API key. Users can set their own API key in the web interface.
65
 
 
 
 
 
 
 
66
  ## License
67
 
68
  [MIT License](LICENSE)
app.py CHANGED
@@ -1,42 +1,36 @@
1
- from flask import Flask, request, jsonify, render_template, session, redirect, url_for, Response
 
2
  import google.generativeai as genai
3
  import json
4
  import uuid
5
  import os
6
  import logging
7
- import sys
8
  from utils.ai_helpers import generate_notebook, stream_notebook_generation, stream_notebook_edit, edit_notebook
9
  from utils.notebook_helpers import format_notebook, extract_notebook_info
10
- # Added for server-side sessions
11
- from flask_session import Session
12
 
13
  # Configure logging
14
  logging.basicConfig(
15
  level=logging.INFO,
16
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
- stream=sys.stdout
18
  )
19
- logger = logging.getLogger('notegenie')
20
 
21
  app = Flask(__name__)
22
  app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "notegenie-secret-key-change-in-production")
 
 
 
 
 
 
 
 
 
23
 
24
- # Detect if running on Hugging Face Spaces
25
- IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
26
- if IS_HUGGINGFACE:
27
- logger.info("Running on Hugging Face Spaces environment")
28
- # Ensure sessions work properly on Hugging Face
29
- app.config["SESSION_COOKIE_SECURE"] = False # Changed from True to False for HTTP connections
30
- app.config["SESSION_COOKIE_HTTPONLY"] = True
31
- app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
32
- # Add more durable storage for Hugging Face
33
- app.config["SESSION_FILE_THRESHOLD"] = 10 # Low threshold to ensure writes
34
- # Important: Don't use large session lifetime on Hugging Face
35
- app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 # 1 day only
36
- # Added: Use server-side sessions to persist API keys across workers
37
- app.config["SESSION_TYPE"] = "filesystem"
38
- app.config["SESSION_FILE_DIR"] = os.path.join(os.getcwd(), "flask_session")
39
- Session(app)
40
 
41
  # Map front-end model names to API model names
42
  MODEL_MAPPING = {
@@ -48,8 +42,35 @@ MODEL_MAPPING = {
48
  def get_api_model_name(frontend_model_name):
49
  return MODEL_MAPPING.get(frontend_model_name, frontend_model_name)
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  @app.route("/", methods=["GET"])
52
  def index():
 
 
 
53
  return render_template("index.html")
54
 
55
  @app.route("/set_api_key", methods=["POST"])
@@ -57,16 +78,25 @@ def set_api_key():
57
  api_key = request.form.get("api_key")
58
  if not api_key:
59
  return jsonify({"success": False, "message": "API key is required"}), 400
 
60
  try:
61
  # Test the API key
62
  genai.configure(api_key=api_key)
63
  model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
64
  response = model.generate_content("Say 'API key is valid'")
65
 
66
- # Store API key in session with permanent flag
67
  session.permanent = True
68
  session["api_key"] = api_key
69
 
 
 
 
 
 
 
 
 
70
  logger.info("API key successfully set and validated")
71
  return jsonify({"success": True})
72
  except Exception as e:
@@ -75,24 +105,13 @@ def set_api_key():
75
 
76
  @app.route("/generate_notebook", methods=["GET", "POST"])
77
  def generate_notebook_route():
78
- # SECURITY FIX: Only use session API key, never a global one
79
- api_key = session.get("api_key") if "api_key" in session else None
80
-
81
- # Fallback: check URL parameter for API key if not set in session
82
- if not api_key:
83
- api_key_param = request.args.get('api_key_param')
84
- if api_key_param:
85
- try:
86
- genai.configure(api_key=api_key_param)
87
- model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
88
- session["api_key"] = api_key_param
89
- api_key = api_key_param
90
- logger.info("API key set from URL parameter in generate_notebook_route")
91
- except Exception as e:
92
- logger.error(f"API key validation error from URL parameter in generate_notebook_route: {str(e)}")
93
  if not api_key:
94
  logger.warning("Generate notebook request without API key")
95
- return jsonify({"success": False, "message": "API key not set - please set your API key in the settings"}), 401
 
 
 
96
 
97
  # Handle both GET (for streaming) and POST requests
98
  if request.method == "GET":
@@ -111,18 +130,15 @@ def generate_notebook_route():
111
 
112
  # Map the frontend model name to the API model name
113
  api_model_name = get_api_model_name(model_name)
114
- logger.info(f"Generate notebook with model: {api_model_name}, stream: {stream}")
115
 
116
  try:
117
- # Configure with the api_key we retrieved
118
- genai.configure(api_key=api_key)
119
-
120
  # OPTIMIZATION: If format_only is True, skip the AI call and just format the provided content
121
  if request.method == "POST" and format_only:
122
  # Use client-provided content as is (it's already the AI response)
123
  notebook_content = prompt
124
  notebook_json = format_notebook(notebook_content)
125
  notebook_info = extract_notebook_info(notebook_content)
 
126
  return jsonify({
127
  "success": True,
128
  "notebook": notebook_json,
@@ -130,12 +146,12 @@ def generate_notebook_route():
130
  "description": notebook_info["description"]
131
  })
132
  elif stream:
133
- logger.info("Starting streaming notebook generation")
134
- return stream_notebook_generation(prompt, api_model_name, api_key)
135
  else:
136
  notebook_content = generate_notebook(prompt, api_model_name)
137
  notebook_json = format_notebook(notebook_content)
138
  notebook_info = extract_notebook_info(notebook_content)
 
139
  return jsonify({
140
  "success": True,
141
  "notebook": notebook_json,
@@ -143,13 +159,14 @@ def generate_notebook_route():
143
  "description": notebook_info["description"]
144
  })
145
  except Exception as e:
146
- logger.error(f"Error generating notebook: {str(e)}", exc_info=True)
147
  return jsonify({"success": False, "message": str(e)}), 500
148
 
149
  @app.route("/prepare_edit_notebook", methods=["POST"])
150
  def prepare_edit_notebook():
151
  """Store the notebook in the session for editing."""
152
- if "api_key" not in session:
 
153
  return jsonify({"success": False, "message": "API key not set"}), 401
154
 
155
  data = request.json
@@ -165,11 +182,12 @@ def prepare_edit_notebook():
165
 
166
  @app.route("/edit_notebook", methods=["GET", "POST"])
167
  def edit_notebook_route():
168
- # SECURITY FIX: Only use session API key, never a global one
169
- api_key = session.get("api_key") if "api_key" in session else None
170
-
171
  if not api_key:
172
- return jsonify({"success": False, "message": "API key not set - please set your API key in the settings"}), 401
 
 
 
173
 
174
  # Get edit prompt and current notebook
175
  if request.method == "GET":
@@ -195,15 +213,14 @@ def edit_notebook_route():
195
  api_model_name = get_api_model_name(model_name)
196
 
197
  try:
198
- # Use the api_key we retrieved, not session["api_key"]
199
- genai.configure(api_key=api_key)
200
  if stream:
201
- return stream_notebook_edit(edit_prompt, notebook_json, api_model_name, api_key)
202
  else:
203
  # Non-streaming path (not used in current UI but kept for API completeness)
204
  edited_content = edit_notebook(edit_prompt, notebook_json, api_model_name)
205
  notebook_json = format_notebook(edited_content)
206
  notebook_info = extract_notebook_info(edited_content)
 
207
  return jsonify({
208
  "success": True,
209
  "notebook": notebook_json,
@@ -211,11 +228,13 @@ def edit_notebook_route():
211
  "description": notebook_info["description"]
212
  })
213
  except Exception as e:
214
- logger.error(f"Error editing notebook: {str(e)}", exc_info=True)
215
  return jsonify({"success": False, "message": str(e)}), 500
216
 
217
  @app.route("/download_notebook", methods=["POST"])
218
  def download_notebook():
 
 
219
  data = request.json
220
  notebook_json = data.get("notebook")
221
  filename = data.get("filename", f"notebook_{uuid.uuid4()}.ipynb")
@@ -231,75 +250,30 @@ def download_notebook():
231
  mimetype="application/json",
232
  headers={"Content-Disposition": f"attachment;filename={filename}"}
233
  )
 
234
  return response
235
 
236
- @app.route("/session_test", methods=["GET"])
237
- def session_test():
238
- if "session_test" not in session:
239
- session["session_test"] = True
240
- is_new = True
241
- else:
242
- is_new = False
243
-
244
- # Log the current session state
245
- session_vars = list(session.keys()) if session else []
246
- logger.info(f"Session check - variables: {session_vars}")
247
-
248
- # Check if API key is available from URL parameter (fallback for HF)
249
- api_key_param = request.args.get('api_key_param')
250
- if api_key_param and "api_key" not in session:
251
- try:
252
- # Validate it quickly before accepting
253
- genai.configure(api_key=api_key_param)
254
- model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
255
- # If no exception, store it
256
- session["api_key"] = api_key_param
257
- logger.info("API key set from URL parameter")
258
- except Exception as e:
259
- logger.error(f"Invalid API key from URL parameter: {str(e)}")
260
-
261
- return jsonify({
262
- "session_works": True,
263
- "is_new_session": is_new,
264
  "has_api_key": "api_key" in session,
265
- "session_vars": session_vars
266
- })
267
-
268
- # Add a session check endpoint to debug session issues
269
- @app.route("/session_check", methods=["GET"])
270
- def session_check():
271
- if "session_test" not in session:
272
- session["session_test"] = True
273
- is_new = True
274
- else:
275
- is_new = False
276
 
277
- # Log the current session state
278
- session_vars = list(session.keys()) if session else []
279
- logger.info(f"Session check - variables: {session_vars}")
280
 
281
- # Check if API key is available from URL parameter (fallback for HF)
282
- # SECURITY FIX: Only set API key in the current user's session, never globally
283
- api_key_param = request.args.get('api_key_param')
284
- if api_key_param and "api_key" not in session:
285
- try:
286
- # Validate it quickly before accepting
287
- genai.configure(api_key=api_key_param)
288
- model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
289
- # If no exception, store it in session only
290
- session["api_key"] = api_key_param
291
- logger.info("API key set from URL parameter")
292
- except Exception as e:
293
- logger.error(f"Invalid API key from URL parameter: {str(e)}")
294
 
295
- return jsonify({
296
- "session_works": True,
297
- "is_new_session": is_new,
298
- "has_api_key": "api_key" in session,
299
- "session_vars": session_vars
300
- })
301
 
302
  if __name__ == "__main__":
303
- debug_mode = os.environ.get("FLASK_ENV") == "development"
304
  port = int(os.environ.get("PORT", 5000))
305
- app.run(host="0.0.0.0", port=port, debug=debug_mode)
 
1
+ from flask import Flask, request, jsonify, render_template, session, redirect, url_for
2
+ from flask_session import Session
3
  import google.generativeai as genai
4
  import json
5
  import uuid
6
  import os
7
  import logging
 
8
  from utils.ai_helpers import generate_notebook, stream_notebook_generation, stream_notebook_edit, edit_notebook
9
  from utils.notebook_helpers import format_notebook, extract_notebook_info
 
 
10
 
11
  # Configure logging
12
  logging.basicConfig(
13
  level=logging.INFO,
14
+ format='%(asctime)s - notegenie - %(levelname)s - %(message)s'
 
15
  )
16
+ logger = logging.getLogger()
17
 
18
  app = Flask(__name__)
19
  app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "notegenie-secret-key-change-in-production")
20
+ app.config["SESSION_TYPE"] = "filesystem"
21
+ app.config["SESSION_PERMANENT"] = True
22
+ app.config["SESSION_USE_SIGNER"] = True
23
+ app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 30 # 30 days
24
+ app.config["SESSION_FILE_DIR"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flask_session")
25
+ os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True) # Ensure directory exists
26
+
27
+ # Set a more permissive file mode for session files to avoid permission issues
28
+ app.config["SESSION_FILE_MODE"] = 0o666
29
 
30
+ Session(app)
31
+
32
+ # API key file storage path (as backup for session)
33
+ API_KEY_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "api_key.txt")
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  # Map front-end model names to API model names
36
  MODEL_MAPPING = {
 
42
  def get_api_model_name(frontend_model_name):
43
  return MODEL_MAPPING.get(frontend_model_name, frontend_model_name)
44
 
45
+ # Function to get API key with multiple fallbacks
46
+ def get_api_key():
47
+ # Try to get from session first
48
+ api_key = session.get("api_key")
49
+
50
+ # If not in session, try to get from file
51
+ if not api_key:
52
+ try:
53
+ if os.path.exists(API_KEY_FILE):
54
+ with open(API_KEY_FILE, "r") as f:
55
+ api_key = f.read().strip()
56
+ # Restore session if we found a key
57
+ if api_key:
58
+ session["api_key"] = api_key
59
+ logger.info("API key restored from backup file")
60
+ except Exception as e:
61
+ logger.error(f"Error reading API key file: {str(e)}")
62
+
63
+ # Try to get from request header or param (for direct API calls)
64
+ if not api_key:
65
+ api_key = request.headers.get("X-API-Key") or request.args.get("api_key")
66
+
67
+ return api_key
68
+
69
  @app.route("/", methods=["GET"])
70
  def index():
71
+ # Test session functionality
72
+ session["session_test"] = True
73
+ logger.info(f"Session check - variables: {list(session.keys())}")
74
  return render_template("index.html")
75
 
76
  @app.route("/set_api_key", methods=["POST"])
 
78
  api_key = request.form.get("api_key")
79
  if not api_key:
80
  return jsonify({"success": False, "message": "API key is required"}), 400
81
+
82
  try:
83
  # Test the API key
84
  genai.configure(api_key=api_key)
85
  model = genai.GenerativeModel("gemini-2.0-pro-exp-02-05")
86
  response = model.generate_content("Say 'API key is valid'")
87
 
88
+ # Store API key in session
89
  session.permanent = True
90
  session["api_key"] = api_key
91
 
92
+ # Also store in backup file as failsafe
93
+ try:
94
+ with open(API_KEY_FILE, "w") as f:
95
+ f.write(api_key)
96
+ os.chmod(API_KEY_FILE, 0o666) # Make readable/writable
97
+ except Exception as e:
98
+ logger.error(f"Failed to write API key to backup file: {str(e)}")
99
+
100
  logger.info("API key successfully set and validated")
101
  return jsonify({"success": True})
102
  except Exception as e:
 
105
 
106
  @app.route("/generate_notebook", methods=["GET", "POST"])
107
  def generate_notebook_route():
108
+ api_key = get_api_key()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  if not api_key:
110
  logger.warning("Generate notebook request without API key")
111
+ return jsonify({"success": False, "message": "API key not set"}), 401
112
+
113
+ # Always configure genai with the API key for each request
114
+ genai.configure(api_key=api_key)
115
 
116
  # Handle both GET (for streaming) and POST requests
117
  if request.method == "GET":
 
130
 
131
  # Map the frontend model name to the API model name
132
  api_model_name = get_api_model_name(model_name)
 
133
 
134
  try:
 
 
 
135
  # OPTIMIZATION: If format_only is True, skip the AI call and just format the provided content
136
  if request.method == "POST" and format_only:
137
  # Use client-provided content as is (it's already the AI response)
138
  notebook_content = prompt
139
  notebook_json = format_notebook(notebook_content)
140
  notebook_info = extract_notebook_info(notebook_content)
141
+
142
  return jsonify({
143
  "success": True,
144
  "notebook": notebook_json,
 
146
  "description": notebook_info["description"]
147
  })
148
  elif stream:
149
+ return stream_notebook_generation(prompt, api_model_name)
 
150
  else:
151
  notebook_content = generate_notebook(prompt, api_model_name)
152
  notebook_json = format_notebook(notebook_content)
153
  notebook_info = extract_notebook_info(notebook_content)
154
+
155
  return jsonify({
156
  "success": True,
157
  "notebook": notebook_json,
 
159
  "description": notebook_info["description"]
160
  })
161
  except Exception as e:
162
+ logger.error(f"Error generating notebook: {str(e)}")
163
  return jsonify({"success": False, "message": str(e)}), 500
164
 
165
  @app.route("/prepare_edit_notebook", methods=["POST"])
166
  def prepare_edit_notebook():
167
  """Store the notebook in the session for editing."""
168
+ api_key = get_api_key()
169
+ if not api_key:
170
  return jsonify({"success": False, "message": "API key not set"}), 401
171
 
172
  data = request.json
 
182
 
183
  @app.route("/edit_notebook", methods=["GET", "POST"])
184
  def edit_notebook_route():
185
+ api_key = get_api_key()
 
 
186
  if not api_key:
187
+ return jsonify({"success": False, "message": "API key not set"}), 401
188
+
189
+ # Always configure genai with the API key for each request
190
+ genai.configure(api_key=api_key)
191
 
192
  # Get edit prompt and current notebook
193
  if request.method == "GET":
 
213
  api_model_name = get_api_model_name(model_name)
214
 
215
  try:
 
 
216
  if stream:
217
+ return stream_notebook_edit(edit_prompt, notebook_json, api_model_name)
218
  else:
219
  # Non-streaming path (not used in current UI but kept for API completeness)
220
  edited_content = edit_notebook(edit_prompt, notebook_json, api_model_name)
221
  notebook_json = format_notebook(edited_content)
222
  notebook_info = extract_notebook_info(edited_content)
223
+
224
  return jsonify({
225
  "success": True,
226
  "notebook": notebook_json,
 
228
  "description": notebook_info["description"]
229
  })
230
  except Exception as e:
231
+ app.logger.error(f"Error editing notebook: {str(e)}")
232
  return jsonify({"success": False, "message": str(e)}), 500
233
 
234
  @app.route("/download_notebook", methods=["POST"])
235
  def download_notebook():
236
+ from flask import Response
237
+
238
  data = request.json
239
  notebook_json = data.get("notebook")
240
  filename = data.get("filename", f"notebook_{uuid.uuid4()}.ipynb")
 
250
  mimetype="application/json",
251
  headers={"Content-Disposition": f"attachment;filename={filename}"}
252
  )
253
+
254
  return response
255
 
256
+ # Add a session diagnostic endpoint
257
+ @app.route("/check_session", methods=["GET"])
258
+ def check_session():
259
+ # For debugging only - would be disabled in production
260
+ session_data = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  "has_api_key": "api_key" in session,
262
+ "session_vars": list(session.keys()),
263
+ "session_file_dir_exists": os.path.exists(app.config["SESSION_FILE_DIR"]),
264
+ "session_file_dir_writable": os.access(app.config["SESSION_FILE_DIR"], os.W_OK),
265
+ "api_key_file_exists": os.path.exists(API_KEY_FILE),
266
+ }
 
 
 
 
 
 
267
 
268
+ # Check if running on Hugging Face Spaces
269
+ is_hf_space = "SPACE_ID" in os.environ
270
+ session_data["is_huggingface_space"] = is_hf_space
271
 
272
+ if is_hf_space:
273
+ logger.info("Running on Hugging Face Spaces environment")
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ return jsonify(session_data)
 
 
 
 
 
276
 
277
  if __name__ == "__main__":
 
278
  port = int(os.environ.get("PORT", 5000))
279
+ app.run(host="0.0.0.0", port=port, debug=(os.environ.get("FLASK_ENV") == "development"))
static/css/style.css CHANGED
@@ -135,27 +135,23 @@ h1, h2, h3, h4, h5, h6 {
135
 
136
  /* Custom scrollbar styles */
137
  .conversation-container::-webkit-scrollbar,
138
- .notebook-preview::-webkit-scrollbar,
139
- .prompt-input::-webkit-scrollbar {
140
  width: 8px;
141
  }
142
 
143
  .conversation-container::-webkit-scrollbar-track,
144
- .notebook-preview::-webkit-scrollbar-track,
145
- .prompt-input::-webkit-scrollbar-track {
146
  background: transparent;
147
  }
148
 
149
  .conversation-container::-webkit-scrollbar-thumb,
150
- .notebook-preview::-webkit-scrollbar-thumb,
151
- .prompt-input::-webkit-scrollbar-thumb {
152
  background-color: rgba(95, 99, 104, 0.3);
153
  border-radius: 20px;
154
  }
155
 
156
  .conversation-container::-webkit-scrollbar-thumb:hover,
157
- .notebook-preview::-webkit-scrollbar-thumb:hover,
158
- .prompt-input::-webkit-scrollbar-thumb:hover {
159
  background-color: rgba(95, 99, 104, 0.5);
160
  }
161
 
 
135
 
136
  /* Custom scrollbar styles */
137
  .conversation-container::-webkit-scrollbar,
138
+ .notebook-preview::-webkit-scrollbar {
 
139
  width: 8px;
140
  }
141
 
142
  .conversation-container::-webkit-scrollbar-track,
143
+ .notebook-preview::-webkit-scrollbar-track {
 
144
  background: transparent;
145
  }
146
 
147
  .conversation-container::-webkit-scrollbar-thumb,
148
+ .notebook-preview::-webkit-scrollbar-thumb {
 
149
  background-color: rgba(95, 99, 104, 0.3);
150
  border-radius: 20px;
151
  }
152
 
153
  .conversation-container::-webkit-scrollbar-thumb:hover,
154
+ .notebook-preview::-webkit-scrollbar-thumb:hover {
 
155
  background-color: rgba(95, 99, 104, 0.5);
156
  }
157
 
static/js/main.js CHANGED
@@ -166,9 +166,6 @@ document.addEventListener('DOMContentLoaded', function() {
166
  // Store API key in localStorage as backup
167
  localStorage.setItem('notegenie_api_key', apiKey);
168
 
169
- // For Hugging Face, add a flag that API key was set in this session
170
- sessionStorage.setItem('api_key_set', 'true');
171
-
172
  if (document.querySelector('#apiKeyModal.show')) {
173
  showApiKeyFeedback('API key saved successfully!', 'success');
174
  setTimeout(() => {
@@ -298,167 +295,116 @@ document.addEventListener('DOMContentLoaded', function() {
298
  }
299
  }, 5000); // Check every 5 seconds
300
 
301
- // Add a retry mechanism
302
- let retryCount = 0;
303
- const MAX_RETRIES = 2;
304
 
305
- function createEventSource() {
306
- // Create a new event source with a unique timestamp to prevent caching
307
- const timestamp = Date.now();
308
-
309
- // On Hugging Face, add API key as URL parameter if stored (fallback mechanism)
310
- let apiKeyParam = '';
311
- if (window.location.hostname.includes('huggingface.co') ||
312
- window.location.hostname.includes('hf.space')) {
313
- const storedApiKey = localStorage.getItem('notegenie_api_key');
314
- if (storedApiKey && !sessionStorage.getItem('api_key_tried_in_url')) {
315
- apiKeyParam = `&api_key_param=${encodeURIComponent(storedApiKey)}`;
316
- sessionStorage.setItem('api_key_tried_in_url', 'true');
317
- }
318
- }
319
-
320
- eventSource = new EventSource(`/generate_notebook?${new URLSearchParams({
321
- prompt: prompt,
322
- model: modelName,
323
- stream: true,
324
- t: timestamp // Add timestamp to prevent caching
325
- }).toString()}${apiKeyParam}`);
326
 
327
- eventSource.onmessage = function(event) {
328
- // Update our last-activity timestamp
329
- lastChunkTime = Date.now();
330
 
331
- try {
332
- const data = JSON.parse(event.data);
 
 
333
 
334
- // Handle heartbeat messages (for Hugging Face)
335
- if (data.heartbeat !== undefined) {
336
- console.log(`Heartbeat received: ${data.heartbeat}`);
337
- return; // Just a keepalive, no content to process
338
  }
339
 
340
- // Handle errors sent from the server
341
- if (data.error) {
342
- console.error("Server error:", data.error);
343
- updateAiMessage(aiMessageId, `**Error:** ${data.error}`);
344
-
345
- // Try to salvage what we have so far
346
- if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
347
- processNotebookResponse(aiResponseText);
348
- }
349
-
350
- eventSource.close();
351
- eventSource = null;
352
- setGeneratingState(false);
353
- clearInterval(connectionTimer);
354
- return;
 
 
355
  }
356
 
357
- if (data.chunk) {
358
- aiResponseText += data.chunk;
359
-
360
- // Extract notebook info as soon as it's available
361
- const nameMatch = aiResponseText.match(/NOTEBOOK_NAME:?\s*(.+?)(?:\n|$)/);
362
- const descMatch = aiResponseText.match(/NOTEBOOK_DESCRIPTION:?\s*(.+?)(?:\n|$)/);
363
-
364
- if (nameMatch && nameMatch[1].trim()) {
365
- // Update notebook title immediately when found
366
- notebookTitleEl.textContent = nameMatch[1].trim();
367
- }
368
-
369
- if (descMatch && descMatch[1].trim()) {
370
- // Update AI message to only display the description, not the full response
371
- updateAiMessage(aiMessageId, `**NoteGenie:** ${descMatch[1].trim()}`);
372
- } else {
373
- // Show a simple generating message while waiting for description
374
- updateAiMessage(aiMessageId, "**NoteGenie:** Generating your notebook...");
375
- }
376
-
377
- // Update preview periodically during streaming
378
- const now = Date.now();
379
- if (now - lastPreviewUpdate > PREVIEW_UPDATE_INTERVAL) {
380
- lastPreviewUpdate = now;
381
-
382
- // Only try to update preview if we have meaningful content
383
- if (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL')) {
384
- updateNotebookPreviewDuringStream(aiResponseText);
385
- }
386
- }
387
  }
388
 
389
- if (data.done) {
390
- eventSource.close();
391
- eventSource = null;
392
- clearInterval(connectionTimer);
393
 
394
- // Process the complete response for final rendering
395
- processNotebookResponse(aiResponseText);
396
- setGeneratingState(false);
 
397
  }
398
- } catch (error) {
399
- console.error('Error parsing event data:', error, event.data);
400
  }
401
- };
402
-
403
- eventSource.onerror = function(err) {
404
- console.error('EventSource error:', err);
405
 
406
- // Don't retry if we're already disconnected
407
- if (eventSource.readyState === 2) {
408
  eventSource.close();
409
  eventSource = null;
410
  clearInterval(connectionTimer);
411
 
412
- if (retryCount < MAX_RETRIES) {
413
- retryCount++;
414
- console.log(`Retrying connection (${retryCount}/${MAX_RETRIES})...`);
415
- updateAiMessage(aiMessageId, `**NoteGenie:** Connection issue, retrying... (${retryCount}/${MAX_RETRIES})`);
416
-
417
- // Wait a moment before retrying
418
- setTimeout(() => {
419
- createEventSource();
420
- }, 2000);
421
- return;
422
- }
423
-
424
- // Check if it's an auth error (most likely API key not set)
425
- if (err.status === 401) {
426
- // Updated error message to be more explicit
427
- updateAiMessage(aiMessageId, '**Error: API key not set or invalid.** \n\nYou must set your own Google Gemini API key to use NoteGenie. Please click the API Key button in the top right corner.');
428
-
429
- // Automatically show the API key modal
430
- showApiKeyModal();
431
- } else {
432
- // Try to salvage what we have so far
433
- if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
434
- updateAiMessage(aiMessageId, '**Warning:** Connection issue occurred but I\'ll try to process what I received so far.');
435
- processNotebookResponse(aiResponseText);
436
- } else {
437
- // Provide more detailed error message for Hugging Face users
438
- let errorMsg = '**Error:** Failed to generate notebook.';
439
-
440
- // Check if we're on Hugging Face (URL check)
441
- if (window.location.hostname.includes('huggingface.co') ||
442
- window.location.hostname.includes('hf.space')) {
443
- errorMsg += ' This might be due to Hugging Face Space limitations. Try: \n\n' +
444
- '1. Refreshing the page \n' +
445
- '2. Using a shorter prompt \n' +
446
- '3. Re-entering your API key';
447
- } else {
448
- errorMsg += ' Please try again.';
449
- }
450
-
451
- updateAiMessage(aiMessageId, errorMsg);
452
- }
453
- }
454
-
455
  setGeneratingState(false);
456
  }
457
- };
458
- }
 
 
459
 
460
- // Initial event source creation
461
- createEventSource();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  }
463
 
464
  function handleEditStreamingResponse(editPrompt, notebook, modelName, aiMessageId) {
@@ -481,6 +427,8 @@ document.addEventListener('DOMContentLoaded', function() {
481
  method: 'POST',
482
  headers: {
483
  'Content-Type': 'application/json',
 
 
484
  },
485
  body: JSON.stringify({
486
  notebook: notebook
@@ -494,12 +442,20 @@ document.addEventListener('DOMContentLoaded', function() {
494
  })
495
  .then(data => {
496
  if (data.success) {
497
- // Now create a new event source for editing - the notebook is now in the session
498
- eventSource = new EventSource(`/edit_notebook?${new URLSearchParams({
499
  edit_prompt: editPrompt,
500
  model: modelName,
501
  stream: true
502
- }).toString()}`);
 
 
 
 
 
 
 
 
503
 
504
  // Track the last time we got a chunk - for timeout detection
505
  let lastChunkTime = Date.now();
@@ -633,15 +589,7 @@ document.addEventListener('DOMContentLoaded', function() {
633
  })
634
  .catch(error => {
635
  console.error('Error preparing notebook for edit:', error);
636
-
637
- // Check if it's an auth error and handle API key issues more clearly
638
- if (error.message && error.message.includes("API key not set")) {
639
- updateAiMessage(aiMessageId, '**Error: API key not set or invalid.** \n\nYou must set your own Google Gemini API key to use NoteGenie. Please click the API Key button in the top right corner.');
640
- showApiKeyModal();
641
- } else {
642
- updateAiMessage(aiMessageId, '**Error:** ' + error.message);
643
- }
644
-
645
  setGeneratingState(false);
646
  });
647
  }
 
166
  // Store API key in localStorage as backup
167
  localStorage.setItem('notegenie_api_key', apiKey);
168
 
 
 
 
169
  if (document.querySelector('#apiKeyModal.show')) {
170
  showApiKeyFeedback('API key saved successfully!', 'success');
171
  setTimeout(() => {
 
295
  }
296
  }, 5000); // Check every 5 seconds
297
 
298
+ // Get API key from localStorage as a backup
299
+ const backupApiKey = localStorage.getItem('notegenie_api_key');
 
300
 
301
+ // Create URL parameters including API key as fallback
302
+ const urlParams = new URLSearchParams({
303
+ prompt: prompt,
304
+ model: modelName,
305
+ stream: true
306
+ });
307
+
308
+ // Add API key to URL params if available from localStorage (as a fallback)
309
+ if (backupApiKey) {
310
+ urlParams.append('api_key', backupApiKey);
311
+ }
312
+
313
+ // Create a new event source with API key included
314
+ eventSource = new EventSource(`/generate_notebook?${urlParams.toString()}`);
315
+
316
+ eventSource.onmessage = function(event) {
317
+ // Update our last-activity timestamp
318
+ lastChunkTime = Date.now();
 
 
 
319
 
320
+ try {
321
+ const data = JSON.parse(event.data);
 
322
 
323
+ // Handle errors sent from the server
324
+ if (data.error) {
325
+ console.error("Server error:", data.error);
326
+ updateAiMessage(aiMessageId, `**Error:** ${data.error}`);
327
 
328
+ // Try to salvage what we have so far
329
+ if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
330
+ processNotebookResponse(aiResponseText);
 
331
  }
332
 
333
+ eventSource.close();
334
+ eventSource = null;
335
+ setGeneratingState(false);
336
+ clearInterval(connectionTimer);
337
+ return;
338
+ }
339
+
340
+ if (data.chunk) {
341
+ aiResponseText += data.chunk;
342
+
343
+ // Extract notebook info as soon as it's available
344
+ const nameMatch = aiResponseText.match(/NOTEBOOK_NAME:?\s*(.+?)(?:\n|$)/);
345
+ const descMatch = aiResponseText.match(/NOTEBOOK_DESCRIPTION:?\s*(.+?)(?:\n|$)/);
346
+
347
+ if (nameMatch && nameMatch[1].trim()) {
348
+ // Update notebook title immediately when found
349
+ notebookTitleEl.textContent = nameMatch[1].trim();
350
  }
351
 
352
+ if (descMatch && descMatch[1].trim()) {
353
+ // Update AI message to only display the description, not the full response
354
+ updateAiMessage(aiMessageId, `**NoteGenie:** ${descMatch[1].trim()}`);
355
+ } else {
356
+ // Show a simple generating message while waiting for description
357
+ updateAiMessage(aiMessageId, "**NoteGenie:** Generating your notebook...");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  }
359
 
360
+ // Update preview periodically during streaming
361
+ const now = Date.now();
362
+ if (now - lastPreviewUpdate > PREVIEW_UPDATE_INTERVAL) {
363
+ lastPreviewUpdate = now;
364
 
365
+ // Only try to update preview if we have meaningful content
366
+ if (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL')) {
367
+ updateNotebookPreviewDuringStream(aiResponseText);
368
+ }
369
  }
 
 
370
  }
 
 
 
 
371
 
372
+ if (data.done) {
 
373
  eventSource.close();
374
  eventSource = null;
375
  clearInterval(connectionTimer);
376
 
377
+ // Process the complete response for final rendering
378
+ processNotebookResponse(aiResponseText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  setGeneratingState(false);
380
  }
381
+ } catch (error) {
382
+ console.error('Error parsing event data:', error, event.data);
383
+ }
384
+ };
385
 
386
+ eventSource.onerror = function(err) {
387
+ console.error('EventSource error:', err);
388
+ eventSource.close();
389
+ eventSource = null;
390
+ clearInterval(connectionTimer);
391
+
392
+ // Check if it's an auth error (most likely API key not set)
393
+ if (err.status === 401) {
394
+ 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.');
395
+ showApiKeyModal();
396
+ } else {
397
+ // Try to salvage what we have so far
398
+ if (aiResponseText && (aiResponseText.includes('MARKDOWN CELL') || aiResponseText.includes('CODE CELL'))) {
399
+ updateAiMessage(aiMessageId, '**Warning:** Connection issue occurred but I\'ll try to process what I received so far.');
400
+ processNotebookResponse(aiResponseText);
401
+ } else {
402
+ updateAiMessage(aiMessageId, '**Error:** Failed to generate notebook. Please try again.');
403
+ }
404
+ }
405
+
406
+ setGeneratingState(false);
407
+ };
408
  }
409
 
410
  function handleEditStreamingResponse(editPrompt, notebook, modelName, aiMessageId) {
 
427
  method: 'POST',
428
  headers: {
429
  'Content-Type': 'application/json',
430
+ // Add API key as header if available
431
+ ...(backupApiKey && {'X-API-Key': backupApiKey})
432
  },
433
  body: JSON.stringify({
434
  notebook: notebook
 
442
  })
443
  .then(data => {
444
  if (data.success) {
445
+ // Create URL parameters including API key as fallback
446
+ const urlParams = new URLSearchParams({
447
  edit_prompt: editPrompt,
448
  model: modelName,
449
  stream: true
450
+ });
451
+
452
+ // Add API key to URL params if available from localStorage (as a fallback)
453
+ if (backupApiKey) {
454
+ urlParams.append('api_key', backupApiKey);
455
+ }
456
+
457
+ // Now create a new event source for editing with the API key included
458
+ eventSource = new EventSource(`/edit_notebook?${urlParams.toString()}`);
459
 
460
  // Track the last time we got a chunk - for timeout detection
461
  let lastChunkTime = Date.now();
 
589
  })
590
  .catch(error => {
591
  console.error('Error preparing notebook for edit:', error);
592
+ updateAiMessage(aiMessageId, '**Error:** ' + error.message);
 
 
 
 
 
 
 
 
593
  setGeneratingState(false);
594
  });
595
  }
templates/index.html CHANGED
@@ -93,7 +93,6 @@
93
  <span class="material-icons info-icon">lightbulb</span>
94
  <div class="info-content">
95
  <p><strong>Tip:</strong> Be specific in your request for best results. Include the topic, intended audience, and desired level of detail.</p>
96
- <p><strong>Important:</strong> NoteGenie requires you to provide your own Google AI Studio API key. Which is free.</p>
97
  </div>
98
  </div>
99
  <div class="example-section">
@@ -254,28 +253,5 @@
254
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
255
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
256
  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
257
-
258
- <!-- Hugging Face Spaces Session Check -->
259
- <script>
260
- if (window.location.hostname.includes('huggingface.co') || window.location.hostname.includes('hf.space')) {
261
- console.log("Running on Hugging Face Spaces");
262
- // Check if cookies are enabled
263
- if (navigator.cookieEnabled) {
264
- console.log("Cookies are enabled");
265
- } else {
266
- console.warn("Cookies are disabled - sessions won't work!");
267
- }
268
-
269
- // Add a hidden diagonstic endpoint
270
- fetch('/session_check')
271
- .then(response => response.json())
272
- .then(data => {
273
- console.log("Session check:", data);
274
- })
275
- .catch(err => {
276
- console.error("Session check failed:", err);
277
- });
278
- }
279
- </script>
280
  </body>
281
  </html>
 
93
  <span class="material-icons info-icon">lightbulb</span>
94
  <div class="info-content">
95
  <p><strong>Tip:</strong> Be specific in your request for best results. Include the topic, intended audience, and desired level of detail.</p>
 
96
  </div>
97
  </div>
98
  <div class="example-section">
 
253
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
254
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
255
  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  </body>
257
  </html>
utils/__pycache__/ai_helpers.cpython-39.pyc DELETED
Binary file (7.73 kB)
 
utils/__pycache__/notebook_helpers.cpython-39.pyc DELETED
Binary file (5.24 kB)
 
utils/ai_helpers.py CHANGED
@@ -2,14 +2,6 @@ import google.generativeai as genai
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,13 +90,9 @@ def generate_notebook(user_prompt, model_name="gemini-2.0-pro-exp-02-05"):
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."""
@@ -115,50 +103,32 @@ def edit_notebook(edit_request, notebook_json, model_name="gemini-2.0-pro-exp-02
115
 
116
  return response.text
117
 
118
- def stream_notebook_generation(user_prompt, model_name="gemini-2.0-pro-exp-02-05", api_key=None):
119
  """Stream notebook generation responses from Gemini API."""
120
- # Configure the API with the key if provided
121
- if api_key:
122
- genai.configure(api_key=api_key)
123
-
124
  model = genai.GenerativeModel(model_name)
125
  enhanced_prompt = craft_notebook_prompt(user_prompt)
126
 
127
  def generate():
128
  try:
129
- logger.info(f"Starting streaming generation with model {model_name}")
130
  response = model.generate_content(enhanced_prompt, stream=True)
131
 
132
  # Send a notification that streaming has started
133
  yield f"data: {json.dumps({'chunk': 'Starting notebook generation...'})}\n\n"
134
 
135
- # Heartbeat counter for Hugging Face compatibility
136
- heartbeat_counter = 0
137
- last_send_time = time.time()
138
-
139
  for chunk in response:
140
- # Send heartbeat on Hugging Face to prevent connection timeout
141
- current_time = time.time()
142
- if IS_HUGGINGFACE and (current_time - last_send_time > 10): # Send heartbeat every 10 seconds
143
- yield f"data: {json.dumps({'heartbeat': heartbeat_counter})}\n\n"
144
- heartbeat_counter += 1
145
- last_send_time = current_time
146
-
147
  try:
148
  # More robust empty chunk detection
149
  if not hasattr(chunk, 'parts') or not chunk.parts:
150
  # Skip this empty chunk and continue
151
- logger.warning("Empty chunk received (no parts)")
152
  continue
153
 
154
  # First try the standard text property
155
  try:
156
  if hasattr(chunk, 'text') and chunk.text:
157
  yield f"data: {json.dumps({'chunk': chunk.text})}\n\n"
158
- last_send_time = time.time()
159
  continue # If we successfully got text, continue to next chunk
160
- except (AttributeError, IndexError) as e:
161
- logger.warning(f"Error accessing text property: {e}")
162
  # If accessing text property fails, we'll try extracting from parts
163
  pass
164
 
@@ -167,139 +137,84 @@ def stream_notebook_generation(user_prompt, model_name="gemini-2.0-pro-exp-02-05
167
  # Extract text from part using different approaches
168
  if hasattr(part, 'text') and part.text:
169
  yield f"data: {json.dumps({'chunk': part.text})}\n\n"
170
- last_send_time = time.time()
171
  elif isinstance(part, dict) and 'text' in part:
172
  yield f"data: {json.dumps({'chunk': part['text']})}\n\n"
173
- last_send_time = time.time()
174
  elif hasattr(part, 'string_value'):
175
  yield f"data: {json.dumps({'chunk': part.string_value})}\n\n"
176
- last_send_time = time.time()
177
 
178
  except (AttributeError, IndexError, TypeError) as e:
179
  # Log the error but continue - don't break the stream
180
- logger.error(f"Error processing chunk: {e}, chunk structure: {repr(chunk)[:200]}")
181
  continue
182
 
183
- # Very brief pause to prevent overwhelming the client
184
  time.sleep(0.01)
185
 
186
- # Final heartbeat for Hugging Face before completion
187
- if IS_HUGGINGFACE:
188
- yield f"data: {json.dumps({'heartbeat': 'final'})}\n\n"
189
-
190
  yield f"data: {json.dumps({'done': True})}\n\n"
191
- logger.info("Streaming generation completed successfully")
192
 
193
  except Exception as e:
194
  # Send error to client and close stream
195
  error_message = f"Error generating notebook: {str(e)}"
196
- logger.error(error_message, exc_info=True)
197
  yield f"data: {json.dumps({'error': error_message})}\n\n"
198
  yield f"data: {json.dumps({'done': True})}\n\n"
199
 
200
- # Set appropriate headers for Hugging Face compatibility
201
- headers = None
202
- if IS_HUGGINGFACE:
203
- headers = {
204
- 'X-Accel-Buffering': 'no', # Disable proxy buffering
205
- 'Cache-Control': 'no-cache',
206
- 'Connection': 'keep-alive'
207
- }
208
-
209
- return Response(
210
- stream_with_context(generate()),
211
- content_type="text/event-stream",
212
- headers=headers
213
- )
214
 
215
- def stream_notebook_edit(edit_request, notebook_json, model_name="gemini-2.0-pro-exp-02-05", api_key=None):
216
  """Stream notebook editing responses from Gemini API."""
217
- # Configure the API with the key if provided
218
- if api_key:
219
- genai.configure(api_key=api_key)
220
-
221
  model = genai.GenerativeModel(model_name)
222
  enhanced_prompt = craft_edit_prompt(edit_request, notebook_json)
223
 
224
  def generate():
225
  try:
226
- logger.info(f"Starting streaming edit with model {model_name}")
227
  response = model.generate_content(enhanced_prompt, stream=True)
228
 
229
  # Send a notification that editing has started
230
  yield f"data: {json.dumps({'chunk': 'Starting notebook edit...'})}\n\n"
231
 
232
- # Heartbeat counter for Hugging Face compatibility
233
- heartbeat_counter = 0
234
- last_send_time = time.time()
235
-
236
  for chunk in response:
237
- # Send heartbeat on Hugging Face to prevent connection timeout
238
- current_time = time.time()
239
- if IS_HUGGINGFACE and (current_time - last_send_time > 10): # Send heartbeat every 10 seconds
240
- yield f"data: {json.dumps({'heartbeat': heartbeat_counter})}\n\n"
241
- heartbeat_counter += 1
242
- last_send_time = current_time
243
-
244
- # Process chunk similar to generation function
245
  try:
246
  # More robust empty chunk detection
247
  if not hasattr(chunk, 'parts') or not chunk.parts:
248
- logger.warning("Empty chunk received (no parts)")
 
249
  continue
250
 
251
- # Try standard text property first
252
  try:
253
  if hasattr(chunk, 'text') and chunk.text:
254
  yield f"data: {json.dumps({'chunk': chunk.text})}\n\n"
255
- last_send_time = time.time()
256
- continue
257
- except (AttributeError, IndexError) as e:
258
- logger.warning(f"Error accessing text property: {e}")
259
  pass
260
 
261
- # Try parts extraction
262
  for part in chunk.parts:
 
263
  if hasattr(part, 'text') and part.text:
264
  yield f"data: {json.dumps({'chunk': part.text})}\n\n"
265
- last_send_time = time.time()
266
  elif isinstance(part, dict) and 'text' in part:
267
  yield f"data: {json.dumps({'chunk': part['text']})}\n\n"
268
- last_send_time = time.time()
269
  elif hasattr(part, 'string_value'):
270
  yield f"data: {json.dumps({'chunk': part.string_value})}\n\n"
271
- last_send_time = time.time()
272
 
273
  except (AttributeError, IndexError, TypeError) as e:
274
- logger.error(f"Error processing chunk: {e}, chunk structure: {repr(chunk)[:200]}")
 
275
  continue
276
 
 
277
  time.sleep(0.01)
278
 
279
- # Final heartbeat for Hugging Face before completion
280
- if IS_HUGGINGFACE:
281
- yield f"data: {json.dumps({'heartbeat': 'final'})}\n\n"
282
-
283
  yield f"data: {json.dumps({'done': True})}\n\n"
284
- logger.info("Streaming edit completed successfully")
285
 
286
  except Exception as e:
 
287
  error_message = f"Error editing notebook: {str(e)}"
288
- logger.error(error_message, exc_info=True)
289
  yield f"data: {json.dumps({'error': error_message})}\n\n"
290
  yield f"data: {json.dumps({'done': True})}\n\n"
291
 
292
- # Set appropriate headers for Hugging Face compatibility
293
- headers = None
294
- if IS_HUGGINGFACE:
295
- headers = {
296
- 'X-Accel-Buffering': 'no', # Disable proxy buffering
297
- 'Cache-Control': 'no-cache',
298
- 'Connection': 'keep-alive'
299
- }
300
-
301
- return Response(
302
- stream_with_context(generate()),
303
- content_type="text/event-stream",
304
- headers=headers
305
- )
 
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
  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."""
 
103
 
104
  return response.text
105
 
106
+ def stream_notebook_generation(user_prompt, model_name="gemini-2.0-pro-exp-02-05"):
107
  """Stream notebook generation responses from Gemini API."""
 
 
 
 
108
  model = genai.GenerativeModel(model_name)
109
  enhanced_prompt = craft_notebook_prompt(user_prompt)
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
  # 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."""
 
 
 
 
166
  model = genai.GenerativeModel(model_name)
167
  enhanced_prompt = craft_edit_prompt(edit_request, notebook_json)
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")