lvwerra HF Staff commited on
Commit
99d2505
·
1 Parent(s): 0f4c888

qwen-coder

Browse files
Files changed (3) hide show
  1. app.py +19 -15
  2. jupyter_handler.py +572 -0
  3. utils.py +138 -284
app.py CHANGED
@@ -6,6 +6,9 @@ from e2b_code_interpreter import Sandbox
6
  from pathlib import Path
7
  from transformers import AutoTokenizer
8
  import json
 
 
 
9
 
10
  if not get_space():
11
  try:
@@ -18,38 +21,39 @@ if not get_space():
18
 
19
  from utils import (
20
  run_interactive_notebook,
21
- create_base_notebook,
22
- update_notebook_display,
23
  )
24
 
25
  E2B_API_KEY = os.environ["E2B_API_KEY"]
26
- HF_TOKEN = os.environ["HF_TOKEN"]
27
  DEFAULT_MAX_TOKENS = 512
28
  SANDBOXES = {}
 
29
  TMP_DIR = './tmp/'
30
  if not os.path.exists(TMP_DIR):
31
  os.makedirs(TMP_DIR)
32
 
33
- notebook_data = create_base_notebook([])[0]
34
  with open(TMP_DIR+"jupyter-agent.ipynb", 'w', encoding='utf-8') as f:
35
- json.dump(notebook_data, f, indent=2)
36
 
37
  with open("ds-system-prompt.txt", "r") as f:
38
  DEFAULT_SYSTEM_PROMPT = f.read()
39
-
40
 
41
  def execute_jupyter_agent(
42
  sytem_prompt, user_input, max_new_tokens, model, files, message_history, request: gr.Request
43
  ):
44
  if request.session_hash not in SANDBOXES:
45
- SANDBOXES[request.session_hash] = Sandbox(api_key=E2B_API_KEY)
46
  sbx = SANDBOXES[request.session_hash]
47
 
48
  save_dir = os.path.join(TMP_DIR, request.session_hash)
49
  os.makedirs(save_dir, exist_ok=True)
50
  save_dir = os.path.join(save_dir, 'jupyter-agent.ipynb')
51
 
52
- client = InferenceClient(api_key=HF_TOKEN, provider="hf-inference")
 
 
 
53
 
54
  tokenizer = AutoTokenizer.from_pretrained(model)
55
  # model = "meta-llama/Llama-3.1-8B-Instruct"
@@ -88,7 +92,7 @@ def execute_jupyter_agent(
88
 
89
  def clear(msg_state):
90
  msg_state = []
91
- return update_notebook_display(create_base_notebook([])[0]), msg_state
92
 
93
 
94
  css = """
@@ -112,10 +116,11 @@ css = """
112
  with gr.Blocks() as demo:
113
  msg_state = gr.State(value=[])
114
 
115
- html_output = gr.HTML(value=update_notebook_display(create_base_notebook([])[0]))
116
 
117
  user_input = gr.Textbox(
118
- value="Solve the Lotka-Volterra equation and plot the results.", lines=3, label="User input"
 
119
  )
120
 
121
  with gr.Row():
@@ -145,11 +150,10 @@ with gr.Blocks() as demo:
145
  )
146
 
147
  model = gr.Dropdown(
148
- value="meta-llama/Llama-3.1-8B-Instruct",
149
  choices=[
150
- "meta-llama/Llama-3.2-3B-Instruct",
151
- "meta-llama/Llama-3.1-8B-Instruct",
152
- "meta-llama/Llama-3.1-70B-Instruct",
153
  ],
154
  label="Models"
155
  )
 
6
  from pathlib import Path
7
  from transformers import AutoTokenizer
8
  import json
9
+ from openai import OpenAI
10
+ from huggingface_hub import HfApi, HfFolder
11
+ from jupyter_handler import JupyterNotebook
12
 
13
  if not get_space():
14
  try:
 
21
 
22
  from utils import (
23
  run_interactive_notebook,
 
 
24
  )
25
 
26
  E2B_API_KEY = os.environ["E2B_API_KEY"]
27
+ HF_TOKEN = os.environ["HF_TOKEN"] #HfFolder.get_token() #
28
  DEFAULT_MAX_TOKENS = 512
29
  SANDBOXES = {}
30
+ SANDBOX_TIMEOUT = 5
31
  TMP_DIR = './tmp/'
32
  if not os.path.exists(TMP_DIR):
33
  os.makedirs(TMP_DIR)
34
 
 
35
  with open(TMP_DIR+"jupyter-agent.ipynb", 'w', encoding='utf-8') as f:
36
+ json.dump(JupyterNotebook().data, f, indent=2)
37
 
38
  with open("ds-system-prompt.txt", "r") as f:
39
  DEFAULT_SYSTEM_PROMPT = f.read()
40
+ DEFAULT_SYSTEM_PROMPT = None
41
 
42
  def execute_jupyter_agent(
43
  sytem_prompt, user_input, max_new_tokens, model, files, message_history, request: gr.Request
44
  ):
45
  if request.session_hash not in SANDBOXES:
46
+ SANDBOXES[request.session_hash] = Sandbox(api_key=E2B_API_KEY, timeout=SANDBOX_TIMEOUT)
47
  sbx = SANDBOXES[request.session_hash]
48
 
49
  save_dir = os.path.join(TMP_DIR, request.session_hash)
50
  os.makedirs(save_dir, exist_ok=True)
51
  save_dir = os.path.join(save_dir, 'jupyter-agent.ipynb')
52
 
53
+ client = OpenAI(
54
+ base_url="https://router.huggingface.co/v1",
55
+ api_key=HF_TOKEN,
56
+ )
57
 
58
  tokenizer = AutoTokenizer.from_pretrained(model)
