abc1181 commited on
Commit
02bfaec
Β·
verified Β·
1 Parent(s): e6f7650

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -136
app.py CHANGED
@@ -1,48 +1,43 @@
1
- import os, json, time, base64, uuid, threading, subprocess, sys
2
- from flask import Flask, request, jsonify, Response, send_file
3
  from flask_cors import CORS
4
  import jupyter_client
5
- import queue, io
6
 
7
  app = Flask(__name__)
8
  CORS(app, resources={r"/*": {"origins": "*"}})
9
 
10
- # ── Auth ──────────────────────────────────────────────────────────────────────
11
- VALID_API_KEY = os.getenv("SANDBOX_API_KEY", "your-secret-key")
12
 
13
  def check_auth():
14
- key = request.headers.get("X-API-KEY") or request.headers.get("Authorization", "").replace("Bearer ", "")
 
 
 
 
 
15
  return key == VALID_API_KEY
16
 
17
- # ── Kernel Session Manager ────────────────────────────────────────────────────
18
- # Each session gets its own isolated Jupyter kernel
19
- sessions = {}
20
  sessions_lock = threading.Lock()
21
 
22
  def get_or_create_kernel(session_id="default"):
23
- """Get existing kernel or create a new isolated one."""
24
  with sessions_lock:
25
  if session_id not in sessions:
26
- print(f"[SANDBOX] Creating new kernel for session: {session_id}")
27
  km = jupyter_client.KernelManager(kernel_name='python3')
28
  km.start_kernel()
29
  kc = km.client()
30
  kc.start_channels()
31
  kc.wait_for_ready(timeout=30)
32
  sessions[session_id] = {
33
- "km": km,
34
- "kc": kc,
35
  "created_at": time.time(),
36
- "last_used": time.time()
37
  }
38
- print(f"[SANDBOX] Kernel ready for session: {session_id}")
39
  else:
40
  sessions[session_id]["last_used"] = time.time()
41
-
42
  return sessions[session_id]["kc"]
43
 
44
  def kill_kernel(session_id):
45
- """Cleanly shut down a kernel session."""
46
  with sessions_lock:
47
  if session_id in sessions:
48
  try:
@@ -51,172 +46,166 @@ def kill_kernel(session_id):
51
  except:
52
  pass
53
  del sessions[session_id]
54
- print(f"[SANDBOX] Kernel killed for session: {session_id}")
55
 
56
  def execute_code(kc, code, timeout=60):
57
- """
58
- Execute code on a Jupyter kernel.
59
- Returns: dict with stdout, stderr, result, artifacts (base64 images)
60
- """
61
- msg_id = kc.execute(code)
62
-
63
- stdout_parts = []
64
- stderr_parts = []
65
- result_parts = []
66
- artifacts = []
67
- error = None
68
-
69
- deadline = time.time() + timeout
70
 
71
  while time.time() < deadline:
72
  try:
73
- msg = kc.get_iopub_msg(timeout=1)
74
  msg_type = msg['msg_type']
75
- content = msg['content']
76
 
77
  if msg_type == 'stream':
78
- if content['name'] == 'stdout':
79
- stdout_parts.append(content['text'])
80
- elif content['name'] == 'stderr':
81
- stderr_parts.append(content['text'])
82
 
83
  elif msg_type == 'execute_result':
84
- result_parts.append(content['data'].get('text/plain', ''))
85
- # Capture HTML output too
86
  if 'text/html' in content['data']:
87
- result_parts.append(content['data']['text/html'])
88
 
89
  elif msg_type == 'display_data':
90
- # ── Artifact capture β€” images, charts, plots ──────────────────
91
  data = content.get('data', {})
92
  if 'image/png' in data:
93
  artifacts.append({
94
- "type": "image/png",
95
- "data": data['image/png'], # already base64
96
  "filename": f"artifact_{len(artifacts)+1}.png"
97
  })
98
  if 'image/jpeg' in data:
