M-Rique commited on
Commit
8b6a4c9
·
1 Parent(s): 0afc4f1

Add streaming, more robust logs

Browse files
Files changed (4) hide show
  1. app.py +105 -354
  2. e2bqwen.py +3 -3
  3. gradio_script.py +237 -0
  4. scripts_and_styling.py +307 -0
app.py CHANGED
@@ -1,10 +1,12 @@
1
  import json
2
  import os
3
  import shutil
 
4
  import time
5
  import uuid
6
  from io import BytesIO
7
  from threading import Timer
 
8
 
9
  import gradio as gr
10
  from dotenv import load_dotenv
@@ -13,9 +15,17 @@ from gradio_modal import Modal
13
  from huggingface_hub import login, upload_folder
14
  from PIL import Image
15
  from smolagents import CodeAgent, InferenceClientModel
16
- from smolagents.gradio_ui import GradioUI, stream_to_gradio
17
 
18
  from e2bqwen import E2BVisionAgent, get_agent_summary_erase_images
 
 
 
 
 
 
 
 
19
 
20
  load_dotenv(override=True)
21
 
@@ -28,11 +38,11 @@ EXAMPLES = [
28
  ]
29
 
30
  E2B_API_KEY = os.getenv("E2B_API_KEY")
31
- SANDBOXES = {}
32
- SANDBOX_METADATA = {}
33
  SANDBOX_TIMEOUT = 300
34
- WIDTH = 1024
35
- HEIGHT = 768
36
  TMP_DIR = "./tmp/"
37
  if not os.path.exists(TMP_DIR):
38
  os.makedirs(TMP_DIR)
@@ -40,298 +50,42 @@ if not os.path.exists(TMP_DIR):
40
  hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_API_KEY")
41
  login(token=hf_token)
42
 
43
- custom_css = """
44
- .modal-container {
45
- margin: var(--size-16) auto!important;
46
- }
47
-
48
- .sandbox-container {
49
- position: relative;
50
- width: 910px;
51
- overflow: hidden;
52
- margin: auto;
53
- }
54
- .sandbox-container {
55
- height: 800px;
56
- }
57
- .sandbox-frame {
58
- display: none;
59
- position: absolute;
60
- top: 0;
61
- left: 0;
62
- width: 910px;
63
- height: 800px;
64
- pointer-events:none;
65
- }
66
-
67
- .sandbox-iframe, .bsod-image {
68
- position: absolute;
69
- width: <<WIDTH>>px;
70
- height: <<HEIGHT>>px;
71
- border: 4px solid #444444;
72
- transform-origin: 0 0;
73
- }
74
-
75
- /* Colored label for task textbox */
76
- .primary-color-label label span {
77
- font-weight: bold;
78
- color: var(--color-accent);
79
- }
80
-
81
- /* Status indicator light */
82
- .status-bar {
83
- display: flex;
84
- flex-direction: row;
85
- align-items: center;
86
- flex-align:center;
87
- z-index: 100;
88
- }
89
-
90
- .status-indicator {
91
- width: 15px;
92
- height: 15px;
93
- border-radius: 50%;
94
- }
95
-
96
- .status-text {
97
- font-size: 16px;
98
- font-weight: bold;
99
- padding-left: 8px;
100
- text-shadow: none;
101
- }
102
-
103
- .status-interactive {
104
- background-color: #2ecc71;
105
- animation: blink 2s infinite;
106
- }
107
-
108
- .status-view-only {
109
- background-color: #e74c3c;
110
- }
111
-
112
- .status-error {
113
- background-color: #e74c3c;
114
- animation: blink-error 1s infinite;
115
- }
116
-
117
- @keyframes blink-error {
118
- 0% { background-color: rgba(231, 76, 60, 1); }
119
- 50% { background-color: rgba(231, 76, 60, 0.4); }
120
- 100% { background-color: rgba(231, 76, 60, 1); }
121
- }
122
-
123
- @keyframes blink {
124
- 0% { background-color: rgba(46, 204, 113, 1); } /* Green at full opacity */
125
- 50% { background-color: rgba(46, 204, 113, 0.4); } /* Green at 40% opacity */
126
- 100% { background-color: rgba(46, 204, 113, 1); } /* Green at full opacity */
127
- }
128
-
129
- #chatbot {
130
- height:1000px!important;
131
- }
132
- #chatbot .role {
133
- max-width:95%
134
- }
135
-
136
- #chatbot .bubble-wrap {
137
- overflow-y: visible;
138
- }
139
-
140
- .logo-container {
141
- display: flex;
142
- flex-direction: column;
143
- align-items: flex-start;
144
- width: 100%;
145
- box-sizing: border-box;
146
- gap: 5px;
147
-
148
- .logo-item {
149
- display: flex;
150
- align-items: center;
151
- padding: 0 30px;
152
- gap: 10px;
153
- text-decoration: none!important;
154
- color: #f59e0b;
155
- font-size:17px;
156
- }
157
- .logo-item:hover {
158
- color: #935f06!important;
159
- }
160
- """.replace("<<WIDTH>>", str(WIDTH + 15)).replace("<<HEIGHT>>", str(HEIGHT + 10))
161
-
162
- footer_html = """
163
- <h3 style="text-align: center; margin-top:50px;"><i>Powered by open source:</i></h2>
164
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
165
- <div class="logo-container">
166
- <a class="logo-item" href="https://github.com/huggingface/smolagents"><i class="fa fa-github"></i>smolagents</a>
167
- <a class="logo-item" href="https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct"><i class="fa fa-github"></i>Qwen2-VL-72B</a>
168
- <a class="logo-item" href="https://github.com/e2b-dev/desktop"><i class="fa fa-github"></i>E2B Desktop</a>
169
- </div>
170
- """
171
- sandbox_html_template = """
172
- <style>
173
- @import url('https://fonts.googleapis.com/css2?family=Oxanium:[email protected]&display=swap');
174
- </style>
175
- <h1 style="color:var(--color-accent);margin:0;">Open Computer Agent - <i>Powered by <a href="https://github.com/huggingface/smolagents">smolagents</a></i><h1>
176
- <div class="sandbox-container" style="margin:0;">
177
- <div class="status-bar">
178
- <div class="status-indicator {status_class}"></div>
179
- <div class="status-text">{status_text}</div>
180
- </div>
181
- <iframe id="sandbox-iframe"
182
- src="{stream_url}"
183
- class="sandbox-iframe"
184
- style="display: block;"
185
- allowfullscreen>
186
- </iframe>
187
- <img src="https://huggingface.co/datasets/mfarre/servedfiles/resolve/main/blue_screen_of_death.gif" class="bsod-image" style="display: none;"/>
188
- <img src="https://huggingface.co/datasets/m-ric/images/resolve/main/HUD_thom.png" class="sandbox-frame" />
189
- </div>
190
- """.replace("<<WIDTH>>", str(WIDTH + 15)).replace("<<HEIGHT>>", str(HEIGHT + 10))
191
-
192
- custom_js = """function() {
193
- document.body.classList.add('dark');
194
-
195
- // Function to check if sandbox is timing out
196
- const checkSandboxTimeout = function() {
197
- const timeElement = document.getElementById('sandbox-creation-time');
198
-
199
- if (timeElement) {
200
- const creationTime = parseFloat(timeElement.getAttribute('data-time'));
201
- const timeoutValue = parseFloat(timeElement.getAttribute('data-timeout'));
202
- const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
203
-
204
- const elapsedTime = currentTime - creationTime;
205
- console.log("Sandbox running for: " + elapsedTime + " seconds of " + timeoutValue + " seconds");
206
-
207
- // If we've exceeded the timeout, show BSOD
208
- if (elapsedTime >= timeoutValue) {
209
- console.log("Sandbox timeout! Showing BSOD");
210
- showBSOD('Error');
211
- // Don't set another timeout, we're done checking
212
- return;
213
- }
214
- }
215
-
216
- // Continue checking every 5 seconds
217
- setTimeout(checkSandboxTimeout, 5000);
218
- };
219
-
220
- const showBSOD = function(statusText = 'Error') {
221
- console.log("Showing BSOD with status: " + statusText);
222
- const iframe = document.getElementById('sandbox-iframe');
223
- const bsod = document.getElementById('bsod-image');
224
-
225
- if (iframe && bsod) {
226
- iframe.style.display = 'none';
227
- bsod.style.display = 'block';
228
-
229
- // Update status indicator
230
- const statusIndicator = document.querySelector('.status-indicator');
231
- const statusTextElem = document.querySelector('.status-text');
232
-
233
- if (statusIndicator) {
234
- statusIndicator.className = 'status-indicator status-error';
235
- }
236
-
237
- if (statusTextElem) {
238
- statusTextElem.innerText = statusText;
239
- }
240
- }
241
- };
242
-
243
- const resetBSOD = function() {
244
- console.log("Resetting BSOD display");
245
- const iframe = document.getElementById('sandbox-iframe');
246
- const bsod = document.getElementById('bsod-image');
247
-
248
- if (iframe && bsod) {
249
- if (bsod.style.display === 'block') {
250
- // BSOD is currently showing, reset it
251
- iframe.style.display = 'block';
252
- bsod.style.display = 'none';
253
- console.log("BSOD reset complete");
254
- return true; // Indicates reset was performed
255
- }
256
- }
257
- return false; // No reset needed
258
- };
259
-
260
- // Function to monitor for error messages
261
- const monitorForErrors = function() {
262
- console.log("Error monitor started");
263
- const resultsInterval = setInterval(function() {
264
- const resultsElements = document.querySelectorAll('textarea, .output-text');
265
- for (let elem of resultsElements) {
266
- const content = elem.value || elem.innerText || '';
267
- if (content.includes('Error running agent')) {
268
- console.log("Error detected!");
269
- showBSOD('Error');
270
- clearInterval(resultsInterval);
271
- break;
272
- }
273
- }
274
- }, 1000);
275
- };
276
-
277
-
278
- // Start monitoring for timeouts immediately
279
- checkSandboxTimeout();
280
-
281
- // Start monitoring for errors
282
- setTimeout(monitorForErrors, 3000);
283
-
284
- // Also monitor for errors after button clicks
285
- document.addEventListener('click', function(e) {
286
- if (e.target.tagName === 'BUTTON') {
287
- if (e.target.innerText === "Let's go!") {
288
- resetBSOD();
289
- }
290
- setTimeout(monitorForErrors, 3000);
291
- }
292
- });
293
-
294
- // Set up an interval to click the refresh button every 5 seconds
295
- setInterval(function() {
296
- const btn = document.getElementById('refresh-log-btn');
297
- if (btn) btn.click();
298
- }, 5000);
299
-
300
- // Force dark mode
301
- const params = new URLSearchParams(window.location.search);
302
- if (!params.has('__theme')) {
303
- params.set('__theme', 'dark');
304
- window.location.search = params.toString();
305
- }
306
- }
307
- """
308
 