59
  # model = "meta-llama/Llama-3.1-8B-Instruct"
 
92
 
93
  def clear(msg_state):
94
  msg_state = []
95
+ return JupyterNotebook().render(), msg_state
96
 
97
 
98
  css = """
 
116
  with gr.Blocks() as demo:
117
  msg_state = gr.State(value=[])
118
 
119
+ html_output = gr.HTML(value=JupyterNotebook().render())
120
 
121
  user_input = gr.Textbox(
122
+ value="Write code to multiply three numbers: 10048, 32, 19", lines=3, label="User input"
123
+ #value="Solve the Lotka-Volterra equation and plot the results. Do it step by step and explain what you are doing and in the end make a super nice and clean plot.", lines=3, label="User input"
124
  )
125
 
126
  with gr.Row():
 
150
  )
151
 
152
  model = gr.Dropdown(
153
+ value="Qwen/Qwen3-Coder-480B-A35B-Instruct",
154
  choices=[
155
+ "Qwen/Qwen3-Coder-30B-A3B-Instruct",
156
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct",
 
157
  ],
158
  label="Models"
159
  )
jupyter_handler.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import nbformat
2
+ from nbconvert import HTMLExporter
3
+ from traitlets.config import Config
4
+ import json
5
+ import copy
6
+ from jinja2 import DictLoader
7
+ import datetime
8
+
9
+
10
+ system_template = """\
11
+ <details>
12
+ <summary style="display: flex; align-items: center; cursor: pointer;">
13
+ <span style="background-color: #374151; color: white; padding: 0px 4px; border-radius: 3px; font-weight: 500; display: inline-block;">System:</span>
14
+ <span class="arrow" style="margin-left: 8px; font-size: 12px;">▶</span>
15
+ </summary>
16
+ <div style="margin-top: 8px; padding: 8px; background-color: #f9fafb; border-radius: 4px; border-left: 3px solid #374151;">
17
+ {}
18
+ </div>
19
+ </details>
20
+
21
+ <style>
22
+ details > summary .arrow {{
23
+ display: inline-block;
24
+ transition: transform 0.2s;
25
+ }}
26
+ details[open] > summary .arrow {{
27
+ transform: rotate(90deg);
28
+ }}
29
+ details > summary {{
30
+ list-style: none;
31
+ }}
32
+ details > summary::-webkit-details-marker {{
33
+ display: none;
34
+ }}
35
+ </style>
36
+ """
37
+
38
+ user_template = """\
39
+ <span style="background-color: #166534; color: white; padding: 0px 4px; border-radius: 3px; font-weight: 500; display: inline-block; margin-bottom: 0px;">User:</span> {}"""
40
+
41
+
42
+ assistant_thinking_template = """\
43
+ <span style="background-color: #1d5b8e; color: white; padding: 0px 4px; border-radius: 3px; font-weight: 500; display: inline-block; margin-bottom: 0px;">Assistant:</span> {}"""
44
+
45
+ assistant_final_answer_template = """<div class="alert alert-block alert-warning">
46
+ <b>Assistant:</b> Final answer: {}
47
+ </div>
48
+ """
49
+
50
+ header_message = """<p align="center">
51
+ <img src="https://huggingface.co/spaces/lvwerra/jupyter-agent/resolve/main/jupyter-agent.png" alt="Jupyter Agent Logo" />
52
+ </p>
53
+
54
+
55
+ <p style="text-align:center;">Let a LLM agent write and execute code inside a notebook!</p>"""
56
+
57
+ bad_html_bad = """input[type="file"] {
58
+ display: block;
59
+ }"""
60
+
61
+
62
+ EXECUTING_WIDGET = """
63
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #e3f2fd; border-radius: 6px; border-left: 3px solid #2196f3;">
64
+ <div style="display: flex; gap: 4px;">
65
+ <div style="width: 6px; height: 6px; background-color: #2196f3; border-radius: 50%; animation: pulse 1.5s ease-in-out infinite;"></div>
66
+ <div style="width: 6px; height: 6px; background-color: #2196f3; border-radius: 50%; animation: pulse 1.5s ease-in-out 0.1s infinite;"></div>
67
+ <div style="width: 6px; height: 6px; background-color: #2196f3; border-radius: 50%; animation: pulse 1.5s ease-in-out 0.2s infinite;"></div>
68
+ </div>
69
+ <span style="color: #1976d2; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
70
+ Executing code...
71
+ </span>
72
+ </div>
73
+
74
+ <style>
75
+ @keyframes pulse {
76
+ 0%, 80%, 100% {
77
+ opacity: 0.3;
78
+ transform: scale(0.8);
79
+ }
80
+ 40% {
81
+ opacity: 1;
82
+ transform: scale(1);
83
+ }
84
+ }
85
+ </style>
86
+ """
87
+
88
+ GENERATING_WIDGET = """
89
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #f3e5f5; border-radius: 6px; border-left: 3px solid #9c27b0;">
90
+ <div style="width: 80px; height: 4px; background-color: #e1bee7; border-radius: 2px; overflow: hidden;">
91
+ <div style="width: 30%; height: 100%; background-color: #9c27b0; border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div>
92
+ </div>
93
+ <span style="color: #7b1fa2; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
94
+ Generating...
95
+ </span>
96
+ </div>
97
+
98
+ <style>
99
+ @keyframes progress {
100
+ 0% { transform: translateX(-100%); }
101
+ 100% { transform: translateX(250%); }
102
+ }
103
+ </style>
104
+ """
105
+
106
+ DONE_WIDGET = """
107
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #e8f5e8; border-radius: 6px; border-left: 3px solid #4caf50;">
108
+ <div style="width: 16px; height: 16px; background-color: #4caf50; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
109
+ <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
110
+ <path d="M1 4L3.5 6.5L9 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
111
+ </svg>
112
+ </div>
113
+ <span style="color: #2e7d32; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
114
+ Generation complete
115
+ </span>
116
+ </div>
117
+ """
118
+
119
+ DONE_WIDGET = """
120
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #e8f5e8; border-radius: 6px; border-left: 3px solid #4caf50; animation: fadeInOut 4s ease-in-out forwards;">
121
+ <div style="width: 16px; height: 16px; background-color: #4caf50; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
122
+ <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
123
+ <path d="M1 4L3.5 6.5L9 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
124
+ </svg>
125
+ </div>
126
+ <span style="color: #2e7d32; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
127
+ Generation complete
128
+ </span>
129
+ </div>
130
+
131
+ <style>
132
+ @keyframes fadeInOut {
133
+ 0% { opacity: 0; transform: translateY(10px); }
134
+ 15% { opacity: 1; transform: translateY(0); }
135
+ 85% { opacity: 1; transform: translateY(0); }
136
+ 100% { opacity: 0; transform: translateY(-10px); }
137
+ }
138
+ </style>
139
+ """
140
+
141
+ ERROR_HTML = """\
142
+ <div style="display: flex; align-items: center; gap: 8px; padding: 12px; background-color: #ffebee; border-radius: 6px; border-left: 3px solid #f44336; margin: 8px 0;">
143
+ <div style="width: 20px; height: 20px; background-color: #f44336; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 12px;">
144
+ !
145
+ </div>
146
+ <div style="color: #c62828; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
147
+ <strong>Error:</strong> {}
148
+ </div>
149
+ </div>"""
150
+
151
+ STOPPED_SANDBOX_HTML = """
152
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #f5f5f5; border-radius: 6px; border-left: 3px solid #9e9e9e; margin-bottom: 16px;">
153
+ <div style="width: 16px; height: 16px; background-color: #9e9e9e; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 10px;">
154
+
155
+ </div>
156
+ <div style="flex: 1;">
157
+ <div style="margin-bottom: 4px; font-size: 13px; color: #757575; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-weight: 500;">
158
+ Sandbox stopped
159
+ </div>
160
+ <div style="width: 100%; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
161
+ <div style="height: 100%; background-color: #9e9e9e; border-radius: 4px; width: 100%;"></div>
162
+ </div>
163
+ <div style="display: flex; justify-content: space-between; margin-top: 4px; font-size: 11px; color: #757575; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
164
+ <span>Started: {start_time}</span>
165
+ <span>Expired: {end_time}</span>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ """
170
+
171
+ TIMEOUT_HTML = """
172
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #fff3e0; border-radius: 6px; border-left: 3px solid #ff9800; margin-bottom: 16px;">
173
+ <div style="width: 16px; height: 16px; background-color: #ff9800; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 10px;">
174
+
175
+ </div>
176
+ <div style="flex: 1;">
177
+ <div style="margin-bottom: 4px; font-size: 13px; color: #f57c00; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-weight: 500;">
178
+ The E2B Sandbox for code execution has a timeout of {total_seconds} seconds.
179
+ </div>
180
+ <div style="width: 100%; height: 8px; background-color: #ffe0b3; border-radius: 4px; overflow: hidden;">
181
+ <div id="progress-bar-{unique_id}" style="height: 100%; background: linear-gradient(90deg, #ff9800 0%, #f57c00 50%, #f44336 100%); border-radius: 4px; width: {current_progress}%; animation: progress-fill-{unique_id} {remaining_seconds}s linear forwards;"></div>
182
+ </div>
183
+ <div style="display: flex; justify-content: space-between; margin-top: 4px; font-size: 11px; color: #f57c00; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
184
+ <span>Started: {start_time}</span>
185
+ <span>Expires: {end_time}</span>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <style>
191
+ @keyframes progress-fill-{unique_id} {{
192
+ from {{ width: {current_progress}%; }}
193
+ to {{ width: 100%; }}
194
+ }}
195
+ </style>
196
+ """
197
+
198
+ # just make the code font a bit smaller
199
+ custom_css = """
200
+ <style type="text/css">
201
+ /* Code font size */
202
+ .highlight pre, .highlight code,
203
+ div.input_area pre, div.output_area pre {
204
+ font-size: 12px !important;
205
+ line-height: 1.4 !important;
206
+ }
207
+
208
+ /* Fix prompt truncation */
209
+ .jp-InputPrompt, .jp-OutputPrompt {
210
+ text-overflow: clip !important;
211
+ }
212
+ </style>
213
+ """
214
+
215
+ # Configure the exporter
216
+ config = Config()
217
+ html_exporter = HTMLExporter(config=config, template_name="classic")
218
+
219
+
220
+ class JupyterNotebook:
221
+ def __init__(self, messages=None):
222
+ self.exec_count = 0
223
+ self.countdown_info = None
224
+ if messages is None:
225
+ messages = []
226
+ self.data, self.code_cell_counter = self.create_base_notebook(messages)
227
+
228
+
229
+ def create_base_notebook(self, messages):
230
+ base_notebook = {
231
+ "metadata": {
232
+ "kernel_info": {"name": "python3"},
233
+ "language_info": {
234
+ "name": "python",
235
+ "version": "3.12",
236
+ },
237
+ },
238
+ "nbformat": 4,
239
+ "nbformat_minor": 0,
240
+ "cells": []
241
+ }
242
+
243
+ # Add header
244
+ base_notebook["cells"].append({
245
+ "cell_type": "markdown",
246
+ "metadata": {},
247
+ "source": header_message
248
+ })
249
+
250
+ # Set initial data
251
+ self.data = base_notebook
252
+
253
+ # Add empty code cell if no messages
254
+ if len(messages) == 0:
255
+ self.data["cells"].append({
256
+ "cell_type": "code",
257
+ "execution_count": None,
258
+ "metadata": {},
259
+ "source": "",
260
+ "outputs": []
261
+ })
262
+ return self.data, 0
263
+
264
+ # Process messages using existing methods
265
+ i = 0
266
+ while i < len(messages):
267
+ message = messages[i]
268
+
269
+ if message["role"] == "system":
270
+ self.add_markdown(message["content"], "system")
271
+
272
+ elif message["role"] == "user":
273
+ self.add_markdown(message["content"], "user")
274
+
275
+ elif message["role"] == "assistant":
276
+ if "tool_calls" in message:
277
+ # Add assistant thinking if there's content
278
+ if message.get("content"):
279
+ self.add_markdown(message["content"], "assistant")
280
+
281
+ # Process tool calls - we know the next message(s) will be tool responses
282
+ for tool_call in message["tool_calls"]:
283
+ if tool_call["function"]["name"] == "add_and_execute_jupyter_code_cell":
284
+ tool_args = json.loads(tool_call["function"]["arguments"])
285
+ code = tool_args["code"]
286
+
287
+ # Get the next tool response (guaranteed to exist)
288
+ tool_message = messages[i + 1]
289
+ if tool_message["role"] == "tool" and tool_message.get("tool_call_id") == tool_call["id"]:
290
+ # Use the raw execution directly!
291
+ execution = tool_message["raw_execution"]
292
+ self.add_code_execution(code, execution, parsed=True)
293
+ i += 1 # Skip the tool message since we just processed it
294
+ else:
295
+ # Regular assistant message
296
+ self.add_markdown(message["content"], "assistant")
297
+
298
+ elif message["role"] == "tool":
299
+ # Skip - should have been handled with corresponding tool_calls
300
+ # This shouldn't happen given our assumptions, but just in case
301
+ pass
302
+
303
+ i += 1
304
+
305
+ return self.data, 0
306
+
307
+ def _update_countdown_cell(self):
308
+ if not self.countdown_info:
309
+ return
310
+
311
+ start_time = self.countdown_info['start_time']
312
+ end_time = self.countdown_info['end_time']
313
+
314
+ current_time = datetime.datetime.now(datetime.timezone.utc)
315
+ remaining_time = end_time - current_time
316
+
317
+ # Show stopped message if expired
318
+ if remaining_time.total_seconds() <= 0:
319
+ # Format display for stopped sandbox
320
+ start_display = start_time.strftime("%H:%M")
321
+ end_display = end_time.strftime("%H:%M")
322
+
323
+ stopped_html = STOPPED_SANDBOX_HTML.format(
324
+ start_time=start_display,
325
+ end_time=end_display
326
+ )
327
+
328
+ # Update countdown cell to show stopped message
329
+ stopped_cell = {
330
+ "cell_type": "markdown",
331
+ "metadata": {},
332
+ "source": stopped_html
333
+ }
334
+
335
+ # Find and update existing countdown cell
336
+ for i, cell in enumerate(self.data["cells"]):
337
+ if cell.get("cell_type") == "markdown" and ("⏱" in str(cell.get("source", "")) or "⏹" in str(cell.get("source", ""))):
338
+ self.data["cells"][i] = stopped_cell
339
+ break
340
+
341
+ return
342
+
343
+ # Calculate current progress
344
+ total_duration = end_time - start_time
345
+ elapsed_time = current_time - start_time
346
+ current_progress = (elapsed_time.total_seconds() / total_duration.total_seconds()) * 100
347
+ current_progress = max(0, min(100, current_progress))
348
+
349
+ # Format display
350
+ start_display = start_time.strftime("%H:%M")
351
+ end_display = end_time.strftime("%H:%M")
352
+ remaining_seconds = int(remaining_time.total_seconds())
353
+ remaining_minutes = remaining_seconds // 60
354
+ remaining_secs = remaining_seconds % 60
355
+ remaining_display = f"{remaining_minutes}:{remaining_secs:02d}"
356
+
357
+ # Generate unique ID to avoid CSS conflicts when updating
358
+ unique_id = int(current_time.timestamp() * 1000) % 100000
359
+
360
+ # Calculate total timeout duration in seconds
361
+ total_seconds = int(total_duration.total_seconds())
362
+
363
+ countdown_html = TIMEOUT_HTML.format(
364
+ start_time=start_display,
365
+ end_time=end_display,
366
+ current_progress=current_progress,
367
+ remaining_seconds=remaining_seconds,
368
+ unique_id=unique_id,
369
+ total_seconds=total_seconds
370
+ )
371
+
372
+ # Update or insert the countdown cell
373
+ countdown_cell = {
374
+ "cell_type": "markdown",
375
+ "metadata": {},
376
+ "source": countdown_html
377
+ }
378
+
379
+ # Find existing countdown cell by looking for the timer emoji
380
+ found_countdown = False
381
+ for i, cell in enumerate(self.data["cells"]):
382
+ if cell.get("cell_type") == "markdown" and "⏱" in str(cell.get("source", "")):
383
+ # Update existing countdown cell
384
+ self.data["cells"][i] = countdown_cell
385
+ found_countdown = True
386
+ break
387
+
388
+ if not found_countdown:
389
+ # Insert new countdown cell at position 1 (after header)
390
+ self.data["cells"].insert(1, countdown_cell)
391
+
392
+ def add_sandbox_countdown(self, start_time, end_time):
393
+ # Store the countdown info for later updates
394
+ self.countdown_info = {
395
+ 'start_time': start_time,
396
+ 'end_time': end_time,
397
+ 'cell_index': 1 # Remember where we put it
398
+ }
399
+
400
+ def add_code_execution(self, code, execution, parsed=False):
401
+ self.exec_count += 1
402
+ self.data["cells"].append({
403
+ "cell_type": "code",
404
+ "execution_count": self.exec_count,
405
+ "metadata": {},
406
+ "source": code,
407
+ "outputs": execution if parsed else self.parse_exec_result_nb(execution)
408
+ })
409
+
410
+ def add_code(self, code):
411
+ """Add a code cell without execution results"""
412
+ self.exec_count += 1
413
+ self.data["cells"].append({
414
+ "cell_type": "code",
415
+ "execution_count": self.exec_count,
416
+ "metadata": {},
417
+ "source": code,
418
+ "outputs": []
419
+ })
420
+
421
+ def append_execution(self, execution):
422
+ """Append execution results to the immediate previous cell if it's a code cell"""
423
+ if (len(self.data["cells"]) > 0 and
424
+ self.data["cells"][-1]["cell_type"] == "code"):
425
+ self.data["cells"][-1]["outputs"] = self.parse_exec_result_nb(execution)
426
+ else:
427
+ raise ValueError("Cannot append execution: previous cell is not a code cell")
428
+
429
+ def add_markdown(self, markdown, role="markdown"):
430
+ if role == "system":
431
+ system_message = markdown if markdown else "default"
432
+ markdown_formatted = system_template.format(system_message.replace('\n', '<br>'))
433
+ elif role == "user":
434
+ markdown_formatted = user_template.format(markdown.replace('\n', '<br>'))
435
+ elif role == "assistant":
436
+ markdown_formatted = assistant_thinking_template.format(markdown)
437
+ markdown_formatted = markdown_formatted.replace('<think>', '&lt;think&gt;')
438
+ markdown_formatted = markdown_formatted.replace('</think>', '&lt;/think&gt;')
439
+ else:
440
+ # Default case for raw markdown
441
+ markdown_formatted = markdown
442
+
443
+ self.data["cells"].append({
444
+ "cell_type": "markdown",
445
+ "metadata": {},
446
+ "source": markdown_formatted
447
+ })
448
+
449
+ def add_error(self, error_message):
450
+ """Add an error message cell to the notebook"""
451
+ error_html = ERROR_HTML.format(error_message)
452
+
453
+ self.data["cells"].append({
454
+ "cell_type": "markdown",
455
+ "metadata": {},
456
+ "source": error_html
457
+ })
458
+
459
+ def add_final_answer(self, answer):
460
+ self.data["cells"].append({
461
+ "cell_type": "markdown",
462
+ "metadata": {},
463
+ "source": assistant_final_answer_template.format(answer)
464
+ })
465
+
466
+ def parse_exec_result_nb(self, execution):
467
+ """Convert an E2B Execution object to Jupyter notebook cell output format"""
468
+ outputs = []
469
+
470
+ if execution.logs.stdout:
471
+ outputs.append({
472
+ 'output_type': 'stream',
473
+ 'name': 'stdout',
474
+ 'text': ''.join(execution.logs.stdout)
475
+ })
476
+
477
+ if execution.logs.stderr:
478
+ outputs.append({
479
+ 'output_type': 'stream',
480
+ 'name': 'stderr',
481
+ 'text': ''.join(execution.logs.stderr)
482
+ })
483
+
484
+ if execution.error:
485
+ outputs.append({
486
+ 'output_type': 'error',
487
+ 'ename': execution.error.name,
488
+ 'evalue': execution.error.value,
489
+ 'traceback': [line for line in execution.error.traceback.split('\n')]
490
+ })
491
+
492
+ for result in execution.results:
493
+ output = {
494
+ 'output_type': 'execute_result' if result.is_main_result else 'display_data',
495
+ 'metadata': {},
496
+ 'data': {}
497
+ }
498
+
499
+ if result.text:
500
+ output['data']['text/plain'] = result.text
501
+ if result.html:
502
+ output['data']['text/html'] = result.html
503
+ if result.png:
504
+ output['data']['image/png'] = result.png
505
+ if result.svg:
506
+ output['data']['image/svg+xml'] = result.svg
507
+ if result.jpeg:
508
+ output['data']['image/jpeg'] = result.jpeg
509
+ if result.pdf:
510
+ output['data']['application/pdf'] = result.pdf
511
+ if result.latex:
512
+ output['data']['text/latex'] = result.latex
513
+ if result.json:
514
+ output['data']['application/json'] = result.json
515
+ if result.javascript:
516
+ output['data']['application/javascript'] = result.javascript
517
+
518
+ if result.is_main_result and execution.execution_count is not None:
519
+ output['execution_count'] = execution.execution_count
520
+
521
+ if output['data']:
522
+ outputs.append(output)
523
+
524
+ return outputs
525
+
526
+ def filter_base64_images(self, message):
527
+ """Filter out base64 encoded images from message content"""
528
+ if isinstance(message, dict) and 'nbformat' in message:
529
+ for output in message['nbformat']:
530
+ if 'data' in output:
531
+ for key in list(output['data'].keys()):
532
+ if key.startswith('image/') or key == 'application/pdf':
533
+ output['data'][key] = '<placeholder_image>'
534
+ return message
535
+
536
+ def render(self, mode="default"):
537
+ if self.countdown_info is not None:
538
+ self._update_countdown_cell()
539
+
540
+ render_data = copy.deepcopy(self.data)
541
+
542
+ if mode == "generating":
543
+ render_data["cells"].append({
544
+ "cell_type": "markdown",
545
+ "metadata": {},
546
+ "source": GENERATING_WIDGET
547
+ })
548
+
549
+ elif mode == "executing":
550
+ render_data["cells"].append({
551
+ "cell_type": "markdown",
552
+ "metadata": {},
553
+ "source": EXECUTING_WIDGET
554
+ })
555
+
556
+ elif mode == "done":
557
+ render_data["cells"].append({
558
+ "cell_type": "markdown",
559
+ "metadata": {},
560
+ "source": DONE_WIDGET
561
+ })
562
+ elif mode != "default":
563
+ raise ValueError(f"Render mode should be generating, executing or done. Given: {mode}.")
564
+
565
+ notebook = nbformat.from_dict(render_data)
566
+ notebook_body, _ = html_exporter.from_notebook_node(notebook)
567
+ notebook_body = notebook_body.replace(bad_html_bad, "")
568
+
569
+ # make code font a bit smaller with custom css
570
+ if "<head>" in notebook_body:
571
+ notebook_body = notebook_body.replace("</head>", f"{custom_css}</head>")
572
+ return notebook_body
utils.py CHANGED
@@ -5,190 +5,51 @@ from huggingface_hub import InferenceClient
5
  from e2b_code_interpreter import Sandbox