99
  artifacts.append({
100
- "type": "image/jpeg",
101
- "data": data['image/jpeg'],
102
  "filename": f"artifact_{len(artifacts)+1}.jpg"
103
  })
104
- if 'text/html' in data:
105
- result_parts.append(data['text/html'])
106
  if 'text/plain' in data:
107
- result_parts.append(data['text/plain'])
108
 
109
  elif msg_type == 'error':
110
  error = {
111
- "ename": content.get('ename', 'Error'),
112
- "evalue": content.get('evalue', ''),
113
  "traceback": "\n".join(
114
- # Strip ANSI escape codes
115
  line.encode('ascii', errors='ignore').decode()
116
  for line in content.get('traceback', [])
117
  )
118
  }
119
 
120
  elif msg_type == 'status' and content.get('execution_state') == 'idle':
121
- # Kernel finished executing
122
  break
123
 
124
  except queue.Empty:
125
  continue
126
- except Exception as e:
127
  break
128
 
129
  return {
130
- "stdout": "".join(stdout_parts),
131
- "stderr": "".join(stderr_parts),
132
- "result": "\n".join(result_parts),
133
  "artifacts": artifacts,
134
- "error": error
135
  }
136
 
137
- # ── Cleanup old idle kernels every 30 mins ────────────────────────────────────
138
- def cleanup_idle_kernels():
139
  while True:
140
  time.sleep(1800)
141
- now = time.time()
142
  to_kill = []
143
  with sessions_lock:
144
  for sid, data in sessions.items():
145
- if now - data["last_used"] > 3600: # 1 hour idle
146
  to_kill.append(sid)
147
  for sid in to_kill:
148
  kill_kernel(sid)
149
- print(f"[SANDBOX] Cleaned up idle kernel: {sid}")
150
 
151
- threading.Thread(target=cleanup_idle_kernels, daemon=True).start()
152
 
153
- # ==========================================
154
- # ENDPOINTS
155
- # ==========================================
156
 
157
  @app.route('/')
158
  def status():
159
  return jsonify({
160
- "status": "online",
161
  "active_sessions": len(sessions),
162
- "message": "Jupyter Kernel Sandbox is running"
163
  })
164
 
165
- # ── Main execute endpoint (compatible with your ollama-tools) ─────────────────
166
  @app.route('/execute', methods=['POST'])
167
  def execute():
168
  if not check_auth():
169
- return jsonify({"output": "Unauthorized: Invalid API Key"}), 401
170
 
171
- data = request.json or {}
172
- code = data.get("code", "")
173
  session_id = data.get("session_id", "default")
174
- timeout = data.get("timeout", 60)
175
 
176
  if not code:
177
- return jsonify({"output": "Error: No code provided"}), 400
178
-
179
- print(f"[SANDBOX] Executing code in session: {session_id}")
180
 
181
  try:
182
- kc = get_or_create_kernel(session_id)
183
  result = execute_code(kc, code, timeout=timeout)
184
 
185
- # Build readable output
186
- output_parts = []
187
- if result["stdout"]: output_parts.append(result["stdout"])
188
- if result["result"]: output_parts.append(result["result"])
189
  if result["error"]:
190
- output_parts.append(
191
- f"Error: {result['error']['ename']}: {result['error']['evalue']}\n{result['error']['traceback']}"
 
192
  )
193
- if result["stderr"] and not result["error"]:
194
- output_parts.append(result["stderr"])
195
 
196
  return jsonify({
197
- "output": "\n".join(output_parts) or "Code executed successfully (no output)",
198
- "artifacts": result["artifacts"], # list of base64 images/charts
199
- "error": result["error"]
200
  })
201
-
202
  except Exception as e:
203
  return jsonify({"output": f"Kernel Error: {str(e)}"}), 500
204
 
205
- # ── Session management ────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  @app.route('/sessions', methods=['GET'])
207
  def list_sessions():
208
  if not check_auth():
209
  return jsonify({"error": "Unauthorized"}), 401
