hud-python 0.4.1__py3-none-any.whl → 0.4.3__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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/telemetry/job.py
CHANGED
|
@@ -1,309 +1,309 @@
|
|
|
1
|
-
"""Job management for HUD SDK.
|
|
2
|
-
|
|
3
|
-
This module provides APIs for managing jobs - logical groupings of related tasks.
|
|
4
|
-
Jobs can be used to track experiments, batch processing, training runs, etc.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
import logging
|
|
11
|
-
import uuid
|
|
12
|
-
from contextlib import contextmanager
|
|
13
|
-
from datetime import UTC, datetime
|
|
14
|
-
from functools import wraps
|
|
15
|
-
from typing import TYPE_CHECKING, Any
|
|
16
|
-
|
|
17
|
-
from hud.settings import settings
|
|
18
|
-
from hud.shared import make_request, make_request_sync
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from collections.abc import Callable, Generator
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Job:
|
|
27
|
-
"""A job represents a collection of related tasks."""
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
job_id: str,
|
|
32
|
-
name: str,
|
|
33
|
-
metadata: dict[str, Any] | None = None,
|
|
34
|
-
dataset_link: str | None = None,
|
|
35
|
-
) -> None:
|
|
36
|
-
self.id = job_id
|
|
37
|
-
self.name = name
|
|
38
|
-
self.metadata = metadata or {}
|
|
39
|
-
self.dataset_link = dataset_link
|
|
40
|
-
self.status = "created"
|
|
41
|
-
self.created_at = datetime.now(UTC)
|
|
42
|
-
self.tasks: list[str] = []
|
|
43
|
-
|
|
44
|
-
def add_task(self, task_id: str) -> None:
|
|
45
|
-
"""Associate a task with this job."""
|
|
46
|
-
self.tasks.append(task_id)
|
|
47
|
-
|
|
48
|
-
async def update_status(self, status: str) -> None:
|
|
49
|
-
"""Update job status on the server."""
|
|
50
|
-
self.status = status
|
|
51
|
-
if settings.telemetry_enabled:
|
|
52
|
-
try:
|
|
53
|
-
payload = {
|
|
54
|
-
"name": self.name,
|
|
55
|
-
"status": status,
|
|
56
|
-
"metadata": self.metadata,
|
|
57
|
-
}
|
|
58
|
-
if self.dataset_link:
|
|
59
|
-
payload["dataset_link"] = self.dataset_link
|
|
60
|
-
|
|
61
|
-
await make_request(
|
|
62
|
-
method="POST",
|
|
63
|
-
url=f"{settings.hud_telemetry_url}/jobs/{self.id}/status",
|
|
64
|
-
json=payload,
|
|
65
|
-
api_key=settings.api_key,
|
|
66
|
-
)
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logger.warning("Failed to update job status: %s", e)
|
|
69
|
-
|
|
70
|
-
def update_status_sync(self, status: str) -> None:
|
|
71
|
-
"""Synchronously update job status on the server."""
|
|
72
|
-
self.status = status
|
|
73
|
-
if settings.telemetry_enabled:
|
|
74
|
-
try:
|
|
75
|
-
payload = {
|
|
76
|
-
"name": self.name,
|
|
77
|
-
"status": status,
|
|
78
|
-
"metadata": self.metadata,
|
|
79
|
-
}
|
|
80
|
-
if self.dataset_link:
|
|
81
|
-
payload["dataset_link"] = self.dataset_link
|
|
82
|
-
|
|
83
|
-
make_request_sync(
|
|
84
|
-
method="POST",
|
|
85
|
-
url=f"{settings.hud_telemetry_url}/jobs/{self.id}/status",
|
|
86
|
-
json=payload,
|
|
87
|
-
api_key=settings.api_key,
|
|
88
|
-
)
|
|
89
|
-
except Exception as e:
|
|
90
|
-
logger.warning("Failed to update job status: %s", e)
|
|
91
|
-
|
|
92
|
-
def __repr__(self) -> str:
|
|
93
|
-
return f"Job(id={self.id!r}, name={self.name!r}, status={self.status!r})"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# Global job registry for the decorator pattern
|
|
97
|
-
_current_job: Job | None = None
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _print_job_url(job_id: str, job_name: str) -> None:
|
|
101
|
-
"""Print the job URL in a colorful box."""
|
|
102
|
-
# Only print HUD URL if HUD telemetry is enabled and has API key
|
|
103
|
-
if not (settings.telemetry_enabled and settings.api_key):
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
url = f"https://app.hud.so/jobs/{job_id}"
|
|
107
|
-
header = f"🚀 Job '{job_name}' started:"
|
|
108
|
-
|
|
109
|
-
# ANSI color codes
|
|
110
|
-
DIM = "\033[90m" # Dim/Gray for border
|
|
111
|
-
GOLD = "\033[33m" # Gold/Yellow for URL
|
|
112
|
-
RESET = "\033[0m"
|
|
113
|
-
BOLD = "\033[1m"
|
|
114
|
-
|
|
115
|
-
# Calculate box width based on the longest line
|
|
116
|
-
box_width = max(len(url), len(header)) + 6
|
|
117
|
-
|
|
118
|
-
# Box drawing characters
|
|
119
|
-
top_border = "╔" + "═" * (box_width - 2) + "╗"
|
|
120
|
-
bottom_border = "╚" + "═" * (box_width - 2) + "╝"
|
|
121
|
-
divider = "╟" + "─" * (box_width - 2) + "╢"
|
|
122
|
-
|
|
123
|
-
# Center the content
|
|
124
|
-
header_padding = (box_width - len(header) - 2) // 2
|
|
125
|
-
url_padding = (box_width - len(url) - 2) // 2
|
|
126
|
-
|
|
127
|
-
# Print the box
|
|
128
|
-
print(f"\n{DIM}{top_border}{RESET}") # noqa: T201
|
|
129
|
-
print( # noqa: T201
|
|
130
|
-
f"{DIM}║{RESET}{' ' * header_padding}{header}{' ' * (box_width - len(header) - header_padding - 3)}{DIM}║{RESET}" # noqa: E501
|
|
131
|
-
)
|
|
132
|
-
print(f"{DIM}{divider}{RESET}") # noqa: T201
|
|
133
|
-
print( # noqa: T201
|
|
134
|
-
f"{DIM}║{RESET}{' ' * url_padding}{BOLD}{GOLD}{url}{RESET}{' ' * (box_width - len(url) - url_padding - 2)}{DIM}║{RESET}" # noqa: E501
|
|
135
|
-
)
|
|
136
|
-
print(f"{DIM}{bottom_border}{RESET}\n") # noqa: T201
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _print_job_complete_url(job_id: str, job_name: str, error_occurred: bool = False) -> None:
|
|
140
|
-
"""Print the job completion URL with appropriate messaging."""
|
|
141
|
-
# Only print HUD URL if HUD telemetry is enabled and has API key
|
|
142
|
-
if not (settings.telemetry_enabled and settings.api_key):
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
url = f"https://app.hud.so/jobs/{job_id}"
|
|
146
|
-
|
|
147
|
-
# ANSI color codes
|
|
148
|
-
GREEN = "\033[92m"
|
|
149
|
-
RED = "\033[91m"
|
|
150
|
-
GOLD = "\033[33m"
|
|
151
|
-
RESET = "\033[0m"
|
|
152
|
-
DIM = "\033[2m"
|
|
153
|
-
BOLD = "\033[1m"
|
|
154
|
-
|
|
155
|
-
if error_occurred:
|
|
156
|
-
print( # noqa: T201
|
|
157
|
-
f"\n{RED}✗ Job '{job_name}' failed!{RESET} {DIM}View details at:{RESET} {BOLD}{GOLD}{url}{RESET}\n" # noqa: E501
|
|
158
|
-
)
|
|
159
|
-
else:
|
|
160
|
-
print( # noqa: T201
|
|
161
|
-
f"\n{GREEN}✓ Job '{job_name}' complete!{RESET} {DIM}View all results at:{RESET} {BOLD}{GOLD}{url}{RESET}\n" # noqa: E501
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def get_current_job() -> Job | None:
|
|
166
|
-
"""Get the currently active job, if any."""
|
|
167
|
-
return _current_job
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@contextmanager
|
|
171
|
-
def job(
|
|
172
|
-
name: str,
|
|
173
|
-
metadata: dict[str, Any] | None = None,
|
|
174
|
-
job_id: str | None = None,
|
|
175
|
-
dataset_link: str | None = None,
|
|
176
|
-
) -> Generator[Job, None, None]:
|
|
177
|
-
"""Context manager for job tracking.
|
|
178
|
-
|
|
179
|
-
Groups related tasks together under a single job for tracking and organization.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
name: Human-readable job name
|
|
183
|
-
metadata: Optional metadata dictionary
|
|
184
|
-
job_id: Optional job ID (auto-generated if not provided)
|
|
185
|
-
dataset_link: Optional HuggingFace dataset identifier (e.g. "hud-evals/SheetBench-50")
|
|
186
|
-
|
|
187
|
-
Yields:
|
|
188
|
-
Job: The job object
|
|
189
|
-
|
|
190
|
-
Example:
|
|
191
|
-
with hud.job("training_run", {"model": "gpt-4"}) as job:
|
|
192
|
-
for epoch in range(10):
|
|
193
|
-
with hud.trace(f"epoch_{epoch}", job_id=job.id):
|
|
194
|
-
train_epoch()
|
|
195
|
-
"""
|
|
196
|
-
global _current_job
|
|
197
|
-
|
|
198
|
-
if not job_id:
|
|
199
|
-
job_id = str(uuid.uuid4())
|
|
200
|
-
|
|
201
|
-
job_obj = Job(job_id, name, metadata, dataset_link)
|
|
202
|
-
|
|
203
|
-
# Set as current job
|
|
204
|
-
old_job = _current_job
|
|
205
|
-
_current_job = job_obj
|
|
206
|
-
|
|
207
|
-
try:
|
|
208
|
-
# Update status to running synchronously to ensure job is registered before tasks start
|
|
209
|
-
job_obj.update_status_sync("running")
|
|
210
|
-
# Print the nice job URL box
|
|
211
|
-
_print_job_url(job_obj.id, job_obj.name)
|
|
212
|
-
yield job_obj
|
|
213
|
-
# Update status to completed synchronously to ensure it completes before process exit
|
|
214
|
-
job_obj.update_status_sync("completed")
|
|
215
|
-
# Print job completion message
|
|
216
|
-
_print_job_complete_url(job_obj.id, job_obj.name, error_occurred=False)
|
|
217
|
-
except Exception:
|
|
218
|
-
# Update status to failed synchronously to ensure it completes before process exit
|
|
219
|
-
job_obj.update_status_sync("failed")
|
|
220
|
-
# Print job failure message
|
|
221
|
-
_print_job_complete_url(job_obj.id, job_obj.name, error_occurred=True)
|
|
222
|
-
raise
|
|
223
|
-
finally:
|
|
224
|
-
_current_job = old_job
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def create_job(
|
|
228
|
-
name: str, metadata: dict[str, Any] | None = None, dataset_link: str | None = None
|
|
229
|
-
) -> Job:
|
|
230
|
-
"""Create a job without using context manager.
|
|
231
|
-
|
|
232
|
-
Useful when you need explicit control over job lifecycle.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
name: Human-readable job name
|
|
236
|
-
metadata: Optional metadata dictionary
|
|
237
|
-
dataset_link: Optional HuggingFace dataset identifier (e.g. "hud-evals/SheetBench-50")
|
|
238
|
-
|
|
239
|
-
Returns:
|
|
240
|
-
Job: The created job object
|
|
241
|
-
|
|
242
|
-
Example:
|
|
243
|
-
job = hud.create_job("data_processing")
|
|
244
|
-
try:
|
|
245
|
-
for item in items:
|
|
246
|
-
with hud.trace(f"process_{item.id}", job_id=job.id):
|
|
247
|
-
process(item)
|
|
248
|
-
finally:
|
|
249
|
-
await job.update_status("completed")
|
|
250
|
-
"""
|
|
251
|
-
job_id = str(uuid.uuid4())
|
|
252
|
-
return Job(job_id, name, metadata, dataset_link)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def job_decorator(name: str | None = None, **metadata: Any) -> Callable:
|
|
256
|
-
"""Decorator for functions that should be tracked as jobs.
|
|
257
|
-
|
|
258
|
-
Args:
|
|
259
|
-
name: Job name (defaults to function name)
|
|
260
|
-
**metadata: Additional metadata for the job
|
|
261
|
-
|
|
262
|
-
Example:
|
|
263
|
-
@hud.job_decorator("model_training", model="gpt-4", dataset="v2")
|
|
264
|
-
async def train_model(config):
|
|
265
|
-
# This entire function execution is tracked as a job
|
|
266
|
-
await model.train(config)
|
|
267
|
-
return model.evaluate()
|
|
268
|
-
"""
|
|
269
|
-
|
|
270
|
-
def decorator(func: Callable) -> Callable:
|
|
271
|
-
job_name = name or func.__name__
|
|
272
|
-
|
|
273
|
-
@wraps(func)
|
|
274
|
-
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
275
|
-
with job(job_name, metadata) as job_obj:
|
|
276
|
-
# Store job ID in function for access
|
|
277
|
-
func._current_job_id = job_obj.id
|
|
278
|
-
try:
|
|
279
|
-
return await func(*args, **kwargs)
|
|
280
|
-
finally:
|
|
281
|
-
delattr(func, "_current_job_id")
|
|
282
|
-
|
|
283
|
-
@wraps(func)
|
|
284
|
-
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
285
|
-
with job(job_name, metadata) as job_obj:
|
|
286
|
-
# Store job ID in function for access
|
|
287
|
-
func._current_job_id = job_obj.id
|
|
288
|
-
try:
|
|
289
|
-
return func(*args, **kwargs)
|
|
290
|
-
finally:
|
|
291
|
-
delattr(func, "_current_job_id")
|
|
292
|
-
|
|
293
|
-
# Return appropriate wrapper based on function type
|
|
294
|
-
if asyncio.iscoroutinefunction(func):
|
|
295
|
-
return async_wrapper
|
|
296
|
-
else:
|
|
297
|
-
return sync_wrapper
|
|
298
|
-
|
|
299
|
-
return decorator
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
# Convenience exports
|
|
303
|
-
__all__ = [
|
|
304
|
-
"Job",
|
|
305
|
-
"create_job",
|
|
306
|
-
"get_current_job",
|
|
307
|
-
"job",
|
|
308
|
-
"job_decorator",
|
|
309
|
-
]
|
|
1
|
+
"""Job management for HUD SDK.
|
|
2
|
+
|
|
3
|
+
This module provides APIs for managing jobs - logical groupings of related tasks.
|
|
4
|
+
Jobs can be used to track experiments, batch processing, training runs, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import uuid
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from hud.settings import settings
|
|
18
|
+
from hud.shared import make_request, make_request_sync
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Callable, Generator
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Job:
|
|
27
|
+
"""A job represents a collection of related tasks."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
job_id: str,
|
|
32
|
+
name: str,
|
|
33
|
+
metadata: dict[str, Any] | None = None,
|
|
34
|
+
dataset_link: str | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.id = job_id
|
|
37
|
+
self.name = name
|
|
38
|
+
self.metadata = metadata or {}
|
|
39
|
+
self.dataset_link = dataset_link
|
|
40
|
+
self.status = "created"
|
|
41
|
+
self.created_at = datetime.now(UTC)
|
|
42
|
+
self.tasks: list[str] = []
|
|
43
|
+
|
|
44
|
+
def add_task(self, task_id: str) -> None:
|
|
45
|
+
"""Associate a task with this job."""
|
|
46
|
+
self.tasks.append(task_id)
|
|
47
|
+
|
|
48
|
+
async def update_status(self, status: str) -> None:
|
|
49
|
+
"""Update job status on the server."""
|
|
50
|
+
self.status = status
|
|
51
|
+
if settings.telemetry_enabled:
|
|
52
|
+
try:
|
|
53
|
+
payload = {
|
|
54
|
+
"name": self.name,
|
|
55
|
+
"status": status,
|
|
56
|
+
"metadata": self.metadata,
|
|
57
|
+
}
|
|
58
|
+
if self.dataset_link:
|
|
59
|
+
payload["dataset_link"] = self.dataset_link
|
|
60
|
+
|
|
61
|
+
await make_request(
|
|
62
|
+
method="POST",
|
|
63
|
+
url=f"{settings.hud_telemetry_url}/jobs/{self.id}/status",
|
|
64
|
+
json=payload,
|
|
65
|
+
api_key=settings.api_key,
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning("Failed to update job status: %s", e)
|
|
69
|
+
|
|
70
|
+
def update_status_sync(self, status: str) -> None:
|
|
71
|
+
"""Synchronously update job status on the server."""
|
|
72
|
+
self.status = status
|
|
73
|
+
if settings.telemetry_enabled:
|
|
74
|
+
try:
|
|
75
|
+
payload = {
|
|
76
|
+
"name": self.name,
|
|
77
|
+
"status": status,
|
|
78
|
+
"metadata": self.metadata,
|
|
79
|
+
}
|
|
80
|
+
if self.dataset_link:
|
|
81
|
+
payload["dataset_link"] = self.dataset_link
|
|
82
|
+
|
|
83
|
+
make_request_sync(
|
|
84
|
+
method="POST",
|
|
85
|
+
url=f"{settings.hud_telemetry_url}/jobs/{self.id}/status",
|
|
86
|
+
json=payload,
|
|
87
|
+
api_key=settings.api_key,
|
|
88
|
+
)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.warning("Failed to update job status: %s", e)
|
|
91
|
+
|
|
92
|
+
def __repr__(self) -> str:
|
|
93
|
+
return f"Job(id={self.id!r}, name={self.name!r}, status={self.status!r})"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Global job registry for the decorator pattern
|
|
97
|
+
_current_job: Job | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _print_job_url(job_id: str, job_name: str) -> None:
|
|
101
|
+
"""Print the job URL in a colorful box."""
|
|
102
|
+
# Only print HUD URL if HUD telemetry is enabled and has API key
|
|
103
|
+
if not (settings.telemetry_enabled and settings.api_key):
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
url = f"https://app.hud.so/jobs/{job_id}"
|
|
107
|
+
header = f"🚀 Job '{job_name}' started:"
|
|
108
|
+
|
|
109
|
+
# ANSI color codes
|
|
110
|
+
DIM = "\033[90m" # Dim/Gray for border
|
|
111
|
+
GOLD = "\033[33m" # Gold/Yellow for URL
|
|
112
|
+
RESET = "\033[0m"
|
|
113
|
+
BOLD = "\033[1m"
|
|
114
|
+
|
|
115
|
+
# Calculate box width based on the longest line
|
|
116
|
+
box_width = max(len(url), len(header)) + 6
|
|
117
|
+
|
|
118
|
+
# Box drawing characters
|
|
119
|
+
top_border = "╔" + "═" * (box_width - 2) + "╗"
|
|
120
|
+
bottom_border = "╚" + "═" * (box_width - 2) + "╝"
|
|
121
|
+
divider = "╟" + "─" * (box_width - 2) + "╢"
|
|
122
|
+
|
|
123
|
+
# Center the content
|
|
124
|
+
header_padding = (box_width - len(header) - 2) // 2
|
|
125
|
+
url_padding = (box_width - len(url) - 2) // 2
|
|
126
|
+
|
|
127
|
+
# Print the box
|
|
128
|
+
print(f"\n{DIM}{top_border}{RESET}") # noqa: T201
|
|
129
|
+
print( # noqa: T201
|
|
130
|
+
f"{DIM}║{RESET}{' ' * header_padding}{header}{' ' * (box_width - len(header) - header_padding - 3)}{DIM}║{RESET}" # noqa: E501
|
|
131
|
+
)
|
|
132
|
+
print(f"{DIM}{divider}{RESET}") # noqa: T201
|
|
133
|
+
print( # noqa: T201
|
|
134
|
+
f"{DIM}║{RESET}{' ' * url_padding}{BOLD}{GOLD}{url}{RESET}{' ' * (box_width - len(url) - url_padding - 2)}{DIM}║{RESET}" # noqa: E501
|
|
135
|
+
)
|
|
136
|
+
print(f"{DIM}{bottom_border}{RESET}\n") # noqa: T201
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _print_job_complete_url(job_id: str, job_name: str, error_occurred: bool = False) -> None:
|
|
140
|
+
"""Print the job completion URL with appropriate messaging."""
|
|
141
|
+
# Only print HUD URL if HUD telemetry is enabled and has API key
|
|
142
|
+
if not (settings.telemetry_enabled and settings.api_key):
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
url = f"https://app.hud.so/jobs/{job_id}"
|
|
146
|
+
|
|
147
|
+
# ANSI color codes
|
|
148
|
+
GREEN = "\033[92m"
|
|
149
|
+
RED = "\033[91m"
|
|
150
|
+
GOLD = "\033[33m"
|
|
151
|
+
RESET = "\033[0m"
|
|
152
|
+
DIM = "\033[2m"
|
|
153
|
+
BOLD = "\033[1m"
|
|
154
|
+
|
|
155
|
+
if error_occurred:
|
|
156
|
+
print( # noqa: T201
|
|
157
|
+
f"\n{RED}✗ Job '{job_name}' failed!{RESET} {DIM}View details at:{RESET} {BOLD}{GOLD}{url}{RESET}\n" # noqa: E501
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
print( # noqa: T201
|
|
161
|
+
f"\n{GREEN}✓ Job '{job_name}' complete!{RESET} {DIM}View all results at:{RESET} {BOLD}{GOLD}{url}{RESET}\n" # noqa: E501
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_current_job() -> Job | None:
|
|
166
|
+
"""Get the currently active job, if any."""
|
|
167
|
+
return _current_job
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@contextmanager
|
|
171
|
+
def job(
|
|
172
|
+
name: str,
|
|
173
|
+
metadata: dict[str, Any] | None = None,
|
|
174
|
+
job_id: str | None = None,
|
|
175
|
+
dataset_link: str | None = None,
|
|
176
|
+
) -> Generator[Job, None, None]:
|
|
177
|
+
"""Context manager for job tracking.
|
|
178
|
+
|
|
179
|
+
Groups related tasks together under a single job for tracking and organization.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
name: Human-readable job name
|
|
183
|
+
metadata: Optional metadata dictionary
|
|
184
|
+
job_id: Optional job ID (auto-generated if not provided)
|
|
185
|
+
dataset_link: Optional HuggingFace dataset identifier (e.g. "hud-evals/SheetBench-50")
|
|
186
|
+
|
|
187
|
+
Yields:
|
|
188
|
+
Job: The job object
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
with hud.job("training_run", {"model": "gpt-4"}) as job:
|
|
192
|
+
for epoch in range(10):
|
|
193
|
+
with hud.trace(f"epoch_{epoch}", job_id=job.id):
|
|
194
|
+
train_epoch()
|
|
195
|
+
"""
|
|
196
|
+
global _current_job
|
|
197
|
+
|
|
198
|
+
if not job_id:
|
|
199
|
+
job_id = str(uuid.uuid4())
|
|
200
|
+
|
|
201
|
+
job_obj = Job(job_id, name, metadata, dataset_link)
|
|
202
|
+
|
|
203
|
+
# Set as current job
|
|
204
|
+
old_job = _current_job
|
|
205
|
+
_current_job = job_obj
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# Update status to running synchronously to ensure job is registered before tasks start
|
|
209
|
+
job_obj.update_status_sync("running")
|
|
210
|
+
# Print the nice job URL box
|
|
211
|
+
_print_job_url(job_obj.id, job_obj.name)
|
|
212
|
+
yield job_obj
|
|
213
|
+
# Update status to completed synchronously to ensure it completes before process exit
|
|
214
|
+
job_obj.update_status_sync("completed")
|
|
215
|
+
# Print job completion message
|
|
216
|
+
_print_job_complete_url(job_obj.id, job_obj.name, error_occurred=False)
|
|
217
|
+
except Exception:
|
|
218
|
+
# Update status to failed synchronously to ensure it completes before process exit
|
|
219
|
+
job_obj.update_status_sync("failed")
|
|
220
|
+
# Print job failure message
|
|
221
|
+
_print_job_complete_url(job_obj.id, job_obj.name, error_occurred=True)
|
|
222
|
+
raise
|
|
223
|
+
finally:
|
|
224
|
+
_current_job = old_job
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def create_job(
|
|
228
|
+
name: str, metadata: dict[str, Any] | None = None, dataset_link: str | None = None
|
|
229
|
+
) -> Job:
|
|
230
|
+
"""Create a job without using context manager.
|
|
231
|
+
|
|
232
|
+
Useful when you need explicit control over job lifecycle.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: Human-readable job name
|
|
236
|
+
metadata: Optional metadata dictionary
|
|
237
|
+
dataset_link: Optional HuggingFace dataset identifier (e.g. "hud-evals/SheetBench-50")
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Job: The created job object
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
job = hud.create_job("data_processing")
|
|
244
|
+
try:
|
|
245
|
+
for item in items:
|
|
246
|
+
with hud.trace(f"process_{item.id}", job_id=job.id):
|
|
247
|
+
process(item)
|
|
248
|
+
finally:
|
|
249
|
+
await job.update_status("completed")
|
|
250
|
+
"""
|
|
251
|
+
job_id = str(uuid.uuid4())
|
|
252
|
+
return Job(job_id, name, metadata, dataset_link)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def job_decorator(name: str | None = None, **metadata: Any) -> Callable:
|
|
256
|
+
"""Decorator for functions that should be tracked as jobs.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
name: Job name (defaults to function name)
|
|
260
|
+
**metadata: Additional metadata for the job
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
@hud.job_decorator("model_training", model="gpt-4", dataset="v2")
|
|
264
|
+
async def train_model(config):
|
|
265
|
+
# This entire function execution is tracked as a job
|
|
266
|
+
await model.train(config)
|
|
267
|
+
return model.evaluate()
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def decorator(func: Callable) -> Callable:
|
|
271
|
+
job_name = name or func.__name__
|
|
272
|
+
|
|
273
|
+
@wraps(func)
|
|
274
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
275
|
+
with job(job_name, metadata) as job_obj:
|
|
276
|
+
# Store job ID in function for access
|
|
277
|
+
func._current_job_id = job_obj.id
|
|
278
|
+
try:
|
|
279
|
+
return await func(*args, **kwargs)
|
|
280
|
+
finally:
|
|
281
|
+
delattr(func, "_current_job_id")
|
|
282
|
+
|
|
283
|
+
@wraps(func)
|
|
284
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
285
|
+
with job(job_name, metadata) as job_obj:
|
|
286
|
+
# Store job ID in function for access
|
|
287
|
+
func._current_job_id = job_obj.id
|
|
288
|
+
try:
|
|
289
|
+
return func(*args, **kwargs)
|
|
290
|
+
finally:
|
|
291
|
+
delattr(func, "_current_job_id")
|
|
292
|
+
|
|
293
|
+
# Return appropriate wrapper based on function type
|
|
294
|
+
if asyncio.iscoroutinefunction(func):
|
|
295
|
+
return async_wrapper
|
|
296
|
+
else:
|
|
297
|
+
return sync_wrapper
|
|
298
|
+
|
|
299
|
+
return decorator
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# Convenience exports
|
|
303
|
+
__all__ = [
|
|
304
|
+
"Job",
|
|
305
|
+
"create_job",
|
|
306
|
+
"get_current_job",
|
|
307
|
+
"job",
|
|
308
|
+
"job_decorator",
|
|
309
|
+
]
|