6
  from transformers import AutoTokenizer
7
  from traitlets.config import Config
8
-
9
- config = Config()
10
- html_exporter = HTMLExporter(config=config, template_name="classic")
11
-
12
-
13
- with open("llama3_template.jinja", "r") as f:
14
- llama_template = f.read()
15
-
16
-
17
- MAX_TURNS = 4
18
-
19
-
20
- def parse_exec_result_nb(execution):
21
- """Convert an E2B Execution object to Jupyter notebook cell output format"""
22
- outputs = []
23
-
24
- if execution.logs.stdout:
25
- outputs.append({
26
- 'output_type': 'stream',
27
- 'name': 'stdout',
28
- 'text': ''.join(execution.logs.stdout)
29
- })
30
-
31
- if execution.logs.stderr:
32
- outputs.append({
33
- 'output_type': 'stream',
34
- 'name': 'stderr',
35
- 'text': ''.join(execution.logs.stderr)
36
- })
37
-
38
- if execution.error:
39
- outputs.append({
40
- 'output_type': 'error',
41
- 'ename': execution.error.name,
42
- 'evalue': execution.error.value,
43
- 'traceback': [line for line in execution.error.traceback.split('\n')]
44
- })
45
-
46
- for result in execution.results:
47
- output = {
48
- 'output_type': 'execute_result' if result.is_main_result else 'display_data',
49
- 'metadata': {},
50
- 'data': {}
51
  }