309
 
310
- def upload_to_hf_and_remove(folder_path):
311
- repo_id = "smolagents/computer-agent-logs"
312
- try:
313
- folder_name = os.path.basename(os.path.normpath(folder_path))
314
 
315
- # Upload the folder to Huggingface
316
- print(f"Uploading {folder_path} to {repo_id}/{folder_name}...")
317
- url = upload_folder(
318
- folder_path=folder_path,
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  repo_id=repo_id,
320
  repo_type="dataset",
321
- path_in_repo=folder_name,
322
  ignore_patterns=[".git/*", ".gitignore"],
323
  )
 
324
 
325
- # Remove the local folder after successful upload
326
- print(f"Upload complete. Removing local folder {folder_path}...")
327
- shutil.rmtree(folder_path)
328
- print("Local folder removed successfully.")
329
-
330
- return url
331
-
332
- except Exception as e:
333
- print(f"Error during upload or cleanup: {str(e)}")
334
- raise
335
 
336
 
337
  def cleanup_sandboxes():
@@ -432,9 +186,10 @@ def generate_interaction_id(session_uuid):
432
 
433
 
434
  def save_final_status(folder, status: str, summary, error_message=None) -> None:
435
- with open(os.path.join(folder, "metadata.json"), "w") as output_file:
436
  output_file.write(
437
- json.dumps(
 
438
  {"status": status, "summary": summary, "error_message": error_message},
439
  )
440
  )
@@ -468,13 +223,16 @@ def create_agent(data_dir, desktop):
468
  model=model,
469
  data_dir=data_dir,
470
  desktop=desktop,
471
- max_steps=200,
472
  verbosity_level=2,
473
  # planning_interval=10,
474
  use_v1_prompt=True,
475
  )
476
 
477
 
 
 
 
478
  class EnrichedGradioUI(GradioUI):
