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