52
-
53
- if result.text:
54
- output['data']['text/plain'] = [result.text] # Array for text/plain
55
- if result.html:
56
- output['data']['text/html'] = result.html
57
- if result.png:
58
- output['data']['image/png'] = result.png
59
- if result.svg:
60
- output['data']['image/svg+xml'] = result.svg
61
- if result.jpeg:
62
- output['data']['image/jpeg'] = result.jpeg
63
- if result.pdf:
64
- output['data']['application/pdf'] = result.pdf
65
- if result.latex:
66
- output['data']['text/latex'] = result.latex
67
- if result.json:
68
- output['data']['application/json'] = result.json
69
- if result.javascript:
70
- output['data']['application/javascript'] = result.javascript
71
-
72
- if result.is_main_result and execution.execution_count is not None:
73
- output['execution_count'] = execution.execution_count
74
-
75
- if output['data']:
76
- outputs.append(output)
77
-
78
- return outputs
79
-
80
-
81
- system_template = """\
82
- <details>
83
- <summary style="display: flex; align-items: center;">
84
- <div class="alert alert-block alert-info" style="margin: 0; width: 100%;">
85
- <b>System: <span class="arrow">▶</span></b>
86
- </div>
87
- </summary>
88
- <div class="alert alert-block alert-info">
89
- {}
90
- </div>
91
- </details>
92
-
93
- <style>
94
- details > summary .arrow {{
95
- display: inline-block;
96
- transition: transform 0.2s;
97
- }}
98
- details[open] > summary .arrow {{
99
- transform: rotate(90deg);
100
- }}
101
- </style>
102
- """
103
-
104
- user_template = """<div class="alert alert-block alert-success">
105
- <b>User:</b> {}
106
- </div>
107
- """
108
-
109
- header_message = """<p align="center">
110
- <img src="https://huggingface.co/spaces/lvwerra/jupyter-agent/resolve/main/jupyter-agent.png" />
111
- </p>
112
-
113
-
114
- <p style="text-align:center;">Let a LLM agent write and execute code inside a notebook!</p>"""
115
-
116
- bad_html_bad = """input[type="file"] {
117
- display: block;
118
- }"""
119
-
120
-
121
- def create_base_notebook(messages):
122
- base_notebook = {
123
- "metadata": {
124
- "kernel_info": {"name": "python3"},
125
- "language_info": {
126
- "name": "python",
127
- "version": "3.12",
128
- },
129
- },
130
- "nbformat": 4,
131
- "nbformat_minor": 0,
132
- "cells": []
133
  }
