import json import os import time from dotenv import load_dotenv from projectdavid import Entity # Load environment variables (Local context) load_dotenv() # Initialize the ProjectDavid SDK # Presumes internal clients (datasets, training, models) are correctly wired in Entity client = Entity( base_url=os.getenv("PROJECT_DAVID_PLATFORM_BASE_URL"), api_key=os.getenv("DEV_PROJECT_DAVID_CORE_TEST_USER_KEY"), ) def create_local_test_file(filename="test_dataset.jsonl"): """Creates a small valid JSONL file for testing.""" sample_data = [ { "messages": [ {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi there!"}, ] }, { "messages": [ {"role": "user", "content": "How are you?"}, {"role": "assistant", "content": "I am fine."}, ] }, ] with open(filename, "w") as f: for entry in sample_data: f.write(json.dumps(entry) + "\n") return filename def run_integration_test(): print("šŸš€ Starting Full Fine-Tuning Pipeline Test (Laptop Mode)...") # ────────────────────────────────────────────────────────────────────────── # STAGE 1: DATA INGESTION # SDK reads local bytes and hands them to the Core API (Port 9000). # The Core API persists the file to the Samba Hub. # ────────────────────────────────────────────────────────────────────────── file_name = create_local_test_file() print(f"šŸ“¦ STAGE 1: Uploading and registering dataset from {file_name}...") dataset = client.datasets.create( name="integration_test_ds", fmt="jsonl", description="Automated integration test dataset", file_path=file_name, ) print(f"āœ… Dataset Created: ID={dataset.id}, Status={dataset.status}") # ────────────────────────────────────────────────────────────────────────── # STAGE 2: ASYNC PREPARATION # Training API (Port 9001) performs an infrastructure-direct read from Samba. # It validates the JSONL format and computes train/eval splits. # ────────────────────────────────────────────────────────────────────────── print(f"āš™ļø STAGE 2: Triggering preparation for dataset {dataset.id}...") client.datasets.prepare(dataset.id) print("ā³ Polling for 'active' status...") max_retries = 15 for i in range(max_retries): dataset = client.datasets.retrieve(dataset.id) if dataset.status == "active": print(f"āœ… Dataset ready! (Samples: {dataset.train_samples})") break elif dataset.status == "failed": print( f"āŒ Dataset preparation failed: {dataset.config.get('preparation_error')}" ) return time.sleep(2) else: print("āŒ Timeout: Dataset preparation taking too long.") return # ────────────────────────────────────────────────────────────────────────── # STAGE 3: JOB ORCHESTRATION # Training API creates a Job record and pushes a ticket to Redis. # This handoff moves the task from the Web tier to the GPU tier. # ────────────────────────────────────────────────────────────────────────── print( f"šŸ”„ STAGE 3: Submitting training job [Qwen-1.5B] for dataset {dataset.id}..." ) job = client.training.create( dataset_id=dataset.id, base_model="Qwen/Qwen2.5-1.5B-Instruct", framework="unsloth", config={"learning_rate": 2e-4, "num_train_epochs": 1, "lora_r": 16}, ) print(f"āœ… Training Job Submitted: ID={job.id}, Status={job.status}") # ────────────────────────────────────────────────────────────────────────── # STAGE 4: REDIS PIPELINE VERIFICATION # Verification of the destructive BRPOP handoff. # Checks if the job is either in the queue OR already picked up by the worker. # ────────────────────────────────────────────────────────────────────────── print("šŸ” STAGE 4: Verifying pipeline connection (Redis -> Worker handoff)...") time.sleep(1) verified = False for attempt in range(5): queue_state = client.training.peek_queue() in_queue = any(item.job_id == job.id for item in queue_state.data) job = client.training.retrieve(job.id) started = job.status in ["in_progress", "completed"] if in_queue or started: print(f"✨ Handoff verified! (In Queue: {in_queue}, Started: {started})") verified = True break time.sleep(2) if not verified: print(f"āŒ FAIL: Job {job.id} never reached the queue or worker.") return # ────────────────────────────────────────────────────────────────────────── # STAGE 5: GPU EXECUTION (MONITORING) # The Worker stages data from Samba to local NVMe and runs the ML subprocess. # We poll the MySQL state until the LoRA adapters are exported. # ────────────────────────────────────────────────────────────────────────── print( f"ā³ STAGE 5: Monitoring GPU training (Check Worker Terminal for live kernels)..." ) while True: job = client.training.retrieve(job.id) if job.status == "completed": print(f"\nšŸ† VICTORY: Training finished successfully!") break elif job.status == "failed": print(f"\nšŸ’„ CRASH: Training failed. Error: {job.last_error}") return else: print(f" Worker Status: {job.status}...", end="\r") time.sleep(10) # ────────────────────────────────────────────────────────────────────────── # STAGE 6: REGISTRY & ACTIVATION # We promote the resulting adapter to be the 'Active' model in the DB. # This instructs the Orchestrator/vLLM to load these weights on next boot. # ────────────────────────────────────────────────────────────────────────── print(f"šŸŽÆ STAGE 6: Promoting model from Job {job.id} to ACTIVE status...") # Locate the model artifact registered by the worker models = client.models.list() new_model = next((m for m in models.data if m.training_job_id == job.id), None) if new_model: print(f"āœ… Found Registry Artifact: {new_model.id}") activation = client.models.activate(new_model.id) print(f"šŸ’” Next Step: {activation.next_step}") # Final Database Verification final_check = client.models.retrieve(new_model.id) if final_check.is_active: print( f"✨ PIPELINE COMPLETE: Model {new_model.id} is now the Active Brain." ) else: print("āŒ FAIL: Could not find fine_tuned_model record in registry.") # Cleanup local file if os.path.exists(file_name): os.remove(file_name) if __name__ == "__main__": try: run_integration_test() except KeyboardInterrupt: print("\nStopped by user.") except Exception as e: print(f"\nāŒ Unexpected Error: {e}")