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/types.py
CHANGED
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import uuid
|
|
4
|
-
from typing import Any, Literal
|
|
5
|
-
|
|
6
|
-
from mcp.types import CallToolRequestParams, CallToolResult
|
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class MCPToolCall(CallToolRequestParams):
|
|
11
|
-
"""A tool call."""
|
|
12
|
-
|
|
13
|
-
id: str = Field(default_factory=lambda: str(uuid.uuid4())) # Unique identifier for reference
|
|
14
|
-
|
|
15
|
-
def __str__(self) -> str:
|
|
16
|
-
response = f"Tool: {self.name}"
|
|
17
|
-
if self.arguments:
|
|
18
|
-
response += f"\nArguments: {self.arguments}"
|
|
19
|
-
return response
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class MCPToolResult(CallToolResult):
|
|
23
|
-
"""A tool result."""
|
|
24
|
-
|
|
25
|
-
def __str__(self) -> str:
|
|
26
|
-
response = f"Content: {self.content}"
|
|
27
|
-
if self.structuredContent:
|
|
28
|
-
response += f"\nStructured Content: {self.structuredContent}"
|
|
29
|
-
if self.isError:
|
|
30
|
-
response += f"\nError: {self.isError}"
|
|
31
|
-
return response
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class AgentResponse(BaseModel):
|
|
35
|
-
"""A model response in the conversation."""
|
|
36
|
-
|
|
37
|
-
# --- FUNCTIONAL ---
|
|
38
|
-
tool_calls: list[MCPToolCall] = Field(default_factory=list)
|
|
39
|
-
done: bool = Field(default=False)
|
|
40
|
-
|
|
41
|
-
# --- TELEMETRY [app.hud.so] ---
|
|
42
|
-
# Responses
|
|
43
|
-
content: str | None = Field(default=None)
|
|
44
|
-
reasoning: str | None = Field(default=None)
|
|
45
|
-
info: dict[str, Any] = Field(default_factory=dict)
|
|
46
|
-
isError: bool = Field(default=False)
|
|
47
|
-
raw: Any | None = Field(default=None) # Include raw response for access to Choice objects
|
|
48
|
-
|
|
49
|
-
# Timestamps
|
|
50
|
-
start_timestamp: str | None = None
|
|
51
|
-
end_timestamp: str | None = None
|
|
52
|
-
|
|
53
|
-
def __str__(self) -> str:
|
|
54
|
-
response = ""
|
|
55
|
-
if self.reasoning:
|
|
56
|
-
response += f"Reasoning: {self.reasoning}\n"
|
|
57
|
-
if self.content:
|
|
58
|
-
response += f"Content: {self.content}\n"
|
|
59
|
-
if self.tool_calls:
|
|
60
|
-
response += f"""Tool Calls: {
|
|
61
|
-
", ".join([f"{tc.name}: {tc.arguments}" for tc in self.tool_calls])
|
|
62
|
-
}"""
|
|
63
|
-
if self.raw:
|
|
64
|
-
response += f"Raw: {self.raw}"
|
|
65
|
-
return response
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class TraceStep(BaseModel):
|
|
69
|
-
"""Canonical data for a single span (shared with telemetry)."""
|
|
70
|
-
|
|
71
|
-
# HUD identifiers
|
|
72
|
-
task_run_id: str | None = Field(default=None)
|
|
73
|
-
job_id: str | None = Field(default=None)
|
|
74
|
-
|
|
75
|
-
# Span category - can be any string, but "mcp" and "agent" are privileged on the platform
|
|
76
|
-
category: Literal["mcp", "agent"] | str = Field(default="mcp") # noqa: PYI051
|
|
77
|
-
|
|
78
|
-
# Generic I/O fields - works for any category
|
|
79
|
-
request: Any | None = None
|
|
80
|
-
result: Any | None = None
|
|
81
|
-
|
|
82
|
-
# Generic span info
|
|
83
|
-
type: str = Field(default="CLIENT")
|
|
84
|
-
|
|
85
|
-
# Timestamps (optional, for local tracking)
|
|
86
|
-
start_timestamp: str | None = None
|
|
87
|
-
end_timestamp: str | None = None
|
|
88
|
-
|
|
89
|
-
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
class Trace(BaseModel):
|
|
93
|
-
"""Unified result from agent execution (task or prompt).
|
|
94
|
-
|
|
95
|
-
Fields:
|
|
96
|
-
- done: Whether the run is complete
|
|
97
|
-
- reward: The reward for the run
|
|
98
|
-
- info: Additional metadata for the run
|
|
99
|
-
- content: The final content/response from the agent
|
|
100
|
-
- isError: Whether the execution resulted in an error
|
|
101
|
-
- trace: The steps taken in the run (empty if not tracing)
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
done: bool = Field(default=True)
|
|
105
|
-
reward: float = Field(default=0.0)
|
|
106
|
-
info: dict[str, Any] = Field(default_factory=dict)
|
|
107
|
-
content: str | None = Field(default=None)
|
|
108
|
-
isError: bool = Field(default=False)
|
|
109
|
-
trace: list[TraceStep] = Field(default_factory=list)
|
|
110
|
-
|
|
111
|
-
def append(self, step: TraceStep) -> None:
|
|
112
|
-
self.trace.append(step)
|
|
113
|
-
|
|
114
|
-
def populate_from_context(self) -> None:
|
|
115
|
-
"""Populate trace steps from the current trace context if available.
|
|
116
|
-
|
|
117
|
-
This checks if we're executing within a hud.trace() context and
|
|
118
|
-
automatically populates the trace field with collected steps.
|
|
119
|
-
"""
|
|
120
|
-
from hud.otel.context import get_current_task_run_id
|
|
121
|
-
from hud.telemetry.replay import get_trace
|
|
122
|
-
|
|
123
|
-
task_run_id = get_current_task_run_id()
|
|
124
|
-
if task_run_id:
|
|
125
|
-
collected_trace = get_trace(task_run_id)
|
|
126
|
-
if collected_trace:
|
|
127
|
-
self.trace = collected_trace.trace
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
__all__ = [
|
|
131
|
-
"AgentResponse",
|
|
132
|
-
"MCPToolCall",
|
|
133
|
-
"MCPToolResult",
|
|
134
|
-
"Trace",
|
|
135
|
-
"TraceStep",
|
|
136
|
-
]
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from mcp.types import CallToolRequestParams, CallToolResult
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MCPToolCall(CallToolRequestParams):
|
|
11
|
+
"""A tool call."""
|
|
12
|
+
|
|
13
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4())) # Unique identifier for reference
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
response = f"Tool: {self.name}"
|
|
17
|
+
if self.arguments:
|
|
18
|
+
response += f"\nArguments: {self.arguments}"
|
|
19
|
+
return response
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MCPToolResult(CallToolResult):
|
|
23
|
+
"""A tool result."""
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
response = f"Content: {self.content}"
|
|
27
|
+
if self.structuredContent:
|
|
28
|
+
response += f"\nStructured Content: {self.structuredContent}"
|
|
29
|
+
if self.isError:
|
|
30
|
+
response += f"\nError: {self.isError}"
|
|
31
|
+
return response
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentResponse(BaseModel):
|
|
35
|
+
"""A model response in the conversation."""
|
|
36
|
+
|
|
37
|
+
# --- FUNCTIONAL ---
|
|
38
|
+
tool_calls: list[MCPToolCall] = Field(default_factory=list)
|
|
39
|
+
done: bool = Field(default=False)
|
|
40
|
+
|
|
41
|
+
# --- TELEMETRY [app.hud.so] ---
|
|
42
|
+
# Responses
|
|
43
|
+
content: str | None = Field(default=None)
|
|
44
|
+
reasoning: str | None = Field(default=None)
|
|
45
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
46
|
+
isError: bool = Field(default=False)
|
|
47
|
+
raw: Any | None = Field(default=None) # Include raw response for access to Choice objects
|
|
48
|
+
|
|
49
|
+
# Timestamps
|
|
50
|
+
start_timestamp: str | None = None
|
|
51
|
+
end_timestamp: str | None = None
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
response = ""
|
|
55
|
+
if self.reasoning:
|
|
56
|
+
response += f"Reasoning: {self.reasoning}\n"
|
|
57
|
+
if self.content:
|
|
58
|
+
response += f"Content: {self.content}\n"
|
|
59
|
+
if self.tool_calls:
|
|
60
|
+
response += f"""Tool Calls: {
|
|
61
|
+
", ".join([f"{tc.name}: {tc.arguments}" for tc in self.tool_calls])
|
|
62
|
+
}"""
|
|
63
|
+
if self.raw:
|
|
64
|
+
response += f"Raw: {self.raw}"
|
|
65
|
+
return response
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TraceStep(BaseModel):
|
|
69
|
+
"""Canonical data for a single span (shared with telemetry)."""
|
|
70
|
+
|
|
71
|
+
# HUD identifiers
|
|
72
|
+
task_run_id: str | None = Field(default=None)
|
|
73
|
+
job_id: str | None = Field(default=None)
|
|
74
|
+
|
|
75
|
+
# Span category - can be any string, but "mcp" and "agent" are privileged on the platform
|
|
76
|
+
category: Literal["mcp", "agent"] | str = Field(default="mcp") # noqa: PYI051
|
|
77
|
+
|
|
78
|
+
# Generic I/O fields - works for any category
|
|
79
|
+
request: Any | None = None
|
|
80
|
+
result: Any | None = None
|
|
81
|
+
|
|
82
|
+
# Generic span info
|
|
83
|
+
type: str = Field(default="CLIENT")
|
|
84
|
+
|
|
85
|
+
# Timestamps (optional, for local tracking)
|
|
86
|
+
start_timestamp: str | None = None
|
|
87
|
+
end_timestamp: str | None = None
|
|
88
|
+
|
|
89
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Trace(BaseModel):
|
|
93
|
+
"""Unified result from agent execution (task or prompt).
|
|
94
|
+
|
|
95
|
+
Fields:
|
|
96
|
+
- done: Whether the run is complete
|
|
97
|
+
- reward: The reward for the run
|
|
98
|
+
- info: Additional metadata for the run
|
|
99
|
+
- content: The final content/response from the agent
|
|
100
|
+
- isError: Whether the execution resulted in an error
|
|
101
|
+
- trace: The steps taken in the run (empty if not tracing)
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
done: bool = Field(default=True)
|
|
105
|
+
reward: float = Field(default=0.0)
|
|
106
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
107
|
+
content: str | None = Field(default=None)
|
|
108
|
+
isError: bool = Field(default=False)
|
|
109
|
+
trace: list[TraceStep] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
def append(self, step: TraceStep) -> None:
|
|
112
|
+
self.trace.append(step)
|
|
113
|
+
|
|
114
|
+
def populate_from_context(self) -> None:
|
|
115
|
+
"""Populate trace steps from the current trace context if available.
|
|
116
|
+
|
|
117
|
+
This checks if we're executing within a hud.trace() context and
|
|
118
|
+
automatically populates the trace field with collected steps.
|
|
119
|
+
"""
|
|
120
|
+
from hud.otel.context import get_current_task_run_id
|
|
121
|
+
from hud.telemetry.replay import get_trace
|
|
122
|
+
|
|
123
|
+
task_run_id = get_current_task_run_id()
|
|
124
|
+
if task_run_id:
|
|
125
|
+
collected_trace = get_trace(task_run_id)
|
|
126
|
+
if collected_trace:
|
|
127
|
+
self.trace = collected_trace.trace
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
"AgentResponse",
|
|
132
|
+
"MCPToolCall",
|
|
133
|
+
"MCPToolResult",
|
|
134
|
+
"Trace",
|
|
135
|
+
"TraceStep",
|
|
136
|
+
]
|
hud/utils/__init__.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from .design import HUDDesign, design
|
|
4
|
-
from .telemetry import stream
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"HUDDesign",
|
|
8
|
-
"design",
|
|
9
|
-
"stream",
|
|
10
|
-
]
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .design import HUDDesign, design
|
|
4
|
+
from .telemetry import stream
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"HUDDesign",
|
|
8
|
+
"design",
|
|
9
|
+
"stream",
|
|
10
|
+
]
|
hud/utils/async_utils.py
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
"""Async utilities for HUD SDK.
|
|
2
|
-
|
|
3
|
-
This module provides utilities for running async code in various environments,
|
|
4
|
-
including Jupyter notebooks and synchronous contexts.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
import logging
|
|
11
|
-
import threading
|
|
12
|
-
from typing import TYPE_CHECKING, Any
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from collections.abc import Coroutine
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def fire_and_forget(coro: Coroutine[Any, Any, Any], description: str = "task") -> None:
|
|
21
|
-
"""Execute a coroutine in a fire-and-forget manner.
|
|
22
|
-
|
|
23
|
-
This function handles running async code in various contexts:
|
|
24
|
-
- When an event loop is already running (normal async context)
|
|
25
|
-
- When no event loop exists (sync context, some Jupyter setups)
|
|
26
|
-
- Gracefully handles interpreter shutdown
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
coro: The coroutine to execute
|
|
30
|
-
description: Description of the task for logging (e.g., "update job status")
|
|
31
|
-
|
|
32
|
-
Example:
|
|
33
|
-
fire_and_forget(
|
|
34
|
-
some_async_function(),
|
|
35
|
-
description="update status"
|
|
36
|
-
)
|
|
37
|
-
"""
|
|
38
|
-
try:
|
|
39
|
-
# Try to get current event loop
|
|
40
|
-
loop = asyncio.get_running_loop()
|
|
41
|
-
# Schedule the coroutine
|
|
42
|
-
task = loop.create_task(coro)
|
|
43
|
-
# Add error handler to prevent unhandled exceptions
|
|
44
|
-
task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None)
|
|
45
|
-
except RuntimeError:
|
|
46
|
-
# No running event loop (e.g., Jupyter without %autoawait, sync context)
|
|
47
|
-
try:
|
|
48
|
-
# Try to run in a thread as a fallback
|
|
49
|
-
def run_in_thread() -> None:
|
|
50
|
-
loop = asyncio.new_event_loop()
|
|
51
|
-
asyncio.set_event_loop(loop)
|
|
52
|
-
try:
|
|
53
|
-
loop.run_until_complete(coro)
|
|
54
|
-
except Exception as e:
|
|
55
|
-
# Suppress warnings about interpreter shutdown
|
|
56
|
-
if "interpreter shutdown" not in str(e):
|
|
57
|
-
logger.debug("Error in threaded %s: %s", description, e)
|
|
58
|
-
|
|
59
|
-
thread = threading.Thread(target=run_in_thread, daemon=True)
|
|
60
|
-
thread.start()
|
|
61
|
-
except Exception as e:
|
|
62
|
-
# If that fails too, just log and continue
|
|
63
|
-
# Special case: suppress "cannot schedule new futures after interpreter shutdown"
|
|
64
|
-
if "interpreter shutdown" not in str(e):
|
|
65
|
-
logger.debug("Could not %s - no event loop available: %s", description, e)
|
|
1
|
+
"""Async utilities for HUD SDK.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for running async code in various environments,
|
|
4
|
+
including Jupyter notebooks and synchronous contexts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Coroutine
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fire_and_forget(coro: Coroutine[Any, Any, Any], description: str = "task") -> None:
|
|
21
|
+
"""Execute a coroutine in a fire-and-forget manner.
|
|
22
|
+
|
|
23
|
+
This function handles running async code in various contexts:
|
|
24
|
+
- When an event loop is already running (normal async context)
|
|
25
|
+
- When no event loop exists (sync context, some Jupyter setups)
|
|
26
|
+
- Gracefully handles interpreter shutdown
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
coro: The coroutine to execute
|
|
30
|
+
description: Description of the task for logging (e.g., "update job status")
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
fire_and_forget(
|
|
34
|
+
some_async_function(),
|
|
35
|
+
description="update status"
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Try to get current event loop
|
|
40
|
+
loop = asyncio.get_running_loop()
|
|
41
|
+
# Schedule the coroutine
|
|
42
|
+
task = loop.create_task(coro)
|
|
43
|
+
# Add error handler to prevent unhandled exceptions
|
|
44
|
+
task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None)
|
|
45
|
+
except RuntimeError:
|
|
46
|
+
# No running event loop (e.g., Jupyter without %autoawait, sync context)
|
|
47
|
+
try:
|
|
48
|
+
# Try to run in a thread as a fallback
|
|
49
|
+
def run_in_thread() -> None:
|
|
50
|
+
loop = asyncio.new_event_loop()
|
|
51
|
+
asyncio.set_event_loop(loop)
|
|
52
|
+
try:
|
|
53
|
+
loop.run_until_complete(coro)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
# Suppress warnings about interpreter shutdown
|
|
56
|
+
if "interpreter shutdown" not in str(e):
|
|
57
|
+
logger.debug("Error in threaded %s: %s", description, e)
|
|
58
|
+
|
|
59
|
+
thread = threading.Thread(target=run_in_thread, daemon=True)
|
|
60
|
+
thread.start()
|
|
61
|
+
except Exception as e:
|
|
62
|
+
# If that fails too, just log and continue
|
|
63
|
+
# Special case: suppress "cannot schedule new futures after interpreter shutdown"
|
|
64
|
+
if "interpreter shutdown" not in str(e):
|
|
65
|
+
logger.debug("Could not %s - no event loop available: %s", description, e)
|