134
- base_notebook["cells"].append({
135
- "cell_type": "markdown",
136
- "metadata": {},
137
- "source": header_message
138
- })
139
 
140
- if len(messages)==0:
141
- base_notebook["cells"].append({
142
- "cell_type": "code",
143
- "execution_count": None,
144
- "metadata": {},
145
- "source": "",
146
- "outputs": []
147
- })
148
 
149
- code_cell_counter = 0
150
-
151
- for message in messages:
152
- if message["role"] == "system":
153
- text = system_template.format(message["content"].replace('\n', '<br>'))
154
- base_notebook["cells"].append({
155
- "cell_type": "markdown",
156
- "metadata": {},
157
- "source": text
158
- })
159
- elif message["role"] == "user":
160
- text = user_template.format(message["content"].replace('\n', '<br>'))
161
- base_notebook["cells"].append({
162
- "cell_type": "markdown",
163
- "metadata": {},
164
- "source": text
165
- })
166
 
167
- elif message["role"] == "assistant" and "tool_calls" in message:
168
- base_notebook["cells"].append({
169
- "cell_type": "code",
170
- "execution_count": None,
171
- "metadata": {},
172
- "source": message["content"],
173
- "outputs": []
174
- })
175
-
176
- elif message["role"] == "ipython":
177
- code_cell_counter +=1
178
- base_notebook["cells"][-1]["outputs"] = message["nbformat"]
179
- base_notebook["cells"][-1]["execution_count"] = code_cell_counter
180
-
181
- elif message["role"] == "assistant" and "tool_calls" not in message:
182
- base_notebook["cells"].append({
183
- "cell_type": "markdown",
184
- "metadata": {},
185
- "source": message["content"]
186
- })
187
-
188
- else:
189
- raise ValueError(message)
190
-
191
- return base_notebook, code_cell_counter
192
 
