File size: 15,116 Bytes
4db4e9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b20c328
 
 
 
 
4db4e9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337ea13
 
 
 
 
 
 
4db4e9d
 
 
 
 
 
 
 
 
 
 
 
 
337ea13
9e2cabe
4db4e9d
 
9e2cabe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337ea13
9e2cabe
 
4db4e9d
 
8c679b3
4db4e9d
8c679b3
4db4e9d
 
8c679b3
4db4e9d
 
 
 
 
8c679b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
"""
HuggingFace Jobs Submission Module

Handles submission of SMOLTRACE evaluation jobs to HuggingFace Jobs platform.
Uses the official HuggingFace Jobs API: `huggingface_hub.run_job()`
"""

import os
import uuid
from typing import Dict, Optional, List


def submit_hf_job(
    model: str,
    provider: str,
    agent_type: str,
    hardware: str,
    dataset_name: str,
    split: str = "train",
    difficulty: str = "all",
    parallel_workers: int = 1,
    hf_token: Optional[str] = None,
    hf_inference_provider: Optional[str] = None,
    search_provider: str = "duckduckgo",
    enable_tools: Optional[List[str]] = None,
    output_format: str = "hub",
    output_dir: Optional[str] = None,
    enable_otel: bool = True,
    enable_gpu_metrics: bool = True,
    private: bool = False,
    debug: bool = False,
    quiet: bool = False,
    run_id: Optional[str] = None,
    timeout: str = "1h"
) -> Dict:
    """
    Submit an evaluation job to HuggingFace Jobs using the run_job API

    Args:
        model: Model identifier (e.g., "openai/gpt-4")
        provider: Provider type ("litellm", "inference", "transformers")
        agent_type: Agent type ("tool", "code", "both")
        hardware: Hardware type (e.g., "auto", "cpu-basic", "t4-small", "a10g-small")
        dataset_name: HuggingFace dataset for evaluation
        split: Dataset split to use
        difficulty: Difficulty filter
        parallel_workers: Number of parallel workers
        hf_token: HuggingFace token
        hf_inference_provider: HF Inference provider
        search_provider: Search provider for agents
        enable_tools: List of tools to enable
        output_format: Output format ("hub" or "json")
        output_dir: Output directory for JSON format
        enable_otel: Enable OpenTelemetry tracing
        enable_gpu_metrics: Enable GPU metrics collection
        private: Make datasets private
        debug: Enable debug mode
        quiet: Enable quiet mode
        run_id: Optional run ID (auto-generated if not provided)
        timeout: Job timeout (default: "1h")

    Returns:
        dict: Job submission result with job_id, status, and details
    """
    try:
        from huggingface_hub import run_job
    except ImportError:
        return {
            "success": False,
            "error": "huggingface_hub package not installed or outdated. Install with: pip install -U huggingface_hub",
            "job_id": None
        }

    # Validate HF token
    token = hf_token or os.environ.get("HF_TOKEN")
    if not token:
        return {
            "success": False,
            "error": "HuggingFace token not configured. Please set HF_TOKEN in Settings.",
            "job_id": None
        }

    # Generate job ID
    job_id = run_id if run_id else f"job_{uuid.uuid4().hex[:8]}"

    # Map hardware to HF Jobs flavor
    if hardware == "auto":
        flavor = _auto_select_hf_hardware(provider, model)
    else:
        flavor = hardware

    # Determine if this is a GPU job
    is_gpu_job = flavor not in ["cpu-basic", "cpu-upgrade"]

    # Select appropriate Docker image
    if is_gpu_job:
        # GPU jobs use PyTorch with CUDA
        image = "pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel"
        pip_packages = "smoltrace ddgs smoltrace[gpu]"
    else:
        # CPU jobs use standard Python
        image = "python:3.12"
        pip_packages = "smoltrace ddgs"

    # Build secrets dictionary
    secrets = {
        "HF_TOKEN": token
    }

    # Add LLM provider API keys from environment
    llm_key_names = [
        "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY",
        "GEMINI_API_KEY", "COHERE_API_KEY", "MISTRAL_API_KEY",
        "TOGETHER_API_KEY", "GROQ_API_KEY", "REPLICATE_API_TOKEN",
        "ANYSCALE_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY",
        "AWS_REGION", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT",
        "LITELLM_API_KEY"
    ]

    for key_name in llm_key_names:
        value = os.environ.get(key_name)
        if value:
            secrets[key_name] = value

    # Build SMOLTRACE command
    cmd_parts = ["smoltrace-eval"]
    cmd_parts.append(f"--model {model}")
    cmd_parts.append(f"--provider {provider}")
    if hf_inference_provider:
        cmd_parts.append(f"--hf-inference-provider {hf_inference_provider}")
    cmd_parts.append(f"--search-provider {search_provider}")
    if enable_tools:
        cmd_parts.append(f"--enable-tools {','.join(enable_tools)}")
    cmd_parts.append(f"--agent-type {agent_type}")
    cmd_parts.append(f"--dataset-name {dataset_name}")
    cmd_parts.append(f"--split {split}")
    if difficulty != "all":
        cmd_parts.append(f"--difficulty {difficulty}")
    if parallel_workers > 1:
        cmd_parts.append(f"--parallel-workers {parallel_workers}")
    cmd_parts.append(f"--output-format {output_format}")
    if output_dir and output_format == "json":
        cmd_parts.append(f"--output-dir {output_dir}")
    if enable_otel:
        cmd_parts.append("--enable-otel")
    if not enable_gpu_metrics:
        cmd_parts.append("--disable-gpu-metrics")
    if private:
        cmd_parts.append("--private")
    if debug:
        cmd_parts.append("--debug")
    if quiet:
        cmd_parts.append("--quiet")
    cmd_parts.append(f"--run-id {job_id}")

    smoltrace_command = " ".join(cmd_parts)

    # Build full command with pip upgrade + install
    # IMPORTANT: Upgrade pip first to avoid dependency resolution issues
    # (older pip in conda struggles with fief-client[cli] backtracking)
    # Set PYTHONIOENCODING to UTF-8 to handle unicode output properly
    full_command = f"export PYTHONIOENCODING=utf-8 && pip install --upgrade pip && pip install {pip_packages} && {smoltrace_command}"

    # Submit job using HuggingFace Jobs API
    try:
        job = run_job(
            image=image,
            command=["bash", "-c", full_command],
            secrets=secrets,
            flavor=flavor,
            timeout=timeout
        )

        return {
            "success": True,
            "job_id": job_id,
            "hf_job_id": job.job_id if hasattr(job, 'job_id') else str(job),
            "platform": "HuggingFace Jobs",
            "hardware": flavor,
            "image": image,
            "command": smoltrace_command,
            "status": "submitted",
            "message": f"Job successfully submitted to HuggingFace Jobs (flavor: {flavor})",
            "instructions": f"""
✅ Job submitted successfully!

**Job Details:**
- Flavor: {flavor}
- Image: {image}
- Timeout: {timeout}

**Monitor your job:**
- View job status: https://huggingface.co/jobs
- HF Job ID: {job.job_id if hasattr(job, 'job_id') else 'check dashboard'}

**What happens next:**
1. Job starts running on HuggingFace infrastructure
2. SMOLTRACE evaluates your model
3. Results are automatically pushed to HuggingFace datasets
4. They will appear in TraceMind leaderboard when complete
            """.strip()
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to submit job to HuggingFace: {str(e)}",
            "job_id": job_id,
            "command": smoltrace_command,
            "debug_info": {
                "image": image,
                "flavor": flavor,
                "timeout": timeout,
                "secrets_configured": list(secrets.keys())
            }
        }