479
  def log_user_message(self, text_input):
480
  import gradio as gr
@@ -495,8 +253,10 @@ class EnrichedGradioUI(GradioUI):
495
  ):
496
  interaction_id = generate_interaction_id(session_uuid)
497
  desktop = get_or_create_sandbox(session_uuid)
 
498
 
499
  data_dir = os.path.join(TMP_DIR, interaction_id)
 
500
  if not os.path.exists(data_dir):
501
  os.makedirs(data_dir)
502
 
@@ -504,20 +264,31 @@ class EnrichedGradioUI(GradioUI):
504
  session_state["agent"] = create_agent(data_dir=data_dir, desktop=desktop)
505
 
506
  try:
507
- stored_messages.append(gr.ChatMessage(role="user", content=task_input))
 
 
 
 
508
  yield stored_messages
509
 
 
 
 
 
 
 
 
510
  screenshot_bytes = session_state["agent"].desktop.screenshot(format="bytes")
511
  initial_screenshot = Image.open(BytesIO(screenshot_bytes))
512
-
513
  for msg in stream_to_gradio(
514
  session_state["agent"],
515
  task=task_input,
516
- task_images=[initial_screenshot],
517
  reset_agent_memory=False,
 
518
  ):
519
  if (
520
  hasattr(session_state["agent"], "last_marked_screenshot")
 
521
  and msg.content == "-----"
522
  ): # Append the last screenshot before the end of step
523
  stored_messages.append(
@@ -529,35 +300,46 @@ class EnrichedGradioUI(GradioUI):
529
  ].last_marked_screenshot.to_string(),
530
  "mime_type": "image/png",
531
  },
 
532
  )
533
  )
534
- stored_messages.append(msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  yield stored_messages
536
 
537
- # THIS ERASES IMAGES FROM AGENT MEMORY, USE WITH CAUTION
538
- if consent_storage and not task_input in EXAMPLES:
539
- summary = get_agent_summary_erase_images(session_state["agent"])
540
- save_final_status(data_dir, "completed", summary=summary)
541
  yield stored_messages
542
 
543
  except Exception as e:
544
  error_message = f"Error in interaction: {str(e)}"
545
- raise e
546
  print(error_message)
547
  stored_messages.append(
548
  gr.ChatMessage(
549
  role="assistant", content="Run failed:\n" + error_message
550
  )
551
  )
552
- if consent_storage:
 
 
 
553
  summary = get_agent_summary_erase_images(session_state["agent"])
554
  save_final_status(
555
- data_dir, "failed", summary=summary, error_message=error_message
556
  )
557
- yield stored_messages
558
- finally:
559
- if consent_storage:
560
- upload_to_hf_and_remove(data_dir)
561
 
562
 
563
  theme = gr.themes.Default(
@@ -565,7 +347,7 @@ theme = gr.themes.Default(
565
  )
566
 
567
  # Create a Gradio app with Blocks
568
- with gr.Blocks(theme=theme, css=custom_css, js=custom_js) as demo:
569
  # Storing session hash in a state variable
570
  session_uuid_state = gr.State(None)
571
  print("Starting the app!")
@@ -588,7 +370,7 @@ In this app, you'll be able to interact with an agent powered by [smolagents](ht
588
  _Please note that we store the task logs by default so **do not write any personal information**; you can uncheck the logs storing on the task bar._
589
  """)
590
  task_input = gr.Textbox(
591
- value="Find me pictures of cute puppies",
592
  label="Enter your task below:",
593
  elem_classes="primary-color-label",
594
  )
@@ -620,54 +402,13 @@ _Please note that we store the task logs by default so **do not write any person
620
  """.strip()
621
  )
622
 
623
- def apply_theme(minimalist_mode: bool):
624
- if not minimalist_mode:
625
- return """
626
- <style>
627
- .sandbox-frame {
628
- display: block!important;
629
- }
630
-
631
- .sandbox-iframe, .bsod-image {
632
- /* top: 73px; */
633
- top: 99px;
634
- /* left: 74px; */
635
- left: 110px;
636
- }
637
- .sandbox-iframe {
638
- transform: scale(0.667);
639
- /* transform: scale(0.59); */
640
- }
641
-
642
- .status-bar {
643
- position: absolute;
644
- bottom: 88px;
645
- left: 355px;
646
- }
647
- .status-text {
648
- color: #fed244;
649
- }
650
- </style>
651
- """
652
- else:
653
- return """
654
- <style>
655
- .sandbox-container {
656
- height: 700px!important;
657
- }
658
- .sandbox-iframe {
659
- transform: scale(0.65);
660
- }
661
- </style>
662
- """
663
-
664
  # Hidden HTML element to inject CSS dynamically
665
  theme_styles = gr.HTML(apply_theme(False), visible=False)
666
  minimalist_toggle.change(
667
  fn=apply_theme, inputs=[minimalist_toggle], outputs=[theme_styles]
668
  )
669
 
670
- footer = gr.HTML(value=footer_html, label="Header")
671
 
672
  chatbot_display = gr.Chatbot(
673
  elem_id="chatbot",
@@ -738,15 +479,13 @@ _Please note that we store the task logs by default so **do not write any person
738
  def interrupt_agent(session_state):
739
  if not session_state["agent"].interrupt_switch:
740
  session_state["agent"].interrupt()
 
741
  return gr.Button("Stopping agent... (could take time)", variant="secondary")
742
  else:
743
  return gr.Button("Stop the agent!", variant="huggingface")
744
 
745
  stop_btn.click(fn=interrupt_agent, inputs=[session_state], outputs=[stop_btn])
746
 
747
- def set_logs_source(session_state):
748
- session_state["replay_log"] = "udupp2fyavq_1743170323"
749
-
750
  demo.load(
751
  fn=lambda: True, # dummy to trigger the load
752
  outputs=[is_interactive],
@@ -757,6 +496,18 @@ _Please note that we store the task logs by default so **do not write any person
757
  outputs=[sandbox_html, session_uuid_state],
758
  )
759
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  # Launch the app
761
  if __name__ == "__main__":
762
  Timer(60, cleanup_sandboxes).start() # Run every minute
 
1
  import json
2
  import os
3
  import shutil
4
+ import tempfile
5
  import time
6
  import uuid
7
  from io import BytesIO
8
  from threading import Timer
9
+ from typing import Any
10
 
11
  import gradio as gr
12
  from dotenv import load_dotenv
 
15
  from huggingface_hub import login, upload_folder
16
  from PIL import Image
17
  from smolagents import CodeAgent, InferenceClientModel
18
+ from smolagents.gradio_ui import GradioUI
19
 
20
  from e2bqwen import E2BVisionAgent, get_agent_summary_erase_images
21
+ from gradio_script import stream_to_gradio
22
+ from scripts_and_styling import (
23
+ CUSTOM_JS,
24
+ FOOTER_HTML,
25
+ SANDBOX_CSS_TEMPLATE,
26
+ SANDBOX_HTML_TEMPLATE,
27
+ apply_theme,
28
+ )
29
 
30
  load_dotenv(override=True)
31
 
 
38
  ]
39
 
40
  E2B_API_KEY = os.getenv("E2B_API_KEY")
41
+ SANDBOXES: dict[str, Sandbox] = {}
42
+ SANDBOX_METADATA: dict[str, dict[str, Any]] = {}
43
  SANDBOX_TIMEOUT = 300
44
+ WIDTH = 1280
45
+ HEIGHT = 960
46
  TMP_DIR = "./tmp/"
47
  if not os.path.exists(TMP_DIR):
48
  os.makedirs(TMP_DIR)
 
50
  hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_API_KEY")
51
  login(token=hf_token)
52
 
53
+ custom_css = SANDBOX_CSS_TEMPLATE.replace("<<WIDTH>>", str(WIDTH + 15)).replace(
54
+ "<<HEIGHT>>", str(HEIGHT + 10)
55
+ )
56
+
57
+ sandbox_html_template = SANDBOX_HTML_TEMPLATE.replace(
58
+ "<<WIDTH>>", str(WIDTH + 15)
59
+ ).replace("<<HEIGHT>>", str(HEIGHT + 10))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
 
62
+ def upload_to_hf_and_remove(folder_paths: list[str]):
63
+ repo_id = "smolagents/computer-agent-logs-2"
 
 
64
 
65
+ with tempfile.TemporaryDirectory(dir=TMP_DIR) as temp_dir:
66
+ print(f"Preparing to upload {len(folder_paths)} folders to {repo_id}...")
67
+
68
+ # Copy all folders into the temporary directory
69
+ for folder_path in folder_paths:
70
+ folder_name = os.path.basename(os.path.normpath(folder_path))
71
+ target_path = os.path.join(temp_dir, folder_name)
72
+ print(f"Copying {folder_path} to temporary directory...")
73
+ shutil.copytree(folder_path, target_path)
74
+ # Remove the original folder after copying
75
+ shutil.rmtree(folder_path)
76
+ print(f"Original folder {folder_path} removed.")
77
+
78
+ # Upload the entire temporary directory
79
+ print(f"Uploading all folders to {repo_id}...")
80
+ upload_folder(
81
+ folder_path=temp_dir,
82
  repo_id=repo_id,
83
  repo_type="dataset",
 
84
  ignore_patterns=[".git/*", ".gitignore"],
85
  )
86
+ print("Upload complete.")
87
 
88
+ return f"Successfully uploaded {len(folder_paths)} folders to {repo_id}"
 
 
 
 
 
 
 
 
 
89
 
90
 
91
  def cleanup_sandboxes():
 
186
 
187
 
188
  def save_final_status(folder, status: str, summary, error_message=None) -> None:
189
+ with open(os.path.join(folder, "metadata.jsonl"), "a") as output_file:
190
  output_file.write(
191
+ "\n"
192
+ + json.dumps(
193
  {"status": status, "summary": summary, "error_message": error_message},
194
  )
195
  )
 
223
  model=model,
224
  data_dir=data_dir,
225
  desktop=desktop,
226
+ max_steps=20,
227
  verbosity_level=2,
228
  # planning_interval=10,
229
  use_v1_prompt=True,
230
  )