193
  def execute_code(sbx, code):
194
  execution = sbx.run_code(code, on_stdout=lambda data: print('stdout:', data))
@@ -202,119 +63,112 @@ def execute_code(sbx, code):
202
  return output, execution
203
 
204
 
205
- def parse_exec_result_llm(execution):
206
- output = ""
207
- if len(execution.logs.stdout) > 0:
208
- output += "\n".join(execution.logs.stdout)
209
- if len(execution.logs.stderr) > 0:
210
- output += "\n".join(execution.logs.stderr)
 
 
 
 
 
 
 
 
211
  if execution.error is not None:
212
- output += execution.error.traceback
213
- return output
214
-
215
-
216
- def update_notebook_display(notebook_data):
217
- notebook = nbformat.from_dict(notebook_data)
218
- notebook_body, _ = html_exporter.from_notebook_node(notebook)
219
- notebook_body = notebook_body.replace(bad_html_bad, "")
220
- return notebook_body
221
 
222
  def run_interactive_notebook(client, model, tokenizer, messages, sbx, max_new_tokens=512):
223
- notebook_data, code_cell_counter = create_base_notebook(messages)
 
 
 
 
 
224
  turns = 0
 
225
 
226
- #code_cell_counter = 0
227
- while turns <= MAX_TURNS:
228
- turns += 1
229
- input_tokens = tokenizer.apply_chat_template(
230
- messages,
231
- chat_template=llama_template,
232
- builtin_tools=["code_interpreter"],
233
- add_generation_prompt=True
234
- )
235
- model_input = tokenizer.decode(input_tokens)
236
 