def _auto_select_hf_hardware(provider: str, model: str) -> str:
    """
    Automatically select HuggingFace Jobs hardware based on model and provider.

    Memory estimation for agentic workloads:
    - Model weights (FP16): ~2GB per 1B params
    - KV cache for long contexts: ~1.5-2x model size for agentic tasks
    - Inference overhead: ~20-30% additional
    - Total: ~4-5GB per 1B params for safe agentic execution

    Args:
        provider: Provider type
        model: Model identifier

    Returns:
        str: HF Jobs flavor
    """
    # API models only need CPU
    if provider in ["litellm", "inference"]:
        return "cpu-basic"

    # Local models need GPU - select based on model size
    # Conservative allocation for agentic tasks (model weights + KV cache + inference overhead)
    # Memory estimation: ~4-5GB per 1B params for safe agentic execution
    model_lower = model.lower()

    # Extract model size using regex to capture the number before 'b'
    import re
    size_match = re.search(r'(\d+\.?\d*)b', model_lower)

    if size_match:
        model_size = float(size_match.group(1))

        # Complete coverage from 0.5B to 100B+ with no gaps
        # HF Jobs has limited GPU options: t4-small, a10g-large, a100-large
        if model_size >= 13:
            # 13B-100B+: A100 large (e.g., 13B, 14B, 27B, 30B, 48B, 70B)
            return "a100-large"
        elif model_size >= 6:
            # 6B-12B: A10G large (e.g., 6B, 7B, 8B, 9B, 10B, 11B, 12B)
            return "a10g-large"
        elif model_size >= 1:
            # 1B-5B: T4 small (e.g., 1B, 2B, 3B, 4B, 5B)
            return "t4-small"
        else:
            # < 1B: T4 small
            return "t4-small"
    else:
        # No size detected in model name - default to A100 (safe for agentic workloads)
        return "a100-large"


