computer-use-ootb-internal 0.0.188__py3-none-any.whl → 0.0.189__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,666 +1,668 @@
1
- import argparse
2
- import time
3
- import json
4
- from datetime import datetime
5
- import threading
6
- import requests
7
- import platform # Add platform import
8
- import pyautogui # Add pyautogui import
9
- import webbrowser # Add webbrowser import
10
- import os # Import os for path joining
11
- import logging # Import logging
12
- import importlib # For dynamic imports
13
- import pkgutil # To find modules
14
- import sys # For logging setup
15
- import traceback # For logging setup
16
- from logging.handlers import RotatingFileHandler # For logging setup
17
- from fastapi import FastAPI, Request
18
- from fastapi.responses import JSONResponse
19
- from fastapi.middleware.cors import CORSMiddleware
20
- from computer_use_ootb_internal.computer_use_demo.tools.computer import get_screen_details
21
- from computer_use_ootb_internal.run_teachmode_ootb_args import simple_teachmode_sampling_loop
22
- from computer_use_ootb_internal.computer_use_demo.executor.teachmode_executor import TeachmodeExecutor
23
- import uvicorn # Assuming uvicorn is used to run FastAPI
24
- import concurrent.futures
25
- import asyncio
26
-
27
- # --- App Logging Setup ---
28
- try:
29
- # Log to user's AppData directory for better accessibility
30
- log_dir_base = os.environ.get('APPDATA', os.path.expanduser('~'))
31
- log_dir = os.path.join(log_dir_base, 'OOTBAppLogs')
32
- os.makedirs(log_dir, exist_ok=True)
33
- log_file = os.path.join(log_dir, 'ootb_app.log')
34
-
35
- log_format = '%(asctime)s - %(levelname)s - %(process)d - %(threadName)s - %(message)s'
36
- log_level = logging.INFO # Or logging.DEBUG for more detail
37
-
38
- # Use rotating file handler
39
- handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=2, encoding='utf-8')
40
- handler.setFormatter(logging.Formatter(log_format))
41
-
42
- # Configure root logger
43
- logging.basicConfig(level=log_level, handlers=[handler])
44
-
45
- # Add stream handler to see logs if running interactively (optional)
46
- # logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
47
-
48
- logging.info("="*20 + " OOTB App Starting " + "="*20)
49
- logging.info(f"Running with args: {sys.argv}")
50
- logging.info(f"Python Executable: {sys.executable}")
51
- logging.info(f"Working Directory: {os.getcwd()}")
52
- logging.info(f"User: {os.getenv('USERNAME')}")
53
-
54
- except Exception as log_setup_e:
55
- print(f"FATAL: Failed to set up logging: {log_setup_e}")
56
- # Fallback logging might be needed here if file logging fails
57
-
58
- # --- Get the root logger ---
59
- root_logger = logging.getLogger()
60
- root_logger.setLevel(log_level) # Ensure root logger level is set
61
-
62
- # --- File Handler (as before) ---
63
- file_handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=2, encoding='utf-8')
64
- file_handler.setFormatter(logging.Formatter(log_format))
65
- root_logger.addHandler(file_handler)
66
-
67
- # --- Console Handler (New) ---
68
- console_handler = logging.StreamHandler(sys.stdout) # Log to standard output
69
- console_handler.setFormatter(logging.Formatter(log_format))
70
- root_logger.addHandler(console_handler)
71
-
72
- # --- End App Logging Setup ---
73
-
74
- app = FastAPI()
75
-
76
- # Add CORS middleware to allow requests from the frontend
77
- app.add_middleware(
78
- CORSMiddleware,
79
- allow_origins=["*"],
80
- allow_credentials=True,
81
- allow_methods=["*"],
82
- allow_headers=["*"],
83
- )
84
-
85
- # Rate limiter for API endpoints
86
- class RateLimiter:
87
- def __init__(self, interval_seconds=2):
88
- self.interval = interval_seconds
89
- self.last_request_time = {}
90
- self.lock = threading.Lock()
91
-
92
- def allow_request(self, endpoint):
93
- with self.lock:
94
- current_time = time.time()
95
- # Priority endpoints always allowed
96
- if endpoint in ["/update_params", "/update_message"]:
97
- return True
98
-
99
- # For other endpoints, apply rate limiting
100
- if endpoint not in self.last_request_time:
101
- self.last_request_time[endpoint] = current_time
102
- return True
103
-
104
- elapsed = current_time - self.last_request_time[endpoint]
105
- if elapsed < self.interval:
106
- return False
107
-
108
- self.last_request_time[endpoint] = current_time
109
- return True
110
-
111
-
112
- def log_ootb_request(server_url, ootb_request_type, data):
113
- logging.info(f"OOTB Request: Type={ootb_request_type}, Data={data}")
114
- # Keep the requests post for now if it serves a specific purpose
115
- logging_data = {
116
- "type": ootb_request_type,
117
- "data": data,
118
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
119
- }
120
- if not server_url.endswith("/update_ootb_logging"):
121
- server_logging_url = server_url + "/update_ootb_logging"
122
- else:
123
- server_logging_url = server_url
124
- try:
125
- requests.post(server_logging_url, json=logging_data, timeout=5)
126
- except Exception as req_log_e:
127
- logging.warning(f"Could not log ootb request to server {server_logging_url}: {req_log_e}")
128
-
129
-
130
- class SharedState:
131
- def __init__(self, args):
132
- self.args = args
133
- self.task_updated = False
134
- self.chatbot_messages = []
135
- # Store all state-related data here
136
- self.model = args.model
137
- self.task = getattr(args, 'task', "")
138
- self.selected_screen = args.selected_screen
139
- self.user_id = args.user_id
140
- self.trace_id = args.trace_id
141
- self.api_keys = args.api_keys
142
- self.server_url = args.server_url
143
- self.message_queue = []
144
- self.is_processing = False
145
- self.should_stop = False
146
- self.is_paused = False
147
- self.full_screen_game_mode = getattr(args, 'full_screen_game_mode', 0)
148
- # Add a new event to better control stopping
149
- self.stop_event = threading.Event()
150
- # Add a reference to the processing thread
151
- self.processing_thread = None
152
- self.max_steps = getattr(args, 'max_steps', 50)
153
-
154
- shared_state = None
155
- rate_limiter = RateLimiter(interval_seconds=2)
156
-
157
- # Set up logging for this module
158
- log = logging.getLogger(__name__)
159
-
160
- def prepare_environment(state):
161
- """Dynamically loads and runs preparation logic based on software name."""
162
- # Determine software name from state (user_id, trace_id, or task)
163
- software_name = ""
164
-
165
- # Check user_id first
166
- user_id = getattr(state, 'user_id', '').lower()
167
- task = getattr(state, 'task', '').lower()
168
- trace_id = getattr(state, 'trace_id', '').lower()
169
-
170
- log.info(f"Checking for software in: user_id='{user_id}', trace_id='{trace_id}', task='{task}'")
171
-
172
- # Look for known software indicators
173
- if "star rail" in user_id or "star rail" in trace_id:
174
- software_name = "star rail"
175
- elif "powerpoint" in user_id or "powerpoint" in trace_id or "powerpoint" in task:
176
- software_name = "powerpoint"
177
- elif "word" in user_id or "word" in trace_id or "word" in task:
178
- software_name = "word"
179
- elif "excel" in user_id or "excel" in trace_id or "excel" in task:
180
- software_name = "excel"
181
- elif "premiere" in user_id or "premiere" in trace_id or "premiere" in task or \
182
- "pr" in user_id or "pr" in trace_id or "pr" in task: # Check for 'premiere' or 'pr'
183
- software_name = "pr" # Module name will be pr_prepare
184
- # Add more software checks here as needed
185
-
186
- # If no specific software found, check task for keywords
187
- if not software_name:
188
- log.info("No specific software detected from IDs or task content")
189
-
190
- if not software_name:
191
- log.info("No specific software preparation identified. Skipping preparation.")
192
- return
193
-
194
- log.info(f"Identified software for preparation: '{software_name}'")
195
-
196
- # Normalize the software name to be a valid Python module name
197
- # Replace spaces/hyphens with underscores, convert to lowercase
198
- module_name_base = software_name.replace(" ", "_").replace("-", "_").lower()
199
- module_to_run = f"{module_name_base}_prepare"
200
-
201
- log.info(f"Attempting preparation for software: '{software_name}' (Module: '{module_to_run}')")
202
-
203
- try:
204
- # Construct the full module path within the package
205
- prep_package = "computer_use_ootb_internal.preparation"
206
- full_module_path = f"{prep_package}.{module_to_run}"
207
-
208
- # Dynamically import the module
209
- # Check if module exists first using pkgutil to avoid import errors
210
- log.debug(f"Looking for preparation module: {full_module_path}")
211
- loader = pkgutil.find_loader(full_module_path)
212
- if loader is None:
213
- log.warning(f"Preparation module '{full_module_path}' not found. Skipping preparation.")
214
- return
215
-
216
- log.debug(f"Importing preparation module: {full_module_path}")
217
- prep_module = importlib.import_module(full_module_path)
218
-
219
- # Check if the module has the expected function
220
- if hasattr(prep_module, "run_preparation") and callable(prep_module.run_preparation):
221
- log.info(f"Running preparation function from {full_module_path}...")
222
- prep_module.run_preparation(state)
223
- log.info(f"Preparation function from {full_module_path} completed.")
224
- else:
225
- log.warning(f"Module {full_module_path} found, but does not have a callable 'run_preparation' function. Skipping.")
226
-
227
- except ModuleNotFoundError:
228
- log.warning(f"Preparation module '{full_module_path}' not found. Skipping preparation.")
229
- except Exception as e:
230
- log.error(f"Error during dynamic preparation loading/execution for '{module_to_run}': {e}", exc_info=True)
231
-
232
-
233
- @app.post("/update_params")
234
- async def update_parameters(request: Request):
235
- logging.info("Received request to /update_params")
236
- try:
237
- data = await request.json()
238
-
239
- if 'task' not in data:
240
- return JSONResponse(
241
- content={"status": "error", "message": "Missing required field: task"},
242
- status_code=400
243
- )
244
-
245
- # Clear message histories before updating parameters
246
- shared_state.message_queue.clear()
247
- shared_state.chatbot_messages.clear()
248
- logging.info("Cleared message queue and chatbot messages.")
249
-
250
- shared_state.args = argparse.Namespace(**data)
251
- shared_state.task_updated = True
252
-
253
- # Update shared state when parameters change
254
- shared_state.model = getattr(shared_state.args, 'model', "teach-mode-gpt-4o")
255
- shared_state.task = getattr(shared_state.args, 'task', "Following the instructions to complete the task.")
256
- shared_state.selected_screen = getattr(shared_state.args, 'selected_screen', 0)
257
- shared_state.user_id = getattr(shared_state.args, 'user_id', "hero_cases")
258
- shared_state.trace_id = getattr(shared_state.args, 'trace_id', "build_scroll_combat")
259
- shared_state.api_keys = getattr(shared_state.args, 'api_keys', "sk-proj-1234567890")
260
- shared_state.server_url = getattr(shared_state.args, 'server_url', "http://ec2-44-234-43-86.us-west-2.compute.amazonaws.com")
261
- shared_state.max_steps = getattr(shared_state.args, 'max_steps', 50)
262
-
263
- log_ootb_request(shared_state.server_url, "update_params", data)
264
-
265
- # Call the (now dynamic) preparation function here, after parameters are updated
266
- prepare_environment(shared_state)
267
-
268
- logging.info("Parameters updated successfully.")
269
- return JSONResponse(
270
- content={"status": "success", "message": "Parameters updated", "new_args": vars(shared_state.args)},
271
- status_code=200
272
- )
273
- except Exception as e:
274
- logging.error("Error processing /update_params:", exc_info=True)
275
- return JSONResponse(content={"status": "error", "message": "Internal server error"}, status_code=500)
276
-
277
- @app.post("/update_message")
278
- async def update_message(request: Request):
279
- data = await request.json()
280
-
281
- if 'message' not in data:
282
- return JSONResponse(
283
- content={"status": "error", "message": "Missing required field: message"},
284
- status_code=400
285
- )
286
-
287
- log_ootb_request(shared_state.server_url, "update_message", data)
288
-
289
- message = data['message']
290
- full_screen_game_mode = data.get('full_screen_game_mode', 0) # Default to 0 if not provided
291
-
292
- # shared_state.chatbot_messages.append({"role": "user", "content": message, "type": "text"})
293
- shared_state.task = message
294
- shared_state.args.task = message
295
- shared_state.full_screen_game_mode = full_screen_game_mode
296
-
297
- # Reset stop event before starting
298
- shared_state.stop_event.clear()
299
-
300
- # Start processing if not already running
301
- if not shared_state.is_processing:
302
- # Create and store the thread
303
- shared_state.processing_thread = threading.Thread(target=process_input, daemon=True)
304
- shared_state.processing_thread.start()
305
-
306
- return JSONResponse(
307
- content={"status": "success", "message": "Message received", "task": shared_state.task},
308
- status_code=200
309
- )
310
-
311
- @app.get("/get_messages")
312
- async def get_messages(request: Request):
313
- # Apply rate limiting
314
- if not rate_limiter.allow_request(request.url.path):
315
- return JSONResponse(
316
- content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
317
- status_code=429
318
- )
319
-
320
- # log_ootb_request(shared_state.server_url, "get_messages", {})
321
-
322
- # Return all messages in the queue and clear it
323
- messages = shared_state.message_queue.copy()
324
- shared_state.message_queue = []
325
-
326
- return JSONResponse(
327
- content={"status": "success", "messages": messages},
328
- status_code=200
329
- )
330
-
331
- @app.get("/get_screens")
332
- async def get_screens(request: Request):
333
- # Apply rate limiting
334
- if not rate_limiter.allow_request(request.url.path):
335
- return JSONResponse(
336
- content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
337
- status_code=429
338
- )
339
-
340
- log_ootb_request(shared_state.server_url, "get_screens", {})
341
-
342
- screen_options, primary_index = get_screen_details()
343
-
344
- return JSONResponse(
345
- content={"status": "success", "screens": screen_options, "primary_index": primary_index},
346
- status_code=200
347
- )
348
-
349
- @app.post("/stop_processing")
350
- async def stop_processing(request: Request):
351
- # Apply rate limiting
352
- if not rate_limiter.allow_request(request.url.path):
353
- return JSONResponse(
354
- content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
355
- status_code=429
356
- )
357
-
358
- log_ootb_request(shared_state.server_url, "stop_processing", {})
359
-
360
- if shared_state.is_processing:
361
- # Set both flags to ensure stopping the current task
362
- shared_state.should_stop = True
363
- shared_state.stop_event.set()
364
-
365
- # Clear message histories
366
- shared_state.message_queue.clear()
367
- shared_state.chatbot_messages.clear()
368
- logging.info("Cleared message queue and chatbot messages during stop.")
369
-
370
- # Send an immediate message to the queue to inform the user
371
- stop_initiated_msg = {"role": "assistant", "content": f"Stopping task '{shared_state.task}'...", "type": "text", "action_type": ""}
372
- # Append the stop message AFTER clearing, so it's the only one left
373
- shared_state.message_queue.append(stop_initiated_msg)
374
- shared_state.chatbot_messages.append(stop_initiated_msg)
375
-
376
- return JSONResponse(
377
- content={"status": "success", "message": "Task is being stopped, server will remain available for new tasks"},
378
- status_code=200
379
- )
380
- else:
381
- # Clear message histories even if not processing, to ensure clean state
382
- shared_state.message_queue.clear()
383
- shared_state.chatbot_messages.clear()
384
- logging.info("Cleared message queue and chatbot messages (no active process to stop).")
385
- return JSONResponse(
386
- content={"status": "error", "message": "No active processing to stop"},
387
- status_code=400
388
- )
389
-
390
- @app.post("/toggle_pause")
391
- async def toggle_pause(request: Request):
392
- # Apply rate limiting
393
- if not rate_limiter.allow_request(request.url.path):
394
- return JSONResponse(
395
- content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
396
- status_code=429
397
- )
398
-
399
- log_ootb_request(shared_state.server_url, "toggle_pause", {})
400
-
401
- if not shared_state.is_processing:
402
- return JSONResponse(
403
- content={"status": "error", "message": "No active processing to pause/resume"},
404
- status_code=400
405
- )
406
-
407
- # Toggle the pause state
408
- shared_state.is_paused = not shared_state.is_paused
409
- current_state = shared_state.is_paused
410
-
411
- print(f"Toggled pause state to: {current_state}")
412
-
413
- status_message = "paused" if current_state else "resumed"
414
-
415
- # Add a message to the queue to inform the user
416
- if current_state:
417
- message = {"role": "assistant", "content": f"Task '{shared_state.task}' has been paused. Click Continue to resume.", "type": "text", "action_type": ""}
418
- else:
419
- message = {"role": "assistant", "content": f"Task '{shared_state.task}' has been resumed.", "type": "text", "action_type": ""}
420
-
421
- shared_state.chatbot_messages.append(message)
422
- shared_state.message_queue.append(message)
423
-
424
- return JSONResponse(
425
- content={
426
- "status": "success",
427
- "message": f"Processing {status_message}",
428
- "is_paused": current_state
429
- },
430
- status_code=200
431
- )
432
-
433
- @app.get("/status")
434
- async def get_status(request: Request):
435
- # Apply rate limiting
436
- if not rate_limiter.allow_request(request.url.path):
437
- return JSONResponse(
438
- content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
439
- status_code=429
440
- )
441
-
442
- # log_ootb_request(shared_state.server_url, "get_status", {})
443
-
444
- print(f"Status check - Processing: {shared_state.is_processing}, Paused: {shared_state.is_paused}")
445
- return JSONResponse(
446
- content={
447
- "status": "success",
448
- "is_processing": shared_state.is_processing,
449
- "is_paused": shared_state.is_paused
450
- },
451
- status_code=200
452
- )
453
-
454
- @app.post("/exec_computer_tool")
455
- async def exec_computer_tool(request: Request):
456
- logging.info("Received request to /exec_computer_tool")
457
- try:
458
- data = await request.json()
459
-
460
- # Extract parameters from the request
461
- selected_screen = data.get('selected_screen', 0)
462
- full_screen_game_mode = data.get('full_screen_game_mode', 0)
463
- response = data.get('response', {})
464
-
465
- logging.info(f"Executing TeachmodeExecutor with: screen={selected_screen}, mode={full_screen_game_mode}, response={response}")
466
-
467
- # Create TeachmodeExecutor in a separate process to avoid event loop conflicts
468
- # Since TeachmodeExecutor uses asyncio.run() internally, we need to run it in a way
469
- # that doesn't conflict with FastAPI's event loop
470
-
471
- def run_executor():
472
- executor = TeachmodeExecutor(
473
- selected_screen=selected_screen,
474
- full_screen_game_mode=full_screen_game_mode
475
- )
476
-
477
- results = []
478
- try:
479
- for action_result in executor(response):
480
- results.append(action_result)
481
- except Exception as exec_error:
482
- logging.error(f"Error executing action: {exec_error}", exc_info=True)
483
- return {"error": str(exec_error)}
484
-
485
- return results
486
-
487
- # Execute in a thread pool to avoid blocking the event loop
488
- with concurrent.futures.ThreadPoolExecutor() as pool:
489
- results = await asyncio.get_event_loop().run_in_executor(pool, run_executor)
490
-
491
- if isinstance(results, dict) and "error" in results:
492
- return JSONResponse(
493
- content={"status": "error", "message": results["error"]},
494
- status_code=500
495
- )
496
-
497
- logging.info(f"Action results: {results}")
498
-
499
- return JSONResponse(
500
- content={"status": "success", "results": results},
501
- status_code=200
502
- )
503
- except Exception as e:
504
- logging.error("Error processing /exec_computer_tool:", exc_info=True)
505
- return JSONResponse(
506
- content={"status": "error", "message": f"Internal server error: {str(e)}"},
507
- status_code=500
508
- )
509
-
510
- def process_input():
511
- global shared_state
512
- logging.info("process_input thread started.")
513
- shared_state.is_processing = True
514
- shared_state.should_stop = False
515
- shared_state.is_paused = False
516
- shared_state.stop_event.clear() # Ensure stop event is cleared at the start
517
-
518
- print(f"start sampling loop: {shared_state.chatbot_messages}")
519
- print(f"shared_state.args before sampling loop: {shared_state.args}")
520
-
521
-
522
- try:
523
- # Get the generator for the sampling loop
524
- sampling_loop = simple_teachmode_sampling_loop(
525
- model=shared_state.model,
526
- task=shared_state.task,
527
- selected_screen=shared_state.selected_screen,
528
- user_id=shared_state.user_id,
529
- trace_id=shared_state.trace_id,
530
- api_keys=shared_state.api_keys,
531
- server_url=shared_state.server_url,
532
- full_screen_game_mode=shared_state.full_screen_game_mode,
533
- max_steps=shared_state.max_steps,
534
- )
535
-
536
- # Process messages from the sampling loop
537
- for loop_msg in sampling_loop:
538
- # Check stop condition more frequently
539
- if shared_state.should_stop or shared_state.stop_event.is_set():
540
- print("Processing stopped by user")
541
- break
542
-
543
- # Check if paused and wait while paused
544
- while shared_state.is_paused and not shared_state.should_stop and not shared_state.stop_event.is_set():
545
- print(f"Processing paused at: {time.strftime('%H:%M:%S')}")
546
- # Wait a short time and check stop condition regularly
547
- for _ in range(5): # Check 5 times per second
548
- if shared_state.should_stop or shared_state.stop_event.is_set():
549
- break
550
- time.sleep(0.2)
551
-
552
- # Check again after pause loop
553
- if shared_state.should_stop or shared_state.stop_event.is_set():
554
- print("Processing stopped while paused or resuming")
555
- break
556
-
557
- shared_state.chatbot_messages.append(loop_msg)
558
- shared_state.message_queue.append(loop_msg)
559
-
560
- # Short sleep to allow stop signals to be processed
561
- for _ in range(5): # Check 5 times per second
562
- if shared_state.should_stop or shared_state.stop_event.is_set():
563
- print("Processing stopped during sleep")
564
- break
565
- time.sleep(0.1)
566
-
567
- if shared_state.should_stop or shared_state.stop_event.is_set():
568
- break
569
-
570
- except Exception as e:
571
- # Handle any exceptions in the processing loop
572
- error_msg = f"Error during task processing: {e}"
573
- print(error_msg)
574
- error_message = {"role": "assistant", "content": error_msg, "type": "error", "action_type": ""}
575
- shared_state.message_queue.append(error_message)
576
-
577
- finally:
578
- # Handle completion or interruption
579
- if shared_state.should_stop or shared_state.stop_event.is_set():
580
- stop_msg = f"Task '{shared_state.task}' was stopped. Ready for new tasks."
581
- final_message = {"role": "assistant", "content": stop_msg, "type": "text", "action_type": ""}
582
- else:
583
- complete_msg = f"Task '{shared_state.task}' completed. Thanks for using Marbot Run."
584
- final_message = {"role": "assistant", "content": complete_msg, "type": "text", "action_type": ""}
585
-
586
- shared_state.chatbot_messages.append(final_message)
587
- shared_state.message_queue.append(final_message)
588
-
589
- # Reset all state flags to allow for new tasks
590
- shared_state.is_processing = False
591
- shared_state.should_stop = False
592
- shared_state.is_paused = False
593
- shared_state.stop_event.clear()
594
- print("Processing completed, ready for new tasks")
595
- logging.info("process_input thread finished.")
596
-
597
- def main():
598
- # Logging is set up at the top level now
599
- logging.info("App main() function starting setup.")
600
- global app, shared_state, rate_limiter # Ensure app is global if needed by uvicorn
601
- parser = argparse.ArgumentParser()
602
- # Add arguments, but NOT host and port
603
- parser.add_argument("--model", type=str, default="teach-mode-gpt-4o", help="Model name")
604
- parser.add_argument("--task", type=str, default="Following the instructions to complete the task.", help="Initial task description")
605
- parser.add_argument("--selected_screen", type=int, default=0, help="Selected screen index")
606
- parser.add_argument("--user_id", type=str, default="hero_cases", help="User ID for the session")
607
- parser.add_argument("--trace_id", type=str, default="build_scroll_combat", help="Trace ID for the session")
608
- parser.add_argument("--api_keys", type=str, default="sk-proj-1234567890", help="API keys")
609
- parser.add_argument("--server_url", type=str, default="http://ec2-44-234-43-86.us-west-2.compute.amazonaws.com", help="Server URL for the session")
610
-
611
- args = parser.parse_args()
612
-
613
- # Validate args or set defaults if needed (keep these)
614
- if not hasattr(args, 'model'): args.model = "default_model"
615
- if not hasattr(args, 'task'): args.task = "default_task"
616
- if not hasattr(args, 'selected_screen'): args.selected_screen = 0
617
- if not hasattr(args, 'user_id'): args.user_id = "unknown_user"
618
- if not hasattr(args, 'trace_id'): args.trace_id = "unknown_trace"
619
- if not hasattr(args, 'api_keys'): args.api_keys = "none"
620
- if not hasattr(args, 'server_url'): args.server_url = "none"
621
-
622
- shared_state = SharedState(args)
623
- rate_limiter = RateLimiter(interval_seconds=2) # Re-initialize rate limiter
624
- logging.info(f"Shared state initialized for user: {args.user_id}")
625
-
626
- # --- Restore original port calculation logic ---
627
- port = 7888 # Default port
628
- host = "0.0.0.0" # Listen on all interfaces
629
-
630
- if platform.system() == "Windows":
631
- try:
632
- username = os.environ["USERNAME"].lower()
633
- logging.info(f"Determining port based on Windows username: {username}")
634
- if username == "altair":
635
- port = 14000
636
- elif username.startswith("guest") and username[5:].isdigit():
637
- num = int(username[5:])
638
- if 1 <= num <= 10: # Assuming max 10 guests for this range
639
- port = 14000 + num
640
- else:
641
- logging.warning(f"Guest user number {num} out of range (1-10), using default port {port}.")
642
- else:
643
- logging.info(f"Username '{username}' doesn't match specific rules, using default port {port}.")
644
- except Exception as e:
645
- logging.error(f"Error determining port from username: {e}. Using default port {port}.", exc_info=True)
646
- else:
647
- logging.info(f"Not running on Windows, using default port {port}.")
648
- # --- End of restored port calculation ---
649
-
650
- logging.info(f"Final Host={host}, Port={port}")
651
-
652
- try:
653
- logging.info(f"Starting Uvicorn server on {host}:{port}")
654
- # Use the calculated port and specific host
655
- uvicorn.run(app, host=host, port=port)
656
- logging.info("Uvicorn server stopped.")
657
- except Exception as main_e:
658
- logging.error("Error in main execution:", exc_info=True)
659
- finally:
660
- logging.info("App main() function finished.")
661
-
662
- if __name__ == "__main__":
663
- main()
664
-
665
- # Test log_ootb_request
1
+ import argparse
2
+ import time
3
+ import json
4
+ from datetime import datetime
5
+ import threading
6
+ import requests
7
+ import platform # Add platform import
8
+ import pyautogui # Add pyautogui import
9
+ import webbrowser # Add webbrowser import
10
+ import os # Import os for path joining
11
+ import logging # Import logging
12
+ import importlib # For dynamic imports
13
+ import pkgutil # To find modules
14
+ import sys # For logging setup
15
+ import traceback # For logging setup
16
+ from logging.handlers import RotatingFileHandler # For logging setup
17
+ from fastapi import FastAPI, Request
18
+ from fastapi.responses import JSONResponse
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from computer_use_ootb_internal.computer_use_demo.tools.computer import get_screen_details
21
+ from computer_use_ootb_internal.run_teachmode_ootb_args import simple_teachmode_sampling_loop
22
+ from computer_use_ootb_internal.computer_use_demo.executor.teachmode_executor import TeachmodeExecutor
23
+ import uvicorn # Assuming uvicorn is used to run FastAPI
24
+ import concurrent.futures
25
+ import asyncio
26
+
27
+ # --- App Logging Setup ---
28
+ try:
29
+ # Log to user's AppData directory for better accessibility
30
+ log_dir_base = os.environ.get('APPDATA', os.path.expanduser('~'))
31
+ log_dir = os.path.join(log_dir_base, 'OOTBAppLogs')
32
+ os.makedirs(log_dir, exist_ok=True)
33
+ log_file = os.path.join(log_dir, 'ootb_app.log')
34
+
35
+ log_format = '%(asctime)s - %(levelname)s - %(process)d - %(threadName)s - %(message)s'
36
+ log_level = logging.INFO # Or logging.DEBUG for more detail
37
+
38
+ # Use rotating file handler
39
+ handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=2, encoding='utf-8')
40
+ handler.setFormatter(logging.Formatter(log_format))
41
+
42
+ # Configure root logger
43
+ logging.basicConfig(level=log_level, handlers=[handler])
44
+
45
+ # Add stream handler to see logs if running interactively (optional)
46
+ # logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
47
+
48
+ logging.info("="*20 + " OOTB App Starting " + "="*20)
49
+ logging.info(f"Running with args: {sys.argv}")
50
+ logging.info(f"Python Executable: {sys.executable}")
51
+ logging.info(f"Working Directory: {os.getcwd()}")
52
+ logging.info(f"User: {os.getenv('USERNAME')}")
53
+
54
+ except Exception as log_setup_e:
55
+ print(f"FATAL: Failed to set up logging: {log_setup_e}")
56
+ # Fallback logging might be needed here if file logging fails
57
+
58
+ # --- Get the root logger ---
59
+ root_logger = logging.getLogger()
60
+ root_logger.setLevel(log_level) # Ensure root logger level is set
61
+
62
+ # --- File Handler (as before) ---
63
+ file_handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=2, encoding='utf-8')
64
+ file_handler.setFormatter(logging.Formatter(log_format))
65
+ root_logger.addHandler(file_handler)
66
+
67
+ # --- Console Handler (New) ---
68
+ console_handler = logging.StreamHandler(sys.stdout) # Log to standard output
69
+ console_handler.setFormatter(logging.Formatter(log_format))
70
+ root_logger.addHandler(console_handler)
71
+
72
+ # --- End App Logging Setup ---
73
+
74
+ app = FastAPI()
75
+
76
+ # Add CORS middleware to allow requests from the frontend
77
+ app.add_middleware(
78
+ CORSMiddleware,
79
+ allow_origins=["*"],
80
+ allow_credentials=True,
81
+ allow_methods=["*"],
82
+ allow_headers=["*"],
83
+ )
84
+
85
+ # Rate limiter for API endpoints
86
+ class RateLimiter:
87
+ def __init__(self, interval_seconds=2):
88
+ self.interval = interval_seconds
89
+ self.last_request_time = {}
90
+ self.lock = threading.Lock()
91
+
92
+ def allow_request(self, endpoint):
93
+ with self.lock:
94
+ current_time = time.time()
95
+ # Priority endpoints always allowed
96
+ if endpoint in ["/update_params", "/update_message"]:
97
+ return True
98
+
99
+ # For other endpoints, apply rate limiting
100
+ if endpoint not in self.last_request_time:
101
+ self.last_request_time[endpoint] = current_time
102
+ return True
103
+
104
+ elapsed = current_time - self.last_request_time[endpoint]
105
+ if elapsed < self.interval:
106
+ return False
107
+
108
+ self.last_request_time[endpoint] = current_time
109
+ return True
110
+
111
+
112
+ def log_ootb_request(server_url, ootb_request_type, data):
113
+ logging.info(f"OOTB Request: Type={ootb_request_type}, Data={data}")
114
+ # Keep the requests post for now if it serves a specific purpose
115
+ logging_data = {
116
+ "type": ootb_request_type,
117
+ "data": data,
118
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
119
+ }
120
+ if not server_url.endswith("/update_ootb_logging"):
121
+ server_logging_url = server_url + "/update_ootb_logging"
122
+ else:
123
+ server_logging_url = server_url
124
+ try:
125
+ requests.post(server_logging_url, json=logging_data, timeout=5)
126
+ except Exception as req_log_e:
127
+ logging.warning(f"Could not log ootb request to server {server_logging_url}: {req_log_e}")
128
+
129
+
130
+ class SharedState:
131
+ def __init__(self, args):
132
+ self.args = args
133
+ self.task_updated = False
134
+ self.chatbot_messages = []
135
+ # Store all state-related data here
136
+ self.model = args.model
137
+ self.task = getattr(args, 'task', "")
138
+ self.selected_screen = args.selected_screen
139
+ self.user_id = args.user_id
140
+ self.trace_id = args.trace_id
141
+ self.api_keys = args.api_keys
142
+ self.server_url = args.server_url
143
+ self.message_queue = []
144
+ self.is_processing = False
145
+ self.should_stop = False
146
+ self.is_paused = False
147
+ self.full_screen_game_mode = getattr(args, 'full_screen_game_mode', 0)
148
+ # Add a new event to better control stopping
149
+ self.stop_event = threading.Event()
150
+ # Add a reference to the processing thread
151
+ self.processing_thread = None
152
+ self.max_steps = getattr(args, 'max_steps', 50)
153
+
154
+ shared_state = None
155
+ rate_limiter = RateLimiter(interval_seconds=2)
156
+
157
+ # Set up logging for this module
158
+ log = logging.getLogger(__name__)
159
+
160
+ def prepare_environment(state):
161
+ """Dynamically loads and runs preparation logic based on software name."""
162
+ # Determine software name from state (user_id, trace_id, or task)
163
+ software_name = ""
164
+
165
+ # Check user_id first
166
+ user_id = getattr(state, 'user_id', '').lower()
167
+ task = getattr(state, 'task', '').lower()
168
+ trace_id = getattr(state, 'trace_id', '').lower()
169
+
170
+ log.info(f"Checking for software in: user_id='{user_id}', trace_id='{trace_id}', task='{task}'")
171
+
172
+ # Look for known software indicators
173
+ if "star rail" in user_id or "star rail" in trace_id:
174
+ software_name = "star rail"
175
+ elif "powerpoint" in user_id or "powerpoint" in trace_id or "powerpoint" in task:
176
+ software_name = "powerpoint"
177
+ elif "word" in user_id or "word" in trace_id or "word" in task:
178
+ software_name = "word"
179
+ elif "excel" in user_id or "excel" in trace_id or "excel" in task:
180
+ software_name = "excel"
181
+ elif "premiere" in user_id or "premiere" in trace_id or "premiere" in task or \
182
+ "pr" in user_id or "pr" in trace_id or "pr" in task: # Check for 'premiere' or 'pr'
183
+ software_name = "pr" # Module name will be pr_prepare
184
+ # Add more software checks here as needed
185
+
186
+ # If no specific software found, check task for keywords
187
+ if not software_name:
188
+ log.info("No specific software detected from IDs or task content")
189
+
190
+ if not software_name:
191
+ log.info("No specific software preparation identified. Skipping preparation.")
192
+ return
193
+
194
+ log.info(f"Identified software for preparation: '{software_name}'")
195
+
196
+ # Normalize the software name to be a valid Python module name
197
+ # Replace spaces/hyphens with underscores, convert to lowercase
198
+ module_name_base = software_name.replace(" ", "_").replace("-", "_").lower()
199
+ module_to_run = f"{module_name_base}_prepare"
200
+
201
+ log.info(f"Attempting preparation for software: '{software_name}' (Module: '{module_to_run}')")
202
+
203
+ try:
204
+ # Construct the full module path within the package
205
+ prep_package = "computer_use_ootb_internal.preparation"
206
+ full_module_path = f"{prep_package}.{module_to_run}"
207
+
208
+ # Dynamically import the module
209
+ # Check if module exists first using pkgutil to avoid import errors
210
+ log.debug(f"Looking for preparation module: {full_module_path}")
211
+ loader = pkgutil.find_loader(full_module_path)
212
+ if loader is None:
213
+ log.warning(f"Preparation module '{full_module_path}' not found. Skipping preparation.")
214
+ return
215
+
216
+ log.debug(f"Importing preparation module: {full_module_path}")
217
+ prep_module = importlib.import_module(full_module_path)
218
+
219
+ # Check if the module has the expected function
220
+ if hasattr(prep_module, "run_preparation") and callable(prep_module.run_preparation):
221
+ log.info(f"Running preparation function from {full_module_path}...")
222
+ prep_module.run_preparation(state)
223
+ log.info(f"Preparation function from {full_module_path} completed.")
224
+ else:
225
+ log.warning(f"Module {full_module_path} found, but does not have a callable 'run_preparation' function. Skipping.")
226
+
227
+ except ModuleNotFoundError:
228
+ log.warning(f"Preparation module '{full_module_path}' not found. Skipping preparation.")
229
+ except Exception as e:
230
+ log.error(f"Error during dynamic preparation loading/execution for '{module_to_run}': {e}", exc_info=True)
231
+
232
+
233
+ @app.post("/update_params")
234
+ async def update_parameters(request: Request):
235
+ logging.info("Received request to /update_params")
236
+ try:
237
+ data = await request.json()
238
+
239
+ if 'task' not in data:
240
+ return JSONResponse(
241
+ content={"status": "error", "message": "Missing required field: task"},
242
+ status_code=400
243
+ )
244
+
245
+ # Clear message histories before updating parameters
246
+ shared_state.message_queue.clear()
247
+ shared_state.chatbot_messages.clear()
248
+ logging.info("Cleared message queue and chatbot messages.")
249
+
250
+ shared_state.args = argparse.Namespace(**data)
251
+ shared_state.task_updated = True
252
+
253
+ # Update shared state when parameters change
254
+ shared_state.model = getattr(shared_state.args, 'model', "teach-mode-gpt-4o")
255
+ shared_state.task = getattr(shared_state.args, 'task', "Following the instructions to complete the task.")
256
+ shared_state.selected_screen = getattr(shared_state.args, 'selected_screen', 0)
257
+ shared_state.user_id = getattr(shared_state.args, 'user_id', "hero_cases")
258
+ shared_state.trace_id = getattr(shared_state.args, 'trace_id', "build_scroll_combat")
259
+ shared_state.api_keys = getattr(shared_state.args, 'api_keys', "sk-proj-1234567890")
260
+ shared_state.server_url = getattr(shared_state.args, 'server_url', "http://ec2-44-234-43-86.us-west-2.compute.amazonaws.com")
261
+ shared_state.max_steps = getattr(shared_state.args, 'max_steps', 50)
262
+
263
+ log_ootb_request(shared_state.server_url, "update_params", data)
264
+
265
+ # Call the (now dynamic) preparation function here, after parameters are updated
266
+ prepare_environment(shared_state)
267
+
268
+ logging.info("Parameters updated successfully.")
269
+ return JSONResponse(
270
+ content={"status": "success", "message": "Parameters updated", "new_args": vars(shared_state.args)},
271
+ status_code=200
272
+ )
273
+ except Exception as e:
274
+ logging.error("Error processing /update_params:", exc_info=True)
275
+ return JSONResponse(content={"status": "error", "message": "Internal server error"}, status_code=500)
276
+
277
+ @app.post("/update_message")
278
+ async def update_message(request: Request):
279
+ data = await request.json()
280
+
281
+ if 'message' not in data:
282
+ return JSONResponse(
283
+ content={"status": "error", "message": "Missing required field: message"},
284
+ status_code=400
285
+ )
286
+
287
+ log_ootb_request(shared_state.server_url, "update_message", data)
288
+
289
+ message = data['message']
290
+
291
+ # shared_state.chatbot_messages.append({"role": "user", "content": message, "type": "text"})
292
+ shared_state.task = message
293
+ shared_state.args.task = message
294
+
295
+ # TODO: adaptively change full_screen_game_mode
296
+ # full_screen_game_mode = data.get('full_screen_game_mode', 0) # Default to 0 if not provided
297
+ # shared_state.full_screen_game_mode = full_screen_game_mode
298
+
299
+ # Reset stop event before starting
300
+ shared_state.stop_event.clear()
301
+
302
+ # Start processing if not already running
303
+ if not shared_state.is_processing:
304
+ # Create and store the thread
305
+ shared_state.processing_thread = threading.Thread(target=process_input, daemon=True)
306
+ shared_state.processing_thread.start()
307
+
308
+ return JSONResponse(
309
+ content={"status": "success", "message": "Message received", "task": shared_state.task},
310
+ status_code=200
311
+ )
312
+
313
+ @app.get("/get_messages")
314
+ async def get_messages(request: Request):
315
+ # Apply rate limiting
316
+ if not rate_limiter.allow_request(request.url.path):
317
+ return JSONResponse(
318
+ content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
319
+ status_code=429
320
+ )
321
+
322
+ # log_ootb_request(shared_state.server_url, "get_messages", {})
323
+
324
+ # Return all messages in the queue and clear it
325
+ messages = shared_state.message_queue.copy()
326
+ shared_state.message_queue = []
327
+
328
+ return JSONResponse(
329
+ content={"status": "success", "messages": messages},
330
+ status_code=200
331
+ )
332
+
333
+ @app.get("/get_screens")
334
+ async def get_screens(request: Request):
335
+ # Apply rate limiting
336
+ if not rate_limiter.allow_request(request.url.path):
337
+ return JSONResponse(
338
+ content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
339
+ status_code=429
340
+ )
341
+
342
+ log_ootb_request(shared_state.server_url, "get_screens", {})
343
+
344
+ screen_options, primary_index = get_screen_details()
345
+
346
+ return JSONResponse(
347
+ content={"status": "success", "screens": screen_options, "primary_index": primary_index},
348
+ status_code=200
349
+ )
350
+
351
+ @app.post("/stop_processing")
352
+ async def stop_processing(request: Request):
353
+ # Apply rate limiting
354
+ if not rate_limiter.allow_request(request.url.path):
355
+ return JSONResponse(
356
+ content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
357
+ status_code=429
358
+ )
359
+
360
+ log_ootb_request(shared_state.server_url, "stop_processing", {})
361
+
362
+ if shared_state.is_processing:
363
+ # Set both flags to ensure stopping the current task
364
+ shared_state.should_stop = True
365
+ shared_state.stop_event.set()
366
+
367
+ # Clear message histories
368
+ shared_state.message_queue.clear()
369
+ shared_state.chatbot_messages.clear()
370
+ logging.info("Cleared message queue and chatbot messages during stop.")
371
+
372
+ # Send an immediate message to the queue to inform the user
373
+ stop_initiated_msg = {"role": "assistant", "content": f"Stopping task '{shared_state.task}'...", "type": "text", "action_type": ""}
374
+ # Append the stop message AFTER clearing, so it's the only one left
375
+ shared_state.message_queue.append(stop_initiated_msg)
376
+ shared_state.chatbot_messages.append(stop_initiated_msg)
377
+
378
+ return JSONResponse(
379
+ content={"status": "success", "message": "Task is being stopped, server will remain available for new tasks"},
380
+ status_code=200
381
+ )
382
+ else:
383
+ # Clear message histories even if not processing, to ensure clean state
384
+ shared_state.message_queue.clear()
385
+ shared_state.chatbot_messages.clear()
386
+ logging.info("Cleared message queue and chatbot messages (no active process to stop).")
387
+ return JSONResponse(
388
+ content={"status": "error", "message": "No active processing to stop"},
389
+ status_code=400
390
+ )
391
+
392
+ @app.post("/toggle_pause")
393
+ async def toggle_pause(request: Request):
394
+ # Apply rate limiting
395
+ if not rate_limiter.allow_request(request.url.path):
396
+ return JSONResponse(
397
+ content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
398
+ status_code=429
399
+ )
400
+
401
+ log_ootb_request(shared_state.server_url, "toggle_pause", {})
402
+
403
+ if not shared_state.is_processing:
404
+ return JSONResponse(
405
+ content={"status": "error", "message": "No active processing to pause/resume"},
406
+ status_code=400
407
+ )
408
+
409
+ # Toggle the pause state
410
+ shared_state.is_paused = not shared_state.is_paused
411
+ current_state = shared_state.is_paused
412
+
413
+ print(f"Toggled pause state to: {current_state}")
414
+
415
+ status_message = "paused" if current_state else "resumed"
416
+
417
+ # Add a message to the queue to inform the user
418
+ if current_state:
419
+ message = {"role": "assistant", "content": f"Task '{shared_state.task}' has been paused. Click Continue to resume.", "type": "text", "action_type": ""}
420
+ else:
421
+ message = {"role": "assistant", "content": f"Task '{shared_state.task}' has been resumed.", "type": "text", "action_type": ""}
422
+
423
+ shared_state.chatbot_messages.append(message)
424
+ shared_state.message_queue.append(message)
425
+
426
+ return JSONResponse(
427
+ content={
428
+ "status": "success",
429
+ "message": f"Processing {status_message}",
430
+ "is_paused": current_state
431
+ },
432
+ status_code=200
433
+ )
434
+
435
+ @app.get("/status")
436
+ async def get_status(request: Request):
437
+ # Apply rate limiting
438
+ if not rate_limiter.allow_request(request.url.path):
439
+ return JSONResponse(
440
+ content={"status": "error", "message": "Rate limit exceeded. Try again after 2 seconds."},
441
+ status_code=429
442
+ )
443
+
444
+ # log_ootb_request(shared_state.server_url, "get_status", {})
445
+
446
+ print(f"Status check - Processing: {shared_state.is_processing}, Paused: {shared_state.is_paused}")
447
+ return JSONResponse(
448
+ content={
449
+ "status": "success",
450
+ "is_processing": shared_state.is_processing,
451
+ "is_paused": shared_state.is_paused
452
+ },
453
+ status_code=200
454
+ )
455
+
456
+ @app.post("/exec_computer_tool")
457
+ async def exec_computer_tool(request: Request):
458
+ logging.info("Received request to /exec_computer_tool")
459
+ try:
460
+ data = await request.json()
461
+
462
+ # Extract parameters from the request
463
+ selected_screen = data.get('selected_screen', 0)
464
+ full_screen_game_mode = data.get('full_screen_game_mode', 0)
465
+ response = data.get('response', {})
466
+
467
+ logging.info(f"Executing TeachmodeExecutor with: screen={selected_screen}, mode={full_screen_game_mode}, response={response}")
468
+
469
+ # Create TeachmodeExecutor in a separate process to avoid event loop conflicts
470
+ # Since TeachmodeExecutor uses asyncio.run() internally, we need to run it in a way
471
+ # that doesn't conflict with FastAPI's event loop
472
+
473
+ def run_executor():
474
+ executor = TeachmodeExecutor(
475
+ selected_screen=selected_screen,
476
+ full_screen_game_mode=full_screen_game_mode
477
+ )
478
+
479
+ results = []
480
+ try:
481
+ for action_result in executor(response):
482
+ results.append(action_result)
483
+ except Exception as exec_error:
484
+ logging.error(f"Error executing action: {exec_error}", exc_info=True)
485
+ return {"error": str(exec_error)}
486
+
487
+ return results
488
+
489
+ # Execute in a thread pool to avoid blocking the event loop
490
+ with concurrent.futures.ThreadPoolExecutor() as pool:
491
+ results = await asyncio.get_event_loop().run_in_executor(pool, run_executor)
492
+
493
+ if isinstance(results, dict) and "error" in results:
494
+ return JSONResponse(
495
+ content={"status": "error", "message": results["error"]},
496
+ status_code=500
497
+ )
498
+
499
+ logging.info(f"Action results: {results}")
500
+
501
+ return JSONResponse(
502
+ content={"status": "success", "results": results},
503
+ status_code=200
504
+ )
505
+ except Exception as e:
506
+ logging.error("Error processing /exec_computer_tool:", exc_info=True)
507
+ return JSONResponse(
508
+ content={"status": "error", "message": f"Internal server error: {str(e)}"},
509
+ status_code=500
510
+ )
511
+
512
+ def process_input():
513
+ global shared_state
514
+ logging.info("process_input thread started.")
515
+ shared_state.is_processing = True
516
+ shared_state.should_stop = False
517
+ shared_state.is_paused = False
518
+ shared_state.stop_event.clear() # Ensure stop event is cleared at the start
519
+
520
+ print(f"start sampling loop: {shared_state.chatbot_messages}")
521
+ print(f"shared_state.args before sampling loop: {shared_state.args}")
522
+
523
+
524
+ try:
525
+ # Get the generator for the sampling loop
526
+ sampling_loop = simple_teachmode_sampling_loop(
527
+ model=shared_state.model,
528
+ task=shared_state.task,
529
+ selected_screen=shared_state.selected_screen,
530
+ user_id=shared_state.user_id,
531
+ trace_id=shared_state.trace_id,
532
+ api_keys=shared_state.api_keys,
533
+ server_url=shared_state.server_url,
534
+ full_screen_game_mode=shared_state.full_screen_game_mode,
535
+ max_steps=shared_state.max_steps,
536
+ )
537
+
538
+ # Process messages from the sampling loop
539
+ for loop_msg in sampling_loop:
540
+ # Check stop condition more frequently
541
+ if shared_state.should_stop or shared_state.stop_event.is_set():
542
+ print("Processing stopped by user")
543
+ break
544
+
545
+ # Check if paused and wait while paused
546
+ while shared_state.is_paused and not shared_state.should_stop and not shared_state.stop_event.is_set():
547
+ print(f"Processing paused at: {time.strftime('%H:%M:%S')}")
548
+ # Wait a short time and check stop condition regularly
549
+ for _ in range(5): # Check 5 times per second
550
+ if shared_state.should_stop or shared_state.stop_event.is_set():
551
+ break
552
+ time.sleep(0.2)
553
+
554
+ # Check again after pause loop
555
+ if shared_state.should_stop or shared_state.stop_event.is_set():
556
+ print("Processing stopped while paused or resuming")
557
+ break
558
+
559
+ shared_state.chatbot_messages.append(loop_msg)
560
+ shared_state.message_queue.append(loop_msg)
561
+
562
+ # Short sleep to allow stop signals to be processed
563
+ for _ in range(5): # Check 5 times per second
564
+ if shared_state.should_stop or shared_state.stop_event.is_set():
565
+ print("Processing stopped during sleep")
566
+ break
567
+ time.sleep(0.1)
568
+
569
+ if shared_state.should_stop or shared_state.stop_event.is_set():
570
+ break
571
+
572
+ except Exception as e:
573
+ # Handle any exceptions in the processing loop
574
+ error_msg = f"Error during task processing: {e}"
575
+ print(error_msg)
576
+ error_message = {"role": "assistant", "content": error_msg, "type": "error", "action_type": ""}
577
+ shared_state.message_queue.append(error_message)
578
+
579
+ finally:
580
+ # Handle completion or interruption
581
+ if shared_state.should_stop or shared_state.stop_event.is_set():
582
+ stop_msg = f"Task '{shared_state.task}' was stopped. Ready for new tasks."
583
+ final_message = {"role": "assistant", "content": stop_msg, "type": "text", "action_type": ""}
584
+ else:
585
+ complete_msg = f"Task '{shared_state.task}' completed. Thanks for using Marbot Run."
586
+ final_message = {"role": "assistant", "content": complete_msg, "type": "text", "action_type": ""}
587
+
588
+ shared_state.chatbot_messages.append(final_message)
589
+ shared_state.message_queue.append(final_message)
590
+
591
+ # Reset all state flags to allow for new tasks
592
+ shared_state.is_processing = False
593
+ shared_state.should_stop = False
594
+ shared_state.is_paused = False
595
+ shared_state.stop_event.clear()
596
+ print("Processing completed, ready for new tasks")
597
+ logging.info("process_input thread finished.")
598
+
599
+ def main():
600
+ # Logging is set up at the top level now
601
+ logging.info("App main() function starting setup.")
602
+ global app, shared_state, rate_limiter # Ensure app is global if needed by uvicorn
603
+ parser = argparse.ArgumentParser()
604
+ # Add arguments, but NOT host and port
605
+ parser.add_argument("--model", type=str, default="teach-mode-gpt-4o", help="Model name")
606
+ parser.add_argument("--task", type=str, default="Following the instructions to complete the task.", help="Initial task description")
607
+ parser.add_argument("--selected_screen", type=int, default=0, help="Selected screen index")
608
+ parser.add_argument("--user_id", type=str, default="hero_cases", help="User ID for the session")
609
+ parser.add_argument("--trace_id", type=str, default="build_scroll_combat", help="Trace ID for the session")
610
+ parser.add_argument("--api_keys", type=str, default="sk-proj-1234567890", help="API keys")
611
+ parser.add_argument("--server_url", type=str, default="http://ec2-44-234-43-86.us-west-2.compute.amazonaws.com", help="Server URL for the session")
612
+
613
+ args = parser.parse_args()
614
+
615
+ # Validate args or set defaults if needed (keep these)
616
+ if not hasattr(args, 'model'): args.model = "default_model"
617
+ if not hasattr(args, 'task'): args.task = "default_task"
618
+ if not hasattr(args, 'selected_screen'): args.selected_screen = 0
619
+ if not hasattr(args, 'user_id'): args.user_id = "unknown_user"
620
+ if not hasattr(args, 'trace_id'): args.trace_id = "unknown_trace"
621
+ if not hasattr(args, 'api_keys'): args.api_keys = "none"
622
+ if not hasattr(args, 'server_url'): args.server_url = "none"
623
+
624
+ shared_state = SharedState(args)
625
+ rate_limiter = RateLimiter(interval_seconds=2) # Re-initialize rate limiter
626
+ logging.info(f"Shared state initialized for user: {args.user_id}")
627
+
628
+ # --- Restore original port calculation logic ---
629
+ port = 7888 # Default port
630
+ host = "0.0.0.0" # Listen on all interfaces
631
+
632
+ if platform.system() == "Windows":
633
+ try:
634
+ username = os.environ["USERNAME"].lower()
635
+ logging.info(f"Determining port based on Windows username: {username}")
636
+ if username == "altair":
637
+ port = 14000
638
+ elif username.startswith("guest") and username[5:].isdigit():
639
+ num = int(username[5:])
640
+ if 1 <= num <= 10: # Assuming max 10 guests for this range
641
+ port = 14000 + num
642
+ else:
643
+ logging.warning(f"Guest user number {num} out of range (1-10), using default port {port}.")
644
+ else:
645
+ logging.info(f"Username '{username}' doesn't match specific rules, using default port {port}.")
646
+ except Exception as e:
647
+ logging.error(f"Error determining port from username: {e}. Using default port {port}.", exc_info=True)
648
+ else:
649
+ logging.info(f"Not running on Windows, using default port {port}.")
650
+ # --- End of restored port calculation ---
651
+
652
+ logging.info(f"Final Host={host}, Port={port}")
653
+
654
+ try:
655
+ logging.info(f"Starting Uvicorn server on {host}:{port}")
656
+ # Use the calculated port and specific host
657
+ uvicorn.run(app, host=host, port=port)
658
+ logging.info("Uvicorn server stopped.")
659
+ except Exception as main_e:
660
+ logging.error("Error in main execution:", exc_info=True)
661
+ finally:
662
+ logging.info("App main() function finished.")
663
+
664
+ if __name__ == "__main__":
665
+ main()
666
+
667
+ # Test log_ootb_request
666
668
  log_ootb_request("http://ec2-44-234-43-86.us-west-2.compute.amazonaws.com", "test_request", {"message": "Test message"})