237
- print(f"Model input:\n{model_input}\n{'='*80}")
238
-
239
- response_stream = client.text_generation(
240
- model=model,
241
- prompt=model_input,
242
- details=True,
243
- stream=True,
244
- do_sample=True,
245
- repetition_penalty=1.1,
246
- temperature=0.8,
247
- max_new_tokens=max_new_tokens,
248
- )
249
-
250
- assistant_response = ""
251
- tokens = []
252
 
253
- code_cell = False
254
- for i, chunk in enumerate(response_stream):
255
- if not chunk.token.special:
256
- content = chunk.token.text
257
- else:
258
- content = ""
259
- tokens.append(chunk.token.text)
260
- assistant_response += content
261
-
262
- if len(tokens)==1:
263
- create_cell=True
264
- code_cell = "<|python_tag|>" in tokens[0]
265
- if code_cell:
266
- code_cell_counter +=1
267
- else:
268
- create_cell = False
269
 
270
- # Update notebook in real-time
271
- if create_cell:
272
- if "<|python_tag|>" in tokens[0]:
273
- notebook_data["cells"].append({
274
- "cell_type": "code",
275
- "execution_count": None,
276
- "metadata": {},
277
- "source": assistant_response,
278
- "outputs": []
279
- })
280
- else:
281
- notebook_data["cells"].append({
282
- "cell_type": "markdown",
283
- "metadata": {},
284
- "source": assistant_response
285
- })
286
- else:
287
- notebook_data["cells"][-1]["source"] = assistant_response
288
- if i%16 == 0:
289
- yield update_notebook_display(notebook_data), notebook_data, messages
290
- yield update_notebook_display(notebook_data), notebook_data, messages
291
-
292
-
293
- # Handle code execution
294
- if code_cell:
295
- notebook_data["cells"][-1]["execution_count"] = code_cell_counter
296
-
 
 
 
 
 
 
297
 