def check_job_status(hf_job_id: str, hf_token: Optional[str] = None) -> Dict:
    """
    Check the status of a HuggingFace Job using the Jobs API

    Args:
        hf_job_id: HF Job ID (format: username/job_hash or just job_hash)
        hf_token: HuggingFace token (optional, uses env if not provided)

    Returns:
        dict: Job status information
    """
    try:
        from huggingface_hub import HfApi
    except ImportError:
        return {
            "success": False,
            "error": "huggingface_hub package not installed",
            "job_id": hf_job_id
        }

    token = hf_token or os.environ.get("HF_TOKEN")
    if not token:
        return {
            "success": False,
            "error": "HuggingFace token not configured",
            "job_id": hf_job_id
        }

    try:
        api = HfApi(token=token)

        # Parse job_id and namespace (username)
        # Format can be "username/job_hash" or just "job_hash"
        if "/" in hf_job_id:
            namespace, job_id_only = hf_job_id.split("/", 1)
            job_info = api.inspect_job(job_id=job_id_only, namespace=namespace)
        else:
            job_info = api.inspect_job(job_id=hf_job_id)

        # Extract status stage from JobStatus object
        if hasattr(job_info, 'status') and hasattr(job_info.status, 'stage'):
            status = job_info.status.stage
        else:
            status = str(job_info.status) if hasattr(job_info, 'status') else "unknown"

        return {
            "success": True,
            "job_id": hf_job_id,
            "status": status,
            "created_at": str(job_info.created_at) if hasattr(job_info, 'created_at') else None,
            "flavor": job_info.flavor if hasattr(job_info, 'flavor') else None,
            "url": job_info.url if hasattr(job_info, 'url') else None,
            "info": str(job_info)
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to fetch job status: {str(e)}",
            "job_id": hf_job_id
        }


def get_job_logs(hf_job_id: str, hf_token: Optional[str] = None) -> Dict:
    """
    Retrieve logs from a HuggingFace Job

    Args:
        hf_job_id: HF Job ID (format: username/job_hash or just job_hash)
        hf_token: HuggingFace token (optional, uses env if not provided)

    Returns:
        dict: Job logs information
    """
    try:
        from huggingface_hub import HfApi
    except ImportError:
        return {
            "success": False,
            "error": "huggingface_hub package not installed",
            "job_id": hf_job_id
        }

    token = hf_token or os.environ.get("HF_TOKEN")
    if not token:
        return {
            "success": False,
            "error": "HuggingFace token not configured",
            "job_id": hf_job_id
        }

    try:
        api = HfApi(token=token)

        # Parse job_id and namespace (username)
        # Format can be "username/job_hash" or just "job_hash"
        if "/" in hf_job_id:
            namespace, job_id_only = hf_job_id.split("/", 1)
            logs_iterable = api.fetch_job_logs(job_id=job_id_only, namespace=namespace)
        else:
            logs_iterable = api.fetch_job_logs(job_id=hf_job_id)

        # Convert iterable to string
        logs = "\n".join(logs_iterable)

        return {
            "success": True,
            "job_id": hf_job_id,
            "logs": logs
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to fetch job logs: {str(e)}",
            "job_id": hf_job_id,
            "logs": ""
        }


def list_user_jobs(hf_token: Optional[str] = None, limit: int = 10) -> Dict:
    """
    List recent jobs for the authenticated user

    Args:
        hf_token: HuggingFace token (optional, uses env if not provided)
        limit: Maximum number of jobs to return (applied after fetching)

    Returns:
        dict: List of user's jobs
    """
    try:
        from huggingface_hub import HfApi
    except ImportError:
        return {
            "success": False,
            "error": "huggingface_hub package not installed"
        }

    token = hf_token or os.environ.get("HF_TOKEN")
    if not token:
        return {
            "success": False,
            "error": "HuggingFace token not configured"
        }

    try:
        api = HfApi(token=token)
        # List user's jobs (no limit parameter in API, so we fetch all and slice)
        all_jobs = api.list_jobs()

        # Limit the results
        jobs_to_display = all_jobs[:limit] if limit > 0 else all_jobs

        job_list = []
        for job in jobs_to_display:
            # Extract owner name from JobOwner object
            owner_name = job.owner.name if hasattr(job, 'owner') and hasattr(job.owner, 'name') else None

            # Build job_id in the format: owner/id
            if owner_name and hasattr(job, 'id'):
                job_id = f"{owner_name}/{job.id}"
            elif hasattr(job, 'id'):
                job_id = job.id
            else:
                job_id = "unknown"

            # Extract status stage from JobStatus object
            if hasattr(job, 'status') and hasattr(job.status, 'stage'):
                status = job.status.stage
            else:
                status = str(job.status) if hasattr(job, 'status') else "unknown"

            job_list.append({
                "job_id": job_id,
                "status": status,
                "created_at": str(job.created_at) if hasattr(job, 'created_at') else None
            })

        return {
            "success": True,
            "jobs": job_list,
            "count": len(job_list)
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to list jobs: {str(e)}",
            "jobs": []
        }