ragaai-catalyst 2.1.5b30__py3-none-any.whl → 2.1.5b33__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.
Files changed (23) hide show
  1. ragaai_catalyst/ragaai_catalyst.py +37 -6
  2. ragaai_catalyst/redteaming/data_generator/scenario_generator.py +2 -2
  3. ragaai_catalyst/redteaming/data_generator/test_case_generator.py +2 -2
  4. ragaai_catalyst/redteaming/evaluator.py +2 -2
  5. ragaai_catalyst/redteaming/llm_generator.py +78 -25
  6. ragaai_catalyst/redteaming/{llm_generator_litellm.py → llm_generator_old.py} +30 -13
  7. ragaai_catalyst/redteaming/red_teaming.py +6 -4
  8. ragaai_catalyst/redteaming/utils/rt.png +0 -0
  9. ragaai_catalyst/synthetic_data_generation.py +23 -13
  10. ragaai_catalyst/tracers/agentic_tracing/tracers/base.py +283 -95
  11. ragaai_catalyst/tracers/agentic_tracing/tracers/llm_tracer.py +3 -3
  12. ragaai_catalyst/tracers/agentic_tracing/upload/trace_uploader.py +675 -0
  13. ragaai_catalyst/tracers/agentic_tracing/upload/upload_agentic_traces.py +73 -20
  14. ragaai_catalyst/tracers/agentic_tracing/upload/upload_code.py +53 -11
  15. ragaai_catalyst/tracers/agentic_tracing/upload/upload_trace_metric.py +9 -2
  16. ragaai_catalyst/tracers/agentic_tracing/utils/create_dataset_schema.py +4 -2
  17. ragaai_catalyst/tracers/agentic_tracing/utils/llm_utils.py +10 -1
  18. ragaai_catalyst/tracers/utils/model_prices_and_context_window_backup.json +9365 -0
  19. {ragaai_catalyst-2.1.5b30.dist-info → ragaai_catalyst-2.1.5b33.dist-info}/METADATA +92 -17
  20. {ragaai_catalyst-2.1.5b30.dist-info → ragaai_catalyst-2.1.5b33.dist-info}/RECORD +23 -20
  21. {ragaai_catalyst-2.1.5b30.dist-info → ragaai_catalyst-2.1.5b33.dist-info}/WHEEL +1 -1
  22. {ragaai_catalyst-2.1.5b30.dist-info → ragaai_catalyst-2.1.5b33.dist-info}/LICENSE +0 -0
  23. {ragaai_catalyst-2.1.5b30.dist-info → ragaai_catalyst-2.1.5b33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,675 @@
1
+ """
2
+ trace_uploader.py - A dedicated process for handling trace uploads
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import json
8
+ import time
9
+ import signal
10
+ import logging
11
+ import argparse
12
+ import tempfile
13
+ from pathlib import Path
14
+ import multiprocessing
15
+ import queue
16
+ from datetime import datetime
17
+ import atexit
18
+ import glob
19
+
20
+ # Set up logging
21
+ log_dir = os.path.join(tempfile.gettempdir(), "ragaai_logs")
22
+ os.makedirs(log_dir, exist_ok=True)
23
+
24
+ logging.basicConfig(
25
+ level=logging.DEBUG,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ handlers=[
28
+ logging.StreamHandler(),
29
+ logging.FileHandler(os.path.join(log_dir, "trace_uploader.log"))
30
+ ]
31
+ )
32
+ logger = logging.getLogger("trace_uploader")
33
+
34
+ try:
35
+ from ragaai_catalyst.tracers.agentic_tracing.upload.upload_agentic_traces import UploadAgenticTraces
36
+ from ragaai_catalyst.tracers.agentic_tracing.upload.upload_code import upload_code
37
+ from ragaai_catalyst.tracers.agentic_tracing.upload.upload_trace_metric import upload_trace_metric
38
+ from ragaai_catalyst.tracers.agentic_tracing.utils.create_dataset_schema import create_dataset_schema_with_trace
39
+ from ragaai_catalyst import RagaAICatalyst
40
+ IMPORTS_AVAILABLE = True
41
+ except ImportError:
42
+ logger.warning("RagaAI Catalyst imports not available - running in test mode")
43
+ IMPORTS_AVAILABLE = False
44
+
45
+ # Define task queue directory
46
+ QUEUE_DIR = os.path.join(tempfile.gettempdir(), "ragaai_tasks")
47
+ os.makedirs(QUEUE_DIR, exist_ok=True)
48
+
49
+ # Clean up any stale processes
50
+ def cleanup_stale_processes():
51
+ """Check for stale processes but allow active uploads to complete"""
52
+ pid_file = os.path.join(tempfile.gettempdir(), "trace_uploader.pid")
53
+ if os.path.exists(pid_file):
54
+ try:
55
+ with open(pid_file, "r") as f:
56
+ old_pid = int(f.read().strip())
57
+ try:
58
+ import psutil
59
+ if psutil.pid_exists(old_pid):
60
+ p = psutil.Process(old_pid)
61
+ if "trace_uploader.py" in " ".join(p.cmdline()):
62
+ # Instead of terminating, just remove the PID file
63
+ # This allows the process to finish its current uploads
64
+ logger.info(f"Removing PID file for process {old_pid}")
65
+ os.remove(pid_file)
66
+ return
67
+ except Exception as e:
68
+ logger.warning(f"Error checking stale process: {e}")
69
+ os.remove(pid_file)
70
+ except Exception as e:
71
+ logger.warning(f"Error reading PID file: {e}")
72
+
73
+ cleanup_stale_processes()
74
+
75
+ # Status codes
76
+ STATUS_PENDING = "pending"
77
+ STATUS_PROCESSING = "processing"
78
+ STATUS_COMPLETED = "completed"
79
+ STATUS_FAILED = "failed"
80
+
81
+ class UploadTask:
82
+ """Class representing a single upload task"""
83
+
84
+ def __init__(self, task_id=None, **kwargs):
85
+ self.task_id = task_id or f"task_{int(time.time())}_{os.getpid()}_{hash(str(time.time()))}"
86
+ self.status = STATUS_PENDING
87
+ self.attempts = 0
88
+ self.max_attempts = 3
89
+ self.created_at = datetime.now().isoformat()
90
+ self.updated_at = self.created_at
91
+ self.error = None
92
+
93
+ # Task details
94
+ self.filepath = kwargs.get("filepath")
95
+ self.hash_id = kwargs.get("hash_id")
96
+ self.zip_path = kwargs.get("zip_path")
97
+ self.project_name = kwargs.get("project_name")
98
+ self.project_id = kwargs.get("project_id")
99
+ self.dataset_name = kwargs.get("dataset_name")
100
+ self.user_details = kwargs.get("user_details", {})
101
+ self.base_url = kwargs.get("base_url")
102
+
103
+ def to_dict(self):
104
+ """Convert task to dictionary for serialization"""
105
+ return {
106
+ "task_id": self.task_id,
107
+ "status": self.status,
108
+ "attempts": self.attempts,
109
+ "max_attempts": self.max_attempts,
110
+ "created_at": self.created_at,
111
+ "updated_at": self.updated_at,
112
+ "error": self.error,
113
+ "filepath": self.filepath,
114
+ "hash_id": self.hash_id,
115
+ "zip_path": self.zip_path,
116
+ "project_name": self.project_name,
117
+ "project_id": self.project_id,
118
+ "dataset_name": self.dataset_name,
119
+ "user_details": self.user_details,
120
+ "base_url": self.base_url
121
+ }
122
+
123
+ @classmethod
124
+ def from_dict(cls, data):
125
+ """Create task from dictionary"""
126
+ task = cls(task_id=data.get("task_id"))
127
+ task.status = data.get("status", STATUS_PENDING)
128
+ task.attempts = data.get("attempts", 0)
129
+ task.max_attempts = data.get("max_attempts", 3)
130
+ task.created_at = data.get("created_at")
131
+ task.updated_at = data.get("updated_at")
132
+ task.error = data.get("error")
133
+ task.filepath = data.get("filepath")
134
+ task.hash_id = data.get("hash_id")
135
+ task.zip_path = data.get("zip_path")
136
+ task.project_name = data.get("project_name")
137
+ task.project_id = data.get("project_id")
138
+ task.dataset_name = data.get("dataset_name")
139
+ task.user_details = data.get("user_details", {})
140
+ task.base_url = data.get("base_url")
141
+ return task
142
+
143
+ def update_status(self, status, error=None):
144
+ """Update task status"""
145
+ self.status = status
146
+ self.updated_at = datetime.now().isoformat()
147
+ if error:
148
+ self.error = str(error)
149
+ self.save()
150
+
151
+ def increment_attempts(self):
152
+ """Increment the attempt counter"""
153
+ self.attempts += 1
154
+ self.updated_at = datetime.now().isoformat()
155
+ self.save()
156
+
157
+ def save(self):
158
+ """Save task to disk"""
159
+ task_path = os.path.join(QUEUE_DIR, f"{self.task_id}.json")
160
+ with open(task_path, "w") as f:
161
+ json.dump(self.to_dict(), f, indent=2)
162
+
163
+ def delete(self):
164
+ """Delete task file from disk"""
165
+ task_path = os.path.join(QUEUE_DIR, f"{self.task_id}.json")
166
+ if os.path.exists(task_path):
167
+ os.remove(task_path)
168
+
169
+ @staticmethod
170
+ def list_pending_tasks():
171
+ """List all pending tasks"""
172
+ tasks = []
173
+ logger.info("Listing pending tasks from queue directory: {}".format(QUEUE_DIR))
174
+ for filename in os.listdir(QUEUE_DIR):
175
+ if filename.endswith(".json"):
176
+ try:
177
+ with open(os.path.join(QUEUE_DIR, filename), "r") as f:
178
+ task_data = json.load(f)
179
+ task = UploadTask.from_dict(task_data)
180
+ if task.status in [STATUS_PENDING, STATUS_FAILED] and task.attempts < task.max_attempts:
181
+ # Verify files still exist
182
+ if (not task.filepath or os.path.exists(task.filepath)) and \
183
+ (not task.zip_path or os.path.exists(task.zip_path)):
184
+ tasks.append(task)
185
+ else:
186
+ # Files missing, mark as failed
187
+ task.update_status(STATUS_FAILED, "Required files missing")
188
+ except Exception as e:
189
+ logger.error(f"Error loading task {filename}: {e}")
190
+ return tasks
191
+
192
+
193
+ class TraceUploader:
194
+ """
195
+ Trace uploader process
196
+ Handles the actual upload work in a separate process
197
+ """
198
+
199
+ def __init__(self):
200
+ self.running = True
201
+ self.processing = False
202
+
203
+ def start(self):
204
+ """Start the uploader loop"""
205
+ logger.info("Trace uploader starting")
206
+
207
+ # Register signal handlers
208
+ signal.signal(signal.SIGTERM, self.handle_signal)
209
+ signal.signal(signal.SIGINT, self.handle_signal)
210
+
211
+ # Register cleanup
212
+ atexit.register(self.cleanup)
213
+
214
+ # Main processing loop
215
+ while self.running:
216
+ try:
217
+ # Get pending tasks
218
+ tasks = UploadTask.list_pending_tasks()
219
+ if tasks:
220
+ logger.info(f"Found {len(tasks)} pending tasks")
221
+ for task in tasks:
222
+ if not self.running:
223
+ break
224
+ self.process_task(task)
225
+ else:
226
+ # No tasks, sleep before checking again
227
+ time.sleep(5)
228
+ except Exception as e:
229
+ logger.error(f"Error in uploader loop: {e}")
230
+ time.sleep(5)
231
+
232
+ logger.info("Trace uploader stopped")
233
+
234
+ def process_task(self, task):
235
+ """Process a single upload task"""
236
+ logger.info(f"Starting to process task {task.task_id}")
237
+ logger.debug(f"Task details: {task.to_dict()}")
238
+
239
+ # Check if file exists
240
+ if not os.path.exists(task.filepath):
241
+ error_msg = f"Task filepath does not exist: {task.filepath}"
242
+ logger.error(error_msg)
243
+ task.update_status(STATUS_FAILED, error_msg)
244
+ return
245
+
246
+ if not IMPORTS_AVAILABLE:
247
+ logger.warning(f"Test mode: Simulating processing of task {task.task_id}")
248
+ time.sleep(2) # Simulate work
249
+ task.update_status(STATUS_COMPLETED)
250
+ return
251
+
252
+ logger.info(f"Processing task {task.task_id} (attempt {task.attempts+1}/{task.max_attempts})")
253
+ self.processing = True
254
+ task.update_status(STATUS_PROCESSING)
255
+ task.increment_attempts()
256
+
257
+ # Log memory state for debugging
258
+ try:
259
+ import psutil
260
+ process = psutil.Process()
261
+ logger.debug(f"Memory usage before processing: {process.memory_info().rss / 1024 / 1024:.2f} MB")
262
+ except ImportError:
263
+ pass
264
+
265
+ try:
266
+ # Step 1: Create dataset schema
267
+ logger.info(f"Creating dataset schema for {task.dataset_name} with base_url: {task.base_url}")
268
+ response = create_dataset_schema_with_trace(
269
+ dataset_name=task.dataset_name,
270
+ project_name=task.project_name,
271
+ base_url=task.base_url
272
+ )
273
+ logger.info(f"Dataset schema created: {response}")
274
+
275
+ # Step 2: Upload trace metrics
276
+ if task.filepath and os.path.exists(task.filepath):
277
+ logger.info(f"Uploading trace metrics for {task.filepath}")
278
+ try:
279
+ response = upload_trace_metric(
280
+ json_file_path=task.filepath,
281
+ dataset_name=task.dataset_name,
282
+ project_name=task.project_name,
283
+ base_url=task.base_url
284
+ )
285
+ logger.info(f"Trace metrics uploaded: {response}")
286
+ except Exception as e:
287
+ logger.error(f"Error uploading trace metrics: {e}")
288
+ # Continue with other uploads
289
+ else:
290
+ logger.warning(f"Trace file {task.filepath} not found, skipping metrics upload")
291
+
292
+ # Step 3: Upload agentic traces
293
+ if task.filepath and os.path.exists(task.filepath):
294
+ logger.info(f"Uploading agentic traces for {task.filepath}")
295
+ try:
296
+ upload_traces = UploadAgenticTraces(
297
+ json_file_path=task.filepath,
298
+ project_name=task.project_name,
299
+ project_id=task.project_id,
300
+ dataset_name=task.dataset_name,
301
+ user_detail=task.user_details,
302
+ base_url=task.base_url,
303
+ )
304
+ upload_traces.upload_agentic_traces()
305
+ logger.info("Agentic traces uploaded successfully")
306
+ except Exception as e:
307
+ logger.error(f"Error uploading agentic traces: {e}")
308
+ # Continue with code upload
309
+ else:
310
+ logger.warning(f"Trace file {task.filepath} not found, skipping traces upload")
311
+
312
+ # Step 4: Upload code hash
313
+ if task.hash_id and task.zip_path and os.path.exists(task.zip_path):
314
+ logger.info(f"Uploading code hash {task.hash_id}")
315
+ try:
316
+ response = upload_code(
317
+ hash_id=task.hash_id,
318
+ zip_path=task.zip_path,
319
+ project_name=task.project_name,
320
+ dataset_name=task.dataset_name,
321
+ base_url=task.base_url
322
+ )
323
+ logger.info(f"Code hash uploaded: {response}")
324
+ except Exception as e:
325
+ logger.error(f"Error uploading code hash: {e}")
326
+ else:
327
+ logger.warning(f"Code zip {task.zip_path} not found, skipping code upload")
328
+
329
+ # Mark task as completed
330
+ task.update_status(STATUS_COMPLETED)
331
+ logger.info(f"Task {task.task_id} completed successfully")
332
+
333
+ # Clean up task file
334
+ task.delete()
335
+
336
+ except Exception as e:
337
+ logger.error(f"Error processing task {task.task_id}: {e}")
338
+ if task.attempts >= task.max_attempts:
339
+ task.update_status(STATUS_FAILED, str(e))
340
+ logger.error(f"Task {task.task_id} failed after {task.attempts} attempts")
341
+ else:
342
+ task.update_status(STATUS_PENDING, str(e))
343
+ logger.warning(f"Task {task.task_id} will be retried (attempt {task.attempts}/{task.max_attempts})")
344
+ finally:
345
+ self.processing = False
346
+
347
+ def handle_signal(self, signum, frame):
348
+ """Handle termination signals"""
349
+ logger.info(f"Received signal {signum}, shutting down gracefully")
350
+ self.running = False
351
+
352
+ def cleanup(self):
353
+ """Cleanup before exit"""
354
+ logger.info("Performing cleanup before exit")
355
+ self.running = False
356
+
357
+
358
+ def submit_upload_task(filepath, hash_id, zip_path, project_name, project_id, dataset_name, user_details, base_url):
359
+ """
360
+ Submit a new upload task to the queue.
361
+ This function can be called from the main application.
362
+
363
+ Returns:
364
+ str: Task ID
365
+ """
366
+ logger.info(f"Submitting new upload task for file: {filepath}")
367
+ logger.debug(f"Task details - Project: {project_name}, Dataset: {dataset_name}, Hash: {hash_id}, Base_URL: {base_url}")
368
+
369
+ # Verify the trace file exists
370
+ if not os.path.exists(filepath):
371
+ logger.error(f"Trace file not found: {filepath}")
372
+ return None
373
+
374
+ # Create task with absolute path to the trace file
375
+ filepath = os.path.abspath(filepath)
376
+ logger.debug(f"Using absolute filepath: {filepath}")
377
+
378
+ task = UploadTask(
379
+ filepath=filepath,
380
+ hash_id=hash_id,
381
+ zip_path=zip_path,
382
+ project_name=project_name,
383
+ project_id=project_id,
384
+ dataset_name=dataset_name,
385
+ user_details=user_details,
386
+ base_url=base_url
387
+ )
388
+
389
+ # Save the task with proper error handling
390
+ task_path = os.path.join(QUEUE_DIR, f"{task.task_id}.json")
391
+ logger.debug(f"Saving task to: {task_path}")
392
+
393
+ try:
394
+ # Ensure queue directory exists
395
+ os.makedirs(QUEUE_DIR, exist_ok=True)
396
+
397
+ with open(task_path, "w") as f:
398
+ json.dump(task.to_dict(), f, indent=2)
399
+
400
+ logger.info(f"Task {task.task_id} created successfully for trace file: {filepath}")
401
+ except Exception as e:
402
+ logger.error(f"Error creating task file: {e}", exc_info=True)
403
+ return None
404
+
405
+ # Ensure uploader process is running
406
+ logger.info("Starting uploader process...")
407
+ pid = ensure_uploader_running()
408
+ if pid:
409
+ logger.info(f"Uploader process running with PID {pid}")
410
+ else:
411
+ logger.warning("Failed to start uploader process, but task was queued")
412
+
413
+ return task.task_id
414
+
415
+
416
+ def get_task_status(task_id):
417
+ """
418
+ Get the status of a task by ID.
419
+ This function can be called from the main application.
420
+
421
+ Returns:
422
+ dict: Task status information
423
+ """
424
+ task_path = os.path.join(QUEUE_DIR, f"{task_id}.json")
425
+ if not os.path.exists(task_path):
426
+ # Check if it might be in completed directory
427
+ completed_path = os.path.join(QUEUE_DIR, "completed", f"{task_id}.json")
428
+ if os.path.exists(completed_path):
429
+ with open(completed_path, "r") as f:
430
+ return json.load(f)
431
+ return {"status": "unknown", "error": "Task not found"}
432
+
433
+ with open(task_path, "r") as f:
434
+ return json.load(f)
435
+
436
+
437
+ def ensure_uploader_running():
438
+ """
439
+ Ensure the uploader process is running.
440
+ Starts it if not already running.
441
+ """
442
+ logger.info("Checking if uploader process is running...")
443
+
444
+ # Check if we can find a running process
445
+ pid_file = os.path.join(tempfile.gettempdir(), "trace_uploader.pid")
446
+ logger.debug(f"PID file location: {pid_file}")
447
+
448
+ if os.path.exists(pid_file):
449
+ try:
450
+ with open(pid_file, "r") as f:
451
+ pid_str = f.read().strip()
452
+ logger.debug(f"Read PID from file: {pid_str}")
453
+ pid = int(pid_str)
454
+
455
+ # Check if process is actually running
456
+ # Use platform-specific process check
457
+ is_running = False
458
+ try:
459
+ if os.name == 'posix': # Unix/Linux/Mac
460
+ logger.debug(f"Checking process {pid} on Unix/Mac")
461
+ os.kill(pid, 0) # This raises an exception if process doesn't exist
462
+ is_running = True
463
+ else: # Windows
464
+ logger.debug(f"Checking process {pid} on Windows")
465
+ import ctypes
466
+ kernel32 = ctypes.windll.kernel32
467
+ SYNCHRONIZE = 0x00100000
468
+ process = kernel32.OpenProcess(SYNCHRONIZE, False, pid)
469
+ if process:
470
+ kernel32.CloseHandle(process)
471
+ is_running = True
472
+ except (ImportError, AttributeError) as e:
473
+ logger.debug(f"Platform-specific check failed: {e}, falling back to cross-platform check")
474
+ # Fall back to cross-platform check
475
+ try:
476
+ import psutil
477
+ is_running = psutil.pid_exists(pid)
478
+ logger.debug(f"psutil check result: {is_running}")
479
+ except ImportError:
480
+ logger.debug("psutil not available, using basic process check")
481
+ # If psutil is not available, make a best guess
482
+ try:
483
+ os.kill(pid, 0)
484
+ is_running = True
485
+ except Exception as e:
486
+ logger.debug(f"Basic process check failed: {e}")
487
+ is_running = False
488
+
489
+ if is_running:
490
+ logger.debug(f"Uploader process already running with PID {pid}")
491
+ return pid
492
+ except (ProcessLookupError, ValueError, PermissionError):
493
+ # Process not running or other error, remove stale PID file
494
+ try:
495
+ os.remove(pid_file)
496
+ except:
497
+ pass
498
+
499
+ # Start new process
500
+ logger.info("Starting new uploader process")
501
+
502
+ # Get the path to this script
503
+ script_path = os.path.abspath(__file__)
504
+
505
+ # Start detached process in a platform-specific way
506
+ try:
507
+ # First, try the preferred method for each platform
508
+ if os.name == 'posix': # Unix/Linux/Mac
509
+ import subprocess
510
+
511
+ # Use double fork method on Unix systems
512
+ try:
513
+ # First fork
514
+ pid = os.fork()
515
+ if pid > 0:
516
+ # Parent process, return
517
+ return pid
518
+
519
+ # Decouple from parent environment
520
+ os.chdir('/')
521
+ os.setsid()
522
+ os.umask(0)
523
+
524
+ # Second fork
525
+ pid = os.fork()
526
+ if pid > 0:
527
+ # Exit from second parent
528
+ os._exit(0)
529
+
530
+ # Redirect standard file descriptors
531
+ sys.stdout.flush()
532
+ sys.stderr.flush()
533
+ si = open(os.devnull, 'r')
534
+ so = open(os.path.join(tempfile.gettempdir(), 'trace_uploader_stdout.log'), 'a+')
535
+ se = open(os.path.join(tempfile.gettempdir(), 'trace_uploader_stderr.log'), 'a+')
536
+ os.dup2(si.fileno(), sys.stdin.fileno())
537
+ os.dup2(so.fileno(), sys.stdout.fileno())
538
+ os.dup2(se.fileno(), sys.stderr.fileno())
539
+
540
+ # Execute the daemon process
541
+ os.execl(sys.executable, sys.executable, script_path, '--daemon')
542
+
543
+ except (AttributeError, OSError):
544
+ # Fork not available, try subprocess
545
+ process = subprocess.Popen(
546
+ [sys.executable, script_path, "--daemon"],
547
+ stdout=subprocess.PIPE,
548
+ stderr=subprocess.PIPE,
549
+ stdin=subprocess.PIPE,
550
+ start_new_session=True # Detach from parent
551
+ )
552
+ pid = process.pid
553
+
554
+ else: # Windows
555
+ import subprocess
556
+ # Use the DETACHED_PROCESS flag on Windows
557
+ startupinfo = subprocess.STARTUPINFO()
558
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
559
+ startupinfo.wShowWindow = 0 # SW_HIDE
560
+
561
+ # Windows-specific flags
562
+ DETACHED_PROCESS = 0x00000008
563
+ CREATE_NO_WINDOW = 0x08000000
564
+
565
+ process = subprocess.Popen(
566
+ [sys.executable, script_path, "--daemon"],
567
+ stdout=subprocess.PIPE,
568
+ stderr=subprocess.PIPE,
569
+ stdin=subprocess.PIPE,
570
+ startupinfo=startupinfo,
571
+ creationflags=DETACHED_PROCESS | CREATE_NO_WINDOW
572
+ )
573
+ pid = process.pid
574
+
575
+ # Write PID to file
576
+ with open(pid_file, "w") as f:
577
+ f.write(str(pid))
578
+
579
+ logger.info(f"Started uploader process with PID {pid}")
580
+ return pid
581
+
582
+ except Exception as e:
583
+ logger.error(f"Error starting uploader process using primary method: {e}")
584
+
585
+ # Fallback method using multiprocessing (works on most platforms)
586
+ try:
587
+ logger.info("Trying fallback method with multiprocessing")
588
+ import multiprocessing
589
+
590
+ def run_uploader():
591
+ """Run the uploader in a separate process"""
592
+ # Redirect output
593
+ sys.stdout = open(os.path.join(tempfile.gettempdir(), 'trace_uploader_stdout.log'), 'a+')
594
+ sys.stderr = open(os.path.join(tempfile.gettempdir(), 'trace_uploader_stderr.log'), 'a+')
595
+
596
+ # Run daemon
597
+ run_daemon()
598
+
599
+ # Start process
600
+ process = multiprocessing.Process(target=run_uploader)
601
+ process.daemon = True # Daemonize it
602
+ process.start()
603
+ pid = process.pid
604
+
605
+ # Write PID to file
606
+ with open(pid_file, "w") as f:
607
+ f.write(str(pid))
608
+
609
+ logger.info(f"Started uploader process with fallback method, PID {pid}")
610
+ return pid
611
+
612
+ except Exception as e2:
613
+ logger.error(f"Error starting uploader process using fallback method: {e2}")
614
+
615
+ # Super fallback - run in the current process if all else fails
616
+ # This is not ideal but better than failing completely
617
+ try:
618
+ logger.warning("Using emergency fallback - running in current process thread")
619
+ import threading
620
+
621
+ thread = threading.Thread(target=run_daemon, daemon=True)
622
+ thread.start()
623
+
624
+ # No real PID since it's a thread, but we'll create a marker file
625
+ with open(pid_file, "w") as f:
626
+ f.write(f"thread_{id(thread)}")
627
+
628
+ return None
629
+ except Exception as e3:
630
+ logger.error(f"All methods failed to start uploader: {e3}")
631
+ return None
632
+
633
+
634
+ def run_daemon():
635
+ """Run the uploader as a daemon process"""
636
+ # Write PID to file
637
+ pid_file = os.path.join(tempfile.gettempdir(), "trace_uploader.pid")
638
+ with open(pid_file, "w") as f:
639
+ f.write(str(os.getpid()))
640
+
641
+ try:
642
+ uploader = TraceUploader()
643
+ uploader.start()
644
+ finally:
645
+ # Clean up PID file
646
+ if os.path.exists(pid_file):
647
+ os.remove(pid_file)
648
+
649
+
650
+ if __name__ == "__main__":
651
+ parser = argparse.ArgumentParser(description="Trace uploader process")
652
+ parser.add_argument("--daemon", action="store_true", help="Run as daemon process")
653
+ parser.add_argument("--test", action="store_true", help="Submit a test task")
654
+ args = parser.parse_args()
655
+
656
+ if args.daemon:
657
+ run_daemon()
658
+ elif args.test:
659
+ # Submit a test task
660
+ test_file = os.path.join(tempfile.gettempdir(), "test_trace.json")
661
+ with open(test_file, "w") as f:
662
+ f.write("{}")
663
+
664
+ task_id = submit_upload_task(
665
+ filepath=test_file,
666
+ hash_id="test_hash",
667
+ zip_path=test_file,
668
+ project_name="test_project",
669
+ project_id="test_id",
670
+ dataset_name="test_dataset",
671
+ user_details={"id": "test_user"}
672
+ )
673
+ print(f"Submitted test task with ID: {task_id}")
674
+ else:
675
+ print("Use --daemon to run as daemon or --test to submit a test task")