298
- exec_result, execution = execute_code(sbx, assistant_response)
299
- messages.append({
300
- "role": "assistant",
301
- "content": assistant_response,
302
- "tool_calls": [{
303
- "type": "function",
304
- "function": {
305
- "name": "code_interpreter",
306
- "arguments": {"code": assistant_response}
307
- }
308
- }]
309
- })
310
- messages.append({"role": "ipython", "content": parse_exec_result_llm(execution), "nbformat": parse_exec_result_nb(execution)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
- # Update the last code cell with execution results
313
- notebook_data["cells"][-1]["outputs"] = parse_exec_result_nb(execution)
314
- update_notebook_display(notebook_data)
315
  else:
316
- messages.append({"role": "assistant", "content": assistant_response})
317
- if tokens[-1] == "<|eot_id|>":
318
- break
319
-
320
- yield update_notebook_display(notebook_data), notebook_data, messages
 
5
  from e2b_code_interpreter import Sandbox
6
  from transformers import AutoTokenizer
7
  from traitlets.config import Config
8
+ from jupyter_handler import JupyterNotebook
9
+ import json
10
+
11
+
12
+ TOOLS = [
13
+ {
14
+ "type": "function",
15
+ "function": {
16
+ "name": "add_and_execute_jupyter_code_cell",
17
+ "description": "A Python code execution environment that runs code in a Jupyter notebook interface. This is stateful - variables and imports persist between executions.",
18
+ "parameters": {
19
+ "type": "object",
20
+ "properties": {
21
+ "code": {
22
+ "type": "string",
23
+ "description": "The Python code to execute."
24
+ }
25
+ },
26
+ "required": ["code"]
27
+ }
28
+ }
29
+ },
30
+ {
31
+ "type": "function",
32
+ "function": {
33
+ "name": "final_answer",
34
+ "description": "Provide the final answer to the user's question after completing all necessary analysis and computation.",
35
+ "parameters": {
36
+ "type": "object",
37
+ "properties": {
38
+ "answer": {
39
+ "type": "string",
40
+ "description": "The complete final answer to the user's question"
41
+ },
42
+ },
43
+ "required": ["answer"]
44
+ }
 
 
 
 
 
 
45
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
+ ]
 
 
 
 
48
 
49
+ TOOLS = TOOLS[:1]
 
 
 
 
 
 
 
50
 
51
+ MAX_TURNS = 40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  def execute_code(sbx, code):
55
  execution = sbx.run_code(code, on_stdout=lambda data: print('stdout:', data))
 
63
  return output, execution
64
 
65
 
66
+ def parse_exec_result_llm(execution, max_code_output=1000):
67
+ output = []
68
+
69
+ def truncate_if_needed(text):
70
+ if len(text) > max_code_output:
71
+ return (text[:max_code_output] + f"\n[Output is truncated as it is more than {max_code_output} characters]")
72
+ return text
73
+
74
+ if execution.results:
75
+ output.append(truncate_if_needed("\n".join([result.text for result in execution.results])))
76
+ if execution.logs.stdout:
77
+ output.append(truncate_if_needed("\n".join(execution.logs.stdout)))
78
+ if execution.logs.stderr:
79
+ output.append(truncate_if_needed("\n".join(execution.logs.stderr)))
80
  if execution.error is not None:
81
+ output.append(truncate_if_needed(execution.error.traceback))
82
+ return "\n".join(output)
83
+
 
 
 
 
 
 
84
 
85
  def run_interactive_notebook(client, model, tokenizer, messages, sbx, max_new_tokens=512):
86
+ notebook = JupyterNotebook(messages)
87
+ sbx_info = sbx.get_info()
88
+ notebook.add_sandbox_countdown(sbx_info.started_at, sbx_info.end_at)
89
+ yield notebook.render(mode="generating"), notebook.data, messages
90
+
91
+ max_code_output = 1000
92
  turns = 0
93
+ done = False
94
 
95
+ print("SBX INFO", sbx.get_info())
 
 
 
 
 
 
 
 
 
96
 
97
+ while not done and (turns <= MAX_TURNS):
98
+ turns += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ try:
101
+ # Inference client call - might fail
102
+ response = client.chat.completions.create(
103
+ messages=messages,
104
+ model=model,
105
+ tools=TOOLS,
106
+ tool_choice="auto",
107
+ )
 
 
 
 
 
 
 
 
108
 
109
+ except Exception as e:
110
+ # Handle inference client errors
111
+ notebook.add_error(f"Inference failed: {str(e)}")
112
+ return notebook.render(), notebook.data, messages
113
+
114
+ # Get the response content and tool calls
115
+ full_response = response.choices[0].message.content or ""
116
+ tool_calls = response.choices[0].message.tool_calls or []
117
+
118
+ # Add markdown cell for assistant's thinking
119
+ notebook.add_markdown(full_response, "assistant")
120
+
121
+ # Handle tool calls
122
+ for tool_call in tool_calls:
123
+ messages.append(
124
+ {
125
+ "role": "assistant",
126
+ "content": full_response,
127
+ "tool_calls": [
128
+ {
129
+ "id": tool_call.id,
130
+ "type": "function",
131
+ "function": {
132
+ "name": tool_call.function.name,
133
+ "arguments": tool_call.function.arguments,
134
+ },
135
+ }
136
+ ],
137
+ }
138
+ )
139
+
140
+ if tool_call.function.name == "add_and_execute_jupyter_code_cell":
141
+ tool_args = json.loads(tool_call.function.arguments)
142
 
143
+ notebook.add_code(tool_args["code"])
144
+ yield notebook.render(mode="executing"), notebook.data, messages
145
+
146
+ try:
147
+ # Execution sandbox call - might timeout
148
+ execution = sbx.run_code(tool_args["code"])
149
+ notebook.append_execution(execution)
150
+
151
+ except Exception as e:
152
+ # Handle sandbox timeout/execution errors
153
+ notebook.add_error(f"Code execution failed: {str(e)}")
154
+ return notebook.render(), notebook.data, messages
155
+
156
+ messages.append(
157
+ {
158
+ "role": "tool",
159
+ "tool_call_id": tool_call.id,
160
+ "content": parse_exec_result_llm(execution, max_code_output=max_code_output),
161
+ "raw_execution": notebook.parse_exec_result_nb(execution)
162
+ }
163
+ )
164
+
165
+ if not tool_calls:
166
+ if len(full_response.strip())==0:
167
+ notebook.add_error(f"No tool call and empty assistant response:\n{response.model_dump_json(indent=2)}")
168
+ messages.append({"role": "assistant", "content": full_response})
169
+ done = True
170
 
171
+ if done:
172
+ yield notebook.render(mode="done"), notebook.data, messages
 
173
  else:
174
+ yield notebook.render(mode="generating"), notebook.data, messages