231
 
232
 
233
+ INTERACTION_IDS = {}
234
+
235
+
236
  class EnrichedGradioUI(GradioUI):
237
  def log_user_message(self, text_input):
238
  import gradio as gr
 
253
  ):
254
  interaction_id = generate_interaction_id(session_uuid)
255
  desktop = get_or_create_sandbox(session_uuid)
256
+ INTERACTION_IDS[interaction_id] = session_uuid
257
 
258
  data_dir = os.path.join(TMP_DIR, interaction_id)
259
+
260
  if not os.path.exists(data_dir):
261
  os.makedirs(data_dir)
262
 
 
264
  session_state["agent"] = create_agent(data_dir=data_dir, desktop=desktop)
265
 
266
  try:
267
+ stored_messages.append(
268
+ gr.ChatMessage(
269
+ role="user", content=task_input, metadata={"status": "done"}
270
+ )
271
+ )
272
  yield stored_messages
273
 
274
+ with open(os.path.join(data_dir, "metadata.jsonl"), "w") as output_file:
275
+ output_file.write(
276
+ json.dumps(
277
+ {"task": task_input},
278
+ )
279
+ )
280
+
281
  screenshot_bytes = session_state["agent"].desktop.screenshot(format="bytes")
282
  initial_screenshot = Image.open(BytesIO(screenshot_bytes))
 
283
  for msg in stream_to_gradio(
284
  session_state["agent"],
285
  task=task_input,
 
286
  reset_agent_memory=False,
287
+ task_images=[initial_screenshot],
288
  ):
289
  if (
290
  hasattr(session_state["agent"], "last_marked_screenshot")
291
+ and isinstance(msg, gr.ChatMessage)
292
  and msg.content == "-----"
293
  ): # Append the last screenshot before the end of step
294
  stored_messages.append(
 
300
  ].last_marked_screenshot.to_string(),
301
  "mime_type": "image/png",
302
  },
303
+ metadata={"status": "done"},
304
  )
305
  )
306
+ if isinstance(msg, gr.ChatMessage):
307
+ stored_messages.append(msg)
308
+ elif isinstance(msg, str): # Then it's only a completion delta
309
+ try:
310
+ if stored_messages[-1].metadata["status"] == "pending":
311
+ stored_messages[-1].content = msg
312
+ else:
313
+ stored_messages.append(
314
+ gr.ChatMessage(
315
+ role="assistant",
316
+ content=msg,
317
+ metadata={"status": "pending"},
318
+ )
319
+ )
320
+ except Exception as e:
321
+ raise e
322
  yield stored_messages
323
 
324
+ status = "completed"
 
 
 
325
  yield stored_messages