210
- return jsonify({
211
- "sessions": [
212
- {
213
- "id": sid,
214
- "created_at": data["created_at"],
215
- "last_used": data["last_used"]
216
- }
217
- for sid, data in sessions.items()
218
- ]
219
- })
220
 
221
  @app.route('/sessions/<session_id>', methods=['DELETE'])
222
  def delete_session(session_id):
@@ -225,7 +214,6 @@ def delete_session(session_id):
225
  kill_kernel(session_id)
226
  return jsonify({"message": f"Session {session_id} terminated"})
227
 
228
- # ── New session ───────────────────────────────────────────────────────────────
229
  @app.route('/sessions', methods=['POST'])
230
  def create_session():
231
  if not check_auth():
@@ -234,44 +222,19 @@ def create_session():
234
  get_or_create_kernel(session_id)
235
  return jsonify({"session_id": session_id})
236
 
237
- # ── Install packages into a session kernel ────────────────────────────────────
238
- @app.route('/install', methods=['POST'])
239
- def install_package():
240
- if not check_auth():
241
- return jsonify({"error": "Unauthorized"}), 401
242
-
243
- data = request.json or {}
244
- package = data.get("package", "")
245
- session_id = data.get("session_id", "default")
246
-
247
- if not package:
248
- return jsonify({"error": "No package specified"}), 400
249
-
250
- kc = get_or_create_kernel(session_id)
251
- result = execute_code(kc, f"import subprocess; subprocess.run(['pip', 'install', '{package}', '-q'])", timeout=120)
252
-
253
- return jsonify({
254
- "message": f"Package {package} installed",
255
- "output": result["stdout"]
256
- })
257
-
258
- # ── File upload into kernel environment ──────────────────────────────────────
259
  @app.route('/upload', methods=['POST'])
260
  def upload_file():
261
  if not check_auth():
262
  return jsonify({"error": "Unauthorized"}), 401
263
-
264
- data = request.json or {}
265
- filename = data.get("filename", "uploaded_file")
266
- file_b64 = data.get("base64", "")
267
  session_id = data.get("session_id", "default")
268
-
269
  file_bytes = base64.b64decode(file_b64)
