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.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {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
+ ]