326
 
327
  except Exception as e:
328
  error_message = f"Error in interaction: {str(e)}"
 
329
  print(error_message)
330
  stored_messages.append(
331
  gr.ChatMessage(
332
  role="assistant", content="Run failed:\n" + error_message
333
  )
334
  )
335
+ status = "failed"
336
+ yield stored_messages
337
+ finally:
338
+ if consent_storage and task_input not in EXAMPLES:
339
  summary = get_agent_summary_erase_images(session_state["agent"])
340
  save_final_status(
341
+ data_dir, status, summary=summary, error_message=error_message
342
  )
 
 
 
 
343
 
344
 
345
  theme = gr.themes.Default(
 
347
  )
348
 
349
  # Create a Gradio app with Blocks
350
+ with gr.Blocks(theme=theme, css=custom_css, js=CUSTOM_JS) as demo:
351
  # Storing session hash in a state variable
352
  session_uuid_state = gr.State(None)
353
  print("Starting the app!")
 
370
  _Please note that we store the task logs by default so **do not write any personal information**; you can uncheck the logs storing on the task bar._
371
  """)
372
  task_input = gr.Textbox(
373
+ placeholder="Find me pictures of cute puppies",
374
  label="Enter your task below:",
375
  elem_classes="primary-color-label",
376
  )
 
402
  """.strip()
403
  )
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  # Hidden HTML element to inject CSS dynamically
406
  theme_styles = gr.HTML(apply_theme(False), visible=False)
407
  minimalist_toggle.change(
408
  fn=apply_theme, inputs=[minimalist_toggle], outputs=[theme_styles]
409
  )
410
 
411
+ footer = gr.HTML(value=FOOTER_HTML, label="Footer")
412
 
413
  chatbot_display = gr.Chatbot(
414
  elem_id="chatbot",
 
479
  def interrupt_agent(session_state):
480
  if not session_state["agent"].interrupt_switch:
481
  session_state["agent"].interrupt()
482
+ print("Stopping agent...")
483
  return gr.Button("Stopping agent... (could take time)", variant="secondary")
484
  else:
485
  return gr.Button("Stop the agent!", variant="huggingface")
486
 
487
  stop_btn.click(fn=interrupt_agent, inputs=[session_state], outputs=[stop_btn])
488
 
 
 
 
489
  demo.load(
490
  fn=lambda: True, # dummy to trigger the load
491
  outputs=[is_interactive],
 
496
  outputs=[sandbox_html, session_uuid_state],
497
  )
498
 
499
+ def upload_interaction_logs():
500
+ data_dirs = []
501
+ for interaction_id in list(INTERACTION_IDS.keys()):
502
+ data_dir = os.path.join(TMP_DIR, interaction_id)
503
+ if os.path.exists(data_dir):
504
+ data_dirs.append(data_dir)
505
+ INTERACTION_IDS.pop(interaction_id)
506
+
507
+ upload_to_hf_and_remove(data_dirs)
508
+
509
+ demo.unload(fn=upload_interaction_logs)
510
+
511
  # Launch the app
512
  if __name__ == "__main__":
513
  Timer(60, cleanup_sandboxes).start() # Run every minute
e2bqwen.py CHANGED
@@ -3,7 +3,7 @@ import time
3
  import unicodedata
4
  from datetime import datetime
5
  from io import BytesIO
6
- from typing import Any, Dict, List, Optional
7
 
8
  # E2B imports
9
  from e2b_desktop import Sandbox
@@ -13,7 +13,6 @@ from PIL import Image, ImageDraw
13
  from smolagents import CodeAgent, HfApiModel, tool
14
  from smolagents.agent_types import AgentImage
15
  from smolagents.memory import ActionStep, TaskStep
16
- from smolagents.models import ChatMessage, Model
17
  from smolagents.monitoring import LogLevel
18
 
19
  E2B_SYSTEM_PROMPT_TEMPLATE = """You are a desktop automation assistant that can control a remote desktop environment. The current date is <<current_date>>.
@@ -189,6 +188,7 @@ class E2BVisionAgent(CodeAgent):
189
  max_steps=max_steps,
190
  verbosity_level=verbosity_level,
191
  planning_interval=self.planning_interval,
 
192
  **kwargs,
193
  )
194
  self.prompt_templates["system_prompt"] = E2B_SYSTEM_PROMPT_TEMPLATE.replace(
@@ -456,4 +456,4 @@ class E2BVisionAgent(CodeAgent):
456
  print("Stopping e2b stream and killing sandbox...")
457
  self.desktop.stream.stop()
458
  self.desktop.kill()
459
- print("E2B sandbox terminated")
 
3
  import unicodedata
4
  from datetime import datetime
5
  from io import BytesIO
6
+ from typing import List
7
 
8
  # E2B imports
9
  from e2b_desktop import Sandbox
 
13
  from smolagents import CodeAgent, HfApiModel, tool
14
  from smolagents.agent_types import AgentImage
15
  from smolagents.memory import ActionStep, TaskStep
 
16
  from smolagents.monitoring import LogLevel
17
 
18
  E2B_SYSTEM_PROMPT_TEMPLATE = """You are a desktop automation assistant that can control a remote desktop environment. The current date is <<current_date>>.
 
188
  max_steps=max_steps,
189
  verbosity_level=verbosity_level,
190
  planning_interval=self.planning_interval,
191
+ stream_outputs=True,
192
  **kwargs,
193
  )