270
-
271
- # Write file via kernel
272
- kc = get_or_create_kernel(session_id)
273
- encoded = base64.b64encode(file_bytes).decode()
274
- code = f"""
275
  import base64
276
  with open('/tmp/{filename}', 'wb') as f:
277
  f.write(base64.b64decode('{encoded}'))
@@ -281,8 +244,7 @@ print('File written to /tmp/{filename}')
281
  return jsonify({"output": result["stdout"], "path": f"/tmp/{filename}"})
282
 
283
  if __name__ == "__main__":
284
- print("[SANDBOX] Jupyter Kernel Sandbox starting on port 7860...")
285
- # Pre-warm default kernel
286
  get_or_create_kernel("default")
287
- print("[SANDBOX] Default kernel ready!")
288
  app.run(host="0.0.0.0", port=7860)
 
1
+ import os, json, time, base64, uuid, threading, queue, io
2
+ from flask import Flask, request, jsonify, Response
3
  from flask_cors import CORS
4
  import jupyter_client
 
5
 
6
  app = Flask(__name__)
7
  CORS(app, resources={r"/*": {"origins": "*"}})
8
 
9
+ VALID_API_KEY = os.getenv("SANDBOX_API_KEY", "")
 
10
 
11
  def check_auth():
12
+ if not VALID_API_KEY:
13
+ return True
14
+ key = (
15
+ request.headers.get("X-API-KEY", "") or
16
+ request.headers.get("Authorization", "").replace("Bearer ", "")
17
+ )
18
  return key == VALID_API_KEY
19
 
20
+ sessions = {}
 
 
21
  sessions_lock = threading.Lock()
22
 
23
  def get_or_create_kernel(session_id="default"):
 
24
  with sessions_lock:
25
  if session_id not in sessions:
 
26
  km = jupyter_client.KernelManager(kernel_name='python3')
27
  km.start_kernel()
28
  kc = km.client()
29
  kc.start_channels()
30
  kc.wait_for_ready(timeout=30)
31
  sessions[session_id] = {
32
+ "km": km, "kc": kc,
 
33
  "created_at": time.time(),
34
+ "last_used": time.time()
35
  }
 
36
  else:
37
  sessions[session_id]["last_used"] = time.time()
 
38
  return sessions[session_id]["kc"]
39
 
40
  def kill_kernel(session_id):
 
41
  with sessions_lock:
42
  if session_id in sessions:
43
  try:
 
46
  except:
47
  pass
48
  del sessions[session_id]
 
49
 
50
  def execute_code(kc, code, timeout=60):
51
+ msg_id = kc.execute(code)
52
+ stdout = []
53
+ stderr = []
54
+ result = []
55
+ artifacts = []
56
+ error = None
57
+ deadline = time.time() + timeout
 
 
 
 
 
 
58
 
59
  while time.time() < deadline:
60
  try:
61
+ msg = kc.get_iopub_msg(timeout=1)
62
  msg_type = msg['msg_type']
63
+ content = msg['content']
64
 
65
  if msg_type == 'stream':
66
+ if content['name'] == 'stdout': stdout.append(content['text'])
67
+ else: stderr.append(content['text'])
 
 
68
 
69
  elif msg_type == 'execute_result':
70
+ result.append(content['data'].get('text/plain', ''))
 
71
  if 'text/html' in content['data']:
72
+ result.append(content['data']['text/html'])
73
 
74
  elif msg_type == 'display_data':
 
75
  data = content.get('data', {})
76
  if 'image/png' in data:
77
  artifacts.append({
78
+ "type": "image/png",
79
+ "data": data['image/png'],
80
  "filename": f"artifact_{len(artifacts)+1}.png"
81
  })
82
  if 'image/jpeg' in data:
83
  artifacts.append({
84
+ "type": "image/jpeg",
85
+ "data": data['image/jpeg'],
86
  "filename": f"artifact_{len(artifacts)+1}.jpg"
87
  })
 
 
88
  if 'text/plain' in data:
89
+ result.append(data['text/plain'])
90
 
91
  elif msg_type == 'error':
92
  error = {
93
+ "ename": content.get('ename', 'Error'),
94
+ "evalue": content.get('evalue', ''),
95
  "traceback": "\n".join(
 
96
  line.encode('ascii', errors='ignore').decode()
97
  for line in content.get('traceback', [])
98
  )
99
  }
100
 
101
  elif msg_type == 'status' and content.get('execution_state') == 'idle':
 
102
  break
103
 
104
  except queue.Empty:
105
  continue
106
+ except Exception:
107
  break
108
 
109
  return {
110
+ "stdout": "".join(stdout),
111
+ "stderr": "".join(stderr),
112
+ "result": "\n".join(result),
113
  "artifacts": artifacts,
114
+ "error": error
115
  }
116
 
117
+ # ── Cleanup idle kernels ──────────────────────────────────────────────────────
118
+ def cleanup_loop():
119
  while True:
120
  time.sleep(1800)
121
+ now = time.time()
122
  to_kill = []
123
  with sessions_lock:
124
  for sid, data in sessions.items():
125
+ if now - data["last_used"] > 3600:
126
  to_kill.append(sid)
127
  for sid in to_kill:
128
  kill_kernel(sid)
 
129
 
130
+ threading.Thread(target=cleanup_loop, daemon=True).start()
131
 
132
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
 
 
133
 
134
  @app.route('/')
135
  def status():
136
  return jsonify({
137
+ "status": "online",
138
  "active_sessions": len(sessions),
139
+ "message": "Obsidian Jupyter Kernel Sandbox"
140
  })
141
 
 
142
  @app.route('/execute', methods=['POST'])
143
  def execute():
144
  if not check_auth():
145
+ return jsonify({"output": "Unauthorized"}), 401
146
 
147
+ data = request.json or {}
148
+ code = data.get("code", "")
149
  session_id = data.get("session_id", "default")
150
+ timeout = data.get("timeout", 60)
151
 
152
  if not code:
153
+ return jsonify({"output": "No code provided"}), 400
 
 
154
 
155
  try:
156
+ kc = get_or_create_kernel(session_id)
157
  result = execute_code(kc, code, timeout=timeout)
158
 
159
+ parts = []
160
+ if result["stdout"]: parts.append(result["stdout"])
161
+ if result["result"]: parts.append(result["result"])
 
162
  if result["error"]:
163
+ parts.append(
164
+ f"Error: {result['error']['ename']}: {result['error']['evalue']}\n"
165
+ f"{result['error']['traceback']}"
166
  )
167
+ elif result["stderr"] and not result["error"]:
168
+ parts.append(result["stderr"])
169
 
170
  return jsonify({
171
+ "output": "\n".join(parts) or "Executed successfully (no output)",
172
+ "artifacts": result["artifacts"],
173
+ "error": result["error"]
174
  })
 
175
  except Exception as e:
176
  return jsonify({"output": f"Kernel Error: {str(e)}"}), 500
177
 
178
+ @app.route('/install', methods=['POST'])
179
+ def install():
180
+ if not check_auth():
181
+ return jsonify({"error": "Unauthorized"}), 401
182
+
183
+ data = request.json or {}
184
+ package = data.get("package", "")
185
+ session_id = data.get("session_id", "default")
186
+
187
+ if not package:
188
+ return jsonify({"error": "No package specified"}), 400
189
+
190
+ kc = get_or_create_kernel(session_id)
191
+ result = execute_code(
192
+ kc,
193
+ f"import subprocess; subprocess.run(['pip', 'install', '{package}', '-q'])",
194
+ timeout=120
195
+ )
196
+ return jsonify({"message": f"{package} installed", "output": result["stdout"]})
197
+
198
  @app.route('/sessions', methods=['GET'])
199
  def list_sessions():
200
  if not check_auth():
201
  return jsonify({"error": "Unauthorized"}), 401
202
+ with sessions_lock:
203
+ return jsonify({
204
+ "sessions": [
205
+ {"id": sid, "created_at": d["created_at"], "last_used": d["last_used"]}
206
+ for sid, d in sessions.items()
207
+ ]
208
+ })
 
 
 
209
 
210
  @app.route('/sessions/<session_id>', methods=['DELETE'])
211
  def delete_session(session_id):
 
214
  kill_kernel(session_id)
215
  return jsonify({"message": f"Session {session_id} terminated"})
216
 
 
217
  @app.route('/sessions', methods=['POST'])
218
  def create_session():
219
  if not check_auth():
 
222
  get_or_create_kernel(session_id)
223
  return jsonify({"session_id": session_id})
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  @app.route('/upload', methods=['POST'])
226
  def upload_file():
227
  if not check_auth():
228
  return jsonify({"error": "Unauthorized"}), 401
229
+ data = request.json or {}
230
+ filename = data.get("filename", "uploaded_file")
231
+ file_b64 = data.get("base64", "")
 
232
  session_id = data.get("session_id", "default")
233
+
234
  file_bytes = base64.b64decode(file_b64)
235
+ kc = get_or_create_kernel(session_id)
236
+ encoded = base64.b64encode(file_bytes).decode()
237
+ code = f"""
 
 
238
  import base64
239
  with open('/tmp/{filename}', 'wb') as f:
240
  f.write(base64.b64decode('{encoded}'))
 
244
  return jsonify({"output": result["stdout"], "path": f"/tmp/{filename}"})
245
 
246
  if __name__ == "__main__":
247
+ print("[SANDBOX] Pre-warming default kernel...")
 
248
  get_or_create_kernel("default")
249
+ print("[SANDBOX] Ready on port 7860")
250
  app.run(host="0.0.0.0", port=7860)