194
  self.prompt_templates["system_prompt"] = E2B_SYSTEM_PROMPT_TEMPLATE.replace(
 
456
  print("Stopping e2b stream and killing sandbox...")
457
  self.desktop.stream.stop()
458
  self.desktop.kill()
459
+ print("E2B sandbox terminated")
gradio_script.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from smolagents.agent_types import AgentAudio, AgentImage, AgentText
4
+ from smolagents.agents import PlanningStep
5
+ from smolagents.gradio_ui import get_step_footnote_content
6
+ from smolagents.memory import ActionStep, FinalAnswerStep, MemoryStep
7
+ from smolagents.models import ChatMessageStreamDelta
8
+ from smolagents.utils import _is_package_available
9
+
10
+
11
+ def pull_messages_from_step(step_log: MemoryStep, skip_model_outputs: bool = False):
12
+ """Extract ChatMessage objects from agent steps with proper nesting.
13
+
14
+ Args:
15
+ step_log: The step log to display as gr.ChatMessage objects.
16
+ skip_model_outputs: If True, skip the model outputs when creating the gr.ChatMessage objects:
17
+ This is used for instance when streaming model outputs have already been displayed.
18
+ """
19
+ if not _is_package_available("gradio"):
20
+ raise ModuleNotFoundError(
21
+ "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
22
+ )
23
+ import gradio as gr
24
+
25
+ if isinstance(step_log, ActionStep):
26
+ # Output the step number
27
+ step_number = (
28
+ f"Step {step_log.step_number}"
29
+ if step_log.step_number is not None
30
+ else "Step"
31
+ )
32
+ if not skip_model_outputs:
33
+ yield gr.ChatMessage(
34
+ role="assistant",
35
+ content=f"**{step_number}**",
36
+ metadata={"status": "done"},
37
+ )
38
+
39
+ # First yield the thought/reasoning from the LLM
40
+ if (
41
+ not skip_model_outputs
42
+ and hasattr(step_log, "model_output")
43
+ and step_log.model_output is not None
44
+ ):
45
+ model_output = step_log.model_output.strip()
46
+ # Remove any trailing <end_code> and extra backticks, handling multiple possible formats
47
+ model_output = re.sub(
48
+ r"```\s*<end_code>", "```", model_output
49
+ ) # handles ```<end_code>
50
+ model_output = re.sub(
51
+ r"<end_code>\s*```", "```", model_output
52
+ ) # handles <end_code>```
53
+ model_output = re.sub(
54
+ r"```\s*\n\s*<end_code>", "```", model_output
55
+ ) # handles ```\n<end_code>
56
+ model_output = model_output.strip()
57
+ yield gr.ChatMessage(
58
+ role="assistant", content=model_output, metadata={"status": "done"}
59
+ )
60
+
61
+ # For tool calls, create a parent message
62
+ if hasattr(step_log, "tool_calls") and step_log.tool_calls is not None:
63
+ first_tool_call = step_log.tool_calls[0]
64
+ used_code = first_tool_call.name == "python_interpreter"
65
+
66
+ # Tool call becomes the parent message with timing info
67
+ # First we will handle arguments based on type
68
+ args = first_tool_call.arguments
69
+ if isinstance(args, dict):
70
+ content = str(args.get("answer", str(args)))
71
+ else:
72
+ content = str(args).strip()
73
+
74
+ if used_code:
75
+ # Clean up the content by removing any end code tags
76
+ content = re.sub(
77
+ r"```.*?\n", "", content
78
+ ) # Remove existing code blocks
79
+ content = re.sub(
80
+ r"\s*<end_code>\s*", "", content
81
+ ) # Remove end_code tags
82
+ content = content.strip()
83
+ if not content.startswith("```python"):
84
+ content = f"```python\n{content}\n```"
85
+
86
+ parent_message_tool = gr.ChatMessage(
87
+ role="assistant",
88
+ content=content,
89
+ metadata={
90
+ "title": f"🛠️ Used tool {first_tool_call.name}",
91
+ "status": "done",
92
+ },
93
+ )
94
+ yield parent_message_tool
95
+
96
+ # Display execution logs if they exist
97
+ if hasattr(step_log, "observations") and (
98
+ step_log.observations is not None and step_log.observations.strip()
99
+ ): # Only yield execution logs if there's actual content
100
+ log_content = step_log.observations.strip()
101
+ if log_content:
102
+ log_content = re.sub(r"^Execution logs:\s*", "", log_content)
103
+ yield gr.ChatMessage(
104
+ role="assistant",
105
+ content=f"```bash\n{log_content}\n",
106
+ metadata={"title": "📝 Execution Logs", "status": "done"},
107
+ )
108
+
109
+ # Display any errors
110
+ if hasattr(step_log, "error") and step_log.error is not None:
111
+ yield gr.ChatMessage(
112
+ role="assistant",
113
+ content=str(step_log.error),
114
+ metadata={"title": "💥 Error", "status": "done"},
115
+ )
116
+
117
+ # Update parent message metadata to done status without yielding a new message
118
+ if getattr(step_log, "observations_images", []):
119
+ for image in step_log.observations_images:
120
+ path_image = AgentImage(image).to_string()
121
+ yield gr.ChatMessage(
122
+ role="assistant",
123
+ content={
124
+ "path": path_image,
125
+ "mime_type": f"image/{path_image.split('.')[-1]}",
126
+ },
127
+ metadata={"title": "🖼️ Output Image", "status": "done"},
128
+ )
129
+
130
+ # Handle standalone errors but not from tool calls
131
+ if hasattr(step_log, "error") and step_log.error is not None:
132
+ yield gr.ChatMessage(
133
+ role="assistant",
134
+ content=str(step_log.error),
135
+ metadata={"title": "💥 Error", "status": "done"},
136
+ )
137
+
138
+ yield gr.ChatMessage(
139
+ role="assistant",
140
+ content=get_step_footnote_content(step_log, step_number),
141
+ metadata={"status": "done"},
142
+ )
143
+ yield gr.ChatMessage(
144
+ role="assistant", content="-----", metadata={"status": "done"}
145
+ )
146
+
147
+ elif isinstance(step_log, PlanningStep):
148
+ yield gr.ChatMessage(
149
+ role="assistant", content="**Planning step**", metadata={"status": "done"}
150
+ )
151
+ yield gr.ChatMessage(
152
+ role="assistant", content=step_log.plan, metadata={"status": "done"}
153
+ )
154
+ yield gr.ChatMessage(
155
+ role="assistant",
156
+ content=get_step_footnote_content(step_log, "Planning step"),
157
+ metadata={"status": "done"},
158
+ )
159
+ yield gr.ChatMessage(
160
+ role="assistant", content="-----", metadata={"status": "done"}
161
+ )
162
+
163
+ elif isinstance(step_log, FinalAnswerStep):
164
+ final_answer = step_log.final_answer
165
+ if isinstance(final_answer, AgentText):
166
+ yield gr.ChatMessage(
167
+ role="assistant",
168
+ content=f"**Final answer:**\n{final_answer.to_string()}\n",
169
+ metadata={"status": "done"},
170
+ )
171
+ elif isinstance(final_answer, AgentImage):
172
+ yield gr.ChatMessage(
173
+ role="assistant",
174
+ content={"path": final_answer.to_string(), "mime_type": "image/png"},
175
+ metadata={"status": "done"},
176
+ )
177
+ elif isinstance(final_answer, AgentAudio):
178
+ yield gr.ChatMessage(
179
+ role="assistant",
180
+ content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
181
+ metadata={"status": "done"},
182
+ )
183
+ else:
184
+ yield gr.ChatMessage(
185
+ role="assistant",
186
+ content=f"**Final answer:** {str(final_answer)}",
187
+ metadata={"status": "done"},
188
+ )
189
+
190
+ else:
191
+ raise ValueError(f"Unsupported step type: {type(step_log)}")
192
+
193
+
194
+ def stream_to_gradio(
195
+ agent,
196
+ task: str,
197
+ task_images: list | None = None,
198
+ reset_agent_memory: bool = False,
199
+ additional_args: dict | None = None,
200
+ ):
201
+ """Runs an agent with the given task and streams the messages from the agent as gradio ChatMessages."""
202
+ total_input_tokens = 0
203
+ total_output_tokens = 0
204
+
205
+ if not _is_package_available("gradio"):
206
+ raise ModuleNotFoundError(
207
+ "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
208
+ )
209
+
210
+ intermediate_text = ""
211
+
212
+ for step_log in agent.run(
213
+ task,
214
+ images=task_images,
215
+ stream=True,
216
+ reset=reset_agent_memory,
217
+ additional_args=additional_args,
218
+ ):
219
+ # Track tokens if model provides them
220
+ if getattr(agent.model, "last_input_token_count", None) is not None:
221
+ total_input_tokens += agent.model.last_input_token_count
222
+ total_output_tokens += agent.model.last_output_token_count
223
+ if isinstance(step_log, (ActionStep, PlanningStep)):
224
+ step_log.input_token_count = agent.model.last_input_token_count
225
+ step_log.output_token_count = agent.model.last_output_token_count
226
+
227
+ if isinstance(step_log, MemoryStep):
228
+ intermediate_text = ""
229
+ for message in pull_messages_from_step(
230
+ step_log,
231
+ # If we're streaming model outputs, no need to display them twice
232
+ skip_model_outputs=getattr(agent, "stream_outputs", False),
233
+ ):
234
+ yield message
235
+ elif isinstance(step_log, ChatMessageStreamDelta):
236
+ intermediate_text += step_log.content or ""
237
+ yield intermediate_text
scripts_and_styling.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SANDBOX_HTML_TEMPLATE = """
2
+ <style>
3
+ @import url('https://fonts.googleapis.com/css2?family=Oxanium:[email protected]&display=swap');
4
+ </style>
5
+ <h1 style="color:var(--color-accent);margin:0;">Open Computer Agent - <i>Powered by <a href="https://github.com/huggingface/smolagents">smolagents</a></i><h1>
6
+ <div class="sandbox-container" style="margin:0;">
7
+ <div class="status-bar">
8
+ <div class="status-indicator {status_class}"></div>
9
+ <div class="status-text">{status_text}</div>
10
+ </div>
11
+ <iframe id="sandbox-iframe"
12
+ src="{stream_url}"
13
+ class="sandbox-iframe"
14
+ style="display: block;"
15
+ allowfullscreen>
16
+ </iframe>
17
+ <img src="https://huggingface.co/datasets/mfarre/servedfiles/resolve/main/blue_screen_of_death.gif" class="bsod-image" style="display: none;"/>
18
+ <img src="https://huggingface.co/datasets/m-ric/images/resolve/main/HUD_thom.png" class="sandbox-frame" />
19
+ </div>
20
+ """
21
+
22
+ SANDBOX_CSS_TEMPLATE = """
23
+ .modal-container {
24
+ margin: var(--size-16) auto!important;
25
+ }
26
+
27
+ .sandbox-container {
28
+ position: relative;
29
+ width: 910px;
30
+ overflow: hidden;
31
+ margin: auto;
32
+ }
33
+ .sandbox-container {
34
+ height: 800px;
35
+ }
36
+ .sandbox-frame {
37
+ display: none;
38
+ position: absolute;
39
+ top: 0;
40
+ left: 0;
41
+ width: 910px;
42
+ height: 800px;
43
+ pointer-events:none;
44
+ }
45
+
46
+ .sandbox-iframe, .bsod-image {
47
+ position: absolute;
48
+ width: <<WIDTH>>px;
49
+ height: <<HEIGHT>>px;
50
+ border: 4px solid #444444;
51
+ transform-origin: 0 0;
52
+ }
53
+
54
+ /* Colored label for task textbox */
55
+ .primary-color-label label span {
56
+ font-weight: bold;
57
+ color: var(--color-accent);
58
+ }
59
+
60
+ /* Status indicator light */
61
+ .status-bar {
62
+ display: flex;
63
+ flex-direction: row;
64
+ align-items: center;
65
+ flex-align:center;
66
+ z-index: 100;
67
+ }
68
+
69
+ .status-indicator {
70
+ width: 15px;
71
+ height: 15px;
72
+ border-radius: 50%;
73
+ }
74
+
75
+ .status-text {
76
+ font-size: 16px;
77
+ font-weight: bold;
78
+ padding-left: 8px;
79
+ text-shadow: none;
80
+ }
81
+
82
+ .status-interactive {
83
+ background-color: #2ecc71;
84
+ animation: blink 2s infinite;
85
+ }
86
+
87
+ .status-view-only {
88
+ background-color: #e74c3c;
89
+ }
90
+
91
+ .status-error {
92
+ background-color: #e74c3c;
93
+ animation: blink-error 1s infinite;
94
+ }
95
+
96
+ @keyframes blink-error {
97
+ 0% { background-color: rgba(231, 76, 60, 1); }
98
+ 50% { background-color: rgba(231, 76, 60, 0.4); }
99
+ 100% { background-color: rgba(231, 76, 60, 1); }
100
+ }
101
+
102
+ @keyframes blink {
103
+ 0% { background-color: rgba(46, 204, 113, 1); } /* Green at full opacity */
104
+ 50% { background-color: rgba(46, 204, 113, 0.4); } /* Green at 40% opacity */
105
+ 100% { background-color: rgba(46, 204, 113, 1); } /* Green at full opacity */
106
+ }
107
+
108
+ #chatbot {
109
+ height:1000px!important;
110
+ }
111
+ #chatbot .role {
112
+ max-width:95%
113
+ }
114
+
115
+ #chatbot .bubble-wrap {
116
+ overflow-y: visible;
117
+ }
118
+
119
+ .logo-container {
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: flex-start;
123
+ width: 100%;
124
+ box-sizing: border-box;
125
+ gap: 5px;
126
+
127
+ .logo-item {
128
+ display: flex;
129
+ align-items: center;
130
+ padding: 0 30px;
131
+ gap: 10px;
132
+ text-decoration: none!important;
133
+ color: #f59e0b;
134
+ font-size:17px;
135
+ }
136
+ .logo-item:hover {
137
+ color: #935f06!important;
138
+ }
139
+ """
140
+
141
+ FOOTER_HTML = """
142
+ <h3 style="text-align: center; margin-top:50px;"><i>Powered by open source:</i></h2>
143
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
144
+ <div class="logo-container">
145
+ <a class="logo-item" href="https://github.com/huggingface/smolagents"><i class="fa fa-github"></i>smolagents</a>
146
+ <a class="logo-item" href="https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct"><i class="fa fa-github"></i>Qwen2-VL-72B</a>
147
+ <a class="logo-item" href="https://github.com/e2b-dev/desktop"><i class="fa fa-github"></i>E2B Desktop</a>
148
+ </div>
149
+ """
150
+
151
+ CUSTOM_JS = """function() {
152
+ document.body.classList.add('dark');
153
+
154
+ // Function to check if sandbox is timing out
155
+ const checkSandboxTimeout = function() {
156
+ const timeElement = document.getElementById('sandbox-creation-time');
157
+
158
+ if (timeElement) {
159
+ const creationTime = parseFloat(timeElement.getAttribute('data-time'));
160
+ const timeoutValue = parseFloat(timeElement.getAttribute('data-timeout'));
161
+ const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
162
+
163
+ const elapsedTime = currentTime - creationTime;
164
+ console.log("Sandbox running for: " + elapsedTime + " seconds of " + timeoutValue + " seconds");
165
+
166
+ // If we've exceeded the timeout, show BSOD
167
+ if (elapsedTime >= timeoutValue) {
168
+ console.log("Sandbox timeout! Showing BSOD");
169
+ showBSOD('Error');
170
+ // Don't set another timeout, we're done checking
171
+ return;
172
+ }
173
+ }
174
+
175
+ // Continue checking every 5 seconds
176
+ setTimeout(checkSandboxTimeout, 5000);
177
+ };
178
+
179
+ const showBSOD = function(statusText = 'Error') {
180
+ console.log("Showing BSOD with status: " + statusText);
181
+ const iframe = document.getElementById('sandbox-iframe');
182
+ const bsod = document.getElementById('bsod-image');
183
+
184
+ if (iframe && bsod) {
185
+ iframe.style.display = 'none';
186
+ bsod.style.display = 'block';
187
+
188
+ // Update status indicator
189
+ const statusIndicator = document.querySelector('.status-indicator');
190
+ const statusTextElem = document.querySelector('.status-text');
191
+
192
+ if (statusIndicator) {
193
+ statusIndicator.className = 'status-indicator status-error';
194
+ }
195
+
196
+ if (statusTextElem) {
197
+ statusTextElem.innerText = statusText;
198
+ }
199
+ }
200
+ };
201
+
202
+ const resetBSOD = function() {
203
+ console.log("Resetting BSOD display");
204
+ const iframe = document.getElementById('sandbox-iframe');
205
+ const bsod = document.getElementById('bsod-image');
206
+
207
+ if (iframe && bsod) {
208
+ if (bsod.style.display === 'block') {
209
+ // BSOD is currently showing, reset it
210
+ iframe.style.display = 'block';
211
+ bsod.style.display = 'none';
212
+ console.log("BSOD reset complete");
213
+ return true; // Indicates reset was performed
214
+ }
215
+ }
216
+ return false; // No reset needed
217
+ };
218
+
219
+ // Function to monitor for error messages
220
+ const monitorForErrors = function() {
221
+ console.log("Error monitor started");
222
+ const resultsInterval = setInterval(function() {
223
+ const resultsElements = document.querySelectorAll('textarea, .output-text');
224
+ for (let elem of resultsElements) {
225
+ const content = elem.value || elem.innerText || '';
226
+ if (content.includes('Error running agent')) {
227
+ console.log("Error detected!");
228
+ showBSOD('Error');
229
+ clearInterval(resultsInterval);
230
+ break;
231
+ }
232
+ }
233
+ }, 1000);
234
+ };
235
+
236
+
237
+ // Start monitoring for timeouts immediately
238
+ checkSandboxTimeout();
239
+
240
+ // Start monitoring for errors
241
+ setTimeout(monitorForErrors, 3000);
242
+
243
+ // Also monitor for errors after button clicks
244
+ document.addEventListener('click', function(e) {
245
+ if (e.target.tagName === 'BUTTON') {
246
+ if (e.target.innerText === "Let's go!") {
247
+ resetBSOD();
248
+ }
249
+ setTimeout(monitorForErrors, 3000);
250
+ }
251
+ });
252
+
253
+ // Set up an interval to click the refresh button every 5 seconds
254
+ setInterval(function() {
255
+ const btn = document.getElementById('refresh-log-btn');
256
+ if (btn) btn.click();
257
+ }, 5000);
258
+
259
+ // Force dark mode
260
+ const params = new URLSearchParams(window.location.search);
261
+ if (!params.has('__theme')) {
262
+ params.set('__theme', 'dark');
263
+ window.location.search = params.toString();
264
+ }
265
+ }
266
+ """
267
+
268
+
269
+ def apply_theme(minimalist_mode: bool):
270
+ if not minimalist_mode:
271
+ return """
272
+ <style>
273
+ .sandbox-frame {
274
+ display: block!important;
275
+ }
276
+
277
+ .sandbox-iframe, .bsod-image {
278
+ /* top: 73px; */
279
+ top: 99px;
280
+ /* left: 74px; */
281
+ left: 110px;
282
+ }
283
+ .sandbox-iframe {
284
+ transform: scale(0.535);
285
+ }
286
+
287
+ .status-bar {
288
+ position: absolute;
289
+ bottom: 88px;
290
+ left: 355px;
291
+ }
292
+ .status-text {
293
+ color: #fed244;
294
+ }
295
+ </style>
296
+ """
297
+ else:
298
+ return """
299
+ <style>
300
+ .sandbox-container {
301
+ height: 700px!important;
302
+ }
303
+ .sandbox-iframe {
304
+ transform: scale(0.7);
305
+ }
306
+ </style>
307
+ """