From c9aee820cf1e538fbdbfbc424846ebdd338c8db5 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Fri, 16 Jan 2026 23:44:54 -0500 Subject: [PATCH 01/16] feat: implement unified baseline adapters for VLM comparison Add comprehensive unified baseline adapters supporting Claude, GPT, and Gemini models across multiple evaluation tracks: Provider Abstraction (models/providers/): - BaseAPIProvider ABC with common interface for all providers - AnthropicProvider: Base64 PNG encoding, Messages API - OpenAIProvider: Data URL format, Chat Completions API - GoogleProvider: Native PIL Image support, GenerateContent API - Factory functions: get_provider(), resolve_model_alias() - Error hierarchy: ProviderError, AuthenticationError, RateLimitError Baseline Module (baselines/): - TrackType enum: TRACK_A (coords), TRACK_B (ReAct), TRACK_C (SoM) - TrackConfig dataclass with factory methods for each track - BaselineConfig with model alias resolution and registry - PromptBuilder for track-specific system prompts and user content - UnifiedResponseParser supporting JSON, function-call, PyAutoGUI formats - ElementRegistry for element_id to coordinate conversion Benchmark Integration: - UnifiedBaselineAgent wrapping UnifiedBaselineAdapter for benchmarks - Converts BenchmarkObservation -> adapter format -> BenchmarkAction - Support for all three tracks via --track flag CLI Commands (baselines/cli.py): - run: Single model prediction with track selection - compare: Multi-model comparison on same task - list-models: Show available models and providers All 92 tests pass. Ready for model comparison experiments. Co-Authored-By: Claude Sonnet 4.5 --- openadapt_ml/baselines/__init__.py | 82 ++- openadapt_ml/baselines/config.py | 197 ++++- openadapt_ml/baselines/parser.py | 807 ++++++++++++++++++--- openadapt_ml/baselines/prompts.py | 713 +++++++++++++++--- openadapt_ml/benchmarks/__init__.py | 2 + openadapt_ml/benchmarks/agent.py | 278 +++++++ openadapt_ml/models/providers/__init__.py | 193 ++++- openadapt_ml/models/providers/anthropic.py | 209 +++++- openadapt_ml/models/providers/base.py | 190 +++++ openadapt_ml/models/providers/google.py | 298 +++++++- openadapt_ml/models/providers/openai.py | 280 ++++++- tests/test_baselines.py | 13 +- 12 files changed, 2935 insertions(+), 327 deletions(-) diff --git a/openadapt_ml/baselines/__init__.py b/openadapt_ml/baselines/__init__.py index 467a8dd..d5e5520 100644 --- a/openadapt_ml/baselines/__init__.py +++ b/openadapt_ml/baselines/__init__.py @@ -7,6 +7,12 @@ - Track B: ReAct-style reasoning with coordinates - Track C: Set-of-Mark element selection +Based on SOTA patterns from: +- Claude Computer Use (Anthropic) +- Microsoft UFO/UFO2 +- OSWorld benchmark +- Agent-S/Agent-S2 (Simular AI) + Usage: from openadapt_ml.baselines import UnifiedBaselineAdapter, BaselineConfig, TrackConfig @@ -21,35 +27,95 @@ track=TrackConfig.track_c(), ) adapter = UnifiedBaselineAdapter(config) + + # OSWorld-compatible configuration + config = BaselineConfig( + provider="openai", + model="gpt-5.2", + track=TrackConfig.osworld_compatible(), + ) + + # Parse responses directly + from openadapt_ml.baselines import UnifiedResponseParser, ElementRegistry + + parser = UnifiedResponseParser() + action = parser.parse('{"action": "CLICK", "x": 0.5, "y": 0.3}') + + # With element ID to coordinate conversion + registry = ElementRegistry.from_a11y_tree(tree) + parser = UnifiedResponseParser(element_registry=registry) + action = parser.parse_and_resolve('{"action": "CLICK", "element_id": 17}') """ from openadapt_ml.baselines.adapter import UnifiedBaselineAdapter from openadapt_ml.baselines.config import ( + # Enums + ActionOutputFormat, + CoordinateSystem, + TrackType, + # Config dataclasses BaselineConfig, ModelSpec, + ReActConfig, + ScreenConfig, + SoMConfig, TrackConfig, - TrackType, + # Registry MODELS, - get_model_spec, + # Helper functions get_default_model, + get_model_spec, +) +from openadapt_ml.baselines.parser import ( + ElementRegistry, + ParsedAction, + UIElement, + UnifiedResponseParser, +) +from openadapt_ml.baselines.prompts import ( + # System prompts + FORMAT_PROMPTS, + SYSTEM_PROMPT_OSWORLD, + SYSTEM_PROMPT_TRACK_A, + SYSTEM_PROMPT_TRACK_B, + SYSTEM_PROMPT_TRACK_C, + SYSTEM_PROMPT_UFO, + SYSTEM_PROMPTS, + # Builder class + PromptBuilder, ) -from openadapt_ml.baselines.parser import ParsedAction, UnifiedResponseParser -from openadapt_ml.baselines.prompts import PromptBuilder __all__ = [ # Main adapter "UnifiedBaselineAdapter", - # Configuration - "BaselineConfig", - "TrackConfig", + # Configuration - Enums + "ActionOutputFormat", + "CoordinateSystem", "TrackType", + # Configuration - Dataclasses + "BaselineConfig", "ModelSpec", + "ReActConfig", + "ScreenConfig", + "SoMConfig", + "TrackConfig", + # Configuration - Registry "MODELS", - "get_model_spec", + # Configuration - Functions "get_default_model", + "get_model_spec", # Parsing + "ElementRegistry", "ParsedAction", + "UIElement", "UnifiedResponseParser", # Prompts + "FORMAT_PROMPTS", "PromptBuilder", + "SYSTEM_PROMPT_OSWORLD", + "SYSTEM_PROMPT_TRACK_A", + "SYSTEM_PROMPT_TRACK_B", + "SYSTEM_PROMPT_TRACK_C", + "SYSTEM_PROMPT_UFO", + "SYSTEM_PROMPTS", ] diff --git a/openadapt_ml/baselines/config.py b/openadapt_ml/baselines/config.py index cbcab37..5950a33 100644 --- a/openadapt_ml/baselines/config.py +++ b/openadapt_ml/baselines/config.py @@ -1,6 +1,11 @@ """Configuration for baseline adapters. Defines track types, model registry, and configuration dataclasses. +Based on SOTA patterns from: +- Claude Computer Use API +- Microsoft UFO/UFO2 +- OSWorld benchmark +- Agent-S/Agent-S2 """ from __future__ import annotations @@ -23,6 +28,113 @@ class TrackType(str, Enum): TRACK_C = "set_of_mark" +class CoordinateSystem(str, Enum): + """Coordinate system for action output. + + NORMALIZED: Coordinates in 0.0-1.0 range (relative to screen) + PIXEL: Absolute pixel coordinates + PERCENTAGE: Coordinates as percentages (0-100) + """ + + NORMALIZED = "normalized" + PIXEL = "pixel" + PERCENTAGE = "percentage" + + +class ActionOutputFormat(str, Enum): + """Output format style for model responses. + + JSON: Structured JSON object + FUNCTION_CALL: Function-style like CLICK(x, y) + PYAUTOGUI: PyAutoGUI-style Python code (OSWorld compatible) + """ + + JSON = "json" + FUNCTION_CALL = "function_call" + PYAUTOGUI = "pyautogui" + + +@dataclass +class SoMConfig: + """Configuration for Set-of-Mark (SoM) overlay. + + Controls how UI elements are labeled and displayed. + Based on patterns from SoM paper and OMNI-parser. + + Attributes: + overlay_enabled: Whether to draw element overlays on screenshot. + label_format: Format for element labels ("[{id}]", "{id}", "e{id}"). + font_size: Font size for labels in pixels. + label_background_color: RGBA tuple for label background. + label_text_color: RGB tuple for label text. + max_elements: Maximum elements to include (0=unlimited). + include_roles: Element roles to include (None=all). + exclude_roles: Element roles to exclude. + min_element_area: Minimum element area in pixels to include. + include_invisible: Whether to include non-visible elements. + """ + + overlay_enabled: bool = True + label_format: str = "[{id}]" # "[1]", "1", "e1" + font_size: int = 12 + label_background_color: tuple[int, int, int, int] = (0, 120, 255, 200) # Blue + label_text_color: tuple[int, int, int] = (255, 255, 255) # White + max_elements: int = 100 + include_roles: list[str] | None = None # None = include all + exclude_roles: list[str] = field( + default_factory=lambda: ["group", "generic", "static_text", "separator"] + ) + min_element_area: int = 100 # Minimum bbox area in pixels + include_invisible: bool = False + + +@dataclass +class ReActConfig: + """Configuration for ReAct-style reasoning. + + Controls the observation-thought-action cycle used in Track B. + Based on ReAct paper and UFO's Observation->Thought->Action pattern. + + Attributes: + require_observation: Whether to require explicit observation. + require_thought: Whether to require reasoning explanation. + require_plan: Whether to require multi-step plan. + max_plan_steps: Maximum steps in plan output. + thinking_budget: Token budget for thinking (Claude extended thinking). + """ + + require_observation: bool = True + require_thought: bool = True + require_plan: bool = False + max_plan_steps: int = 5 + thinking_budget: int | None = None # For Claude extended thinking + + +@dataclass +class ScreenConfig: + """Screen/display configuration for coordinate handling. + + Attributes: + width: Display width in pixels. + height: Display height in pixels. + coordinate_system: How coordinates are represented. + scale_factor: DPI scale factor (1.0 = standard, 2.0 = retina). + """ + + width: int = 1920 + height: int = 1080 + coordinate_system: CoordinateSystem = CoordinateSystem.NORMALIZED + scale_factor: float = 1.0 + + def normalize_coords(self, x: float, y: float) -> tuple[float, float]: + """Convert pixel coordinates to normalized (0-1).""" + return (x / self.width, y / self.height) + + def denormalize_coords(self, x: float, y: float) -> tuple[int, int]: + """Convert normalized coordinates to pixels.""" + return (int(x * self.width), int(y * self.height)) + + @dataclass class TrackConfig: """Configuration for a specific evaluation track. @@ -30,54 +142,125 @@ class TrackConfig: Attributes: track_type: The track type (A, B, or C). output_format: Expected output format string. + action_format: Style of action output (JSON, function, pyautogui). use_som: Whether to use Set-of-Mark overlay. + som_config: Configuration for SoM (Track C). use_a11y_tree: Whether to include accessibility tree. max_a11y_elements: Max elements in a11y tree (truncation). include_reasoning: Whether to request reasoning steps. + react_config: Configuration for ReAct (Track B). include_history: Whether to include action history. max_history_steps: Max history steps to include. + screen_config: Screen/coordinate configuration. + verify_after_action: Request screenshot verification after actions. """ track_type: TrackType output_format: str + action_format: ActionOutputFormat = ActionOutputFormat.JSON use_som: bool = False + som_config: SoMConfig | None = None use_a11y_tree: bool = True max_a11y_elements: int = 50 include_reasoning: bool = False + react_config: ReActConfig | None = None include_history: bool = True max_history_steps: int = 5 + screen_config: ScreenConfig = field(default_factory=ScreenConfig) + verify_after_action: bool = False # Claude computer use best practice @classmethod - def track_a(cls) -> "TrackConfig": - """Create Track A (Direct Coordinates) config.""" + def track_a(cls, **kwargs: Any) -> "TrackConfig": + """Create Track A (Direct Coordinates) config. + + Simplest track: screenshot + goal -> coordinates. + No reasoning or element IDs. + """ return cls( track_type=TrackType.TRACK_A, output_format='{"action": "CLICK", "x": float, "y": float}', + action_format=ActionOutputFormat.JSON, use_som=False, use_a11y_tree=True, include_reasoning=False, + **kwargs, ) @classmethod - def track_b(cls) -> "TrackConfig": - """Create Track B (ReAct with Coordinates) config.""" + def track_b(cls, **kwargs: Any) -> "TrackConfig": + """Create Track B (ReAct with Coordinates) config. + + Includes observation->thought->action cycle. + Based on ReAct, UFO, and Claude thinking patterns. + """ + react_config = kwargs.pop("react_config", None) or ReActConfig() return cls( track_type=TrackType.TRACK_B, - output_format='{"thought": str, "action": "CLICK", "x": float, "y": float}', + output_format='{"observation": str, "thought": str, "action": "CLICK", "x": float, "y": float}', + action_format=ActionOutputFormat.JSON, use_som=False, use_a11y_tree=True, include_reasoning=True, + react_config=react_config, + **kwargs, ) @classmethod - def track_c(cls) -> "TrackConfig": - """Create Track C (Set-of-Mark) config.""" + def track_c(cls, **kwargs: Any) -> "TrackConfig": + """Create Track C (Set-of-Mark) config. + + Uses numbered element labels instead of coordinates. + Based on SoM paper and OMNI-parser patterns. + """ + som_config = kwargs.pop("som_config", None) or SoMConfig() return cls( track_type=TrackType.TRACK_C, output_format='{"action": "CLICK", "element_id": int}', + action_format=ActionOutputFormat.JSON, use_som=True, + som_config=som_config, use_a11y_tree=True, include_reasoning=False, + **kwargs, + ) + + @classmethod + def osworld_compatible(cls, **kwargs: Any) -> "TrackConfig": + """Create OSWorld-compatible config. + + Uses PyAutoGUI-style action format for OSWorld benchmark. + """ + return cls( + track_type=TrackType.TRACK_A, + output_format="pyautogui.click(x, y)", + action_format=ActionOutputFormat.PYAUTOGUI, + use_som=False, + use_a11y_tree=True, + include_reasoning=False, + **kwargs, + ) + + @classmethod + def ufo_compatible(cls, **kwargs: Any) -> "TrackConfig": + """Create UFO-compatible config. + + Uses UFO's AppAgent output format with observation/thought/plan. + """ + react_config = kwargs.pop("react_config", None) or ReActConfig( + require_observation=True, + require_thought=True, + require_plan=True, + ) + return cls( + track_type=TrackType.TRACK_B, + output_format='{"Observation": str, "Thought": str, "ControlLabel": int, "Function": str, "Args": list}', + action_format=ActionOutputFormat.JSON, + use_som=True, + som_config=SoMConfig(), + use_a11y_tree=True, + include_reasoning=True, + react_config=react_config, + **kwargs, ) diff --git a/openadapt_ml/baselines/parser.py b/openadapt_ml/baselines/parser.py index c73ca95..0139070 100644 --- a/openadapt_ml/baselines/parser.py +++ b/openadapt_ml/baselines/parser.py @@ -1,14 +1,167 @@ """Response parsing for baseline adapters. -Extracts structured actions from VLM responses. +Extracts structured actions from VLM responses with support for: +- JSON format extraction +- Function-call syntax (CLICK(x, y)) +- PyAutoGUI format (OSWorld compatible) +- UFO format (Observation/Thought/ControlLabel) +- Element ID to coordinate normalization +- Robust fallback parsing + +Based on patterns from: +- Claude Computer Use +- OSWorld benchmark +- Microsoft UFO +- Agent-S """ from __future__ import annotations import json +import logging import re from dataclasses import dataclass, field -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from openadapt_ml.baselines.config import ScreenConfig + +logger = logging.getLogger(__name__) + + +@dataclass +class UIElement: + """UI element with bounding box for coordinate conversion. + + Used to convert element_id actions to coordinate actions. + """ + + element_id: int | str + role: str = "" + name: str = "" + bbox: tuple[float, float, float, float] | None = None # (x1, y1, x2, y2) + + @property + def center(self) -> tuple[float, float] | None: + """Get center point of element.""" + if self.bbox is None: + return None + x1, y1, x2, y2 = self.bbox + return ((x1 + x2) / 2, (y1 + y2) / 2) + + +@dataclass +class ElementRegistry: + """Registry of UI elements for element_id to coordinate conversion. + + Used by the parser to convert Track C (SoM) element IDs to + Track A coordinates when needed. + """ + + elements: dict[int, UIElement] = field(default_factory=dict) + screen_width: int = 1920 + screen_height: int = 1080 + is_normalized: bool = True # Whether bbox coordinates are normalized (0-1) + + def add_element( + self, + element_id: int | str, + bbox: tuple[float, float, float, float], + role: str = "", + name: str = "", + ) -> None: + """Add an element to the registry.""" + eid = int(element_id) if isinstance(element_id, str) else element_id + self.elements[eid] = UIElement( + element_id=eid, role=role, name=name, bbox=bbox + ) + + def get_element(self, element_id: int) -> UIElement | None: + """Get element by ID.""" + return self.elements.get(element_id) + + def get_center_coords( + self, element_id: int, normalize: bool = True + ) -> tuple[float, float] | None: + """Get center coordinates for an element. + + Args: + element_id: Element ID to look up. + normalize: Whether to return normalized (0-1) coordinates. + + Returns: + (x, y) center coordinates, or None if element not found. + """ + element = self.get_element(element_id) + if element is None or element.bbox is None: + return None + + center = element.center + if center is None: + return None + + x, y = center + + # Handle normalization + if self.is_normalized and not normalize: + # Convert from normalized to pixels + x = x * self.screen_width + y = y * self.screen_height + elif not self.is_normalized and normalize: + # Convert from pixels to normalized + x = x / self.screen_width + y = y / self.screen_height + + return (x, y) + + @classmethod + def from_a11y_tree( + cls, + tree: dict[str, Any] | list[dict[str, Any]], + screen_width: int = 1920, + screen_height: int = 1080, + ) -> "ElementRegistry": + """Build registry from accessibility tree. + + Args: + tree: Accessibility tree as dict or list of element dicts. + screen_width: Screen width for coordinate conversion. + screen_height: Screen height for coordinate conversion. + + Returns: + ElementRegistry with all elements from tree. + """ + registry = cls(screen_width=screen_width, screen_height=screen_height) + + def process_node(node: dict[str, Any]) -> None: + node_id = node.get("id", node.get("node_id", node.get("element_id"))) + if node_id is not None: + try: + eid = int(str(node_id).replace("e", "").replace("[", "").replace("]", "")) + bbox = node.get("bbox", node.get("bounds")) + if bbox and len(bbox) >= 4: + registry.add_element( + element_id=eid, + bbox=tuple(bbox[:4]), + role=node.get("role", ""), + name=node.get("name", ""), + ) + except (ValueError, TypeError): + pass + + # Process children + for child in node.get("children", []): + if isinstance(child, dict): + process_node(child) + + if isinstance(tree, dict): + process_node(tree) + elif isinstance(tree, list): + for node in tree: + if isinstance(node, dict): + process_node(node) + + return registry @dataclass @@ -16,16 +169,22 @@ class ParsedAction: """Parsed action from model response. Attributes: - action_type: Action type (click, type, key, scroll, done, unknown). + action_type: Action type (click, type, key, scroll, done, wait, fail, unknown). x: X coordinate (normalized 0-1) for coordinate actions. y: Y coordinate (normalized 0-1) for coordinate actions. element_id: Element ID for SoM actions. text: Text content for type actions. key: Key name for key actions. + modifiers: Key modifiers (ctrl, shift, alt) for key/hotkey actions. direction: Scroll direction for scroll actions. - thought: Reasoning text (for ReAct track). + amount: Scroll amount for scroll actions. + observation: Observed state description (ReAct/UFO format). + thought: Reasoning text (ReAct/UFO format). + plan: Multi-step plan (UFO format). + status: Execution status (UFO format: CONTINUE, FINISH, ERROR). raw_response: Original model response. parse_error: Error message if parsing failed. + confidence: Parser confidence score (0-1). metadata: Additional parsed data. """ @@ -35,10 +194,16 @@ class ParsedAction: element_id: int | None = None text: str | None = None key: str | None = None + modifiers: list[str] | None = None direction: str | None = None + amount: int | None = None + observation: str | None = None thought: str | None = None + plan: list[str] | None = None + status: str | None = None raw_response: str | None = None parse_error: str | None = None + confidence: float = 1.0 metadata: dict[str, Any] = field(default_factory=dict) @property @@ -46,6 +211,21 @@ def is_valid(self) -> bool: """Check if the action was successfully parsed.""" return self.parse_error is None and self.action_type != "unknown" + @property + def is_terminal(self) -> bool: + """Check if this action terminates the episode.""" + return self.action_type in ("done", "fail") + + @property + def has_coordinates(self) -> bool: + """Check if action has coordinate data.""" + return self.x is not None and self.y is not None + + @property + def has_element_id(self) -> bool: + """Check if action has element ID.""" + return self.element_id is not None + def to_dict(self) -> dict[str, Any]: """Convert to action dictionary for benchmark integration.""" result: dict[str, Any] = {"type": self.action_type} @@ -60,35 +240,158 @@ def to_dict(self) -> dict[str, Any]: result["text"] = self.text if self.key is not None: result["key"] = self.key + if self.modifiers: + result["modifiers"] = self.modifiers if self.direction is not None: result["direction"] = self.direction + if self.amount is not None: + result["amount"] = self.amount + if self.observation is not None: + result["observation"] = self.observation if self.thought is not None: result["thought"] = self.thought + if self.plan: + result["plan"] = self.plan + if self.status is not None: + result["status"] = self.status return result + def to_pyautogui( + self, + screen_width: int = 1920, + screen_height: int = 1080, + ) -> str: + """Convert to PyAutoGUI code string. + + Args: + screen_width: Screen width for coordinate conversion. + screen_height: Screen height for coordinate conversion. + + Returns: + PyAutoGUI code string. + """ + if self.action_type == "click": + if self.x is not None and self.y is not None: + px = int(self.x * screen_width) + py = int(self.y * screen_height) + return f"pyautogui.click({px}, {py})" + elif self.element_id is not None: + return f"# CLICK element {self.element_id} (needs coordinate conversion)" + elif self.action_type == "type": + text = self.text or "" + return f"pyautogui.write('{text}')" + elif self.action_type == "key": + key = self.key or "" + if self.modifiers: + keys = ", ".join([f"'{k}'" for k in self.modifiers + [key]]) + return f"pyautogui.hotkey({keys})" + return f"pyautogui.press('{key}')" + elif self.action_type == "scroll": + direction = self.direction or "down" + amount = self.amount or 3 + clicks = -amount if direction == "down" else amount + return f"pyautogui.scroll({clicks})" + elif self.action_type == "done": + return "DONE" + elif self.action_type == "wait": + return "WAIT" + elif self.action_type == "fail": + return "FAIL" + + return f"# Unknown action: {self.action_type}" + + def with_coordinates( + self, + x: float, + y: float, + source: str = "conversion", + ) -> "ParsedAction": + """Create a copy with coordinates added. + + Useful for converting element_id actions to coordinate actions. + + Args: + x: X coordinate (normalized 0-1). + y: Y coordinate (normalized 0-1). + source: Source of coordinates for metadata. + + Returns: + New ParsedAction with coordinates. + """ + return ParsedAction( + action_type=self.action_type, + x=x, + y=y, + element_id=self.element_id, + text=self.text, + key=self.key, + modifiers=self.modifiers, + direction=self.direction, + amount=self.amount, + observation=self.observation, + thought=self.thought, + plan=self.plan, + status=self.status, + raw_response=self.raw_response, + parse_error=self.parse_error, + confidence=self.confidence, + metadata={**self.metadata, "coord_source": source}, + ) + class UnifiedResponseParser: - """Parser for VLM responses across all tracks. + """Parser for VLM responses across all tracks and formats. Supports: - JSON format: {"action": "CLICK", "x": 0.5, "y": 0.3} - Function format: CLICK(0.5, 0.3) or CLICK([17]) - - Mixed format: Thought + action + - PyAutoGUI format: pyautogui.click(960, 540) + - UFO format: {"Observation": ..., "Thought": ..., "ControlLabel": 17} + - Mixed format: ReAct-style with thought + action Example: parser = UnifiedResponseParser() - action = parser.parse("{"action": "CLICK", "x": 0.5, "y": 0.3}") + action = parser.parse('{"action": "CLICK", "x": 0.5, "y": 0.3}') print(action.x, action.y) # 0.5, 0.3 + + # With element registry for ID->coordinate conversion + registry = ElementRegistry.from_a11y_tree(tree) + parser = UnifiedResponseParser(element_registry=registry) + action = parser.parse('{"action": "CLICK", "element_id": 17}') + action = parser.resolve_element_id(action) + print(action.x, action.y) # Converted coordinates """ + def __init__( + self, + element_registry: ElementRegistry | None = None, + screen_config: "ScreenConfig | None" = None, + normalize_coordinates: bool = True, + ): + """Initialize parser. + + Args: + element_registry: Optional registry for element_id conversion. + screen_config: Optional screen configuration for coordinate handling. + normalize_coordinates: Whether to normalize coordinates to 0-1. + """ + self.element_registry = element_registry + self.screen_config = screen_config + self.normalize_coordinates = normalize_coordinates + + # Default screen dimensions + self._screen_width = screen_config.width if screen_config else 1920 + self._screen_height = screen_config.height if screen_config else 1080 + def parse(self, response: str) -> ParsedAction: """Parse model response into structured action. - Tries multiple parsing strategies: - 1. JSON extraction - 2. Regex patterns for function-style actions - 3. Fallback text patterns + Tries multiple parsing strategies in order: + 1. JSON extraction (most reliable) + 2. PyAutoGUI code patterns + 3. Function-style patterns (CLICK, TYPE, etc.) + 4. Special keywords (DONE, WAIT, FAIL) Args: response: Raw model response string. @@ -96,123 +399,339 @@ def parse(self, response: str) -> ParsedAction: Returns: ParsedAction with extracted fields. """ + if not response: + return ParsedAction( + action_type="unknown", + raw_response=response, + parse_error="Empty response", + ) + response = response.strip() - # Try JSON first + # Try JSON first (most structured) action = self._try_json_parse(response) if action.is_valid: action.raw_response = response return action - # Try regex patterns + # Try PyAutoGUI format + action = self._try_pyautogui_parse(response) + if action.is_valid: + action.raw_response = response + return action + + # Try function-call patterns action = self._try_regex_parse(response) if action.is_valid: action.raw_response = response return action + # Try special keywords + action = self._try_keyword_parse(response) + if action.is_valid: + action.raw_response = response + return action + # Return unknown action with error return ParsedAction( action_type="unknown", raw_response=response, parse_error="No action pattern found in response", + confidence=0.0, ) def _try_json_parse(self, response: str) -> ParsedAction: """Try to extract and parse JSON from response.""" - # Find JSON in response - json_match = re.search(r'\{[^{}]*\}', response) - if not json_match: - return ParsedAction(action_type="unknown", parse_error="No JSON found") - - try: - data = json.loads(json_match.group()) - except json.JSONDecodeError as e: - return ParsedAction(action_type="unknown", parse_error=f"JSON parse error: {e}") - - return self._dict_to_action(data) + # Try to find JSON object in response + json_patterns = [ + r'```json\s*(\{[^`]*\})\s*```', # Markdown code block + r'```\s*(\{[^`]*\})\s*```', # Plain code block + r'(\{[^{}]*\})', # Simple JSON object + r'(\{[^{}]*\{[^{}]*\}[^{}]*\})', # Nested JSON (max 1 level) + ] + + for pattern in json_patterns: + matches = re.findall(pattern, response, re.DOTALL) + for match in matches: + try: + data = json.loads(match) + action = self._dict_to_action(data) + if action.is_valid: + return action + except json.JSONDecodeError: + continue + + return ParsedAction(action_type="unknown", parse_error="No valid JSON found") def _dict_to_action(self, data: dict[str, Any]) -> ParsedAction: - """Convert parsed dict to ParsedAction.""" - action_type = data.get("action", data.get("type", "")).lower() - thought = data.get("thought") + """Convert parsed dict to ParsedAction. + + Handles multiple formats: + - Standard: {"action": "CLICK", "x": 0.5, "y": 0.3} + - UFO: {"Observation": ..., "Thought": ..., "ControlLabel": 17} + - ReAct: {"observation": ..., "thought": ..., "action": "CLICK"} + """ + # Extract ReAct/UFO fields first + observation = data.get("observation", data.get("Observation")) + thought = data.get("thought", data.get("Thought")) + plan = data.get("plan", data.get("Plan")) + status = data.get("status", data.get("Status")) + + # Get action type (handle various key names) + action_type = ( + data.get("action", "") + or data.get("type", "") + or data.get("Function", "") # UFO format + ).lower() + + # Handle UFO ControlLabel as element click + control_label = data.get("ControlLabel", data.get("control_label")) + if control_label is not None and not action_type: + action_type = "click" if action_type == "click": - # Check for element_id (SoM) vs coordinates - if "element_id" in data: - element_id = data["element_id"] - if isinstance(element_id, str): - # Extract number from "e17" or "[17]" format - match = re.search(r'\d+', element_id) - element_id = int(match.group()) if match else None + # Check for element_id first (SoM/UFO) + element_id = data.get("element_id", data.get("ControlLabel")) + if element_id is not None: return ParsedAction( action_type="click", - element_id=element_id, + element_id=self._normalize_element_id(element_id), + observation=observation, thought=thought, + plan=plan, + status=status, ) - elif "x" in data and "y" in data: + + # Then check for coordinates + if "x" in data and "y" in data: + x, y = self._normalize_coords(float(data["x"]), float(data["y"])) return ParsedAction( action_type="click", - x=float(data["x"]), - y=float(data["y"]), + x=x, + y=y, + observation=observation, thought=thought, + plan=plan, + status=status, ) - else: - return ParsedAction( - action_type="click", - parse_error="CLICK missing coordinates or element_id", - ) - elif action_type == "type": + # Check coordinate array format + if "coordinate" in data: + coords = data["coordinate"] + if isinstance(coords, (list, tuple)) and len(coords) >= 2: + x, y = self._normalize_coords(float(coords[0]), float(coords[1])) + return ParsedAction( + action_type="click", + x=x, + y=y, + observation=observation, + thought=thought, + ) + + return ParsedAction( + action_type="click", + parse_error="CLICK missing coordinates or element_id", + observation=observation, + thought=thought, + ) + + elif action_type in ("type", "input_text", "write"): + text = data.get("text", "") + # Handle UFO Args format + args = data.get("Args", data.get("args", [])) + if not text and args: + text = args[0] if args else "" return ParsedAction( action_type="type", - text=data.get("text", ""), + text=text, + observation=observation, thought=thought, ) - elif action_type == "key": + elif action_type in ("key", "press", "hotkey"): + key = data.get("key", "") + modifiers = data.get("modifiers", []) + + # Handle UFO Args format for hotkey + args = data.get("Args", data.get("args", [])) + if args and not key: + if len(args) == 1: + key = args[0] + else: + modifiers = args[:-1] + key = args[-1] + return ParsedAction( action_type="key", - key=data.get("key", ""), + key=key, + modifiers=modifiers if modifiers else None, + observation=observation, thought=thought, ) elif action_type == "scroll": + direction = data.get("direction", data.get("scroll_direction", "down")) + amount = data.get("amount", data.get("scroll_amount", 3)) + + # Handle UFO Args format + args = data.get("Args", data.get("args", [])) + if args and not direction: + direction = args[0] if args else "down" + return ParsedAction( action_type="scroll", - direction=data.get("direction", "down"), + direction=direction, + amount=amount, + observation=observation, + thought=thought, + ) + + elif action_type in ("done", "finish", "complete"): + return ParsedAction( + action_type="done", + status="FINISH", + observation=observation, + thought=thought, + ) + + elif action_type in ("wait", "pause"): + return ParsedAction( + action_type="wait", + observation=observation, thought=thought, ) - elif action_type == "done": - return ParsedAction(action_type="done", thought=thought) + elif action_type in ("fail", "error", "impossible"): + return ParsedAction( + action_type="fail", + status="ERROR", + observation=observation, + thought=thought, + ) else: return ParsedAction( action_type="unknown", parse_error=f"Unknown action type: {action_type}", + observation=observation, + thought=thought, ) - def _try_regex_parse(self, response: str) -> ParsedAction: - """Try regex patterns for function-style actions.""" - # CLICK(x, y) pattern - click_coords = re.search( - r'CLICK\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*\)', + def _try_pyautogui_parse(self, response: str) -> ParsedAction: + """Try to parse PyAutoGUI-style code.""" + # pyautogui.click(x, y) + click_match = re.search( + r'pyautogui\.click\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)', + response, + re.IGNORECASE, + ) + if click_match: + x = int(click_match.group(1)) + y = int(click_match.group(2)) + x, y = self._normalize_coords(x, y) + return ParsedAction(action_type="click", x=x, y=y) + + # pyautogui.doubleClick(x, y) + dclick_match = re.search( + r'pyautogui\.doubleClick\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)', + response, + re.IGNORECASE, + ) + if dclick_match: + x = int(dclick_match.group(1)) + y = int(dclick_match.group(2)) + x, y = self._normalize_coords(x, y) + return ParsedAction( + action_type="click", + x=x, + y=y, + metadata={"double_click": True}, + ) + + # pyautogui.write('text') + write_match = re.search( + r'pyautogui\.write\s*\(\s*[\'"](.+?)[\'"]\s*\)', + response, + re.IGNORECASE, + ) + if write_match: + return ParsedAction(action_type="type", text=write_match.group(1)) + + # pyautogui.press('key') + press_match = re.search( + r'pyautogui\.press\s*\(\s*[\'"](.+?)[\'"]\s*\)', + response, + re.IGNORECASE, + ) + if press_match: + return ParsedAction(action_type="key", key=press_match.group(1)) + + # pyautogui.hotkey('key1', 'key2') + hotkey_match = re.search( + r'pyautogui\.hotkey\s*\(\s*(.+?)\s*\)', response, re.IGNORECASE, ) - if click_coords: - try: + if hotkey_match: + keys_str = hotkey_match.group(1) + # Extract keys from quotes + keys = re.findall(r'[\'"]([^\'"]+)[\'"]', keys_str) + if keys: + modifiers = keys[:-1] if len(keys) > 1 else None + key = keys[-1] return ParsedAction( - action_type="click", - x=float(click_coords.group(1)), - y=float(click_coords.group(2)), + action_type="key", + key=key, + modifiers=modifiers, ) - except ValueError: - pass - # CLICK([id]) pattern for SoM + # pyautogui.scroll(amount) + scroll_match = re.search( + r'pyautogui\.scroll\s*\(\s*(-?\d+)\s*\)', + response, + re.IGNORECASE, + ) + if scroll_match: + clicks = int(scroll_match.group(1)) + direction = "up" if clicks > 0 else "down" + return ParsedAction( + action_type="scroll", + direction=direction, + amount=abs(clicks), + ) + + return ParsedAction(action_type="unknown", parse_error="No PyAutoGUI pattern matched") + + def _try_regex_parse(self, response: str) -> ParsedAction: + """Try regex patterns for function-style actions.""" + # CLICK(x, y) - normalized coordinates + click_norm = re.search( + r'CLICK\s*\(\s*(0?\.\d+)\s*,\s*(0?\.\d+)\s*\)', + response, + re.IGNORECASE, + ) + if click_norm: + return ParsedAction( + action_type="click", + x=float(click_norm.group(1)), + y=float(click_norm.group(2)), + ) + + # CLICK(x, y) - larger numbers (pixels) + click_pixel = re.search( + r'CLICK\s*\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)', + response, + re.IGNORECASE, + ) + if click_pixel: + x = float(click_pixel.group(1)) + y = float(click_pixel.group(2)) + x, y = self._normalize_coords(x, y) + return ParsedAction(action_type="click", x=x, y=y) + + # CLICK([id]) - element ID click_element = re.search( - r'CLICK\s*\(\s*\[?\s*(\d+)\s*\]?\s*\)', + r'CLICK\s*\(\s*\[\s*(\d+)\s*\]\s*\)', response, re.IGNORECASE, ) @@ -222,59 +741,163 @@ def _try_regex_parse(self, response: str) -> ParsedAction: element_id=int(click_element.group(1)), ) - # TYPE("text") pattern + # CLICK(id) without brackets + click_id = re.search( + r'CLICK\s*\(\s*(\d+)\s*\)', + response, + re.IGNORECASE, + ) + if click_id: + # Check if it's likely an element ID (small number) vs coordinate + val = int(click_id.group(1)) + if val < 1000: # Likely element ID + return ParsedAction(action_type="click", element_id=val) + + # TYPE("text") or TYPE('text') type_match = re.search( r'TYPE\s*\(\s*["\'](.+?)["\']\s*\)', response, re.IGNORECASE, ) if type_match: - return ParsedAction( - action_type="type", - text=type_match.group(1), - ) + return ParsedAction(action_type="type", text=type_match.group(1)) - # KEY(key) pattern + # KEY(key) or KEY(mod+key) key_match = re.search( - r'KEY\s*\(\s*([a-zA-Z_]+)\s*\)', + r'KEY\s*\(\s*([a-zA-Z0-9_+]+)\s*\)', response, re.IGNORECASE, ) if key_match: - return ParsedAction( - action_type="key", - key=key_match.group(1).lower(), - ) - - # SCROLL(direction) pattern + key_str = key_match.group(1).lower() + if '+' in key_str: + parts = key_str.split('+') + modifiers = parts[:-1] + key = parts[-1] + return ParsedAction(action_type="key", key=key, modifiers=modifiers) + return ParsedAction(action_type="key", key=key_str) + + # SCROLL(direction) or SCROLL(direction, amount) scroll_match = re.search( - r'SCROLL\s*\(\s*([a-zA-Z]+)\s*\)', + r'SCROLL\s*\(\s*([a-zA-Z]+)(?:\s*,\s*(\d+))?\s*\)', response, re.IGNORECASE, ) if scroll_match: + direction = scroll_match.group(1).lower() + amount = int(scroll_match.group(2)) if scroll_match.group(2) else 3 + return ParsedAction(action_type="scroll", direction=direction, amount=amount) + + return ParsedAction(action_type="unknown", parse_error="No regex pattern matched") + + def _try_keyword_parse(self, response: str) -> ParsedAction: + """Try special keywords.""" + response_upper = response.upper().strip() + + # DONE() or just DONE + if re.search(r'\bDONE\s*\(\s*\)\s*$', response, re.IGNORECASE) or response_upper == "DONE": + return ParsedAction(action_type="done") + + # WAIT() or WAIT + if re.search(r'\bWAIT\s*\(\s*\)\s*$', response, re.IGNORECASE) or response_upper == "WAIT": + return ParsedAction(action_type="wait") + + # FAIL() or FAIL + if re.search(r'\bFAIL\s*\(\s*\)\s*$', response, re.IGNORECASE) or response_upper == "FAIL": + return ParsedAction(action_type="fail") + + # Look for "task is complete" or similar phrases + if re.search(r'task\s+(?:is\s+)?(?:complete|done|finished)', response, re.IGNORECASE): return ParsedAction( - action_type="scroll", - direction=scroll_match.group(1).lower(), + action_type="done", + confidence=0.7, + metadata={"inferred": True}, ) - # DONE() pattern - if re.search(r'DONE\s*\(\s*\)', response, re.IGNORECASE): - return ParsedAction(action_type="done") + return ParsedAction(action_type="unknown", parse_error="No keyword matched") - return ParsedAction(action_type="unknown", parse_error="No regex pattern matched") + def _normalize_coords(self, x: float, y: float) -> tuple[float, float]: + """Normalize coordinates to 0-1 range if needed.""" + if not self.normalize_coordinates: + return (x, y) - def normalize_element_id(self, action: ParsedAction) -> ParsedAction: - """Normalize element_id to integer format. + # If coordinates are large, assume they're pixels + if x > 1.5 or y > 1.5: + x = x / self._screen_width + y = y / self._screen_height + + # Clamp to valid range + x = max(0.0, min(1.0, x)) + y = max(0.0, min(1.0, y)) + + return (x, y) + + def _normalize_element_id(self, element_id: Any) -> int | None: + """Normalize element_id to integer format.""" + if element_id is None: + return None + + if isinstance(element_id, int): + return element_id + + if isinstance(element_id, str): + # Extract number from "e17", "[17]", "element_17" etc. + match = re.search(r'\d+', element_id) + if match: + return int(match.group()) + + try: + return int(element_id) + except (ValueError, TypeError): + return None + + def resolve_element_id( + self, + action: ParsedAction, + registry: ElementRegistry | None = None, + ) -> ParsedAction: + """Convert element_id to coordinates if registry available. Args: - action: Action with possibly string element_id. + action: ParsedAction with element_id. + registry: Element registry (uses self.element_registry if None). Returns: - Action with normalized integer element_id. + ParsedAction with coordinates added if conversion succeeded, + original action otherwise. """ - if action.element_id is not None and isinstance(action.element_id, str): - match = re.search(r'\d+', str(action.element_id)) - if match: - action.element_id = int(match.group()) + if not action.has_element_id or action.has_coordinates: + return action + + reg = registry or self.element_registry + if reg is None: + return action + + coords = reg.get_center_coords(action.element_id, normalize=True) + if coords is not None: + return action.with_coordinates( + x=coords[0], + y=coords[1], + source=f"element_{action.element_id}", + ) + return action + + def parse_and_resolve( + self, + response: str, + registry: ElementRegistry | None = None, + ) -> ParsedAction: + """Parse response and resolve element_id to coordinates. + + Convenience method that combines parse() and resolve_element_id(). + + Args: + response: Raw model response. + registry: Optional element registry for ID conversion. + + Returns: + ParsedAction with coordinates if available. + """ + action = self.parse(response) + return self.resolve_element_id(action, registry) diff --git a/openadapt_ml/baselines/prompts.py b/openadapt_ml/baselines/prompts.py index 7619b4e..82383de 100644 --- a/openadapt_ml/baselines/prompts.py +++ b/openadapt_ml/baselines/prompts.py @@ -1,103 +1,409 @@ """Prompt templates for baseline adapters. Provides track-specific system prompts and user content builders. +Based on SOTA patterns from: +- Claude Computer Use (Anthropic) +- UFO/UFO2 (Microsoft) +- OSWorld benchmark +- Agent-S/Agent-S2 (Simular AI) + +Key design principles: +1. Structured observation -> thought -> action flow (ReAct) +2. Clear action format specification with examples +3. Explicit coordinate system definition +4. Screen verification after action (Claude best practice) +5. Error handling guidance """ from __future__ import annotations +import textwrap from typing import TYPE_CHECKING, Any -from openadapt_ml.baselines.config import TrackConfig, TrackType +from openadapt_ml.baselines.config import ( + ActionOutputFormat, + TrackConfig, + TrackType, +) if TYPE_CHECKING: from PIL import Image -# System prompts for each track -SYSTEM_PROMPTS = { - TrackType.TRACK_A: """You are a GUI automation agent. Your task is to interact with graphical user interfaces by analyzing screenshots and determining the next action. - -CAPABILITIES: -- CLICK(x, y): Click at normalized coordinates (0.0-1.0) where (0,0) is top-left -- TYPE("text"): Type the specified text -- KEY(key): Press a key (e.g., KEY(enter), KEY(escape)) -- SCROLL(direction): Scroll up or down -- DONE(): Mark task as complete - -RULES: -1. Analyze the screenshot carefully before acting -2. Use normalized coordinates (0.0-1.0) for all clicks -3. Return ONLY the action in JSON format -4. If the goal is achieved, use DONE() - -OUTPUT FORMAT: +# ============================================================================= +# TRACK A: Direct Coordinate Prediction +# ============================================================================= + +SYSTEM_PROMPT_TRACK_A = """You are a GUI automation agent that controls computer interfaces by analyzing screenshots. + +## YOUR CAPABILITIES + +You can perform these actions: +- **CLICK**: Click at specific screen coordinates +- **TYPE**: Enter text at the current cursor position +- **KEY**: Press keyboard keys or key combinations +- **SCROLL**: Scroll in a direction +- **DONE**: Mark task as complete when the goal is achieved + +## COORDINATE SYSTEM + +- Coordinates are **normalized** between 0.0 and 1.0 +- (0.0, 0.0) is the **top-left** corner of the screen +- (1.0, 1.0) is the **bottom-right** corner +- For example, the center of the screen is (0.5, 0.5) + +## OUTPUT FORMAT + +Respond with a single JSON object containing your action: + +```json {"action": "CLICK", "x": 0.5, "y": 0.3} -or +``` + +```json {"action": "TYPE", "text": "hello world"} -or +``` + +```json {"action": "KEY", "key": "enter"} -or -{"action": "SCROLL", "direction": "down"} -or -{"action": "DONE"}""", - TrackType.TRACK_B: """You are a GUI automation agent using ReAct (Reason + Act) to interact with interfaces. - -CAPABILITIES: -- CLICK(x, y): Click at normalized coordinates (0.0-1.0) where (0,0) is top-left -- TYPE("text"): Type the specified text -- KEY(key): Press a key (e.g., KEY(enter), KEY(escape)) -- SCROLL(direction): Scroll up or down -- DONE(): Mark task as complete - -PROCESS: -1. OBSERVE: Describe what you see in the screenshot -2. THINK: Reason about what action to take and why -3. ACT: Execute the chosen action - -RULES: -1. Always explain your reasoning before acting -2. Use normalized coordinates (0.0-1.0) for all clicks -3. Return both thought and action in JSON format -4. If the goal is achieved, use DONE() - -OUTPUT FORMAT: -{"thought": "I can see a login form. The username field is at the top. I should click it first to enter credentials.", "action": "CLICK", "x": 0.5, "y": 0.3} -or -{"thought": "The username field is now focused. I should type the username.", "action": "TYPE", "text": "user@example.com"} -or -{"thought": "The task is complete - I can see the success message.", "action": "DONE"}""", - TrackType.TRACK_C: """You are a GUI automation agent. UI elements are marked with numbered labels [1], [2], etc. - -CAPABILITIES: -- CLICK([id]): Click the element with the given ID -- TYPE("text"): Type the specified text -- KEY(key): Press a key (e.g., KEY(enter), KEY(escape)) -- SCROLL(direction): Scroll up or down -- DONE(): Mark task as complete - -RULES: -1. Use element IDs from the labels, NOT coordinates -2. Each element has a number in brackets like [1], [17], [42] -3. Return ONLY the action in JSON format -4. If the goal is achieved, use DONE() - -OUTPUT FORMAT: +``` + +```json +{"action": "SCROLL", "direction": "down", "amount": 3} +``` + +```json +{"action": "DONE"} +``` + +## RULES + +1. **Analyze carefully**: Study the screenshot to identify UI elements +2. **Be precise**: Aim for the center of clickable elements +3. **One action at a time**: Return exactly one action per response +4. **Validate coordinates**: Ensure x and y are between 0.0 and 1.0 +5. **Complete the task**: Use DONE only when the goal is fully achieved +6. **Handle errors**: If an action fails, try an alternative approach + +## IMPORTANT + +- Return ONLY the JSON object, no additional text +- If you cannot determine the correct action, explain in a "reason" field and still provide your best guess""" + + +# ============================================================================= +# TRACK B: ReAct-style Reasoning with Coordinates +# ============================================================================= + +SYSTEM_PROMPT_TRACK_B = """You are a GUI automation agent using ReAct (Reasoning + Acting) to complete tasks. + +## YOUR CAPABILITIES + +You can perform these actions: +- **CLICK**: Click at specific screen coordinates +- **TYPE**: Enter text at the current cursor position +- **KEY**: Press keyboard keys or key combinations +- **SCROLL**: Scroll in a direction +- **DONE**: Mark task as complete + +## COORDINATE SYSTEM + +- Coordinates are **normalized** between 0.0 and 1.0 +- (0.0, 0.0) is the **top-left** corner +- (1.0, 1.0) is the **bottom-right** corner + +## ReAct PROCESS + +For each step, follow this process: + +1. **OBSERVE**: Describe what you see in the screenshot + - What application/window is visible? + - What UI elements are present? + - What is the current state? + +2. **THINK**: Reason about the next action + - What is the goal? + - What progress has been made? + - What is the logical next step? + - Where exactly should I click? + +3. **ACT**: Execute the action + +## OUTPUT FORMAT + +Respond with a JSON object containing observation, thought, and action: + +```json +{ + "observation": "I see a login form with username and password fields. The username field is empty and appears to be focused.", + "thought": "To log in, I first need to enter the username. The username field is positioned at approximately x=0.5, y=0.35.", + "action": "CLICK", + "x": 0.5, + "y": 0.35 +} +``` + +```json +{ + "observation": "The username field is now active with a cursor blinking.", + "thought": "I should type the username now.", + "action": "TYPE", + "text": "user@example.com" +} +``` + +```json +{ + "observation": "I can see the confirmation page showing 'Success! You are logged in.'", + "thought": "The task is complete - the login was successful.", + "action": "DONE" +} +``` + +## RULES + +1. **Always explain your reasoning** before acting +2. **Be specific** in observations - describe what you actually see +3. **Justify coordinates** - explain why you chose those coordinates +4. **Track progress** - consider previous actions when planning +5. **Verify completion** - ensure the goal is fully achieved before DONE + +## TIPS + +- If an element is hard to click, try using keyboard navigation +- After clicking, verify the expected result occurred +- For text fields, click to focus before typing""" + + +# ============================================================================= +# TRACK C: Set-of-Mark Element Selection +# ============================================================================= + +SYSTEM_PROMPT_TRACK_C = """You are a GUI automation agent. UI elements in the screenshot are labeled with numbered markers like [1], [2], [3], etc. + +## YOUR CAPABILITIES + +You can perform these actions: +- **CLICK**: Click an element by its label number +- **TYPE**: Enter text at the current cursor position +- **KEY**: Press keyboard keys or key combinations +- **SCROLL**: Scroll in a direction +- **DONE**: Mark task as complete + +## ELEMENT LABELS + +- Each interactive UI element is marked with a number in brackets: [1], [2], [3], etc. +- The accessibility tree below lists all labeled elements with their roles and names +- Use the element ID (the number) to specify which element to click + +## OUTPUT FORMAT + +Respond with a JSON object: + +```json {"action": "CLICK", "element_id": 17} -or +``` + +```json {"action": "TYPE", "text": "hello world"} -or +``` + +```json {"action": "KEY", "key": "enter"} -or +``` + +```json {"action": "SCROLL", "direction": "down"} -or -{"action": "DONE"}""", +``` + +```json +{"action": "DONE"} +``` + +## RULES + +1. **Use element IDs** - Click by element number, NOT coordinates +2. **Match carefully** - Find the element that matches your intent +3. **Check roles** - Consider element type (button, textfield, checkbox) +4. **Read labels** - Use element names to identify correct targets +5. **One action** - Return exactly one action per response + +## ELEMENT SELECTION TIPS + +- Look for buttons with matching text labels +- Text fields are often named by their placeholder or label +- If multiple similar elements exist, choose based on position +- Some elements may be nested - prefer the most specific match + +## IMPORTANT + +- Return ONLY the JSON object +- element_id must be an integer from the labeled elements""" + + +# ============================================================================= +# OSWORLD-COMPATIBLE PROMPTS (PyAutoGUI format) +# ============================================================================= + +SYSTEM_PROMPT_OSWORLD = """You are a GUI automation agent controlling a computer through PyAutoGUI. + +## ENVIRONMENT + +You are interacting with a desktop environment (Ubuntu/Windows/macOS). +Execute tasks by generating Python code using the PyAutoGUI library. + +## AVAILABLE ACTIONS + +```python +# Mouse actions +pyautogui.click(x, y) # Click at pixel coordinates +pyautogui.doubleClick(x, y) # Double-click +pyautogui.rightClick(x, y) # Right-click +pyautogui.moveTo(x, y) # Move mouse +pyautogui.drag(dx, dy) # Drag relative + +# Keyboard actions +pyautogui.write('text') # Type text +pyautogui.press('key') # Press single key +pyautogui.hotkey('ctrl', 'c') # Key combination + +# Scrolling +pyautogui.scroll(clicks) # Scroll (positive=up, negative=down) + +# Special +WAIT # Agent should wait +FAIL # Task is infeasible +DONE # Task is complete +``` + +## COORDINATE SYSTEM + +- Coordinates are in **pixels** from the screen's top-left corner +- Screen dimensions are provided in the observation + +## OUTPUT FORMAT + +Output a single line of Python code or special command: + +``` +pyautogui.click(960, 540) +``` + +``` +pyautogui.write('Hello, World!') +``` + +``` +pyautogui.hotkey('ctrl', 's') +``` + +``` +DONE +``` + +## RULES + +1. **One action per response** - Output exactly one line +2. **Use pixel coordinates** - Not normalized +3. **Be precise** - Aim for the center of elements +4. **Handle failures** - Output FAIL if task is impossible +5. **Wait when needed** - Output WAIT if UI is loading + +## TIPS + +- Click in the center of buttons and links +- For text fields, click to focus before typing +- Use hotkeys when available (faster, more reliable) +- Scroll to reveal off-screen elements""" + + +# ============================================================================= +# UFO-COMPATIBLE PROMPTS +# ============================================================================= + +SYSTEM_PROMPT_UFO = """You are an AppAgent in the UFO framework, controlling Windows applications. + +## YOUR ROLE + +You interact with application UI by selecting controls and executing functions. +Each control is labeled with a number that you reference in your response. + +## PROCESS + +For each step: +1. **Observe** the current application state +2. **Think** about what action achieves the goal +3. **Select** the appropriate control and function +4. **Plan** subsequent steps + +## OUTPUT FORMAT + +Respond with a JSON object: + +```json +{ + "Observation": "The Notepad application is open with an empty document.", + "Thought": "To save the file, I need to use File > Save or Ctrl+S. I'll click the File menu first.", + "ControlLabel": 3, + "ControlText": "File", + "Function": "click", + "Args": [], + "Status": "CONTINUE", + "Plan": ["Click Save in the menu", "Enter filename", "Click Save button"], + "Comment": "Starting the save workflow" +} +``` + +## AVAILABLE FUNCTIONS + +- **click**: Click the control +- **input_text**: Type text (Args: ["text to type"]) +- **select**: Select option from dropdown (Args: ["option"]) +- **scroll**: Scroll control (Args: ["up"] or ["down"]) +- **hotkey**: Press key combination (Args: ["ctrl", "s"]) +- **wait**: Wait for UI update (Args: [seconds]) + +## STATUS VALUES + +- **CONTINUE**: More actions needed +- **FINISH**: Task completed successfully +- **ERROR**: Something went wrong +- **PENDING**: Waiting for user input + +## RULES + +1. **Always provide Observation and Thought** +2. **ControlLabel must match a labeled element** +3. **Plan should list remaining steps** +4. **Use FINISH only when goal is achieved**""" + + +# ============================================================================= +# System Prompt Registry +# ============================================================================= + +SYSTEM_PROMPTS = { + TrackType.TRACK_A: SYSTEM_PROMPT_TRACK_A, + TrackType.TRACK_B: SYSTEM_PROMPT_TRACK_B, + TrackType.TRACK_C: SYSTEM_PROMPT_TRACK_C, } +# Additional format-specific prompts +FORMAT_PROMPTS = { + ActionOutputFormat.PYAUTOGUI: SYSTEM_PROMPT_OSWORLD, +} + + +# ============================================================================= +# PromptBuilder Class +# ============================================================================= + class PromptBuilder: """Builds prompts for baseline API calls. Constructs system prompts and user content based on track configuration. + Supports multiple output formats and benchmark compatibility. Example: builder = PromptBuilder(track_config) @@ -118,21 +424,71 @@ def __init__(self, track: TrackConfig): """ self.track = track - def get_system_prompt(self, demo: str | None = None) -> str: + def get_system_prompt( + self, + demo: str | None = None, + custom_instructions: str | None = None, + ) -> str: """Get the system prompt for this track. Args: - demo: Optional demo text to include. + demo: Optional demo text to include as an example. + custom_instructions: Optional custom instructions to append. Returns: System prompt string. """ - base_prompt = SYSTEM_PROMPTS[self.track.track_type] + # Select base prompt based on format or track + if self.track.action_format == ActionOutputFormat.PYAUTOGUI: + base_prompt = SYSTEM_PROMPT_OSWORLD + else: + base_prompt = SYSTEM_PROMPTS.get( + self.track.track_type, SYSTEM_PROMPT_TRACK_A + ) + + parts = [base_prompt] + # Add demo example if provided if demo: - base_prompt += f"\n\nEXAMPLE DEMONSTRATION:\n{demo}" + parts.append(self._format_demo_section(demo)) + + # Add screen verification instruction if enabled + if self.track.verify_after_action: + parts.append(self._get_verification_instruction()) + + # Add custom instructions + if custom_instructions: + parts.append(f"\n## ADDITIONAL INSTRUCTIONS\n\n{custom_instructions}") + + return "\n\n".join(parts) + + def _format_demo_section(self, demo: str) -> str: + """Format demonstration example section.""" + return textwrap.dedent(f""" + ## EXAMPLE DEMONSTRATION + + Here is an example of successfully completing a similar task: + + {demo} + + Follow a similar pattern for your task. + """).strip() - return base_prompt + def _get_verification_instruction(self) -> str: + """Get instruction for post-action verification. + + Based on Claude Computer Use best practices. + """ + return textwrap.dedent(""" + ## VERIFICATION + + After each action, a new screenshot will be provided. Verify that: + 1. The action was executed correctly + 2. The UI state changed as expected + 3. You are making progress toward the goal + + If something unexpected happened, explain what went wrong and try again. + """).strip() def build_user_content( self, @@ -141,6 +497,8 @@ def build_user_content( a11y_tree: str | dict[str, Any] | None = None, history: list[dict[str, Any]] | None = None, encode_image_fn: Any = None, + screen_info: dict[str, Any] | None = None, + window_info: dict[str, Any] | None = None, ) -> list[dict[str, Any]]: """Build user message content for API call. @@ -150,6 +508,8 @@ def build_user_content( a11y_tree: Accessibility tree (string or dict). history: List of previous actions. encode_image_fn: Function to encode image for API. + screen_info: Screen dimensions and other info. + window_info: Active window information. Returns: List of content blocks for API message. @@ -157,26 +517,33 @@ def build_user_content( content: list[dict[str, Any]] = [] # Build text prompt - text_parts = [f"GOAL: {goal}"] + text_parts = [self._format_goal(goal)] + + # Add screen info if provided + if screen_info: + text_parts.append(self._format_screen_info(screen_info)) + + # Add window info if provided + if window_info: + text_parts.append(self._format_window_info(window_info)) # Add accessibility tree if configured if self.track.use_a11y_tree and a11y_tree: tree_text = self._format_a11y_tree(a11y_tree) if tree_text: - text_parts.append(f"\nACCESSIBILITY TREE:\n{tree_text}") + text_parts.append(self._format_a11y_section(tree_text)) # Add action history if configured if self.track.include_history and history: history_text = self._format_history(history) if history_text: - text_parts.append(f"\nPREVIOUS ACTIONS:\n{history_text}") + text_parts.append(self._format_history_section(history_text)) - # Add instruction - text_parts.append("\nAnalyze the screenshot and provide the next action.") - text_parts.append(f"OUTPUT FORMAT: {self.track.output_format}") + # Add instruction based on track + text_parts.append(self._get_action_instruction()) - # Add text content - content.append({"type": "text", "text": "\n".join(text_parts)}) + # Combine text parts + content.append({"type": "text", "text": "\n\n".join(text_parts)}) # Add screenshot if provided if screenshot is not None and encode_image_fn is not None: @@ -184,6 +551,47 @@ def build_user_content( return content + def _format_goal(self, goal: str) -> str: + """Format the task goal.""" + return f"## TASK\n\n{goal}" + + def _format_screen_info(self, screen_info: dict[str, Any]) -> str: + """Format screen information.""" + width = screen_info.get("width", "unknown") + height = screen_info.get("height", "unknown") + return f"## SCREEN\n\nResolution: {width} x {height} pixels" + + def _format_window_info(self, window_info: dict[str, Any]) -> str: + """Format active window information.""" + parts = ["## ACTIVE WINDOW"] + + if "title" in window_info: + parts.append(f"Title: {window_info['title']}") + if "app" in window_info: + parts.append(f"Application: {window_info['app']}") + if "url" in window_info: + parts.append(f"URL: {window_info['url']}") + + return "\n".join(parts) + + def _format_a11y_section(self, tree_text: str) -> str: + """Format accessibility tree section with header.""" + header = "## UI ELEMENTS" if self.track.use_som else "## ACCESSIBILITY TREE" + return f"{header}\n\n{tree_text}" + + def _format_history_section(self, history_text: str) -> str: + """Format history section with header.""" + return f"## PREVIOUS ACTIONS\n\n{history_text}" + + def _get_action_instruction(self) -> str: + """Get instruction for action output based on track.""" + if self.track.track_type == TrackType.TRACK_B: + return "## YOUR TURN\n\nAnalyze the screenshot, explain your reasoning, and provide the next action." + elif self.track.track_type == TrackType.TRACK_C: + return "## YOUR TURN\n\nAnalyze the screenshot and select the appropriate element to interact with." + else: + return "## YOUR TURN\n\nAnalyze the screenshot and provide the next action." + def _format_a11y_tree(self, tree: str | dict[str, Any]) -> str: """Format accessibility tree for prompt. @@ -204,13 +612,17 @@ def _format_a11y_tree(self, tree: str | dict[str, Any]) -> str: max_lines = self.track.max_a11y_elements lines = text.split("\n") if len(lines) > max_lines: + original_count = len(lines) lines = lines[:max_lines] - lines.append(f"... (truncated, {len(lines)} of {max_lines} elements)") + lines.append(f"... (showing {max_lines} of {original_count} elements)") return "\n".join(lines) def _dict_to_tree_string( - self, tree: dict[str, Any], indent: int = 0, max_depth: int = 5 + self, + tree: dict[str, Any], + indent: int = 0, + max_depth: int = 5, ) -> str: """Convert dict tree to formatted string. @@ -230,24 +642,42 @@ def _dict_to_tree_string( role = tree.get("role", "unknown") name = tree.get("name", "") - node_id = tree.get("id", "") + node_id = tree.get("id", tree.get("node_id", "")) - # Format node - if node_id: + # Format node based on track + if self.track.use_som and node_id: + # SoM format: [id] role "name" line = f"{prefix}[{node_id}] {role}" + elif node_id: + # Non-SoM with ID + line = f"{prefix}({node_id}) {role}" else: line = f"{prefix}{role}" if name: + # Truncate long names + if len(name) > 50: + name = name[:47] + "..." line += f': "{name}"' + # Add bounding box if available (useful for debugging) + bbox = tree.get("bbox", tree.get("bounds")) + if bbox and isinstance(bbox, (list, tuple)) and len(bbox) >= 4: + # Show center point for SoM + if self.track.use_som: + cx = (bbox[0] + bbox[2]) / 2 + cy = (bbox[1] + bbox[3]) / 2 + line += f" @ ({cx:.2f}, {cy:.2f})" + lines.append(line) # Process children children = tree.get("children", []) for child in children: if isinstance(child, dict): - lines.append(self._dict_to_tree_string(child, indent + 1, max_depth)) + child_text = self._dict_to_tree_string(child, indent + 1, max_depth) + if child_text: + lines.append(child_text) return "\n".join(lines) @@ -269,26 +699,87 @@ def _format_history(self, history: list[dict[str, Any]]) -> str: for i, action in enumerate(recent, 1): action_type = action.get("type", action.get("action", "unknown")).upper() + line = self._format_single_action(i, action_type, action) + lines.append(line) - if action_type == "CLICK": - if "element_id" in action: - lines.append(f"{i}. CLICK([{action['element_id']}])") - elif "x" in action and "y" in action: - lines.append(f"{i}. CLICK({action['x']:.3f}, {action['y']:.3f})") - else: - lines.append(f"{i}. CLICK()") - elif action_type == "TYPE": - text = action.get("text", "") - lines.append(f'{i}. TYPE("{text}")') - elif action_type == "KEY": - key = action.get("key", "") - lines.append(f"{i}. KEY({key})") - elif action_type == "SCROLL": - direction = action.get("direction", "down") - lines.append(f"{i}. SCROLL({direction})") - elif action_type == "DONE": - lines.append(f"{i}. DONE()") + return "\n".join(lines) + + def _format_single_action( + self, step: int, action_type: str, action: dict[str, Any] + ) -> str: + """Format a single action for history display.""" + if action_type == "CLICK": + if "element_id" in action: + return f"{step}. CLICK([{action['element_id']}])" + elif "x" in action and "y" in action: + return f"{step}. CLICK({action['x']:.3f}, {action['y']:.3f})" else: - lines.append(f"{i}. {action_type}()") + return f"{step}. CLICK()" + elif action_type == "TYPE": + text = action.get("text", "") + # Truncate long text + if len(text) > 30: + text = text[:27] + "..." + return f'{step}. TYPE("{text}")' + elif action_type == "KEY": + key = action.get("key", "") + return f"{step}. KEY({key})" + elif action_type == "SCROLL": + direction = action.get("direction", "down") + amount = action.get("amount", 1) + return f"{step}. SCROLL({direction}, {amount})" + elif action_type == "DONE": + return f"{step}. DONE()" + elif action_type == "WAIT": + return f"{step}. WAIT()" + else: + return f"{step}. {action_type}()" - return "\n".join(lines) + def build_verification_prompt( + self, + goal: str, + previous_action: dict[str, Any], + screenshot: "Image" | None = None, + encode_image_fn: Any = None, + ) -> list[dict[str, Any]]: + """Build a verification prompt after an action. + + Used to verify action results and decide next steps. + Based on Claude Computer Use best practices. + + Args: + goal: Original task goal. + previous_action: The action that was just executed. + screenshot: Screenshot after action execution. + encode_image_fn: Function to encode image. + + Returns: + List of content blocks. + """ + content: list[dict[str, Any]] = [] + + action_str = self._format_single_action(0, previous_action.get("type", ""), previous_action) + action_str = action_str[3:] # Remove "0. " prefix + + text = textwrap.dedent(f""" + ## VERIFICATION CHECK + + **Goal**: {goal} + + **Previous Action**: {action_str} + + Analyze the screenshot and verify: + 1. Did the action execute correctly? + 2. Is the UI state as expected? + 3. Are we making progress toward the goal? + + If the goal is achieved, respond with {{"action": "DONE"}}. + Otherwise, provide the next action. + """).strip() + + content.append({"type": "text", "text": text}) + + if screenshot is not None and encode_image_fn is not None: + content.append(encode_image_fn(screenshot)) + + return content diff --git a/openadapt_ml/benchmarks/__init__.py b/openadapt_ml/benchmarks/__init__.py index 9090446..878bde1 100644 --- a/openadapt_ml/benchmarks/__init__.py +++ b/openadapt_ml/benchmarks/__init__.py @@ -88,6 +88,7 @@ RandomAgent, ScriptedAgent, SmartMockAgent, + UnifiedBaselineAgent, ) from openadapt_ml.benchmarks.base import ( BenchmarkAction, @@ -131,6 +132,7 @@ def _get_azure_classes(): "BenchmarkAgent", "PolicyAgent", "APIBenchmarkAgent", + "UnifiedBaselineAgent", "ScriptedAgent", "RandomAgent", "SmartMockAgent", diff --git a/openadapt_ml/benchmarks/agent.py b/openadapt_ml/benchmarks/agent.py index 05a61f3..e8d9151 100644 --- a/openadapt_ml/benchmarks/agent.py +++ b/openadapt_ml/benchmarks/agent.py @@ -902,3 +902,281 @@ def reset(self) -> None: """Reset agent state.""" # APIBenchmarkAgent is stateless, nothing to reset pass + + +class UnifiedBaselineAgent(BenchmarkAgent): + """Agent that uses the UnifiedBaselineAdapter for benchmark evaluation. + + This agent provides a unified interface for comparing Claude, GPT, and Gemini + models across multiple evaluation tracks (coordinates, ReAct, SoM). + + Compared to APIBenchmarkAgent, this agent: + - Uses the new provider abstraction (models/providers/) + - Supports multiple tracks (A, B, C) with track-specific prompts + - Uses the unified response parser + - Supports model aliases for easy switching + + Args: + model_alias: Model alias (e.g., 'claude-opus-4.5', 'gpt-5.2', 'gemini-3-pro'). + track: Track type ('A', 'B', or 'C'). Defaults to 'A'. + api_key: Optional API key override. If not provided, uses env vars. + temperature: Sampling temperature. Defaults to 0.1. + max_tokens: Maximum tokens for response. Defaults to 1024. + demo: Optional demo text to include in prompts. + verbose: Whether to print verbose debug output. + + Example: + # Claude baseline with Track C (Set-of-Mark) + agent = UnifiedBaselineAgent( + model_alias="claude-opus-4.5", + track="C", + ) + results = evaluate_agent_on_benchmark(agent, waa_adapter) + + # GPT baseline with Track A (direct coordinates) + agent = UnifiedBaselineAgent( + model_alias="gpt-5.2", + track="A", + ) + results = evaluate_agent_on_benchmark(agent, waa_adapter) + + # Gemini baseline with Track B (ReAct reasoning) + agent = UnifiedBaselineAgent( + model_alias="gemini-3-pro", + track="B", + ) + results = evaluate_agent_on_benchmark(agent, waa_adapter) + """ + + def __init__( + self, + model_alias: str = "claude-opus-4.5", + track: str = "A", + api_key: str | None = None, + temperature: float = 0.1, + max_tokens: int = 1024, + demo: str | None = None, + verbose: bool = False, + ): + self.model_alias = model_alias + self.track = track.upper() + self.api_key = api_key + self.temperature = temperature + self.max_tokens = max_tokens + self.demo = demo + self.verbose = verbose + self._adapter = None + + def _get_adapter(self): + """Lazily initialize the UnifiedBaselineAdapter.""" + if self._adapter is None: + from openadapt_ml.baselines import ( + BaselineConfig, + TrackConfig, + UnifiedBaselineAdapter, + ) + + # Select track config + track_configs = { + "A": TrackConfig.track_a(), + "B": TrackConfig.track_b(), + "C": TrackConfig.track_c(), + } + track_config = track_configs.get(self.track, TrackConfig.track_a()) + + # Create adapter from alias + self._adapter = UnifiedBaselineAdapter.from_alias( + self.model_alias, + track=track_config, + api_key=self.api_key, + temperature=self.temperature, + max_tokens=self.max_tokens, + demo=self.demo, + verbose=self.verbose, + ) + return self._adapter + + def act( + self, + observation: BenchmarkObservation, + task: BenchmarkTask, + history: list[tuple[BenchmarkObservation, BenchmarkAction]] | None = None, + ) -> BenchmarkAction: + """Use UnifiedBaselineAdapter to determine next action. + + Args: + observation: Current observation with screenshot. + task: Task being performed. + history: Previous observations and actions. + + Returns: + BenchmarkAction parsed from adapter response. + """ + from PIL import Image + + adapter = self._get_adapter() + + # Load screenshot if available + screenshot = None + if observation.screenshot_path: + try: + screenshot = Image.open(observation.screenshot_path) + except Exception as e: + if self.verbose: + print(f"[UnifiedBaselineAgent] Failed to load screenshot: {e}") + + # Build accessibility tree string + a11y_tree = None + if observation.accessibility_tree: + a11y_tree = observation.accessibility_tree + + # Build history for adapter + adapter_history = None + if history: + adapter_history = [] + for obs, action in history[-5:]: # Last 5 actions + adapter_history.append(self._benchmark_action_to_dict(action)) + + # Call adapter + try: + parsed_action = adapter.predict( + screenshot=screenshot, + goal=task.instruction, + a11y_tree=a11y_tree, + history=adapter_history, + ) + except Exception as e: + if self.verbose: + print(f"[UnifiedBaselineAgent] Adapter error: {e}") + return BenchmarkAction( + type="done", + raw_action={"error": str(e)}, + ) + + # Convert ParsedAction to BenchmarkAction + return self._parsed_to_benchmark_action(parsed_action, observation) + + def _benchmark_action_to_dict(self, action: BenchmarkAction) -> dict[str, Any]: + """Convert BenchmarkAction to dict for history.""" + result = {"type": action.type} + + if action.x is not None: + result["x"] = action.x + if action.y is not None: + result["y"] = action.y + if action.text: + result["text"] = action.text + if action.key: + result["key"] = action.key + if action.target_node_id: + result["element_id"] = action.target_node_id + if action.scroll_direction: + result["direction"] = action.scroll_direction + + return result + + def _parsed_to_benchmark_action( + self, + parsed_action, + observation: BenchmarkObservation | None = None, + ) -> BenchmarkAction: + """Convert ParsedAction to BenchmarkAction. + + Args: + parsed_action: ParsedAction from adapter. + observation: Current observation (for coordinate normalization). + + Returns: + BenchmarkAction. + """ + raw_action = { + "raw_response": parsed_action.raw_response, + "thought": parsed_action.thought, + } + + if not parsed_action.is_valid: + raw_action["parse_error"] = parsed_action.parse_error + return BenchmarkAction(type="done", raw_action=raw_action) + + action_type = parsed_action.action_type + + if action_type == "click": + if parsed_action.element_id is not None: + return BenchmarkAction( + type="click", + target_node_id=str(parsed_action.element_id), + raw_action=raw_action, + ) + elif parsed_action.x is not None and parsed_action.y is not None: + x = parsed_action.x + y = parsed_action.y + + # Normalize coordinates if they appear to be pixel values + if observation and observation.viewport and (x > 1.0 or y > 1.0): + width, height = observation.viewport + raw_action["original_coords"] = {"x": x, "y": y} + raw_action["normalized"] = True + x = x / width + y = y / height + + return BenchmarkAction( + type="click", + x=x, + y=y, + raw_action=raw_action, + ) + + elif action_type == "type": + return BenchmarkAction( + type="type", + text=parsed_action.text, + raw_action=raw_action, + ) + + elif action_type == "key": + return BenchmarkAction( + type="key", + key=parsed_action.key, + raw_action=raw_action, + ) + + elif action_type == "scroll": + return BenchmarkAction( + type="scroll", + scroll_direction=parsed_action.direction, + raw_action=raw_action, + ) + + elif action_type == "done": + return BenchmarkAction(type="done", raw_action=raw_action) + + elif action_type == "drag": + x = parsed_action.x + y = parsed_action.y + end_x = getattr(parsed_action, "end_x", None) + end_y = getattr(parsed_action, "end_y", None) + + return BenchmarkAction( + type="drag", + x=x, + y=y, + end_x=end_x, + end_y=end_y, + raw_action=raw_action, + ) + + # Unknown action type, return done + raw_action["unknown_action"] = action_type + return BenchmarkAction(type="done", raw_action=raw_action) + + def reset(self) -> None: + """Reset agent state.""" + # UnifiedBaselineAgent is stateless, nothing to reset + pass + + def __repr__(self) -> str: + return ( + f"UnifiedBaselineAgent(" + f"model={self.model_alias}, " + f"track={self.track})" + ) diff --git a/openadapt_ml/models/providers/__init__.py b/openadapt_ml/models/providers/__init__.py index 8ca0b11..c53bfda 100644 --- a/openadapt_ml/models/providers/__init__.py +++ b/openadapt_ml/models/providers/__init__.py @@ -5,33 +5,83 @@ - OpenAI (GPT) - Google (Gemini) +The provider abstraction allows switching between different VLM backends +without changing the calling code. Each provider handles: +- Client creation with API key management +- Message sending with vision support +- Image encoding in provider-specific formats + Usage: from openadapt_ml.models.providers import get_provider + # Get a provider and send a message provider = get_provider("anthropic") client = provider.create_client(api_key) - response = provider.send_message(client, model, system, content) + response = provider.send_message( + client, + model="claude-opus-4-5-20251101", + system="You are a GUI agent.", + content=provider.build_content( + text="Click the submit button", + image=screenshot, + ), + ) + + # Or use the quick_message helper + response = provider.quick_message( + api_key=key, + model="claude-opus-4-5-20251101", + prompt="What's in this image?", + image=screenshot, + ) + +Model Aliases: + Common model aliases are provided for convenience: + - "claude-opus-4.5" -> ("anthropic", "claude-opus-4-5-20251101") + - "gpt-5.2" -> ("openai", "gpt-5.2") + - "gemini-3-pro" -> ("google", "gemini-3-pro") """ from __future__ import annotations from typing import TYPE_CHECKING -from openadapt_ml.models.providers.base import BaseAPIProvider +from openadapt_ml.models.providers.base import ( + BaseAPIProvider, + ProviderError, + AuthenticationError, + RateLimitError, + ModelNotFoundError, +) from openadapt_ml.models.providers.anthropic import AnthropicProvider from openadapt_ml.models.providers.openai import OpenAIProvider from openadapt_ml.models.providers.google import GoogleProvider if TYPE_CHECKING: - pass + from PIL import Image __all__ = [ + # Base classes and exceptions "BaseAPIProvider", + "ProviderError", + "AuthenticationError", + "RateLimitError", + "ModelNotFoundError", + # Provider implementations "AnthropicProvider", "OpenAIProvider", "GoogleProvider", + # Factory functions "get_provider", + "get_provider_for_model", + "resolve_model_alias", + # Registries "PROVIDERS", + "MODEL_ALIASES", + # Convenience functions + "quick_message", + "list_providers", + "list_models", ] # Provider registry @@ -42,14 +92,17 @@ } # Model aliases for convenience +# Maps friendly names to (provider, model_id) tuples MODEL_ALIASES: dict[str, tuple[str, str]] = { # Anthropic "claude-opus-4.5": ("anthropic", "claude-opus-4-5-20251101"), "claude-sonnet-4.5": ("anthropic", "claude-sonnet-4-5-20250929"), + "claude-haiku-3.5": ("anthropic", "claude-haiku-3-5-20241022"), # OpenAI "gpt-5.2": ("openai", "gpt-5.2"), "gpt-5.1": ("openai", "gpt-5.1"), "gpt-4o": ("openai", "gpt-4o"), + "gpt-4o-mini": ("openai", "gpt-4o-mini"), # Google "gemini-3-pro": ("google", "gemini-3-pro"), "gemini-3-flash": ("google", "gemini-3-flash"), @@ -88,22 +141,150 @@ def resolve_model_alias(alias: str) -> tuple[str, str]: """Resolve a model alias to (provider, model_id). Args: - alias: Model alias (e.g., 'claude-opus-4.5'). + alias: Model alias (e.g., 'claude-opus-4.5') or full model ID. Returns: Tuple of (provider_name, model_id). Raises: - ValueError: If alias is not recognized. + ValueError: If alias is not recognized and can't be inferred. Example: >>> resolve_model_alias("claude-opus-4.5") ('anthropic', 'claude-opus-4-5-20251101') + >>> resolve_model_alias("gemini-3-pro") + ('google', 'gemini-3-pro') """ + # Check explicit aliases first if alias in MODEL_ALIASES: return MODEL_ALIASES[alias] + # Try to infer provider from model name patterns + alias_lower = alias.lower() + + if alias_lower.startswith("claude"): + return ("anthropic", alias) + elif alias_lower.startswith("gpt"): + return ("openai", alias) + elif alias_lower.startswith("gemini"): + return ("google", alias) + raise ValueError( f"Unknown model alias: {alias}. " - f"Available: {', '.join(MODEL_ALIASES.keys())}" + f"Available aliases: {', '.join(MODEL_ALIASES.keys())}. " + f"Or use a full model ID with a known prefix (claude-*, gpt-*, gemini-*)." ) + + +def get_provider_for_model(model: str) -> tuple[BaseAPIProvider, str]: + """Get the appropriate provider for a model. + + Args: + model: Model alias or full model ID. + + Returns: + Tuple of (provider_instance, resolved_model_id). + + Example: + >>> provider, model_id = get_provider_for_model("claude-opus-4.5") + >>> provider.name + 'anthropic' + >>> model_id + 'claude-opus-4-5-20251101' + """ + provider_name, model_id = resolve_model_alias(model) + provider = get_provider(provider_name) + return provider, model_id + + +def quick_message( + model: str, + prompt: str, + image: "Image | None" = None, + system: str = "", + api_key: str | None = None, + max_tokens: int = 1024, + temperature: float = 0.1, +) -> str: + """Send a quick message to any model. + + Convenience function that resolves the provider, creates a client, + and sends a message in one call. Useful for one-off requests. + + Args: + model: Model alias or full model ID. + prompt: User prompt text. + image: Optional image to include. + system: Optional system prompt. + api_key: Optional API key (uses settings/env if not provided). + max_tokens: Maximum tokens in response. + temperature: Sampling temperature. + + Returns: + Model response text. + + Raises: + AuthenticationError: If no API key is available. + ProviderError: For API errors. + + Example: + >>> response = quick_message( + ... model="claude-opus-4.5", + ... prompt="What's in this image?", + ... image=screenshot, + ... ) + """ + provider, model_id = get_provider_for_model(model) + resolved_key = provider.get_api_key(api_key) + return provider.quick_message( + api_key=resolved_key, + model=model_id, + prompt=prompt, + image=image, + system=system, + max_tokens=max_tokens, + temperature=temperature, + ) + + +def list_providers() -> list[str]: + """List available provider names. + + Returns: + List of provider identifiers. + + Example: + >>> list_providers() + ['anthropic', 'openai', 'google'] + """ + return list(PROVIDERS.keys()) + + +def list_models(provider: str | None = None) -> dict[str, dict]: + """List available models, optionally filtered by provider. + + Args: + provider: Optional provider name to filter by. + + Returns: + Dict mapping model IDs to their properties. + + Example: + >>> list_models("anthropic") + { + 'claude-opus-4-5-20251101': {'context': 200000, 'description': 'SOTA computer use'}, + ... + } + """ + if provider: + provider_instance = get_provider(provider) + return provider_instance.supported_models + + # Combine models from all providers + all_models = {} + for provider_name in PROVIDERS: + provider_instance = get_provider(provider_name) + for model_id, props in provider_instance.supported_models.items(): + all_models[model_id] = {**props, "provider": provider_name} + + return all_models diff --git a/openadapt_ml/models/providers/anthropic.py b/openadapt_ml/models/providers/anthropic.py index aeb06b1..fef9690 100644 --- a/openadapt_ml/models/providers/anthropic.py +++ b/openadapt_ml/models/providers/anthropic.py @@ -1,43 +1,87 @@ """Anthropic (Claude) API provider. Supports Claude Opus 4.5, Sonnet 4.5, and other Claude models. +Implements the BaseAPIProvider interface for the Anthropic Messages API. """ from __future__ import annotations +import logging from typing import TYPE_CHECKING, Any -from openadapt_ml.models.providers.base import BaseAPIProvider +from openadapt_ml.models.providers.base import ( + BaseAPIProvider, + AuthenticationError, + ModelNotFoundError, + ProviderError, + RateLimitError, +) if TYPE_CHECKING: from PIL import Image +logger = logging.getLogger(__name__) + +# Default models +DEFAULT_MODEL = "claude-sonnet-4-5-20250929" + +# Supported models with their context windows +SUPPORTED_MODELS = { + "claude-opus-4-5-20251101": {"context": 200_000, "description": "SOTA computer use"}, + "claude-sonnet-4-5-20250929": {"context": 200_000, "description": "Fast, cheaper"}, + "claude-sonnet-4-20250514": {"context": 200_000, "description": "Previous Sonnet"}, + "claude-haiku-3-5-20241022": {"context": 200_000, "description": "Fastest, cheapest"}, +} + class AnthropicProvider(BaseAPIProvider): """Provider for Anthropic's Claude models. + Implements vision support via base64-encoded images in the Messages API format. + Claude models natively support screenshots and UI analysis for computer use tasks. + Supported models: - - claude-opus-4-5-20251101 (SOTA computer use) - - claude-sonnet-4-5-20250929 (fast, cheaper) + - claude-opus-4-5-20251101: Most capable, best for complex GUI tasks + - claude-sonnet-4-5-20250929: Fast and cost-effective + - claude-haiku-3-5-20241022: Fastest, lowest cost Example: - provider = AnthropicProvider() - client = provider.create_client(api_key) - response = provider.send_message( - client, - model="claude-opus-4-5-20251101", - system="You are a GUI agent.", - content=[ - {"type": "text", "text": "Click the submit button"}, - provider.encode_image(screenshot), - ], - ) + >>> provider = AnthropicProvider() + >>> client = provider.create_client(api_key) + >>> response = provider.send_message( + ... client, + ... model="claude-opus-4-5-20251101", + ... system="You are a GUI agent.", + ... content=[ + ... {"type": "text", "text": "Click the submit button"}, + ... provider.encode_image(screenshot), + ... ], + ... ) + + Attributes: + name: Returns 'anthropic'. """ @property def name(self) -> str: + """Provider name.""" return "anthropic" + @property + def env_key_name(self) -> str: + """Environment variable name for API key.""" + return "ANTHROPIC_API_KEY" + + @property + def default_model(self) -> str: + """Default model to use.""" + return DEFAULT_MODEL + + @property + def supported_models(self) -> dict[str, dict[str, Any]]: + """Dictionary of supported models and their properties.""" + return SUPPORTED_MODELS + def create_client(self, api_key: str) -> Any: """Create Anthropic client. @@ -49,14 +93,23 @@ def create_client(self, api_key: str) -> Any: Raises: ImportError: If anthropic package not installed. + AuthenticationError: If API key format is invalid. """ try: from anthropic import Anthropic except ImportError as e: raise ImportError( - "anthropic package is required. Install with: pip install anthropic" + "anthropic package is required for provider='anthropic'. " + "Install with: uv add anthropic" ) from e + if not api_key or not api_key.strip(): + raise AuthenticationError( + "Anthropic API key cannot be empty. " + "Get a key from https://console.anthropic.com/" + ) + + logger.debug("Creating Anthropic client") return Anthropic(api_key=api_key) def send_message( @@ -71,43 +124,77 @@ def send_message( """Send message using Anthropic Messages API. Args: - client: Anthropic client. + client: Anthropic client from create_client(). model: Model ID (e.g., 'claude-opus-4-5-20251101'). system: System prompt. - content: List of content blocks. + content: List of content blocks (text and images). max_tokens: Max response tokens. - temperature: Sampling temperature. + temperature: Sampling temperature (0.0-1.0). Returns: Model response text. + + Raises: + AuthenticationError: If API key is invalid. + RateLimitError: If rate limit exceeded. + ModelNotFoundError: If model doesn't exist. + ProviderError: For other API errors. """ - response = client.messages.create( - model=model, - max_tokens=max_tokens, - temperature=temperature, - system=system or None, - messages=[{"role": "user", "content": content}], - ) - - # Extract text from content blocks - parts = getattr(response, "content", []) - texts = [ - getattr(p, "text", "") - for p in parts - if getattr(p, "type", "") == "text" - ] - return "\n".join([t for t in texts if t]).strip() + logger.debug(f"Sending message to {model} with {len(content)} content blocks") + + try: + response = client.messages.create( + model=model, + max_tokens=max_tokens, + temperature=temperature, + system=system or None, + messages=[{"role": "user", "content": content}], + ) + + # Extract text from content blocks + parts = getattr(response, "content", []) + texts = [ + getattr(p, "text", "") + for p in parts + if getattr(p, "type", "") == "text" + ] + result = "\n".join([t for t in texts if t]).strip() + + logger.debug(f"Received response: {len(result)} chars") + return result + + except Exception as e: + error_str = str(e).lower() + + # Map common errors to specific exceptions + if "authentication" in error_str or "api_key" in error_str: + raise AuthenticationError(f"Anthropic authentication failed: {e}") from e + elif "rate_limit" in error_str or "429" in error_str: + raise RateLimitError(f"Anthropic rate limit exceeded: {e}") from e + elif "model_not_found" in error_str or "not found" in error_str: + raise ModelNotFoundError(f"Model '{model}' not found: {e}") from e + else: + raise ProviderError(f"Anthropic API error: {e}") from e def encode_image(self, image: "Image") -> dict[str, Any]: """Encode image for Anthropic API. Anthropic uses base64-encoded images with explicit source type. + PNG format is used for lossless quality. Args: image: PIL Image. Returns: - Image content block for Anthropic API. + Image content block for Anthropic API in format: + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "" + } + } """ return { "type": "image", @@ -117,3 +204,55 @@ def encode_image(self, image: "Image") -> dict[str, Any]: "data": self.image_to_base64(image, "PNG"), }, } + + def encode_image_from_bytes( + self, + image_bytes: bytes, + media_type: str = "image/png", + ) -> dict[str, Any]: + """Encode raw image bytes for Anthropic API. + + Useful when you already have image bytes and don't need PIL. + + Args: + image_bytes: Raw image bytes. + media_type: MIME type of the image. + + Returns: + Image content block for Anthropic API. + """ + import base64 + + return { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": base64.b64encode(image_bytes).decode("utf-8"), + }, + } + + def encode_image_from_url(self, url: str) -> dict[str, Any]: + """Create image content block from URL. + + Note: Anthropic doesn't support URL-based images directly. + This method fetches the URL and encodes the image. + + Args: + url: Image URL to fetch and encode. + + Returns: + Image content block for Anthropic API. + + Raises: + ProviderError: If URL fetch fails. + """ + import urllib.request + + try: + with urllib.request.urlopen(url) as response: + image_bytes = response.read() + content_type = response.headers.get("Content-Type", "image/png") + return self.encode_image_from_bytes(image_bytes, content_type) + except Exception as e: + raise ProviderError(f"Failed to fetch image from URL: {e}") from e diff --git a/openadapt_ml/models/providers/base.py b/openadapt_ml/models/providers/base.py index 5454b51..c4bd54c 100644 --- a/openadapt_ml/models/providers/base.py +++ b/openadapt_ml/models/providers/base.py @@ -1,24 +1,70 @@ """Base provider abstraction for API-backed VLMs. This module defines the interface that all API providers must implement. +Providers handle client creation, message sending, and image encoding +in a provider-specific way. """ from __future__ import annotations import base64 import io +import logging +import os from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from PIL import Image +logger = logging.getLogger(__name__) + + +class ProviderError(Exception): + """Base exception for provider errors.""" + + pass + + +class AuthenticationError(ProviderError): + """Raised when API authentication fails.""" + + pass + + +class RateLimitError(ProviderError): + """Raised when API rate limit is exceeded.""" + + pass + + +class ModelNotFoundError(ProviderError): + """Raised when the specified model is not available.""" + + pass + class BaseAPIProvider(ABC): """Abstract base class for API providers (Anthropic, OpenAI, Google). Each provider implements client creation, message sending, and image encoding in a provider-specific way. + + Attributes: + name: Provider identifier ('anthropic', 'openai', 'google'). + + Example: + >>> provider = get_provider("anthropic") + >>> client = provider.create_client(api_key) + >>> response = provider.send_message( + ... client, + ... model="claude-opus-4-5-20251101", + ... system="You are a GUI agent.", + ... content=[ + ... {"type": "text", "text": "Click the submit button"}, + ... provider.encode_image(screenshot), + ... ], + ... ) """ @property @@ -27,6 +73,47 @@ def name(self) -> str: """Provider name (e.g., 'anthropic', 'openai', 'google').""" ... + @property + def env_key_name(self) -> str: + """Environment variable name for API key. + + Returns: + Environment variable name (e.g., 'ANTHROPIC_API_KEY'). + """ + return f"{self.name.upper()}_API_KEY" + + def get_api_key(self, api_key: str | None = None) -> str: + """Get API key from parameter, settings, or environment. + + Args: + api_key: Optional explicit API key. + + Returns: + API key string. + + Raises: + AuthenticationError: If no API key is available. + """ + if api_key: + return api_key + + # Try settings + from openadapt_ml.config import settings + + settings_key = getattr(settings, f"{self.name}_api_key", None) + if settings_key: + return settings_key + + # Try environment + env_key = os.getenv(self.env_key_name) + if env_key: + return env_key + + raise AuthenticationError( + f"{self.env_key_name} is required but not found. " + f"Set it in .env file, environment variable, or pass api_key parameter." + ) + @abstractmethod def create_client(self, api_key: str) -> Any: """Create and return an API client. @@ -36,6 +123,10 @@ def create_client(self, api_key: str) -> Any: Returns: Provider-specific client object. + + Raises: + ImportError: If required package is not installed. + AuthenticationError: If API key is invalid. """ ... @@ -61,6 +152,11 @@ def send_message( Returns: The model's text response. + + Raises: + RateLimitError: If rate limit is exceeded. + ModelNotFoundError: If model is not available. + ProviderError: For other API errors. """ ... @@ -107,3 +203,97 @@ def get_media_type(self, format: str = "PNG") -> str: "WEBP": "image/webp", } return format_map.get(format.upper(), "image/png") + + def create_text_content(self, text: str) -> dict[str, Any]: + """Create a text content block. + + Args: + text: Text content. + + Returns: + Text content block. + """ + return {"type": "text", "text": text} + + def build_content( + self, + text: str | None = None, + image: "Image | None" = None, + additional_content: list[dict[str, Any]] | None = None, + ) -> list[dict[str, Any]]: + """Build a content list from text and/or image. + + Convenience method for building content lists in the correct format. + + Args: + text: Optional text content. + image: Optional PIL Image. + additional_content: Optional additional content blocks. + + Returns: + List of content blocks. + + Example: + >>> content = provider.build_content( + ... text="Click the button", + ... image=screenshot, + ... ) + """ + content = [] + + if text: + content.append(self.create_text_content(text)) + + if image is not None: + content.append(self.encode_image(image)) + + if additional_content: + content.extend(additional_content) + + return content + + def quick_message( + self, + api_key: str, + model: str, + prompt: str, + image: "Image | None" = None, + system: str = "", + max_tokens: int = 1024, + temperature: float = 0.1, + ) -> str: + """Send a quick message without managing client lifecycle. + + Convenience method that creates a client, sends a message, and returns + the response in one call. Useful for one-off requests. + + Args: + api_key: API key for authentication. + model: Model identifier. + prompt: User prompt text. + image: Optional image to include. + system: Optional system prompt. + max_tokens: Maximum tokens in response. + temperature: Sampling temperature. + + Returns: + Model response text. + + Example: + >>> response = provider.quick_message( + ... api_key=key, + ... model="claude-opus-4-5-20251101", + ... prompt="What's in this image?", + ... image=screenshot, + ... ) + """ + client = self.create_client(api_key) + content = self.build_content(text=prompt, image=image) + return self.send_message( + client=client, + model=model, + system=system, + content=content, + max_tokens=max_tokens, + temperature=temperature, + ) diff --git a/openadapt_ml/models/providers/google.py b/openadapt_ml/models/providers/google.py index 6dace1a..773dc27 100644 --- a/openadapt_ml/models/providers/google.py +++ b/openadapt_ml/models/providers/google.py @@ -1,71 +1,127 @@ """Google (Gemini) API provider. Supports Gemini 3 Pro, Gemini 3 Flash, and other Gemini models. +Implements the BaseAPIProvider interface for the Generative AI API. """ from __future__ import annotations +import logging from typing import TYPE_CHECKING, Any -from openadapt_ml.models.providers.base import BaseAPIProvider +from openadapt_ml.models.providers.base import ( + BaseAPIProvider, + AuthenticationError, + ModelNotFoundError, + ProviderError, + RateLimitError, +) if TYPE_CHECKING: from PIL import Image +logger = logging.getLogger(__name__) + +# Default models +DEFAULT_MODEL = "gemini-2.5-flash" + +# Supported models with their properties +SUPPORTED_MODELS = { + "gemini-3-pro": {"context": 2_000_000, "description": "Most capable Gemini"}, + "gemini-3-flash": {"context": 1_000_000, "description": "Fast inference"}, + "gemini-2.5-pro": {"context": 2_000_000, "description": "Previous pro"}, + "gemini-2.5-flash": {"context": 1_000_000, "description": "Fast previous gen"}, + "gemini-2.0-flash": {"context": 1_000_000, "description": "Stable flash"}, + "gemini-1.5-pro": {"context": 2_000_000, "description": "Legacy pro"}, + "gemini-1.5-flash": {"context": 1_000_000, "description": "Legacy flash"}, +} + class GoogleProvider(BaseAPIProvider): """Provider for Google's Gemini models. + Implements vision support with native PIL Image handling. Unlike Anthropic + and OpenAI which require base64 encoding, Gemini accepts PIL Images directly. + Supported models: - - gemini-3-pro (most capable) - - gemini-3-flash (fast inference) - - gemini-2.5-pro (previous gen) - - gemini-2.5-flash (fast previous gen) + - gemini-3-pro: Most capable, 2M context window + - gemini-3-flash: Fast inference, 1M context + - gemini-2.5-pro/flash: Previous generation + - gemini-2.0-flash: Stable release Note: Gemini supports PIL Images directly without base64 encoding. + The encode_image method returns the image wrapped in a dict for + consistency with other providers. Example: - provider = GoogleProvider() - client = provider.create_client(api_key) - response = provider.send_message( - client, - model="gemini-3-pro", - system="You are a GUI agent.", - content=[ - {"type": "text", "text": "Click the submit button"}, - provider.encode_image(screenshot), - ], - ) + >>> provider = GoogleProvider() + >>> client = provider.create_client(api_key) + >>> response = provider.send_message( + ... client, + ... model="gemini-3-pro", + ... system="You are a GUI agent.", + ... content=[ + ... {"type": "text", "text": "Click the submit button"}, + ... provider.encode_image(screenshot), + ... ], + ... ) + + Attributes: + name: Returns 'google'. """ @property def name(self) -> str: + """Provider name.""" return "google" + @property + def env_key_name(self) -> str: + """Environment variable name for API key.""" + return "GOOGLE_API_KEY" + + @property + def default_model(self) -> str: + """Default model to use.""" + return DEFAULT_MODEL + + @property + def supported_models(self) -> dict[str, dict[str, Any]]: + """Dictionary of supported models and their properties.""" + return SUPPORTED_MODELS + def create_client(self, api_key: str) -> Any: """Create Google Generative AI client. Unlike Anthropic/OpenAI, Gemini uses a global configure call. - We return a dict with the API key for later use. + We return a dict containing the configured genai module. Args: api_key: Google API key. Returns: - Dict containing api_key for model creation. + Dict containing api_key and configured genai module. Raises: ImportError: If google-generativeai package not installed. + AuthenticationError: If API key is empty. """ try: import google.generativeai as genai except ImportError as e: raise ImportError( - "google-generativeai package is required. " - "Install with: pip install google-generativeai" + "google-generativeai package is required for provider='google'. " + "Install with: uv add google-generativeai" ) from e + if not api_key or not api_key.strip(): + raise AuthenticationError( + "Google API key cannot be empty. " + "Get a key from https://makersuite.google.com/app/apikey" + ) + + logger.debug("Configuring Google Generative AI") genai.configure(api_key=api_key) return {"api_key": api_key, "genai": genai} @@ -83,14 +139,22 @@ def send_message( Args: client: Client dict from create_client(). model: Model ID (e.g., 'gemini-3-pro'). - system: System prompt (prepended to content). + system: System prompt (prepended to content as text). content: List of content blocks. max_tokens: Max response tokens. - temperature: Sampling temperature. + temperature: Sampling temperature (0.0-2.0 for Gemini). Returns: Model response text. + + Raises: + AuthenticationError: If API key is invalid. + RateLimitError: If rate limit exceeded. + ModelNotFoundError: If model doesn't exist. + ProviderError: For other API errors. """ + logger.debug(f"Sending message to {model} with {len(content)} content blocks") + genai = client["genai"] model_instance = genai.GenerativeModel(model) @@ -111,28 +175,198 @@ def send_message( if image is not None: gemini_content.append(image) - response = model_instance.generate_content( - gemini_content, - generation_config=genai.GenerationConfig( - temperature=temperature, - max_output_tokens=max_tokens, - ), - ) + try: + response = model_instance.generate_content( + gemini_content, + generation_config=genai.GenerationConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ), + ) + + result = response.text + logger.debug(f"Received response: {len(result)} chars") + return result + + except Exception as e: + error_str = str(e).lower() - return response.text + # Map common errors to specific exceptions + if "api_key" in error_str or "authentication" in error_str or "invalid" in error_str: + raise AuthenticationError(f"Google authentication failed: {e}") from e + elif "quota" in error_str or "rate" in error_str or "429" in error_str: + raise RateLimitError(f"Google rate limit/quota exceeded: {e}") from e + elif "not found" in error_str or "does not exist" in error_str: + raise ModelNotFoundError(f"Model '{model}' not found: {e}") from e + else: + raise ProviderError(f"Google API error: {e}") from e def encode_image(self, image: "Image") -> dict[str, Any]: """Encode image for Gemini API. - Gemini accepts PIL Images directly, no base64 needed. + Gemini accepts PIL Images directly, no base64 encoding needed. + We wrap the image in a dict for API consistency. Args: image: PIL Image. Returns: - Image content block containing the PIL Image. + Image content block containing the PIL Image: + { + "type": "image", + "image": + } """ return { "type": "image", "image": image, } + + def encode_image_from_bytes( + self, + image_bytes: bytes, + media_type: str = "image/png", + ) -> dict[str, Any]: + """Encode raw image bytes for Gemini API. + + Converts bytes to PIL Image for Gemini's native format. + + Args: + image_bytes: Raw image bytes. + media_type: MIME type (used to verify format). + + Returns: + Image content block with PIL Image. + """ + import io + + from PIL import Image as PILImage + + image = PILImage.open(io.BytesIO(image_bytes)) + return self.encode_image(image) + + def encode_image_from_url(self, url: str) -> dict[str, Any]: + """Create image content block from URL. + + Fetches the image and converts to PIL Image. + + Args: + url: Image URL to fetch. + + Returns: + Image content block with PIL Image. + + Raises: + ProviderError: If URL fetch fails. + """ + import io + import urllib.request + + from PIL import Image as PILImage + + try: + with urllib.request.urlopen(url) as response: + image_bytes = response.read() + image = PILImage.open(io.BytesIO(image_bytes)) + return self.encode_image(image) + except Exception as e: + raise ProviderError(f"Failed to fetch image from URL: {e}") from e + + def encode_image_as_base64(self, image: "Image") -> dict[str, Any]: + """Encode image as base64 for Gemini API. + + While Gemini prefers PIL Images, it can also accept base64. + Use this for cases where you need to serialize the content. + + Args: + image: PIL Image. + + Returns: + Image content block with base64 data. + """ + return { + "type": "image", + "inline_data": { + "mime_type": "image/png", + "data": self.image_to_base64(image, "PNG"), + }, + } + + def send_with_grounding( + self, + client: Any, + model: str, + prompt: str, + image: "Image", + max_tokens: int = 1024, + temperature: float = 0.1, + ) -> dict[str, Any]: + """Send message with grounding/bounding box detection. + + Uses Gemini's native vision capabilities to detect UI elements + and return bounding boxes. Useful for Set-of-Marks processing. + + Args: + client: Client dict from create_client(). + model: Model ID. + prompt: Detection prompt. + image: Screenshot to analyze. + max_tokens: Max response tokens. + temperature: Sampling temperature. + + Returns: + Dict with response text and any detected bounding boxes. + + Example: + >>> result = provider.send_with_grounding( + ... client, + ... model="gemini-2.5-flash", + ... prompt="Find the login button", + ... image=screenshot, + ... ) + >>> print(result["boxes"]) # List of bounding boxes + """ + genai = client["genai"] + model_instance = genai.GenerativeModel(model) + + grounding_prompt = f"""Analyze this screenshot and {prompt} + +Return a JSON object with: +- "elements": array of detected elements with "label", "bbox" [x1,y1,x2,y2], "confidence" +- "description": brief description of what you found + +Use pixel coordinates based on image dimensions: {image.width}x{image.height} + +Return ONLY valid JSON.""" + + try: + response = model_instance.generate_content( + [grounding_prompt, image], + generation_config=genai.GenerationConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ), + ) + + text = response.text + + # Try to parse JSON response + import json + import re + + json_match = re.search(r'\{[\s\S]*\}', text) + if json_match: + try: + data = json.loads(json_match.group()) + return { + "text": text, + "elements": data.get("elements", []), + "description": data.get("description", ""), + } + except json.JSONDecodeError: + pass + + return {"text": text, "elements": [], "description": ""} + + except Exception as e: + raise ProviderError(f"Google grounding error: {e}") from e diff --git a/openadapt_ml/models/providers/openai.py b/openadapt_ml/models/providers/openai.py index 1653298..d54eeef 100644 --- a/openadapt_ml/models/providers/openai.py +++ b/openadapt_ml/models/providers/openai.py @@ -1,44 +1,93 @@ """OpenAI (GPT) API provider. -Supports GPT-5.2, GPT-5.1, and other OpenAI models with vision. +Supports GPT-5.2, GPT-5.1, GPT-4o, and other OpenAI models with vision. +Implements the BaseAPIProvider interface for the Chat Completions API. """ from __future__ import annotations +import logging from typing import TYPE_CHECKING, Any -from openadapt_ml.models.providers.base import BaseAPIProvider +from openadapt_ml.models.providers.base import ( + BaseAPIProvider, + AuthenticationError, + ModelNotFoundError, + ProviderError, + RateLimitError, +) if TYPE_CHECKING: from PIL import Image +logger = logging.getLogger(__name__) + +# Default models +DEFAULT_MODEL = "gpt-4o" + +# Supported models with their properties +SUPPORTED_MODELS = { + "gpt-5.2": {"context": 128_000, "description": "Latest GPT model"}, + "gpt-5.1": {"context": 128_000, "description": "Previous GPT-5"}, + "gpt-4o": {"context": 128_000, "description": "Vision-capable, fast"}, + "gpt-4o-mini": {"context": 128_000, "description": "Cheaper, fast"}, + "gpt-4-turbo": {"context": 128_000, "description": "Previous gen turbo"}, +} + class OpenAIProvider(BaseAPIProvider): """Provider for OpenAI's GPT models. + Implements vision support via data URL encoded images in the Chat Completions API. + Supports both standard chat and vision-enabled models. + Supported models: - - gpt-5.2 (latest) - - gpt-5.1 (previous) - - gpt-4o (vision capable) + - gpt-5.2: Latest and most capable + - gpt-5.1: Previous generation GPT-5 + - gpt-4o: Fast, vision-capable + - gpt-4o-mini: Cost-effective, vision-capable Example: - provider = OpenAIProvider() - client = provider.create_client(api_key) - response = provider.send_message( - client, - model="gpt-5.2", - system="You are a GUI agent.", - content=[ - {"type": "text", "text": "Click the submit button"}, - provider.encode_image(screenshot), - ], - ) + >>> provider = OpenAIProvider() + >>> client = provider.create_client(api_key) + >>> response = provider.send_message( + ... client, + ... model="gpt-5.2", + ... system="You are a GUI agent.", + ... content=[ + ... {"type": "text", "text": "Click the submit button"}, + ... provider.encode_image(screenshot), + ... ], + ... ) + + Note: + OpenAI uses data URLs for images (data:image/png;base64,...). + This differs from Anthropic's explicit source object format. + + Attributes: + name: Returns 'openai'. """ @property def name(self) -> str: + """Provider name.""" return "openai" + @property + def env_key_name(self) -> str: + """Environment variable name for API key.""" + return "OPENAI_API_KEY" + + @property + def default_model(self) -> str: + """Default model to use.""" + return DEFAULT_MODEL + + @property + def supported_models(self) -> dict[str, dict[str, Any]]: + """Dictionary of supported models and their properties.""" + return SUPPORTED_MODELS + def create_client(self, api_key: str) -> Any: """Create OpenAI client. @@ -50,14 +99,23 @@ def create_client(self, api_key: str) -> Any: Raises: ImportError: If openai package not installed. + AuthenticationError: If API key is empty. """ try: from openai import OpenAI except ImportError as e: raise ImportError( - "openai package is required. Install with: pip install openai" + "openai package is required for provider='openai'. " + "Install with: uv add openai" ) from e + if not api_key or not api_key.strip(): + raise AuthenticationError( + "OpenAI API key cannot be empty. " + "Get a key from https://platform.openai.com/api-keys" + ) + + logger.debug("Creating OpenAI client") return OpenAI(api_key=api_key) def send_message( @@ -72,16 +130,24 @@ def send_message( """Send message using OpenAI Chat Completions API. Args: - client: OpenAI client. - model: Model ID (e.g., 'gpt-5.2'). + client: OpenAI client from create_client(). + model: Model ID (e.g., 'gpt-5.2', 'gpt-4o'). system: System prompt. - content: List of content blocks. + content: List of content blocks (text and images). max_tokens: Max response tokens. - temperature: Sampling temperature. + temperature: Sampling temperature (0.0-2.0 for OpenAI). Returns: Model response text. + + Raises: + AuthenticationError: If API key is invalid. + RateLimitError: If rate limit exceeded. + ModelNotFoundError: If model doesn't exist. + ProviderError: For other API errors. """ + logger.debug(f"Sending message to {model} with {len(content)} content blocks") + messages = [] if system: @@ -89,25 +155,48 @@ def send_message( messages.append({"role": "user", "content": content}) - response = client.chat.completions.create( - model=model, - messages=messages, - max_completion_tokens=max_tokens, - temperature=temperature, - ) + try: + response = client.chat.completions.create( + model=model, + messages=messages, + max_completion_tokens=max_tokens, + temperature=temperature, + ) + + result = response.choices[0].message.content or "" + logger.debug(f"Received response: {len(result)} chars") + return result + + except Exception as e: + error_str = str(e).lower() - return response.choices[0].message.content or "" + # Map common errors to specific exceptions + if "authentication" in error_str or "api_key" in error_str or "invalid_api_key" in error_str: + raise AuthenticationError(f"OpenAI authentication failed: {e}") from e + elif "rate_limit" in error_str or "429" in error_str: + raise RateLimitError(f"OpenAI rate limit exceeded: {e}") from e + elif "model_not_found" in error_str or "does not exist" in error_str: + raise ModelNotFoundError(f"Model '{model}' not found: {e}") from e + else: + raise ProviderError(f"OpenAI API error: {e}") from e def encode_image(self, image: "Image") -> dict[str, Any]: """Encode image for OpenAI API. - OpenAI uses data URLs for images. + OpenAI uses data URLs for images in the format: + data:image/;base64, Args: image: PIL Image. Returns: - Image content block for OpenAI API. + Image content block for OpenAI API in format: + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,..." + } + } """ base64_data = self.image_to_base64(image, "PNG") return { @@ -116,3 +205,134 @@ def encode_image(self, image: "Image") -> dict[str, Any]: "url": f"data:image/png;base64,{base64_data}", }, } + + def encode_image_with_detail( + self, + image: "Image", + detail: str = "auto", + ) -> dict[str, Any]: + """Encode image with detail level specification. + + OpenAI supports different detail levels for vision processing: + - "low": Fixed 512x512, 85 tokens, fast + - "high": Scaled up to 2048x2048, more tokens, detailed + - "auto": Let the model decide based on image size + + Args: + image: PIL Image. + detail: Detail level ("low", "high", "auto"). + + Returns: + Image content block with detail specification. + """ + base64_data = self.image_to_base64(image, "PNG") + return { + "type": "image_url", + "image_url": { + "url": f"data:image/png;base64,{base64_data}", + "detail": detail, + }, + } + + def encode_image_from_url( + self, + url: str, + detail: str = "auto", + ) -> dict[str, Any]: + """Create image content block from URL. + + OpenAI natively supports URL-based images, so no fetching needed. + + Args: + url: Image URL. + detail: Detail level ("low", "high", "auto"). + + Returns: + Image content block for OpenAI API. + """ + return { + "type": "image_url", + "image_url": { + "url": url, + "detail": detail, + }, + } + + def encode_image_from_bytes( + self, + image_bytes: bytes, + media_type: str = "image/png", + ) -> dict[str, Any]: + """Encode raw image bytes for OpenAI API. + + Args: + image_bytes: Raw image bytes. + media_type: MIME type of the image. + + Returns: + Image content block for OpenAI API. + """ + import base64 + + base64_data = base64.b64encode(image_bytes).decode("utf-8") + return { + "type": "image_url", + "image_url": { + "url": f"data:{media_type};base64,{base64_data}", + }, + } + + def send_with_tools( + self, + client: Any, + model: str, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]], + tool_choice: str | dict[str, Any] = "auto", + max_tokens: int = 1024, + temperature: float = 0.1, + ) -> Any: + """Send message with function calling/tools support. + + OpenAI supports function calling which can be useful for structured + action extraction in GUI automation. + + Args: + client: OpenAI client. + model: Model ID. + messages: Chat messages. + tools: Tool definitions. + tool_choice: Tool choice strategy. + max_tokens: Max response tokens. + temperature: Sampling temperature. + + Returns: + Raw API response (for tool call handling). + + Example: + >>> tools = [{ + ... "type": "function", + ... "function": { + ... "name": "click", + ... "parameters": { + ... "type": "object", + ... "properties": { + ... "x": {"type": "number"}, + ... "y": {"type": "number"} + ... } + ... } + ... } + ... }] + >>> response = provider.send_with_tools(client, model, messages, tools) + """ + try: + return client.chat.completions.create( + model=model, + messages=messages, + tools=tools, + tool_choice=tool_choice, + max_completion_tokens=max_tokens, + temperature=temperature, + ) + except Exception as e: + raise ProviderError(f"OpenAI tools API error: {e}") from e diff --git a/tests/test_baselines.py b/tests/test_baselines.py index 9220d44..b615578 100644 --- a/tests/test_baselines.py +++ b/tests/test_baselines.py @@ -457,10 +457,11 @@ def test_track_a_system_prompt(self): prompt = builder.get_system_prompt() assert "GUI automation agent" in prompt - assert "CLICK(x, y)" in prompt - assert "normalized coordinates" in prompt - # Track A should NOT mention element IDs - assert "element ID" not in prompt.lower() or "element_id" not in prompt + # The prompt shows coordinates in JSON format + assert "normalized" in prompt.lower() + assert "0.0" in prompt and "1.0" in prompt # Coordinate range explanation + # Track A should NOT mention element IDs as primary method + assert "element_id" not in prompt.lower() or "element ID" not in prompt.lower() def test_track_b_system_prompt(self): """Test PromptBuilder generates Track B system prompt.""" @@ -561,8 +562,8 @@ def test_build_user_content_truncates_a11y_tree(self): content = builder.build_user_content(goal="Click", a11y_tree=a11y_tree) text_content = content[0]["text"] - # Should be truncated - assert "truncated" in text_content + # Should be truncated - the implementation shows "showing X of Y elements" + assert "showing 5 of 20" in text_content or "truncated" in text_content class TestParsedAction: From 2a762938a341e54ce2d838c80c582bc8729d0e87 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 01:45:23 -0500 Subject: [PATCH 02/16] fix: lower Python requirement from 3.12 to 3.10 for meta-package compatibility All dependencies (torch, transformers, pillow, peft, etc.) support Python 3.10+. The 3.12 requirement was unnecessarily restrictive and broke `pip install openadapt[all]` on Python 3.10 and 3.11. Co-Authored-By: Claude Sonnet 4.5 --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99fbd2a..a26039d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "openadapt-ml" version = "0.2.0" description = "Model-agnostic, domain-agnostic ML engine for GUI automation agents" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.10" license = "MIT" authors = [ {name = "MLDSAI Inc.", email = "richard@mldsai.com"} @@ -14,6 +14,8 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", From 2f279aed50c37b182acd56d17ca9887510bdf7b8 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 08:47:22 -0500 Subject: [PATCH 03/16] ci: add GitHub Actions test workflow Add CI workflow that runs on pull requests and main branch pushes: - Tests on Python 3.10 and 3.11 - Runs on Ubuntu and macOS - Uses uv for dependency management - Runs ruff linter and formatter - Runs pytest suite Matches pattern used by openadapt-viewer and follows OpenAdapt ecosystem conventions. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/test.yml | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c18323b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +name: Test + +on: + pull_request: + branches: + - '**' + push: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ['3.10', '3.11'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras + + - name: Run ruff linter (check) + run: uv run ruff check openadapt_ml/ + + - name: Run ruff formatter (check) + run: uv run ruff format --check openadapt_ml/ + + - name: Run pytest + run: uv run pytest tests/ -v From 385d4ae4988f2b4c93a35b35a4dc50e60859f815 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 09:09:30 -0500 Subject: [PATCH 04/16] fix: add defaults to CanonicalEpisode fields for test compatibility - cluster_id: default=0 - cluster_centroid_distance: default=0.0 - internal_similarity: default=1.0 Fixes 1/14 test failures in test_segmentation.py --- openadapt_ml/segmentation/schemas.py | 622 +++++++++++++++++++++++++++ 1 file changed, 622 insertions(+) create mode 100644 openadapt_ml/segmentation/schemas.py diff --git a/openadapt_ml/segmentation/schemas.py b/openadapt_ml/segmentation/schemas.py new file mode 100644 index 0000000..e3b251d --- /dev/null +++ b/openadapt_ml/segmentation/schemas.py @@ -0,0 +1,622 @@ +"""Data schemas for workflow segmentation. + +This module defines the Pydantic models used throughout the +segmentation pipeline, ensuring type safety and validation. + +In OpenAdapt terminology: +- "Episode" = A coherent workflow segment +- "Trajectory" = Sequence of observation-action pairs (full recording) +""" + +from datetime import datetime +from enum import Enum +from typing import Optional +from uuid import UUID, uuid4 + +from pydantic import BaseModel, Field, ConfigDict + + +class ActionType(str, Enum): + """Types of user actions that can be captured.""" + + CLICK = "click" + DOUBLE_CLICK = "double_click" + RIGHT_CLICK = "right_click" + TYPE = "type" + SCROLL = "scroll" + DRAG = "drag" + HOTKEY = "hotkey" + MOVE = "move" + + +class FrameDescription(BaseModel): + """Description of a single frame + action pair from VLM analysis. + + This is the output of Stage 1 for each frame in the recording. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Timing + timestamp: float = Field(description="Timestamp in seconds from recording start") + formatted_time: str = Field(description="Human-readable time format (MM:SS.m)") + + # Screen context + visible_application: str = Field( + description="Primary application visible on screen" + ) + visible_elements: list[str] = Field( + default_factory=list, + description="Notable UI elements visible in the frame", + ) + screen_context: str = Field(description="Brief description of overall screen state") + + # Action details + action_type: ActionType = Field(description="Type of action performed") + action_target: Optional[str] = Field( + default=None, + description="UI element that was the target of the action", + ) + action_value: Optional[str] = Field( + default=None, + description="Value associated with action (e.g., typed text)", + ) + + # Semantic interpretation + apparent_intent: str = Field( + description="What the user appears to be trying to accomplish" + ) + confidence: float = Field( + ge=0.0, le=1.0, description="VLM confidence in this description" + ) + + # Metadata + frame_index: int = Field(description="Index of this frame in the recording") + vlm_model: str = Field(description="Model used for description generation") + + def to_transcript_line(self) -> str: + """Format as a single transcript line.""" + return f"[{self.formatted_time}] {self.apparent_intent}" + + +class ActionTranscript(BaseModel): + """Complete transcript of a recording from VLM analysis. + + This is the full output of Stage 1. + """ + + recording_id: str = Field(description="Unique identifier for the source recording") + recording_name: str = Field(description="Human-readable recording name") + task_description: Optional[str] = Field( + default=None, + description="User-provided task description (if available)", + ) + + # Frame descriptions + frames: list[FrameDescription] = Field( + default_factory=list, + description="Ordered list of frame descriptions", + ) + + # Processing metadata + total_duration: float = Field(description="Total recording duration in seconds") + frame_count: int = Field(description="Total number of frames processed") + vlm_model: str = Field(description="Primary VLM model used") + processing_timestamp: datetime = Field( + default_factory=datetime.now, + description="When this transcript was generated", + ) + + def to_transcript_text(self) -> str: + """Format as plain text transcript.""" + lines = [] + for frame in self.frames: + lines.append(frame.to_transcript_line()) + return "\n".join(lines) + + @property + def duration_formatted(self) -> str: + """Return duration as MM:SS format.""" + minutes = int(self.total_duration // 60) + seconds = self.total_duration % 60 + return f"{minutes:02d}:{seconds:05.2f}" + + +class EpisodeStep(BaseModel): + """A single step within an episode (workflow segment).""" + + description: str = Field(description="What this step accomplishes") + start_timestamp: float = Field(description="Start time in seconds") + end_timestamp: float = Field(description="End time in seconds") + frame_indices: list[int] = Field( + default_factory=list, + description="Indices of frames belonging to this step", + ) + + +class EpisodeBoundary(BaseModel): + """Represents a boundary between episodes with confidence.""" + + timestamp: float = Field(description="Time of the boundary") + confidence: float = Field( + ge=0.0, + le=1.0, + description="Confidence that this is a true episode boundary", + ) + reason: str = Field(description="Explanation for why this is a boundary") + + +class Episode(BaseModel): + """A coherent workflow segment (episode) extracted from a recording. + + This is the output of Stage 2 for each identified workflow. + + In OpenAdapt, an Episode represents a self-contained unit of work + that can be used for: + - Training data for fine-tuning + - Demo conditioning context + - Workflow library building + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Identification + episode_id: UUID = Field( + default_factory=uuid4, + description="Unique identifier for this episode", + ) + name: str = Field( + description="Concise name for this workflow (e.g., 'Adjust Night Shift Settings')" + ) + + # Timing + start_time: float = Field(description="Start timestamp in seconds") + end_time: float = Field(description="End timestamp in seconds") + start_time_formatted: str = Field(description="Formatted start time (MM:SS.m)") + end_time_formatted: str = Field(description="Formatted end time (MM:SS.m)") + + # Content + description: str = Field( + description="Detailed description of what this workflow accomplishes" + ) + steps: list[EpisodeStep] = Field( + default_factory=list, + description="Ordered list of steps in this workflow", + ) + step_summaries: list[str] = Field( + default_factory=list, + description="Simple list of step descriptions for quick reference", + ) + + # Context + application: str = Field(description="Primary application used in this workflow") + prerequisites: list[str] = Field( + default_factory=list, + description="Conditions that must be true before starting", + ) + outcomes: list[str] = Field( + default_factory=list, + description="Expected state changes after completion", + ) + + # Hierarchy + parent_episode_id: Optional[UUID] = Field( + default=None, + description="Parent episode if this is a subtask", + ) + child_episode_ids: list[UUID] = Field( + default_factory=list, + description="Child episodes if this contains subtasks", + ) + + # Quality metrics + boundary_confidence: float = Field( + ge=0.0, + le=1.0, + description="Confidence in episode boundaries", + ) + coherence_score: float = Field( + ge=0.0, + le=1.0, + description="How coherent/self-contained this episode is", + ) + + # Source + recording_id: str = Field(description="Source recording identifier") + frame_indices: list[int] = Field( + default_factory=list, + description="Indices of frames in this episode", + ) + + @property + def duration(self) -> float: + """Episode duration in seconds.""" + return self.end_time - self.start_time + + @property + def step_count(self) -> int: + """Number of steps in this episode.""" + return len(self.steps) + + +class EpisodeExtractionResult(BaseModel): + """Complete extraction result for a single recording. + + This is the full output of Stage 2. + """ + + recording_id: str = Field(description="Source recording identifier") + recording_name: str = Field(description="Human-readable recording name") + + # Extracted episodes + episodes: list[Episode] = Field( + default_factory=list, + description="Extracted workflow episodes", + ) + + # Boundaries + boundaries: list[EpisodeBoundary] = Field( + default_factory=list, + description="All identified episode boundaries", + ) + + # Processing metadata + llm_model: str = Field(description="LLM model used for extraction") + processing_timestamp: datetime = Field(default_factory=datetime.now) + + # Quality metrics + coverage: float = Field( + ge=0.0, + le=1.0, + description="Fraction of recording covered by episodes", + ) + avg_confidence: float = Field( + ge=0.0, + le=1.0, + description="Average boundary confidence", + ) + + +class CanonicalEpisode(BaseModel): + """A deduplicated, canonical episode definition. + + This represents a workflow type that may appear across multiple recordings. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Identification + canonical_id: UUID = Field( + default_factory=uuid4, + description="Unique identifier for this canonical episode", + ) + canonical_name: str = Field(description="Standardized name for this workflow") + + # Variants + variant_names: list[str] = Field( + default_factory=list, + description="Alternative names from merged episodes", + ) + variant_descriptions: list[str] = Field( + default_factory=list, + description="Alternative descriptions from merged episodes", + ) + + # Source tracking + source_recordings: list[str] = Field( + default_factory=list, + description="Recording IDs containing this workflow", + ) + source_episode_ids: list[UUID] = Field( + default_factory=list, + description="Original episode IDs that were merged", + ) + occurrence_count: int = Field( + ge=1, + description="Number of times this workflow appears", + ) + + # Canonical definition + canonical_description: str = Field( + description="Best/merged description of this workflow" + ) + canonical_steps: list[str] = Field( + default_factory=list, + description="Standardized step list", + ) + + # Embedding + embedding: Optional[list[float]] = Field( + default=None, + description="Vector embedding for similarity search", + ) + + # Clustering metadata + cluster_id: int = Field(default=0, description="Cluster ID from deduplication") + cluster_centroid_distance: float = Field( + default=0.0, + ge=0.0, + description="Distance from cluster centroid", + ) + + # Quality + internal_similarity: float = Field( + default=1.0, + ge=0.0, + le=1.0, + description="Average similarity between merged variants", + ) + + +class EpisodeAnnotation(BaseModel): + """Annotation for an episode indicating its quality for training. + + This model is used to mark episodes as "gold" (suitable for training) + or exclude them with reasons. VLM-based auto-annotation can populate + initial values, which humans can then verify. + + Attributes: + annotation_id: Unique identifier for this annotation + episode_id: ID of the Episode being annotated + start_frame: Exact start frame index (refined from Episode) + end_frame: Exact end frame index (refined from Episode) + is_gold: Whether this episode should be included in training export + exclusion_reason: Why this episode was excluded (if not gold) + confidence: VLM confidence in the annotation (0-1) + human_verified: Whether a human has confirmed this annotation + notes: Optional human notes about the episode + failure_signals: Detected failure signals from post-episode analysis + created_at: When this annotation was created + verified_at: When a human verified this annotation + verified_by: Who verified this annotation + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Identification + annotation_id: UUID = Field( + default_factory=uuid4, + description="Unique identifier for this annotation", + ) + episode_id: UUID = Field( + description="ID of the Episode being annotated", + ) + + # Refined boundaries + start_frame: int = Field( + ge=0, + description="Exact start frame index", + ) + end_frame: int = Field( + ge=0, + description="Exact end frame index", + ) + + # Quality assessment + is_gold: bool = Field( + default=False, + description="Should this episode be included in training export?", + ) + exclusion_reason: Optional[str] = Field( + default=None, + description="Why this episode was excluded (e.g., 'task failed', 'incomplete', 'error visible')", + ) + confidence: float = Field( + ge=0.0, + le=1.0, + default=0.5, + description="VLM confidence in the annotation", + ) + + # Human verification + human_verified: bool = Field( + default=False, + description="Has a human confirmed this annotation?", + ) + notes: Optional[str] = Field( + default=None, + description="Optional human notes about the episode", + ) + + # Failure detection + failure_signals: list[str] = Field( + default_factory=list, + description="Detected failure signals from post-episode analysis", + ) + + # Metadata + created_at: datetime = Field( + default_factory=datetime.now, + description="When this annotation was created", + ) + verified_at: Optional[datetime] = Field( + default=None, + description="When a human verified this annotation", + ) + verified_by: Optional[str] = Field( + default=None, + description="Who verified this annotation", + ) + + +class AnnotatedEpisodeLibrary(BaseModel): + """Collection of episodes with their annotations. + + This is used for reviewing, exporting, and managing annotated episodes. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Identification + library_id: UUID = Field( + default_factory=uuid4, + description="Unique identifier for this library", + ) + created_at: datetime = Field(default_factory=datetime.now) + + # Content + episodes: list[Episode] = Field( + default_factory=list, + description="All episodes in this library", + ) + annotations: list[EpisodeAnnotation] = Field( + default_factory=list, + description="Annotations for episodes", + ) + + # Source tracking + source_recordings: list[str] = Field( + default_factory=list, + description="Recording IDs that were processed", + ) + + # Statistics + @property + def total_episodes(self) -> int: + """Total number of episodes.""" + return len(self.episodes) + + @property + def annotated_count(self) -> int: + """Number of episodes with annotations.""" + annotated_ids = {a.episode_id for a in self.annotations} + return len(annotated_ids) + + @property + def gold_count(self) -> int: + """Number of gold episodes.""" + return sum(1 for a in self.annotations if a.is_gold) + + @property + def verified_count(self) -> int: + """Number of human-verified annotations.""" + return sum(1 for a in self.annotations if a.human_verified) + + @property + def export_ready_count(self) -> int: + """Number of episodes ready for export (gold AND verified).""" + return sum(1 for a in self.annotations if a.is_gold and a.human_verified) + + def get_annotation(self, episode_id: UUID) -> Optional[EpisodeAnnotation]: + """Get annotation for a specific episode.""" + for annotation in self.annotations: + if annotation.episode_id == episode_id: + return annotation + return None + + def get_episode(self, episode_id: UUID) -> Optional[Episode]: + """Get episode by ID.""" + for episode in self.episodes: + if episode.episode_id == episode_id: + return episode + return None + + def get_gold_episodes(self) -> list[tuple[Episode, EpisodeAnnotation]]: + """Get all gold episodes with their annotations.""" + result = [] + for annotation in self.annotations: + if annotation.is_gold: + episode = self.get_episode(annotation.episode_id) + if episode: + result.append((episode, annotation)) + return result + + def get_verified_gold_episodes(self) -> list[tuple[Episode, EpisodeAnnotation]]: + """Get episodes that are both gold AND human-verified.""" + result = [] + for annotation in self.annotations: + if annotation.is_gold and annotation.human_verified: + episode = self.get_episode(annotation.episode_id) + if episode: + result.append((episode, annotation)) + return result + + def get_pending_review(self) -> list[tuple[Episode, EpisodeAnnotation]]: + """Get episodes that have annotations but need human verification.""" + result = [] + for annotation in self.annotations: + if not annotation.human_verified: + episode = self.get_episode(annotation.episode_id) + if episode: + result.append((episode, annotation)) + return result + + def to_dict(self) -> dict: + """Convert to dictionary for JSON serialization.""" + return self.model_dump(mode="json") + + @classmethod + def from_dict(cls, data: dict) -> "AnnotatedEpisodeLibrary": + """Create from dictionary.""" + return cls.model_validate(data) + + +class EpisodeLibrary(BaseModel): + """Complete deduplicated episode library. + + This is the final output of Stage 3 - a library of canonical + workflow episodes that can be used for training data curation, + demo conditioning, and workflow retrieval. + """ + + # Library metadata + library_id: UUID = Field( + default_factory=uuid4, + description="Unique identifier for this library version", + ) + created_at: datetime = Field(default_factory=datetime.now) + + # Workflows + episodes: list[CanonicalEpisode] = Field( + default_factory=list, + description="All canonical episodes", + ) + + # Statistics + total_recordings_processed: int = Field( + ge=0, + description="Number of recordings analyzed", + ) + total_episodes_extracted: int = Field( + ge=0, + description="Total episodes before deduplication", + ) + unique_episode_count: int = Field( + ge=0, + description="Number of unique episodes after deduplication", + ) + deduplication_ratio: float = Field( + ge=0.0, + le=1.0, + description="Fraction of episodes that were duplicates", + ) + + # Processing parameters + similarity_threshold: float = Field( + ge=0.0, + le=1.0, + description="Threshold used for clustering", + ) + embedding_model: str = Field(description="Model used for embeddings") + + def get_episode_by_name(self, name: str) -> Optional[CanonicalEpisode]: + """Find episode by canonical name.""" + for episode in self.episodes: + if episode.canonical_name.lower() == name.lower(): + return episode + if name.lower() in [v.lower() for v in episode.variant_names]: + return episode + return None + + def get_episodes_for_recording(self, recording_id: str) -> list[CanonicalEpisode]: + """Get all episodes that appear in a specific recording.""" + return [e for e in self.episodes if recording_id in e.source_recordings] + + def to_dict(self) -> dict: + """Convert to dictionary for JSON serialization.""" + return self.model_dump(mode="json") + + @classmethod + def from_dict(cls, data: dict) -> "EpisodeLibrary": + """Create from dictionary.""" + return cls.model_validate(data) From 64809afbd75df69f6c3c68eeb2adab23bdc54c36 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 09:31:23 -0500 Subject: [PATCH 05/16] feat: add openadapt-viewer dependency and adapter module (Phase 1) Phase 1 of viewer consolidation plan: Foundation Changes: - Add openadapt-viewer as local file dependency in pyproject.toml - Create openadapt_ml/training/viewer_components.py adapter module * screenshot_with_predictions() - Screenshot with human/AI overlays * training_metrics() - Training stats metrics grid * playback_controls() - Playback UI controls * correctness_badge() - Pass/fail badge component * generate_comparison_summary() - Model comparison summary - Add tests/test_viewer_screenshots.py with component validation tests - Add openadapt_ml/training/viewer_migration_example.py validation example Design: - Zero breaking changes to existing viewer.py code - Adapter pattern wraps openadapt-viewer with ML-specific context - Functions accept openadapt-ml data structures - Can be incrementally adopted in future phases Next steps (Phase 2): - Gradually migrate viewer.py to use these adapters - Replace inline HTML generation with component calls Co-Authored-By: Claude Sonnet 4.5 --- openadapt_ml/training/viewer_components.py | 164 +++ .../training/viewer_migration_example.py | 73 ++ tests/test_viewer_screenshots.py | 35 + uv.lock | 1079 ++++++++++++++++- 4 files changed, 1309 insertions(+), 42 deletions(-) create mode 100644 openadapt_ml/training/viewer_components.py create mode 100644 openadapt_ml/training/viewer_migration_example.py create mode 100644 tests/test_viewer_screenshots.py diff --git a/openadapt_ml/training/viewer_components.py b/openadapt_ml/training/viewer_components.py new file mode 100644 index 0000000..4540b40 --- /dev/null +++ b/openadapt_ml/training/viewer_components.py @@ -0,0 +1,164 @@ +"""Adapter module for openadapt-viewer components. + +This module provides wrapper functions that adapt openadapt-viewer components +for openadapt-ml specific use cases, particularly for training visualization. + +Migration Approach: +------------------ +Phase 1 (Foundation): Create this adapter module to establish patterns +Phase 2 (Integration): Gradually migrate viewer.py to use these adapters +Phase 3 (Consolidation): Remove duplicate code from viewer.py +Phase 4 (Completion): Full dependency on openadapt-viewer + +Design Principles: +----------------- +1. Each function wraps openadapt-viewer components with ML-specific context +2. Functions accept openadapt-ml data structures (TrainingState, predictions, etc.) +3. No breaking changes to existing viewer.py code +4. Can be incrementally adopted in future phases +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +# Import openadapt-viewer components +from openadapt_viewer.components import ( + screenshot_display as _screenshot_display, + playback_controls as _playback_controls, + metrics_grid as _metrics_grid, + badge as _badge, +) + + +def screenshot_with_predictions( + screenshot_path: str | Path, + human_action: dict[str, Any] | None = None, + predicted_action: dict[str, Any] | None = None, + step_number: int | None = None, + show_difference: bool = True, +) -> str: + """Generate screenshot display with human and AI action overlays.""" + overlays = [] + + if human_action: + overlays.append({ + "type": human_action.get("type", "click"), + "x": human_action.get("x", 0), + "y": human_action.get("y", 0), + "label": "H", + "variant": "human", + "color": "#34d399", + }) + + if predicted_action: + overlays.append({ + "type": predicted_action.get("type", "click"), + "x": predicted_action.get("x", 0), + "y": predicted_action.get("y", 0), + "label": "AI", + "variant": "predicted", + "color": "#00d4aa", + }) + + caption = f"Step {step_number}" if step_number is not None else None + + return _screenshot_display( + image_path=str(screenshot_path), + overlays=overlays, + caption=caption, + ) + + +def training_metrics( + epoch: int | None = None, + loss: float | None = None, + accuracy: float | None = None, + elapsed_time: float | None = None, + learning_rate: float | None = None, + **additional_metrics: Any, +) -> str: + """Generate metrics grid for training statistics.""" + metrics = [] + + if epoch is not None: + metrics.append({"label": "Epoch", "value": epoch}) + + if loss is not None: + color = "success" if loss < 0.1 else "warning" if loss < 0.5 else "error" + metrics.append({"label": "Loss", "value": f"{loss:.4f}", "color": color}) + + if accuracy is not None: + color = "success" if accuracy > 0.9 else "warning" if accuracy > 0.7 else "error" + metrics.append({"label": "Accuracy", "value": f"{accuracy:.2%}", "color": color}) + + if elapsed_time is not None: + hours = int(elapsed_time // 3600) + minutes = int((elapsed_time % 3600) // 60) + seconds = int(elapsed_time % 60) + time_str = f"{hours}h {minutes}m {seconds}s" + metrics.append({"label": "Elapsed", "value": time_str}) + + if learning_rate is not None: + metrics.append({"label": "LR", "value": f"{learning_rate:.2e}"}) + + for key, value in additional_metrics.items(): + label = key.replace("_", " ").title() + metrics.append({"label": label, "value": str(value)}) + + return _metrics_grid(metrics) + + +def playback_controls( + step_count: int, + initial_step: int = 0, +) -> str: + """Generate playback controls for step-by-step viewer.""" + return _playback_controls( + step_count=step_count, + initial_step=initial_step, + ) + + +def correctness_badge(is_correct: bool, show_label: bool = True) -> str: + """Generate a badge indicating prediction correctness.""" + if is_correct: + text = "Correct" if show_label else "✓" + color = "success" + else: + text = "Incorrect" if show_label else "✗" + color = "error" + + return _badge(text=text, color=color) + + +def generate_comparison_summary( + total_steps: int, + correct_steps: int, + model_name: str | None = None, +) -> str: + """Generate a summary card for model comparison results.""" + accuracy = correct_steps / total_steps if total_steps > 0 else 0 + incorrect_steps = total_steps - correct_steps + + metrics = [ + {"label": "Total Steps", "value": total_steps}, + {"label": "Correct", "value": correct_steps, "color": "success"}, + {"label": "Incorrect", "value": incorrect_steps, "color": "error" if incorrect_steps > 0 else "muted"}, + {"label": "Accuracy", "value": f"{accuracy:.1%}", "color": "success" if accuracy > 0.9 else "warning"}, + ] + + if model_name: + metrics.insert(0, {"label": "Model", "value": model_name}) + + return _metrics_grid(metrics) + + +__all__ = [ + "screenshot_with_predictions", + "training_metrics", + "playback_controls", + "correctness_badge", + "generate_comparison_summary", +] diff --git a/openadapt_ml/training/viewer_migration_example.py b/openadapt_ml/training/viewer_migration_example.py new file mode 100644 index 0000000..522d205 --- /dev/null +++ b/openadapt_ml/training/viewer_migration_example.py @@ -0,0 +1,73 @@ +"""Validation example demonstrating viewer component migration. + +This script generates a simple viewer using the new openadapt-viewer components +through the viewer_components adapter module. + +Usage: + uv run python -m openadapt_ml.training.viewer_migration_example +""" + +from pathlib import Path + +from openadapt_viewer.builders import PageBuilder +from openadapt_ml.training.viewer_components import ( + screenshot_with_predictions, + training_metrics, + playback_controls, + generate_comparison_summary, + correctness_badge, +) + + +def generate_example_viewer(output_path: Path) -> None: + """Generate example viewer demonstrating new components.""" + builder = PageBuilder(title="Viewer Migration Example", include_alpine=True) + + builder.add_header( + title="Viewer Component Migration Example", + subtitle="Demonstrating Phase 1 Foundation", + nav_tabs=[ + {"href": "#", "label": "Training", "active": False}, + {"href": "#", "label": "Viewer", "active": True}, + ], + ) + + # Section 1: Training Metrics + builder.add_section( + content=training_metrics( + epoch=3, + loss=0.045, + accuracy=0.95, + elapsed_time=3600, + learning_rate=1e-4, + ), + title="Training Metrics", + ) + + # Section 2: Comparison Summary + builder.add_section( + content=generate_comparison_summary( + total_steps=20, + correct_steps=18, + model_name="qwen3-vl-2b", + ), + title="Model Comparison Summary", + ) + + # Section 3: Playback Controls + builder.add_section( + content=playback_controls(step_count=20), + title="Playback Controls", + ) + + # Render to file + html = builder.render() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(html) + print(f"✓ Generated example viewer: {output_path}") + + +if __name__ == "__main__": + output = Path("viewer_migration_example.html") + generate_example_viewer(output) + print(f"\nView the example: file://{output.resolve()}") diff --git a/tests/test_viewer_screenshots.py b/tests/test_viewer_screenshots.py new file mode 100644 index 0000000..af49ee6 --- /dev/null +++ b/tests/test_viewer_screenshots.py @@ -0,0 +1,35 @@ +"""Screenshot regression tests for viewer components. + +Running tests: + uv run pytest tests/test_viewer_screenshots.py -v +""" + +from openadapt_ml.training.viewer_components import ( + screenshot_with_predictions, + training_metrics, + playback_controls, + generate_comparison_summary, + correctness_badge, +) + + +def test_component_generation(): + """Test that components generate valid HTML.""" + html = screenshot_with_predictions( + screenshot_path="test.png", + human_action={"type": "click", "x": 0.5, "y": 0.5}, + predicted_action={"type": "click", "x": 0.5, "y": 0.5}, + ) + assert "oa-screenshot" in html + + html = training_metrics(epoch=1, loss=0.1) + assert "oa-metrics" in html + + html = playback_controls(step_count=10) + assert "oa-playback" in html + + html = generate_comparison_summary(total_steps=10, correct_steps=8) + assert "oa-metrics" in html + + html = correctness_badge(is_correct=True) + assert "oa-badge" in html diff --git a/uv.lock b/uv.lock index 33ba86d..68193f3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,10 @@ version = 1 -requires-python = ">=3.12" +requires-python = ">=3.10" resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version == '3.13.*'", - "python_full_version >= '3.14'", + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.13'", ] [[package]] @@ -12,7 +13,8 @@ version = "1.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -40,6 +42,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -48,6 +51,40 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 } wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950 }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099 }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072 }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588 }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334 }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656 }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625 }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604 }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370 }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023 }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680 }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407 }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047 }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264 }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275 }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053 }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687 }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051 }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234 }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979 }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297 }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172 }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405 }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449 }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444 }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038 }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156 }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340 }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041 }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024 }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590 }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355 }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701 }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678 }, { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 }, { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 }, { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 }, @@ -164,6 +201,7 @@ name = "anyio" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -176,11 +214,23 @@ wheels = [ name = "asgiref" version = "3.11.0" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096 }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -196,6 +246,20 @@ version = "16.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/c3/fd72a0315bc6c943ced1105aaac6e0ec1be57c70d8a616bd05acaa21ffee/av-16.0.1.tar.gz", hash = "sha256:dd2ce779fa0b5f5889a6d9e00fbbbc39f58e247e52d31044272648fe16ff1dbf", size = 3904030 } wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3c/eefa29b7d0f5afdf7af9197bbecad8ec2ad06bcb5ac7e909c05a624b00a6/av-16.0.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:8b141aaa29a3afc96a1d467d106790782c1914628b57309eaadb8c10c299c9c0", size = 27206679 }, + { url = "https://files.pythonhosted.org/packages/ac/89/a474feb07d5b94aa5af3771b0fe328056e2e0a840039b329f4fa2a1fd13a/av-16.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4b8a08a59a5be0082af063d3f4b216e3950340121c6ea95b505a3f5f5cc8f21d", size = 21774556 }, + { url = "https://files.pythonhosted.org/packages/be/e5/4361010dcac398bc224823e4b2a47803845e159af9f95164662c523770dc/av-16.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:792e7fc3c08eae005ff36486983966476e553cbb55aaeb0ec99adc4909377320", size = 38176763 }, + { url = "https://files.pythonhosted.org/packages/d4/db/b27bdd20c9dc80de5b8792dae16dd6f4edf16408c0c7b28070c6228a8057/av-16.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:4e8ef5df76d8d0ee56139789f80bb90ad1a82a7e6df6e080e2e95c06fa22aea7", size = 39696277 }, + { url = "https://files.pythonhosted.org/packages/4e/c8/dd48e6a3ac1e922c141475a0dc30e2b6dfdef9751b3274829889a9281cce/av-16.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4f7a6985784a7464f078e419c71f5528c3e550ee5d605e7149b4a37a111eb136", size = 39576660 }, + { url = "https://files.pythonhosted.org/packages/b9/f0/223d047e2e60672a2fb5e51e28913de8d52195199f3e949cbfda1e6cd64b/av-16.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3f45c8d7b803b6faa2a25a26de5964a0a897de68298d9c9672c7af9d65d8b48a", size = 40752775 }, + { url = "https://files.pythonhosted.org/packages/18/73/73acad21c9203bc63d806e8baf42fe705eb5d36dafd1996b71ab5861a933/av-16.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:58e6faf1d9328d8cc6be14c5aadacb7d2965ed6d6ae1af32696993096543ff00", size = 32302328 }, + { url = "https://files.pythonhosted.org/packages/49/d3/f2a483c5273fccd556dfa1fce14fab3b5d6d213b46e28e54e254465a2255/av-16.0.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:e310d1fb42879df9bad2152a8db6d2ff8bf332c8c36349a09d62cc122f5070fb", size = 27191982 }, + { url = "https://files.pythonhosted.org/packages/e0/39/dff28bd252131b3befd09d8587992fe18c09d5125eaefc83a6434d5f56ff/av-16.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:2f4b357e5615457a84e6b6290916b22864b76b43d5079e1a73bc27581a5b9bac", size = 21760305 }, + { url = "https://files.pythonhosted.org/packages/4a/4d/2312d50a09c84a9b4269f7fea5de84f05dd2b7c7113dd961d31fad6c64c4/av-16.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:286665c77034c3a98080169b8b5586d5568a15da81fbcdaf8099252f2d232d7c", size = 38691616 }, + { url = "https://files.pythonhosted.org/packages/15/9a/3d2d30b56252f998e53fced13720e2ce809c4db477110f944034e0fa4c9f/av-16.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f88de8e5b8ea29e41af4d8d61df108323d050ccfbc90f15b13ec1f99ce0e841e", size = 40216464 }, + { url = "https://files.pythonhosted.org/packages/98/cb/3860054794a47715b4be0006105158c7119a57be58d9e8882b72e4d4e1dd/av-16.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0cdb71ebe4d1b241cf700f8f0c44a7d2a6602b921e16547dd68c0842113736e1", size = 40094077 }, + { url = "https://files.pythonhosted.org/packages/41/58/79830fb8af0a89c015250f7864bbd427dff09c70575c97847055f8a302f7/av-16.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:28c27a65d40e8cf82b6db2543f8feeb8b56d36c1938f50773494cd3b073c7223", size = 41279948 }, + { url = "https://files.pythonhosted.org/packages/83/79/6e1463b04382f379f857113b851cf5f9d580a2f7bd794211cd75352f4e04/av-16.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ffea39ac7574f234f5168f9b9602e8d4ecdd81853238ec4d661001f03a6d3f64", size = 32297586 }, { url = "https://files.pythonhosted.org/packages/44/78/12a11d7a44fdd8b26a65e2efa1d8a5826733c8887a989a78306ec4785956/av-16.0.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:e41a8fef85dfb2c717349f9ff74f92f9560122a9f1a94b1c6c9a8a9c9462ba71", size = 27206375 }, { url = "https://files.pythonhosted.org/packages/27/19/3a4d3882852a0ee136121979ce46f6d2867b974eb217a2c9a070939f55ad/av-16.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6352a64b25c9f985d4f279c2902db9a92424e6f2c972161e67119616f0796cb9", size = 21752603 }, { url = "https://files.pythonhosted.org/packages/cb/6e/f7abefba6e008e2f69bebb9a17ba38ce1df240c79b36a5b5fcacf8c8fcfd/av-16.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5201f7b4b5ed2128118cb90c2a6d64feedb0586ca7c783176896c78ffb4bbd5c", size = 38931978 }, @@ -414,7 +478,8 @@ name = "bitsandbytes" version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "torch" }, ] @@ -452,6 +517,31 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079 }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475 }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, @@ -506,6 +596,38 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709 }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814 }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467 }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280 }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454 }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609 }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586 }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290 }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663 }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964 }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064 }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792 }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198 }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262 }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988 }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324 }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742 }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863 }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837 }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550 }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162 }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019 }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310 }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022 }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098 }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991 }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456 }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978 }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969 }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, @@ -578,15 +700,102 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +] + [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.13'", +] dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 }, { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 }, @@ -642,6 +851,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, ] [[package]] @@ -650,6 +864,7 @@ version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } wheels = [ @@ -698,6 +913,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163 }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474 }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132 }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992 }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944 }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957 }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447 }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528 }, ] [[package]] @@ -720,7 +943,8 @@ dependencies = [ { name = "httpx" }, { name = "huggingface-hub" }, { name = "multiprocess" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pandas" }, { name = "pyarrow" }, @@ -767,6 +991,18 @@ version = "1.9.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/63/fe/a17c106a1f4061ce83f04d14bcedcfb2c38c7793ea56bfb906a6fadae8cb/evdev-1.9.2.tar.gz", hash = "sha256:5d3278892ce1f92a74d6bf888cc8525d9f68af85dbe336c95d1c87fb8f423069", size = 33301 } +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + [[package]] name = "filelock" version = "3.20.0" @@ -794,6 +1030,22 @@ version = "4.61.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884 } wheels = [ + { url = "https://files.pythonhosted.org/packages/29/f3/91bba2721fb173fc68e09d15b6ccf3ad4f83d127fbff579be7e5984888a6/fonttools-4.61.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", size = 2850151 }, + { url = "https://files.pythonhosted.org/packages/f5/8c/a1691dec01038ac7e7bb3ab83300dcc5087b11d8f48640928c02a873eb92/fonttools-4.61.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", size = 2389769 }, + { url = "https://files.pythonhosted.org/packages/2d/dd/5bb369a44319d92ba25612511eb8ed2a6fa75239979e0388907525626902/fonttools-4.61.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", size = 4893189 }, + { url = "https://files.pythonhosted.org/packages/5e/02/51373fa8846bd22bb54e5efb30a824b417b058083f775a194a432f21a45f/fonttools-4.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", size = 4854415 }, + { url = "https://files.pythonhosted.org/packages/8b/64/9cdbbb804577a7e6191448851c57e6a36eb02aa4bf6a9668b528c968e44e/fonttools-4.61.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", size = 4870927 }, + { url = "https://files.pythonhosted.org/packages/92/68/e40b22919dc96dc30a70b58fec609ab85112de950bdecfadf8dd478c5a88/fonttools-4.61.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", size = 4988674 }, + { url = "https://files.pythonhosted.org/packages/9b/5c/e857349ce8aedb2451b9448282e86544b2b7f1c8b10ea0fe49b7cb369b72/fonttools-4.61.0-cp310-cp310-win32.whl", hash = "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", size = 1497663 }, + { url = "https://files.pythonhosted.org/packages/f9/0c/62961d5fe6f764d6cbc387ef2c001f5f610808c7aded837409836c0b3e7c/fonttools-4.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", size = 1546143 }, + { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553 }, + { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298 }, + { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133 }, + { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410 }, + { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005 }, + { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026 }, + { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035 }, + { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290 }, { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930 }, { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016 }, { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425 }, @@ -835,6 +1087,38 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230 }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621 }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889 }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464 }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649 }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188 }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748 }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351 }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767 }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887 }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785 }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312 }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650 }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659 }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837 }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989 }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912 }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046 }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119 }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067 }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160 }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544 }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797 }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886 }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731 }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544 }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806 }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382 }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647 }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064 }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937 }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 }, @@ -937,8 +1221,8 @@ name = "google-ai-generativelanguage" version = "0.6.15" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version >= '3.14'" }, - { name = "google-api-core", version = "2.28.1", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version < '3.14'" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version != '3.12.*'" }, + { name = "google-api-core", version = "2.28.1", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version == '3.12.*'" }, { name = "google-auth" }, { name = "proto-plus" }, { name = "protobuf" }, @@ -953,14 +1237,16 @@ name = "google-api-core" version = "2.25.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", + "python_full_version >= '3.13'", ] dependencies = [ - { name = "google-auth", marker = "python_full_version >= '3.14'" }, - { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, - { name = "proto-plus", marker = "python_full_version >= '3.14'" }, - { name = "protobuf", marker = "python_full_version >= '3.14'" }, - { name = "requests", marker = "python_full_version >= '3.14'" }, + { name = "google-auth", marker = "python_full_version != '3.12.*'" }, + { name = "googleapis-common-protos", marker = "python_full_version != '3.12.*'" }, + { name = "proto-plus", marker = "python_full_version != '3.12.*'" }, + { name = "protobuf", marker = "python_full_version != '3.12.*'" }, + { name = "requests", marker = "python_full_version != '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266 } wheels = [ @@ -969,8 +1255,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio", marker = "python_full_version >= '3.14'" }, - { name = "grpcio-status", marker = "python_full_version >= '3.14'" }, + { name = "grpcio", marker = "python_full_version != '3.12.*'" }, + { name = "grpcio-status", marker = "python_full_version != '3.12.*'" }, ] [[package]] @@ -978,15 +1264,14 @@ name = "google-api-core" version = "2.28.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", ] dependencies = [ - { name = "google-auth", marker = "python_full_version < '3.14'" }, - { name = "googleapis-common-protos", marker = "python_full_version < '3.14'" }, - { name = "proto-plus", marker = "python_full_version < '3.14'" }, - { name = "protobuf", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "google-auth", marker = "python_full_version == '3.12.*'" }, + { name = "googleapis-common-protos", marker = "python_full_version == '3.12.*'" }, + { name = "proto-plus", marker = "python_full_version == '3.12.*'" }, + { name = "protobuf", marker = "python_full_version == '3.12.*'" }, + { name = "requests", marker = "python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759 } wheels = [ @@ -995,8 +1280,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio", marker = "python_full_version < '3.14'" }, - { name = "grpcio-status", marker = "python_full_version < '3.14'" }, + { name = "grpcio", marker = "python_full_version == '3.12.*'" }, + { name = "grpcio-status", marker = "python_full_version == '3.12.*'" }, ] [[package]] @@ -1004,8 +1289,8 @@ name = "google-api-python-client" version = "2.187.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "google-api-core", version = "2.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.12.*'" }, + { name = "google-api-core", version = "2.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, { name = "google-auth" }, { name = "google-auth-httplib2" }, { name = "httplib2" }, @@ -1049,8 +1334,8 @@ version = "0.8.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-ai-generativelanguage" }, - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "google-api-core", version = "2.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.12.*'" }, + { name = "google-api-core", version = "2.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, { name = "google-api-python-client" }, { name = "google-auth" }, { name = "protobuf" }, @@ -1083,6 +1368,26 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037 }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482 }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178 }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684 }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133 }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507 }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651 }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568 }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879 }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892 }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567 }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017 }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027 }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913 }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417 }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683 }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109 }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676 }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688 }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315 }, { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718 }, { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627 }, { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167 }, @@ -1283,6 +1588,31 @@ version = "0.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652 }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829 }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052 }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585 }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541 }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423 }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958 }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084 }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054 }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368 }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847 }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435 }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548 }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915 }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966 }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047 }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835 }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587 }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492 }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046 }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392 }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096 }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899 }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070 }, { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 }, { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 }, { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 }, @@ -1382,6 +1712,32 @@ version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564 } wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159 }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578 }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312 }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458 }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640 }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074 }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036 }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310 }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943 }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488 }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787 }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730 }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036 }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167 }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579 }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309 }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596 }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548 }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618 }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437 }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742 }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810 }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579 }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071 }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840 }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159 }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686 }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460 }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952 }, @@ -1446,6 +1802,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835 }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988 }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260 }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183 }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675 }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277 }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994 }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744 }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104 }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592 }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281 }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009 }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929 }, ] [[package]] @@ -1454,6 +1820,14 @@ version = "0.46.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456 } wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766 }, + { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176 }, + { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629 }, + { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651 }, + { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766 }, + { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175 }, + { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630 }, + { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652 }, { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767 }, { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176 }, { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630 }, @@ -1474,6 +1848,28 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057 }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050 }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681 }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705 }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524 }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282 }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745 }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571 }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056 }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932 }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058 }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287 }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940 }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887 }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692 }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471 }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923 }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572 }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077 }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876 }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, @@ -1548,11 +1944,13 @@ name = "matplotlib" version = "3.10.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "contourpy" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "cycler" }, { name = "fonttools" }, { name = "kiwisolver" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, @@ -1560,6 +1958,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865 } wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/87/3932d5778ab4c025db22710b61f49ccaed3956c5cf46ffb2ffa7492b06d9/matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380", size = 8247141 }, + { url = "https://files.pythonhosted.org/packages/45/a8/bfed45339160102bce21a44e38a358a1134a5f84c26166de03fb4a53208f/matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d", size = 8107995 }, + { url = "https://files.pythonhosted.org/packages/e2/3c/5692a2d9a5ba848fda3f48d2b607037df96460b941a59ef236404b39776b/matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297", size = 8680503 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/86ace53c48b05d0e6e9c127b2ace097434901f3e7b93f050791c8243201a/matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42", size = 9514982 }, + { url = "https://files.pythonhosted.org/packages/a6/81/ead71e2824da8f72640a64166d10e62300df4ae4db01a0bac56c5b39fa51/matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7", size = 9566429 }, + { url = "https://files.pythonhosted.org/packages/65/7d/954b3067120456f472cce8fdcacaf4a5fcd522478db0c37bb243c7cb59dd/matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3", size = 8108174 }, + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507 }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565 }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668 }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051 }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878 }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142 }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439 }, { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389 }, { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247 }, { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996 }, @@ -1595,6 +2006,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066 }, { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832 }, { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585 }, + { url = "https://files.pythonhosted.org/packages/1e/6c/a9bcf03e9afb2a873e0a5855f79bce476d1023f26f8212969f2b7504756c/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537", size = 8241204 }, + { url = "https://files.pythonhosted.org/packages/5b/fd/0e6f5aa762ed689d9fa8750b08f1932628ffa7ed30e76423c399d19407d2/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657", size = 8104607 }, + { url = "https://files.pythonhosted.org/packages/b9/a9/21c9439d698fac5f0de8fc68b2405b738ed1f00e1279c76f2d9aa5521ead/matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b", size = 8682257 }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283 }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733 }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919 }, ] [[package]] @@ -1681,8 +2098,47 @@ wheels = [ name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 } wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153 }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993 }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607 }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847 }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616 }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333 }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239 }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618 }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655 }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245 }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523 }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129 }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999 }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711 }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504 }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422 }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050 }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153 }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604 }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715 }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332 }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212 }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671 }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491 }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322 }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694 }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715 }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189 }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845 }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374 }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345 }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940 }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229 }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308 }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037 }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023 }, { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877 }, { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467 }, { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834 }, @@ -1785,6 +2241,12 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503 } wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f8/7f9a8f08bf98cea1dfaa181e05cc8bbcb59cecf044b5a9ac3cce39f9c449/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df", size = 135083 }, + { url = "https://files.pythonhosted.org/packages/e5/03/b7b10dbfc17b2b3ce07d4d30b3ba8367d0ed32d6d46cd166e298f161dd46/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213", size = 135128 }, + { url = "https://files.pythonhosted.org/packages/c1/a3/5f8d3b9690ea5580bee5868ab7d7e2cfca74b7e826b28192b40aa3881cdc/multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2", size = 135132 }, + { url = "https://files.pythonhosted.org/packages/55/4d/9af0d1279c84618bcd35bf5fd7e371657358c7b0a523e54a9cffb87461f8/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6", size = 144695 }, + { url = "https://files.pythonhosted.org/packages/17/bf/87323e79dd0562474fad3373c21c66bc6c3c9963b68eb2a209deb4c8575e/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3", size = 144742 }, + { url = "https://files.pythonhosted.org/packages/dd/74/cb8c831e58dc6d5cf450b17c7db87f14294a1df52eb391da948b5e0a0b94/multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797", size = 144745 }, { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948 }, { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462 }, { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287 }, @@ -1793,10 +2255,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478 }, ] +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + [[package]] name = "networkx" version = "3.6" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.13'", +] sdist = { url = "https://files.pythonhosted.org/packages/e8/fc/7b6fd4d22c8c4dc5704430140d8b3f520531d4fe7328b8f8d03f5a7950e8/networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad", size = 2511464 } wheels = [ { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713 }, @@ -1808,10 +2287,19 @@ version = "0.63.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666 } wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810 }, + { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735 }, + { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707 }, + { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374 }, + { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501 }, + { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945 }, + { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827 }, + { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262 }, { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981 }, { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656 }, { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857 }, @@ -1826,12 +2314,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161 }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version >= '3.13'", +] sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950 } wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641 }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324 }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872 }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148 }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282 }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903 }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672 }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896 }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608 }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555 }, { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873 }, { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838 }, { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378 }, @@ -1887,6 +2456,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806 }, { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760 }, { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459 }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689 }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053 }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635 }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768 }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263 }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213 }, ] [[package]] @@ -2174,7 +2750,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, { name = "numba" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "tiktoken" }, { name = "torch" }, { name = "tqdm" }, @@ -2427,13 +3004,28 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763 }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217 }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791 }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373 }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444 }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459 }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086 }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790 }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831 }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267 }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281 }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453 }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361 }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702 }, { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 }, { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 }, { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 }, @@ -2476,7 +3068,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, { name = "huggingface-hub" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -2496,6 +3089,28 @@ version = "12.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828 } wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/08/26e68b6b5da219c2a2cb7b563af008b53bb8e6b6fcb3fa40715fcdb2523a/pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", size = 5289809 }, + { url = "https://files.pythonhosted.org/packages/cb/e9/4e58fb097fb74c7b4758a680aacd558810a417d1edaa7000142976ef9d2f/pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", size = 4650606 }, + { url = "https://files.pythonhosted.org/packages/4b/e0/1fa492aa9f77b3bc6d471c468e62bfea1823056bf7e5e4f1914d7ab2565e/pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", size = 6221023 }, + { url = "https://files.pythonhosted.org/packages/c1/09/4de7cd03e33734ccd0c876f0251401f1314e819cbfd89a0fcb6e77927cc6/pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", size = 8024937 }, + { url = "https://files.pythonhosted.org/packages/2e/69/0688e7c1390666592876d9d474f5e135abb4acb39dcb583c4dc5490f1aff/pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", size = 6334139 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/880921e98f525b9b44ce747ad1ea8f73fd7e992bafe3ca5e5644bf433dea/pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", size = 7026074 }, + { url = "https://files.pythonhosted.org/packages/28/03/96f718331b19b355610ef4ebdbbde3557c726513030665071fd025745671/pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", size = 6448852 }, + { url = "https://files.pythonhosted.org/packages/3a/a0/6a193b3f0cc9437b122978d2c5cbce59510ccf9a5b48825096ed7472da2f/pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", size = 7117058 }, + { url = "https://files.pythonhosted.org/packages/a7/c4/043192375eaa4463254e8e61f0e2ec9a846b983929a8d0a7122e0a6d6fff/pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", size = 6295431 }, + { url = "https://files.pythonhosted.org/packages/92/c6/c2f2fc7e56301c21827e689bb8b0b465f1b52878b57471a070678c0c33cd/pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", size = 7000412 }, + { url = "https://files.pythonhosted.org/packages/b2/d2/5f675067ba82da7a1c238a73b32e3fd78d67f9d9f80fbadd33a40b9c0481/pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", size = 2435903 }, + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798 }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589 }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472 }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887 }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964 }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756 }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075 }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955 }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440 }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256 }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025 }, { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377 }, { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343 }, { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981 }, @@ -2557,6 +3172,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045 }, { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282 }, { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630 }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068 }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994 }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639 }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839 }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505 }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654 }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850 }, ] [[package]] @@ -2574,6 +3196,36 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534 }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526 }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263 }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012 }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491 }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319 }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856 }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241 }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552 }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778 }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047 }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093 }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638 }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229 }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208 }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777 }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647 }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929 }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778 }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144 }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030 }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252 }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064 }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429 }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727 }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097 }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084 }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637 }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064 }, { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, @@ -2710,6 +3362,20 @@ version = "22.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151 } wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968 }, + { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085 }, + { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613 }, + { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059 }, + { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043 }, + { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505 }, + { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641 }, + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022 }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834 }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348 }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480 }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148 }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964 }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517 }, { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578 }, { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906 }, { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677 }, @@ -2817,6 +3483,33 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, @@ -2881,6 +3574,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, ] [[package]] @@ -2972,6 +3681,8 @@ version = "12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532 } wheels = [ + { url = "https://files.pythonhosted.org/packages/63/bf/3dbb1783388da54e650f8a6b88bde03c101d9ba93dfe8ab1b1873f1cd999/pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7", size = 676748 }, + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263 }, { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335 }, { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370 }, { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586 }, @@ -2991,6 +3702,8 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247 } wheels = [ + { url = "https://files.pythonhosted.org/packages/52/9d/3cf36e7b08832e71f5d48ddfa1047865cf2dfc53df8c0f2a82843ea9507a/pyobjc_framework_applicationservices-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4fd1b008757182b9e2603a63c6ffa930cc412fab47294ec64260ab3f8ec695d", size = 32791 }, + { url = "https://files.pythonhosted.org/packages/17/86/d07eff705ff909a0ffa96d14fc14026e9fc9dd716233648c53dfd5056b8e/pyobjc_framework_applicationservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdddd492eeac6d14ff2f5bd342aba29e30dffa72a2d358c08444da22129890e2", size = 32784 }, { url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835 }, { url = "https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd1f4dbb38234a24ae6819f5e22485cf7dd3dd4074ff3bf9a9fdb4c01a3b4a38", size = 32859 }, { url = "https://files.pythonhosted.org/packages/66/3a/0f1d4dcf2345e875e5ea9761d5a70969e241d24089133d21f008dde596f5/pyobjc_framework_applicationservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8a5d2845249b6a85ba9e320a9848468c3f8cd6f59605a9a43f406a7810eaa830", size = 33115 }, @@ -3007,6 +3720,8 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191 } wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/aa/2b2d7ec3ac4b112a605e9bd5c5e5e4fd31d60a8a4b610ab19cc4838aa92a/pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48", size = 383825 }, + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812 }, { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590 }, { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689 }, { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843 }, @@ -3025,6 +3740,8 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124 } wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1c/ddecc72a672d681476c668bcedcfb8ade16383c028eac566ac7458fb91ef/pyobjc_framework_coretext-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1c8315dcef6699c2953461d97117fe81402f7c29cff36d2950dacce028a362fd", size = 29987 }, + { url = "https://files.pythonhosted.org/packages/f0/81/7b8efc41e743adfa2d74b92dec263c91bcebfb188d2a8f5eea1886a195ff/pyobjc_framework_coretext-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f6742ba5b0bb7629c345e99eff928fbfd9e9d3d667421ac1a2a43bdb7ba9833", size = 29990 }, { url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108 }, { url = "https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cbb2c28580e6704ce10b9a991ccd9563a22b3a75f67c36cf612544bd8b21b5f", size = 30110 }, { url = "https://files.pythonhosted.org/packages/0f/5d/85e059349e9cfbd57269a1f11f56747b3ff5799a3bcbd95485f363c623d8/pyobjc_framework_coretext-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:14100d1e39efb30f57869671fb6fce8d668f80c82e25e7930fb364866e5c0dab", size = 30697 }, @@ -3042,6 +3759,8 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099 } wheels = [ + { url = "https://files.pythonhosted.org/packages/17/f4/50c42c84796886e4d360407fb629000bb68d843b2502c88318375441676f/pyobjc_framework_quartz-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c6f312ae79ef8b3019dcf4b3374c52035c7c7bc4a09a1748b61b041bb685a0ed", size = 217799 }, + { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795 }, { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798 }, { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206 }, { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317 }, @@ -3077,6 +3796,9 @@ sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f name = "pyscreeze" version = "1.0.1" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow", marker = "python_full_version < '3.12'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/cb456ac4f1a73723d5b866933b7986f02bacea27516629c00f8e7da94c2d/pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be", size = 27826 } [[package]] @@ -3085,10 +3807,12 @@ version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } wheels = [ @@ -3155,6 +3879,24 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, @@ -3215,6 +3957,35 @@ version = "2025.11.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087 }, + { url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544 }, + { url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408 }, + { url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584 }, + { url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733 }, + { url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691 }, + { url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662 }, + { url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587 }, + { url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709 }, + { url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773 }, + { url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164 }, + { url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832 }, + { url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802 }, + { url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722 }, + { url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289 }, + { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081 }, + { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554 }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407 }, + { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418 }, + { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448 }, + { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139 }, + { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439 }, + { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965 }, + { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398 }, + { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897 }, + { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906 }, + { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812 }, + { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737 }, + { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290 }, { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312 }, { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256 }, { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921 }, @@ -3321,6 +4092,35 @@ version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490 }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751 }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696 }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136 }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699 }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022 }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522 }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579 }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305 }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503 }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322 }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792 }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901 }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823 }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157 }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676 }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938 }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932 }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830 }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033 }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828 }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683 }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583 }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496 }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669 }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011 }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406 }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024 }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069 }, { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, @@ -3394,6 +4194,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292 }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128 }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542 }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004 }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063 }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099 }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177 }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015 }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736 }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981 }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782 }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191 }, ] [[package]] @@ -3463,6 +4275,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368 }, { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423 }, { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380 }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430 }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890 }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885 }, ] [[package]] @@ -3513,7 +4329,8 @@ version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156 } wheels = [ @@ -3569,6 +4386,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991 }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798 }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865 }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856 }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308 }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697 }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375 }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565 }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284 }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201 }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444 }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080 }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240 }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422 }, { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728 }, { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049 }, { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008 }, @@ -3631,6 +4462,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 }, ] +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663 }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469 }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039 }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007 }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875 }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271 }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770 }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626 }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842 }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894 }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053 }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481 }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720 }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014 }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820 }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712 }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296 }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553 }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915 }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038 }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245 }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335 }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962 }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396 }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530 }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227 }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748 }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725 }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901 }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375 }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639 }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897 }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697 }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567 }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556 }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014 }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339 }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490 }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398 }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515 }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806 }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340 }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106 }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561 }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477 }, +] + [[package]] name = "torch" version = "2.9.1" @@ -3639,7 +4524,8 @@ dependencies = [ { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, - { name = "networkx" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, @@ -3655,12 +4541,20 @@ dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "setuptools" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, { name = "sympy" }, { name = "triton", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, { name = "typing-extensions" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/56/9577683b23072075ed2e40d725c52c2019d71a972fab8e083763da8e707e/torch-2.9.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e", size = 104207681 }, + { url = "https://files.pythonhosted.org/packages/38/45/be5a74f221df8f4b609b78ff79dc789b0cc9017624544ac4dd1c03973150/torch-2.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c", size = 899794036 }, + { url = "https://files.pythonhosted.org/packages/67/95/a581e8a382596b69385a44bab2733f1273d45c842f5d4a504c0edc3133b6/torch-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65", size = 110969861 }, + { url = "https://files.pythonhosted.org/packages/ad/51/1756dc128d2bf6ea4e0a915cb89ea5e730315ff33d60c1ff56fd626ba3eb/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951", size = 74452222 }, + { url = "https://files.pythonhosted.org/packages/15/db/c064112ac0089af3d2f7a2b5bfbabf4aa407a78b74f87889e524b91c5402/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430 }, + { url = "https://files.pythonhosted.org/packages/56/be/76eaa36c9cd032d3b01b001e2c5a05943df75f26211f68fae79e62f87734/torch-2.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b", size = 899821446 }, + { url = "https://files.pythonhosted.org/packages/47/cc/7a2949e38dfe3244c4df21f0e1c27bce8aedd6c604a587dd44fc21017cb4/torch-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb", size = 110973074 }, + { url = "https://files.pythonhosted.org/packages/1e/ce/7d251155a783fb2c1bb6837b2b7023c622a2070a0a72726ca1df47e7ea34/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475", size = 74463887 }, { url = "https://files.pythonhosted.org/packages/0f/27/07c645c7673e73e53ded71705045d6cb5bae94c4b021b03aa8d03eee90ab/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592 }, { url = "https://files.pythonhosted.org/packages/19/17/e377a460603132b00760511299fceba4102bd95db1a0ee788da21298ccff/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281 }, { url = "https://files.pythonhosted.org/packages/b1/1a/64f5769025db846a82567fa5b7d21dba4558a7234ee631712ee4771c436c/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568 }, @@ -3688,11 +4582,20 @@ name = "torchvision" version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow" }, { name = "torch" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/09/d51aadf8591138e08b74c64a6eb783630c7a31ca2634416277115a9c3a2b/torchvision-0.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ded5e625788572e4e1c4d155d1bbc48805c113794100d70e19c76e39e4d53465", size = 1891441 }, + { url = "https://files.pythonhosted.org/packages/6b/49/a35df863e7c153aad82af7505abd8264a5b510306689712ef86bea862822/torchvision-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:54ed17c3d30e718e08d8da3fd5b30ea44b0311317e55647cb97077a29ecbc25b", size = 2386226 }, + { url = "https://files.pythonhosted.org/packages/49/20/f2d7cd1eea052887c1083afff0b8df5228ec93b53e03759f20b1a3c6d22a/torchvision-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f476da4e085b7307aaab6f540219617d46d5926aeda24be33e1359771c83778f", size = 8046093 }, + { url = "https://files.pythonhosted.org/packages/d8/cf/0ff4007c09903199307da5f53a192ff5d62b45447069e9ef3a19bdc5ff12/torchvision-0.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:fbdbdae5e540b868a681240b7dbd6473986c862445ee8a138680a6a97d6c34ff", size = 3696202 }, + { url = "https://files.pythonhosted.org/packages/e7/69/30f5f03752aa1a7c23931d2519b31e557f3f10af5089d787cddf3b903ecf/torchvision-0.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600", size = 1891436 }, + { url = "https://files.pythonhosted.org/packages/0c/69/49aae86edb75fe16460b59a191fcc0f568c2378f780bb063850db0fe007a/torchvision-0.24.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1e39619de698e2821d71976c92c8a9e50cdfd1e993507dfb340f2688bfdd8283", size = 2387757 }, + { url = "https://files.pythonhosted.org/packages/11/c9/1dfc3db98797b326f1d0c3f3bb61c83b167a813fc7eab6fcd2edb8c7eb9d/torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a0f106663e60332aa4fcb1ca2159ef8c3f2ed266b0e6df88de261048a840e0df", size = 8047682 }, + { url = "https://files.pythonhosted.org/packages/fa/bb/cfc6a6f6ccc84a534ed1fdf029ae5716dd6ff04e57ed9dc2dab38bf652d5/torchvision-0.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:a9308cdd37d8a42e14a3e7fd9d271830c7fecb150dd929b642f3c1460514599a", size = 4037588 }, { url = "https://files.pythonhosted.org/packages/f0/af/18e2c6b9538a045f60718a0c5a058908ccb24f88fde8e6f0fc12d5ff7bd3/torchvision-0.24.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193", size = 1891433 }, { url = "https://files.pythonhosted.org/packages/9d/43/600e5cfb0643d10d633124f5982d7abc2170dfd7ce985584ff16edab3e76/torchvision-0.24.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050", size = 2386737 }, { url = "https://files.pythonhosted.org/packages/93/b1/db2941526ecddd84884132e2742a55c9311296a6a38627f9e2627f5ac889/torchvision-0.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7", size = 8049868 }, @@ -3734,7 +4637,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "huggingface-hub" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -3753,6 +4657,10 @@ name = "triton" version = "3.5.1" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/2e/f95e673222afa2c7f0c687d8913e98fcf2589ef0b1405de76894e37fe18f/triton-3.5.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f63e34dcb32d7bd3a1d0195f60f30d2aee8b08a69a0424189b71017e23dfc3d2", size = 159821655 }, + { url = "https://files.pythonhosted.org/packages/fd/6e/676ab5019b4dde8b9b7bab71245102fc02778ef3df48218b298686b9ffd6/triton-3.5.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94", size = 170320692 }, + { url = "https://files.pythonhosted.org/packages/dc/dc/6ce44d055f2fc2403c4ec6b3cfd3a9b25f57b7d95efadccdea91497f8e81/triton-3.5.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da47169e30a779bade679ce78df4810fca6d78a955843d2ddb11f226adc517dc", size = 159928005 }, + { url = "https://files.pythonhosted.org/packages/b0/72/ec90c3519eaf168f22cb1757ad412f3a2add4782ad3a92861c9ad135d886/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802 }, { url = "https://files.pythonhosted.org/packages/db/53/2bcc46879910991f09c063eea07627baef2bc62fe725302ba8f46a2c1ae5/triton-3.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275a045b6ed670dd1bd005c3e6c2d61846c74c66f4512d6f33cc027b11de8fd4", size = 159940689 }, { url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207 }, { url = "https://files.pythonhosted.org/packages/f1/ba/805684a992ee32d486b7948d36aed2f5e3c643fc63883bf8bdca1c3f3980/triton-3.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56765ffe12c554cd560698398b8a268db1f616c120007bfd8829d27139abd24a", size = 159955460 }, @@ -3833,6 +4741,26 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482 }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676 }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957 }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975 }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149 }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209 }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551 }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464 }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748 }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810 }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482 }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674 }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959 }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376 }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604 }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782 }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076 }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457 }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745 }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806 }, { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 }, { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 }, { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 }, @@ -3882,6 +4810,36 @@ version = "3.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160 } wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845 }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807 }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786 }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830 }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606 }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872 }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217 }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139 }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669 }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018 }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058 }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628 }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577 }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487 }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863 }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844 }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809 }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665 }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550 }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384 }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749 }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880 }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912 }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654 }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867 }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012 }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409 }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574 }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481 }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861 }, { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744 }, { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816 }, { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035 }, @@ -3957,6 +4915,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586 }, { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526 }, { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898 }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662 }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056 }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251 }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481 }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565 }, ] [[package]] @@ -3970,6 +4933,38 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 } wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517 }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495 }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545 }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598 }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893 }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240 }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965 }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026 }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637 }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082 }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811 }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223 }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118 }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852 }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012 }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607 }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027 }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963 }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406 }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581 }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924 }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890 }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819 }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601 }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072 }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311 }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094 }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944 }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804 }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858 }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637 }, { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000 }, { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338 }, { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909 }, From b6cd42b483c8cbb36d2d1de45f86c36faedad9c0 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 09:57:36 -0500 Subject: [PATCH 06/16] fix: resolve ruff linting errors in cli.py Fixed 158 linting errors in openadapt_ml/benchmarks/cli.py: - F541: Removed extraneous f-string prefixes (150 instances auto-fixed) - E402: Moved warnings import to top of file with other imports - F841: Removed unused variables (qemu_commands, run_name, all_ready, server_process) - E741: Renamed ambiguous variable 'l' to 'line' - F821: Added missing time import to cmd_vm function Also updated README.md with documentation about openadapt-evals integration. Co-Authored-By: Claude Sonnet 4.5 --- README.md | 8 +- openadapt_ml/benchmarks/cli.py | 320 ++++++++++++++++----------------- 2 files changed, 158 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 49a3552..dfab24c 100644 --- a/README.md +++ b/README.md @@ -789,13 +789,15 @@ uv run python -m openadapt_ml.cloud.local serve --port 8080 --open simplicity when handling multimodal inputs. The training configs are sized accordingly. - **Evaluation**: - - v1 focuses on smoke tests and qualitative behavior on synthetic data. - More formal evaluation scripts and metrics are planned. + - Benchmark evaluation has been moved to the standalone [openadapt-evals](https://github.com/OpenAdaptAI/openadapt-evals) package (January 2026) + - Use `pip install openadapt-evals` for WAA, WebArena, and other benchmark integrations + - VM management CLI remains in openadapt-ml for infrastructure control + - Backward-compatible imports still work with deprecation warnings - **Windows Agent Arena (WAA) on Azure**: - WAA requires nested virtualization (Windows VM inside Docker via QEMU) - Azure ML managed compute does not support nested virtualization - For real WAA evaluation, use dedicated VMs with Dv3/Ev3 series or run locally - - Mock evaluation (`test-mock`) validates the pipeline without Windows VMs + - Mock evaluation (`WAAMockAdapter` in openadapt-evals) validates the pipeline without Windows VMs - See `CLAUDE.md` for detailed workarounds and infrastructure setup For deeper architectural details, see [`docs/design.md`](docs/design.md). diff --git a/openadapt_ml/benchmarks/cli.py b/openadapt_ml/benchmarks/cli.py index 8211cb0..e963d2c 100644 --- a/openadapt_ml/benchmarks/cli.py +++ b/openadapt_ml/benchmarks/cli.py @@ -89,6 +89,7 @@ import json import logging import sys +import warnings from pathlib import Path from openadapt_ml.config import settings @@ -104,7 +105,6 @@ logging.getLogger("openadapt_ml.benchmarks.azure").setLevel(logging.WARNING) # Suppress Azure SDK experimental class warnings -import warnings warnings.filterwarnings("ignore", message=".*experimental class.*") # SSH options to handle host key changes when VMs are recreated @@ -198,20 +198,13 @@ def bypass_product_key_dialog(ip: str, max_attempts: int = 3) -> bool: # QEMU sendkey commands to navigate to "I don't have a product key" link # The link is at the bottom of the dialog - Tab navigates through UI elements # We need to Tab to the link and press Enter - qemu_commands = ''' -# Navigate to "I don't have a product key" link and click it -# Tab through: text field -> Next button -> Back button -> link -sendkey tab -sendkey tab -sendkey tab -sendkey tab -sendkey ret -''' + # Navigate to "I don't have a product key" link and click it + # Tab through: text field -> Next button -> Back button -> link for attempt in range(max_attempts): try: # Send commands via QEMU monitor (port 7100 in container) - ssh_cmd = f''' + ssh_cmd = ''' # Use telnet to send QEMU commands ( echo "sendkey tab" @@ -354,8 +347,8 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: version = output.split("\n")[0] if output else "unknown" print(f" Azure CLI: ✓ {version}") else: - print(f" Azure CLI: ✗ Not installed") - print(f" Install: brew install azure-cli") + print(" Azure CLI: ✗ Not installed") + print(" Install: brew install azure-cli") return # Check login @@ -363,7 +356,7 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: if ok: print(f" Logged in: ✓ {output}") else: - print(f" Logged in: ✗ Run: az login") + print(" Logged in: ✗ Run: az login") return # Check resource group @@ -376,7 +369,7 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: print(f" Resource group: ✓ {rg} ({output})") else: print(f" Resource group: ✗ {rg} not found") - print(f" Run: python scripts/setup_azure.py") + print(" Run: python scripts/setup_azure.py") return # Check ML workspace @@ -407,10 +400,10 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: "WAA Docker image" ) if ok: - print(f" WAA Docker image: ✓ winarena") + print(" WAA Docker image: ✓ winarena") else: - print(f" WAA Docker image: ✗ Not imported") - print(f" Run: python scripts/setup_azure.py") + print(" WAA Docker image: ✗ Not imported") + print(" Run: python scripts/setup_azure.py") # Check .env file env_path = Path(".env") @@ -419,7 +412,7 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: has_azure = "AZURE_SUBSCRIPTION_ID" in env_content print(f" .env file: ✓ {'Azure credentials found' if has_azure else 'Missing Azure credentials'}") else: - print(f" .env file: ✗ Not found") + print(" .env file: ✗ Not found") # Check WAA submodule waa_path = find_waa_path() @@ -433,8 +426,8 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: except Exception as e: print(f" WAA submodule: ⚠ Found but error: {e}") else: - print(f" WAA submodule: ✗ Not found") - print(f" Run: git submodule update --init --recursive") + print(" WAA submodule: ✗ Not found") + print(" Run: git submodule update --init --recursive") print() print("Ready for benchmark evaluation!" if ok else "Some resources missing - run setup_azure.py") @@ -473,7 +466,7 @@ def cmd_run_local(args: argparse.Namespace) -> None: sys.exit(1) # Run evaluation - print(f"\nRunning WAA evaluation...") + print("\nRunning WAA evaluation...") print(f" WAA path: {waa_path}") print(f" Tasks: {len(task_ids) if task_ids else 'all (154)'}") print(f" Max steps: {args.max_steps}") @@ -675,7 +668,7 @@ def cmd_run_azure(args: argparse.Namespace) -> None: num_tasks = len(task_ids) if task_ids else len(all_task_ids) estimate = estimate_cost(num_tasks=num_tasks, num_workers=args.workers) - print(f"\n=== Azure WAA Evaluation ===") + print("\n=== Azure WAA Evaluation ===") print(f" Workers: {args.workers}") print(f" Tasks: {num_tasks}") print(f" Job timeout: {args.timeout} hours") @@ -816,7 +809,7 @@ def cmd_test_mock(args: argparse.Namespace) -> None: evaluate_agent_on_benchmark, ) - print(f"\n=== Testing with Mock Adapter ===") + print("\n=== Testing with Mock Adapter ===") print(f" Tasks: {args.tasks}") print(f" Max steps: {args.max_steps}") print() @@ -860,7 +853,7 @@ def cmd_test_smart(args: argparse.Namespace) -> None: evaluate_agent_on_benchmark, ) - print(f"\n=== Testing with SmartMockAgent ===") + print("\n=== Testing with SmartMockAgent ===") print(f" Tasks: {args.tasks}") print(f" Max steps: {args.max_steps}") print() @@ -902,7 +895,7 @@ def cmd_test_collection(args: argparse.Namespace) -> None: from openadapt_ml.benchmarks import RandomAgent, WAAMockAdapter from openadapt_ml.benchmarks.runner import EvaluationConfig, evaluate_agent_on_benchmark - print(f"\n=== Testing Benchmark Data Collection ===") + print("\n=== Testing Benchmark Data Collection ===") print(f" Tasks: {args.tasks}") print(f" Max steps: {args.max_steps}") print(f" Output dir: {args.output}") @@ -937,7 +930,7 @@ def cmd_test_collection(args: argparse.Namespace) -> None: success_rate = success_count / len(results) if results else 0.0 avg_steps = sum(r.num_steps for r in results) / len(results) if results else 0.0 - print(f"\n=== Results ===") + print("\n=== Results ===") print(f"Total tasks: {len(results)}") print(f"Success: {success_count} ({success_rate:.1%})") print(f"Failure: {len(results) - success_count}") @@ -950,26 +943,25 @@ def cmd_test_collection(args: argparse.Namespace) -> None: run_dir = run_dirs[0].parent with open(run_dirs[0]) as f: metadata = json.load(f) - run_name = metadata.get("run_name", run_dir.name) + _ = metadata.get("run_name", run_dir.name) else: run_dir = output_dir - run_name = "unknown" - print(f"\n=== Output Directory ===") + print("\n=== Output Directory ===") print(f"Location: {run_dir.absolute()}") - print(f"\nDirectory structure:") + print("\nDirectory structure:") print(f" {run_dir.name}/") - print(f" ├── metadata.json") - print(f" ├── summary.json") - print(f" └── tasks/") - print(f" ├── task_001/") - print(f" │ ├── task.json") - print(f" │ ├── execution.json") - print(f" │ └── screenshots/") - print(f" │ ├── step_000.png") - print(f" │ ├── step_001.png") - print(f" │ └── ...") - print(f" └── ...") + print(" ├── metadata.json") + print(" ├── summary.json") + print(" └── tasks/") + print(" ├── task_001/") + print(" │ ├── task.json") + print(" │ ├── execution.json") + print(" │ └── screenshots/") + print(" │ ├── step_000.png") + print(" │ ├── step_001.png") + print(" │ └── ...") + print(" └── ...") print(f"\nYou can inspect the results at: {run_dir.absolute()}") print() @@ -985,7 +977,6 @@ def cmd_waa_demo(args: argparse.Namespace) -> None: """ from openadapt_ml.experiments.waa_demo.runner import ( DemoConditionedAgent, - TASKS, get_complete_demos, get_task, ) @@ -995,7 +986,7 @@ def cmd_waa_demo(args: argparse.Namespace) -> None: ) from openadapt_ml.benchmarks.runner import EvaluationConfig, evaluate_agent_on_benchmark - print(f"\n=== WAA Demo-Conditioned Experiment ===") + print("\n=== WAA Demo-Conditioned Experiment ===") print(f" Condition: {args.condition}") print(f" Provider: {args.provider}") print(f" Tasks: {args.tasks or 'all with demos'}") @@ -1126,7 +1117,7 @@ def cmd_run_api(args: argparse.Namespace) -> None: "openai": "GPT-5.1", } - print(f"\n=== API-Backed Benchmark Evaluation ===") + print("\n=== API-Backed Benchmark Evaluation ===") print(f" Provider: {args.provider} ({provider_names.get(args.provider, 'Unknown')})") print(f" Tasks: {args.tasks}") print(f" Max steps: {args.max_steps}") @@ -1137,14 +1128,14 @@ def cmd_run_api(args: argparse.Namespace) -> None: key_name = "ANTHROPIC_API_KEY" if args.provider == "anthropic" else "OPENAI_API_KEY" if not os.getenv(key_name): print(f"WARNING: {key_name} environment variable not set!") - print(f" Set it in your .env file or export it before running.") + print(" Set it in your .env file or export it before running.") print() # Determine which adapter to use task_ids = None if args.mock: # User explicitly requested mock adapter - print(f" Adapter: Mock (forced by --mock flag)") + print(" Adapter: Mock (forced by --mock flag)") print() adapter = WAAMockAdapter(num_tasks=args.tasks, domains=["browser", "office"]) else: @@ -1246,7 +1237,7 @@ def cmd_run_api(args: argparse.Namespace) -> None: if run_dirs: run_dir = run_dirs[0].parent print(f"Results saved to: {run_dir.absolute()}") - print(f"View with: uv run python -m openadapt_ml.cloud.local serve --open") + print("View with: uv run python -m openadapt_ml.cloud.local serve --open") print() @@ -1301,7 +1292,7 @@ def cmd_status(args: argparse.Namespace) -> None: try: client = AzureMLClient(config) computes = client.list_compute_instances(prefix="w") - print(f"Connection: OK") + print("Connection: OK") if computes: print(f"\nActive Compute Instances ({len(computes)}):") @@ -1315,7 +1306,7 @@ def cmd_status(args: argparse.Namespace) -> None: print("\nNo active compute instances.") except Exception as e: - print(f"Connection: FAILED") + print("Connection: FAILED") print(f" Error: {e}") print() @@ -1458,7 +1449,7 @@ def cmd_list_jobs(args: argparse.Namespace) -> None: sys.exit(1) # Filter out experimental warnings - lines = [l for l in result.stdout.split("\n") if "experimental" not in l.lower()] + lines = [line for line in result.stdout.split("\n") if "experimental" not in line.lower()] print("\n".join(lines[:args.limit + 3])) # +3 for header rows @@ -1583,7 +1574,6 @@ def capture_vm_screenshot(ip: str, output_path: Path | str = None) -> Path | Non Path to the saved screenshot, or None on failure """ import subprocess - import tempfile import shlex if output_path is None: @@ -1997,12 +1987,12 @@ def launch_benchmark_viewer( import os import sys - print(f"\n=== Launching Benchmark Viewer ===\n") + print("\n=== Launching Benchmark Viewer ===\n") print(f" VM IP: {vm_ip}") print(f" Internal IP: {internal_ip}") print(f" Local port: {port}") print(f" Dashboard: http://localhost:{port}/benchmark.html") - print(f" VNC available via button in viewer when VM is ready") + print(" VNC available via button in viewer when VM is ready") print() # Set environment variables for the server to use @@ -2019,7 +2009,7 @@ def launch_benchmark_viewer( if open_browser: serve_cmd.append("--open") - print(f" Press Ctrl+C to stop\n") + print(" Press Ctrl+C to stop\n") try: # Run the server @@ -2058,6 +2048,7 @@ def cmd_vm(args: argparse.Namespace) -> None: inside Docker/QEMU. """ import subprocess + import time vm_name = args.name resource_group = args.resource_group @@ -2086,7 +2077,7 @@ def cmd_vm(args: argparse.Namespace) -> None: print(" - Standard_D8s_v3 (8 vCPU, 32GB) ~$0.38/hr") print(" - Standard_D4ds_v5 (4 vCPU, 16GB) ~$0.19/hr") print(" - Standard_D8ds_v5 (8 vCPU, 32GB) ~$0.38/hr") - print(f"\nTry different locations if sizes are unavailable: westus2, centralus, westeurope") + print("\nTry different locations if sizes are unavailable: westus2, centralus, westeurope") return elif args.action == "create": @@ -2094,7 +2085,7 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f" Resource Group: {resource_group}") print(f" Location: {location}") print(f" VM Size: {vm_size} (supports nested virtualization)") - print(f" OS: Ubuntu 22.04 LTS") + print(" OS: Ubuntu 22.04 LTS") print() # Check if VM already exists @@ -2131,13 +2122,13 @@ def cmd_vm(args: argparse.Namespace) -> None: vm_info = json.loads(result.stdout) public_ip = vm_info.get("publicIpAddress", "unknown") - print(f"\n✓ VM created successfully!") + print("\n✓ VM created successfully!") print(f"\n Public IP: {public_ip}") print(f" SSH command: ssh azureuser@{public_ip}") - print(f"\n Next steps:") - print(f" 1. SSH into the VM: uv run python -m openadapt_ml.benchmarks.cli vm ssh") - print(f" 2. Verify nested virt: egrep -c '(vmx|svm)' /proc/cpuinfo") - print(f" 3. Install Docker and run WAA") + print("\n Next steps:") + print(" 1. SSH into the VM: uv run python -m openadapt_ml.benchmarks.cli vm ssh") + print(" 2. Verify nested virt: egrep -c '(vmx|svm)' /proc/cpuinfo") + print(" 3. Install Docker and run WAA") elif args.action == "status": print(f"\n=== WAA Eval VM Status: {vm_name} ===\n") @@ -2151,7 +2142,7 @@ def cmd_vm(args: argparse.Namespace) -> None: if result.returncode != 0: print(f"✗ VM '{vm_name}' not found in resource group '{resource_group}'") - print(f" Create it with: uv run python -m openadapt_ml.benchmarks.cli vm create") + print(" Create it with: uv run python -m openadapt_ml.benchmarks.cli vm create") sys.exit(1) import json @@ -2216,9 +2207,9 @@ def cmd_vm(args: argparse.Namespace) -> None: sys.exit(1) print(f"✓ VM '{vm_name}' deallocation initiated") - print(f"\n Cost savings: Deallocated VMs do not incur compute charges.") - print(f" Storage costs still apply. Use 'vm delete' to stop all charges.") - print(f" To restart: python -m openadapt_ml.benchmarks.cli vm start") + print("\n Cost savings: Deallocated VMs do not incur compute charges.") + print(" Storage costs still apply. Use 'vm delete' to stop all charges.") + print(" To restart: python -m openadapt_ml.benchmarks.cli vm start") elif args.action == "start": print(f"\n=== Starting VM: {vm_name} ===\n") @@ -2235,7 +2226,7 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f"✓ VM '{vm_name}' start initiated") # Wait for VM to be running and recover Docker (temp disk is wiped on deallocate) - print(f"\n Waiting for VM to be ready...") + print("\n Waiting for VM to be ready...") ip = None for _ in range(30): # Wait up to 60 seconds time.sleep(2) @@ -2253,14 +2244,14 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f" ✓ VM is running (IP: {ip})") # Recover Docker - /mnt is wiped when VM is deallocated - print(f" Checking Docker...") + print(" Checking Docker...") result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "sudo docker ps 2>&1"], capture_output=True, text=True ) if "Cannot connect to the Docker daemon" in result.stdout or result.returncode != 0: - print(f" Docker not running. Recovering...") + print(" Docker not running. Recovering...") # Create Docker directories (symlinked to /mnt which gets wiped) # Kill any stale processes, create dirs, start services recovery_cmd = ''' @@ -2286,15 +2277,15 @@ def cmd_vm(args: argparse.Namespace) -> None: capture_output=True, text=True ) if result.returncode == 0: - print(f" ✓ Docker recovered and running") + print(" ✓ Docker recovered and running") else: - print(f" ⚠ Docker may not be running. Check with 'vm diag'") + print(" ⚠ Docker may not be running. Check with 'vm diag'") else: - print(f" ✓ Docker is running") + print(" ✓ Docker is running") else: - print(f"\n Use 'vm status' to check when the VM is running.") + print("\n Use 'vm status' to check when the VM is running.") - print(f" Use 'vm monitor' to start dashboard when VM is ready.") + print(" Use 'vm monitor' to start dashboard when VM is ready.") elif args.action == "setup": print(f"\n=== Setting up WAA Eval VM: {vm_name} ===\n") @@ -2340,11 +2331,11 @@ def cmd_vm(args: argparse.Namespace) -> None: if cpu_count and int(cpu_count) > 0: print(f" ✓ Nested virt supported ({cpu_count} CPUs with vmx/svm)") else: - print(f" ⚠ Nested virt may not be supported") + print(" ⚠ Nested virt may not be supported") print("\n[3/3] Setup complete!") - print(f"\n Next: Pull WAA image with 'vm pull-image'") - print(f" Or SSH in: uv run python -m openadapt_ml.benchmarks.cli vm ssh") + print("\n Next: Pull WAA image with 'vm pull-image'") + print(" Or SSH in: uv run python -m openadapt_ml.benchmarks.cli vm ssh") elif args.action == "pull-image": print(f"\n=== Pulling WAA Docker Image to VM: {vm_name} ===\n") @@ -2388,12 +2379,12 @@ def cmd_vm(args: argparse.Namespace) -> None: capture_output=False # Show output live ) if result.returncode != 0: - print(f"\n✗ Error pulling image") + print("\n✗ Error pulling image") sys.exit(1) - print(f"\n✓ WAA image pulled successfully!") + print("\n✓ WAA image pulled successfully!") print(f"\n Image ready: {image}") - print(f" Run WAA with: uv run python -m openadapt_ml.benchmarks.cli vm ssh") + print(" Run WAA with: uv run python -m openadapt_ml.benchmarks.cli vm ssh") elif args.action == "setup-waa": from openadapt_ml.benchmarks.vm_monitor import VMPoolRegistry @@ -2535,7 +2526,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print("✗ Could not create VM in any region") sys.exit(1) - print(f"\n[2/6] Installing Docker with /mnt storage (147GB)...") + print("\n[2/6] Installing Docker with /mnt storage (147GB)...") docker_cmds = [ "sudo apt-get update -qq", "sudo apt-get install -y -qq docker.io", @@ -2558,7 +2549,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: else: print(" ✓ Docker installed with /mnt storage") - print(f"\n[3/6] Verifying nested virtualization...") + print("\n[3/6] Verifying nested virtualization...") result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "egrep -c '(vmx|svm)' /proc/cpuinfo"], @@ -2571,7 +2562,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(" ✗ Nested virtualization not supported - WAA won't work") sys.exit(1) - print(f"\n[4/6] Pulling dockurr/windows image (for Windows VM)...") + print("\n[4/6] Pulling dockurr/windows image (for Windows VM)...") # Use dockurr/windows directly - the ACR winarena image has broken dockur result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", @@ -2582,7 +2573,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(f" ⚠ Image pull warning: {result.stderr[:100] if result.stderr else ''}") print(" ✓ Windows image pulled") - print(f"\n[5/6] Cloning WindowsAgentArena repository...") + print("\n[5/6] Cloning WindowsAgentArena repository...") result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "cd ~ && git clone --depth 1 https://github.com/microsoft/WindowsAgentArena.git 2>/dev/null || echo 'Already cloned'"], @@ -2590,7 +2581,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: ) print(" ✓ WAA repo cloned") - print(f"\n[6/6] Creating WAA config file...") + print("\n[6/6] Creating WAA config file...") api_key = args.api_key or settings.openai_api_key or "" if not api_key: print(" ⚠ No API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file") @@ -2613,10 +2604,10 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(" WAA Setup Complete!") print(f"{'='*60}") print(f"\n VM IP: {ip}") - print(f"\n Next step: Prepare Windows image (one-time, ~20 min):") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm prepare-windows") - print(f"\n Or run WAA directly (will auto-prepare on first run):") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print("\n Next step: Prepare Windows image (one-time, ~20 min):") + print(" uv run python -m openadapt_ml.benchmarks.cli vm prepare-windows") + print("\n Or run WAA directly (will auto-prepare on first run):") + print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") else: # Multi-worker mode: create multiple VMs in parallel @@ -2640,11 +2631,10 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: sys.exit(1) # Wait for VMs to be ready and get IPs - print(f"\n[2/4] Waiting for VMs to get public IPs...") + print("\n[2/4] Waiting for VMs to get public IPs...") import time as time_mod workers_with_ips = [] for _ in range(30): # Wait up to 5 minutes - all_ready = True for name in created_vms: if any(n == name for n, _ in workers_with_ips): continue # Already got IP @@ -2656,8 +2646,6 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: if result.stdout.strip(): workers_with_ips.append((name, result.stdout.strip())) print(f" ✓ {name}: {result.stdout.strip()}") - else: - all_ready = False if len(workers_with_ips) == len(created_vms): break @@ -2686,7 +2674,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(f" ✗ {name}: Setup failed") # Create pool registry - print(f"\n[4/4] Registering VM pool...") + print("\n[4/4] Registering VM pool...") registry = VMPoolRegistry() pool = registry.create_pool( workers=workers_with_ips, @@ -2702,16 +2690,16 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(f"\n Workers: {len(workers_with_ips)}") for name, ip in workers_with_ips: print(f" - {name}: {ip}") - print(f"\n Next steps:") - print(f" 1. Check pool status:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm pool-status") - print(f" 2. Prepare Windows on all workers (in parallel):") - print(f" # TODO: implement prepare-windows --pool") - print(f" 3. Run parallel benchmark:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 30") + print("\n Next steps:") + print(" 1. Check pool status:") + print(" uv run python -m openadapt_ml.benchmarks.cli vm pool-status") + print(" 2. Prepare Windows on all workers (in parallel):") + print(" # TODO: implement prepare-windows --pool") + print(" 3. Run parallel benchmark:") + print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 30") elif args.action == "prepare-windows": - print(f"\n=== Preparing Windows 11 VM for WAA (Fully Automated) ===\n") + print("\n=== Preparing Windows 11 VM for WAA (Fully Automated) ===\n") print("This builds a custom WAA container with automatic setup scripts.") print("First run downloads Windows 11 (~7GB). Setup is fully automatic - no VNC needed.\n") @@ -2866,12 +2854,12 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: probe_result = None if probe_result and probe_result.stdout.strip(): - print(f"\n✓ WAA Server ready!") + print("\n✓ WAA Server ready!") print(f"\n Windows VNC: http://{ip}:8006") print(f" WAA Server: http://{ip}:5000 (internal via localhost:5000)") print(f" QMP Port: {ip}:7200") - print(f"\n To run WAA benchmarks:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print("\n To run WAA benchmarks:") + print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") break # Show progress from docker logs @@ -2898,7 +2886,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Ensure unbuffered output for real-time streaming os.environ['PYTHONUNBUFFERED'] = '1' - print(f"\n=== Running WAA Benchmark ===\n", flush=True) + print("\n=== Running WAA Benchmark ===\n", flush=True) # Helper function to write live status for the viewer def write_live_status(status: str, phase: str = None, detail: str = None, @@ -2980,14 +2968,14 @@ def write_live_status(status: str, phase: str = None, detail: str = None, print(" Set ANTHROPIC_API_KEY env var or in .env file") sys.exit(1) api_key = anthropic_key - print(f" API Key: ANTHROPIC_API_KEY (set)") + print(" API Key: ANTHROPIC_API_KEY (set)") else: # navi and api-openai both use OpenAI if not openai_key: print("✗ No OpenAI API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file") sys.exit(1) api_key = openai_key - print(f" API Key: OPENAI_API_KEY (set)") + print(" API Key: OPENAI_API_KEY (set)") # Set environment variables for the server to use (for SSE endpoint) os.environ["WAA_VM_IP"] = ip @@ -2995,7 +2983,6 @@ def write_live_status(status: str, phase: str = None, detail: str = None, # Launch benchmark viewer in background if --open is set # Use the proper server from local.py that has /api/benchmark-live endpoint - server_process = None if open_viewer: print(f"\n Launching benchmark viewer at http://localhost:{port}/benchmark.html") @@ -3213,7 +3200,7 @@ def start_server(): sys.exit(1) # Start WAA container with full benchmark run - print(f"[5/5] Starting WAA benchmark (this will take a while)...") + print("[5/5] Starting WAA benchmark (this will take a while)...") print(f" Agent will run {num_tasks} tasks using {model}") if open_viewer: print(f" Viewer running at: http://localhost:{port}/benchmark.html") @@ -3334,8 +3321,8 @@ def start_server(): if returncode == 0: write_live_status("complete", detail=f"Benchmark complete! {tasks_completed}/{num_tasks} tasks", tasks_completed=tasks_completed, total_tasks=num_tasks) - print(f"\n✓ WAA evaluation complete!") - print(f"\n Results saved to: ~/waa-results on the VM") + print("\n✓ WAA evaluation complete!") + print("\n Results saved to: ~/waa-results on the VM") print(f" To download: scp azureuser@{ip}:~/waa-results/* ./benchmark_results/") else: write_live_status("error", detail=f"Benchmark finished with errors (exit code: {returncode})", @@ -3345,21 +3332,21 @@ def start_server(): # Auto-shutdown VM if --auto-shutdown flag is set auto_shutdown = getattr(args, 'auto_shutdown', False) if auto_shutdown: - print(f"\n=== Auto-shutdown: Deallocating VM to save costs ===\n") + print("\n=== Auto-shutdown: Deallocating VM to save costs ===\n") deallocate_result = subprocess.run( ["az", "vm", "deallocate", "-g", resource_group, "-n", vm_name, "--no-wait"], capture_output=True, text=True ) if deallocate_result.returncode == 0: print(f"✓ VM '{vm_name}' deallocation initiated") - print(f"\n Cost savings: Deallocated VMs do not incur compute charges.") - print(f" Note: Storage costs still apply. Delete VM with 'vm delete' to stop all charges.") + print("\n Cost savings: Deallocated VMs do not incur compute charges.") + print(" Note: Storage costs still apply. Delete VM with 'vm delete' to stop all charges.") print(f" To restart: az vm start -g {resource_group} -n {vm_name}") else: print(f"✗ Failed to deallocate VM: {deallocate_result.stderr}") elif args.action == "fix-storage": - print(f"\n=== Fix WAA Storage (Move to /mnt for More Space) ===\n") + print("\n=== Fix WAA Storage (Move to /mnt for More Space) ===\n") print("Moves WAA storage from root disk (~10GB free) to /mnt temp disk (~115GB free).\n") # Get VM IP @@ -3451,13 +3438,13 @@ def start_server(): print(f"\n{'='*60}") print(" Storage Fixed!") print(f"{'='*60}") - print(f"\n Storage now on /mnt: ~115GB available") + print("\n Storage now on /mnt: ~115GB available") print(f" VNC: http://{ip}:8006") - print(f"\n If Windows was installing, it will resume automatically.") - print(f" Monitor: uv run python -m openadapt_ml.benchmarks.cli vm status") + print("\n If Windows was installing, it will resume automatically.") + print(" Monitor: uv run python -m openadapt_ml.benchmarks.cli vm status") elif args.action == "docker-prune": - print(f"\n=== Docker Cleanup (Free Disk Space) ===\n") + print("\n=== Docker Cleanup (Free Disk Space) ===\n") # Get VM IP ip = get_vm_ip(resource_group, vm_name) @@ -3488,7 +3475,7 @@ def start_server(): if prune_result.returncode == 0: # Extract space reclaimed output = prune_result.stdout - print(f" ✓ Docker cleanup complete") + print(" ✓ Docker cleanup complete") if "Total reclaimed space" in output: for line in output.split('\n'): if "Total reclaimed space" in line: @@ -3504,10 +3491,10 @@ def start_server(): capture_output=True, text=True ) print(f" {df_result.stdout}") - print(f"\n Retry build: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") + print("\n Retry build: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") elif args.action == "docker-move": - print(f"\n=== Move Docker Data to /mnt (147GB) ===\n") + print("\n=== Move Docker Data to /mnt (147GB) ===\n") print("Reconfigures Docker to use /mnt/docker for all images and layers.") print("This solves 'no space left on device' errors during docker build.\n") @@ -3611,12 +3598,12 @@ def start_server(): print(f"\n{'='*60}") print(" Docker Data Moved to /mnt!") print(f"{'='*60}") - print(f"\n Root disk now has space for OS only.") - print(f" Docker images will use /mnt/docker (147GB available).") - print(f"\n Next: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") + print("\n Root disk now has space for OS only.") + print(" Docker images will use /mnt/docker (147GB available).") + print("\n Next: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") elif args.action == "reset-windows": - print(f"\n=== Reset Windows (Clean Install) ===\n") + print("\n=== Reset Windows (Clean Install) ===\n") print("Deletes existing Windows disk image and does a fresh install.\n") # Get VM IP @@ -3708,10 +3695,10 @@ def start_server(): probe_result = None if probe_result and probe_result.stdout.strip(): - print(f"\n✓ WAA Server ready!") + print("\n✓ WAA Server ready!") print(f" Probe response: {probe_result.stdout.strip()[:100]}") - print(f"\n To run benchmarks:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print("\n To run benchmarks:") + print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") break # Show progress from docker logs @@ -3727,7 +3714,7 @@ def start_server(): print(" Windows installation may still be in progress.") elif args.action == "screenshot": - print(f"\n=== Capturing VM Screenshot ===\n") + print("\n=== Capturing VM Screenshot ===\n") ip = get_vm_ip(resource_group, vm_name) if not ip: @@ -3742,14 +3729,14 @@ def start_server(): if result_path: print(f" ✓ Screenshot saved to: {result_path}") - print(f"\n View at: http://localhost:8080/vm_screenshot.png (if server running)") + print("\n View at: http://localhost:8080/vm_screenshot.png (if server running)") else: print(" ✗ Failed to capture screenshot") print(" Make sure the winarena container is running and QEMU monitor is accessible.") sys.exit(1) elif args.action == "probe": - print(f"\n=== Checking WAA /probe Endpoint ===\n") + print("\n=== Checking WAA /probe Endpoint ===\n") ip = get_vm_ip(resource_group, vm_name) if not ip: @@ -3767,10 +3754,10 @@ def start_server(): max_attempts = getattr(args, 'max_attempts', 30) interval = getattr(args, 'interval', 20) if poll_waa_probe(ip, max_attempts=max_attempts, interval=interval, internal_ip=internal_ip): - print(f"\n Ready to run benchmarks:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print("\n Ready to run benchmarks:") + print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") else: - print(f"\n VNC (via SSH tunnel): http://localhost:8006") + print("\n VNC (via SSH tunnel): http://localhost:8006") print(f" Start tunnel: ssh -L 8006:{ip}:8006 azureuser@{ip}") sys.exit(1) else: @@ -3780,20 +3767,20 @@ def start_server(): is_ready, response = check_waa_probe(ip, internal_ip=internal_ip) if is_ready: - print(f" ✓ WAA server is READY") + print(" ✓ WAA server is READY") print(f" Response: {response[:100] if response else '(empty)'}") - print(f"\n Ready to run benchmarks:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print("\n Ready to run benchmarks:") + print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") else: print(" ✗ WAA server NOT responding") - print(f"\n To poll until ready, use: vm probe --wait") - print(f" VNC (via SSH tunnel): http://localhost:8006") + print("\n To poll until ready, use: vm probe --wait") + print(" VNC (via SSH tunnel): http://localhost:8006") print(f" Start tunnel: ssh -L 8006:{ip}:8006 azureuser@{ip}") elif args.action == "pool-status": from openadapt_ml.benchmarks.vm_monitor import VMPoolRegistry, VMMonitor, VMConfig - print(f"\n=== VM Pool Status ===\n") + print("\n=== VM Pool Status ===\n") registry = VMPoolRegistry() pool = registry.get_pool() @@ -3875,9 +3862,9 @@ def delete_vm(name: str) -> tuple[str, bool, str]: from datetime import datetime, timezone import json - print(f"\n=== Cleanup Stale Azure Resources ===\n") + print("\n=== Cleanup Stale Azure Resources ===\n") print(f" Resource Group: {resource_group}") - print(f" Workspace: openadapt-ml") + print(" Workspace: openadapt-ml") print(f" Job threshold: {args.max_hours} hours") print(f" VM threshold: {args.vm_max_hours} hours") print() @@ -3936,7 +3923,7 @@ def delete_vm(name: str) -> tuple[str, bool, str]: continue except json.JSONDecodeError: - print(f" Warning: Could not parse job list") + print(" Warning: Could not parse job list") else: print(f" Warning: Could not list jobs: {result.stderr[:100]}") @@ -4008,7 +3995,7 @@ def delete_vm(name: str) -> tuple[str, bool, str]: pass except json.JSONDecodeError: - print(f" Warning: Could not parse VM list") + print(" Warning: Could not parse VM list") else: print(f" Warning: Could not list VMs: {result.stderr[:100]}") @@ -4100,7 +4087,7 @@ def delete_vm(name: str) -> tuple[str, bool, str]: port = getattr(args, 'port', 8765) auto_shutdown_hours = getattr(args, 'auto_shutdown_hours', 0) - print(f"\n=== VM Monitor Dashboard ===\n") + print("\n=== VM Monitor Dashboard ===\n") # Check if server is already running on port def is_port_in_use(port: int) -> bool: @@ -4139,7 +4126,7 @@ def start_server(): server_thread = threading.Thread(target=start_server, daemon=True) server_thread.start() time.sleep(1) - print(f" ✓ Dashboard started") + print(" ✓ Dashboard started") # Start SSH tunnels for VNC and WAA ip = get_vm_ip(resource_group, vm_name) @@ -4159,11 +4146,11 @@ def start_server(): # Open browser url = f"http://localhost:{port}/benchmark.html" print(f"\n Opening: {url}") - print(f" VNC: http://localhost:8006") + print(" VNC: http://localhost:8006") if auto_shutdown_hours > 0: shutdown_time = datetime.now() + timedelta(hours=auto_shutdown_hours) print(f" Auto-shutdown: {shutdown_time.strftime('%H:%M:%S')} ({auto_shutdown_hours}h)") - print(f"\n Press Ctrl+C to stop monitoring.\n") + print("\n Press Ctrl+C to stop monitoring.\n") webbrowser.open(url) # Track start time for auto-shutdown @@ -4248,11 +4235,11 @@ def start_server(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") sys.exit(1) - print(f"\n=== Testing Docker Run Command ===\n") + print("\n=== Testing Docker Run Command ===\n") print(f" VM IP: {ip}") # First check for port conflicts - print(f"\n[1/3] Checking for port conflicts...") + print("\n[1/3] Checking for port conflicts...") check_cmd = "docker ps -a --format '{{.Names}} {{.Ports}}' 2>/dev/null || echo 'No containers'" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", check_cmd], @@ -4261,7 +4248,7 @@ def start_server(): print(f" Containers: {result.stdout.strip()}") # Clean up any conflicting containers - print(f"\n[2/3] Cleaning up old containers...") + print("\n[2/3] Cleaning up old containers...") cleanup_cmd = "docker rm -f winarena winarena-test 2>/dev/null || true" subprocess.run(["ssh", *SSH_OPTS, f"azureuser@{ip}", cleanup_cmd], capture_output=True) @@ -4279,7 +4266,7 @@ def start_server(): waa-auto:latest \ "/copy-oem.sh echo OEM_FILES_COPIED && ls -la /tmp/smb/"''' - print(f"\n[3/3] Testing docker run with copy-oem.sh...") + print("\n[3/3] Testing docker run with copy-oem.sh...") print(f" Command: {docker_cmd[:100]}...") result = subprocess.run( @@ -4294,9 +4281,9 @@ def start_server(): print(f"\n STDERR:\n{result.stderr}") if "OEM_FILES_COPIED" in result.stdout and "install.bat" in result.stdout: - print(f"\n✓ Docker test PASSED - OEM files copied correctly") + print("\n✓ Docker test PASSED - OEM files copied correctly") else: - print(f"\n✗ Docker test FAILED - OEM files not copied") + print("\n✗ Docker test FAILED - OEM files not copied") elif args.action == "start-server": # Start WAA Flask server inside Windows (for existing installations) @@ -4306,7 +4293,7 @@ def start_server(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") sys.exit(1) - print(f"\n=== Starting WAA Server ===\n") + print("\n=== Starting WAA Server ===\n") print(f" VM IP: {ip}") # Step 1: Copy startup script to VM @@ -4448,12 +4435,12 @@ def send_keys_string(sock, text): time.sleep(5) is_ready, response = check_waa_probe(ip, internal_ip='172.30.0.2') if is_ready: - print(f"\n✓ WAA server is running!") + print("\n✓ WAA server is running!") print(f" Response: {response}") break print(f" Attempt {i+1}/6: Not ready yet...") else: - print(f"\n⚠ Server may not have started. Check VNC at http://localhost:8006") + print("\n⚠ Server may not have started. Check VNC at http://localhost:8006") print(" You can manually run: \\\\host.lan\\Data\\start_waa_server.bat") elif args.action == "fix-oem": @@ -4463,7 +4450,7 @@ def send_keys_string(sock, text): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") sys.exit(1) - print(f"Copying OEM files to Samba share...") + print("Copying OEM files to Samba share...") result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "docker exec winarena sh -c 'cp -r /oem/* /tmp/smb/ 2>&1 && ls -la /tmp/smb/'"], @@ -4503,7 +4490,7 @@ def send_keys_string(sock, text): print(f"Error: {result.stderr}") elif args.action == "stop-build": - print(f"\n=== Stop Docker Build on VM ===\n") + print("\n=== Stop Docker Build on VM ===\n") # Get VM IP ip = get_vm_ip(resource_group, vm_name) @@ -4630,7 +4617,6 @@ def cmd_view(args: argparse.Namespace) -> None: """ import http.server import socketserver - import threading import webbrowser from openadapt_ml.benchmarks.viewer import generate_benchmark_viewer @@ -4649,10 +4635,10 @@ def cmd_view(args: argparse.Namespace) -> None: else: print(" (no benchmark runs found)") else: - print(f" (directory does not exist)") + print(" (directory does not exist)") sys.exit(1) - print(f"\n=== Benchmark Viewer ===\n") + print("\n=== Benchmark Viewer ===\n") print(f" Run: {args.run_name}") print(f" Directory: {benchmark_dir}") @@ -4752,7 +4738,7 @@ def cmd_export_traces(args: argparse.Namespace) -> None: print(" (no runs found)") sys.exit(1) - print(f"\n=== Export WAA Traces as Training Data ===\n") + print("\n=== Export WAA Traces as Training Data ===\n") print(f" Source: {benchmark_dir}") print(f" Output: {args.output}") print(f" Filter: {args.status}") @@ -4769,10 +4755,10 @@ def cmd_export_traces(args: argparse.Namespace) -> None: create_jsonl=not args.no_jsonl, ) - print(f"\n=== Export Complete ===") + print("\n=== Export Complete ===") print(f" Exported {len(episodes)} episodes") print(f" Total steps: {sum(len(ep.steps) for ep in episodes)}") - print(f"\nOutput files:") + print("\nOutput files:") print(f" Episodes: {args.output}/episodes/") if not args.no_screenshots: print(f" Screenshots: {args.output}/screenshots/") From 977f295e5770220db39bedb7c3b62fb371ce5ff4 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 10:02:50 -0500 Subject: [PATCH 07/16] chore: trigger CI From 71e094caca3e24ec86493035b9adac0b81c84325 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 10:09:02 -0500 Subject: [PATCH 08/16] fix: resolve all remaining ruff linting errors (158 total fixed) Fixed across all modules: - E402: Moved imports to top (benchmarks/__init__.py) - E741: Renamed ambiguous variable 'l' to 'loss_entry' (trainer.py, lambda_labs.py) - E722: Replaced bare except with specific exceptions (lambda_labs.py) - F401: Added noqa comments for re-exported imports (ingest/) - F811: Renamed shadowing variable in config.py - F821: Added Episode import to TYPE_CHECKING block (grounding.py) - F541: Removed extraneous f-string prefixes (auto-fixed 95 instances) - F841: Removed unused variables (auto-fixed 20 instances) All modules now pass ruff check without errors. Co-Authored-By: Claude Sonnet 4.5 --- openadapt_ml/baselines/adapter.py | 2 +- openadapt_ml/baselines/cli.py | 3 +- openadapt_ml/benchmarks/__init__.py | 19 +- openadapt_ml/benchmarks/agent.py | 4 +- openadapt_ml/benchmarks/azure.py | 12 +- openadapt_ml/benchmarks/trace_export.py | 3 +- openadapt_ml/benchmarks/waa.py | 2 +- .../benchmarks/waa_deploy/api_agent.py | 2 +- openadapt_ml/benchmarks/waa_live.py | 3 +- openadapt_ml/cloud/azure_inference.py | 4 +- openadapt_ml/cloud/lambda_labs.py | 56 +- openadapt_ml/cloud/local.py | 22 +- openadapt_ml/cloud/ssh_tunnel.py | 3 +- openadapt_ml/datasets/next_action.py | 3 - openadapt_ml/evals/grounding.py | 3 +- openadapt_ml/evals/trajectory_matching.py | 2 - .../experiments/demo_prompt/format_demo.py | 2 +- .../experiments/demo_prompt/run_experiment.py | 2 - .../representation_shootout/conditions.py | 1 - .../representation_shootout/config.py | 6 +- .../representation_shootout/runner.py | 6 +- openadapt_ml/experiments/waa_demo/runner.py | 3 - openadapt_ml/export/parquet.py | 1 - openadapt_ml/grounding/detector.py | 2 +- openadapt_ml/ingest/__init__.py | 2 +- openadapt_ml/ingest/capture.py | 3 +- openadapt_ml/ingest/loader.py | 3 +- openadapt_ml/ingest/synthetic.py | 2 +- openadapt_ml/models/qwen_vl.py | 1 - openadapt_ml/retrieval/demo_retriever.py | 1 - openadapt_ml/retrieval/embeddings.py | 4 +- openadapt_ml/runtime/policy.py | 2 +- openadapt_ml/runtime/safety_gate.py | 2 +- openadapt_ml/scripts/compare.py | 2 +- openadapt_ml/scripts/eval_policy.py | 4 +- openadapt_ml/scripts/make_gif.py | 2 +- openadapt_ml/training/stub_provider.py | 1 - openadapt_ml/training/trainer.py | 26 +- openadapt_ml/training/trl_trainer.py | 2 +- openadapt_ml/training/viewer.py | 9 +- .../training/viewer_migration_example.py | 2 - viewer_migration_example.html | 755 ++++++++++++++++++ 42 files changed, 851 insertions(+), 138 deletions(-) create mode 100644 viewer_migration_example.html diff --git a/openadapt_ml/baselines/adapter.py b/openadapt_ml/baselines/adapter.py index 8d7372f..198ec79 100644 --- a/openadapt_ml/baselines/adapter.py +++ b/openadapt_ml/baselines/adapter.py @@ -8,7 +8,7 @@ import os from typing import TYPE_CHECKING, Any -from openadapt_ml.baselines.config import BaselineConfig, TrackConfig, get_model_spec +from openadapt_ml.baselines.config import BaselineConfig, TrackConfig from openadapt_ml.baselines.parser import ParsedAction, UnifiedResponseParser from openadapt_ml.baselines.prompts import PromptBuilder from openadapt_ml.config import settings diff --git a/openadapt_ml/baselines/cli.py b/openadapt_ml/baselines/cli.py index 4e21d88..c36ef0e 100644 --- a/openadapt_ml/baselines/cli.py +++ b/openadapt_ml/baselines/cli.py @@ -8,11 +8,10 @@ import json import sys from pathlib import Path -from typing import Any import click -from openadapt_ml.baselines.config import MODELS, TrackConfig, TrackType +from openadapt_ml.baselines.config import MODELS @click.group() diff --git a/openadapt_ml/benchmarks/__init__.py b/openadapt_ml/benchmarks/__init__.py index 878bde1..64250df 100644 --- a/openadapt_ml/benchmarks/__init__.py +++ b/openadapt_ml/benchmarks/__init__.py @@ -69,18 +69,10 @@ print(f"Success rate: {metrics['success_rate']:.1%}") ``` """ +from __future__ import annotations import warnings -# Emit deprecation warning on import -warnings.warn( - "openadapt_ml.benchmarks is deprecated. " - "Please use openadapt_evals for standalone benchmark evaluation. " - "See CLAUDE.md for migration guide.", - DeprecationWarning, - stacklevel=2, -) - from openadapt_ml.benchmarks.agent import ( APIBenchmarkAgent, BenchmarkAgent, @@ -109,6 +101,15 @@ from openadapt_ml.benchmarks.waa_live import WAALiveAdapter, WAALiveConfig from openadapt_ml.benchmarks.viewer import generate_benchmark_viewer +# Emit deprecation warning on import +warnings.warn( + "openadapt_ml.benchmarks is deprecated. " + "Please use openadapt_evals for standalone benchmark evaluation. " + "See CLAUDE.md for migration guide.", + DeprecationWarning, + stacklevel=2, +) + # Azure orchestration (lazy import to avoid requiring azure-ai-ml) def _get_azure_classes(): from openadapt_ml.benchmarks.azure import ( diff --git a/openadapt_ml/benchmarks/agent.py b/openadapt_ml/benchmarks/agent.py index e8d9151..c6c6c14 100644 --- a/openadapt_ml/benchmarks/agent.py +++ b/openadapt_ml/benchmarks/agent.py @@ -22,7 +22,6 @@ from __future__ import annotations -import json import re from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any @@ -36,7 +35,7 @@ if TYPE_CHECKING: from openadapt_ml.models.api_adapter import ApiVLMAdapter from openadapt_ml.runtime.policy import AgentPolicy - from openadapt_ml.schema import Action, ActionType + from openadapt_ml.schema import Action class BenchmarkAgent(ABC): @@ -971,7 +970,6 @@ def _get_adapter(self): """Lazily initialize the UnifiedBaselineAdapter.""" if self._adapter is None: from openadapt_ml.baselines import ( - BaselineConfig, TrackConfig, UnifiedBaselineAdapter, ) diff --git a/openadapt_ml/benchmarks/azure.py b/openadapt_ml/benchmarks/azure.py index f151423..a3d81eb 100644 --- a/openadapt_ml/benchmarks/azure.py +++ b/openadapt_ml/benchmarks/azure.py @@ -30,13 +30,11 @@ import json import logging -import os -import tempfile import time from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable +from typing import Callable from openadapt_ml.benchmarks.agent import BenchmarkAgent from openadapt_ml.benchmarks.base import BenchmarkResult, BenchmarkTask @@ -532,15 +530,15 @@ def run_evaluation( # Provision VMs in parallel print(f"[2/4] Provisioning {num_workers} Azure VM(s)... (this takes 3-5 minutes)") self._provision_workers(workers) - print(f" VM(s) ready") + print(" VM(s) ready") # Submit jobs to workers - print(f"[3/4] Submitting evaluation jobs...") + print("[3/4] Submitting evaluation jobs...") self._submit_worker_jobs(workers, task_batches, agent, max_steps_per_task, timeout_hours) - print(f" Jobs submitted") + print(" Jobs submitted") # Wait for completion and collect results - print(f"[4/4] Waiting for workers to complete...") + print("[4/4] Waiting for workers to complete...") results = self._wait_and_collect_results(workers, on_worker_complete) self._current_run.status = "completed" diff --git a/openadapt_ml/benchmarks/trace_export.py b/openadapt_ml/benchmarks/trace_export.py index 3e93c29..b0f9a40 100644 --- a/openadapt_ml/benchmarks/trace_export.py +++ b/openadapt_ml/benchmarks/trace_export.py @@ -51,7 +51,6 @@ from pathlib import Path from typing import Any, Literal -from openadapt_ml.benchmarks.data_collection import ExecutionStep from openadapt_ml.benchmarks.viewer import ( load_benchmark_metadata, load_benchmark_summary, @@ -122,7 +121,7 @@ def export(self) -> list[Episode]: """ # Load benchmark data metadata = load_benchmark_metadata(self.benchmark_dir) - summary = load_benchmark_summary(self.benchmark_dir) + load_benchmark_summary(self.benchmark_dir) tasks = load_task_results(self.benchmark_dir) logger.info( diff --git a/openadapt_ml/benchmarks/waa.py b/openadapt_ml/benchmarks/waa.py index 9f2f94d..812f157 100644 --- a/openadapt_ml/benchmarks/waa.py +++ b/openadapt_ml/benchmarks/waa.py @@ -717,7 +717,7 @@ def _mock_observation(self) -> BenchmarkObservation: def _generate_mock_screenshot(self, path: Path) -> None: """Generate a simple mock screenshot image.""" try: - from PIL import Image, ImageDraw, ImageFont + from PIL import Image, ImageDraw # Create a simple gray image with some UI elements img = Image.new("RGB", (1920, 1200), color=(240, 240, 240)) diff --git a/openadapt_ml/benchmarks/waa_deploy/api_agent.py b/openadapt_ml/benchmarks/waa_deploy/api_agent.py index 9eddfb6..98a2353 100644 --- a/openadapt_ml/benchmarks/waa_deploy/api_agent.py +++ b/openadapt_ml/benchmarks/waa_deploy/api_agent.py @@ -43,7 +43,7 @@ import os import re from io import BytesIO -from typing import Any, Dict, List +from typing import Dict, List from PIL import Image diff --git a/openadapt_ml/benchmarks/waa_live.py b/openadapt_ml/benchmarks/waa_live.py index 51a3e70..2484d38 100644 --- a/openadapt_ml/benchmarks/waa_live.py +++ b/openadapt_ml/benchmarks/waa_live.py @@ -25,10 +25,9 @@ from __future__ import annotations import base64 -import io import logging import time -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any import requests diff --git a/openadapt_ml/cloud/azure_inference.py b/openadapt_ml/cloud/azure_inference.py index 5d7db06..aa50bcd 100644 --- a/openadapt_ml/cloud/azure_inference.py +++ b/openadapt_ml/cloud/azure_inference.py @@ -144,7 +144,7 @@ def submit_checkpoint( blob_name = f"checkpoints/epoch_{epoch}/{checkpoint_path.name}" logger.info(f"Uploading checkpoint to {blob_name}...") - checkpoint_blob_client = self.blob_service.get_blob_client( + self.blob_service.get_blob_client( container=self.checkpoints_container, blob=blob_name ) @@ -415,7 +415,7 @@ def main(): if args.command == "inference-submit": # Submit checkpoint for inference - print(f"Submitting checkpoint for inference...") + print("Submitting checkpoint for inference...") job = queue.submit_checkpoint( checkpoint_path=args.checkpoint, capture_path=args.capture, diff --git a/openadapt_ml/cloud/lambda_labs.py b/openadapt_ml/cloud/lambda_labs.py index cd331fd..27c18a4 100644 --- a/openadapt_ml/cloud/lambda_labs.py +++ b/openadapt_ml/cloud/lambda_labs.py @@ -54,8 +54,6 @@ def start_dashboard_server(output_dir: Path, port: int = DEFAULT_SERVER_PORT) -> Returns: (process, url): The server process and the dashboard URL """ - import webbrowser - import threading # Start simple HTTP server in background thread server_proc = subprocess.Popen( @@ -714,7 +712,7 @@ def download_results( if result.returncode == 0: print(" Training logs downloaded to training_output_lambda/") else: - print(f" Warning: Failed to download logs") + print(" Warning: Failed to download logs") success = False # Download checkpoint @@ -730,7 +728,7 @@ def download_results( if result.returncode == 0: print(" Checkpoint downloaded to checkpoints_lambda/") else: - print(f" Warning: Failed to download checkpoint (may not exist yet)") + print(" Warning: Failed to download checkpoint (may not exist yet)") # Regenerate all dashboards with static navigation and correct status if include_logs: @@ -755,7 +753,7 @@ def get_training_status(self, instance: Instance) -> dict: try: import json return json.loads(result.stdout.strip()) - except: + except (json.JSONDecodeError, ValueError): return {} @@ -797,10 +795,10 @@ def main(): subparsers = parser.add_subparsers(dest="command", help="Command") # List instances command - list_parser = subparsers.add_parser("list", help="List available instance types") + subparsers.add_parser("list", help="List available instance types") # Status command - status_parser = subparsers.add_parser("status", help="Show running instances") + subparsers.add_parser("status", help="Show running instances") # Launch command launch_parser = subparsers.add_parser("launch", help="Launch a GPU instance") @@ -836,7 +834,7 @@ def main(): rsync_parser.add_argument("--delete", action="store_true", help="Delete extraneous files from dest") # Setup command - setup_parser = subparsers.add_parser("setup", help="Set up SSH key for Lambda Labs") + subparsers.add_parser("setup", help="Set up SSH key for Lambda Labs") # Train command - full automated training pipeline train_parser = subparsers.add_parser("train", help="Run training on Lambda GPU") @@ -942,7 +940,6 @@ def main(): print("Available GPU instances:\n") types = client.list_instance_types() for t in types: - avail = "available" if t.available_regions else "no capacity" print(f" {t}") print(f"\nTotal: {len(types)} instance types") print("\nLaunch with: python -m openadapt_ml.cloud.lambda_labs launch --type ") @@ -968,7 +965,7 @@ def main(): ssh_key_names=[ssh_key], name=args.name, ) - print(f"\nInstance launched!") + print("\nInstance launched!") print(f" ID: {instance.id}") print(f" IP: {instance.ip}") print(f" Type: {instance.instance_type}") @@ -1056,7 +1053,6 @@ def main(): instance = None start_time = time_module.time() - launched_new = False training_completed = False # Track if training actually finished # Instance pricing (approximate $/hr) @@ -1091,7 +1087,6 @@ def main(): name="openadapt-training", ) print(f"Instance launched: {instance.id[:8]}... at {instance.ip}") - launched_new = True price_per_hour = INSTANCE_PRICES.get(instance.instance_type, 1.00) print(f" Instance type: {instance.instance_type} (~${price_per_hour:.2f}/hr)") @@ -1184,7 +1179,7 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, # Upload capture if provided remote_capture = None if args.capture: - setup_logs.append(f"Uploading capture data...") + setup_logs.append("Uploading capture data...") update_dashboard("installing", setup_logs) if client.upload_capture(instance, args.capture, "~/capture"): remote_capture = "~/capture" @@ -1207,7 +1202,7 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print("Starting training...") print("=" * 50 + "\n") - proc = client.run_training( + client.run_training( instance, config=args.config, capture=remote_capture, @@ -1342,7 +1337,7 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, if result.returncode == 0: print(f" Comparison generated: {output_name}") else: - print(f" Warning: Comparison generation failed") + print(" Warning: Comparison generation failed") if result.stderr: print(f" Error: {result.stderr}") else: @@ -1362,7 +1357,7 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print(f"\nInstance still running: {instance.ip}") print(f" Current cost: ~${cost:.2f}") if not training_completed: - print(f" (Not terminating - training did not complete successfully)") + print(" (Not terminating - training did not complete successfully)") print(f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}") elif args.command == "train-status": @@ -1977,13 +1972,13 @@ def update_dashboard(status): return total_steps = len(losses) - epochs = sorted(set(l["epoch"] for l in losses)) + epochs = sorted(set(loss_entry["epoch"] for loss_entry in losses)) total_epochs = data.get("total_epochs", 5) - min_loss = min(l["loss"] for l in losses) + min_loss = min(loss_entry["loss"] for loss_entry in losses) current_loss = losses[-1]["loss"] print(f"\n{'='*50}") - print(f"TRAINING STATUS") + print("TRAINING STATUS") print(f"{'='*50}") print(f"Steps: {total_steps}") print(f"Epochs: {max(epochs)+1}/{total_epochs}") @@ -1999,17 +1994,17 @@ def update_dashboard(status): is_running = int(proc_result.stdout.strip()) > 0 if is_running: - print(f"Status: RUNNING") + print("Status: RUNNING") else: - print(f"Status: STOPPED") + print("Status: STOPPED") # Early stopping analysis window = min(args.window, len(losses)) if window < 2: print("\nNot enough data for early stopping analysis.") else: - recent_losses = [l["loss"] for l in losses[-window:]] - older_losses = [l["loss"] for l in losses[-window*2:-window]] if len(losses) >= window*2 else [l["loss"] for l in losses[:window]] + recent_losses = [loss_entry["loss"] for loss_entry in losses[-window:]] + older_losses = [loss_entry["loss"] for loss_entry in losses[-window*2:-window]] if len(losses) >= window*2 else [loss_entry["loss"] for loss_entry in losses[:window]] recent_avg = sum(recent_losses) / len(recent_losses) older_avg = sum(older_losses) / len(older_losses) if older_losses else recent_avg @@ -2027,14 +2022,14 @@ def update_dashboard(status): should_stop = improvement < args.threshold and loss_variance < 0.1 if should_stop: - print(f"\n⚠️ EARLY STOPPING RECOMMENDED") + print("\n⚠️ EARLY STOPPING RECOMMENDED") print(f" Loss has plateaued (improvement < {args.threshold*100}%)") if not is_running: - print(f" (Training already stopped)") + print(" (Training already stopped)") else: - print(f"\n To stop: uv run python -m openadapt_ml.cloud.lambda_labs kill") + print("\n To stop: uv run python -m openadapt_ml.cloud.lambda_labs kill") else: - print(f"\n✓ Training still improving, continue.") + print("\n✓ Training still improving, continue.") # Time estimate if is_running and len(losses) >= 2: @@ -2046,7 +2041,7 @@ def update_dashboard(status): eta_mins = eta_seconds / 60 print(f"\n{'='*50}") - print(f"TIME ESTIMATE") + print("TIME ESTIMATE") print(f"{'='*50}") print(f"Remaining epochs: {remaining_epochs}") print(f"Est. remaining steps: {remaining_steps:.0f}") @@ -2202,7 +2197,6 @@ def update_dashboard(status): # Start web server for live dashboard with stop button support import http.server import socketserver - import threading import time as time_module from pathlib import Path @@ -2421,7 +2415,7 @@ def log_message(self, format, *args): screenshots_link.symlink_to(screenshots_dir) print(f" Linked: {screenshots_link} -> {screenshots_dir}") break - except Exception as e: + except Exception: pass # Silently continue if auto-link fails print(f"Regenerating viewer from {output_dir}...") @@ -2435,7 +2429,7 @@ def log_message(self, format, *args): target = output_dir / "viewer.html" print(f"\nGenerated: {target.absolute()}") - print(f"View with: uv run python -m openadapt_ml.cloud.lambda_labs serve --open") + print("View with: uv run python -m openadapt_ml.cloud.lambda_labs serve --open") if args.open: subprocess.run(["open", str(target)], capture_output=True) diff --git a/openadapt_ml/cloud/local.py b/openadapt_ml/cloud/local.py index 42abab9..5ba9a44 100644 --- a/openadapt_ml/cloud/local.py +++ b/openadapt_ml/cloud/local.py @@ -27,7 +27,6 @@ import json import os import shutil -import signal import socketserver import subprocess import sys @@ -274,7 +273,7 @@ def cmd_status(args: argparse.Namespace) -> int: print(f"Output: {current_dir}") if status.get("epoch"): - print(f"\nProgress:") + print("\nProgress:") print(f" Epoch: {status['epoch']}") print(f" Step: {status['step']}") if status.get("loss"): @@ -381,7 +380,7 @@ def cmd_check(args: argparse.Namespace) -> int: min_loss = min(losses) max_loss = max(losses) - print(f"\nLoss progression:") + print("\nLoss progression:") print(f" First: {first_loss:.4f}") print(f" Last: {last_loss:.4f}") print(f" Min: {min_loss:.4f}") @@ -394,7 +393,7 @@ def cmd_check(args: argparse.Namespace) -> int: recent_avg = sum(recent) / len(recent) recent_std = (sum((x - recent_avg) ** 2 for x in recent) / len(recent)) ** 0.5 - print(f"\nRecent stability (last 10 steps):") + print("\nRecent stability (last 10 steps):") print(f" Avg loss: {recent_avg:.4f}") print(f" Std dev: {recent_std:.4f}") @@ -542,7 +541,7 @@ def run_benchmark(): print(f"Stderr: {result.stderr}") if result.returncode == 0: - print(f"✅ Benchmark complete. Regenerating viewer...") + print("✅ Benchmark complete. Regenerating viewer...") progress_file.write_text(json.dumps({ "status": "complete", "provider": provider, @@ -873,7 +872,6 @@ def _get_vm_detailed_metadata(self, vm_ip: str, container_name: str, logs: str, dict with disk_usage_gb, memory_usage_mb, setup_script_phase, probe_response, qmp_connected, dependencies """ import subprocess - import re metadata = { "disk_usage_gb": None, @@ -1049,8 +1047,6 @@ def _parse_dependencies_from_logs(self, logs: str, phase: str) -> list[dict]: def _fetch_background_tasks(self): """Fetch status of all background tasks: Azure VM, Docker containers, benchmarks.""" import subprocess - from datetime import datetime - import time tasks = [] @@ -1144,7 +1140,6 @@ def _fetch_background_tasks(self): if len(parts) >= 3: container_name, status, image = parts[0], parts[1], parts[2] # Parse "Up X minutes" to determine if healthy - is_healthy = "Up" in status # Check for Windows VM specifically if "windows" in image.lower() or container_name == "winarena": @@ -1251,7 +1246,7 @@ def _fetch_background_tasks(self): "dependencies": vm_metadata["dependencies"], } }) - except Exception as e: + except Exception: # SSH failed, VM might still be starting pass @@ -1443,7 +1438,6 @@ def _get_current_run(self) -> dict: - elapsed_minutes: int """ import subprocess - from datetime import datetime import re result = { @@ -1603,7 +1597,7 @@ async def _detect_running_benchmark(self, vm_ip: str, container_name: str = "win if step_match: result["progress"]["current_step"] = int(step_match.group(1)) - except Exception as e: + except Exception: # SSH or parsing failed - leave defaults pass @@ -2052,7 +2046,7 @@ def run(): print(f"[Benchmark] Stderr: {result.stderr}") if result.returncode == 0: - print(f"[Benchmark] Complete. Regenerating viewer...") + print("[Benchmark] Complete. Regenerating viewer...") progress_file.write_text(json.dumps({ "status": "complete", "model": model, @@ -2173,7 +2167,7 @@ def cmd_viewer(args: argparse.Namespace) -> int: dashboard_html = generate_training_dashboard(state, config) (current_dir / "dashboard.html").write_text(dashboard_html) - print(f" Regenerated: dashboard.html") + print(" Regenerated: dashboard.html") # Generate unified viewer using consolidated function viewer_path = generate_unified_viewer_from_output_dir(current_dir) diff --git a/openadapt_ml/cloud/ssh_tunnel.py b/openadapt_ml/cloud/ssh_tunnel.py index cd2336a..5c4ac92 100644 --- a/openadapt_ml/cloud/ssh_tunnel.py +++ b/openadapt_ml/cloud/ssh_tunnel.py @@ -51,9 +51,8 @@ import socket import subprocess import time -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path -from typing import Any logger = logging.getLogger(__name__) diff --git a/openadapt_ml/datasets/next_action.py b/openadapt_ml/datasets/next_action.py index c9f6f2b..ebd7dbc 100644 --- a/openadapt_ml/datasets/next_action.py +++ b/openadapt_ml/datasets/next_action.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Any, Dict, List -import torch from torch.utils.data import Dataset from openadapt_ml.schema import Action, ActionType, Episode, Step, UIElement @@ -269,8 +268,6 @@ def _generate_thought_for_step( actions back to the stated objective. """ - action = step.action - t = action.type if scenario == "registration": return _generate_registration_thought(step_index, step, goal, total_steps) diff --git a/openadapt_ml/evals/grounding.py b/openadapt_ml/evals/grounding.py index 9768dff..e189078 100644 --- a/openadapt_ml/evals/grounding.py +++ b/openadapt_ml/evals/grounding.py @@ -20,6 +20,7 @@ from PIL import Image from openadapt_ml.grounding.base import GroundingModule, RegionCandidate + from openadapt_ml.schemas.sessions import Episode @dataclass @@ -212,7 +213,7 @@ def evaluate_grounder_on_episode( """ from PIL import Image - from openadapt_ml.schema import Episode, ActionType + from openadapt_ml.schema import ActionType test_cases = [] diff --git a/openadapt_ml/evals/trajectory_matching.py b/openadapt_ml/evals/trajectory_matching.py index bdd49db..8550300 100644 --- a/openadapt_ml/evals/trajectory_matching.py +++ b/openadapt_ml/evals/trajectory_matching.py @@ -233,7 +233,6 @@ def evaluate_episode( coord_error: Optional[float] = None click_hit = False - bbox_hit = False element_hit = False # Helper to get element index - check element.element_id or raw field @@ -273,7 +272,6 @@ def _get_element_index(action: Action) -> Optional[int]: bbox_total += 1 if in_bbox: bbox_hits += 1 - bbox_hit = True # Full step correctness: type matches AND element/coord match for relevant actions if type_match: diff --git a/openadapt_ml/experiments/demo_prompt/format_demo.py b/openadapt_ml/experiments/demo_prompt/format_demo.py index 84ac91a..a64471d 100644 --- a/openadapt_ml/experiments/demo_prompt/format_demo.py +++ b/openadapt_ml/experiments/demo_prompt/format_demo.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from openadapt_ml.schema import Action, ActionType, Episode, Step + from openadapt_ml.schema import Action, Episode, Step def format_action(action: "Action") -> str: diff --git a/openadapt_ml/experiments/demo_prompt/run_experiment.py b/openadapt_ml/experiments/demo_prompt/run_experiment.py index 6e1af4d..13cf853 100644 --- a/openadapt_ml/experiments/demo_prompt/run_experiment.py +++ b/openadapt_ml/experiments/demo_prompt/run_experiment.py @@ -8,14 +8,12 @@ import argparse import base64 import json -import sys from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Any from openadapt_ml.experiments.demo_prompt.format_demo import ( - format_episode_as_demo, format_episode_verbose, generate_length_matched_control, get_demo_screenshot_paths, diff --git a/openadapt_ml/experiments/representation_shootout/conditions.py b/openadapt_ml/experiments/representation_shootout/conditions.py index 18ed8da..bde4c46 100644 --- a/openadapt_ml/experiments/representation_shootout/conditions.py +++ b/openadapt_ml/experiments/representation_shootout/conditions.py @@ -14,7 +14,6 @@ from __future__ import annotations import logging -import math from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any diff --git a/openadapt_ml/experiments/representation_shootout/config.py b/openadapt_ml/experiments/representation_shootout/config.py index 90c17cf..a1a8817 100644 --- a/openadapt_ml/experiments/representation_shootout/config.py +++ b/openadapt_ml/experiments/representation_shootout/config.py @@ -142,13 +142,13 @@ def coords_cues(cls, **kwargs: Any) -> ConditionConfig: ) @classmethod - def marks(cls, **kwargs: Any) -> ConditionConfig: + def marks(cls, **kwargs: Any) -> ConditionConfig: # noqa: F811 """Create Condition C (Marks/Element IDs) config.""" - marks = kwargs.pop("marks", None) or MarksConfig() + marks_config = kwargs.pop("marks", None) or MarksConfig() return cls( name=ConditionName.MARKS, output_format=OutputFormat.ELEMENT_ID, - marks=marks, + marks=marks_config, loss_type="cross_entropy", **kwargs, ) diff --git a/openadapt_ml/experiments/representation_shootout/runner.py b/openadapt_ml/experiments/representation_shootout/runner.py index 852d5a4..db18f83 100644 --- a/openadapt_ml/experiments/representation_shootout/runner.py +++ b/openadapt_ml/experiments/representation_shootout/runner.py @@ -25,13 +25,10 @@ import json import logging import sys -from dataclasses import asdict from datetime import datetime from pathlib import Path -from typing import Any from openadapt_ml.experiments.representation_shootout.conditions import ( - ActionHistory, ConditionBase, Observation, UIElement, @@ -39,7 +36,6 @@ create_condition, ) from openadapt_ml.experiments.representation_shootout.config import ( - ConditionConfig, ConditionName, DriftConfig, ExperimentConfig, @@ -631,7 +627,7 @@ def main() -> int: try: runner = ExperimentRunner(config) if args.data: - samples = runner.load_samples(args.data) + runner.load_samples(args.data) recommendation = runner.run() runner.print_summary(recommendation) return 0 diff --git a/openadapt_ml/experiments/waa_demo/runner.py b/openadapt_ml/experiments/waa_demo/runner.py index 7fc1713..27f1da9 100644 --- a/openadapt_ml/experiments/waa_demo/runner.py +++ b/openadapt_ml/experiments/waa_demo/runner.py @@ -26,7 +26,6 @@ from typing import TYPE_CHECKING, Any from openadapt_ml.experiments.waa_demo.demos import ( - DEMOS, format_demo_for_prompt, get_complete_demos, get_demo, @@ -34,8 +33,6 @@ ) from openadapt_ml.experiments.waa_demo.tasks import ( TASKS, - WATask, - get_manual_tasks, get_recorded_tasks, get_task, ) diff --git a/openadapt_ml/export/parquet.py b/openadapt_ml/export/parquet.py index a8977e1..296ba67 100644 --- a/openadapt_ml/export/parquet.py +++ b/openadapt_ml/export/parquet.py @@ -7,7 +7,6 @@ from __future__ import annotations import json -from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/openadapt_ml/grounding/detector.py b/openadapt_ml/grounding/detector.py index 1e13f38..8af6c31 100644 --- a/openadapt_ml/grounding/detector.py +++ b/openadapt_ml/grounding/detector.py @@ -20,7 +20,7 @@ from openadapt_ml.grounding.base import GroundingModule, RegionCandidate if TYPE_CHECKING: - from PIL import Image, ImageDraw, ImageFont + from PIL import Image class GeminiGrounder(GroundingModule): diff --git a/openadapt_ml/ingest/__init__.py b/openadapt_ml/ingest/__init__.py index ec40c57..f11a8b2 100644 --- a/openadapt_ml/ingest/__init__.py +++ b/openadapt_ml/ingest/__init__.py @@ -27,7 +27,7 @@ # Conditionally export capture functions if openadapt-capture is installed try: - from openadapt_ml.ingest.capture import ( + from openadapt_ml.ingest.capture import ( # noqa: F401 capture_to_episode, capture_to_session, load_captures_as_sessions, diff --git a/openadapt_ml/ingest/capture.py b/openadapt_ml/ingest/capture.py index e46aa75..5a4f4f3 100644 --- a/openadapt_ml/ingest/capture.py +++ b/openadapt_ml/ingest/capture.py @@ -6,7 +6,6 @@ from __future__ import annotations -import uuid from pathlib import Path from typing import TYPE_CHECKING @@ -101,7 +100,7 @@ def capture_to_episode( """ try: from openadapt_capture import Capture - from openadapt_capture.events import ( + from openadapt_capture.events import ( # noqa: F401 EventType, KeyTypeEvent, MouseClickEvent, diff --git a/openadapt_ml/ingest/loader.py b/openadapt_ml/ingest/loader.py index 33fa000..5662d6f 100644 --- a/openadapt_ml/ingest/loader.py +++ b/openadapt_ml/ingest/loader.py @@ -8,9 +8,8 @@ import json from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Union -from pydantic import ValidationError from openadapt_ml.schema import Action, ActionType, Episode, Observation, Step diff --git a/openadapt_ml/ingest/synthetic.py b/openadapt_ml/ingest/synthetic.py index 0243794..4a95d77 100644 --- a/openadapt_ml/ingest/synthetic.py +++ b/openadapt_ml/ingest/synthetic.py @@ -743,7 +743,7 @@ def _draw_registration_screen( layout = _compute_registration_layout(jitter=jitter) label_x = 180 - box_w, box_h = 400, 36 + _box_w, _box_h = 400, 36 start_y = 100 field_spacing = 70 diff --git a/openadapt_ml/models/qwen_vl.py b/openadapt_ml/models/qwen_vl.py index 8683134..044a575 100644 --- a/openadapt_ml/models/qwen_vl.py +++ b/openadapt_ml/models/qwen_vl.py @@ -2,7 +2,6 @@ from typing import Any, Dict, List, Optional -from PIL import Image import torch from peft import LoraConfig, PeftModel, get_peft_model from transformers import AutoProcessor, Qwen3VLForConditionalGeneration, Qwen2_5_VLForConditionalGeneration diff --git a/openadapt_ml/retrieval/demo_retriever.py b/openadapt_ml/retrieval/demo_retriever.py index 58bb172..6714f16 100644 --- a/openadapt_ml/retrieval/demo_retriever.py +++ b/openadapt_ml/retrieval/demo_retriever.py @@ -32,7 +32,6 @@ from __future__ import annotations -import hashlib import json import logging from dataclasses import dataclass, field diff --git a/openadapt_ml/retrieval/embeddings.py b/openadapt_ml/retrieval/embeddings.py index 5582795..5db6488 100644 --- a/openadapt_ml/retrieval/embeddings.py +++ b/openadapt_ml/retrieval/embeddings.py @@ -25,9 +25,9 @@ import re from abc import ABC, abstractmethod from collections import Counter -from math import log, sqrt +from math import log from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) diff --git a/openadapt_ml/runtime/policy.py b/openadapt_ml/runtime/policy.py index 6c4b0cf..af46efd 100644 --- a/openadapt_ml/runtime/policy.py +++ b/openadapt_ml/runtime/policy.py @@ -3,7 +3,7 @@ import json import re from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, Optional, Tuple from PIL import Image diff --git a/openadapt_ml/runtime/safety_gate.py b/openadapt_ml/runtime/safety_gate.py index d3d891d..6c72c26 100644 --- a/openadapt_ml/runtime/safety_gate.py +++ b/openadapt_ml/runtime/safety_gate.py @@ -37,7 +37,7 @@ import re from dataclasses import dataclass, field from enum import Enum -from typing import Any, Optional +from typing import Optional from openadapt_ml.schema import Action, ActionType, Observation, Step diff --git a/openadapt_ml/scripts/compare.py b/openadapt_ml/scripts/compare.py index aac7e23..67b7c1b 100644 --- a/openadapt_ml/scripts/compare.py +++ b/openadapt_ml/scripts/compare.py @@ -17,7 +17,7 @@ from typing import Any from openadapt_ml.ingest.capture import capture_to_episode -from openadapt_ml.schema import Episode, Step, ActionType +from openadapt_ml.schema import Episode, ActionType from openadapt_ml.datasets.next_action import SYSTEM_PROMPT, format_action from openadapt_ml.training.trainer import _get_shared_header_css, _generate_shared_header_html diff --git a/openadapt_ml/scripts/eval_policy.py b/openadapt_ml/scripts/eval_policy.py index 9799a89..153b026 100644 --- a/openadapt_ml/scripts/eval_policy.py +++ b/openadapt_ml/scripts/eval_policy.py @@ -3,11 +3,11 @@ import argparse import json from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional import yaml -from openadapt_ml.datasets.next_action import build_next_action_sft_samples, parse_action_som +from openadapt_ml.datasets.next_action import build_next_action_sft_samples from openadapt_ml.evals.trajectory_matching import evaluate_policy_on_episodes from openadapt_ml.ingest.synthetic import generate_synthetic_episodes from openadapt_ml.models.dummy_adapter import DummyAdapter diff --git a/openadapt_ml/scripts/make_gif.py b/openadapt_ml/scripts/make_gif.py index f7d5aad..c6fc6ea 100644 --- a/openadapt_ml/scripts/make_gif.py +++ b/openadapt_ml/scripts/make_gif.py @@ -4,7 +4,7 @@ import glob import os from pathlib import Path -from typing import Dict, List, Optional +from typing import List, Optional from PIL import Image, ImageDraw, ImageFont diff --git a/openadapt_ml/training/stub_provider.py b/openadapt_ml/training/stub_provider.py index 7d3449f..c086bc4 100644 --- a/openadapt_ml/training/stub_provider.py +++ b/openadapt_ml/training/stub_provider.py @@ -2,7 +2,6 @@ import json import random -import sys import time from datetime import datetime from pathlib import Path diff --git a/openadapt_ml/training/trainer.py b/openadapt_ml/training/trainer.py index d2b7e4b..11ba12e 100644 --- a/openadapt_ml/training/trainer.py +++ b/openadapt_ml/training/trainer.py @@ -4,9 +4,9 @@ import time from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Dict, List -from openadapt_ml.schema import Episode, Step, Action, ActionType +from openadapt_ml.schema import ActionType from openadapt_ml.training.shared_ui import ( get_shared_header_css as _get_shared_header_css, generate_shared_header_html as _generate_shared_header_html, @@ -386,10 +386,10 @@ def generate_training_dashboard(state: TrainingState, config: TrainingConfig) -> # Calculate stats if state.losses: - min_loss = min(l["loss"] for l in state.losses) - avg_loss = sum(l["loss"] for l in state.losses) / len(state.losses) + min_loss = min(loss_entry["loss"] for loss_entry in state.losses) + sum(loss_entry["loss"] for loss_entry in state.losses) / len(state.losses) recent_losses = state.losses[-10:] if len(state.losses) >= 10 else state.losses - recent_avg = sum(l["loss"] for l in recent_losses) / len(recent_losses) + recent_avg = sum(loss_entry["loss"] for loss_entry in recent_losses) / len(recent_losses) # Calculate step times step_times = [] for i in range(1, len(state.losses)): @@ -397,16 +397,16 @@ def generate_training_dashboard(state: TrainingState, config: TrainingConfig) -> avg_step_time = sum(step_times) / len(step_times) if step_times else 0 # Loss by epoch epoch_losses: dict = {} - for l in state.losses: - ep = l["epoch"] + for loss_entry in state.losses: + ep = loss_entry["epoch"] if ep not in epoch_losses: epoch_losses[ep] = [] - epoch_losses[ep].append(l["loss"]) + epoch_losses[ep].append(loss_entry["loss"]) epoch_avg = {ep: sum(losses)/len(losses) for ep, losses in epoch_losses.items()} # Estimate ETA # Steps per epoch = steps in completed epochs / completed epochs completed_epochs = state.epoch - steps_in_completed = sum(1 for l in state.losses if l["epoch"] < completed_epochs) + steps_in_completed = sum(1 for loss_entry in state.losses if loss_entry["epoch"] < completed_epochs) if completed_epochs > 0 and steps_in_completed > 0: steps_per_epoch = steps_in_completed / completed_epochs else: @@ -420,7 +420,7 @@ def generate_training_dashboard(state: TrainingState, config: TrainingConfig) -> # Check if training is complete (all steps done) is_training_complete = remaining_steps == 0 and len(state.losses) > 0 else: - min_loss = avg_loss = recent_avg = avg_step_time = 0.0 + min_loss = recent_avg = avg_step_time = 0.0 epoch_avg = {} eta_seconds = 0 steps_per_epoch = 0 @@ -431,10 +431,9 @@ def generate_training_dashboard(state: TrainingState, config: TrainingConfig) -> epoch_avg_json = json.dumps(list(epoch_avg.items())) # Generate comparison viewer preview if capture path available - comparison_viewer_path = "" if state.capture_path: try: - from openadapt_ml.scripts.compare import generate_comparison_html, generate_comparison_data + from openadapt_ml.scripts.compare import generate_comparison_html from openadapt_ml.ingest.capture import capture_to_episode capture_path = Path(state.capture_path) @@ -469,8 +468,7 @@ def generate_training_dashboard(state: TrainingState, config: TrainingConfig) -> output_dir.mkdir(parents=True, exist_ok=True) comparison_output = output_dir / "comparison_preview.html" generate_comparison_html(capture_path, episode, comparison_data, comparison_output) - comparison_viewer_path = str(comparison_output.name) # Relative path - except Exception as e: + except Exception: pass # Fail silently if comparison viewer can't be generated html = f''' diff --git a/openadapt_ml/training/trl_trainer.py b/openadapt_ml/training/trl_trainer.py index 016dec8..24ac1ef 100644 --- a/openadapt_ml/training/trl_trainer.py +++ b/openadapt_ml/training/trl_trainer.py @@ -271,7 +271,7 @@ def train_with_trl( ) print(f"\n{'='*50}") - print(f"Starting training:") + print("Starting training:") print(f" Model: {config.model_name}") print(f" Samples: {len(trl_samples)}") print(f" Epochs: {config.num_epochs}") diff --git a/openadapt_ml/training/viewer.py b/openadapt_ml/training/viewer.py index f2b591b..b0abf92 100644 --- a/openadapt_ml/training/viewer.py +++ b/openadapt_ml/training/viewer.py @@ -33,7 +33,7 @@ def _copy_transcript_and_audio(capture_path: Path | None, output_dir: Path) -> N transcript_dst = output_dir / "transcript.json" if transcript_src.exists() and not transcript_dst.exists(): shutil.copy2(transcript_src, transcript_dst) - print(f" Copied transcript.json from capture") + print(" Copied transcript.json from capture") # Convert audio to mp3 if it exists (ffmpeg required) audio_dst = output_dir / "audio.mp3" @@ -221,7 +221,7 @@ def generate_unified_viewer_from_output_dir(output_dir: Path) -> Path | None: has_predictions = any(p.get("predicted_action") for p in predictions) if has_predictions and "Preview" not in predictions_by_checkpoint: predictions_by_checkpoint["Preview"] = predictions - print(f" Loaded predictions from comparison_preview.html") + print(" Loaded predictions from comparison_preview.html") except Exception as e: print(f" Warning: Could not extract data from comparison_preview.html: {e}") @@ -288,7 +288,7 @@ def _generate_unified_viewer_from_extracted_data( capture_modified_time_json = json.dumps(capture_modified_time) # Find first image to get dimensions (for display) - first_image_path = base_data[0].get("image_path", "") if base_data else "" + base_data[0].get("image_path", "") if base_data else "" html = f''' @@ -2929,7 +2929,8 @@ def _add_static_nav_to_comparison( # Build nav links if not provided if nav_links is None: - nav_links = _build_nav_links() + # Default nav links (empty for now, can be customized by caller) + nav_links = [] # Build nav HTML with active state for current file # NOTE: No "Dashboards:" label to match training dashboard nav diff --git a/openadapt_ml/training/viewer_migration_example.py b/openadapt_ml/training/viewer_migration_example.py index 522d205..45d1a72 100644 --- a/openadapt_ml/training/viewer_migration_example.py +++ b/openadapt_ml/training/viewer_migration_example.py @@ -11,11 +11,9 @@ from openadapt_viewer.builders import PageBuilder from openadapt_ml.training.viewer_components import ( - screenshot_with_predictions, training_metrics, playback_controls, generate_comparison_summary, - correctness_badge, ) diff --git a/viewer_migration_example.html b/viewer_migration_example.html new file mode 100644 index 0000000..5c74328 --- /dev/null +++ b/viewer_migration_example.html @@ -0,0 +1,755 @@ + + + + + + Viewer Migration Example + + + + + + +
+
+
+

Viewer Component Migration Example

+

Demonstrating Phase 1 Foundation

+
+
+ + + + +
+
+
+ + + + +
+ +
+

Training Metrics

+
+
+ +
Epoch
+
3
+ +
+
+ +
Loss
+
0.0450
+ +
+
+ +
Accuracy
+
95.00%
+ +
+
+ +
Elapsed
+
1h 0m 0s
+ +
+
+ +
LR
+
1.00e-04
+ +
+
+
+ + +
+

Model Comparison Summary

+
+
+ +
Model
+
qwen3-vl-2b
+ +
+
+ +
Total Steps
+
20
+ +
+
+ +
Correct
+
18
+ +
+
+ +
Incorrect
+
2
+ +
+
+ +
Accuracy
+
90.0%
+ +
+
+
+ + +
+

Playback Controls

+
+ + + + + + + + + + + + + + + + + + + + Step 1 of 20 + + + + + +
+
+ +
+ + + + + \ No newline at end of file From 2500689c5bcb58bbc38d6c5184c8d42716c25ce0 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sat, 17 Jan 2026 10:10:37 -0500 Subject: [PATCH 09/16] style: apply ruff formatting to all Python files Applied ruff format to ensure consistent code style across all modules. Co-Authored-By: Claude Sonnet 4.5 --- openadapt_ml/baselines/cli.py | 65 +- openadapt_ml/baselines/parser.py | 77 +- openadapt_ml/baselines/prompts.py | 4 +- openadapt_ml/benchmarks/__init__.py | 10 +- openadapt_ml/benchmarks/agent.py | 23 +- openadapt_ml/benchmarks/azure.py | 30 +- openadapt_ml/benchmarks/base.py | 4 +- openadapt_ml/benchmarks/cli.py | 2773 ++++++++++++----- openadapt_ml/benchmarks/data_collection.py | 22 +- openadapt_ml/benchmarks/live_tracker.py | 54 +- openadapt_ml/benchmarks/runner.py | 22 +- openadapt_ml/benchmarks/trace_export.py | 45 +- openadapt_ml/benchmarks/viewer.py | 12 +- openadapt_ml/benchmarks/vm_monitor.py | 81 +- openadapt_ml/benchmarks/waa.py | 98 +- .../benchmarks/waa_deploy/api_agent.py | 39 +- openadapt_ml/benchmarks/waa_live.py | 67 +- openadapt_ml/cloud/azure_inference.py | 4 +- openadapt_ml/cloud/lambda_labs.py | 977 ++++-- openadapt_ml/cloud/local.py | 1430 ++++++--- openadapt_ml/cloud/ssh_tunnel.py | 87 +- openadapt_ml/datasets/next_action.py | 67 +- openadapt_ml/evals/grounding.py | 8 +- openadapt_ml/evals/plot_eval_metrics.py | 28 +- openadapt_ml/evals/trajectory_matching.py | 65 +- .../experiments/demo_prompt/format_demo.py | 20 +- .../experiments/demo_prompt/run_experiment.py | 40 +- .../representation_shootout/conditions.py | 49 +- .../representation_shootout/config.py | 30 +- .../representation_shootout/evaluator.py | 32 +- .../representation_shootout/runner.py | 20 +- openadapt_ml/experiments/waa_demo/runner.py | 36 +- openadapt_ml/export/parquet.py | 59 +- openadapt_ml/grounding/detector.py | 30 +- openadapt_ml/ingest/__init__.py | 12 +- openadapt_ml/ingest/capture.py | 44 +- openadapt_ml/ingest/loader.py | 8 +- openadapt_ml/ingest/synthetic.py | 287 +- openadapt_ml/models/api_adapter.py | 18 +- openadapt_ml/models/base_adapter.py | 12 +- openadapt_ml/models/providers/__init__.py | 4 +- openadapt_ml/models/providers/anthropic.py | 14 +- openadapt_ml/models/providers/google.py | 8 +- openadapt_ml/models/providers/openai.py | 6 +- openadapt_ml/models/qwen_vl.py | 64 +- openadapt_ml/retrieval/demo_retriever.py | 73 +- openadapt_ml/retrieval/embeddings.py | 13 +- openadapt_ml/retrieval/retriever.py | 4 +- openadapt_ml/runtime/__init__.py | 6 +- openadapt_ml/runtime/policy.py | 21 +- openadapt_ml/runtime/safety_gate.py | 8 +- openadapt_ml/schema/converters.py | 101 +- openadapt_ml/schema/episode.py | 49 +- openadapt_ml/scripts/compare.py | 137 +- openadapt_ml/scripts/demo_policy.py | 5 +- openadapt_ml/scripts/eval_policy.py | 20 +- openadapt_ml/scripts/prepare_synthetic.py | 4 +- openadapt_ml/scripts/train.py | 30 +- openadapt_ml/training/benchmark_viewer.py | 93 +- openadapt_ml/training/shared_ui.py | 14 +- openadapt_ml/training/stub_provider.py | 91 +- openadapt_ml/training/trainer.py | 208 +- openadapt_ml/training/trl_trainer.py | 25 +- openadapt_ml/training/viewer.py | 235 +- openadapt_ml/training/viewer_components.py | 56 +- 65 files changed, 5608 insertions(+), 2470 deletions(-) diff --git a/openadapt_ml/baselines/cli.py b/openadapt_ml/baselines/cli.py index c36ef0e..625fab2 100644 --- a/openadapt_ml/baselines/cli.py +++ b/openadapt_ml/baselines/cli.py @@ -22,35 +22,41 @@ def baselines(): @baselines.command() @click.option( - "--model", "-m", + "--model", + "-m", required=True, type=click.Choice(list(MODELS.keys())), help="Model alias to use", ) @click.option( - "--track", "-t", + "--track", + "-t", type=click.Choice(["A", "B", "C"]), default="A", help="Evaluation track (A=coords, B=ReAct, C=SoM)", ) @click.option( - "--image", "-i", + "--image", + "-i", type=click.Path(exists=True), required=True, help="Screenshot image path", ) @click.option( - "--goal", "-g", + "--goal", + "-g", required=True, help="Task goal/instruction", ) @click.option( - "--output", "-o", + "--output", + "-o", type=click.Path(), help="Output JSON file path", ) @click.option( - "--verbose", "-v", + "--verbose", + "-v", is_flag=True, help="Enable verbose output", ) @@ -121,7 +127,9 @@ def run( click.echo(f"Thought: {action.thought}") else: click.echo(f"Parse Error: {action.parse_error}") - click.echo(f"Raw Response: {action.raw_response[:200] if action.raw_response else 'None'}...") + click.echo( + f"Raw Response: {action.raw_response[:200] if action.raw_response else 'None'}..." + ) # Save output if requested if output: @@ -139,29 +147,34 @@ def run( @baselines.command() @click.option( - "--models", "-m", + "--models", + "-m", required=True, help="Comma-separated model aliases", ) @click.option( - "--track", "-t", + "--track", + "-t", type=click.Choice(["A", "B", "C"]), default="A", help="Evaluation track", ) @click.option( - "--image", "-i", + "--image", + "-i", type=click.Path(exists=True), required=True, help="Screenshot image path", ) @click.option( - "--goal", "-g", + "--goal", + "-g", required=True, help="Task goal/instruction", ) @click.option( - "--output", "-o", + "--output", + "-o", type=click.Path(), help="Output JSON file path", ) @@ -220,23 +233,27 @@ def compare( adapter = UnifiedBaselineAdapter.from_alias(model, track=track_config) action = adapter.predict(screenshot, goal) - results.append({ - "model": model, - "success": action.is_valid, - "action": action.to_dict(), - "error": action.parse_error, - }) + results.append( + { + "model": model, + "success": action.is_valid, + "action": action.to_dict(), + "error": action.parse_error, + } + ) status = "OK" if action.is_valid else "FAILED" click.echo(f" {status}: {action.action_type}") except Exception as e: - results.append({ - "model": model, - "success": False, - "action": None, - "error": str(e), - }) + results.append( + { + "model": model, + "success": False, + "action": None, + "error": str(e), + } + ) click.echo(f" ERROR: {e}") # Summary table diff --git a/openadapt_ml/baselines/parser.py b/openadapt_ml/baselines/parser.py index 0139070..9fe0651 100644 --- a/openadapt_ml/baselines/parser.py +++ b/openadapt_ml/baselines/parser.py @@ -72,9 +72,7 @@ def add_element( ) -> None: """Add an element to the registry.""" eid = int(element_id) if isinstance(element_id, str) else element_id - self.elements[eid] = UIElement( - element_id=eid, role=role, name=name, bbox=bbox - ) + self.elements[eid] = UIElement(element_id=eid, role=role, name=name, bbox=bbox) def get_element(self, element_id: int) -> UIElement | None: """Get element by ID.""" @@ -137,7 +135,9 @@ def process_node(node: dict[str, Any]) -> None: node_id = node.get("id", node.get("node_id", node.get("element_id"))) if node_id is not None: try: - eid = int(str(node_id).replace("e", "").replace("[", "").replace("]", "")) + eid = int( + str(node_id).replace("e", "").replace("[", "").replace("]", "") + ) bbox = node.get("bbox", node.get("bounds")) if bbox and len(bbox) >= 4: registry.add_element( @@ -277,7 +277,9 @@ def to_pyautogui( py = int(self.y * screen_height) return f"pyautogui.click({px}, {py})" elif self.element_id is not None: - return f"# CLICK element {self.element_id} (needs coordinate conversion)" + return ( + f"# CLICK element {self.element_id} (needs coordinate conversion)" + ) elif self.action_type == "type": text = self.text or "" return f"pyautogui.write('{text}')" @@ -444,10 +446,10 @@ def _try_json_parse(self, response: str) -> ParsedAction: """Try to extract and parse JSON from response.""" # Try to find JSON object in response json_patterns = [ - r'```json\s*(\{[^`]*\})\s*```', # Markdown code block - r'```\s*(\{[^`]*\})\s*```', # Plain code block - r'(\{[^{}]*\})', # Simple JSON object - r'(\{[^{}]*\{[^{}]*\}[^{}]*\})', # Nested JSON (max 1 level) + r"```json\s*(\{[^`]*\})\s*```", # Markdown code block + r"```\s*(\{[^`]*\})\s*```", # Plain code block + r"(\{[^{}]*\})", # Simple JSON object + r"(\{[^{}]*\{[^{}]*\}[^{}]*\})", # Nested JSON (max 1 level) ] for pattern in json_patterns: @@ -621,7 +623,7 @@ def _try_pyautogui_parse(self, response: str) -> ParsedAction: """Try to parse PyAutoGUI-style code.""" # pyautogui.click(x, y) click_match = re.search( - r'pyautogui\.click\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)', + r"pyautogui\.click\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)", response, re.IGNORECASE, ) @@ -633,7 +635,7 @@ def _try_pyautogui_parse(self, response: str) -> ParsedAction: # pyautogui.doubleClick(x, y) dclick_match = re.search( - r'pyautogui\.doubleClick\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)', + r"pyautogui\.doubleClick\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)", response, re.IGNORECASE, ) @@ -668,7 +670,7 @@ def _try_pyautogui_parse(self, response: str) -> ParsedAction: # pyautogui.hotkey('key1', 'key2') hotkey_match = re.search( - r'pyautogui\.hotkey\s*\(\s*(.+?)\s*\)', + r"pyautogui\.hotkey\s*\(\s*(.+?)\s*\)", response, re.IGNORECASE, ) @@ -687,7 +689,7 @@ def _try_pyautogui_parse(self, response: str) -> ParsedAction: # pyautogui.scroll(amount) scroll_match = re.search( - r'pyautogui\.scroll\s*\(\s*(-?\d+)\s*\)', + r"pyautogui\.scroll\s*\(\s*(-?\d+)\s*\)", response, re.IGNORECASE, ) @@ -700,13 +702,15 @@ def _try_pyautogui_parse(self, response: str) -> ParsedAction: amount=abs(clicks), ) - return ParsedAction(action_type="unknown", parse_error="No PyAutoGUI pattern matched") + return ParsedAction( + action_type="unknown", parse_error="No PyAutoGUI pattern matched" + ) def _try_regex_parse(self, response: str) -> ParsedAction: """Try regex patterns for function-style actions.""" # CLICK(x, y) - normalized coordinates click_norm = re.search( - r'CLICK\s*\(\s*(0?\.\d+)\s*,\s*(0?\.\d+)\s*\)', + r"CLICK\s*\(\s*(0?\.\d+)\s*,\s*(0?\.\d+)\s*\)", response, re.IGNORECASE, ) @@ -719,7 +723,7 @@ def _try_regex_parse(self, response: str) -> ParsedAction: # CLICK(x, y) - larger numbers (pixels) click_pixel = re.search( - r'CLICK\s*\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)', + r"CLICK\s*\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)", response, re.IGNORECASE, ) @@ -731,7 +735,7 @@ def _try_regex_parse(self, response: str) -> ParsedAction: # CLICK([id]) - element ID click_element = re.search( - r'CLICK\s*\(\s*\[\s*(\d+)\s*\]\s*\)', + r"CLICK\s*\(\s*\[\s*(\d+)\s*\]\s*\)", response, re.IGNORECASE, ) @@ -743,7 +747,7 @@ def _try_regex_parse(self, response: str) -> ParsedAction: # CLICK(id) without brackets click_id = re.search( - r'CLICK\s*\(\s*(\d+)\s*\)', + r"CLICK\s*\(\s*(\d+)\s*\)", response, re.IGNORECASE, ) @@ -764,14 +768,14 @@ def _try_regex_parse(self, response: str) -> ParsedAction: # KEY(key) or KEY(mod+key) key_match = re.search( - r'KEY\s*\(\s*([a-zA-Z0-9_+]+)\s*\)', + r"KEY\s*\(\s*([a-zA-Z0-9_+]+)\s*\)", response, re.IGNORECASE, ) if key_match: key_str = key_match.group(1).lower() - if '+' in key_str: - parts = key_str.split('+') + if "+" in key_str: + parts = key_str.split("+") modifiers = parts[:-1] key = parts[-1] return ParsedAction(action_type="key", key=key, modifiers=modifiers) @@ -779,35 +783,50 @@ def _try_regex_parse(self, response: str) -> ParsedAction: # SCROLL(direction) or SCROLL(direction, amount) scroll_match = re.search( - r'SCROLL\s*\(\s*([a-zA-Z]+)(?:\s*,\s*(\d+))?\s*\)', + r"SCROLL\s*\(\s*([a-zA-Z]+)(?:\s*,\s*(\d+))?\s*\)", response, re.IGNORECASE, ) if scroll_match: direction = scroll_match.group(1).lower() amount = int(scroll_match.group(2)) if scroll_match.group(2) else 3 - return ParsedAction(action_type="scroll", direction=direction, amount=amount) + return ParsedAction( + action_type="scroll", direction=direction, amount=amount + ) - return ParsedAction(action_type="unknown", parse_error="No regex pattern matched") + return ParsedAction( + action_type="unknown", parse_error="No regex pattern matched" + ) def _try_keyword_parse(self, response: str) -> ParsedAction: """Try special keywords.""" response_upper = response.upper().strip() # DONE() or just DONE - if re.search(r'\bDONE\s*\(\s*\)\s*$', response, re.IGNORECASE) or response_upper == "DONE": + if ( + re.search(r"\bDONE\s*\(\s*\)\s*$", response, re.IGNORECASE) + or response_upper == "DONE" + ): return ParsedAction(action_type="done") # WAIT() or WAIT - if re.search(r'\bWAIT\s*\(\s*\)\s*$', response, re.IGNORECASE) or response_upper == "WAIT": + if ( + re.search(r"\bWAIT\s*\(\s*\)\s*$", response, re.IGNORECASE) + or response_upper == "WAIT" + ): return ParsedAction(action_type="wait") # FAIL() or FAIL - if re.search(r'\bFAIL\s*\(\s*\)\s*$', response, re.IGNORECASE) or response_upper == "FAIL": + if ( + re.search(r"\bFAIL\s*\(\s*\)\s*$", response, re.IGNORECASE) + or response_upper == "FAIL" + ): return ParsedAction(action_type="fail") # Look for "task is complete" or similar phrases - if re.search(r'task\s+(?:is\s+)?(?:complete|done|finished)', response, re.IGNORECASE): + if re.search( + r"task\s+(?:is\s+)?(?:complete|done|finished)", response, re.IGNORECASE + ): return ParsedAction( action_type="done", confidence=0.7, @@ -842,7 +861,7 @@ def _normalize_element_id(self, element_id: Any) -> int | None: if isinstance(element_id, str): # Extract number from "e17", "[17]", "element_17" etc. - match = re.search(r'\d+', element_id) + match = re.search(r"\d+", element_id) if match: return int(match.group()) diff --git a/openadapt_ml/baselines/prompts.py b/openadapt_ml/baselines/prompts.py index 82383de..9d11b8a 100644 --- a/openadapt_ml/baselines/prompts.py +++ b/openadapt_ml/baselines/prompts.py @@ -758,7 +758,9 @@ def build_verification_prompt( """ content: list[dict[str, Any]] = [] - action_str = self._format_single_action(0, previous_action.get("type", ""), previous_action) + action_str = self._format_single_action( + 0, previous_action.get("type", ""), previous_action + ) action_str = action_str[3:] # Remove "0. " prefix text = textwrap.dedent(f""" diff --git a/openadapt_ml/benchmarks/__init__.py b/openadapt_ml/benchmarks/__init__.py index 64250df..a6d0d29 100644 --- a/openadapt_ml/benchmarks/__init__.py +++ b/openadapt_ml/benchmarks/__init__.py @@ -69,6 +69,7 @@ print(f"Success rate: {metrics['success_rate']:.1%}") ``` """ + from __future__ import annotations import warnings @@ -110,6 +111,7 @@ stacklevel=2, ) + # Azure orchestration (lazy import to avoid requiring azure-ai-ml) def _get_azure_classes(): from openadapt_ml.benchmarks.azure import ( @@ -117,6 +119,7 @@ def _get_azure_classes(): AzureWAAOrchestrator, estimate_cost, ) + return AzureConfig, AzureWAAOrchestrator, estimate_cost @@ -165,5 +168,10 @@ def __getattr__(name: str): AzureWAAOrchestrator, estimate_cost, ) - return {"AzureConfig": AzureConfig, "AzureWAAOrchestrator": AzureWAAOrchestrator, "estimate_cost": estimate_cost}[name] + + return { + "AzureConfig": AzureConfig, + "AzureWAAOrchestrator": AzureWAAOrchestrator, + "estimate_cost": estimate_cost, + }[name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/openadapt_ml/benchmarks/agent.py b/openadapt_ml/benchmarks/agent.py index c6c6c14..aea4d9b 100644 --- a/openadapt_ml/benchmarks/agent.py +++ b/openadapt_ml/benchmarks/agent.py @@ -269,7 +269,9 @@ def _to_benchmark_action( end_x, end_y = action.normalized_end # Extract action type value (enum -> string) - action_type = action.type.value if hasattr(action.type, 'value') else action.type + action_type = ( + action.type.value if hasattr(action.type, "value") else action.type + ) # Extract element info if available target_node_id = None @@ -859,9 +861,18 @@ def _parse_response( end_y = float(drag_match.group(4)) # Normalize coordinates if they appear to be pixel values - if observation and observation.viewport and (x > 1.0 or y > 1.0 or end_x > 1.0 or end_y > 1.0): + if ( + observation + and observation.viewport + and (x > 1.0 or y > 1.0 or end_x > 1.0 or end_y > 1.0) + ): width, height = observation.viewport - raw_action["original_coords"] = {"x": x, "y": y, "end_x": end_x, "end_y": end_y} + raw_action["original_coords"] = { + "x": x, + "y": y, + "end_x": end_x, + "end_y": end_y, + } raw_action["normalized"] = True x = x / width y = y / height @@ -1173,8 +1184,4 @@ def reset(self) -> None: pass def __repr__(self) -> str: - return ( - f"UnifiedBaselineAgent(" - f"model={self.model_alias}, " - f"track={self.track})" - ) + return f"UnifiedBaselineAgent(model={self.model_alias}, track={self.track})" diff --git a/openadapt_ml/benchmarks/azure.py b/openadapt_ml/benchmarks/azure.py index a3d81eb..157c62b 100644 --- a/openadapt_ml/benchmarks/azure.py +++ b/openadapt_ml/benchmarks/azure.py @@ -231,7 +231,9 @@ def client(self): resource_group_name=self.config.resource_group, workspace_name=self.config.workspace_name, ) - logger.info(f"Connected to Azure ML workspace: {self.config.workspace_name}") + logger.info( + f"Connected to Azure ML workspace: {self.config.workspace_name}" + ) return self._client def _get_credential(self): @@ -239,11 +241,13 @@ def _get_credential(self): from openadapt_ml.config import settings # Use service principal if credentials are configured - if all([ - settings.azure_client_id, - settings.azure_client_secret, - settings.azure_tenant_id, - ]): + if all( + [ + settings.azure_client_id, + settings.azure_client_secret, + settings.azure_tenant_id, + ] + ): logger.info("Using service principal authentication") return self._ClientSecretCredential( tenant_id=settings.azure_tenant_id, @@ -299,7 +303,10 @@ def create_compute_instance( f"/providers/Microsoft.ManagedIdentity" f"/userAssignedIdentities/{self.config.managed_identity_name}" ) - compute.identity = {"type": "UserAssigned", "user_assigned_identities": [identity_id]} + compute.identity = { + "type": "UserAssigned", + "user_assigned_identities": [identity_id], + } print(f" Creating VM: {name}...", end="", flush=True) self.client.compute.begin_create_or_update(compute).result() @@ -379,6 +386,7 @@ def submit_job( import time import uuid + timestamp = int(time.time()) unique_id = str(uuid.uuid4())[:8] job_name = f"waa-{compute_name}-{timestamp}-{unique_id}" @@ -528,13 +536,17 @@ def run_evaluation( try: # Provision VMs in parallel - print(f"[2/4] Provisioning {num_workers} Azure VM(s)... (this takes 3-5 minutes)") + print( + f"[2/4] Provisioning {num_workers} Azure VM(s)... (this takes 3-5 minutes)" + ) self._provision_workers(workers) print(" VM(s) ready") # Submit jobs to workers print("[3/4] Submitting evaluation jobs...") - self._submit_worker_jobs(workers, task_batches, agent, max_steps_per_task, timeout_hours) + self._submit_worker_jobs( + workers, task_batches, agent, max_steps_per_task, timeout_hours + ) print(" Jobs submitted") # Wait for completion and collect results diff --git a/openadapt_ml/benchmarks/base.py b/openadapt_ml/benchmarks/base.py index d572b83..522914e 100644 --- a/openadapt_ml/benchmarks/base.py +++ b/openadapt_ml/benchmarks/base.py @@ -330,7 +330,9 @@ def benchmark_type(self) -> str: @abstractmethod def load_trajectories( self, split: str = "test" - ) -> Iterator[tuple[BenchmarkTask, list[tuple[BenchmarkObservation, BenchmarkAction]]]]: + ) -> Iterator[ + tuple[BenchmarkTask, list[tuple[BenchmarkObservation, BenchmarkAction]]] + ]: """Iterate over expert trajectories. Args: diff --git a/openadapt_ml/benchmarks/cli.py b/openadapt_ml/benchmarks/cli.py index e963d2c..02464c4 100644 --- a/openadapt_ml/benchmarks/cli.py +++ b/openadapt_ml/benchmarks/cli.py @@ -98,7 +98,9 @@ # Pre-configure loggers to be quiet by default (before any Azure imports) logging.getLogger("azure").setLevel(logging.WARNING) -logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.WARNING) +logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel( + logging.WARNING +) logging.getLogger("azure.ai.ml").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("msrest").setLevel(logging.WARNING) @@ -114,11 +116,16 @@ # ServerAliveCountMax=10: Disconnect after 10 missed keepalives (10 min tolerance) # TCPKeepAlive=yes: Enable TCP-level keepalive as additional safeguard SSH_OPTS = [ - "-o", "StrictHostKeyChecking=no", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "ServerAliveInterval=60", - "-o", "ServerAliveCountMax=10", - "-o", "TCPKeepAlive=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "ServerAliveInterval=60", + "-o", + "ServerAliveCountMax=10", + "-o", + "TCPKeepAlive=yes", ] @@ -167,14 +174,15 @@ def setup_logging(verbose: bool = False) -> None: """ level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( - level=level, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + level=level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) # Suppress noisy Azure SDK logs unless verbose if not verbose: logging.getLogger("azure").setLevel(logging.WARNING) - logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.WARNING) + logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel( + logging.WARNING + ) logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("msrest").setLevel(logging.WARNING) @@ -204,7 +212,7 @@ def bypass_product_key_dialog(ip: str, max_attempts: int = 3) -> bool: for attempt in range(max_attempts): try: # Send commands via QEMU monitor (port 7100 in container) - ssh_cmd = ''' + ssh_cmd = """ # Use telnet to send QEMU commands ( echo "sendkey tab" @@ -218,11 +226,19 @@ def bypass_product_key_dialog(ip: str, max_attempts: int = 3) -> bool: echo "sendkey ret" sleep 0.5 ) | timeout 10 docker exec -i winarena nc localhost 7100 2>/dev/null -''' +""" result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=10", - f"azureuser@{ip}", ssh_cmd], - capture_output=True, text=True, timeout=30 + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=10", + f"azureuser@{ip}", + ssh_cmd, + ], + capture_output=True, + text=True, + timeout=30, ) if "QEMU" in result.stdout or result.returncode == 0: @@ -311,7 +327,9 @@ def cmd_estimate(args: argparse.Namespace) -> None: print(f"Tasks: {estimate['num_tasks']}") print(f"Workers: {estimate['num_workers']}") print(f"Tasks per worker: {estimate['tasks_per_worker']:.1f}") - print(f"Estimated duration: {estimate['estimated_duration_minutes']:.1f} minutes") + print( + f"Estimated duration: {estimate['estimated_duration_minutes']:.1f} minutes" + ) print(f"Total VM hours: {estimate['total_vm_hours']:.2f}") print(f"Estimated cost: ${estimate['estimated_cost_usd']:.2f}") print(f"Cost per task: ${estimate['cost_per_task_usd']:.4f}") @@ -331,7 +349,10 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: text=True, timeout=30, ) - return result.returncode == 0, result.stdout.strip() or result.stderr.strip() + return ( + result.returncode == 0, + result.stdout.strip() or result.stderr.strip(), + ) except FileNotFoundError: return False, "Azure CLI not installed" except subprocess.TimeoutExpired: @@ -352,7 +373,9 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: return # Check login - ok, output = run_az(["account", "show", "--query", "name", "-o", "tsv"], "Azure login") + ok, output = run_az( + ["account", "show", "--query", "name", "-o", "tsv"], "Azure login" + ) if ok: print(f" Logged in: ✓ {output}") else: @@ -363,7 +386,7 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: rg = args.resource_group ok, output = run_az( ["group", "show", "--name", rg, "--query", "location", "-o", "tsv"], - "Resource group" + "Resource group", ) if ok: print(f" Resource group: ✓ {rg} ({output})") @@ -375,8 +398,20 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: # Check ML workspace ws = args.workspace ok, output = run_az( - ["ml", "workspace", "show", "--name", ws, "--resource-group", rg, "--query", "location", "-o", "tsv"], - "ML workspace" + [ + "ml", + "workspace", + "show", + "--name", + ws, + "--resource-group", + rg, + "--query", + "location", + "-o", + "tsv", + ], + "ML workspace", ) if ok: print(f" ML workspace: ✓ {ws} ({output})") @@ -386,8 +421,19 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: # Check ACR acr = args.acr_name ok, output = run_az( - ["acr", "show", "--name", acr, "--resource-group", rg, "--query", "loginServer", "-o", "tsv"], - "Container registry" + [ + "acr", + "show", + "--name", + acr, + "--resource-group", + rg, + "--query", + "loginServer", + "-o", + "tsv", + ], + "Container registry", ) if ok: print(f" Container registry: ✓ {output}") @@ -396,8 +442,20 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: # Check WAA Docker image ok, output = run_az( - ["acr", "repository", "show", "--name", acr, "--repository", "winarena", "--query", "imageName", "-o", "tsv"], - "WAA Docker image" + [ + "acr", + "repository", + "show", + "--name", + acr, + "--repository", + "winarena", + "--query", + "imageName", + "-o", + "tsv", + ], + "WAA Docker image", ) if ok: print(" WAA Docker image: ✓ winarena") @@ -410,7 +468,9 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: if env_path.exists(): env_content = env_path.read_text() has_azure = "AZURE_SUBSCRIPTION_ID" in env_content - print(f" .env file: ✓ {'Azure credentials found' if has_azure else 'Missing Azure credentials'}") + print( + f" .env file: ✓ {'Azure credentials found' if has_azure else 'Missing Azure credentials'}" + ) else: print(" .env file: ✗ Not found") @@ -419,6 +479,7 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: if waa_path: # Count tasks from openadapt_ml.benchmarks import WAAAdapter + try: adapter = WAAAdapter(waa_repo_path=waa_path) task_count = len(adapter.list_tasks()) @@ -430,7 +491,11 @@ def run_az(cmd: list[str], description: str) -> tuple[bool, str]: print(" Run: git submodule update --init --recursive") print() - print("Ready for benchmark evaluation!" if ok else "Some resources missing - run setup_azure.py") + print( + "Ready for benchmark evaluation!" + if ok + else "Some resources missing - run setup_azure.py" + ) def cmd_run_local(args: argparse.Namespace) -> None: @@ -585,14 +650,18 @@ def _write_azure_job_status( jobs.insert(0, job_entry) # Most recent first # Update job entry - job_entry.update({ - "status": status, - "workers": workers, - "num_tasks": num_tasks, - "task_ids": task_ids[:5] if task_ids and len(task_ids) > 5 else task_ids, # First 5 for display - "azure_dashboard_url": azure_url, - "updated_at": datetime.datetime.now().isoformat(), - }) + job_entry.update( + { + "status": status, + "workers": workers, + "num_tasks": num_tasks, + "task_ids": task_ids[:5] + if task_ids and len(task_ids) > 5 + else task_ids, # First 5 for display + "azure_dashboard_url": azure_url, + "updated_at": datetime.datetime.now().isoformat(), + } + ) if end_time: job_entry["ended_at"] = end_time @@ -841,7 +910,9 @@ def cmd_test_mock(args: argparse.Namespace) -> None: if domain_metrics: print("=== By Domain ===") for domain, dm in domain_metrics.items(): - print(f" {domain}: {dm['success_rate']:.1%} ({dm['success_count']}/{dm['num_tasks']})") + print( + f" {domain}: {dm['success_rate']:.1%} ({dm['success_count']}/{dm['num_tasks']})" + ) print() @@ -872,7 +943,9 @@ def cmd_test_smart(args: argparse.Namespace) -> None: # Print results success_count = sum(1 for r in results if r.success) print("=== Results ===") - print(f"Success rate: {success_count}/{len(results)} ({100*success_count/len(results):.0f}%)") + print( + f"Success rate: {success_count}/{len(results)} ({100 * success_count / len(results):.0f}%)" + ) if success_count != len(results): print("\nWARNING: Expected 100% success with SmartMockAgent") @@ -893,7 +966,10 @@ def cmd_test_collection(args: argparse.Namespace) -> None: from pathlib import Path from openadapt_ml.benchmarks import RandomAgent, WAAMockAdapter - from openadapt_ml.benchmarks.runner import EvaluationConfig, evaluate_agent_on_benchmark + from openadapt_ml.benchmarks.runner import ( + EvaluationConfig, + evaluate_agent_on_benchmark, + ) print("\n=== Testing Benchmark Data Collection ===") print(f" Tasks: {args.tasks}") @@ -904,7 +980,9 @@ def cmd_test_collection(args: argparse.Namespace) -> None: # Create mock adapter adapter = WAAMockAdapter(num_tasks=args.tasks, domains=["browser", "office"]) - agent = RandomAgent(action_types=["click", "type", "scroll", "done"], seed=args.seed) + agent = RandomAgent( + action_types=["click", "type", "scroll", "done"], seed=args.seed + ) # Configure evaluation with data collection config = EvaluationConfig( @@ -938,7 +1016,11 @@ def cmd_test_collection(args: argparse.Namespace) -> None: # Find the actual output directory by reading metadata output_dir = Path(args.output) - run_dirs = sorted(output_dir.glob("*/metadata.json"), key=lambda p: p.stat().st_mtime, reverse=True) + run_dirs = sorted( + output_dir.glob("*/metadata.json"), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) if run_dirs: run_dir = run_dirs[0].parent with open(run_dirs[0]) as f: @@ -984,7 +1066,10 @@ def cmd_waa_demo(args: argparse.Namespace) -> None: WAAMockAdapter, compute_metrics, ) - from openadapt_ml.benchmarks.runner import EvaluationConfig, evaluate_agent_on_benchmark + from openadapt_ml.benchmarks.runner import ( + EvaluationConfig, + evaluate_agent_on_benchmark, + ) print("\n=== WAA Demo-Conditioned Experiment ===") print(f" Condition: {args.condition}") @@ -1023,6 +1108,7 @@ def cmd_waa_demo(args: argparse.Namespace) -> None: waa_path = find_waa_path() if waa_path and sys.platform == "win32": from openadapt_ml.benchmarks import WAAAdapter + print(f" Using real WAA from: {waa_path}") adapter = WAAAdapter(waa_repo_path=waa_path) else: @@ -1066,7 +1152,9 @@ def cmd_waa_demo(args: argparse.Namespace) -> None: ) except Exception as e: print(f"\nERROR: {e}") - key_name = "ANTHROPIC_API_KEY" if args.provider == "anthropic" else "OPENAI_API_KEY" + key_name = ( + "ANTHROPIC_API_KEY" if args.provider == "anthropic" else "OPENAI_API_KEY" + ) if "API key" in str(e) or "api_key" in str(e).lower(): print(f"\nMake sure {key_name} is set in your environment or .env file.") sys.exit(1) @@ -1092,7 +1180,11 @@ def cmd_waa_demo(args: argparse.Namespace) -> None: # Output location output_dir = Path(args.output) - run_dirs = sorted(output_dir.glob("*/metadata.json"), key=lambda p: p.stat().st_mtime, reverse=True) + run_dirs = sorted( + output_dir.glob("*/metadata.json"), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) if run_dirs: run_dir = run_dirs[0].parent print(f"Results saved to: {run_dir.absolute()}") @@ -1110,7 +1202,10 @@ def cmd_run_api(args: argparse.Namespace) -> None: compute_domain_metrics, compute_metrics, ) - from openadapt_ml.benchmarks.runner import EvaluationConfig, evaluate_agent_on_benchmark + from openadapt_ml.benchmarks.runner import ( + EvaluationConfig, + evaluate_agent_on_benchmark, + ) provider_names = { "anthropic": "Claude", @@ -1118,13 +1213,16 @@ def cmd_run_api(args: argparse.Namespace) -> None: } print("\n=== API-Backed Benchmark Evaluation ===") - print(f" Provider: {args.provider} ({provider_names.get(args.provider, 'Unknown')})") + print( + f" Provider: {args.provider} ({provider_names.get(args.provider, 'Unknown')})" + ) print(f" Tasks: {args.tasks}") print(f" Max steps: {args.max_steps}") print(f" Output dir: {args.output}") # Check for API key import os + key_name = "ANTHROPIC_API_KEY" if args.provider == "anthropic" else "OPENAI_API_KEY" if not os.getenv(key_name): print(f"WARNING: {key_name} environment variable not set!") @@ -1155,10 +1253,13 @@ def cmd_run_api(args: argparse.Namespace) -> None: # Real WAA available if sys.platform != "win32" and not args.force: print(f" Adapter: WAA (detected at {waa_path})") - print("ERROR: WAA requires Windows. Use --mock to use mock adapter instead.") + print( + "ERROR: WAA requires Windows. Use --mock to use mock adapter instead." + ) sys.exit(1) from openadapt_ml.benchmarks import WAAAdapter + print(f" Adapter: WAA (real, from {waa_path})") print() adapter = WAAAdapter(waa_repo_path=waa_path) @@ -1167,10 +1268,16 @@ def cmd_run_api(args: argparse.Namespace) -> None: else: # WAA not found, fall back to mock print(" Adapter: Mock (WAA not found)") - print(" Note: To use real WAA, run: git submodule update --init --recursive") - print(" Or specify with: --waa-path /path/to/WindowsAgentArena") + print( + " Note: To use real WAA, run: git submodule update --init --recursive" + ) + print( + " Or specify with: --waa-path /path/to/WindowsAgentArena" + ) print() - adapter = WAAMockAdapter(num_tasks=args.tasks, domains=["browser", "office"]) + adapter = WAAMockAdapter( + num_tasks=args.tasks, domains=["browser", "office"] + ) # Create API-backed agent agent = APIBenchmarkAgent( @@ -1228,12 +1335,18 @@ def cmd_run_api(args: argparse.Namespace) -> None: if domain_metrics: print("=== By Domain ===") for domain, dm in domain_metrics.items(): - print(f" {domain}: {dm['success_rate']:.1%} ({dm['success_count']}/{dm['num_tasks']})") + print( + f" {domain}: {dm['success_rate']:.1%} ({dm['success_count']}/{dm['num_tasks']})" + ) print() # Find output directory output_dir = Path(args.output) - run_dirs = sorted(output_dir.glob("*/metadata.json"), key=lambda p: p.stat().st_mtime, reverse=True) + run_dirs = sorted( + output_dir.glob("*/metadata.json"), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) if run_dirs: run_dir = run_dirs[0].parent print(f"Results saved to: {run_dir.absolute()}") @@ -1375,11 +1488,18 @@ def cmd_cleanup_vms(args: argparse.Namespace) -> None: # List current VMs result = subprocess.run( [ - "az", "ml", "compute", "list", - "--resource-group", args.resource_group, - "--workspace-name", args.workspace, - "--query", "[].name", - "-o", "tsv", + "az", + "ml", + "compute", + "list", + "--resource-group", + args.resource_group, + "--workspace-name", + args.workspace, + "--query", + "[].name", + "-o", + "tsv", ], capture_output=True, text=True, @@ -1410,10 +1530,16 @@ def cmd_cleanup_vms(args: argparse.Namespace) -> None: print(f"Deleting {vm}...", end=" ", flush=True) del_result = subprocess.run( [ - "az", "ml", "compute", "delete", - "--name", vm, - "--resource-group", args.resource_group, - "--workspace-name", args.workspace, + "az", + "ml", + "compute", + "delete", + "--name", + vm, + "--resource-group", + args.resource_group, + "--workspace-name", + args.workspace, "--yes", ], capture_output=True, @@ -1435,10 +1561,16 @@ def cmd_list_jobs(args: argparse.Namespace) -> None: result = subprocess.run( [ - "az", "ml", "job", "list", - "--resource-group", args.resource_group, - "--workspace-name", args.workspace, - "-o", "table", + "az", + "ml", + "job", + "list", + "--resource-group", + args.resource_group, + "--workspace-name", + args.workspace, + "-o", + "table", ], capture_output=True, text=True, @@ -1449,8 +1581,10 @@ def cmd_list_jobs(args: argparse.Namespace) -> None: sys.exit(1) # Filter out experimental warnings - lines = [line for line in result.stdout.split("\n") if "experimental" not in line.lower()] - print("\n".join(lines[:args.limit + 3])) # +3 for header rows + lines = [ + line for line in result.stdout.split("\n") if "experimental" not in line.lower() + ] + print("\n".join(lines[: args.limit + 3])) # +3 for header rows def cmd_job_logs(args: argparse.Namespace) -> None: @@ -1463,11 +1597,18 @@ def cmd_job_logs(args: argparse.Namespace) -> None: with tempfile.TemporaryDirectory() as tmpdir: result = subprocess.run( [ - "az", "ml", "job", "download", - "--name", args.job_name, - "--resource-group", args.resource_group, - "--workspace-name", args.workspace, - "--download-path", tmpdir, + "az", + "ml", + "job", + "download", + "--name", + args.job_name, + "--resource-group", + args.resource_group, + "--workspace-name", + args.workspace, + "--download-path", + tmpdir, "--all", ], capture_output=True, @@ -1511,9 +1652,22 @@ def get_vm_ip(resource_group: str, vm_name: str) -> str | None: import subprocess result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode == 0 and result.stdout.strip(): return result.stdout.strip() @@ -1536,9 +1690,17 @@ def ensure_docker_running(ip: str) -> bool: # Check if Docker is running check_result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=10", f"azureuser@{ip}", - "docker info 2>/dev/null | head -1"], - capture_output=True, text=True, timeout=30 + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=10", + f"azureuser@{ip}", + "docker info 2>/dev/null | head -1", + ], + capture_output=True, + text=True, + timeout=30, ) if "Client:" in check_result.stdout: @@ -1547,15 +1709,22 @@ def ensure_docker_running(ip: str) -> bool: # Docker not running, try to start it print(" Docker not running, starting...") start_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "sudo systemctl start docker && sleep 3 && docker info 2>/dev/null | head -1"], - capture_output=True, text=True, timeout=60 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "sudo systemctl start docker && sleep 3 && docker info 2>/dev/null | head -1", + ], + capture_output=True, + text=True, + timeout=60, ) if "Client:" in start_result.stdout: print(" ✓ Docker started") # Wait for Docker to be fully ready import time + time.sleep(5) return True @@ -1584,27 +1753,37 @@ def capture_vm_screenshot(ip: str, output_path: Path | str = None) -> Path | Non # Take screenshot via QEMU QMP monitor (port 7200) and convert to PNG on VM # dockurr/windows uses QMP protocol on port 7200 # Use Python PIL to convert PPM to PNG (ImageMagick not installed in container) - screenshot_script = ''' + screenshot_script = """ printf '%s\\n' '{"execute": "qmp_capabilities"}' '{"execute": "screendump", "arguments": {"filename": "/tmp/screen.ppm"}}' | nc -q1 localhost 7200 > /dev/null 2>&1 sleep 1 python3 -c "from PIL import Image; Image.open('/tmp/screen.ppm').save('/tmp/screen.png')" 2>/dev/null && cat /tmp/screen.png | base64 -''' +""" result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=10", - f"azureuser@{ip}", - f"docker exec winarena bash -c {shlex.quote(screenshot_script)}"], - capture_output=True, text=True, timeout=60 + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=10", + f"azureuser@{ip}", + f"docker exec winarena bash -c {shlex.quote(screenshot_script)}", + ], + capture_output=True, + text=True, + timeout=60, ) if result.returncode == 0 and result.stdout.strip(): # Decode base64 and save import base64 + png_data = base64.b64decode(result.stdout.strip()) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(png_data) return output_path else: - logger.warning(f"Screenshot capture failed: {result.stderr[:200] if result.stderr else 'No output'}") + logger.warning( + f"Screenshot capture failed: {result.stderr[:200] if result.stderr else 'No output'}" + ) return None except subprocess.TimeoutExpired: logger.warning("Screenshot capture timed out") @@ -1614,7 +1793,9 @@ def capture_vm_screenshot(ip: str, output_path: Path | str = None) -> Path | Non return None -def check_waa_probe(ip: str, timeout: int = 5, internal_ip: str = "172.30.0.2") -> tuple[bool, str | None]: +def check_waa_probe( + ip: str, timeout: int = 5, internal_ip: str = "172.30.0.2" +) -> tuple[bool, str | None]: """Check if the WAA /probe endpoint is responding. Args: @@ -1633,10 +1814,17 @@ def check_waa_probe(ip: str, timeout: int = 5, internal_ip: str = "172.30.0.2") # Run curl from inside the Docker container, not the VM host # Port 5000 is only accessible within Docker's network result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=5", - f"azureuser@{ip}", - f"docker exec winarena curl -s --connect-timeout {timeout} http://{internal_ip}:5000/probe 2>/dev/null"], - capture_output=True, text=True, timeout=30 + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=5", + f"azureuser@{ip}", + f"docker exec winarena curl -s --connect-timeout {timeout} http://{internal_ip}:5000/probe 2>/dev/null", + ], + capture_output=True, + text=True, + timeout=30, ) response = result.stdout.strip() if result.stdout else None return bool(response), response @@ -1646,7 +1834,9 @@ def check_waa_probe(ip: str, timeout: int = 5, internal_ip: str = "172.30.0.2") return False, None -def poll_waa_probe(ip: str, max_attempts: int = 30, interval: int = 20, internal_ip: str = "172.30.0.2") -> bool: +def poll_waa_probe( + ip: str, max_attempts: int = 30, interval: int = 20, internal_ip: str = "172.30.0.2" +) -> bool: """Poll the WAA /probe endpoint until it responds or timeout. Args: @@ -1660,7 +1850,9 @@ def poll_waa_probe(ip: str, max_attempts: int = 30, interval: int = 20, internal """ import time - print(f" Polling /probe endpoint at {internal_ip}:5000 (max {max_attempts * interval}s)...") + print( + f" Polling /probe endpoint at {internal_ip}:5000 (max {max_attempts * interval}s)..." + ) print(f" Monitor Windows at: http://{ip}:8006 (VNC)") print() @@ -1693,8 +1885,8 @@ def cmd_analyze(args: argparse.Namespace) -> None: results_dir = args.results_dir vm_ip = args.vm_ip - remote = getattr(args, 'remote', False) - verbose = getattr(args, 'verbose', False) + remote = getattr(args, "remote", False) + verbose = getattr(args, "verbose", False) # If --remote flag, run analysis via SSH on the VM if vm_ip and remote: @@ -1702,7 +1894,7 @@ def cmd_analyze(args: argparse.Namespace) -> None: remote_path = "/mnt/WindowsAgentArena/src/win-arena-container/client/results/pyautogui/a11y_tree" # Build SSH command to analyze results on VM - analysis_script = ''' + analysis_script = """ import os import json from pathlib import Path @@ -1757,14 +1949,20 @@ def cmd_analyze(args: argparse.Namespace) -> None: "failed_tasks": failed_tasks }} print(json.dumps(result)) -'''.format(remote_path=remote_path) +""".format(remote_path=remote_path) try: - result = subprocess.run([ - "ssh", *SSH_OPTS, - f"azureuser@{vm_ip}", - f"python3 -c '{analysis_script}'" - ], capture_output=True, text=True, timeout=30) + result = subprocess.run( + [ + "ssh", + *SSH_OPTS, + f"azureuser@{vm_ip}", + f"python3 -c '{analysis_script}'", + ], + capture_output=True, + text=True, + timeout=30, + ) if result.returncode != 0: print(f"SSH analysis failed: {result.stderr}") @@ -1786,10 +1984,12 @@ def cmd_analyze(args: argparse.Namespace) -> None: print(f"\nModel: {data['model']}") print("-" * 40) - for domain, stats in sorted(data['domains'].items()): - status = "✓" if stats['success'] > 0 else "○" + for domain, stats in sorted(data["domains"].items()): + status = "✓" if stats["success"] > 0 else "○" rate = f"{stats['success']}/{stats['total']}" - print(f" {status} {domain:20s} {rate:8s} ({stats['fail']} fail, {stats['incomplete']} incomplete)") + print( + f" {status} {domain:20s} {rate:8s} ({stats['fail']} fail, {stats['incomplete']} incomplete)" + ) print("\n" + "=" * 60) print("SUMMARY") @@ -1800,20 +2000,22 @@ def cmd_analyze(args: argparse.Namespace) -> None: print(f"Successful: {data['success']}") print(f"Failed: {data['fail']}") print(f"Success rate: {data['success_rate']:.1f}% (of evaluated)") - if data['total_tasks'] > 0: - print(f"Completion rate: {data['evaluated'] / data['total_tasks'] * 100:.1f}%") + if data["total_tasks"] > 0: + print( + f"Completion rate: {data['evaluated'] / data['total_tasks'] * 100:.1f}%" + ) if verbose: print("\n" + "-" * 40) print("SUCCESSFUL TASKS:") - for task in data['successful_tasks']: + for task in data["successful_tasks"]: print(f" ✓ {task}") print("\nFAILED TASKS:") - for task in data['failed_tasks']: + for task in data["failed_tasks"]: print(f" ✗ {task}") if args.output: - data['date'] = datetime.now().isoformat() + data["date"] = datetime.now().isoformat() Path(args.output).write_text(json.dumps(data, indent=2)) print(f"\nSummary saved to: {args.output}") @@ -1830,11 +2032,17 @@ def cmd_analyze(args: argparse.Namespace) -> None: print(f"Downloading to {results_dir}...") try: - subprocess.run([ - "scp", "-r", *SSH_OPTS, - f"azureuser@{vm_ip}:{remote_path}/pyautogui", - results_dir - ], check=True, capture_output=True) + subprocess.run( + [ + "scp", + "-r", + *SSH_OPTS, + f"azureuser@{vm_ip}:{remote_path}/pyautogui", + results_dir, + ], + check=True, + capture_output=True, + ) results_dir = Path(results_dir) / "pyautogui" except subprocess.CalledProcessError as e: print(f"Failed to fetch results: {e}") @@ -1914,13 +2122,15 @@ def cmd_analyze(args: argparse.Namespace) -> None: "total": task_count, "success": success, "fail": fail, - "incomplete": incomplete + "incomplete": incomplete, } # Format output status = "✓" if success > 0 else "○" rate = f"{success}/{task_count}" if task_count > 0 else "0/0" - print(f" {status} {domain:20s} {rate:8s} ({fail} fail, {incomplete} incomplete)") + print( + f" {status} {domain:20s} {rate:8s} ({fail} fail, {incomplete} incomplete)" + ) # Summary print("\n" + "=" * 60) @@ -1933,7 +2143,9 @@ def cmd_analyze(args: argparse.Namespace) -> None: print(f"Successful: {total_success}") print(f"Failed: {total_fail}") if evaluated > 0: - print(f"Success rate: {total_success / evaluated * 100:.1f}% (of evaluated)") + print( + f"Success rate: {total_success / evaluated * 100:.1f}% (of evaluated)" + ) if total_tasks > 0: print(f"Completion rate: {evaluated / total_tasks * 100:.1f}%") @@ -1959,7 +2171,7 @@ def cmd_analyze(args: argparse.Namespace) -> None: "success_rate": total_success / evaluated * 100 if evaluated > 0 else 0, "domains": domain_stats, "successful_tasks": successful_tasks, - "failed_tasks": failed_tasks + "failed_tasks": failed_tasks, } output_path = Path(args.output) output_path.write_text(json.dumps(summary, indent=2)) @@ -1970,7 +2182,7 @@ def launch_benchmark_viewer( vm_ip: str, port: int = 8765, open_browser: bool = True, - internal_ip: str = "172.30.0.2" + internal_ip: str = "172.30.0.2", ) -> None: """Launch the benchmark viewer for monitoring a running WAA benchmark. @@ -2001,10 +2213,15 @@ def launch_benchmark_viewer( # Build the serve command - use --benchmark to generate benchmark.html serve_cmd = [ - sys.executable, "-m", "openadapt_ml.cloud.local", "serve", - "--port", str(port), + sys.executable, + "-m", + "openadapt_ml.cloud.local", + "serve", + "--port", + str(port), "--quiet", - "--benchmark", "benchmark_results", # This triggers benchmark.html generation + "--benchmark", + "benchmark_results", # This triggers benchmark.html generation ] if open_browser: serve_cmd.append("--open") @@ -2028,15 +2245,12 @@ def cmd_viewer(args: argparse.Namespace) -> None: for benchmark status and opens the browser. """ vm_ip = args.vm_ip - port = getattr(args, 'port', 8765) - no_open = getattr(args, 'no_open', False) - internal_ip = getattr(args, 'internal_ip', '172.30.0.2') + port = getattr(args, "port", 8765) + no_open = getattr(args, "no_open", False) + internal_ip = getattr(args, "internal_ip", "172.30.0.2") launch_benchmark_viewer( - vm_ip=vm_ip, - port=port, - open_browser=not no_open, - internal_ip=internal_ip + vm_ip=vm_ip, port=port, open_browser=not no_open, internal_ip=internal_ip ) @@ -2056,15 +2270,29 @@ def cmd_vm(args: argparse.Namespace) -> None: location = args.location if args.action == "list-sizes": - print(f"\n=== Available VM Sizes with Nested Virtualization in {location} ===\n") + print( + f"\n=== Available VM Sizes with Nested Virtualization in {location} ===\n" + ) print("Checking available D-series sizes (support nested virt)...") # Get available sizes result = subprocess.run( - ["az", "vm", "list-skus", "--location", location, - "--size", "Standard_D", "--all", "--output", "table", - "--query", "[?restrictions[?reasonCode=='NotAvailableForSubscription']==`[]`].{Name:name, vCPUs:capabilities[?name=='vCPUs'].value|[0], Memory:capabilities[?name=='MemoryGB'].value|[0]}"], - capture_output=True, text=True + [ + "az", + "vm", + "list-skus", + "--location", + location, + "--size", + "Standard_D", + "--all", + "--output", + "table", + "--query", + "[?restrictions[?reasonCode=='NotAvailableForSubscription']==`[]`].{Name:name, vCPUs:capabilities[?name=='vCPUs'].value|[0], Memory:capabilities[?name=='MemoryGB'].value|[0]}", + ], + capture_output=True, + text=True, ) if result.returncode != 0: @@ -2077,7 +2305,9 @@ def cmd_vm(args: argparse.Namespace) -> None: print(" - Standard_D8s_v3 (8 vCPU, 32GB) ~$0.38/hr") print(" - Standard_D4ds_v5 (4 vCPU, 16GB) ~$0.19/hr") print(" - Standard_D8ds_v5 (8 vCPU, 32GB) ~$0.38/hr") - print("\nTry different locations if sizes are unavailable: westus2, centralus, westeurope") + print( + "\nTry different locations if sizes are unavailable: westus2, centralus, westeurope" + ) return elif args.action == "create": @@ -2091,26 +2321,39 @@ def cmd_vm(args: argparse.Namespace) -> None: # Check if VM already exists check = subprocess.run( ["az", "vm", "show", "-g", resource_group, "-n", vm_name, "-o", "json"], - capture_output=True, text=True + capture_output=True, + text=True, ) if check.returncode == 0: - print(f"✗ VM '{vm_name}' already exists. Use 'vm status' to check it or 'vm delete' first.") + print( + f"✗ VM '{vm_name}' already exists. Use 'vm status' to check it or 'vm delete' first." + ) sys.exit(1) print("Creating VM (this takes 2-3 minutes)...") result = subprocess.run( [ - "az", "vm", "create", - "--resource-group", resource_group, - "--name", vm_name, - "--location", location, - "--image", "Ubuntu2204", - "--size", vm_size, - "--admin-username", "azureuser", + "az", + "vm", + "create", + "--resource-group", + resource_group, + "--name", + vm_name, + "--location", + location, + "--image", + "Ubuntu2204", + "--size", + vm_size, + "--admin-username", + "azureuser", "--generate-ssh-keys", - "--public-ip-sku", "Standard", + "--public-ip-sku", + "Standard", ], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.returncode != 0: @@ -2119,6 +2362,7 @@ def cmd_vm(args: argparse.Namespace) -> None: # Parse output to get IP import json + vm_info = json.loads(result.stdout) public_ip = vm_info.get("publicIpAddress", "unknown") @@ -2126,7 +2370,9 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f"\n Public IP: {public_ip}") print(f" SSH command: ssh azureuser@{public_ip}") print("\n Next steps:") - print(" 1. SSH into the VM: uv run python -m openadapt_ml.benchmarks.cli vm ssh") + print( + " 1. SSH into the VM: uv run python -m openadapt_ml.benchmarks.cli vm ssh" + ) print(" 2. Verify nested virt: egrep -c '(vmx|svm)' /proc/cpuinfo") print(" 3. Install Docker and run WAA") @@ -2134,33 +2380,61 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f"\n=== WAA Eval VM Status: {vm_name} ===\n") result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "{name:name,powerState:powerState,publicIps:publicIps,size:hardwareProfile.vmSize}", - "-o", "json"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "{name:name,powerState:powerState,publicIps:publicIps,size:hardwareProfile.vmSize}", + "-o", + "json", + ], + capture_output=True, + text=True, ) if result.returncode != 0: print(f"✗ VM '{vm_name}' not found in resource group '{resource_group}'") - print(" Create it with: uv run python -m openadapt_ml.benchmarks.cli vm create") + print( + " Create it with: uv run python -m openadapt_ml.benchmarks.cli vm create" + ) sys.exit(1) import json + info = json.loads(result.stdout) print(f" Name: {info.get('name')}") print(f" State: {info.get('powerState')}") print(f" Size: {info.get('size')}") print(f" Public IP: {info.get('publicIps')}") - if info.get('publicIps'): + if info.get("publicIps"): print(f"\n SSH command: ssh azureuser@{info.get('publicIps')}") elif args.action == "ssh": # Get IP and SSH result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): @@ -2170,6 +2444,7 @@ def cmd_vm(args: argparse.Namespace) -> None: ip = result.stdout.strip() print(f"Connecting to {vm_name} at {ip}...") import os + # Use SSH_OPTS for consistent keepalive settings os.execvp("ssh", ["ssh", *SSH_OPTS, f"azureuser@{ip}"]) @@ -2178,14 +2453,15 @@ def cmd_vm(args: argparse.Namespace) -> None: if not args.yes: confirm = input(f"Are you sure you want to delete VM '{vm_name}'? (y/N): ") - if confirm.lower() != 'y': + if confirm.lower() != "y": print("Cancelled.") return print("Deleting VM and associated resources...") result = subprocess.run( ["az", "vm", "delete", "-g", resource_group, "-n", vm_name, "--yes"], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.returncode != 0: @@ -2198,8 +2474,18 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f"\n=== Deallocating VM: {vm_name} (stops billing, preserves disk) ===\n") result = subprocess.run( - ["az", "vm", "deallocate", "-g", resource_group, "-n", vm_name, "--no-wait"], - capture_output=True, text=True + [ + "az", + "vm", + "deallocate", + "-g", + resource_group, + "-n", + vm_name, + "--no-wait", + ], + capture_output=True, + text=True, ) if result.returncode != 0: @@ -2216,7 +2502,8 @@ def cmd_vm(args: argparse.Namespace) -> None: result = subprocess.run( ["az", "vm", "start", "-g", resource_group, "-n", vm_name, "--no-wait"], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.returncode != 0: @@ -2235,7 +2522,9 @@ def cmd_vm(args: argparse.Namespace) -> None: # Check if SSH is accessible result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "echo ready"], - capture_output=True, text=True, timeout=10 + capture_output=True, + text=True, + timeout=10, ) if result.returncode == 0: break @@ -2247,14 +2536,18 @@ def cmd_vm(args: argparse.Namespace) -> None: print(" Checking Docker...") result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "sudo docker ps 2>&1"], - capture_output=True, text=True + capture_output=True, + text=True, ) - if "Cannot connect to the Docker daemon" in result.stdout or result.returncode != 0: + if ( + "Cannot connect to the Docker daemon" in result.stdout + or result.returncode != 0 + ): print(" Docker not running. Recovering...") # Create Docker directories (symlinked to /mnt which gets wiped) # Kill any stale processes, create dirs, start services - recovery_cmd = ''' + recovery_cmd = """ sudo pkill -9 dockerd 2>/dev/null sudo pkill -9 containerd 2>/dev/null sudo rm -f /var/run/docker.pid /var/run/containerd/containerd.pid @@ -2264,17 +2557,19 @@ def cmd_vm(args: argparse.Namespace) -> None: sudo systemctl start containerd sleep 3 sudo systemctl start docker - ''' + """ subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", recovery_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) time.sleep(5) # Verify Docker is running result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", "sudo docker ps"], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.returncode == 0: print(" ✓ Docker recovered and running") @@ -2292,12 +2587,27 @@ def cmd_vm(args: argparse.Namespace) -> None: # Get VM IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): - print(f"✗ Could not get IP for VM '{vm_name}'. Create it first with 'vm create'") + print( + f"✗ Could not get IP for VM '{vm_name}'. Create it first with 'vm create'" + ) sys.exit(1) ip = result.stdout.strip() @@ -2314,7 +2624,8 @@ def cmd_vm(args: argparse.Namespace) -> None: ) result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", docker_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.returncode != 0: print(f"✗ Error installing Docker: {result.stderr}") @@ -2323,9 +2634,9 @@ def cmd_vm(args: argparse.Namespace) -> None: print("\n[2/3] Verifying nested virtualization...") result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "egrep -c '(vmx|svm)' /proc/cpuinfo"], - capture_output=True, text=True + ["ssh", *SSH_OPTS, f"azureuser@{ip}", "egrep -c '(vmx|svm)' /proc/cpuinfo"], + capture_output=True, + text=True, ) cpu_count = result.stdout.strip() if cpu_count and int(cpu_count) > 0: @@ -2346,9 +2657,22 @@ def cmd_vm(args: argparse.Namespace) -> None: # Get VM IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): print(f"✗ Could not get IP for VM '{vm_name}'") @@ -2360,9 +2684,20 @@ def cmd_vm(args: argparse.Namespace) -> None: print("\n[1/2] Getting ACR access token...") result = subprocess.run( - ["az", "acr", "login", "--name", acr_name, "--expose-token", - "--query", "accessToken", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "acr", + "login", + "--name", + acr_name, + "--expose-token", + "--query", + "accessToken", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0: print(f"✗ Error getting ACR token: {result.stderr}") @@ -2376,7 +2711,7 @@ def cmd_vm(args: argparse.Namespace) -> None: pull_cmd = f"sudo docker login {acr_url} -u 00000000-0000-0000-0000-000000000000 -p '{token}' && sudo docker pull {image}" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", pull_cmd], - capture_output=False # Show output live + capture_output=False, # Show output live ) if result.returncode != 0: print("\n✗ Error pulling image") @@ -2391,11 +2726,11 @@ def cmd_vm(args: argparse.Namespace) -> None: from concurrent.futures import ThreadPoolExecutor, as_completed # Comprehensive one-command WAA setup with multi-worker support - num_workers = getattr(args, 'workers', 1) + num_workers = getattr(args, "workers", 1) - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(" WAA Benchmark Setup - Full Automation") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") print("This will set up everything needed to run WAA benchmarks:") print(" 1. Create Azure VM(s) with nested virtualization") print(" 2. Install Docker with proper disk configuration") @@ -2406,26 +2741,41 @@ def cmd_vm(args: argparse.Namespace) -> None: print(f"\n [Multi-worker mode: creating {num_workers} VMs in parallel]") print() - def create_single_vm(worker_name: str, worker_location: str) -> tuple[str, str | None]: + def create_single_vm( + worker_name: str, worker_location: str + ) -> tuple[str, str | None]: """Create a single VM. Returns (name, ip) or (name, None) on failure.""" locations_to_try = [worker_location, "westus2", "centralus", "eastus2"] for loc in locations_to_try: result = subprocess.run( - ["az", "vm", "create", - "--resource-group", resource_group, - "--name", worker_name, - "--location", loc, - "--image", "Ubuntu2204", - "--size", "Standard_D4ds_v5", - "--admin-username", "azureuser", - "--generate-ssh-keys", - "--public-ip-sku", "Standard", - "--no-wait" if num_workers > 1 else ""], - capture_output=True, text=True + [ + "az", + "vm", + "create", + "--resource-group", + resource_group, + "--name", + worker_name, + "--location", + loc, + "--image", + "Ubuntu2204", + "--size", + "Standard_D4ds_v5", + "--admin-username", + "azureuser", + "--generate-ssh-keys", + "--public-ip-sku", + "Standard", + "--no-wait" if num_workers > 1 else "", + ], + capture_output=True, + text=True, ) if result.returncode == 0: if num_workers == 1: import json as json_mod + vm_info = json_mod.loads(result.stdout) return (worker_name, vm_info.get("publicIpAddress", "")) else: @@ -2442,29 +2792,47 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: "sudo usermod -aG docker $USER", "sudo systemctl stop docker", "sudo mkdir -p /mnt/docker", - "echo '{\"data-root\": \"/mnt/docker\"}' | sudo tee /etc/docker/daemon.json", + 'echo \'{"data-root": "/mnt/docker"}\' | sudo tee /etc/docker/daemon.json', "sudo systemctl start docker", ] result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=30", - f"azureuser@{ip}", " && ".join(docker_cmds)], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=30", + f"azureuser@{ip}", + " && ".join(docker_cmds), + ], + capture_output=True, + text=True, ) if result.returncode != 0: return False # Pull Windows image subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "sudo docker pull dockurr/windows:latest 2>&1 | tail -5"], - capture_output=True, text=True, timeout=300 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "sudo docker pull dockurr/windows:latest 2>&1 | tail -5", + ], + capture_output=True, + text=True, + timeout=300, ) # Clone WAA repo subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "cd ~ && git clone --depth 1 https://github.com/microsoft/WindowsAgentArena.git 2>/dev/null || echo 'Already cloned'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "cd ~ && git clone --depth 1 https://github.com/microsoft/WindowsAgentArena.git 2>/dev/null || echo 'Already cloned'", + ], + capture_output=True, + text=True, ) # Create config @@ -2477,7 +2845,8 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: EOF''' subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", config_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) return True @@ -2485,9 +2854,22 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: if num_workers == 1: # Get VM IP or create VM result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) vm_created = False @@ -2501,19 +2883,32 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: locations_to_try = [location, "westus2", "centralus", "eastus2"] for loc in locations_to_try: result = subprocess.run( - ["az", "vm", "create", - "--resource-group", resource_group, - "--name", vm_name, - "--location", loc, - "--image", "Ubuntu2204", - "--size", "Standard_D4ds_v5", # v5 series supports nested virt - "--admin-username", "azureuser", - "--generate-ssh-keys", - "--public-ip-sku", "Standard"], - capture_output=True, text=True + [ + "az", + "vm", + "create", + "--resource-group", + resource_group, + "--name", + vm_name, + "--location", + loc, + "--image", + "Ubuntu2204", + "--size", + "Standard_D4ds_v5", # v5 series supports nested virt + "--admin-username", + "azureuser", + "--generate-ssh-keys", + "--public-ip-sku", + "Standard", + ], + capture_output=True, + text=True, ) if result.returncode == 0: import json as json_mod + vm_info = json_mod.loads(result.stdout) ip = vm_info.get("publicIpAddress", "") print(f" ✓ VM created in {loc}: {ip}") @@ -2536,24 +2931,38 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Configure Docker to use larger /mnt disk "sudo systemctl stop docker", "sudo mkdir -p /mnt/docker", - "echo '{\"data-root\": \"/mnt/docker\"}' | sudo tee /etc/docker/daemon.json", + 'echo \'{"data-root": "/mnt/docker"}\' | sudo tee /etc/docker/daemon.json', "sudo systemctl start docker", ] result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=30", - f"azureuser@{ip}", " && ".join(docker_cmds)], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=30", + f"azureuser@{ip}", + " && ".join(docker_cmds), + ], + capture_output=True, + text=True, ) if result.returncode != 0: - print(f" ⚠ Docker setup warning: {result.stderr[:200] if result.stderr else 'unknown'}") + print( + f" ⚠ Docker setup warning: {result.stderr[:200] if result.stderr else 'unknown'}" + ) else: print(" ✓ Docker installed with /mnt storage") print("\n[3/6] Verifying nested virtualization...") result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "egrep -c '(vmx|svm)' /proc/cpuinfo"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "egrep -c '(vmx|svm)' /proc/cpuinfo", + ], + capture_output=True, + text=True, ) cpu_count = result.stdout.strip() if cpu_count and int(cpu_count) > 0: @@ -2565,26 +2974,41 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print("\n[4/6] Pulling dockurr/windows image (for Windows VM)...") # Use dockurr/windows directly - the ACR winarena image has broken dockur result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "sudo docker pull dockurr/windows:latest 2>&1 | tail -5"], - capture_output=True, text=True, timeout=300 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "sudo docker pull dockurr/windows:latest 2>&1 | tail -5", + ], + capture_output=True, + text=True, + timeout=300, ) if result.returncode != 0: - print(f" ⚠ Image pull warning: {result.stderr[:100] if result.stderr else ''}") + print( + f" ⚠ Image pull warning: {result.stderr[:100] if result.stderr else ''}" + ) print(" ✓ Windows image pulled") print("\n[5/6] Cloning WindowsAgentArena repository...") result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "cd ~ && git clone --depth 1 https://github.com/microsoft/WindowsAgentArena.git 2>/dev/null || echo 'Already cloned'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "cd ~ && git clone --depth 1 https://github.com/microsoft/WindowsAgentArena.git 2>/dev/null || echo 'Already cloned'", + ], + capture_output=True, + text=True, ) print(" ✓ WAA repo cloned") print("\n[6/6] Creating WAA config file...") api_key = args.api_key or settings.openai_api_key or "" if not api_key: - print(" ⚠ No API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file") + print( + " ⚠ No API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file" + ) api_key = "placeholder-set-your-key" config_cmd = f'''cat > ~/WindowsAgentArena/config.json << 'EOF' @@ -2596,18 +3020,21 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: EOF''' subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", config_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) print(" ✓ Config created") - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(" WAA Setup Complete!") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"\n VM IP: {ip}") print("\n Next step: Prepare Windows image (one-time, ~20 min):") print(" uv run python -m openadapt_ml.benchmarks.cli vm prepare-windows") print("\n Or run WAA directly (will auto-prepare on first run):") - print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print( + " uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5" + ) else: # Multi-worker mode: create multiple VMs in parallel @@ -2616,8 +3043,10 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: created_vms = [] with ThreadPoolExecutor(max_workers=5) as executor: - futures = {executor.submit(create_single_vm, name, location): name - for name in worker_names} + futures = { + executor.submit(create_single_vm, name, location): name + for name in worker_names + } for future in as_completed(futures): name, result_loc = future.result() if result_loc: @@ -2633,15 +3062,29 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Wait for VMs to be ready and get IPs print("\n[2/4] Waiting for VMs to get public IPs...") import time as time_mod + workers_with_ips = [] for _ in range(30): # Wait up to 5 minutes for name in created_vms: if any(n == name for n, _ in workers_with_ips): continue # Already got IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.stdout.strip(): workers_with_ips.append((name, result.stdout.strip())) @@ -2658,13 +3101,19 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Set up Docker and WAA on each VM in parallel api_key = args.api_key or settings.openai_api_key or "" if not api_key: - print(" ⚠ No API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file") + print( + " ⚠ No API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file" + ) api_key = "placeholder-set-your-key" - print(f"\n[3/4] Setting up Docker and WAA on {len(workers_with_ips)} VMs...") + print( + f"\n[3/4] Setting up Docker and WAA on {len(workers_with_ips)} VMs..." + ) with ThreadPoolExecutor(max_workers=5) as executor: - futures = {executor.submit(setup_single_vm, name, ip, api_key): name - for name, ip in workers_with_ips} + futures = { + executor.submit(setup_single_vm, name, ip, api_key): name + for name, ip in workers_with_ips + } for future in as_completed(futures): name = futures[future] success = future.result() @@ -2680,13 +3129,15 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: workers=workers_with_ips, resource_group=resource_group, location=location, - vm_size="Standard_D4ds_v5" + vm_size="Standard_D4ds_v5", + ) + print( + f" ✓ Pool {pool.pool_id} registered with {len(pool.workers)} workers" ) - print(f" ✓ Pool {pool.pool_id} registered with {len(pool.workers)} workers") - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(" Multi-Worker WAA Setup Complete!") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"\n Workers: {len(workers_with_ips)}") for name, ip in workers_with_ips: print(f" - {name}: {ip}") @@ -2696,18 +3147,35 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(" 2. Prepare Windows on all workers (in parallel):") print(" # TODO: implement prepare-windows --pool") print(" 3. Run parallel benchmark:") - print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 30") + print( + " uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 30" + ) elif args.action == "prepare-windows": print("\n=== Preparing Windows 11 VM for WAA (Fully Automated) ===\n") print("This builds a custom WAA container with automatic setup scripts.") - print("First run downloads Windows 11 (~7GB). Setup is fully automatic - no VNC needed.\n") + print( + "First run downloads Windows 11 (~7GB). Setup is fully automatic - no VNC needed.\n" + ) # Get VM IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") @@ -2729,19 +3197,26 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Sync Dockerfile to VM and build subprocess.run( - ["scp", *SSH_OPTS, str(dockerfile_path), - f"azureuser@{ip}:~/build-waa/Dockerfile"], - capture_output=True, text=True + [ + "scp", + *SSH_OPTS, + str(dockerfile_path), + f"azureuser@{ip}:~/build-waa/Dockerfile", + ], + capture_output=True, + text=True, ) - build_cmd = ''' + build_cmd = """ mkdir -p ~/build-waa cp -r ~/WindowsAgentArena/src/win-arena-container/vm ~/build-waa/ cd ~/build-waa && docker build --no-cache --pull -t waa-auto:latest . 2>&1 | tail -10 -''' +""" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", build_cmd], - capture_output=True, text=True, timeout=1800 # 30 min for Docker build + capture_output=True, + text=True, + timeout=1800, # 30 min for Docker build ) if "Successfully" not in result.stdout and result.returncode != 0: print(f" ✗ Failed to build image: {result.stderr}") @@ -2753,15 +3228,20 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Use /mnt/waa-storage for temp disk (115GB) instead of ~/waa-storage (root, <10GB) print("\n[2/4] Cleaning up for fresh Windows installation...") subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker stop winarena 2>/dev/null; docker rm -f winarena 2>/dev/null; " + - "rm -f /mnt/waa-storage/data.img /mnt/waa-storage/windows.* 2>/dev/null; " + - "sudo mkdir -p /mnt/waa-storage /mnt/waa-results; " + - "sudo chown azureuser:azureuser /mnt/waa-storage /mnt/waa-results; " + - "# Migrate old storage if exists\n" + - "[ -d ~/waa-storage ] && mv ~/waa-storage/* /mnt/waa-storage/ 2>/dev/null; " + - "rm -rf ~/waa-storage 2>/dev/null"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker stop winarena 2>/dev/null; docker rm -f winarena 2>/dev/null; " + + "rm -f /mnt/waa-storage/data.img /mnt/waa-storage/windows.* 2>/dev/null; " + + "sudo mkdir -p /mnt/waa-storage /mnt/waa-results; " + + "sudo chown azureuser:azureuser /mnt/waa-storage /mnt/waa-results; " + + "# Migrate old storage if exists\n" + + "[ -d ~/waa-storage ] && mv ~/waa-storage/* /mnt/waa-storage/ 2>/dev/null; " + + "rm -rf ~/waa-storage 2>/dev/null", + ], + capture_output=True, + text=True, ) print(" ✓ Cleanup complete (using /mnt for 115GB temp disk)") @@ -2769,7 +3249,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Use VERSION=11e for Windows 11 Enterprise (accepts GVLK keys, no product key dialog) # Note: VERSION=11 would download Pro, which also works but is less suitable for benchmarks print("\n[3/4] Starting automated WAA container...") - docker_cmd = '''docker run -d \ + docker_cmd = """docker run -d \ --name winarena \ --device=/dev/kvm \ --cap-add NET_ADMIN \ @@ -2782,11 +3262,13 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: -e RAM_SIZE=12G \ -e CPU_CORES=4 \ -e DISK_SIZE=64G \ - waa-auto:latest''' + waa-auto:latest""" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", docker_cmd], - capture_output=True, text=True, timeout=60 + capture_output=True, + text=True, + timeout=60, ) if result.returncode != 0: print(f" ✗ Failed to start container: {result.stderr}") @@ -2795,7 +3277,9 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # Step 4: Wait for Windows to boot (Enterprise edition with GVLK should skip product key) print("\n[4/5] Waiting for Windows to boot...") - print(" Using Windows 11 Enterprise with GVLK key (should skip product key dialog)") + print( + " Using Windows 11 Enterprise with GVLK key (should skip product key dialog)" + ) import time as time_module @@ -2805,12 +3289,20 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: time_module.sleep(10) # Check docker logs for progress log_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker logs winarena 2>&1 | tail -1"], - capture_output=True, text=True, timeout=30 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker logs winarena 2>&1 | tail -1", + ], + capture_output=True, + text=True, + timeout=30, + ) + last_log = ( + log_result.stdout.strip()[:60] if log_result.stdout else "Starting..." ) - last_log = log_result.stdout.strip()[:60] if log_result.stdout else "Starting..." - print(f" [{(i+1)*10}s] {last_log}...") + print(f" [{(i + 1) * 10}s] {last_log}...") # If Windows has started, the log will show "Windows started successfully" if "Windows started" in log_result.stdout: @@ -2837,6 +3329,7 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(" Expected time: ~10-15 minutes\n") import time + for i in range(90): # Wait up to 15 minutes time.sleep(10) @@ -2845,10 +3338,17 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: # See docs/waa_network_architecture.md for architecture details try: probe_result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=5", - f"azureuser@{ip}", - "curl -s --connect-timeout 3 http://localhost:5000/probe 2>/dev/null"], - capture_output=True, text=True, timeout=30 + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=5", + f"azureuser@{ip}", + "curl -s --connect-timeout 3 http://localhost:5000/probe 2>/dev/null", + ], + capture_output=True, + text=True, + timeout=30, ) except subprocess.TimeoutExpired: probe_result = None @@ -2859,17 +3359,28 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: print(f" WAA Server: http://{ip}:5000 (internal via localhost:5000)") print(f" QMP Port: {ip}:7200") print("\n To run WAA benchmarks:") - print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print( + " uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5" + ) break # Show progress from docker logs log_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker logs winarena 2>&1 | tail -2"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker logs winarena 2>&1 | tail -2", + ], + capture_output=True, + text=True, ) - last_log = log_result.stdout.strip().split('\n')[-1][:70] if log_result.stdout else "Starting..." - print(f" [{(i+1)*10}s] {last_log}...") + last_log = ( + log_result.stdout.strip().split("\n")[-1][:70] + if log_result.stdout + else "Starting..." + ) + print(f" [{(i + 1) * 10}s] {last_log}...") else: print(f"\n⚠ Timeout waiting for WAA server. Check: http://{ip}:8006") print(" The Windows VM may still be installing. Try again later.") @@ -2884,17 +3395,25 @@ def setup_single_vm(worker_name: str, ip: str, api_key: str) -> bool: from datetime import datetime # Ensure unbuffered output for real-time streaming - os.environ['PYTHONUNBUFFERED'] = '1' + os.environ["PYTHONUNBUFFERED"] = "1" print("\n=== Running WAA Benchmark ===\n", flush=True) # Helper function to write live status for the viewer - def write_live_status(status: str, phase: str = None, detail: str = None, - task_id: str = None, step: int = None, total_steps: int = None, - tasks_completed: int = 0, total_tasks: int = 0, - current_task: dict = None): + def write_live_status( + status: str, + phase: str = None, + detail: str = None, + task_id: str = None, + step: int = None, + total_steps: int = None, + tasks_completed: int = 0, + total_tasks: int = 0, + current_task: dict = None, + ): """Write status to benchmark_live.json for live viewer updates.""" from pathlib import Path + # Use training_output/current symlink directly to avoid path issues output_dir = Path("training_output/current") if not output_dir.exists(): @@ -2924,13 +3443,28 @@ def write_live_status(status: str, phase: str = None, detail: str = None, live_file.write_text(json.dumps(data, indent=2)) # Initialize with waiting status - write_live_status("setup", phase="initializing", detail="Connecting to Azure VM...") + write_live_status( + "setup", phase="initializing", detail="Connecting to Azure VM..." + ) # Get VM IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") @@ -2938,13 +3472,13 @@ def write_live_status(status: str, phase: str = None, detail: str = None, ip = result.stdout.strip() num_tasks = args.num_tasks - model = getattr(args, 'model', 'gpt-4o') - agent = getattr(args, 'agent', 'navi') - open_viewer = getattr(args, 'open', True) - port = getattr(args, 'port', 8765) - internal_ip = getattr(args, 'internal_ip', '172.30.0.2') - domain = getattr(args, 'domain', None) - task_ids = getattr(args, 'task_ids', None) + model = getattr(args, "model", "gpt-4o") + agent = getattr(args, "agent", "navi") + open_viewer = getattr(args, "open", True) + port = getattr(args, "port", 8765) + internal_ip = getattr(args, "internal_ip", "172.30.0.2") + domain = getattr(args, "domain", None) + task_ids = getattr(args, "task_ids", None) print(f" VM IP: {ip}") print(f" Model: {model}") @@ -2958,8 +3492,10 @@ def write_live_status(status: str, phase: str = None, detail: str = None, # Get API key based on agent type # For api-claude, need ANTHROPIC_API_KEY; for navi/api-openai, need OPENAI_API_KEY - api_key = args.api_key if hasattr(args, 'api_key') and args.api_key else None - anthropic_key = settings.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", "") + api_key = args.api_key if hasattr(args, "api_key") and args.api_key else None + anthropic_key = settings.anthropic_api_key or os.environ.get( + "ANTHROPIC_API_KEY", "" + ) openai_key = api_key or settings.openai_api_key or "" if agent == "api-claude": @@ -2972,7 +3508,9 @@ def write_live_status(status: str, phase: str = None, detail: str = None, else: # navi and api-openai both use OpenAI if not openai_key: - print("✗ No OpenAI API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file") + print( + "✗ No OpenAI API key provided. Set with --api-key, OPENAI_API_KEY env var, or in .env file" + ) sys.exit(1) api_key = openai_key print(" API Key: OPENAI_API_KEY (set)") @@ -2984,11 +3522,17 @@ def write_live_status(status: str, phase: str = None, detail: str = None, # Launch benchmark viewer in background if --open is set # Use the proper server from local.py that has /api/benchmark-live endpoint if open_viewer: - print(f"\n Launching benchmark viewer at http://localhost:{port}/benchmark.html") + print( + f"\n Launching benchmark viewer at http://localhost:{port}/benchmark.html" + ) def start_server(): # Use the full-featured server from local.py with API endpoints - from openadapt_ml.cloud.local import get_current_output_dir, _regenerate_benchmark_viewer_if_available, cmd_serve + from openadapt_ml.cloud.local import ( + get_current_output_dir, + _regenerate_benchmark_viewer_if_available, + cmd_serve, + ) import argparse serve_dir = get_current_output_dir() @@ -2998,7 +3542,9 @@ def start_server(): _regenerate_benchmark_viewer_if_available(serve_dir) # Use cmd_serve with the proper handler that has API endpoints - fake_args = argparse.Namespace(port=port, open=False, no_regenerate=True, quiet=True) + fake_args = argparse.Namespace( + port=port, open=False, no_regenerate=True, quiet=True + ) cmd_serve(fake_args) server_thread = threading.Thread(target=start_server, daemon=True) @@ -3006,6 +3552,7 @@ def start_server(): # Give server time to start import time + time.sleep(1) # Open browser @@ -3015,23 +3562,45 @@ def start_server(): # Ensure Docker is running (may not auto-start after VM restart) print("[1/5] Ensuring Docker is running...", flush=True) - write_live_status("setup", phase="docker", detail="Ensuring Docker is running...", total_tasks=num_tasks) + write_live_status( + "setup", + phase="docker", + detail="Ensuring Docker is running...", + total_tasks=num_tasks, + ) if not ensure_docker_running(ip): - write_live_status("error", phase="docker", detail="Docker is not running and could not be started") + write_live_status( + "error", + phase="docker", + detail="Docker is not running and could not be started", + ) print("✗ Docker is not running and could not be started.", flush=True) - print(" Try: uv run python -m openadapt_ml.benchmarks.cli vm diag", flush=True) + print( + " Try: uv run python -m openadapt_ml.benchmarks.cli vm diag", + flush=True, + ) sys.exit(1) print(" ✓ Docker is running", flush=True) # Stop any existing container - fresh = getattr(args, 'fresh', False) + fresh = getattr(args, "fresh", False) step = 2 print(f"[{step}/5] Stopping any existing WAA container...") - write_live_status("setup", phase="container_stop", detail="Stopping any existing WAA container...", total_tasks=num_tasks) + write_live_status( + "setup", + phase="container_stop", + detail="Stopping any existing WAA container...", + total_tasks=num_tasks, + ) subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker stop winarena 2>/dev/null; docker rm -f winarena 2>/dev/null"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker stop winarena 2>/dev/null; docker rm -f winarena 2>/dev/null", + ], + capture_output=True, + text=True, ) # If --fresh, delete Windows storage and reinstall @@ -3051,23 +3620,30 @@ def start_server(): """ result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", cleanup_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.stdout.strip(): - for line in result.stdout.strip().split('\n'): + for line in result.stdout.strip().split("\n"): print(f" {line}") print(" ✓ Windows storage reset - fresh install will begin") # Ensure waa-auto image exists (auto-rebuild if needed) - rebuild = getattr(args, 'rebuild', False) + rebuild = getattr(args, "rebuild", False) print("[3/5] Checking waa-auto Docker image...", flush=True) - write_live_status("setup", phase="image_check", detail="Checking waa-auto Docker image...", total_tasks=num_tasks) + write_live_status( + "setup", + phase="image_check", + detail="Checking waa-auto Docker image...", + total_tasks=num_tasks, + ) # Check if waa-auto exists and is recent (built with current dockurr/windows) check_image_cmd = "docker images waa-auto:latest --format '{{.ID}}' | head -1" check_result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", check_image_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) waa_auto_exists = bool(check_result.stdout.strip()) @@ -3084,9 +3660,14 @@ def start_server(): api_agent_path = waa_deploy_dir / "api_agent.py" if dockerfile_path.exists(): scp_result = subprocess.run( - ["scp", *SSH_OPTS, - str(dockerfile_path), f"azureuser@{ip}:~/Dockerfile.waa"], - capture_output=True, text=True + [ + "scp", + *SSH_OPTS, + str(dockerfile_path), + f"azureuser@{ip}:~/Dockerfile.waa", + ], + capture_output=True, + text=True, ) if scp_result.returncode != 0: print(f" ✗ Failed to copy Dockerfile: {scp_result.stderr}") @@ -3095,12 +3676,19 @@ def start_server(): # Copy api_agent.py (required by Dockerfile) if api_agent_path.exists(): scp_result = subprocess.run( - ["scp", *SSH_OPTS, - str(api_agent_path), f"azureuser@{ip}:~/api_agent.py"], - capture_output=True, text=True + [ + "scp", + *SSH_OPTS, + str(api_agent_path), + f"azureuser@{ip}:~/api_agent.py", + ], + capture_output=True, + text=True, ) if scp_result.returncode != 0: - print(f" ✗ Failed to copy api_agent.py: {scp_result.stderr}") + print( + f" ✗ Failed to copy api_agent.py: {scp_result.stderr}" + ) sys.exit(1) else: print(f" ✗ api_agent.py not found at {api_agent_path}") @@ -3108,13 +3696,18 @@ def start_server(): # Build the image (using /home/azureuser as context to avoid /tmp issues) print(" Building waa-auto image (streaming output)...") - print(" This may take 5-15 minutes for first build (model weights are ~2GB)...") + print( + " This may take 5-15 minutes for first build (model weights are ~2GB)..." + ) print(flush=True) # Update live status for build phase - write_live_status("setup", phase="docker_build", - detail="Building waa-auto Docker image...", - total_tasks=num_tasks) + write_live_status( + "setup", + phase="docker_build", + detail="Building waa-auto Docker image...", + total_tasks=num_tasks, + ) # Stream build output so user can see progress # Note: --progress=plain requires BuildKit; fall back to legacy builder without it @@ -3125,7 +3718,7 @@ def start_server(): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 + bufsize=1, ) build_output = [] @@ -3139,23 +3732,44 @@ def start_server(): line = line.rstrip() build_output.append(line) # Show key progress lines - if any(x in line.lower() for x in ['step', 'copying', 'downloading', '#', 'cached', 'done', 'error', 'failed']): - print(f" {line[-100:]}", flush=True) # Truncate long lines, flush immediately + if any( + x in line.lower() + for x in [ + "step", + "copying", + "downloading", + "#", + "cached", + "done", + "error", + "failed", + ] + ): + print( + f" {line[-100:]}", flush=True + ) # Truncate long lines, flush immediately # Update live status periodically (every 10 lines) if len(build_output) - last_status_update >= 10: last_status_update = len(build_output) - write_live_status("setup", phase="docker_build", - detail=f"Building... {line[-60:]}", - total_tasks=num_tasks) + write_live_status( + "setup", + phase="docker_build", + detail=f"Building... {line[-60:]}", + total_tasks=num_tasks, + ) except subprocess.TimeoutExpired: build_process.kill() print(" ✗ Build timed out after 30 minutes", flush=True) - write_live_status("error", phase="docker_build", detail="Build timed out") + write_live_status( + "error", phase="docker_build", detail="Build timed out" + ) sys.exit(1) - full_output = '\n'.join(build_output) - if "Successfully tagged waa-auto:latest" in full_output or \ - "naming to docker.io/library/waa-auto:latest" in full_output: + full_output = "\n".join(build_output) + if ( + "Successfully tagged waa-auto:latest" in full_output + or "naming to docker.io/library/waa-auto:latest" in full_output + ): print() print(" ✓ waa-auto image built successfully") else: @@ -3165,14 +3779,26 @@ def start_server(): print(f" {line}") print() print(" ✗ CRITICAL: waa-auto build failed!") - print(" The official windowsarena/winarena image is BROKEN (uses outdated dockurr/windows v0.00)") - print(" The waa-auto image is REQUIRED for Windows 11 to auto-download.") + print( + " The official windowsarena/winarena image is BROKEN (uses outdated dockurr/windows v0.00)" + ) + print( + " The waa-auto image is REQUIRED for Windows 11 to auto-download." + ) print() print(" Troubleshooting:") - print(" 1. Check Docker storage: uv run python -m openadapt_ml.benchmarks.cli vm diag") - print(" 2. If disk full: uv run python -m openadapt_ml.benchmarks.cli vm fix-storage") - print(" 3. Clean Docker: ssh azureuser@ 'docker system prune -af'") - print(" 4. Retry: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") + print( + " 1. Check Docker storage: uv run python -m openadapt_ml.benchmarks.cli vm diag" + ) + print( + " 2. If disk full: uv run python -m openadapt_ml.benchmarks.cli vm fix-storage" + ) + print( + " 3. Clean Docker: ssh azureuser@ 'docker system prune -af'" + ) + print( + " 4. Retry: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild" + ) sys.exit(1) else: print(f" ✗ CRITICAL: Dockerfile not found at {dockerfile_path}") @@ -3186,17 +3812,22 @@ def start_server(): verify_cmd = "docker images waa-auto:latest --format '{{.Repository}}:{{.Tag}}' | head -1" verify_result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", verify_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) if verify_result.stdout.strip() == "waa-auto:latest": docker_image = "waa-auto:latest" print(f" ✓ Using: {docker_image} (with dockurr/windows auto-download)") else: print(" ✗ CRITICAL: waa-auto image not found!") - print(" The official windowsarena/winarena image is BROKEN and cannot be used.") + print( + " The official windowsarena/winarena image is BROKEN and cannot be used." + ) print() print(" Run with --rebuild to build waa-auto:") - print(f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild --num-tasks {num_tasks}") + print( + f" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild --num-tasks {num_tasks}" + ) sys.exit(1) # Start WAA container with full benchmark run @@ -3234,18 +3865,28 @@ def start_server(): "/entry.sh --start-client true --model {model} --agent {agent} --result-dir /results{task_filter_args}"''' # Update status to running - write_live_status("running", phase="benchmark", detail="Starting WAA benchmark...", - total_tasks=num_tasks, tasks_completed=0) + write_live_status( + "running", + phase="benchmark", + detail="Starting WAA benchmark...", + total_tasks=num_tasks, + tasks_completed=0, + ) # Use Popen to stream output and parse progress in real-time # SSH_OPTS includes keepalive settings (ServerAliveInterval=60, ServerAliveCountMax=10) # to prevent timeout during long benchmark runs (1.5+ hours) process = subprocess.Popen( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", f"mkdir -p ~/waa-results && {docker_cmd}"], + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + f"mkdir -p ~/waa-results && {docker_cmd}", + ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 # Line buffered + bufsize=1, # Line buffered ) # Parse output in real-time to update live status @@ -3254,10 +3895,17 @@ def start_server(): current_step = 0 # Patterns for WAA output parsing - task_start_pattern = re.compile(r'(?:Starting|Running)\s+task[:\s]+(\S+)', re.IGNORECASE) - task_complete_pattern = re.compile(r'(?:Task|Completed)[:\s]+(\S+)[:\s]+(?:PASS|FAIL|SUCCESS|FAILED|score)', re.IGNORECASE) - step_pattern = re.compile(r'(?:Step|Action)\s+(\d+)', re.IGNORECASE) - windows_boot_pattern = re.compile(r'(?:Booting|Starting|Windows|QEMU|KVM)', re.IGNORECASE) + task_start_pattern = re.compile( + r"(?:Starting|Running)\s+task[:\s]+(\S+)", re.IGNORECASE + ) + task_complete_pattern = re.compile( + r"(?:Task|Completed)[:\s]+(\S+)[:\s]+(?:PASS|FAIL|SUCCESS|FAILED|score)", + re.IGNORECASE, + ) + step_pattern = re.compile(r"(?:Step|Action)\s+(\d+)", re.IGNORECASE) + windows_boot_pattern = re.compile( + r"(?:Booting|Starting|Windows|QEMU|KVM)", re.IGNORECASE + ) try: for line in process.stdout: @@ -3266,48 +3914,65 @@ def start_server(): # Parse Windows boot progress if windows_boot_pattern.search(line): - if 'download' in line.lower(): - write_live_status("setup", phase="windows_download", - detail=line[:100], total_tasks=num_tasks) - elif 'install' in line.lower() or 'boot' in line.lower(): - write_live_status("setup", phase="windows_boot", - detail=line[:100], total_tasks=num_tasks) + if "download" in line.lower(): + write_live_status( + "setup", + phase="windows_download", + detail=line[:100], + total_tasks=num_tasks, + ) + elif "install" in line.lower() or "boot" in line.lower(): + write_live_status( + "setup", + phase="windows_boot", + detail=line[:100], + total_tasks=num_tasks, + ) # Parse task start task_start = task_start_pattern.search(line) if task_start: current_task_id = task_start.group(1) current_step = 0 - write_live_status("running", phase="task", - detail=f"Running task: {current_task_id}", - task_id=current_task_id, - step=current_step, - tasks_completed=tasks_completed, - total_tasks=num_tasks, - current_task={"task_id": current_task_id, "instruction": ""}) + write_live_status( + "running", + phase="task", + detail=f"Running task: {current_task_id}", + task_id=current_task_id, + step=current_step, + tasks_completed=tasks_completed, + total_tasks=num_tasks, + current_task={"task_id": current_task_id, "instruction": ""}, + ) # Parse step progress step_match = step_pattern.search(line) if step_match and current_task_id: current_step = int(step_match.group(1)) - write_live_status("running", phase="step", - detail=line[:100], - task_id=current_task_id, - step=current_step, - tasks_completed=tasks_completed, - total_tasks=num_tasks) + write_live_status( + "running", + phase="step", + detail=line[:100], + task_id=current_task_id, + step=current_step, + tasks_completed=tasks_completed, + total_tasks=num_tasks, + ) # Parse task completion task_complete = task_complete_pattern.search(line) if task_complete: completed_task = task_complete.group(1) tasks_completed += 1 - success = 'pass' in line.lower() or 'success' in line.lower() - write_live_status("running", phase="task_complete", - detail=f"Completed: {completed_task} ({'PASS' if success else 'FAIL'})", - task_id=completed_task, - tasks_completed=tasks_completed, - total_tasks=num_tasks) + success = "pass" in line.lower() or "success" in line.lower() + write_live_status( + "running", + phase="task_complete", + detail=f"Completed: {completed_task} ({'PASS' if success else 'FAIL'})", + task_id=completed_task, + tasks_completed=tasks_completed, + total_tasks=num_tasks, + ) # Wait for process to complete process.wait() @@ -3319,41 +3984,78 @@ def start_server(): returncode = 1 if returncode == 0: - write_live_status("complete", detail=f"Benchmark complete! {tasks_completed}/{num_tasks} tasks", - tasks_completed=tasks_completed, total_tasks=num_tasks) + write_live_status( + "complete", + detail=f"Benchmark complete! {tasks_completed}/{num_tasks} tasks", + tasks_completed=tasks_completed, + total_tasks=num_tasks, + ) print("\n✓ WAA evaluation complete!") print("\n Results saved to: ~/waa-results on the VM") - print(f" To download: scp azureuser@{ip}:~/waa-results/* ./benchmark_results/") + print( + f" To download: scp azureuser@{ip}:~/waa-results/* ./benchmark_results/" + ) else: - write_live_status("error", detail=f"Benchmark finished with errors (exit code: {returncode})", - tasks_completed=tasks_completed, total_tasks=num_tasks) + write_live_status( + "error", + detail=f"Benchmark finished with errors (exit code: {returncode})", + tasks_completed=tasks_completed, + total_tasks=num_tasks, + ) print(f"\n⚠ WAA run finished with issues (exit code: {returncode})") # Auto-shutdown VM if --auto-shutdown flag is set - auto_shutdown = getattr(args, 'auto_shutdown', False) + auto_shutdown = getattr(args, "auto_shutdown", False) if auto_shutdown: print("\n=== Auto-shutdown: Deallocating VM to save costs ===\n") deallocate_result = subprocess.run( - ["az", "vm", "deallocate", "-g", resource_group, "-n", vm_name, "--no-wait"], - capture_output=True, text=True + [ + "az", + "vm", + "deallocate", + "-g", + resource_group, + "-n", + vm_name, + "--no-wait", + ], + capture_output=True, + text=True, ) if deallocate_result.returncode == 0: print(f"✓ VM '{vm_name}' deallocation initiated") print("\n Cost savings: Deallocated VMs do not incur compute charges.") - print(" Note: Storage costs still apply. Delete VM with 'vm delete' to stop all charges.") + print( + " Note: Storage costs still apply. Delete VM with 'vm delete' to stop all charges." + ) print(f" To restart: az vm start -g {resource_group} -n {vm_name}") else: print(f"✗ Failed to deallocate VM: {deallocate_result.stderr}") elif args.action == "fix-storage": print("\n=== Fix WAA Storage (Move to /mnt for More Space) ===\n") - print("Moves WAA storage from root disk (~10GB free) to /mnt temp disk (~115GB free).\n") + print( + "Moves WAA storage from root disk (~10GB free) to /mnt temp disk (~115GB free).\n" + ) # Get VM IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") @@ -3372,16 +4074,22 @@ def start_server(): """ result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", check_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) print(result.stdout) # Step 2: Stop container print("[2/4] Stopping WAA container...") subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker stop winarena 2>/dev/null; docker rm winarena 2>/dev/null"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker stop winarena 2>/dev/null; docker rm winarena 2>/dev/null", + ], + capture_output=True, + text=True, ) print(" ✓ Container stopped") @@ -3406,14 +4114,15 @@ def start_server(): """ result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", move_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) print(result.stdout) print(" ✓ Storage moved to /mnt/waa-storage") # Step 4: Restart container with new mount print("\n[4/4] Restarting WAA container with /mnt storage...") - docker_cmd = '''docker run -d \ + docker_cmd = """docker run -d \ --name winarena \ --device=/dev/kvm \ --cap-add NET_ADMIN \ @@ -3424,20 +4133,22 @@ def start_server(): -e RAM_SIZE=12G \ -e CPU_CORES=4 \ -e DISK_SIZE=64G \ - waa-auto:latest''' + waa-auto:latest""" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", docker_cmd], - capture_output=True, text=True, timeout=60 + capture_output=True, + text=True, + timeout=60, ) if result.returncode != 0: print(f" ✗ Failed to start container: {result.stderr}") sys.exit(1) print(" ✓ WAA container restarted with /mnt storage") - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(" Storage Fixed!") - print(f"{'='*60}") + print(f"{'=' * 60}") print("\n Storage now on /mnt: ~115GB available") print(f" VNC: http://{ip}:8006") print("\n If Windows was installing, it will resume automatically.") @@ -3458,26 +4169,36 @@ def start_server(): # Check disk space before print("[1/3] Current disk usage...") df_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'", + ], + capture_output=True, + text=True, ) print(f" {df_result.stdout}") # Docker system prune print("[2/3] Cleaning Docker (images, containers, build cache)...") prune_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker system prune -af --volumes 2>&1"], - capture_output=True, text=True, - timeout=300 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker system prune -af --volumes 2>&1", + ], + capture_output=True, + text=True, + timeout=300, ) if prune_result.returncode == 0: # Extract space reclaimed output = prune_result.stdout print(" ✓ Docker cleanup complete") if "Total reclaimed space" in output: - for line in output.split('\n'): + for line in output.split("\n"): if "Total reclaimed space" in line: print(f" {line.strip()}") else: @@ -3486,12 +4207,19 @@ def start_server(): # Check disk space after print("[3/3] Disk usage after cleanup...") df_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'", + ], + capture_output=True, + text=True, ) print(f" {df_result.stdout}") - print("\n Retry build: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") + print( + "\n Retry build: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild" + ) elif args.action == "docker-move": print("\n=== Move Docker Data to /mnt (147GB) ===\n") @@ -3510,19 +4238,29 @@ def start_server(): # Step 1: Check current disk usage print("[1/5] Current disk usage...") df_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'", + ], + capture_output=True, + text=True, ) print(f" {df_result.stdout}") # Step 2: Stop Docker and containerd print("[2/5] Stopping Docker and containerd...") stop_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "sudo systemctl stop docker docker.socket containerd 2>&1 && echo 'stopped'"], - capture_output=True, text=True, - timeout=60 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "sudo systemctl stop docker docker.socket containerd 2>&1 && echo 'stopped'", + ], + capture_output=True, + text=True, + timeout=60, ) if "stopped" in stop_result.stdout: print(" ✓ Docker and containerd stopped") @@ -3531,7 +4269,7 @@ def start_server(): # Step 3: Create symlinks from /var/lib/docker and /var/lib/containerd to /mnt print("[3/5] Creating symlinks to /mnt (most reliable method)...") - config_cmd = ''' + config_cmd = """ # Create directories on /mnt sudo mkdir -p /mnt/docker sudo mkdir -p /mnt/containerd @@ -3545,11 +4283,12 @@ def start_server(): # Verify symlinks ls -la /var/lib/docker /var/lib/containerd 2>&1 | head -4 echo "configured" -''' +""" config_result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", config_cmd], - capture_output=True, text=True, - timeout=120 + capture_output=True, + text=True, + timeout=120, ) if "configured" in config_result.stdout: print(" ✓ Docker configured to use /mnt/docker") @@ -3559,10 +4298,15 @@ def start_server(): # Step 4: Start Docker and containerd print("[4/5] Starting Docker and containerd...") start_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "sudo systemctl start containerd docker && sleep 3 && docker info 2>&1 | head -3"], - capture_output=True, text=True, - timeout=60 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "sudo systemctl start containerd docker && sleep 3 && docker info 2>&1 | head -3", + ], + capture_output=True, + text=True, + timeout=60, ) if "Client:" in start_result.stdout or "Server:" in start_result.stdout: print(" ✓ Docker started with new data root") @@ -3570,37 +4314,49 @@ def start_server(): print(f" Warning: {start_result.stderr[:200]}") print(" Trying to start Docker again...") subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "sudo systemctl start docker"], - capture_output=True, text=True, - timeout=30 + ["ssh", *SSH_OPTS, f"azureuser@{ip}", "sudo systemctl start docker"], + capture_output=True, + text=True, + timeout=30, ) # Step 5: Verify new data root print("[5/5] Verifying Docker data root...") verify_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker info 2>&1 | grep 'Docker Root Dir'"], - capture_output=True, text=True, - timeout=30 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker info 2>&1 | grep 'Docker Root Dir'", + ], + capture_output=True, + text=True, + timeout=30, ) print(f" {verify_result.stdout.strip()}") # Check disk after print("\n Disk usage after move:") df_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "df -h / /mnt 2>/dev/null | grep -E 'Filesystem|/dev'", + ], + capture_output=True, + text=True, ) print(f" {df_result.stdout}") - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(" Docker Data Moved to /mnt!") - print(f"{'='*60}") + print(f"{'=' * 60}") print("\n Root disk now has space for OS only.") print(" Docker images will use /mnt/docker (147GB available).") - print("\n Next: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") + print( + "\n Next: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild" + ) elif args.action == "reset-windows": print("\n=== Reset Windows (Clean Install) ===\n") @@ -3608,9 +4364,22 @@ def start_server(): # Get VM IP result = subprocess.run( - ["az", "vm", "show", "-d", "-g", resource_group, "-n", vm_name, - "--query", "publicIps", "-o", "tsv"], - capture_output=True, text=True + [ + "az", + "vm", + "show", + "-d", + "-g", + resource_group, + "-n", + vm_name, + "--query", + "publicIps", + "-o", + "tsv", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or not result.stdout.strip(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") @@ -3623,9 +4392,14 @@ def start_server(): # Step 1: Stop container print("[1/3] Stopping WAA container...") subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker stop winarena 2>/dev/null; docker rm winarena 2>/dev/null"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker stop winarena 2>/dev/null; docker rm winarena 2>/dev/null", + ], + capture_output=True, + text=True, ) print(" ✓ Container stopped") @@ -3643,14 +4417,15 @@ def start_server(): """ result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", cleanup_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) print(result.stdout) print(" ✓ Disk image deleted (ISO cache preserved for faster reinstall)") # Step 3: Restart with fresh install print("\n[3/3] Starting fresh Windows installation...") - docker_cmd = '''docker run -d \ + docker_cmd = """docker run -d \ --name winarena \ --device=/dev/kvm \ --cap-add NET_ADMIN \ @@ -3661,11 +4436,13 @@ def start_server(): -e RAM_SIZE=12G \ -e CPU_CORES=4 \ -e DISK_SIZE=64G \ - waa-auto:latest''' + waa-auto:latest""" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", docker_cmd], - capture_output=True, text=True, timeout=60 + capture_output=True, + text=True, + timeout=60, ) if result.returncode != 0: print(f" ✗ Failed to start container: {result.stderr}") @@ -3678,6 +4455,7 @@ def start_server(): print(" WAA server will start on port 5000 when ready.\n") import time + for i in range(45): # Wait up to 15 minutes time.sleep(20) @@ -3686,10 +4464,17 @@ def start_server(): # See docs/waa_network_architecture.md for architecture details try: probe_result = subprocess.run( - ["ssh", *SSH_OPTS, "-o", "ConnectTimeout=5", - f"azureuser@{ip}", - "curl -s --connect-timeout 3 http://localhost:5000/probe 2>/dev/null"], - capture_output=True, text=True, timeout=30 + [ + "ssh", + *SSH_OPTS, + "-o", + "ConnectTimeout=5", + f"azureuser@{ip}", + "curl -s --connect-timeout 3 http://localhost:5000/probe 2>/dev/null", + ], + capture_output=True, + text=True, + timeout=30, ) except subprocess.TimeoutExpired: probe_result = None @@ -3698,17 +4483,28 @@ def start_server(): print("\n✓ WAA Server ready!") print(f" Probe response: {probe_result.stdout.strip()[:100]}") print("\n To run benchmarks:") - print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print( + " uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5" + ) break # Show progress from docker logs log_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker logs winarena 2>&1 | tail -2"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker logs winarena 2>&1 | tail -2", + ], + capture_output=True, + text=True, ) - last_log = log_result.stdout.strip().split('\n')[-1][:70] if log_result.stdout else "Starting..." - print(f" [{(i+1)*20}s] {last_log}...") + last_log = ( + log_result.stdout.strip().split("\n")[-1][:70] + if log_result.stdout + else "Starting..." + ) + print(f" [{(i + 1) * 20}s] {last_log}...") else: print(f"\n⚠ Timeout waiting for WAA. Check: http://{ip}:8006") print(" Windows installation may still be in progress.") @@ -3729,10 +4525,14 @@ def start_server(): if result_path: print(f" ✓ Screenshot saved to: {result_path}") - print("\n View at: http://localhost:8080/vm_screenshot.png (if server running)") + print( + "\n View at: http://localhost:8080/vm_screenshot.png (if server running)" + ) else: print(" ✗ Failed to capture screenshot") - print(" Make sure the winarena container is running and QEMU monitor is accessible.") + print( + " Make sure the winarena container is running and QEMU monitor is accessible." + ) sys.exit(1) elif args.action == "probe": @@ -3747,15 +4547,22 @@ def start_server(): # Use 172.30.0.2 for our custom waa-auto image (dockurr/windows base) # Use 20.20.20.21 for official windowsarena/winarena image - internal_ip = getattr(args, 'internal_ip', '172.30.0.2') + internal_ip = getattr(args, "internal_ip", "172.30.0.2") - if getattr(args, 'wait', False): + if getattr(args, "wait", False): # Polling mode - keep checking until ready - max_attempts = getattr(args, 'max_attempts', 30) - interval = getattr(args, 'interval', 20) - if poll_waa_probe(ip, max_attempts=max_attempts, interval=interval, internal_ip=internal_ip): + max_attempts = getattr(args, "max_attempts", 30) + interval = getattr(args, "interval", 20) + if poll_waa_probe( + ip, + max_attempts=max_attempts, + interval=interval, + internal_ip=internal_ip, + ): print("\n Ready to run benchmarks:") - print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print( + " uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5" + ) else: print("\n VNC (via SSH tunnel): http://localhost:8006") print(f" Start tunnel: ssh -L 8006:{ip}:8006 azureuser@{ip}") @@ -3770,7 +4577,9 @@ def start_server(): print(" ✓ WAA server is READY") print(f" Response: {response[:100] if response else '(empty)'}") print("\n Ready to run benchmarks:") - print(" uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5") + print( + " uv run python -m openadapt_ml.benchmarks.cli vm run-waa --num-tasks 5" + ) else: print(" ✗ WAA server NOT responding") print("\n To poll until ready, use: vm probe --wait") @@ -3778,7 +4587,11 @@ def start_server(): print(f" Start tunnel: ssh -L 8006:{ip}:8006 azureuser@{ip}") elif args.action == "pool-status": - from openadapt_ml.benchmarks.vm_monitor import VMPoolRegistry, VMMonitor, VMConfig + from openadapt_ml.benchmarks.vm_monitor import ( + VMPoolRegistry, + VMMonitor, + VMConfig, + ) print("\n=== VM Pool Status ===\n") @@ -3802,10 +4615,12 @@ def start_server(): for w in pool.workers: waa_status = "Ready" if w.waa_ready else "---" task_progress = f"{len(w.completed_tasks)}/{len(w.assigned_tasks)}" - print(f"{w.name:<15} {w.ip:<16} {w.status:<12} {waa_status:<6} {task_progress:<10}") + print( + f"{w.name:<15} {w.ip:<16} {w.status:<12} {waa_status:<6} {task_progress:<10}" + ) # Optionally probe each VM for live status - if getattr(args, 'wait', False): # Reuse --wait flag for probing + if getattr(args, "wait", False): # Reuse --wait flag for probing print("\nProbing VMs for WAA readiness...") for w in pool.workers: monitor = VMMonitor(VMConfig(name=w.name, ssh_host=w.ip)) @@ -3829,9 +4644,9 @@ def start_server(): for w in pool.workers: print(f" - {w.name} ({w.ip})") - if not getattr(args, 'yes', False): + if not getattr(args, "yes", False): confirm = input("\nType 'yes' to confirm: ") - if confirm.lower() != 'yes': + if confirm.lower() != "yes": print("Aborted.") sys.exit(0) @@ -3839,7 +4654,8 @@ def start_server(): def delete_vm(name: str) -> tuple[str, bool, str]: result = subprocess.run( ["az", "vm", "delete", "-g", pool.resource_group, "-n", name, "--yes"], - capture_output=True, text=True + capture_output=True, + text=True, ) if result.returncode == 0: return (name, True, "deleted") @@ -3876,10 +4692,16 @@ def delete_vm(name: str) -> tuple[str, bool, str]: print("Checking Azure ML jobs...") result = subprocess.run( [ - "az", "ml", "job", "list", - "--resource-group", resource_group, - "--workspace-name", "openadapt-ml", - "-o", "json", + "az", + "ml", + "job", + "list", + "--resource-group", + resource_group, + "--workspace-name", + "openadapt-ml", + "-o", + "json", ], capture_output=True, text=True, @@ -3897,7 +4719,9 @@ def delete_vm(name: str) -> tuple[str, bool, str]: continue # Parse creation time - creation_time_str = job.get("creation_context", {}).get("created_at") + creation_time_str = job.get("creation_context", {}).get( + "created_at" + ) if not creation_time_str: continue @@ -3912,13 +4736,15 @@ def delete_vm(name: str) -> tuple[str, bool, str]: duration_hours = (now - creation_time).total_seconds() / 3600 if duration_hours > args.max_hours: - stale_jobs.append({ - "name": job.get("name", "unknown"), - "display_name": job.get("display_name", ""), - "status": status, - "duration_hours": duration_hours, - "created_at": creation_time_str, - }) + stale_jobs.append( + { + "name": job.get("name", "unknown"), + "display_name": job.get("display_name", ""), + "status": status, + "duration_hours": duration_hours, + "created_at": creation_time_str, + } + ) except (ValueError, TypeError): continue @@ -3931,9 +4757,14 @@ def delete_vm(name: str) -> tuple[str, bool, str]: print("Checking Azure VMs...") result = subprocess.run( [ - "az", "vm", "list", "-d", - "--resource-group", resource_group, - "-o", "json", + "az", + "vm", + "list", + "-d", + "--resource-group", + resource_group, + "-o", + "json", ], capture_output=True, text=True, @@ -3955,10 +4786,15 @@ def delete_vm(name: str) -> tuple[str, bool, str]: # Get VM instance view for start time instance_result = subprocess.run( [ - "az", "vm", "get-instance-view", - "--resource-group", resource_group, - "--name", vm_name, - "-o", "json", + "az", + "vm", + "get-instance-view", + "--resource-group", + resource_group, + "--name", + vm_name, + "-o", + "json", ], capture_output=True, text=True, @@ -3968,26 +4804,44 @@ def delete_vm(name: str) -> tuple[str, bool, str]: try: instance_view = json.loads(instance_result.stdout) # Look for VM start time in statuses - statuses = instance_view.get("instanceView", {}).get("statuses", []) + statuses = instance_view.get("instanceView", {}).get( + "statuses", [] + ) for status in statuses: - if status.get("code", "").startswith("PowerState/running"): + if status.get("code", "").startswith( + "PowerState/running" + ): start_time_str = status.get("time") if start_time_str: try: - start_time_str = start_time_str.replace("Z", "+00:00") - start_time = datetime.fromisoformat(start_time_str) + start_time_str = start_time_str.replace( + "Z", "+00:00" + ) + start_time = datetime.fromisoformat( + start_time_str + ) if start_time.tzinfo is None: - start_time = start_time.replace(tzinfo=timezone.utc) + start_time = start_time.replace( + tzinfo=timezone.utc + ) - duration_hours = (now - start_time).total_seconds() / 3600 + duration_hours = ( + now - start_time + ).total_seconds() / 3600 if duration_hours > args.vm_max_hours: - stale_vms.append({ - "name": vm_name, - "size": vm.get("hardwareProfile", {}).get("vmSize", "unknown"), - "duration_hours": duration_hours, - "public_ip": vm.get("publicIps", ""), - }) + stale_vms.append( + { + "name": vm_name, + "size": vm.get( + "hardwareProfile", {} + ).get("vmSize", "unknown"), + "duration_hours": duration_hours, + "public_ip": vm.get( + "publicIps", "" + ), + } + ) except (ValueError, TypeError): pass break @@ -4012,7 +4866,7 @@ def delete_vm(name: str) -> tuple[str, bool, str]: print("-" * 64) for job in stale_jobs: duration_str = f"{job['duration_hours']:.1f}h" - name = job['name'][:38] + ".." if len(job['name']) > 40 else job['name'] + name = job["name"][:38] + ".." if len(job["name"]) > 40 else job["name"] print(f"{name:<40} {job['status']:<12} {duration_str:<12}") print() @@ -4022,11 +4876,13 @@ def delete_vm(name: str) -> tuple[str, bool, str]: print("-" * 75) for vm in stale_vms: duration_str = f"{vm['duration_hours']:.1f}h" - print(f"{vm['name']:<25} {vm['size']:<20} {duration_str:<12} {vm['public_ip']:<16}") + print( + f"{vm['name']:<25} {vm['size']:<20} {duration_str:<12} {vm['public_ip']:<16}" + ) print() # --- Confirmation --- - if not getattr(args, 'yes', False): + if not getattr(args, "yes", False): actions = [] if stale_jobs: actions.append(f"cancel {len(stale_jobs)} job(s)") @@ -4034,7 +4890,7 @@ def delete_vm(name: str) -> tuple[str, bool, str]: actions.append(f"deallocate {len(stale_vms)} VM(s)") confirm = input(f"This will {' and '.join(actions)}. Continue? (y/N): ") - if confirm.lower() != 'y': + if confirm.lower() != "y": print("Cancelled.") return @@ -4044,10 +4900,16 @@ def delete_vm(name: str) -> tuple[str, bool, str]: for job in stale_jobs: result = subprocess.run( [ - "az", "ml", "job", "cancel", - "--name", job["name"], - "--resource-group", resource_group, - "--workspace-name", "openadapt-ml", + "az", + "ml", + "job", + "cancel", + "--name", + job["name"], + "--resource-group", + resource_group, + "--workspace-name", + "openadapt-ml", ], capture_output=True, text=True, @@ -4063,9 +4925,13 @@ def delete_vm(name: str) -> tuple[str, bool, str]: for vm in stale_vms: result = subprocess.run( [ - "az", "vm", "deallocate", - "--resource-group", resource_group, - "--name", vm["name"], + "az", + "vm", + "deallocate", + "--resource-group", + resource_group, + "--name", + vm["name"], "--no-wait", ], capture_output=True, @@ -4085,21 +4951,24 @@ def delete_vm(name: str) -> tuple[str, bool, str]: import time from datetime import datetime, timedelta - port = getattr(args, 'port', 8765) - auto_shutdown_hours = getattr(args, 'auto_shutdown_hours', 0) + port = getattr(args, "port", 8765) + auto_shutdown_hours = getattr(args, "auto_shutdown_hours", 0) print("\n=== VM Monitor Dashboard ===\n") # Check if server is already running on port def is_port_in_use(port: int) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(('localhost', port)) == 0 + return s.connect_ex(("localhost", port)) == 0 if is_port_in_use(port): print(f" Dashboard already running on port {port}") else: print(f" Starting dashboard server on port {port}...") # Start server in background - from openadapt_ml.cloud.local import get_current_output_dir, _regenerate_benchmark_viewer_if_available + from openadapt_ml.cloud.local import ( + get_current_output_dir, + _regenerate_benchmark_viewer_if_available, + ) import os serve_dir = get_current_output_dir().resolve() # Use absolute path @@ -4111,6 +4980,7 @@ def start_server(): # Import the actual server from local.py from openadapt_ml.cloud.local import cmd_serve import argparse + # Pass benchmark=str(serve_dir) to serve from the correct directory # This bypasses get_current_output_dir() relative path issues fake_args = argparse.Namespace( @@ -4133,13 +5003,16 @@ def start_server(): if ip: try: from openadapt_ml.cloud.ssh_tunnel import get_tunnel_manager + tunnel_manager = get_tunnel_manager() tunnel_manager.start_tunnels_for_vm(ip, "azureuser") tunnel_status = tunnel_manager.get_tunnel_status() - if tunnel_status.get('vnc') and tunnel_status['vnc'].active: + if tunnel_status.get("vnc") and tunnel_status["vnc"].active: print(f" ✓ VNC tunnel started (localhost:8006 -> {ip}:8006)") else: - print(f" ⚠ VNC tunnel not started - run manually: ssh -L 8006:{ip}:8006 azureuser@{ip}") + print( + f" ⚠ VNC tunnel not started - run manually: ssh -L 8006:{ip}:8006 azureuser@{ip}" + ) except Exception as e: print(f" ⚠ Could not start tunnels: {e}") @@ -4149,7 +5022,9 @@ def start_server(): print(" VNC: http://localhost:8006") if auto_shutdown_hours > 0: shutdown_time = datetime.now() + timedelta(hours=auto_shutdown_hours) - print(f" Auto-shutdown: {shutdown_time.strftime('%H:%M:%S')} ({auto_shutdown_hours}h)") + print( + f" Auto-shutdown: {shutdown_time.strftime('%H:%M:%S')} ({auto_shutdown_hours}h)" + ) print("\n Press Ctrl+C to stop monitoring.\n") webbrowser.open(url) @@ -4164,23 +5039,44 @@ def start_server(): elapsed_str = f"{int(elapsed.total_seconds() // 3600)}h{int((elapsed.total_seconds() % 3600) // 60)}m" if ip: - is_ready, response = check_waa_probe(ip, internal_ip='172.30.0.2') + is_ready, response = check_waa_probe(ip, internal_ip="172.30.0.2") status = "READY" if is_ready else "waiting..." - print(f" [{time.strftime('%H:%M:%S')}] WAA: {status} | Elapsed: {elapsed_str} ", end='\r') + print( + f" [{time.strftime('%H:%M:%S')}] WAA: {status} | Elapsed: {elapsed_str} ", + end="\r", + ) else: - print(f" [{time.strftime('%H:%M:%S')}] VM not found | Elapsed: {elapsed_str} ", end='\r') + print( + f" [{time.strftime('%H:%M:%S')}] VM not found | Elapsed: {elapsed_str} ", + end="\r", + ) # Check auto-shutdown timeout - if auto_shutdown_hours > 0 and elapsed.total_seconds() >= auto_shutdown_hours * 3600: + if ( + auto_shutdown_hours > 0 + and elapsed.total_seconds() >= auto_shutdown_hours * 3600 + ): print(f"\n\n Auto-shutdown triggered after {auto_shutdown_hours}h") deallocate_result = subprocess.run( - ["az", "vm", "deallocate", "-g", resource_group, "-n", vm_name, "--no-wait"], - capture_output=True, text=True + [ + "az", + "vm", + "deallocate", + "-g", + resource_group, + "-n", + vm_name, + "--no-wait", + ], + capture_output=True, + text=True, ) if deallocate_result.returncode == 0: print(f" ✓ VM '{vm_name}' deallocation initiated") else: - print(f" ✗ Failed to deallocate: {deallocate_result.stderr[:50]}") + print( + f" ✗ Failed to deallocate: {deallocate_result.stderr[:50]}" + ) break time.sleep(10) @@ -4194,15 +5090,20 @@ def start_server(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") sys.exit(1) - cmd = getattr(args, 'cmd', None) + cmd = getattr(args, "cmd", None) if not cmd: print("✗ No command specified. Use: vm exec --cmd 'your command'") sys.exit(1) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - f"docker exec winarena sh -c '{cmd}'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + f"docker exec winarena sh -c '{cmd}'", + ], + capture_output=True, + text=True, ) print(result.stdout) if result.returncode != 0: @@ -4215,14 +5116,13 @@ def start_server(): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") sys.exit(1) - cmd = getattr(args, 'cmd', None) + cmd = getattr(args, "cmd", None) if not cmd: print("✗ No command specified. Use: vm host-exec --cmd 'your command'") sys.exit(1) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", cmd], - capture_output=True, text=True + ["ssh", *SSH_OPTS, f"azureuser@{ip}", cmd], capture_output=True, text=True ) print(result.stdout) if result.returncode != 0: @@ -4243,14 +5143,17 @@ def start_server(): check_cmd = "docker ps -a --format '{{.Names}} {{.Ports}}' 2>/dev/null || echo 'No containers'" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", check_cmd], - capture_output=True, text=True + capture_output=True, + text=True, ) print(f" Containers: {result.stdout.strip()}") # Clean up any conflicting containers print("\n[2/3] Cleaning up old containers...") cleanup_cmd = "docker rm -f winarena winarena-test 2>/dev/null || true" - subprocess.run(["ssh", *SSH_OPTS, f"azureuser@{ip}", cleanup_cmd], capture_output=True) + subprocess.run( + ["ssh", *SSH_OPTS, f"azureuser@{ip}", cleanup_cmd], capture_output=True + ) # Build the same docker command as run-waa but with timeout # Note: waa-auto has ENTRYPOINT ["/bin/bash", "-c"] so we pass the command as a string @@ -4271,7 +5174,9 @@ def start_server(): result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", docker_cmd], - capture_output=True, text=True, timeout=60 + capture_output=True, + text=True, + timeout=60, ) print(f"\n Exit code: {result.returncode}") @@ -4306,8 +5211,14 @@ def start_server(): print("[1/4] Copying startup script to VM...") scp_result = subprocess.run( - ["scp", *SSH_OPTS, str(startup_script), f"azureuser@{ip}:~/start_waa_server.bat"], - capture_output=True, text=True + [ + "scp", + *SSH_OPTS, + str(startup_script), + f"azureuser@{ip}:~/start_waa_server.bat", + ], + capture_output=True, + text=True, ) if scp_result.returncode != 0: print(f"✗ Failed to copy script: {scp_result.stderr}") @@ -4318,23 +5229,37 @@ def start_server(): print("[2/4] Copying script to Samba share...") # First copy to VM, then use docker cp to copy into container result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker cp ~/start_waa_server.bat winarena:/tmp/smb/start_waa_server.bat && docker exec winarena ls -la /tmp/smb/start_waa_server.bat"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker cp ~/start_waa_server.bat winarena:/tmp/smb/start_waa_server.bat && docker exec winarena ls -la /tmp/smb/start_waa_server.bat", + ], + capture_output=True, + text=True, ) if result.returncode != 0 or "No such file" in result.stdout + result.stderr: # Try creating directory first result2 = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker exec winarena mkdir -p /tmp/smb && docker cp ~/start_waa_server.bat winarena:/tmp/smb/start_waa_server.bat"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker exec winarena mkdir -p /tmp/smb && docker cp ~/start_waa_server.bat winarena:/tmp/smb/start_waa_server.bat", + ], + capture_output=True, + text=True, ) if result2.returncode != 0: print(f" ⚠ Samba copy failed: {result2.stderr[:100]}") else: - print(" ✓ Script available at \\\\host.lan\\Data\\start_waa_server.bat") + print( + " ✓ Script available at \\\\host.lan\\Data\\start_waa_server.bat" + ) else: - print(" ✓ Script available at \\\\host.lan\\Data\\start_waa_server.bat") + print( + " ✓ Script available at \\\\host.lan\\Data\\start_waa_server.bat" + ) # Step 3: Use QMP to send keyboard command to run the script # QMP is exposed on port 7200 in the container @@ -4418,27 +5343,35 @@ def send_keys_string(sock, text): # Run QMP command inside container (which has access to port 7200) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - f"docker exec winarena python3 -c '{qmp_cmd}'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + f"docker exec winarena python3 -c '{qmp_cmd}'", + ], + capture_output=True, + text=True, ) if "OK" in result.stdout: print(" ✓ Command sent to Windows") else: - print(f" ⚠ QMP command may have failed: {result.stdout} {result.stderr[:100]}") + print( + f" ⚠ QMP command may have failed: {result.stdout} {result.stderr[:100]}" + ) # Step 4: Wait and verify server is running print("[4/4] Waiting for server to start...") import time + for i in range(6): time.sleep(5) - is_ready, response = check_waa_probe(ip, internal_ip='172.30.0.2') + is_ready, response = check_waa_probe(ip, internal_ip="172.30.0.2") if is_ready: print("\n✓ WAA server is running!") print(f" Response: {response}") break - print(f" Attempt {i+1}/6: Not ready yet...") + print(f" Attempt {i + 1}/6: Not ready yet...") else: print("\n⚠ Server may not have started. Check VNC at http://localhost:8006") print(" You can manually run: \\\\host.lan\\Data\\start_waa_server.bat") @@ -4452,13 +5385,20 @@ def send_keys_string(sock, text): print("Copying OEM files to Samba share...") result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker exec winarena sh -c 'cp -r /oem/* /tmp/smb/ 2>&1 && ls -la /tmp/smb/'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker exec winarena sh -c 'cp -r /oem/* /tmp/smb/ 2>&1 && ls -la /tmp/smb/'", + ], + capture_output=True, + text=True, ) print(result.stdout) if result.returncode == 0: - print("✓ OEM files copied. In Windows, run: \\\\host.lan\\Data\\install.bat") + print( + "✓ OEM files copied. In Windows, run: \\\\host.lan\\Data\\install.bat" + ) else: print(f"Error: {result.stderr}") @@ -4469,21 +5409,28 @@ def send_keys_string(sock, text): print(f"✗ VM '{vm_name}' not found. Run 'vm setup-waa' first.") sys.exit(1) - num_lines = getattr(args, 'lines', 50) - follow = getattr(args, 'follow', False) + num_lines = getattr(args, "lines", 50) + follow = getattr(args, "follow", False) if follow: # Follow logs (streaming) print(f"Following logs from winarena container on {ip}...") print("Press Ctrl+C to stop.\n") import os - os.execvp("ssh", ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker logs -f winarena"]) + + os.execvp( + "ssh", ["ssh", *SSH_OPTS, f"azureuser@{ip}", "docker logs -f winarena"] + ) else: result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - f"docker logs --tail {num_lines} winarena 2>&1"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + f"docker logs --tail {num_lines} winarena 2>&1", + ], + capture_output=True, + text=True, ) print(result.stdout) if result.returncode != 0: @@ -4503,16 +5450,17 @@ def send_keys_string(sock, text): # Kill Docker build processes print("[1/2] Stopping Docker build processes...") - kill_cmd = ''' + kill_cmd = """ pkill -f 'docker build' 2>/dev/null || true pkill -f 'docker-buildx' 2>/dev/null || true pkill -f buildkitd 2>/dev/null || true echo "killed" -''' +""" result = subprocess.run( ["ssh", *SSH_OPTS, f"azureuser@{ip}", kill_cmd], - capture_output=True, text=True, - timeout=30 + capture_output=True, + text=True, + timeout=30, ) if "killed" in result.stdout: print(" ✓ Build processes stopped") @@ -4522,13 +5470,20 @@ def send_keys_string(sock, text): # Clean up Docker build cache print("[2/2] Cleaning Docker build cache...") prune_result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker builder prune -af 2>&1 | tail -5"], - capture_output=True, text=True, - timeout=120 + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker builder prune -af 2>&1 | tail -5", + ], + capture_output=True, + text=True, + timeout=120, ) print(f" {prune_result.stdout}") - print("\n Ready to retry: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild") + print( + "\n Ready to retry: uv run python -m openadapt_ml.benchmarks.cli vm run-waa --rebuild" + ) elif args.action == "diag": print(f"\n=== VM Diagnostics: {vm_name} ===\n") @@ -4546,9 +5501,14 @@ def send_keys_string(sock, text): print("[1/4] Disk Usage") print("-" * 50) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "df -h / /mnt 2>/dev/null || df -h /"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "df -h / /mnt 2>/dev/null || df -h /", + ], + capture_output=True, + text=True, ) if result.returncode == 0: print(result.stdout) @@ -4559,9 +5519,14 @@ def send_keys_string(sock, text): print("[2/4] Docker Status") print("-" * 50) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker system df 2>/dev/null || echo 'Docker not installed'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker system df 2>/dev/null || echo 'Docker not installed'", + ], + capture_output=True, + text=True, ) if result.returncode == 0: print(result.stdout) @@ -4572,9 +5537,14 @@ def send_keys_string(sock, text): print("[3/4] Docker Images") print("-" * 50) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker images --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}' 2>/dev/null || echo 'Docker not installed'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker images --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}' 2>/dev/null || echo 'Docker not installed'", + ], + capture_output=True, + text=True, ) if result.returncode == 0: print(result.stdout) @@ -4585,9 +5555,14 @@ def send_keys_string(sock, text): print("[4/4] Running Containers") print("-" * 50) result = subprocess.run( - ["ssh", *SSH_OPTS, f"azureuser@{ip}", - "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null || echo 'Docker not installed'"], - capture_output=True, text=True + [ + "ssh", + *SSH_OPTS, + f"azureuser@{ip}", + "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null || echo 'Docker not installed'", + ], + capture_output=True, + text=True, ) if result.returncode == 0: print(result.stdout) @@ -4597,7 +5572,7 @@ def send_keys_string(sock, text): # WAA probe status print("\n[Bonus] WAA Probe Status") print("-" * 50) - is_ready, response = check_waa_probe(ip, internal_ip='172.30.0.2') + is_ready, response = check_waa_probe(ip, internal_ip="172.30.0.2") if is_ready: print(f" ✓ WAA server READY: {response[:100] if response else '(empty)'}") else: @@ -4647,7 +5622,7 @@ def cmd_view(args: argparse.Namespace) -> None: output_path = generate_benchmark_viewer( benchmark_dir=benchmark_dir, output_path=benchmark_dir / "benchmark.html", - embed_screenshots=getattr(args, 'embed_screenshots', False), + embed_screenshots=getattr(args, "embed_screenshots", False), ) print(f" Generated: {output_path}") @@ -4669,7 +5644,7 @@ def log_message(self, format, *log_args): print(f"\n Viewer: {url}") print(" Press Ctrl+C to stop\n") - if not getattr(args, 'no_open', False): + if not getattr(args, "no_open", False): webbrowser.open(url) httpd.serve_forever() @@ -4699,14 +5674,16 @@ def cmd_export_traces(args: argparse.Namespace) -> None: ) # List available runs - if getattr(args, 'list', False): + if getattr(args, "list", False): print("\n=== Available Benchmark Runs ===\n") runs = list_available_runs(args.input) if not runs: print(f"No benchmark runs found in {args.input}/") print("\nRun a benchmark first:") - print(" uv run python -m openadapt_ml.benchmarks.cli test-collection --tasks 5") + print( + " uv run python -m openadapt_ml.benchmarks.cli test-collection --tasks 5" + ) return for run in runs: @@ -4715,7 +5692,9 @@ def cmd_export_traces(args: argparse.Namespace) -> None: num_success = run.get("num_success", 0) print(f" {run['run_name']}") print(f" Model: {run.get('model_id', 'unknown')}") - print(f" Tasks: {num_tasks} ({num_success} passed, {success_rate:.1f}% success)") + print( + f" Tasks: {num_tasks} ({num_success} passed, {success_rate:.1f}% success)" + ) print(f" Created: {run.get('created_at', 'N/A')}") print() return @@ -4771,13 +5750,18 @@ def cmd_export_traces(args: argparse.Namespace) -> None: print("Next steps:") print(" # Load episodes in Python:") print(" from openadapt_ml.schema import load_episode") - print(f" episode = load_episode('{args.output}/episodes/{episodes[0].episode_id}.json')" if episodes else "") + print( + f" episode = load_episode('{args.output}/episodes/{episodes[0].episode_id}.json')" + if episodes + else "" + ) print() except Exception as e: print(f"\nError: {e}") if args.verbose: import traceback + traceback.print_exc() sys.exit(1) @@ -4855,7 +5839,9 @@ def main() -> None: # Setup (new!) p_setup = subparsers.add_parser("setup", help="One-command setup (Azure + WAA)") - p_setup.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompts") + p_setup.add_argument( + "--yes", "-y", action="store_true", help="Skip confirmation prompts" + ) p_setup.add_argument("--force", action="store_true", help="Continue on errors") p_setup.add_argument("--verbose", "-v", action="store_true", help="Verbose output") @@ -4864,40 +5850,76 @@ def main() -> None: p_status.add_argument("--verbose", "-v", action="store_true", help="Verbose output") # Az-status (lightweight, no Azure SDK) - p_az_status = subparsers.add_parser("az-status", help="Check Azure resource status (uses az CLI)") - p_az_status.add_argument("--resource-group", default="openadapt-agents", help="Azure resource group name") - p_az_status.add_argument("--workspace", default="openadapt-ml", help="Azure ML workspace name") - p_az_status.add_argument("--acr-name", default="openadaptacr", help="Azure Container Registry name") + p_az_status = subparsers.add_parser( + "az-status", help="Check Azure resource status (uses az CLI)" + ) + p_az_status.add_argument( + "--resource-group", default="openadapt-agents", help="Azure resource group name" + ) + p_az_status.add_argument( + "--workspace", default="openadapt-ml", help="Azure ML workspace name" + ) + p_az_status.add_argument( + "--acr-name", default="openadaptacr", help="Azure Container Registry name" + ) # Cleanup - p_cleanup = subparsers.add_parser("cleanup", help="Delete all Azure compute instances") + p_cleanup = subparsers.add_parser( + "cleanup", help="Delete all Azure compute instances" + ) p_cleanup.add_argument("--yes", "-y", action="store_true", help="Skip confirmation") - p_cleanup.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + p_cleanup.add_argument( + "--verbose", "-v", action="store_true", help="Verbose output" + ) # Estimate costs p_estimate = subparsers.add_parser("estimate", help="Estimate Azure costs") p_estimate.add_argument("--tasks", type=int, default=154, help="Number of tasks") - p_estimate.add_argument("--workers", type=int, default=1, help="Number of workers (default: 1 for free trial)") - p_estimate.add_argument("--duration", type=float, default=1.0, help="Avg task duration (minutes)") - p_estimate.add_argument("--vm-cost", type=float, default=0.19, help="VM hourly cost ($ for D4_v3)") + p_estimate.add_argument( + "--workers", + type=int, + default=1, + help="Number of workers (default: 1 for free trial)", + ) + p_estimate.add_argument( + "--duration", type=float, default=1.0, help="Avg task duration (minutes)" + ) + p_estimate.add_argument( + "--vm-cost", type=float, default=0.19, help="VM hourly cost ($ for D4_v3)" + ) # Run local - p_local = subparsers.add_parser("run-local", help="Run evaluation locally (Windows)") - p_local.add_argument("--waa-path", help="Path to WAA repository (auto-detected if not specified)") + p_local = subparsers.add_parser( + "run-local", help="Run evaluation locally (Windows)" + ) + p_local.add_argument( + "--waa-path", help="Path to WAA repository (auto-detected if not specified)" + ) p_local.add_argument("--tasks", help="Comma-separated task IDs (default: all)") p_local.add_argument("--max-steps", type=int, default=15, help="Max steps per task") p_local.add_argument("--agent", default="random", help="Agent type") p_local.add_argument("--seed", type=int, default=42, help="Random seed") p_local.add_argument("--output", help="Output JSON path") - p_local.add_argument("--force", action="store_true", help="Force run on non-Windows") + p_local.add_argument( + "--force", action="store_true", help="Force run on non-Windows" + ) p_local.add_argument("--verbose", "-v", action="store_true", help="Verbose output") # Run Azure p_azure = subparsers.add_parser("run-azure", help="Run evaluation on Azure") p_azure.add_argument("--config", help="Azure config JSON path") - p_azure.add_argument("--waa-path", help="Path to WAA repository (auto-detected if not specified)") - p_azure.add_argument("--workers", type=int, default=1, help="Number of workers (default: 1 for free trial)") - p_azure.add_argument("--num-tasks", type=int, help="Number of random tasks to run (default: all)") + p_azure.add_argument( + "--waa-path", help="Path to WAA repository (auto-detected if not specified)" + ) + p_azure.add_argument( + "--workers", + type=int, + default=1, + help="Number of workers (default: 1 for free trial)", + ) + p_azure.add_argument( + "--num-tasks", type=int, help="Number of random tasks to run (default: all)" + ) p_azure.add_argument("--task-ids", help="Comma-separated specific task IDs to run") p_azure.add_argument("--max-steps", type=int, default=15, help="Max steps per task") p_azure.add_argument("--agent", default="random", help="Agent type") @@ -4905,9 +5927,15 @@ def main() -> None: p_azure.add_argument("--experiment", default="waa-eval", help="Experiment name") p_azure.add_argument("--output", help="Output JSON path") p_azure.add_argument("--yes", "-y", action="store_true", help="Skip confirmation") - p_azure.add_argument("--no-cleanup", action="store_true", help="Don't delete VMs after") - p_azure.add_argument("--timeout", type=float, default=4.0, - help="Job timeout in hours (default: 4). Jobs are auto-canceled after this duration.") + p_azure.add_argument( + "--no-cleanup", action="store_true", help="Don't delete VMs after" + ) + p_azure.add_argument( + "--timeout", + type=float, + default=4.0, + help="Job timeout in hours (default: 4). Jobs are auto-canceled after this duration.", + ) p_azure.add_argument("--verbose", "-v", action="store_true", help="Verbose output") # Test mock @@ -4917,50 +5945,109 @@ def main() -> None: p_mock.add_argument("--seed", type=int, default=42, help="Random seed") # Test smart (SmartMockAgent - expected 100% success) - p_smart = subparsers.add_parser("test-smart", help="Test mock adapter with SmartMockAgent (expected 100%% success)") + p_smart = subparsers.add_parser( + "test-smart", + help="Test mock adapter with SmartMockAgent (expected 100%% success)", + ) p_smart.add_argument("--tasks", type=int, default=5, help="Number of mock tasks") p_smart.add_argument("--max-steps", type=int, default=10, help="Max steps per task") # Test collection - p_collection = subparsers.add_parser("test-collection", help="Test benchmark data collection") - p_collection.add_argument("--tasks", type=int, default=5, help="Number of mock tasks (default: 5)") - p_collection.add_argument("--max-steps", type=int, default=10, help="Max steps per task (default: 10)") + p_collection = subparsers.add_parser( + "test-collection", help="Test benchmark data collection" + ) + p_collection.add_argument( + "--tasks", type=int, default=5, help="Number of mock tasks (default: 5)" + ) + p_collection.add_argument( + "--max-steps", type=int, default=10, help="Max steps per task (default: 10)" + ) p_collection.add_argument("--seed", type=int, default=42, help="Random seed") - p_collection.add_argument("--model-id", default="random-agent-test", help="Model identifier") - p_collection.add_argument("--output", default="benchmark_results", help="Output directory") + p_collection.add_argument( + "--model-id", default="random-agent-test", help="Model identifier" + ) + p_collection.add_argument( + "--output", default="benchmark_results", help="Output directory" + ) p_collection.add_argument("--run-name", help="Run name (default: auto-generated)") # Run API-backed evaluation - p_api = subparsers.add_parser("run-api", help="Run evaluation with API-backed VLM (Claude/GPT-5.1)") - p_api.add_argument("--provider", choices=["anthropic", "openai"], default="anthropic", - help="API provider (anthropic=Claude, openai=GPT-5.1)") - p_api.add_argument("--tasks", type=int, default=5, help="Number of mock tasks (default: 5)") - p_api.add_argument("--max-steps", type=int, default=10, help="Max steps per task (default: 10)") - p_api.add_argument("--max-tokens", type=int, default=512, help="Max tokens for API response") - p_api.add_argument("--no-a11y", action="store_true", help="Disable accessibility tree in prompt") - p_api.add_argument("--no-history", action="store_true", help="Disable action history in prompt") + p_api = subparsers.add_parser( + "run-api", help="Run evaluation with API-backed VLM (Claude/GPT-5.1)" + ) + p_api.add_argument( + "--provider", + choices=["anthropic", "openai"], + default="anthropic", + help="API provider (anthropic=Claude, openai=GPT-5.1)", + ) + p_api.add_argument( + "--tasks", type=int, default=5, help="Number of mock tasks (default: 5)" + ) + p_api.add_argument( + "--max-steps", type=int, default=10, help="Max steps per task (default: 10)" + ) + p_api.add_argument( + "--max-tokens", type=int, default=512, help="Max tokens for API response" + ) + p_api.add_argument( + "--no-a11y", action="store_true", help="Disable accessibility tree in prompt" + ) + p_api.add_argument( + "--no-history", action="store_true", help="Disable action history in prompt" + ) p_api.add_argument("--output", default="benchmark_results", help="Output directory") p_api.add_argument("--run-name", help="Run name (default: auto-generated)") p_api.add_argument("--model-id", help="Model identifier (default: {provider}-api)") - p_api.add_argument("--mock", action="store_true", help="Force use of mock adapter (even if WAA is available)") - p_api.add_argument("--waa-path", help="Path to WAA repository (auto-detected if not specified)") + p_api.add_argument( + "--mock", + action="store_true", + help="Force use of mock adapter (even if WAA is available)", + ) + p_api.add_argument( + "--waa-path", help="Path to WAA repository (auto-detected if not specified)" + ) p_api.add_argument("--task-ids", help="Comma-separated task IDs for real WAA") p_api.add_argument("--force", action="store_true", help="Force run on non-Windows") p_api.add_argument("--verbose", "-v", action="store_true", help="Verbose output") # WAA Demo-conditioned experiment - p_demo = subparsers.add_parser("waa-demo", help="Run WAA demo-conditioned experiment (zero-shot vs demo)") - p_demo.add_argument("--condition", choices=["zero-shot", "demo"], default="demo", - help="Experiment condition (default: demo)") - p_demo.add_argument("--provider", choices=["anthropic", "openai"], default="anthropic", - help="VLM API provider (default: anthropic)") - p_demo.add_argument("--tasks", help="Comma-separated task numbers 1-10 (default: all with demos)") - p_demo.add_argument("--max-steps", type=int, default=15, help="Max steps per task (default: 15)") - p_demo.add_argument("--max-tokens", type=int, default=512, help="Max tokens for API response") - p_demo.add_argument("--mock", action="store_true", help="Use mock adapter (no Windows required)") - p_demo.add_argument("--no-a11y", action="store_true", help="Disable accessibility tree in prompt") - p_demo.add_argument("--no-history", action="store_true", help="Disable action history in prompt") - p_demo.add_argument("--output", default="benchmark_results", help="Output directory") + p_demo = subparsers.add_parser( + "waa-demo", help="Run WAA demo-conditioned experiment (zero-shot vs demo)" + ) + p_demo.add_argument( + "--condition", + choices=["zero-shot", "demo"], + default="demo", + help="Experiment condition (default: demo)", + ) + p_demo.add_argument( + "--provider", + choices=["anthropic", "openai"], + default="anthropic", + help="VLM API provider (default: anthropic)", + ) + p_demo.add_argument( + "--tasks", help="Comma-separated task numbers 1-10 (default: all with demos)" + ) + p_demo.add_argument( + "--max-steps", type=int, default=15, help="Max steps per task (default: 15)" + ) + p_demo.add_argument( + "--max-tokens", type=int, default=512, help="Max tokens for API response" + ) + p_demo.add_argument( + "--mock", action="store_true", help="Use mock adapter (no Windows required)" + ) + p_demo.add_argument( + "--no-a11y", action="store_true", help="Disable accessibility tree in prompt" + ) + p_demo.add_argument( + "--no-history", action="store_true", help="Disable action history in prompt" + ) + p_demo.add_argument( + "--output", default="benchmark_results", help="Output directory" + ) p_demo.add_argument("--run-name", help="Run name (default: auto-generated)") p_demo.add_argument("--verbose", "-v", action="store_true", help="Verbose output") @@ -4969,100 +6056,336 @@ def main() -> None: p_config.add_argument("--output", default="azure_config.json", help="Output path") # Cleanup VMs (frees quota) - p_cleanup_vms = subparsers.add_parser("cleanup-vms", help="Clean up Azure compute instances to free quota") - p_cleanup_vms.add_argument("--resource-group", default="openadapt-agents", help="Azure resource group") - p_cleanup_vms.add_argument("--workspace", default="openadapt-ml", help="Azure ML workspace name") - p_cleanup_vms.add_argument("--yes", "-y", action="store_true", help="Skip confirmation") + p_cleanup_vms = subparsers.add_parser( + "cleanup-vms", help="Clean up Azure compute instances to free quota" + ) + p_cleanup_vms.add_argument( + "--resource-group", default="openadapt-agents", help="Azure resource group" + ) + p_cleanup_vms.add_argument( + "--workspace", default="openadapt-ml", help="Azure ML workspace name" + ) + p_cleanup_vms.add_argument( + "--yes", "-y", action="store_true", help="Skip confirmation" + ) # List jobs p_list_jobs = subparsers.add_parser("list-jobs", help="List recent Azure ML jobs") - p_list_jobs.add_argument("--resource-group", default="openadapt-agents", help="Azure resource group") - p_list_jobs.add_argument("--workspace", default="openadapt-ml", help="Azure ML workspace name") - p_list_jobs.add_argument("--limit", type=int, default=20, help="Max number of jobs to show") + p_list_jobs.add_argument( + "--resource-group", default="openadapt-agents", help="Azure resource group" + ) + p_list_jobs.add_argument( + "--workspace", default="openadapt-ml", help="Azure ML workspace name" + ) + p_list_jobs.add_argument( + "--limit", type=int, default=20, help="Max number of jobs to show" + ) # Job logs - p_job_logs = subparsers.add_parser("job-logs", help="Download and display logs for an Azure ML job") + p_job_logs = subparsers.add_parser( + "job-logs", help="Download and display logs for an Azure ML job" + ) p_job_logs.add_argument("job_name", help="Job name (from list-jobs output)") - p_job_logs.add_argument("--resource-group", default="openadapt-agents", help="Azure resource group") - p_job_logs.add_argument("--workspace", default="openadapt-ml", help="Azure ML workspace name") + p_job_logs.add_argument( + "--resource-group", default="openadapt-agents", help="Azure resource group" + ) + p_job_logs.add_argument( + "--workspace", default="openadapt-ml", help="Azure ML workspace name" + ) # Analyze WAA results p_analyze = subparsers.add_parser("analyze", help="Analyze WAA benchmark results") p_analyze.add_argument("--results-dir", help="Path to results directory (local)") p_analyze.add_argument("--vm-ip", help="IP of Azure VM to analyze results on") - p_analyze.add_argument("--remote", action="store_true", help="Run analysis on VM via SSH (faster, no download)") + p_analyze.add_argument( + "--remote", + action="store_true", + help="Run analysis on VM via SSH (faster, no download)", + ) p_analyze.add_argument("--output", help="Output JSON path for summary") - p_analyze.add_argument("--verbose", "-v", action="store_true", help="Show detailed task-level results") + p_analyze.add_argument( + "--verbose", "-v", action="store_true", help="Show detailed task-level results" + ) # WAA eval VM management - p_vm = subparsers.add_parser("vm", help="Manage dedicated WAA eval VM (with nested virtualization)") - p_vm.add_argument("action", choices=["monitor", "create", "status", "ssh", "delete", "deallocate", "start", "list-sizes", "setup", "pull-image", "setup-waa", "run-waa", "prepare-windows", "fix-storage", "docker-prune", "docker-move", "stop-build", "fix-oem", "reset-windows", "screenshot", "probe", "pool-status", "delete-pool", "cleanup-stale", "diag", "logs", "exec", "host-exec", "test-docker", "start-server"], help="Action to perform") - p_vm.add_argument("--resource-group", default="openadapt-agents", help="Azure resource group") + p_vm = subparsers.add_parser( + "vm", help="Manage dedicated WAA eval VM (with nested virtualization)" + ) + p_vm.add_argument( + "action", + choices=[ + "monitor", + "create", + "status", + "ssh", + "delete", + "deallocate", + "start", + "list-sizes", + "setup", + "pull-image", + "setup-waa", + "run-waa", + "prepare-windows", + "fix-storage", + "docker-prune", + "docker-move", + "stop-build", + "fix-oem", + "reset-windows", + "screenshot", + "probe", + "pool-status", + "delete-pool", + "cleanup-stale", + "diag", + "logs", + "exec", + "host-exec", + "test-docker", + "start-server", + ], + help="Action to perform", + ) + p_vm.add_argument( + "--resource-group", default="openadapt-agents", help="Azure resource group" + ) p_vm.add_argument("--name", default="waa-eval-vm", help="VM name") - p_vm.add_argument("--size", default="Standard_D4s_v3", help="VM size (must support nested virt)") + p_vm.add_argument( + "--size", default="Standard_D4s_v3", help="VM size (must support nested virt)" + ) p_vm.add_argument("--location", default="eastus", help="Azure region") - p_vm.add_argument("--acr", default="openadaptacr", help="Azure Container Registry name") - p_vm.add_argument("--api-key", help="OpenAI API key for WAA agent (or set OPENAI_API_KEY env var)") - p_vm.add_argument("--tasks", help="Comma-separated task IDs to run (e.g., notepad_1,notepad_2)") - p_vm.add_argument("--num-tasks", type=int, default=5, help="Number of tasks to run (for run-waa)") - p_vm.add_argument("--domain", choices=["general", "office", "web", "coding", "system", "creative", "data", "communication", "media", "gaming", "utility"], help="WAA domain to filter tasks (for run-waa)") - p_vm.add_argument("--task-ids", help="Comma-separated task IDs to run (e.g., 'task_001,task_015,task_042') for run-waa") - p_vm.add_argument("--model", default="gpt-4o", help="Model to use (gpt-4o, gpt-5.2, etc.)") - p_vm.add_argument("--agent", default="navi", - choices=["navi", "api-claude", "api-openai"], - help="Agent type: navi (default WAA), api-claude (Claude Sonnet 4.5), api-openai (GPT-5.1)") + p_vm.add_argument( + "--acr", default="openadaptacr", help="Azure Container Registry name" + ) + p_vm.add_argument( + "--api-key", help="OpenAI API key for WAA agent (or set OPENAI_API_KEY env var)" + ) + p_vm.add_argument( + "--tasks", help="Comma-separated task IDs to run (e.g., notepad_1,notepad_2)" + ) + p_vm.add_argument( + "--num-tasks", type=int, default=5, help="Number of tasks to run (for run-waa)" + ) + p_vm.add_argument( + "--domain", + choices=[ + "general", + "office", + "web", + "coding", + "system", + "creative", + "data", + "communication", + "media", + "gaming", + "utility", + ], + help="WAA domain to filter tasks (for run-waa)", + ) + p_vm.add_argument( + "--task-ids", + help="Comma-separated task IDs to run (e.g., 'task_001,task_015,task_042') for run-waa", + ) + p_vm.add_argument( + "--model", default="gpt-4o", help="Model to use (gpt-4o, gpt-5.2, etc.)" + ) + p_vm.add_argument( + "--agent", + default="navi", + choices=["navi", "api-claude", "api-openai"], + help="Agent type: navi (default WAA), api-claude (Claude Sonnet 4.5), api-openai (GPT-5.1)", + ) # Multi-worker options - p_vm.add_argument("--workers", type=int, default=1, help="Number of worker VMs to create (for setup-waa)") + p_vm.add_argument( + "--workers", + type=int, + default=1, + help="Number of worker VMs to create (for setup-waa)", + ) # Probe options - p_vm.add_argument("--wait", action="store_true", help="For probe: Poll until server is ready") - p_vm.add_argument("--interval", type=int, default=20, help="For probe: Seconds between poll attempts") - p_vm.add_argument("--max-attempts", type=int, default=30, help="For probe: Max poll attempts (default 30 = 10min)") - p_vm.add_argument("--internal-ip", default="172.30.0.2", help="Internal IP of Windows VM (172.30.0.2 for waa-auto, 20.20.20.21 for official)") - p_vm.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompts") + p_vm.add_argument( + "--wait", action="store_true", help="For probe: Poll until server is ready" + ) + p_vm.add_argument( + "--interval", + type=int, + default=20, + help="For probe: Seconds between poll attempts", + ) + p_vm.add_argument( + "--max-attempts", + type=int, + default=30, + help="For probe: Max poll attempts (default 30 = 10min)", + ) + p_vm.add_argument( + "--internal-ip", + default="172.30.0.2", + help="Internal IP of Windows VM (172.30.0.2 for waa-auto, 20.20.20.21 for official)", + ) + p_vm.add_argument( + "--yes", "-y", action="store_true", help="Skip confirmation prompts" + ) # Viewer auto-launch options (for run-waa) - p_vm.add_argument("--open", action="store_true", default=True, help="Auto-open benchmark viewer (default: True)") - p_vm.add_argument("--no-open", action="store_false", dest="open", help="Disable auto-open of benchmark viewer") - p_vm.add_argument("--port", type=int, default=8765, help="Port for local dashboard server (default: 8765)") + p_vm.add_argument( + "--open", + action="store_true", + default=True, + help="Auto-open benchmark viewer (default: True)", + ) + p_vm.add_argument( + "--no-open", + action="store_false", + dest="open", + help="Disable auto-open of benchmark viewer", + ) + p_vm.add_argument( + "--port", + type=int, + default=8765, + help="Port for local dashboard server (default: 8765)", + ) # Auto-shutdown option (for run-waa) - p_vm.add_argument("--auto-shutdown", action="store_true", default=False, help="Deallocate VM after benchmark completes to save costs (for run-waa)") - p_vm.add_argument("--auto-shutdown-hours", type=float, default=0, help="For monitor: auto-deallocate VM after N hours (0=disabled)") - p_vm.add_argument("--rebuild", action="store_true", default=False, help="Force rebuild of waa-auto Docker image (for run-waa)") - p_vm.add_argument("--fresh", action="store_true", default=False, help="Delete Windows storage and start fresh installation (for run-waa)") + p_vm.add_argument( + "--auto-shutdown", + action="store_true", + default=False, + help="Deallocate VM after benchmark completes to save costs (for run-waa)", + ) + p_vm.add_argument( + "--auto-shutdown-hours", + type=float, + default=0, + help="For monitor: auto-deallocate VM after N hours (0=disabled)", + ) + p_vm.add_argument( + "--rebuild", + action="store_true", + default=False, + help="Force rebuild of waa-auto Docker image (for run-waa)", + ) + p_vm.add_argument( + "--fresh", + action="store_true", + default=False, + help="Delete Windows storage and start fresh installation (for run-waa)", + ) # Log viewing options (for logs action) - p_vm.add_argument("--lines", "-n", type=int, default=50, help="Number of log lines to show (for logs)") - p_vm.add_argument("--follow", "-f", action="store_true", default=False, help="Follow log output (for logs)") + p_vm.add_argument( + "--lines", + "-n", + type=int, + default=50, + help="Number of log lines to show (for logs)", + ) + p_vm.add_argument( + "--follow", + "-f", + action="store_true", + default=False, + help="Follow log output (for logs)", + ) # Cleanup-stale options - p_vm.add_argument("--max-hours", type=float, default=2.0, help="For cleanup-stale: cancel jobs running longer than this (default: 2 hours)") - p_vm.add_argument("--vm-max-hours", type=float, default=24.0, help="For cleanup-stale: deallocate VMs running longer than this (default: 24 hours)") + p_vm.add_argument( + "--max-hours", + type=float, + default=2.0, + help="For cleanup-stale: cancel jobs running longer than this (default: 2 hours)", + ) + p_vm.add_argument( + "--vm-max-hours", + type=float, + default=24.0, + help="For cleanup-stale: deallocate VMs running longer than this (default: 24 hours)", + ) # Exec command option p_vm.add_argument("--cmd", help="Command to execute in container (for exec action)") # Benchmark viewer subcommand - for monitoring already-running benchmarks - p_viewer = subparsers.add_parser("viewer", help="Launch benchmark viewer for monitoring a running VM") - p_viewer.add_argument("--vm-ip", required=True, help="IP address of the Azure VM to monitor") - p_viewer.add_argument("--port", type=int, default=8765, help="Port for local dashboard server (default: 8765)") - p_viewer.add_argument("--no-open", action="store_true", help="Don't auto-open browser") - p_viewer.add_argument("--internal-ip", default="172.30.0.2", help="Internal IP of Windows VM (default: 172.30.0.2)") + p_viewer = subparsers.add_parser( + "viewer", help="Launch benchmark viewer for monitoring a running VM" + ) + p_viewer.add_argument( + "--vm-ip", required=True, help="IP address of the Azure VM to monitor" + ) + p_viewer.add_argument( + "--port", + type=int, + default=8765, + help="Port for local dashboard server (default: 8765)", + ) + p_viewer.add_argument( + "--no-open", action="store_true", help="Don't auto-open browser" + ) + p_viewer.add_argument( + "--internal-ip", + default="172.30.0.2", + help="Internal IP of Windows VM (default: 172.30.0.2)", + ) # View benchmark results - generate and serve HTML viewer for collected benchmark data - p_view = subparsers.add_parser("view", help="View benchmark results from collected data") - p_view.add_argument("--run-name", required=True, help="Name of the benchmark run to view") - p_view.add_argument("--output", default="benchmark_results", help="Base directory containing benchmark runs (default: benchmark_results)") - p_view.add_argument("--port", type=int, default=8765, help="Port for local server (default: 8765)") - p_view.add_argument("--no-open", action="store_true", help="Don't auto-open browser") - p_view.add_argument("--embed-screenshots", action="store_true", help="Embed screenshots as base64 (creates larger but standalone HTML)") + p_view = subparsers.add_parser( + "view", help="View benchmark results from collected data" + ) + p_view.add_argument( + "--run-name", required=True, help="Name of the benchmark run to view" + ) + p_view.add_argument( + "--output", + default="benchmark_results", + help="Base directory containing benchmark runs (default: benchmark_results)", + ) + p_view.add_argument( + "--port", type=int, default=8765, help="Port for local server (default: 8765)" + ) + p_view.add_argument( + "--no-open", action="store_true", help="Don't auto-open browser" + ) + p_view.add_argument( + "--embed-screenshots", + action="store_true", + help="Embed screenshots as base64 (creates larger but standalone HTML)", + ) # Export traces as training data - p_export = subparsers.add_parser("export-traces", help="Export benchmark traces as training data for VLM fine-tuning") + p_export = subparsers.add_parser( + "export-traces", + help="Export benchmark traces as training data for VLM fine-tuning", + ) p_export.add_argument("--run-name", help="Name of the benchmark run to export") - p_export.add_argument("--input", default="benchmark_results", help="Base directory containing benchmark runs (default: benchmark_results)") - p_export.add_argument("--output", "-o", default="training_data", help="Output directory for training data (default: training_data)") - p_export.add_argument("--status", choices=["passed", "failed", "all"], default="passed", - help="Filter tasks by status (default: passed)") - p_export.add_argument("--list", "-l", action="store_true", help="List available benchmark runs") - p_export.add_argument("--no-screenshots", action="store_true", help="Don't copy screenshots to output") - p_export.add_argument("--no-jsonl", action="store_true", help="Don't create training_samples.jsonl file") - p_export.add_argument("--verbose", "-v", action="store_true", help="Verbose output with stack traces") + p_export.add_argument( + "--input", + default="benchmark_results", + help="Base directory containing benchmark runs (default: benchmark_results)", + ) + p_export.add_argument( + "--output", + "-o", + default="training_data", + help="Output directory for training data (default: training_data)", + ) + p_export.add_argument( + "--status", + choices=["passed", "failed", "all"], + default="passed", + help="Filter tasks by status (default: passed)", + ) + p_export.add_argument( + "--list", "-l", action="store_true", help="List available benchmark runs" + ) + p_export.add_argument( + "--no-screenshots", action="store_true", help="Don't copy screenshots to output" + ) + p_export.add_argument( + "--no-jsonl", + action="store_true", + help="Don't create training_samples.jsonl file", + ) + p_export.add_argument( + "--verbose", "-v", action="store_true", help="Verbose output with stack traces" + ) args = parser.parse_args() @@ -5077,10 +6400,10 @@ def main() -> None: elif args.command == "estimate": cmd_estimate(args) elif args.command == "run-local": - setup_logging(getattr(args, 'verbose', False)) + setup_logging(getattr(args, "verbose", False)) cmd_run_local(args) elif args.command == "run-azure": - setup_logging(getattr(args, 'verbose', False)) + setup_logging(getattr(args, "verbose", False)) cmd_run_azure(args) elif args.command == "test-mock": cmd_test_mock(args) diff --git a/openadapt_ml/benchmarks/data_collection.py b/openadapt_ml/benchmarks/data_collection.py index b14f39e..8147a94 100644 --- a/openadapt_ml/benchmarks/data_collection.py +++ b/openadapt_ml/benchmarks/data_collection.py @@ -203,7 +203,9 @@ def record_step( screenshot_path = self._save_screenshot(step_idx, observation.screenshot) elif observation.screenshot_path is not None: # Copy existing screenshot - screenshot_path = self._copy_screenshot(step_idx, observation.screenshot_path) + screenshot_path = self._copy_screenshot( + step_idx, observation.screenshot_path + ) # Create execution step record step = ExecutionStep( @@ -265,10 +267,19 @@ def save_summary(self, all_results: list[BenchmarkResult]) -> None: "model_id": self.model_id, "num_tasks": len(all_results), "num_success": sum(1 for r in all_results if r.success), - "success_rate": sum(1 for r in all_results if r.success) / len(all_results) if all_results else 0.0, - "avg_score": sum(r.score for r in all_results) / len(all_results) if all_results else 0.0, - "avg_steps": sum(r.num_steps for r in all_results) / len(all_results) if all_results else 0.0, - "avg_time_seconds": sum(r.total_time_seconds for r in all_results) / len(all_results) if all_results else 0.0, + "success_rate": sum(1 for r in all_results if r.success) / len(all_results) + if all_results + else 0.0, + "avg_score": sum(r.score for r in all_results) / len(all_results) + if all_results + else 0.0, + "avg_steps": sum(r.num_steps for r in all_results) / len(all_results) + if all_results + else 0.0, + "avg_time_seconds": sum(r.total_time_seconds for r in all_results) + / len(all_results) + if all_results + else 0.0, "tasks": [ { "task_id": r.task_id, @@ -330,6 +341,7 @@ def _copy_screenshot(self, step_idx: int, source_path: str) -> str: # Copy file import shutil + shutil.copy2(source_path, dest_path) return f"screenshots/{filename}" diff --git a/openadapt_ml/benchmarks/live_tracker.py b/openadapt_ml/benchmarks/live_tracker.py index 5f65dec..4126a8d 100644 --- a/openadapt_ml/benchmarks/live_tracker.py +++ b/openadapt_ml/benchmarks/live_tracker.py @@ -78,12 +78,14 @@ def start_task(self, task: BenchmarkTask) -> None: result=None, ) - self._write_state({ - "status": "running", - "total_tasks": self.total_tasks, - "tasks_completed": self.tasks_completed, - "current_task": asdict(self.current_task), - }) + self._write_state( + { + "status": "running", + "total_tasks": self.total_tasks, + "tasks_completed": self.tasks_completed, + "current_task": asdict(self.current_task), + } + ) def record_step( self, @@ -124,12 +126,14 @@ def record_step( self.current_task.steps.append(step) # Write updated state - self._write_state({ - "status": "running", - "total_tasks": self.total_tasks, - "tasks_completed": self.tasks_completed, - "current_task": asdict(self.current_task), - }) + self._write_state( + { + "status": "running", + "total_tasks": self.total_tasks, + "tasks_completed": self.tasks_completed, + "current_task": asdict(self.current_task), + } + ) def finish_task(self, result: BenchmarkResult) -> None: """Finish tracking the current task. @@ -152,23 +156,27 @@ def finish_task(self, result: BenchmarkResult) -> None: self.tasks_completed += 1 # Write updated state - self._write_state({ - "status": "running", - "total_tasks": self.total_tasks, - "tasks_completed": self.tasks_completed, - "current_task": asdict(self.current_task), - }) + self._write_state( + { + "status": "running", + "total_tasks": self.total_tasks, + "tasks_completed": self.tasks_completed, + "current_task": asdict(self.current_task), + } + ) # Clear current task self.current_task = None def finish(self) -> None: """Mark evaluation as complete.""" - self._write_state({ - "status": "complete", - "total_tasks": self.total_tasks, - "tasks_completed": self.tasks_completed, - }) + self._write_state( + { + "status": "complete", + "total_tasks": self.total_tasks, + "tasks_completed": self.tasks_completed, + } + ) def _write_state(self, state: dict[str, Any]) -> None: """Write current state to JSON file. diff --git a/openadapt_ml/benchmarks/runner.py b/openadapt_ml/benchmarks/runner.py index f6fefa2..320af27 100644 --- a/openadapt_ml/benchmarks/runner.py +++ b/openadapt_ml/benchmarks/runner.py @@ -124,9 +124,13 @@ def evaluate_agent_on_benchmark( # Run evaluation if config.parallel > 1 and adapter.supports_parallel: - results = _evaluate_parallel(agent, adapter, tasks, config, trace_collector, live_tracker) + results = _evaluate_parallel( + agent, adapter, tasks, config, trace_collector, live_tracker + ) else: - results = _evaluate_sequential(agent, adapter, tasks, config, trace_collector, live_tracker) + results = _evaluate_sequential( + agent, adapter, tasks, config, trace_collector, live_tracker + ) # Save summary if trace collection is enabled if trace_collector is not None: @@ -175,7 +179,9 @@ def _evaluate_sequential( if config.verbose: logger.info(f"Task {i + 1}/{len(tasks)}: {task.task_id}") - result = _run_single_task(agent, adapter, task, config, trace_collector, live_tracker) + result = _run_single_task( + agent, adapter, task, config, trace_collector, live_tracker + ) results.append(result) if config.on_task_complete: @@ -213,7 +219,15 @@ def _evaluate_parallel( with ThreadPoolExecutor(max_workers=config.parallel) as executor: # Submit all tasks future_to_task = { - executor.submit(_run_single_task, agent, adapter, task, config, trace_collector, live_tracker): task + executor.submit( + _run_single_task, + agent, + adapter, + task, + config, + trace_collector, + live_tracker, + ): task for task in tasks } diff --git a/openadapt_ml/benchmarks/trace_export.py b/openadapt_ml/benchmarks/trace_export.py index b0f9a40..918449c 100644 --- a/openadapt_ml/benchmarks/trace_export.py +++ b/openadapt_ml/benchmarks/trace_export.py @@ -152,7 +152,9 @@ def export(self) -> list[Episode]: episodes.append(episode) # Save episode JSON - episode_path = self.output_dir / "episodes" / f"{episode.episode_id}.json" + episode_path = ( + self.output_dir / "episodes" / f"{episode.episode_id}.json" + ) save_episode(episode, episode_path) # Copy screenshots if enabled @@ -169,7 +171,9 @@ def export(self) -> list[Episode]: ) except Exception as e: - error_msg = f"Failed to export task {task.get('task_id', 'unknown')}: {e}" + error_msg = ( + f"Failed to export task {task.get('task_id', 'unknown')}: {e}" + ) logger.error(error_msg) stats.errors.append(error_msg) stats.skipped_tasks += 1 @@ -507,7 +511,10 @@ def _create_training_jsonl(self, episodes: list[Episode]) -> None: "action": { "type": step.action.type.value, "coordinates": ( - {"x": step.action.coordinates.x, "y": step.action.coordinates.y} + { + "x": step.action.coordinates.x, + "y": step.action.coordinates.y, + } if step.action.coordinates else None ), @@ -519,7 +526,9 @@ def _create_training_jsonl(self, episodes: list[Episode]) -> None: "scroll_amount": step.action.scroll_amount, }, "reasoning": step.reasoning, - "domain": episode.metadata.get("domain") if episode.metadata else None, + "domain": episode.metadata.get("domain") + if episode.metadata + else None, "success": episode.success, } f.write(json.dumps(sample) + "\n") @@ -567,7 +576,9 @@ def export_traces( return exporter.export() -def list_available_runs(benchmark_results_dir: str | Path = "benchmark_results") -> list[dict[str, Any]]: +def list_available_runs( + benchmark_results_dir: str | Path = "benchmark_results", +) -> list[dict[str, Any]]: """List available benchmark runs for export. Args: @@ -596,20 +607,24 @@ def list_available_runs(benchmark_results_dir: str | Path = "benchmark_results") if metadata_path.exists(): with open(metadata_path) as f: metadata = json.load(f) - run_info.update({ - "benchmark_name": metadata.get("benchmark_name"), - "model_id": metadata.get("model_id"), - "created_at": metadata.get("created_at"), - }) + run_info.update( + { + "benchmark_name": metadata.get("benchmark_name"), + "model_id": metadata.get("model_id"), + "created_at": metadata.get("created_at"), + } + ) if summary_path.exists(): with open(summary_path) as f: summary = json.load(f) - run_info.update({ - "num_tasks": summary.get("num_tasks", 0), - "num_success": summary.get("num_success", 0), - "success_rate": summary.get("success_rate", 0.0), - }) + run_info.update( + { + "num_tasks": summary.get("num_tasks", 0), + "num_success": summary.get("num_success", 0), + "success_rate": summary.get("success_rate", 0.0), + } + ) runs.append(run_info) diff --git a/openadapt_ml/benchmarks/viewer.py b/openadapt_ml/benchmarks/viewer.py index c43aa4a..327c98c 100644 --- a/openadapt_ml/benchmarks/viewer.py +++ b/openadapt_ml/benchmarks/viewer.py @@ -133,7 +133,9 @@ def load_task_results(benchmark_dir: Path) -> list[dict[str, Any]]: screenshots_dir = task_dir / "screenshots" if screenshots_dir.exists(): screenshot_paths = sorted(screenshots_dir.glob("*.png")) - task_data["screenshots"] = [str(p.relative_to(benchmark_dir)) for p in screenshot_paths] + task_data["screenshots"] = [ + str(p.relative_to(benchmark_dir)) for p in screenshot_paths + ] else: task_data["screenshots"] = [] @@ -294,7 +296,7 @@ def _generate_benchmark_viewer_html( num_success = sum(1 for t in tasks if t.get("execution", {}).get("success", False)) success_rate = (num_success / num_tasks * 100) if num_tasks > 0 else 0 - html = f''' + html = f""" @@ -785,7 +787,7 @@ def _generate_benchmark_viewer_html(
Failed
-
{success_rate:.1f}%
+
= 50 else "error"}">{success_rate:.1f}%
Success Rate
@@ -838,7 +840,7 @@ def _generate_benchmark_viewer_html( const summary = {summary_json}; const domainStats = {domain_stats_json}; const tasks = {tasks_json}; - const embedScreenshots = {'true' if embed_screenshots else 'false'}; + const embedScreenshots = {"true" if embed_screenshots else "false"}; let currentTaskIndex = -1; let currentStepIndex = 0; @@ -1214,6 +1216,6 @@ def _generate_benchmark_viewer_html( -''' +""" return html diff --git a/openadapt_ml/benchmarks/vm_monitor.py b/openadapt_ml/benchmarks/vm_monitor.py index bf0bc36..9b030f1 100644 --- a/openadapt_ml/benchmarks/vm_monitor.py +++ b/openadapt_ml/benchmarks/vm_monitor.py @@ -120,9 +120,12 @@ def check_ssh(self) -> bool: result = subprocess.run( [ "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", f"ConnectTimeout={self.timeout}", - "-o", "BatchMode=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + f"ConnectTimeout={self.timeout}", + "-o", + "BatchMode=yes", f"{self.config.ssh_user}@{self.config.ssh_host}", "echo ok", ], @@ -145,9 +148,12 @@ def check_waa_probe(self) -> tuple[bool, str | None]: result = subprocess.run( [ "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", f"ConnectTimeout={self.timeout}", - "-o", "BatchMode=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + f"ConnectTimeout={self.timeout}", + "-o", + "BatchMode=yes", f"{self.config.ssh_user}@{self.config.ssh_host}", cmd, ], @@ -173,9 +179,12 @@ def get_container_status(self) -> tuple[bool, str | None]: result = subprocess.run( [ "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", f"ConnectTimeout={self.timeout}", - "-o", "BatchMode=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + f"ConnectTimeout={self.timeout}", + "-o", + "BatchMode=yes", f"{self.config.ssh_user}@{self.config.ssh_host}", cmd, ], @@ -191,9 +200,12 @@ def get_container_status(self) -> tuple[bool, str | None]: log_result = subprocess.run( [ "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", f"ConnectTimeout={self.timeout}", - "-o", "BatchMode=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + f"ConnectTimeout={self.timeout}", + "-o", + "BatchMode=yes", f"{self.config.ssh_user}@{self.config.ssh_host}", log_cmd, ], @@ -220,9 +232,12 @@ def get_disk_usage(self) -> float | None: result = subprocess.run( [ "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", f"ConnectTimeout={self.timeout}", - "-o", "BatchMode=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + f"ConnectTimeout={self.timeout}", + "-o", + "BatchMode=yes", f"{self.config.ssh_user}@{self.config.ssh_host}", cmd, ], @@ -233,7 +248,7 @@ def get_disk_usage(self) -> float | None: if result.returncode == 0 and result.stdout.strip(): try: bytes_size = int(result.stdout.strip()) - return round(bytes_size / (1024 ** 3), 2) + return round(bytes_size / (1024**3), 2) except ValueError: continue return None @@ -257,7 +272,9 @@ def check_status(self) -> VMStatus: if status.ssh_reachable: # Check container - status.container_running, status.container_logs = self.get_container_status() + status.container_running, status.container_logs = ( + self.get_container_status() + ) # Check WAA probe status.waa_ready, status.waa_probe_response = self.check_waa_probe() @@ -412,7 +429,9 @@ def create_pool( resource_group=resource_group, location=location, vm_size=vm_size, - workers=[PoolWorker(name=name, ip=ip, status="ready") for name, ip in workers], + workers=[ + PoolWorker(name=name, ip=ip, status="ready") for name, ip in workers + ], ) self.save() return self._pool @@ -468,7 +487,9 @@ def delete_pool(self) -> bool: class VMRegistry: """Manage a registry of VMs and their status.""" - def __init__(self, registry_file: str | Path = "benchmark_results/vm_registry.json"): + def __init__( + self, registry_file: str | Path = "benchmark_results/vm_registry.json" + ): """Initialize registry. Args: @@ -546,17 +567,23 @@ def main(): parser.add_argument("--host", help="SSH host") parser.add_argument("--user", default="azureuser", help="SSH user") parser.add_argument("--container", default="winarena", help="Docker container name") - parser.add_argument("--interval", type=int, default=30, help="Check interval in seconds") + parser.add_argument( + "--interval", type=int, default=30, help="Check interval in seconds" + ) parser.add_argument("--output", help="Output file for status updates (JSON lines)") parser.add_argument("--list", action="store_true", help="List all registered VMs") - parser.add_argument("--check-all", action="store_true", help="Check all registered VMs") + parser.add_argument( + "--check-all", action="store_true", help="Check all registered VMs" + ) args = parser.parse_args() if args.list: registry = VMRegistry() for vm in registry.list(): - print(f" {vm.name}: {vm.ssh_user}@{vm.ssh_host} (container: {vm.docker_container})") + print( + f" {vm.name}: {vm.ssh_user}@{vm.ssh_host} (container: {vm.docker_container})" + ) return if args.check_all: @@ -586,12 +613,14 @@ def print_status(status: VMStatus): ts = datetime.now().strftime("%H:%M:%S") waa_str = "READY!" if status.waa_ready else "not ready" disk_str = f"{status.disk_usage_gb}GB" if status.disk_usage_gb else "?" - print(f"[{ts}] SSH: {'✓' if status.ssh_reachable else '✗'} | " - f"VNC: {'✓' if status.vnc_reachable else '✗'} | " - f"WAA: {waa_str} | Disk: {disk_str}") + print( + f"[{ts}] SSH: {'✓' if status.ssh_reachable else '✗'} | " + f"VNC: {'✓' if status.vnc_reachable else '✗'} | " + f"WAA: {waa_str} | Disk: {disk_str}" + ) if status.container_logs: # Show last log line - last_line = status.container_logs.split('\n')[-1][:80] + last_line = status.container_logs.split("\n")[-1][:80] print(f" Log: {last_line}") print(f"Monitoring {args.host}... (Ctrl+C to stop)") diff --git a/openadapt_ml/benchmarks/waa.py b/openadapt_ml/benchmarks/waa.py index 812f157..120a954 100644 --- a/openadapt_ml/benchmarks/waa.py +++ b/openadapt_ml/benchmarks/waa.py @@ -218,7 +218,9 @@ def load_task(self, task_id: str) -> BenchmarkTask: task_map = {t.task_id: t for t in tasks} if task_id not in task_map: - raise KeyError(f"Task '{task_id}' not found. Available: {list(task_map.keys())[:10]}...") + raise KeyError( + f"Task '{task_id}' not found. Available: {list(task_map.keys())[:10]}..." + ) return task_map[task_id] @@ -375,7 +377,9 @@ def _load_task_from_json(self, domain: str, task_id: str) -> BenchmarkTask | Non return self._load_task_from_file(task_file, domain) - def _load_task_from_file(self, task_file: Path, domain: str) -> BenchmarkTask | None: + def _load_task_from_file( + self, task_file: Path, domain: str + ) -> BenchmarkTask | None: """Load a task from a JSON file.""" try: with open(task_file, encoding="utf-8") as f: @@ -428,6 +432,7 @@ def _to_benchmark_observation(self, waa_obs: dict | Any) -> BenchmarkObservation if hasattr(screenshot, "tobytes"): # PIL Image - convert to PNG bytes import io + buf = io.BytesIO() screenshot.save(buf, format="PNG") screenshot_bytes = buf.getvalue() @@ -675,7 +680,9 @@ def evaluate(self, task: BenchmarkTask) -> BenchmarkResult: score = 1.0 elif typed_text or clicked_ids: # Partial credit for taking meaningful actions - score = 0.3 + (0.1 * min(len(clicked_ids), 3)) + (0.2 if typed_text else 0.0) + score = ( + 0.3 + (0.1 * min(len(clicked_ids), 3)) + (0.2 if typed_text else 0.0) + ) return BenchmarkResult( task_id=task.task_id, @@ -747,15 +754,78 @@ def _generate_mock_screenshot(self, path: Path) -> None: except ImportError: # Fallback: create a minimal valid PNG if PIL not available # This is a 1x1 gray PNG - minimal_png = bytes([ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, # PNG signature - 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, # IHDR chunk - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, - 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, # IDAT chunk - 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xFE, - 0xD4, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, # IEND chunk - 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 - ]) + minimal_png = bytes( + [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, # PNG signature + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, # IHDR chunk + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x02, + 0x00, + 0x00, + 0x00, + 0x90, + 0x77, + 0x53, + 0xDE, + 0x00, + 0x00, + 0x00, + 0x0C, + 0x49, + 0x44, + 0x41, # IDAT chunk + 0x54, + 0x08, + 0xD7, + 0x63, + 0xF8, + 0xCF, + 0xC0, + 0x00, + 0x00, + 0x00, + 0x03, + 0x00, + 0x01, + 0x00, + 0x05, + 0xFE, + 0xD4, + 0xEF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, # IEND chunk + 0x4E, + 0x44, + 0xAE, + 0x42, + 0x60, + 0x82, + ] + ) path.write_bytes(minimal_png) diff --git a/openadapt_ml/benchmarks/waa_deploy/api_agent.py b/openadapt_ml/benchmarks/waa_deploy/api_agent.py index 98a2353..2d4f416 100644 --- a/openadapt_ml/benchmarks/waa_deploy/api_agent.py +++ b/openadapt_ml/benchmarks/waa_deploy/api_agent.py @@ -210,6 +210,7 @@ def __init__( ) try: from anthropic import Anthropic + self._client = Anthropic(api_key=self.api_key) except ImportError: raise RuntimeError( @@ -225,6 +226,7 @@ def __init__( ) try: from openai import OpenAI + self._client = OpenAI(api_key=self.api_key) except ImportError: raise RuntimeError( @@ -240,9 +242,13 @@ def __init__( self.memory_block_text = "# empty memory block" self.step_counter = 0 - logger.info(f"ApiAgent initialized with provider={provider}, model={self.model}") + logger.info( + f"ApiAgent initialized with provider={provider}, model={self.model}" + ) if self.demo: - logger.info(f"Demo trajectory provided ({len(self.demo)} chars) - will persist across all steps") + logger.info( + f"Demo trajectory provided ({len(self.demo)} chars) - will persist across all steps" + ) def predict(self, instruction: str, obs: Dict) -> tuple: """Predict the next action based on observation. @@ -325,10 +331,9 @@ def predict(self, instruction: str, obs: Dict) -> tuple: # Add action history if enabled (enhanced: includes reasoning, not just raw actions) if self.use_history and self.history: # Use rich history with reasoning (like PC Agent-E) - history_entries = self.history[-self.history_cutoff:] + history_entries = self.history[-self.history_cutoff :] history_str = "\n\n".join( - f"[Step {i+1}] {entry}" - for i, entry in enumerate(history_entries) + f"[Step {i + 1}] {entry}" for i, entry in enumerate(history_entries) ) content_parts.append(f"History of previous steps:\n{history_str}") logs["history_entries"] = len(history_entries) @@ -381,14 +386,18 @@ def predict(self, instruction: str, obs: Dict) -> tuple: actions = [code_text] self.prev_actions.append(code_text) # Store rich history with reasoning (memory + action) - self._add_to_history(f"Thought: {self.memory_block_text}\nAction: {code_text}") + self._add_to_history( + f"Thought: {self.memory_block_text}\nAction: {code_text}" + ) else: # Try to extract action from response text action = self._parse_action_from_text(response_text, w, h) if action: actions = [action] self.prev_actions.append(action) - self._add_to_history(f"Thought: {self.memory_block_text}\nAction: {action}") + self._add_to_history( + f"Thought: {self.memory_block_text}\nAction: {action}" + ) else: logger.warning("Could not extract action from response") actions = ["# Could not parse action"] @@ -483,33 +492,25 @@ def _parse_action_from_text(self, text: str, width: int, height: int) -> str | N Python code string or None if parsing failed. """ # Try to find click coordinates - click_match = re.search( - r"click.*?(\d+)\s*,\s*(\d+)", text, re.IGNORECASE - ) + click_match = re.search(r"click.*?(\d+)\s*,\s*(\d+)", text, re.IGNORECASE) if click_match: x, y = int(click_match.group(1)), int(click_match.group(2)) return f"computer.click({x}, {y})" # Try to find type text - type_match = re.search( - r'type[:\s]+["\'](.+?)["\']', text, re.IGNORECASE - ) + type_match = re.search(r'type[:\s]+["\'](.+?)["\']', text, re.IGNORECASE) if type_match: text_to_type = type_match.group(1) return f'computer.type("{text_to_type}")' # Try to find key press - key_match = re.search( - r"press[:\s]+(\w+)", text, re.IGNORECASE - ) + key_match = re.search(r"press[:\s]+(\w+)", text, re.IGNORECASE) if key_match: key = key_match.group(1).lower() return f'computer.press("{key}")' # Try to find hotkey - hotkey_match = re.search( - r"hotkey[:\s]+(\w+)\s*\+\s*(\w+)", text, re.IGNORECASE - ) + hotkey_match = re.search(r"hotkey[:\s]+(\w+)\s*\+\s*(\w+)", text, re.IGNORECASE) if hotkey_match: key1, key2 = hotkey_match.group(1).lower(), hotkey_match.group(2).lower() return f'computer.hotkey("{key1}", "{key2}")' diff --git a/openadapt_ml/benchmarks/waa_live.py b/openadapt_ml/benchmarks/waa_live.py index 2484d38..4dfd0d4 100644 --- a/openadapt_ml/benchmarks/waa_live.py +++ b/openadapt_ml/benchmarks/waa_live.py @@ -110,10 +110,7 @@ def check_connection(self) -> bool: True if server responds to /probe endpoint. """ try: - resp = requests.get( - f"{self.config.server_url}/probe", - timeout=5.0 - ) + resp = requests.get(f"{self.config.server_url}/probe", timeout=5.0) return resp.status_code == 200 except requests.RequestException: return False @@ -168,10 +165,7 @@ def reset(self, task: BenchmarkTask) -> BenchmarkObservation: # Try to close all windows for clean state try: - requests.post( - f"{self.config.server_url}/setup/close_all", - timeout=30.0 - ) + requests.post(f"{self.config.server_url}/setup/close_all", timeout=30.0) logger.info("Closed all windows for clean state") except requests.RequestException as e: logger.warning(f"Failed to close windows: {e}") @@ -212,7 +206,7 @@ def step( resp = requests.post( f"{self.config.server_url}/execute_windows", json={"command": command}, - timeout=self.config.timeout + timeout=self.config.timeout, ) if resp.status_code != 200: logger.error(f"Execute failed ({resp.status_code}): {resp.text}") @@ -228,10 +222,7 @@ def step( time.sleep(self.config.action_delay) # Check if done - done = ( - action.type == "done" or - self._step_count >= self.config.max_steps - ) + done = action.type == "done" or self._step_count >= self.config.max_steps obs = self._get_observation() info = { @@ -286,10 +277,7 @@ def _get_observation(self) -> BenchmarkObservation: # Get screenshot try: - resp = requests.get( - f"{self.config.server_url}/screenshot", - timeout=30.0 - ) + resp = requests.get(f"{self.config.server_url}/screenshot", timeout=30.0) if resp.status_code == 200: screenshot = resp.content self._current_screenshot = screenshot @@ -304,7 +292,7 @@ def _get_observation(self) -> BenchmarkObservation: resp = requests.get( f"{self.config.server_url}/accessibility", params={"backend": self.config.a11y_backend}, - timeout=30.0 + timeout=30.0, ) if resp.status_code == 200: result = resp.json() @@ -312,7 +300,9 @@ def _get_observation(self) -> BenchmarkObservation: self._current_a11y = a11y_tree # Extract rects for element-based grounding self._current_rects = self._extract_rects_from_a11y(a11y_tree) - logger.debug("Got accessibility tree with %d elements", len(self._current_rects)) + logger.debug( + "Got accessibility tree with %d elements", len(self._current_rects) + ) else: logger.warning(f"A11y request failed: {resp.status_code}") except requests.RequestException as e: @@ -434,14 +424,16 @@ def _update_waa_computer(self) -> None: try: resp = requests.post( - f"{self.config.server_url}/update_computer", - json=payload, - timeout=30.0 + f"{self.config.server_url}/update_computer", json=payload, timeout=30.0 ) if resp.status_code == 200: - logger.debug("Updated WAA computer with %d rects", len(self._current_rects)) + logger.debug( + "Updated WAA computer with %d rects", len(self._current_rects) + ) else: - logger.warning(f"update_computer failed: {resp.status_code} - {resp.text}") + logger.warning( + f"update_computer failed: {resp.status_code} - {resp.text}" + ) except requests.RequestException as e: logger.error(f"update_computer request error: {e}") @@ -462,7 +454,7 @@ def _run_task_setup(self, raw_config: dict) -> None: requests.post( f"{self.config.server_url}/setup/launch", json={"app": app}, - timeout=30.0 + timeout=30.0, ) logger.info(f"Launched app: {app}") except requests.RequestException as e: @@ -475,7 +467,7 @@ def _run_task_setup(self, raw_config: dict) -> None: requests.post( f"{self.config.server_url}/execute_windows", json={"command": cmd, "shell": "powershell"}, - timeout=60.0 + timeout=60.0, ) logger.info(f"Ran setup command: {cmd[:50]}...") except requests.RequestException as e: @@ -547,7 +539,9 @@ def _translate_action(self, action: BenchmarkAction) -> str | None: logger.warning(f"Unknown action type: {action.type}") return None - def _translate_click_action(self, action: BenchmarkAction, click_method: str) -> str: + def _translate_click_action( + self, action: BenchmarkAction, click_method: str + ) -> str: """Translate click-type action to element-based command. Args: @@ -563,7 +557,9 @@ def _translate_click_action(self, action: BenchmarkAction, click_method: str) -> if elem_id in self._current_rects: return f"computer.mouse.move_id('{elem_id}'); computer.mouse.{click_method}()" else: - logger.warning(f"Element ID '{elem_id}' not found in rects, falling back to coordinates") + logger.warning( + f"Element ID '{elem_id}' not found in rects, falling back to coordinates" + ) # Fallback: use coordinates if provided (less precise) x = action.x if action.x is not None else 0 @@ -600,9 +596,18 @@ def _translate_key_action(self, action: BenchmarkAction) -> str: "End": "end", "PageUp": "pageup", "PageDown": "pagedown", - "F1": "f1", "F2": "f2", "F3": "f3", "F4": "f4", - "F5": "f5", "F6": "f6", "F7": "f7", "F8": "f8", - "F9": "f9", "F10": "f10", "F11": "f11", "F12": "f12", + "F1": "f1", + "F2": "f2", + "F3": "f3", + "F4": "f4", + "F5": "f5", + "F6": "f6", + "F7": "f7", + "F8": "f8", + "F9": "f9", + "F10": "f10", + "F11": "f11", + "F12": "f12", } key = key_map.get(key, key.lower()) diff --git a/openadapt_ml/cloud/azure_inference.py b/openadapt_ml/cloud/azure_inference.py index aa50bcd..f5a49d3 100644 --- a/openadapt_ml/cloud/azure_inference.py +++ b/openadapt_ml/cloud/azure_inference.py @@ -378,9 +378,7 @@ def main(): submit_parser.add_argument( "--checkpoint", "-c", required=True, help="Path to checkpoint directory" ) - submit_parser.add_argument( - "--capture", required=True, help="Path to capture data" - ) + submit_parser.add_argument("--capture", required=True, help="Path to capture data") submit_parser.add_argument( "--epoch", "-e", type=int, default=0, help="Epoch number" ) diff --git a/openadapt_ml/cloud/lambda_labs.py b/openadapt_ml/cloud/lambda_labs.py index 27c18a4..9670ebe 100644 --- a/openadapt_ml/cloud/lambda_labs.py +++ b/openadapt_ml/cloud/lambda_labs.py @@ -44,7 +44,9 @@ DEFAULT_SERVER_PORT = 8765 -def start_dashboard_server(output_dir: Path, port: int = DEFAULT_SERVER_PORT) -> tuple[subprocess.Popen, str]: +def start_dashboard_server( + output_dir: Path, port: int = DEFAULT_SERVER_PORT +) -> tuple[subprocess.Popen, str]: """Start a background HTTP server for the dashboard. Args: @@ -94,7 +96,9 @@ def open_dashboard_in_browser(output_dir: Path, port: int = DEFAULT_SERVER_PORT) return None -def setup_capture_screenshots_symlink(output_dir: Path, capture_path: str | Path) -> bool: +def setup_capture_screenshots_symlink( + output_dir: Path, capture_path: str | Path +) -> bool: """Create symlink from output_dir/screenshots to capture's screenshots folder. This allows the dashboard to serve screenshots via relative paths. @@ -126,7 +130,9 @@ def setup_capture_screenshots_symlink(output_dir: Path, capture_path: str | Path return False -def rewrite_evaluation_paths(evaluations: list[dict], remote_prefix: str = "/home/ubuntu/capture/") -> list[dict]: +def rewrite_evaluation_paths( + evaluations: list[dict], remote_prefix: str = "/home/ubuntu/capture/" +) -> list[dict]: """Rewrite Lambda paths in evaluations to relative paths. Converts: /home/ubuntu/capture/screenshots/foo.png -> screenshots/foo.png @@ -144,7 +150,9 @@ def rewrite_evaluation_paths(evaluations: list[dict], remote_prefix: str = "/hom return evaluations -def download_checkpoints_from_instance(instance_ip: str, output_dir: Path, ssh_key: str | None = None) -> bool: +def download_checkpoints_from_instance( + instance_ip: str, output_dir: Path, ssh_key: str | None = None +) -> bool: """Download checkpoints from Lambda instance. Args: @@ -159,7 +167,9 @@ def download_checkpoints_from_instance(instance_ip: str, output_dir: Path, ssh_k checkpoints_dir.mkdir(parents=True, exist_ok=True) ssh_key = ssh_key or str(Path.home() / ".ssh" / "lambda_id_ed25519") - ssh_opts = f"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {ssh_key}" + ssh_opts = ( + f"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {ssh_key}" + ) # Download checkpoints from remote remote_path = f"ubuntu@{instance_ip}:~/openadapt-ml/checkpoints/" @@ -185,6 +195,7 @@ def check_stop_signal(output_dir: Path) -> bool: @dataclass class InstanceType: """Lambda Labs instance type.""" + name: str price_cents_per_hour: int description: str @@ -214,6 +225,7 @@ def __str__(self) -> str: @dataclass class Instance: """Running Lambda Labs instance.""" + id: str name: str instance_type: str @@ -234,6 +246,7 @@ def __init__(self, api_key: str | None = None): # Try provided key, then settings, then env var if not api_key: from openadapt_ml.config import settings + api_key = settings.lambda_api_key or os.environ.get("LAMBDA_API_KEY") self.api_key = api_key @@ -266,19 +279,25 @@ def list_instance_types(self) -> list[InstanceType]: for name, info in data.get("data", {}).items(): specs = info.get("instance_type", {}).get("specs", {}) - regions = [r["name"] for r in info.get("regions_with_capacity_available", [])] - - types.append(InstanceType( - name=name, - price_cents_per_hour=info.get("instance_type", {}).get("price_cents_per_hour", 0), - description=info.get("instance_type", {}).get("description", ""), - gpu_count=specs.get("gpus", 0), - gpu_type=info.get("instance_type", {}).get("gpu_description", ""), - vcpus=specs.get("vcpus", 0), - memory_gb=specs.get("memory_gib", 0), - storage_gb=specs.get("storage_gib", 0), - available_regions=regions, - )) + regions = [ + r["name"] for r in info.get("regions_with_capacity_available", []) + ] + + types.append( + InstanceType( + name=name, + price_cents_per_hour=info.get("instance_type", {}).get( + "price_cents_per_hour", 0 + ), + description=info.get("instance_type", {}).get("description", ""), + gpu_count=specs.get("gpus", 0), + gpu_type=info.get("instance_type", {}).get("gpu_description", ""), + vcpus=specs.get("vcpus", 0), + memory_gb=specs.get("memory_gib", 0), + storage_gb=specs.get("storage_gib", 0), + available_regions=regions, + ) + ) # Sort by price types.sort(key=lambda t: t.price_cents_per_hour) @@ -307,15 +326,17 @@ def list_instances(self) -> list[Instance]: else: ssh_key_names = ssh_keys # Already list of strings - instances.append(Instance( - id=inst["id"], - name=inst.get("name", ""), - instance_type=inst.get("instance_type", {}).get("name", "unknown"), - status=inst.get("status", "unknown"), - ip=inst.get("ip"), - region=inst.get("region", {}).get("name", "unknown"), - ssh_key_names=ssh_key_names, - )) + instances.append( + Instance( + id=inst["id"], + name=inst.get("name", ""), + instance_type=inst.get("instance_type", {}).get("name", "unknown"), + status=inst.get("status", "unknown"), + ip=inst.get("ip"), + region=inst.get("region", {}).get("name", "unknown"), + ssh_key_names=ssh_key_names, + ) + ) return instances @@ -391,9 +412,18 @@ def launch_instance( for attempt in range(60): # Wait up to 5 minutes for SSH try: result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", - f"ubuntu@{instance.ip}", "echo ready"], - capture_output=True, text=True, timeout=20 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=10", + f"ubuntu@{instance.ip}", + "echo ready", + ], + capture_output=True, + text=True, + timeout=20, ) if result.returncode == 0: print("SSH ready!") @@ -401,7 +431,7 @@ def launch_instance( except subprocess.TimeoutExpired: pass if attempt % 6 == 5: # Log progress every 30 seconds - print(f" Still waiting for SSH ({(attempt+1)*5}s elapsed)...") + print(f" Still waiting for SSH ({(attempt + 1) * 5}s elapsed)...") time.sleep(5) print("Warning: SSH may not be ready yet, continuing anyway...") @@ -409,7 +439,9 @@ def launch_instance( def terminate_instance(self, instance_id: str) -> bool: """Terminate an instance.""" - data = self._post("/instance-operations/terminate", {"instance_ids": [instance_id]}) + data = self._post( + "/instance-operations/terminate", {"instance_ids": [instance_id]} + ) terminated = data.get("data", {}).get("terminated_instances", []) return any(t.get("id") == instance_id for t in terminated) @@ -419,7 +451,13 @@ def get_ssh_command(self, instance: Instance, user: str = "ubuntu") -> str: return "# Instance IP not yet available" return f"ssh {user}@{instance.ip}" - def ssh_run(self, instance: Instance, command: str, timeout: int | None = None, retries: int = 3) -> subprocess.CompletedProcess: + def ssh_run( + self, + instance: Instance, + command: str, + timeout: int | None = None, + retries: int = 3, + ) -> subprocess.CompletedProcess: """Run a command on an instance via SSH. Args: @@ -435,12 +473,17 @@ def ssh_run(self, instance: Instance, command: str, timeout: int | None = None, raise RuntimeError("Instance has no IP address") ssh_cmd = [ - "ssh", "-o", "StrictHostKeyChecking=no", - "-o", "ConnectTimeout=30", # Increased from 10 - "-o", "ServerAliveInterval=60", # Keep connection alive - "-o", "ServerAliveCountMax=3", + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=30", # Increased from 10 + "-o", + "ServerAliveInterval=60", # Keep connection alive + "-o", + "ServerAliveCountMax=3", f"ubuntu@{instance.ip}", - command + command, ] last_error = None @@ -460,7 +503,12 @@ def ssh_run(self, instance: Instance, command: str, timeout: int | None = None, raise last_error if last_error else RuntimeError("SSH failed") - def setup_instance(self, instance: Instance, repo_url: str = "https://github.com/OpenAdaptAI/openadapt-ml.git", clean_gpu: bool = True) -> bool: + def setup_instance( + self, + instance: Instance, + repo_url: str = "https://github.com/OpenAdaptAI/openadapt-ml.git", + clean_gpu: bool = True, + ) -> bool: """Set up training environment on instance. Clones repo, installs uv, syncs dependencies. @@ -473,7 +521,9 @@ def setup_instance(self, instance: Instance, repo_url: str = "https://github.com if clean_gpu: print(" Clearing GPU memory...") try: - self.ssh_run(instance, ''' + self.ssh_run( + instance, + """ python3 -c " import torch if torch.cuda.is_available(): @@ -483,11 +533,13 @@ def setup_instance(self, instance: Instance, repo_url: str = "https://github.com " 2>/dev/null || true # Kill any stale python processes using GPU pkill -f "python.*train" 2>/dev/null || true -''', timeout=60) +""", + timeout=60, + ) except Exception as e: print(f" GPU cleanup skipped: {e}") - setup_script = f''' + setup_script = f""" set -e cd ~ @@ -507,10 +559,12 @@ def setup_instance(self, instance: Instance, repo_url: str = "https://github.com cd openadapt-ml uv sync echo "SETUP_COMPLETE" -''' +""" try: - result = self.ssh_run(instance, setup_script, timeout=900) # 15 min timeout for setup + result = self.ssh_run( + instance, setup_script, timeout=900 + ) # 15 min timeout for setup if "SETUP_COMPLETE" in result.stdout: print(" Environment ready") @@ -526,7 +580,9 @@ def setup_instance(self, instance: Instance, repo_url: str = "https://github.com print(f" Setup failed: {e}") return False - def sync_local_code(self, instance: Instance, local_repo_path: str = ".", retries: int = 3) -> bool: + def sync_local_code( + self, instance: Instance, local_repo_path: str = ".", retries: int = 3 + ) -> bool: """Sync local code changes to remote instance. Uses rsync to push local code, excluding .venv, .git, etc. @@ -549,19 +605,30 @@ def sync_local_code(self, instance: Instance, local_repo_path: str = ".", retrie ssh_opts = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=60" rsync_cmd = [ - "rsync", "-avz", "--progress", + "rsync", + "-avz", + "--progress", "--timeout=120", # 2 minute timeout per file - "--exclude", ".venv", - "--exclude", ".git", - "--exclude", "__pycache__", - "--exclude", "*.pyc", - "--exclude", ".env", - "--exclude", "training_output", - "--exclude", "checkpoints", - "--exclude", "synthetic*", - "-e", ssh_opts, + "--exclude", + ".venv", + "--exclude", + ".git", + "--exclude", + "__pycache__", + "--exclude", + "*.pyc", + "--exclude", + ".env", + "--exclude", + "training_output", + "--exclude", + "checkpoints", + "--exclude", + "synthetic*", + "-e", + ssh_opts, f"{local_repo_path}/", - f"ubuntu@{instance.ip}:~/openadapt-ml/" + f"ubuntu@{instance.ip}:~/openadapt-ml/", ] for attempt in range(retries): @@ -575,7 +642,13 @@ def sync_local_code(self, instance: Instance, local_repo_path: str = ".", retrie return False - def upload_capture(self, instance: Instance, local_path: str, remote_path: str = "~/capture", retries: int = 3) -> bool: + def upload_capture( + self, + instance: Instance, + local_path: str, + remote_path: str = "~/capture", + retries: int = 3, + ) -> bool: """Upload a capture directory to instance via rsync. Args: @@ -596,11 +669,14 @@ def upload_capture(self, instance: Instance, local_path: str, remote_path: str = ssh_opts = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=60" rsync_cmd = [ - "rsync", "-avz", "--progress", + "rsync", + "-avz", + "--progress", "--timeout=120", # 2 minute timeout per file - "-e", ssh_opts, + "-e", + ssh_opts, f"{local_path}/", - f"ubuntu@{instance.ip}:{remote_path}/" + f"ubuntu@{instance.ip}:{remote_path}/", ] for attempt in range(retries): @@ -644,16 +720,18 @@ def run_training( train_cmd += f' --goal "{goal}"' # Full script with environment setup - script = f''' + script = f""" cd ~/openadapt-ml export PATH="$HOME/.local/bin:$PATH" {train_cmd} -''' +""" ssh_cmd = [ - "ssh", "-o", "StrictHostKeyChecking=no", + "ssh", + "-o", + "StrictHostKeyChecking=no", f"ubuntu@{instance.ip}", - script + script, ] print(f"Running training on {instance.ip}...") @@ -703,10 +781,12 @@ def download_results( if include_logs: print(" Downloading training logs...") rsync_cmd = [ - "rsync", "-avz", - "-e", "ssh -o StrictHostKeyChecking=no", + "rsync", + "-avz", + "-e", + "ssh -o StrictHostKeyChecking=no", f"ubuntu@{instance.ip}:{remote_path}/training_output/", - f"{local_path}/training_output_lambda/" + f"{local_path}/training_output_lambda/", ] result = subprocess.run(rsync_cmd, capture_output=True) if result.returncode == 0: @@ -719,10 +799,12 @@ def download_results( if include_checkpoint: print(" Downloading checkpoint...") rsync_cmd = [ - "rsync", "-avz", - "-e", "ssh -o StrictHostKeyChecking=no", + "rsync", + "-avz", + "-e", + "ssh -o StrictHostKeyChecking=no", f"ubuntu@{instance.ip}:{remote_path}/checkpoints/", - f"{local_path}/checkpoints_lambda/" + f"{local_path}/checkpoints_lambda/", ] result = subprocess.run(rsync_cmd, capture_output=True) if result.returncode == 0: @@ -734,6 +816,7 @@ def download_results( if include_logs: try: from openadapt_ml.training.trainer import regenerate_all_dashboards + output_dir = Path(local_path) / "training_output_lambda" if output_dir.exists(): print(" Regenerating dashboards with static navigation...") @@ -752,6 +835,7 @@ def get_training_status(self, instance: Instance) -> dict: ) try: import json + return json.loads(result.stdout.strip()) except (json.JSONDecodeError, ValueError): return {} @@ -803,11 +887,14 @@ def main(): # Launch command launch_parser = subparsers.add_parser("launch", help="Launch a GPU instance") launch_parser.add_argument( - "--type", "-t", + "--type", + "-t", default="gpu_1x_a100", help="Instance type (default: gpu_1x_a100)", ) - launch_parser.add_argument("--region", "-r", help="Region (auto-selects if not specified)") + launch_parser.add_argument( + "--region", "-r", help="Region (auto-selects if not specified)" + ) launch_parser.add_argument("--name", "-n", help="Instance name") # Terminate command @@ -815,23 +902,52 @@ def main(): term_parser.add_argument("instance_id", help="Instance ID to terminate") # SSH command - run commands or get interactive shell - ssh_parser = subparsers.add_parser("ssh", help="SSH into Lambda instance or run command") - ssh_parser.add_argument("instance_id", nargs="?", help="Instance ID (uses first if not specified)") - ssh_parser.add_argument("--cmd", "-c", help="Command to run (opens shell if not specified)") - ssh_parser.add_argument("--timeout", "-t", type=int, default=60, help="Command timeout in seconds") + ssh_parser = subparsers.add_parser( + "ssh", help="SSH into Lambda instance or run command" + ) + ssh_parser.add_argument( + "instance_id", nargs="?", help="Instance ID (uses first if not specified)" + ) + ssh_parser.add_argument( + "--cmd", "-c", help="Command to run (opens shell if not specified)" + ) + ssh_parser.add_argument( + "--timeout", "-t", type=int, default=60, help="Command timeout in seconds" + ) # Serve command - start dashboard server with stop button support - serve_parser = subparsers.add_parser("serve", help="Start dashboard server with stop button support") - serve_parser.add_argument("--output", "-o", default="training_output", help="Output directory (default: training_output)") - serve_parser.add_argument("--port", "-p", type=int, default=8765, help="Port (default: 8765)") - serve_parser.add_argument("--open", action="store_true", help="Open dashboard in browser") + serve_parser = subparsers.add_parser( + "serve", help="Start dashboard server with stop button support" + ) + serve_parser.add_argument( + "--output", + "-o", + default="training_output", + help="Output directory (default: training_output)", + ) + serve_parser.add_argument( + "--port", "-p", type=int, default=8765, help="Port (default: 8765)" + ) + serve_parser.add_argument( + "--open", action="store_true", help="Open dashboard in browser" + ) # Rsync command - copy files to/from Lambda instance - rsync_parser = subparsers.add_parser("rsync", help="Rsync files to/from Lambda instance") - rsync_parser.add_argument("source", help="Source path (prefix with 'remote:' for remote paths)") - rsync_parser.add_argument("dest", help="Destination path (prefix with 'remote:' for remote paths)") - rsync_parser.add_argument("instance_id", nargs="?", help="Instance ID (uses first if not specified)") - rsync_parser.add_argument("--delete", action="store_true", help="Delete extraneous files from dest") + rsync_parser = subparsers.add_parser( + "rsync", help="Rsync files to/from Lambda instance" + ) + rsync_parser.add_argument( + "source", help="Source path (prefix with 'remote:' for remote paths)" + ) + rsync_parser.add_argument( + "dest", help="Destination path (prefix with 'remote:' for remote paths)" + ) + rsync_parser.add_argument( + "instance_id", nargs="?", help="Instance ID (uses first if not specified)" + ) + rsync_parser.add_argument( + "--delete", action="store_true", help="Delete extraneous files from dest" + ) # Setup command subparsers.add_parser("setup", help="Set up SSH key for Lambda Labs") @@ -840,87 +956,215 @@ def main(): train_parser = subparsers.add_parser("train", help="Run training on Lambda GPU") train_parser.add_argument("--capture", "-c", help="Local path to capture directory") train_parser.add_argument("--goal", "-g", help="Task goal description") - train_parser.add_argument("--config", default="configs/qwen3vl_capture_4bit.yaml", help="Config file (default: 4bit for memory efficiency)") - train_parser.add_argument("--type", "-t", default="gpu_1x_a10", help="Instance type") - train_parser.add_argument("--instance", "-i", help="Use existing instance ID instead of launching new") - train_parser.add_argument("--no-terminate", action="store_true", help="Don't terminate instance after training") - train_parser.add_argument("--max-runtime", type=int, default=60, help="Max runtime in minutes before auto-terminate (default: 60)") - train_parser.add_argument("--open", action="store_true", help="Open dashboard in browser when training starts") + train_parser.add_argument( + "--config", + default="configs/qwen3vl_capture_4bit.yaml", + help="Config file (default: 4bit for memory efficiency)", + ) + train_parser.add_argument( + "--type", "-t", default="gpu_1x_a10", help="Instance type" + ) + train_parser.add_argument( + "--instance", "-i", help="Use existing instance ID instead of launching new" + ) + train_parser.add_argument( + "--no-terminate", + action="store_true", + help="Don't terminate instance after training", + ) + train_parser.add_argument( + "--max-runtime", + type=int, + default=60, + help="Max runtime in minutes before auto-terminate (default: 60)", + ) + train_parser.add_argument( + "--open", + action="store_true", + help="Open dashboard in browser when training starts", + ) # Training status command - train_status_parser = subparsers.add_parser("train-status", help="Check training status on instance") + train_status_parser = subparsers.add_parser( + "train-status", help="Check training status on instance" + ) train_status_parser.add_argument("instance_id", nargs="?", help="Instance ID") # Monitor command - live dashboard for Lambda training - monitor_parser = subparsers.add_parser("monitor", help="Monitor Lambda training with live dashboard") + monitor_parser = subparsers.add_parser( + "monitor", help="Monitor Lambda training with live dashboard" + ) monitor_parser.add_argument("instance_id", nargs="?", help="Instance ID") - monitor_parser.add_argument("--open", action="store_true", help="Open dashboard in browser") - monitor_parser.add_argument("--interval", type=int, default=5, help="Poll interval in seconds (default: 5)") - monitor_parser.add_argument("--capture", type=str, help="Local capture path for screenshot symlink") - monitor_parser.add_argument("--auto-stop-loss", type=float, default=0.5, help="Auto-terminate when loss drops below this (default: 0.5)") - monitor_parser.add_argument("--download-checkpoints", action="store_true", default=True, help="Auto-download checkpoints each epoch") - monitor_parser.add_argument("--no-download-checkpoints", action="store_false", dest="download_checkpoints", help="Disable checkpoint download") - monitor_parser.add_argument("--stub", action="store_true", help="Use stub training provider (no GPU, instant simulation)") + monitor_parser.add_argument( + "--open", action="store_true", help="Open dashboard in browser" + ) + monitor_parser.add_argument( + "--interval", type=int, default=5, help="Poll interval in seconds (default: 5)" + ) + monitor_parser.add_argument( + "--capture", type=str, help="Local capture path for screenshot symlink" + ) + monitor_parser.add_argument( + "--auto-stop-loss", + type=float, + default=0.5, + help="Auto-terminate when loss drops below this (default: 0.5)", + ) + monitor_parser.add_argument( + "--download-checkpoints", + action="store_true", + default=True, + help="Auto-download checkpoints each epoch", + ) + monitor_parser.add_argument( + "--no-download-checkpoints", + action="store_false", + dest="download_checkpoints", + help="Disable checkpoint download", + ) + monitor_parser.add_argument( + "--stub", + action="store_true", + help="Use stub training provider (no GPU, instant simulation)", + ) # Refresh command - one-shot dashboard update - refresh_parser = subparsers.add_parser("refresh", help="One-shot refresh of training dashboard") + refresh_parser = subparsers.add_parser( + "refresh", help="One-shot refresh of training dashboard" + ) refresh_parser.add_argument("instance_id", nargs="?", help="Instance ID") - refresh_parser.add_argument("--open", action="store_true", help="Open dashboard in browser") - refresh_parser.add_argument("--capture", type=str, help="Local capture path for screenshot preview") + refresh_parser.add_argument( + "--open", action="store_true", help="Open dashboard in browser" + ) + refresh_parser.add_argument( + "--capture", type=str, help="Local capture path for screenshot preview" + ) # Checkpoints command - list remote checkpoints - checkpoints_parser = subparsers.add_parser("checkpoints", help="List checkpoints on remote instance") + checkpoints_parser = subparsers.add_parser( + "checkpoints", help="List checkpoints on remote instance" + ) checkpoints_parser.add_argument("instance_id", nargs="?", help="Instance ID") # Download results command - download_parser = subparsers.add_parser("download", help="Download training results from instance") + download_parser = subparsers.add_parser( + "download", help="Download training results from instance" + ) download_parser.add_argument("instance_id", nargs="?", help="Instance ID") - download_parser.add_argument("--output", "-o", default=".", help="Local output directory") + download_parser.add_argument( + "--output", "-o", default=".", help="Local output directory" + ) # Check files on instance - files_parser = subparsers.add_parser("files", help="List training files on instance") + files_parser = subparsers.add_parser( + "files", help="List training files on instance" + ) files_parser.add_argument("instance_id", nargs="?", help="Instance ID") - files_parser.add_argument("--path", "-p", default="~/openadapt-ml", help="Path to check") + files_parser.add_argument( + "--path", "-p", default="~/openadapt-ml", help="Path to check" + ) # Kill command - terminate training processes - kill_parser = subparsers.add_parser("kill", help="Kill training/inference processes on instance") + kill_parser = subparsers.add_parser( + "kill", help="Kill training/inference processes on instance" + ) kill_parser.add_argument("instance_id", nargs="?", help="Instance ID") - kill_parser.add_argument("--local", action="store_true", help="Also kill local Lambda-related processes") - kill_parser.add_argument("--all", action="store_true", help="Kill all Python processes on instance (careful!)") + kill_parser.add_argument( + "--local", action="store_true", help="Also kill local Lambda-related processes" + ) + kill_parser.add_argument( + "--all", + action="store_true", + help="Kill all Python processes on instance (careful!)", + ) # Check command - analyze training status and early stopping - check_parser = subparsers.add_parser("check", help="Check training health and early stopping status") + check_parser = subparsers.add_parser( + "check", help="Check training health and early stopping status" + ) check_parser.add_argument("instance_id", nargs="?", help="Instance ID") - check_parser.add_argument("--threshold", "-t", type=float, default=0.01, - help="Early stopping threshold (loss improvement over last N steps)") - check_parser.add_argument("--window", "-w", type=int, default=10, - help="Number of recent steps to check for improvement") + check_parser.add_argument( + "--threshold", + "-t", + type=float, + default=0.01, + help="Early stopping threshold (loss improvement over last N steps)", + ) + check_parser.add_argument( + "--window", + "-w", + type=int, + default=10, + help="Number of recent steps to check for improvement", + ) # Compare command - run comparison on Lambda and sync back - compare_parser = subparsers.add_parser("compare", help="Run human vs AI comparison on Lambda") + compare_parser = subparsers.add_parser( + "compare", help="Run human vs AI comparison on Lambda" + ) compare_parser.add_argument("instance_id", nargs="?", help="Instance ID") - compare_parser.add_argument("--checkpoint", "-c", help="Checkpoint to use (default: latest)") - compare_parser.add_argument("--epoch", "-e", type=int, help="Use checkpoint from specific epoch") - compare_parser.add_argument("--open", action="store_true", help="Open viewer after generation") + compare_parser.add_argument( + "--checkpoint", "-c", help="Checkpoint to use (default: latest)" + ) + compare_parser.add_argument( + "--epoch", "-e", type=int, help="Use checkpoint from specific epoch" + ) + compare_parser.add_argument( + "--open", action="store_true", help="Open viewer after generation" + ) # Results viewer command - downloads and generates comparison viewer - results_parser = subparsers.add_parser("results", help="Download results and generate comparison viewer") - results_parser.add_argument("--capture", "-c", required=True, help="Local capture directory (for comparison)") + results_parser = subparsers.add_parser( + "results", help="Download results and generate comparison viewer" + ) + results_parser.add_argument( + "--capture", + "-c", + required=True, + help="Local capture directory (for comparison)", + ) results_parser.add_argument("--goal", "-g", help="Task goal description") - results_parser.add_argument("--open", action="store_true", help="Open viewer in browser") + results_parser.add_argument( + "--open", action="store_true", help="Open viewer in browser" + ) results_parser.add_argument("instance_id", nargs="?", help="Instance ID") # Sync command - sync training output and regenerate navigation for file:// protocol - sync_parser = subparsers.add_parser("sync", help="Sync training output from Lambda and regenerate navigation") + sync_parser = subparsers.add_parser( + "sync", help="Sync training output from Lambda and regenerate navigation" + ) sync_parser.add_argument("instance_id", nargs="?", help="Instance ID") - sync_parser.add_argument("--output", "-o", default="training_output", help="Local output directory (default: training_output)") - sync_parser.add_argument("--open", action="store_true", help="Open dashboard in browser after sync") + sync_parser.add_argument( + "--output", + "-o", + default="training_output", + help="Local output directory (default: training_output)", + ) + sync_parser.add_argument( + "--open", action="store_true", help="Open dashboard in browser after sync" + ) # Viewer command - regenerate local viewer (no Lambda required) - viewer_parser = subparsers.add_parser("viewer", help="Regenerate local viewer (no Lambda required)") - viewer_parser.add_argument("--output", "-o", default="training_output", help="Training output directory (default: training_output)") - viewer_parser.add_argument("--dashboard", "-d", action="store_true", help="Regenerate dashboard instead of viewer") - viewer_parser.add_argument("--open", action="store_true", help="Open in browser (use 'serve' instead for better experience)") + viewer_parser = subparsers.add_parser( + "viewer", help="Regenerate local viewer (no Lambda required)" + ) + viewer_parser.add_argument( + "--output", + "-o", + default="training_output", + help="Training output directory (default: training_output)", + ) + viewer_parser.add_argument( + "--dashboard", + "-d", + action="store_true", + help="Regenerate dashboard instead of viewer", + ) + viewer_parser.add_argument( + "--open", + action="store_true", + help="Open in browser (use 'serve' instead for better experience)", + ) args = parser.parse_args() @@ -942,7 +1186,9 @@ def main(): for t in types: print(f" {t}") print(f"\nTotal: {len(types)} instance types") - print("\nLaunch with: python -m openadapt_ml.cloud.lambda_labs launch --type ") + print( + "\nLaunch with: python -m openadapt_ml.cloud.lambda_labs launch --type " + ) elif args.command == "status": instances = client.list_instances() @@ -971,7 +1217,9 @@ def main(): print(f" Type: {instance.instance_type}") print(f" Region: {instance.region}") print(f"\nConnect with: ssh ubuntu@{instance.ip}") - print(f"\nTerminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}") + print( + f"\nTerminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}" + ) elif args.command == "terminate": if client.terminate_instance(args.instance_id): @@ -986,14 +1234,16 @@ def main(): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return else: instance = instances[0] - if hasattr(args, 'cmd') and args.cmd: + if hasattr(args, "cmd") and args.cmd: # Run single command print(f"Running on {instance.ip}: {args.cmd}") result = client.ssh_run(instance, args.cmd, timeout=args.timeout) @@ -1015,7 +1265,9 @@ def main(): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1032,8 +1284,11 @@ def main(): dest = f"ubuntu@{instance.ip}:{dest[7:]}" rsync_cmd = [ - "rsync", "-avz", "--progress", - "-e", "ssh -o StrictHostKeyChecking=no", + "rsync", + "-avz", + "--progress", + "-e", + "ssh -o StrictHostKeyChecking=no", ] if args.delete: rsync_cmd.append("--delete") @@ -1067,7 +1322,9 @@ def main(): # Get or launch instance if args.instance: instances = client.list_instances() - instance = next((i for i in instances if i.id.startswith(args.instance)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance)), None + ) if not instance: print(f"Error: Instance {args.instance} not found") return @@ -1095,16 +1352,21 @@ def main(): # Generate initial dashboard with setup status from pathlib import Path from openadapt_ml.training.trainer import ( - TrainingState, TrainingConfig, generate_training_dashboard, - setup_job_directory + TrainingState, + TrainingConfig, + generate_training_dashboard, + setup_job_directory, ) import time as time_module + job_id = time_module.strftime("%Y%m%d_%H%M%S") output_dir = setup_job_directory("training_output", job_id) dashboard_path = output_dir / "dashboard.html" log_path = output_dir / "training_log.json" - def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, epoch: int = 0): + def update_dashboard( + status: str, logs: list, step: int = 0, loss: float = 0.0, epoch: int = 0 + ): """Update dashboard with current setup/training status.""" state = TrainingState(job_id=job_id) state.cloud_provider = "lambda" @@ -1151,9 +1413,13 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, update_dashboard("installing", setup_logs) break if setup_attempt < 2: - setup_logs.append(f"Setup attempt {setup_attempt + 1} failed, retrying in 30s...") + setup_logs.append( + f"Setup attempt {setup_attempt + 1} failed, retrying in 30s..." + ) update_dashboard("booting", setup_logs) - print(f" Setup attempt {setup_attempt + 1} failed, retrying in 30s...") + print( + f" Setup attempt {setup_attempt + 1} failed, retrying in 30s..." + ) time_module.sleep(30) if not setup_success: @@ -1162,14 +1428,18 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print("\nError: Failed to set up instance after 3 attempts") print(f"Instance still running: {instance.ip}") print("Debug via: ssh ubuntu@" + instance.ip) - print(f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}") + print( + f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}" + ) return # Don't terminate - let user debug # Sync local code to ensure remote has latest changes setup_logs.append("Syncing local code to instance...") update_dashboard("installing", setup_logs) if not client.sync_local_code(instance): - setup_logs.append("Warning: Failed to sync local code, using remote repo version") + setup_logs.append( + "Warning: Failed to sync local code, using remote repo version" + ) update_dashboard("installing", setup_logs) print("Warning: Failed to sync local code, using remote repo version") else: @@ -1192,7 +1462,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print("\nError: Failed to upload capture after retries") print(f"Instance still running: {instance.ip}") print("Debug via: ssh ubuntu@" + instance.ip) - print(f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}") + print( + f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}" + ) return # Don't terminate - let user debug # Run training in background and poll for status @@ -1214,7 +1486,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, poll_interval = 10 # seconds last_step = 0 last_epoch = 0 - print(f"Polling training status every {poll_interval}s (Ctrl+C to stop)...\n") + print( + f"Polling training status every {poll_interval}s (Ctrl+C to stop)...\n" + ) while True: try: @@ -1229,7 +1503,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, # Print progress when step changes if step > last_step or epoch > last_epoch: - print(f" Epoch {epoch+1}/{total_epochs} | Step {step} | Loss: {loss:.4f} | Elapsed: {elapsed_training:.0f}s") + print( + f" Epoch {epoch + 1}/{total_epochs} | Step {step} | Loss: {loss:.4f} | Elapsed: {elapsed_training:.0f}s" + ) last_step = step last_epoch = epoch @@ -1241,7 +1517,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, status["instance_type"] = instance.instance_type # Add cloud provider info status["cloud_provider"] = "lambda" - status["cloud_dashboard_url"] = "https://cloud.lambda.ai/instances" + status["cloud_dashboard_url"] = ( + "https://cloud.lambda.ai/instances" + ) status["cloud_instance_id"] = instance.id status["setup_status"] = "training" status["setup_logs"] = setup_logs @@ -1269,9 +1547,11 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, config = TrainingConfig( num_train_epochs=total_epochs, - learning_rate=status.get("learning_rate", 5e-5) + learning_rate=status.get("learning_rate", 5e-5), + ) + dashboard_path.write_text( + generate_training_dashboard(state, config) ) - dashboard_path.write_text(generate_training_dashboard(state, config)) # Check if training is complete (all epochs done) if epoch >= total_epochs - 1: @@ -1313,13 +1593,15 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print("=" * 50) # Determine the final checkpoint path (main checkpoint after training) - checkpoint_path = "/home/ubuntu/openadapt-ml/checkpoints/qwen3vl2b_capture_lora" + checkpoint_path = ( + "/home/ubuntu/openadapt-ml/checkpoints/qwen3vl2b_capture_lora" + ) # Check if checkpoint exists result = client.ssh_run( instance, f"ls {checkpoint_path}/adapter_config.json 2>/dev/null && echo 'exists'", - timeout=30 + timeout=30, ) if "exists" in result.stdout: @@ -1331,7 +1613,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, --checkpoint {checkpoint_path} \ --output training_output/{output_name} 2>&1""" - print(" Generating comparison viewer (this may take a few minutes)...") + print( + " Generating comparison viewer (this may take a few minutes)..." + ) result = client.ssh_run(instance, cmd, timeout=600) if result.returncode == 0: @@ -1352,13 +1636,15 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print(f"\nTerminating instance {instance.id[:8]}...") client.terminate_instance(instance.id) print("Instance terminated.") - print(f"\nFinal cost: ~${cost:.2f} ({elapsed/60:.1f} minutes)") + print(f"\nFinal cost: ~${cost:.2f} ({elapsed / 60:.1f} minutes)") else: print(f"\nInstance still running: {instance.ip}") print(f" Current cost: ~${cost:.2f}") if not training_completed: print(" (Not terminating - training did not complete successfully)") - print(f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}") + print( + f"Terminate with: python -m openadapt_ml.cloud.lambda_labs terminate {instance.id}" + ) elif args.command == "train-status": instances = client.list_instances() @@ -1367,7 +1653,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1393,7 +1681,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1403,10 +1693,14 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, print(f"Checking checkpoints on {instance.ip}...") ssh_cmd = [ - "ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=10", f"ubuntu@{instance.ip}", "ls -la ~/openadapt-ml/checkpoints/ 2>/dev/null && " - "du -sh ~/openadapt-ml/checkpoints/ 2>/dev/null || echo 'No checkpoints directory found'" + "du -sh ~/openadapt-ml/checkpoints/ 2>/dev/null || echo 'No checkpoints directory found'", ] result = subprocess.run(ssh_cmd, capture_output=True, text=True) @@ -1421,7 +1715,11 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, # One-shot dashboard refresh import time as time_module from pathlib import Path - from openadapt_ml.training.trainer import TrainingState, TrainingConfig, generate_training_dashboard + from openadapt_ml.training.trainer import ( + TrainingState, + TrainingConfig, + generate_training_dashboard, + ) instances = client.list_instances() if not instances: @@ -1429,7 +1727,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1437,7 +1737,11 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, instance = instances[0] # Use current job directory via symlink - from openadapt_ml.training.trainer import get_current_job_directory, setup_job_directory + from openadapt_ml.training.trainer import ( + get_current_job_directory, + setup_job_directory, + ) + base_dir = Path("training_output") base_dir.mkdir(exist_ok=True) @@ -1454,7 +1758,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, log_path = output_dir / "training_log.json" # Setup screenshots symlink if local capture path provided - local_capture = args.capture if hasattr(args, 'capture') and args.capture else None + local_capture = ( + args.capture if hasattr(args, "capture") and args.capture else None + ) if local_capture: setup_capture_screenshots_symlink(output_dir, local_capture) @@ -1478,7 +1784,9 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, state.instance_type = instance.instance_type state.config_path = status.get("config_path", "") # Use local capture path for screenshots if provided, else remote path - state.capture_path = args.capture if args.capture else status.get("capture_path", "") + state.capture_path = ( + args.capture if args.capture else status.get("capture_path", "") + ) state.epoch = status.get("epoch", 0) state.step = status.get("step", 0) state.loss = status.get("loss", 0) @@ -1496,7 +1804,7 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, config = TrainingConfig( num_train_epochs=status.get("total_epochs", 5), - learning_rate=status.get("learning_rate", 5e-5) + learning_rate=status.get("learning_rate", 5e-5), ) dashboard_path.write_text(generate_training_dashboard(state, config)) @@ -1504,6 +1812,7 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, # Regenerate navigation for file:// protocol try: from openadapt_ml.training.trainer import regenerate_all_dashboards + regenerate_all_dashboards(output_dir) except Exception: pass # Silent fail for navigation @@ -1512,11 +1821,14 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, step = status.get("step", 0) loss = status.get("loss", 0) elapsed = status.get("elapsed_time", 0) - print(f"Epoch {epoch+1}/{state.total_epochs} | Step {step} | Loss: {loss:.4f} | Elapsed: {elapsed:.0f}s") + print( + f"Epoch {epoch + 1}/{state.total_epochs} | Step {step} | Loss: {loss:.4f} | Elapsed: {elapsed:.0f}s" + ) print(f"Dashboard: {dashboard_path.absolute()}") if args.open: import subprocess as sp + sp.run(["open", str(dashboard_path)], capture_output=True) else: print("No training data yet") @@ -1528,10 +1840,12 @@ def update_dashboard(status: str, logs: list, step: int = 0, loss: float = 0.0, from pathlib import Path # Stub mode - simulate training without actual GPU - if getattr(args, 'stub', False): + if getattr(args, "stub", False): from openadapt_ml.training.stub_provider import StubTrainingProvider from openadapt_ml.training.trainer import ( - TrainingState, TrainingConfig, generate_training_dashboard + TrainingState, + TrainingConfig, + generate_training_dashboard, ) print("\n[Stub Mode] Simulating training without GPU...") @@ -1569,7 +1883,7 @@ def update_dashboard(status): config = TrainingConfig( num_train_epochs=status.get("total_epochs", 5), - learning_rate=state.learning_rate + learning_rate=state.learning_rate, ) dashboard_path = output_dir / "dashboard.html" @@ -1593,7 +1907,9 @@ def update_dashboard(status): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1616,9 +1932,13 @@ def update_dashboard(status): # Use job-scoped directory structure from openadapt_ml.training.trainer import ( - TrainingState, TrainingConfig, generate_training_dashboard, - setup_job_directory, get_current_job_directory + TrainingState, + TrainingConfig, + generate_training_dashboard, + setup_job_directory, + get_current_job_directory, ) + base_dir = Path("training_output") base_dir.mkdir(exist_ok=True) @@ -1649,7 +1969,11 @@ def update_dashboard(status): state.instance_ip = instance.ip or "" state.instance_type = instance.instance_type state.setup_status = "booting" - state.setup_logs = ["Starting Lambda Cloud instance...", f"Instance ID: {instance.id[:8]}...", f"Instance type: {instance.instance_type}"] + state.setup_logs = [ + "Starting Lambda Cloud instance...", + f"Instance ID: {instance.id[:8]}...", + f"Instance type: {instance.instance_type}", + ] config = TrainingConfig(num_train_epochs=5, learning_rate=5e-5) dashboard_path.write_text(generate_training_dashboard(state, config)) @@ -1660,12 +1984,14 @@ def update_dashboard(status): last_step = 0 last_epoch = -1 - auto_stop_loss = getattr(args, 'auto_stop_loss', 0.5) - download_checkpoints = getattr(args, 'download_checkpoints', True) + auto_stop_loss = getattr(args, "auto_stop_loss", 0.5) + download_checkpoints = getattr(args, "download_checkpoints", True) step_stall_count = 0 # Track how many times step hasn't increased print(f" Auto-stop loss threshold: {auto_stop_loss}") - print(f" Checkpoint download: {'enabled' if download_checkpoints else 'disabled'}") + print( + f" Checkpoint download: {'enabled' if download_checkpoints else 'disabled'}" + ) try: while True: @@ -1679,10 +2005,11 @@ def update_dashboard(status): # Update status with termination info before terminating termination_status = { "termination_status": "user_stop", - "termination_message": "Training stopped by user via dashboard" + "termination_message": "Training stopped by user via dashboard", } current_log = log_path.read_text() if log_path.exists() else "{}" import json as json_module + current_data = json_module.loads(current_log) current_data.update(termination_status) log_path.write_text(json_module.dumps(current_data, indent=2)) @@ -1706,8 +2033,14 @@ def update_dashboard(status): remote_job_id = status.get("job_id") # Detect job_id change - clear old data if new job started - if remote_job_id and current_job_id and remote_job_id != current_job_id: - print(f"\n New job detected: {remote_job_id} (was: {current_job_id})") + if ( + remote_job_id + and current_job_id + and remote_job_id != current_job_id + ): + print( + f"\n New job detected: {remote_job_id} (was: {current_job_id})" + ) print(" Clearing old job data...") last_step = 0 # Reset step tracking current_job_id = remote_job_id @@ -1722,25 +2055,37 @@ def update_dashboard(status): status["instance_type"] = instance.instance_type # Add cloud provider info status["cloud_provider"] = "lambda" - status["cloud_dashboard_url"] = "https://cloud.lambda.ai/instances" + status["cloud_dashboard_url"] = ( + "https://cloud.lambda.ai/instances" + ) status["cloud_instance_id"] = instance.id status["setup_status"] = status.get("setup_status", "training") # Setup screenshots symlink if local capture path provided - local_capture = args.capture if hasattr(args, 'capture') and args.capture else None + local_capture = ( + args.capture + if hasattr(args, "capture") and args.capture + else None + ) if local_capture: setup_capture_screenshots_symlink(output_dir, local_capture) # Rewrite evaluation paths from Lambda to relative if "evaluations" in status: - status["evaluations"] = rewrite_evaluation_paths(status["evaluations"]) + status["evaluations"] = rewrite_evaluation_paths( + status["evaluations"] + ) log_path.write_text(json.dumps(status, indent=2)) if step > last_step: - print(f" Epoch {epoch+1} | Step {step} | Loss: {loss:.4f} | Elapsed: {elapsed:.0f}s") + print( + f" Epoch {epoch + 1} | Step {step} | Loss: {loss:.4f} | Elapsed: {elapsed:.0f}s" + ) last_step = step - step_stall_count = 0 # Reset stall counter when step increases + step_stall_count = ( + 0 # Reset stall counter when step increases + ) if not current_job_id: current_job_id = remote_job_id @@ -1759,39 +2104,59 @@ def update_dashboard(status): state.start_time = time_module.time() - elapsed # Cloud provider info state.cloud_provider = "lambda" - state.cloud_dashboard_url = "https://cloud.lambda.ai/instances" + state.cloud_dashboard_url = ( + "https://cloud.lambda.ai/instances" + ) state.cloud_instance_id = instance.id state.setup_status = status.get("setup_status", "training") state.setup_logs = status.get("setup_logs", []) - state.termination_status = status.get("termination_status", "") - state.termination_message = status.get("termination_message", "") + state.termination_status = status.get( + "termination_status", "" + ) + state.termination_message = status.get( + "termination_message", "" + ) config = TrainingConfig( num_train_epochs=status.get("total_epochs", 5), - learning_rate=status.get("learning_rate", 5e-5) + learning_rate=status.get("learning_rate", 5e-5), ) - dashboard_path.write_text(generate_training_dashboard(state, config)) + dashboard_path.write_text( + generate_training_dashboard(state, config) + ) # Download checkpoints on epoch change if download_checkpoints and epoch > last_epoch: - print(f" Epoch {epoch+1} completed - downloading checkpoints...") - if download_checkpoints_from_instance(instance.ip, output_dir): - print(f" Checkpoints saved to {output_dir}/checkpoints/") + print( + f" Epoch {epoch + 1} completed - downloading checkpoints..." + ) + if download_checkpoints_from_instance( + instance.ip, output_dir + ): + print( + f" Checkpoints saved to {output_dir}/checkpoints/" + ) else: print(" Warning: checkpoint download failed") last_epoch = epoch # Auto-terminate when loss is low enough if loss < auto_stop_loss and loss > 0: - print(f"\n Loss {loss:.4f} < threshold {auto_stop_loss}") + print( + f"\n Loss {loss:.4f} < threshold {auto_stop_loss}" + ) print(" Downloading final checkpoints...") if download_checkpoints: - download_checkpoints_from_instance(instance.ip, output_dir) + download_checkpoints_from_instance( + instance.ip, output_dir + ) # Update status with termination info status["termination_status"] = "auto_low_loss" - status["termination_message"] = f"Training auto-stopped: loss {loss:.4f} < threshold {auto_stop_loss}" + status["termination_message"] = ( + f"Training auto-stopped: loss {loss:.4f} < threshold {auto_stop_loss}" + ) log_path.write_text(json.dumps(status, indent=2)) print(f" Auto-terminating instance {instance.id}...") @@ -1805,14 +2170,20 @@ def update_dashboard(status): # If on last epoch and step hasn't increased for 3 polls, training is complete if epoch >= total_epochs - 1 and step_stall_count >= 3: - print(f"\n Training complete (epoch {epoch+1}/{total_epochs}, step stopped increasing)") + print( + f"\n Training complete (epoch {epoch + 1}/{total_epochs}, step stopped increasing)" + ) print(" Downloading final checkpoints...") if download_checkpoints: - download_checkpoints_from_instance(instance.ip, output_dir) + download_checkpoints_from_instance( + instance.ip, output_dir + ) # Update status with termination info status["termination_status"] = "auto_complete" - status["termination_message"] = f"Training completed successfully ({epoch+1}/{total_epochs} epochs)" + status["termination_message"] = ( + f"Training completed successfully ({epoch + 1}/{total_epochs} epochs)" + ) log_path.write_text(json.dumps(status, indent=2)) print(f" Terminating instance {instance.id}...") @@ -1844,7 +2215,9 @@ def update_dashboard(status): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1852,9 +2225,13 @@ def update_dashboard(status): instance = instances[0] print(f"Files on {instance.ip} at {args.path}:") - result = client.ssh_run(instance, f"find {args.path} -type f -name '*.pt' -o -name '*.json' -o -name '*.bin' 2>/dev/null | head -20", timeout=30) + result = client.ssh_run( + instance, + f"find {args.path} -type f -name '*.pt' -o -name '*.json' -o -name '*.bin' 2>/dev/null | head -20", + timeout=30, + ) if result.stdout: - for line in result.stdout.strip().split('\n'): + for line in result.stdout.strip().split("\n"): print(f" {line}") else: print(" (no checkpoint files found)") @@ -1867,18 +2244,16 @@ def update_dashboard(status): if args.local: print("\nKilling local Lambda-related processes...") subprocess.run( - ["pkill", "-f", "ssh.*ubuntu@.*openadapt"], - capture_output=True - ) - subprocess.run( - ["pkill", "-f", "lambda_labs"], - capture_output=True + ["pkill", "-f", "ssh.*ubuntu@.*openadapt"], capture_output=True ) + subprocess.run(["pkill", "-f", "lambda_labs"], capture_output=True) print("Done.") return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1891,11 +2266,11 @@ def update_dashboard(status): result = client.ssh_run( instance, "ps aux | grep python | grep -v grep | grep -v jupyter", - timeout=30 + timeout=30, ) if result.stdout.strip(): print("Found Python processes:") - for line in result.stdout.strip().split('\n'): + for line in result.stdout.strip().split("\n"): print(f" {line[:100]}...") else: print("No training/inference Python processes found.") @@ -1903,7 +2278,9 @@ def update_dashboard(status): if args.all: print("\nKilling ALL Python processes (except jupyter)...") - cmd = "pkill -f 'python.*train\\|python.*compare\\|python.*openadapt' || true" + cmd = ( + "pkill -f 'python.*train\\|python.*compare\\|python.*openadapt' || true" + ) else: print("\nKilling training and inference processes...") cmd = "pkill -f 'python.*train' ; pkill -f 'python.*compare' || true" @@ -1914,20 +2291,16 @@ def update_dashboard(status): if args.local: print("\nKilling local Lambda-related processes...") subprocess.run( - ["pkill", "-f", "ssh.*ubuntu@.*openadapt"], - capture_output=True - ) - subprocess.run( - ["pkill", "-f", "lambda_labs.*train"], - capture_output=True + ["pkill", "-f", "ssh.*ubuntu@.*openadapt"], capture_output=True ) + subprocess.run(["pkill", "-f", "lambda_labs.*train"], capture_output=True) print("Local processes killed.") print("\nDone. Current status:") result = client.ssh_run( instance, "ps aux | grep python | grep -v grep | grep -v jupyter | wc -l", - timeout=30 + timeout=30, ) count = result.stdout.strip() print(f" {count} Python processes remaining on instance") @@ -1940,7 +2313,9 @@ def update_dashboard(status): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -1953,7 +2328,7 @@ def update_dashboard(status): result = client.ssh_run( instance, "cat ~/openadapt-ml/training_output/training_log.json 2>/dev/null", - timeout=30 + timeout=30, ) if not result.stdout.strip(): @@ -1977,19 +2352,17 @@ def update_dashboard(status): min_loss = min(loss_entry["loss"] for loss_entry in losses) current_loss = losses[-1]["loss"] - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("TRAINING STATUS") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Steps: {total_steps}") - print(f"Epochs: {max(epochs)+1}/{total_epochs}") + print(f"Epochs: {max(epochs) + 1}/{total_epochs}") print(f"Current loss: {current_loss:.4f}") print(f"Min loss: {min_loss:.4f}") # Check if training is running proc_result = client.ssh_run( - instance, - "ps aux | grep 'python.*train' | grep -v grep | wc -l", - timeout=30 + instance, "ps aux | grep 'python.*train' | grep -v grep | wc -l", timeout=30 ) is_running = int(proc_result.stdout.strip()) > 0 @@ -2004,45 +2377,57 @@ def update_dashboard(status): print("\nNot enough data for early stopping analysis.") else: recent_losses = [loss_entry["loss"] for loss_entry in losses[-window:]] - older_losses = [loss_entry["loss"] for loss_entry in losses[-window*2:-window]] if len(losses) >= window*2 else [loss_entry["loss"] for loss_entry in losses[:window]] + older_losses = ( + [loss_entry["loss"] for loss_entry in losses[-window * 2 : -window]] + if len(losses) >= window * 2 + else [loss_entry["loss"] for loss_entry in losses[:window]] + ) recent_avg = sum(recent_losses) / len(recent_losses) - older_avg = sum(older_losses) / len(older_losses) if older_losses else recent_avg + older_avg = ( + sum(older_losses) / len(older_losses) if older_losses else recent_avg + ) improvement = (older_avg - recent_avg) / older_avg if older_avg > 0 else 0 loss_variance = max(recent_losses) - min(recent_losses) - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print(f"EARLY STOPPING ANALYSIS (window={window})") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Recent avg loss: {recent_avg:.4f}") print(f"Prior avg loss: {older_avg:.4f}") - print(f"Improvement: {improvement*100:.2f}%") + print(f"Improvement: {improvement * 100:.2f}%") print(f"Loss variance: {loss_variance:.4f}") should_stop = improvement < args.threshold and loss_variance < 0.1 if should_stop: print("\n⚠️ EARLY STOPPING RECOMMENDED") - print(f" Loss has plateaued (improvement < {args.threshold*100}%)") + print(f" Loss has plateaued (improvement < {args.threshold * 100}%)") if not is_running: print(" (Training already stopped)") else: - print("\n To stop: uv run python -m openadapt_ml.cloud.lambda_labs kill") + print( + "\n To stop: uv run python -m openadapt_ml.cloud.lambda_labs kill" + ) else: print("\n✓ Training still improving, continue.") # Time estimate if is_running and len(losses) >= 2: - avg_time_per_step = losses[-1].get("time", 0) / len(losses) if losses[-1].get("time") else 50 + avg_time_per_step = ( + losses[-1].get("time", 0) / len(losses) + if losses[-1].get("time") + else 50 + ) steps_per_epoch = len(losses) / (max(epochs) + 1) remaining_epochs = total_epochs - max(epochs) - 1 remaining_steps = remaining_epochs * steps_per_epoch eta_seconds = remaining_steps * avg_time_per_step eta_mins = eta_seconds / 60 - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("TIME ESTIMATE") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Remaining epochs: {remaining_epochs}") print(f"Est. remaining steps: {remaining_steps:.0f}") print(f"ETA: {eta_mins:.1f} minutes") @@ -2055,7 +2440,9 @@ def update_dashboard(status): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -2066,24 +2453,26 @@ def update_dashboard(status): if args.checkpoint: checkpoint_path = args.checkpoint elif args.epoch is not None: - checkpoint_path = f"/home/ubuntu/openadapt-ml/checkpoints/epoch_{args.epoch}" + checkpoint_path = ( + f"/home/ubuntu/openadapt-ml/checkpoints/epoch_{args.epoch}" + ) else: # Use latest (main checkpoint) - checkpoint_path = "/home/ubuntu/openadapt-ml/checkpoints/qwen3vl2b_capture_lora" + checkpoint_path = ( + "/home/ubuntu/openadapt-ml/checkpoints/qwen3vl2b_capture_lora" + ) # Check if checkpoint exists result = client.ssh_run( instance, f"ls {checkpoint_path}/adapter_config.json 2>/dev/null && echo 'exists'", - timeout=30 + timeout=30, ) if "exists" not in result.stdout: print(f"Checkpoint not found at {checkpoint_path}") # List available checkpoints result = client.ssh_run( - instance, - "ls -la ~/openadapt-ml/checkpoints/", - timeout=30 + instance, "ls -la ~/openadapt-ml/checkpoints/", timeout=30 ) print(f"Available checkpoints:\n{result.stdout}") return @@ -2108,9 +2497,7 @@ def update_dashboard(status): # Check if file was created result = client.ssh_run( - instance, - f"ls -la ~/openadapt-ml/training_output/{output_name}", - timeout=30 + instance, f"ls -la ~/openadapt-ml/training_output/{output_name}", timeout=30 ) if result.returncode != 0: print("Comparison file not created.") @@ -2123,11 +2510,15 @@ def update_dashboard(status): local_output.parent.mkdir(parents=True, exist_ok=True) print(f"Syncing to {local_output}...") - subprocess.run([ - "rsync", "-avz", - f"ubuntu@{instance.ip}:~/openadapt-ml/training_output/{output_name}", - str(local_output) - ], capture_output=True) + subprocess.run( + [ + "rsync", + "-avz", + f"ubuntu@{instance.ip}:~/openadapt-ml/training_output/{output_name}", + str(local_output), + ], + capture_output=True, + ) print(f"Done! Comparison saved to: {local_output}") @@ -2142,7 +2533,9 @@ def update_dashboard(status): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -2159,7 +2552,9 @@ def update_dashboard(status): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -2175,10 +2570,17 @@ def update_dashboard(status): checkpoint_path = "checkpoints_lambda/qwen3vl2b_capture_lora" import subprocess as sp + cmd = [ - "uv", "run", "python", "-m", "openadapt_ml.scripts.compare", - "--capture", args.capture, - "--checkpoint", checkpoint_path, + "uv", + "run", + "python", + "-m", + "openadapt_ml.scripts.compare", + "--capture", + args.capture, + "--checkpoint", + checkpoint_path, ] if args.goal: cmd.extend(["--goal", args.goal]) @@ -2200,7 +2602,9 @@ def update_dashboard(status): import time as time_module from pathlib import Path - output_dir = Path(args.output) if hasattr(args, 'output') else Path("training_output") + output_dir = ( + Path(args.output) if hasattr(args, "output") else Path("training_output") + ) port = args.port if not output_dir.exists(): @@ -2213,13 +2617,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(output_dir), **kwargs) def do_POST(self): - if self.path == '/api/stop': + if self.path == "/api/stop": # Create stop signal file stop_file = output_dir / "STOP_TRAINING" stop_file.touch() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(b'{"status": "stop signal created"}') print(f" Stop signal created: {stop_file}") @@ -2229,15 +2633,14 @@ def do_POST(self): def do_OPTIONS(self): # Handle CORS preflight self.send_response(200) - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS') - self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def log_message(self, format, *args): pass # Suppress log messages - # Start web server with socketserver.TCPServer(("", port), Handler) as httpd: url = f"http://localhost:{port}/dashboard.html" @@ -2256,8 +2659,10 @@ def log_message(self, format, *args): # Sync training output from Lambda and regenerate navigation for file:// protocol from pathlib import Path from openadapt_ml.training.trainer import ( - TrainingState, TrainingConfig, generate_training_dashboard, - regenerate_all_dashboards + TrainingState, + TrainingConfig, + generate_training_dashboard, + regenerate_all_dashboards, ) instances = client.list_instances() @@ -2266,7 +2671,9 @@ def log_message(self, format, *args): return if args.instance_id: - instance = next((i for i in instances if i.id.startswith(args.instance_id)), None) + instance = next( + (i for i in instances if i.id.startswith(args.instance_id)), None + ) if not instance: print(f"Instance {args.instance_id} not found.") return @@ -2280,10 +2687,13 @@ def log_message(self, format, *args): # Sync all training output files rsync_cmd = [ - "rsync", "-avz", "--progress", - "-e", "ssh -o StrictHostKeyChecking=no", + "rsync", + "-avz", + "--progress", + "-e", + "ssh -o StrictHostKeyChecking=no", f"ubuntu@{instance.ip}:~/openadapt-ml/training_output/", - str(output_dir) + "/" + str(output_dir) + "/", ] result = subprocess.run(rsync_cmd, capture_output=False) @@ -2297,6 +2707,7 @@ def log_message(self, format, *args): if log_path.exists(): try: import time as time_module + status = json.loads(log_path.read_text()) # Update with instance info @@ -2330,7 +2741,7 @@ def log_message(self, format, *args): config = TrainingConfig( num_train_epochs=status.get("total_epochs", 5), - learning_rate=status.get("learning_rate", 5e-5) + learning_rate=status.get("learning_rate", 5e-5), ) dashboard_path.write_text(generate_training_dashboard(state, config)) @@ -2384,7 +2795,7 @@ def log_message(self, format, *args): # First try training log log_data = json.loads((output_dir / "training_log.json").read_text()) capture_path = log_data.get("capture_path", "") - capture_match = re.search(r'capture_(\d+)', capture_path) + capture_match = re.search(r"capture_(\d+)", capture_path) if capture_match: capture_id = capture_match.group(1) @@ -2395,25 +2806,35 @@ def log_message(self, format, *args): base_data = pred_data.get("base_data", []) if base_data: image_path = base_data[0].get("image_path", "") - capture_match = re.search(r'capture_(\d+)', image_path) + capture_match = re.search(r"capture_(\d+)", image_path) if capture_match: capture_id = capture_match.group(1) break if capture_id: # Search for local screenshots in openadapt-capture - openadapt_capture_dir = Path.home() / "oa" / "src" / "openadapt-capture" + openadapt_capture_dir = ( + Path.home() / "oa" / "src" / "openadapt-capture" + ) if openadapt_capture_dir.exists(): for capture_dir in openadapt_capture_dir.iterdir(): if capture_dir.is_dir(): screenshots_dir = capture_dir / "screenshots" if screenshots_dir.exists(): # Check if this capture has our screenshots - sample_file = list(screenshots_dir.glob(f"capture_{capture_id}_step_*.png")) + sample_file = list( + screenshots_dir.glob( + f"capture_{capture_id}_step_*.png" + ) + ) if sample_file: - print(f"Found local screenshots in {screenshots_dir}") + print( + f"Found local screenshots in {screenshots_dir}" + ) screenshots_link.symlink_to(screenshots_dir) - print(f" Linked: {screenshots_link} -> {screenshots_dir}") + print( + f" Linked: {screenshots_link} -> {screenshots_dir}" + ) break except Exception: pass # Silently continue if auto-link fails diff --git a/openadapt_ml/cloud/local.py b/openadapt_ml/cloud/local.py index 5ba9a44..c881ff6 100644 --- a/openadapt_ml/cloud/local.py +++ b/openadapt_ml/cloud/local.py @@ -108,7 +108,10 @@ def _is_mock_benchmark(benchmark_dir: Path) -> bool: # Check for test runs (but allow waa-mock evaluations with real API models) # Only filter out purely synthetic test data directories - if any(term in benchmark_dir.name.lower() for term in ["test_run", "test_cli", "quick_demo"]): + if any( + term in benchmark_dir.name.lower() + for term in ["test_run", "test_cli", "quick_demo"] + ): return True return False @@ -192,6 +195,7 @@ def _regenerate_benchmark_viewer_if_available(output_dir: Path) -> bool: except Exception as e: print(f" Could not regenerate benchmark viewer: {e}") import traceback + traceback.print_exc() return False @@ -200,6 +204,7 @@ def detect_device() -> str: """Detect available compute device.""" try: import torch + if torch.cuda.is_available(): device_name = torch.cuda.get_device_name(0) return f"cuda ({device_name})" @@ -250,10 +255,13 @@ def get_training_status() -> dict[str, Any]: # Find checkpoints checkpoints_dir = Path("checkpoints") if checkpoints_dir.exists(): - status["checkpoints"] = sorted([ - d.name for d in checkpoints_dir.iterdir() - if d.is_dir() and (d / "adapter_config.json").exists() - ]) + status["checkpoints"] = sorted( + [ + d.name + for d in checkpoints_dir.iterdir() + if d.is_dir() and (d / "adapter_config.json").exists() + ] + ) return status @@ -263,9 +271,9 @@ def cmd_status(args: argparse.Namespace) -> int: status = get_training_status() current_dir = get_current_output_dir() - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("LOCAL TRAINING STATUS") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Device: {status['device']}") print(f"Status: {'RUNNING' if status['running'] else 'IDLE'}") if status.get("job_id"): @@ -286,7 +294,9 @@ def cmd_status(args: argparse.Namespace) -> int: for cp in status["checkpoints"][-5:]: # Show last 5 print(f" - {cp}") - print(f"\nDashboard: {'✓' if status['has_dashboard'] else '✗'} {current_dir}/dashboard.html") + print( + f"\nDashboard: {'✓' if status['has_dashboard'] else '✗'} {current_dir}/dashboard.html" + ) print(f"Viewer: {'✓' if status['has_viewer'] else '✗'} {current_dir}/viewer.html") print() @@ -319,9 +329,9 @@ def cmd_train(args: argparse.Namespace) -> int: print(f"Error: Config not found: {config_path}") return 1 - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("STARTING LOCAL TRAINING") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Capture: {capture_path}") print(f"Goal: {goal}") print(f"Config: {config}") @@ -330,10 +340,15 @@ def cmd_train(args: argparse.Namespace) -> int: # Build command cmd = [ - sys.executable, "-m", "openadapt_ml.scripts.train", - "--config", str(config_path), - "--capture", str(capture_path), - "--goal", goal, + sys.executable, + "-m", + "openadapt_ml.scripts.train", + "--config", + str(config_path), + "--capture", + str(capture_path), + "--goal", + goal, ] if args.open: @@ -352,14 +367,16 @@ def cmd_check(args: argparse.Namespace) -> int: """Check training health and early stopping analysis.""" status = get_training_status() - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("TRAINING HEALTH CHECK") - print(f"{'='*50}") + print(f"{'=' * 50}") raw_losses = status.get("losses", []) if not raw_losses: print("No training data found.") - print("Run training first with: uv run python -m openadapt_ml.cloud.local train --capture ") + print( + "Run training first with: uv run python -m openadapt_ml.cloud.local train --capture " + ) return 1 # Extract loss values (handle both dict and float formats) @@ -391,7 +408,9 @@ def cmd_check(args: argparse.Namespace) -> int: if len(losses) >= 10: recent = losses[-10:] recent_avg = sum(recent) / len(recent) - recent_std = (sum((x - recent_avg) ** 2 for x in recent) / len(recent)) ** 0.5 + recent_std = ( + sum((x - recent_avg) ** 2 for x in recent) / len(recent) + ) ** 0.5 print("\nRecent stability (last 10 steps):") print(f" Avg loss: {recent_avg:.4f}") @@ -419,7 +438,7 @@ def cmd_serve(args: argparse.Namespace) -> int: port = args.port # Determine what to serve: benchmark directory or training output - if hasattr(args, 'benchmark') and args.benchmark: + if hasattr(args, "benchmark") and args.benchmark: serve_dir = Path(args.benchmark).expanduser().resolve() if not serve_dir.exists(): print(f"Error: Benchmark directory not found: {serve_dir}") @@ -429,7 +448,10 @@ def cmd_serve(args: argparse.Namespace) -> int: if not args.no_regenerate: print("Regenerating benchmark viewer...") try: - from openadapt_ml.training.benchmark_viewer import generate_benchmark_viewer + from openadapt_ml.training.benchmark_viewer import ( + generate_benchmark_viewer, + ) + generate_benchmark_viewer(serve_dir) except Exception as e: print(f"Warning: Could not regenerate benchmark viewer: {e}") @@ -458,7 +480,7 @@ def cmd_serve(args: argparse.Namespace) -> int: start_page = "dashboard.html" # Override start page if specified - if hasattr(args, 'start_page') and args.start_page: + if hasattr(args, "start_page") and args.start_page: start_page = args.start_page # Serve from the specified directory @@ -475,33 +497,41 @@ def log_message(self, format, *log_args): super().log_message(format, *log_args) def do_POST(self): - if self.path == '/api/stop': + if self.path == "/api/stop": # Create stop signal file stop_file = serve_dir / "STOP_TRAINING" stop_file.touch() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(b'{"status": "stop_signal_created"}') print(f"\n⏹ Stop signal created: {stop_file}") - elif self.path == '/api/run-benchmark': + elif self.path == "/api/run-benchmark": # Parse request body for provider - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length).decode('utf-8') if content_length else '{}' + content_length = int(self.headers.get("Content-Length", 0)) + body = ( + self.rfile.read(content_length).decode("utf-8") + if content_length + else "{}" + ) try: params = json.loads(body) except json.JSONDecodeError: params = {} - provider = params.get('provider', 'anthropic') - tasks = params.get('tasks', 5) + provider = params.get("provider", "anthropic") + tasks = params.get("tasks", 5) self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() - self.wfile.write(json.dumps({"status": "started", "provider": provider, "tasks": tasks}).encode()) + self.wfile.write( + json.dumps( + {"status": "started", "provider": provider, "tasks": tasks} + ).encode() + ) # Run benchmark in background thread with progress logging def run_benchmark(): @@ -515,25 +545,45 @@ def run_benchmark(): # Create progress log file (in cwd which is serve_dir) progress_file = Path("benchmark_progress.json") - print(f"\n🚀 Starting {provider} benchmark evaluation ({tasks} tasks)...") + print( + f"\n🚀 Starting {provider} benchmark evaluation ({tasks} tasks)..." + ) # Write initial progress - progress_file.write_text(json.dumps({ - "status": "running", - "provider": provider, - "tasks_total": tasks, - "tasks_complete": 0, - "message": f"Starting {provider} evaluation..." - })) + progress_file.write_text( + json.dumps( + { + "status": "running", + "provider": provider, + "tasks_total": tasks, + "tasks_complete": 0, + "message": f"Starting {provider} evaluation...", + } + ) + ) # Copy environment with loaded vars env = os.environ.copy() result = subprocess.run( - ["uv", "run", "python", "-m", "openadapt_ml.benchmarks.cli", "run-api", - "--provider", provider, "--tasks", str(tasks), - "--model-id", f"{provider}-api"], - capture_output=True, text=True, cwd=str(project_root), env=env + [ + "uv", + "run", + "python", + "-m", + "openadapt_ml.benchmarks.cli", + "run-api", + "--provider", + provider, + "--tasks", + str(tasks), + "--model-id", + f"{provider}-api", + ], + capture_output=True, + text=True, + cwd=str(project_root), + env=env, ) print(f"\n📋 Benchmark output:\n{result.stdout}") @@ -542,76 +592,94 @@ def run_benchmark(): if result.returncode == 0: print("✅ Benchmark complete. Regenerating viewer...") - progress_file.write_text(json.dumps({ - "status": "complete", - "provider": provider, - "message": "Evaluation complete! Refreshing results..." - })) + progress_file.write_text( + json.dumps( + { + "status": "complete", + "provider": provider, + "message": "Evaluation complete! Refreshing results...", + } + ) + ) # Regenerate benchmark viewer _regenerate_benchmark_viewer_if_available(serve_dir) else: print(f"❌ Benchmark failed: {result.stderr}") - progress_file.write_text(json.dumps({ - "status": "error", - "provider": provider, - "message": f"Evaluation failed: {result.stderr[:200]}" - })) + progress_file.write_text( + json.dumps( + { + "status": "error", + "provider": provider, + "message": f"Evaluation failed: {result.stderr[:200]}", + } + ) + ) threading.Thread(target=run_benchmark, daemon=True).start() - elif self.path == '/api/vms/register': + elif self.path == "/api/vms/register": # Register a new VM - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length).decode('utf-8') if content_length else '{}' + content_length = int(self.headers.get("Content-Length", 0)) + body = ( + self.rfile.read(content_length).decode("utf-8") + if content_length + else "{}" + ) try: vm_data = json.loads(body) result = self._register_vm(vm_data) self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(result).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) - elif self.path == '/api/benchmark/start': + elif self.path == "/api/benchmark/start": # Start a benchmark run with configurable parameters - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length).decode('utf-8') if content_length else '{}' + content_length = int(self.headers.get("Content-Length", 0)) + body = ( + self.rfile.read(content_length).decode("utf-8") + if content_length + else "{}" + ) try: params = json.loads(body) result = self._start_benchmark_run(params) self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(result).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) else: self.send_error(404, "Not found") def do_GET(self): - if self.path.startswith('/api/benchmark-progress'): + if self.path.startswith("/api/benchmark-progress"): # Return benchmark progress - progress_file = Path("benchmark_progress.json") # Relative to serve_dir (cwd) + progress_file = Path( + "benchmark_progress.json" + ) # Relative to serve_dir (cwd) if progress_file.exists(): progress = progress_file.read_text() else: progress = json.dumps({"status": "idle"}) self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(progress.encode()) - elif self.path.startswith('/api/benchmark-live'): + elif self.path.startswith("/api/benchmark-live"): # Return live evaluation state live_file = Path("benchmark_live.json") # Relative to serve_dir (cwd) if live_file.exists(): @@ -620,32 +688,33 @@ def do_GET(self): live_state = json.dumps({"status": "idle"}) self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(live_state.encode()) - elif self.path.startswith('/api/tasks'): + elif self.path.startswith("/api/tasks"): # Return background task status (VM, Docker, benchmarks) try: tasks = self._fetch_background_tasks() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(tasks).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) - elif self.path.startswith('/api/azure-jobs'): + elif self.path.startswith("/api/azure-jobs"): # Return LIVE Azure job status from Azure ML # Supports ?force=true parameter for manual refresh (always fetches live) try: from urllib.parse import urlparse, parse_qs + query = parse_qs(urlparse(self.path).query) - force_refresh = query.get('force', ['false'])[0].lower() == 'true' + force_refresh = query.get("force", ["false"])[0].lower() == "true" # Always fetch live data (force just indicates manual refresh for logging) if force_refresh: @@ -653,22 +722,23 @@ def do_GET(self): jobs = self._fetch_live_azure_jobs() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(jobs).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) - elif self.path.startswith('/api/benchmark-sse'): + elif self.path.startswith("/api/benchmark-sse"): # Server-Sent Events endpoint for real-time benchmark updates try: from urllib.parse import urlparse, parse_qs + query = parse_qs(urlparse(self.path).query) - interval = int(query.get('interval', [5])[0]) + interval = int(query.get("interval", [5])[0]) # Validate interval (min 1s, max 60s) interval = max(1, min(60, interval)) @@ -676,57 +746,60 @@ def do_GET(self): self._stream_benchmark_updates(interval) except Exception as e: self.send_error(500, f"SSE error: {e}") - elif self.path.startswith('/api/vms'): + elif self.path.startswith("/api/vms"): # Return VM registry with live status try: vms = self._fetch_vm_registry() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(vms).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) - elif self.path.startswith('/api/azure-job-logs'): + elif self.path.startswith("/api/azure-job-logs"): # Return live logs for running Azure job try: # Parse job_id from query string from urllib.parse import urlparse, parse_qs + query = parse_qs(urlparse(self.path).query) - job_id = query.get('job_id', [None])[0] + job_id = query.get("job_id", [None])[0] logs = self._fetch_azure_job_logs(job_id) self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(logs).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) - elif self.path.startswith('/api/probe-vm'): + elif self.path.startswith("/api/probe-vm"): # Probe the VM to check if WAA server is responding try: result = self._probe_vm() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(result).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() - self.wfile.write(json.dumps({"error": str(e), "responding": False}).encode()) - elif self.path.startswith('/api/tunnels'): + self.wfile.write( + json.dumps({"error": str(e), "responding": False}).encode() + ) + elif self.path.startswith("/api/tunnels"): # Return SSH tunnel status try: tunnel_mgr = get_tunnel_manager() @@ -742,44 +815,46 @@ def do_GET(self): for name, s in status.items() } self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(result).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) - elif self.path.startswith('/api/current-run'): + elif self.path.startswith("/api/current-run"): # Return currently running benchmark info try: result = self._get_current_run() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(result).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() - self.wfile.write(json.dumps({"error": str(e), "running": False}).encode()) - elif self.path.startswith('/api/background-tasks'): + self.wfile.write( + json.dumps({"error": str(e), "running": False}).encode() + ) + elif self.path.startswith("/api/background-tasks"): # Alias for /api/tasks - background task status try: tasks = self._fetch_background_tasks() self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(tasks).encode()) except Exception as e: self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps({"error": str(e)}).encode()) else: @@ -789,13 +864,25 @@ def do_GET(self): def _fetch_live_azure_jobs(self): """Fetch live job status from Azure ML.""" import subprocess + result = subprocess.run( - ["az", "ml", "job", "list", - "--resource-group", "openadapt-agents", - "--workspace-name", "openadapt-ml", - "--query", "[].{name:name,display_name:display_name,status:status,creation_context:creation_context.created_at}", - "-o", "json"], - capture_output=True, text=True, timeout=30 + [ + "az", + "ml", + "job", + "list", + "--resource-group", + "openadapt-agents", + "--workspace-name", + "openadapt-ml", + "--query", + "[].{name:name,display_name:display_name,status:status,creation_context:creation_context.created_at}", + "-o", + "json", + ], + capture_output=True, + text=True, + timeout=30, ) if result.returncode != 0: raise Exception(f"Azure CLI error: {result.stderr}") @@ -807,14 +894,16 @@ def _fetch_live_azure_jobs(self): formatted = [] for job in jobs[:10]: # Limit to 10 most recent - formatted.append({ - "job_id": job.get("name", "unknown"), - "display_name": job.get("display_name", ""), - "status": job.get("status", "unknown").lower(), - "started_at": job.get("creation_context", ""), - "azure_dashboard_url": f"https://ml.azure.com/experiments/id/{experiment_id}/runs/{job.get('name', '')}?wsid={wsid}", - "is_live": True # Flag to indicate this is live data - }) + formatted.append( + { + "job_id": job.get("name", "unknown"), + "display_name": job.get("display_name", ""), + "status": job.get("status", "unknown").lower(), + "started_at": job.get("creation_context", ""), + "azure_dashboard_url": f"https://ml.azure.com/experiments/id/{experiment_id}/runs/{job.get('name', '')}?wsid={wsid}", + "is_live": True, # Flag to indicate this is live data + } + ) return formatted def _fetch_azure_job_logs(self, job_id: str | None): @@ -824,34 +913,63 @@ def _fetch_azure_job_logs(self, job_id: str | None): if not job_id: # Get the most recent running job jobs = self._fetch_live_azure_jobs() - running = [j for j in jobs if j['status'] == 'running'] + running = [j for j in jobs if j["status"] == "running"] if running: - job_id = running[0]['job_id'] + job_id = running[0]["job_id"] else: - return {"logs": "No running jobs found", "job_id": None, "status": "idle"} + return { + "logs": "No running jobs found", + "job_id": None, + "status": "idle", + } # Try to stream logs for running job using az ml job stream try: result = subprocess.run( - ["az", "ml", "job", "stream", - "--name", job_id, - "--resource-group", "openadapt-agents", - "--workspace-name", "openadapt-ml"], - capture_output=True, text=True, timeout=3 # Short timeout + [ + "az", + "ml", + "job", + "stream", + "--name", + job_id, + "--resource-group", + "openadapt-agents", + "--workspace-name", + "openadapt-ml", + ], + capture_output=True, + text=True, + timeout=3, # Short timeout ) if result.returncode == 0 and result.stdout.strip(): - return {"logs": result.stdout[-5000:], "job_id": job_id, "status": "streaming"} + return { + "logs": result.stdout[-5000:], + "job_id": job_id, + "status": "streaming", + } except subprocess.TimeoutExpired: pass # Fall through to job show # Get job details instead result = subprocess.run( - ["az", "ml", "job", "show", - "--name", job_id, - "--resource-group", "openadapt-agents", - "--workspace-name", "openadapt-ml", - "-o", "json"], - capture_output=True, text=True, timeout=10 + [ + "az", + "ml", + "job", + "show", + "--name", + job_id, + "--resource-group", + "openadapt-agents", + "--workspace-name", + "openadapt-ml", + "-o", + "json", + ], + capture_output=True, + text=True, + timeout=10, ) if result.returncode == 0: @@ -859,13 +977,19 @@ def _fetch_azure_job_logs(self, job_id: str | None): return { "logs": f"Job {job_id} is {job_info.get('status', 'unknown')}\\n\\nCommand: {job_info.get('command', 'N/A')}", "job_id": job_id, - "status": job_info.get('status', 'unknown').lower(), - "command": job_info.get('command', '') + "status": job_info.get("status", "unknown").lower(), + "command": job_info.get("command", ""), } - return {"logs": f"Could not fetch logs: {result.stderr}", "job_id": job_id, "status": "error"} + return { + "logs": f"Could not fetch logs: {result.stderr}", + "job_id": job_id, + "status": "error", + } - def _get_vm_detailed_metadata(self, vm_ip: str, container_name: str, logs: str, phase: str) -> dict: + def _get_vm_detailed_metadata( + self, vm_ip: str, container_name: str, logs: str, phase: str + ) -> dict: """Get detailed VM metadata for the VM Details panel. Returns: @@ -879,17 +1003,26 @@ def _get_vm_detailed_metadata(self, vm_ip: str, container_name: str, logs: str, "setup_script_phase": None, "probe_response": None, "qmp_connected": False, - "dependencies": [] + "dependencies": [], } # 1. Get disk usage from docker stats try: disk_result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - f"docker exec {container_name} df -h /storage 2>/dev/null | tail -1"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + f"docker exec {container_name} df -h /storage 2>/dev/null | tail -1", + ], + capture_output=True, + text=True, + timeout=10, ) if disk_result.returncode == 0 and disk_result.stdout.strip(): # Parse: "Filesystem Size Used Avail Use% Mounted on" @@ -898,27 +1031,40 @@ def _get_vm_detailed_metadata(self, vm_ip: str, container_name: str, logs: str, if len(parts) >= 3: used_str = parts[2] # e.g., "9.2G" total_str = parts[1] # e.g., "30G" + # Convert to GB (handle M/G suffixes) def to_gb(s): - if s.endswith('G'): + if s.endswith("G"): return float(s[:-1]) - elif s.endswith('M'): + elif s.endswith("M"): return float(s[:-1]) / 1024 - elif s.endswith('K'): + elif s.endswith("K"): return float(s[:-1]) / (1024 * 1024) return 0 - metadata["disk_usage_gb"] = f"{to_gb(used_str):.1f} GB / {to_gb(total_str):.0f} GB used" + + metadata["disk_usage_gb"] = ( + f"{to_gb(used_str):.1f} GB / {to_gb(total_str):.0f} GB used" + ) except Exception: pass # 2. Get memory usage from docker stats try: mem_result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - f"docker stats {container_name} --no-stream --format '{{{{.MemUsage}}}}'"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + f"docker stats {container_name} --no-stream --format '{{{{.MemUsage}}}}'", + ], + capture_output=True, + text=True, + timeout=10, ) if mem_result.returncode == 0 and mem_result.stdout.strip(): # Example: "1.5GiB / 4GiB" @@ -927,16 +1073,27 @@ def to_gb(s): pass # 3. Parse setup script phase from logs - metadata["setup_script_phase"] = self._parse_setup_phase_from_logs(logs, phase) + metadata["setup_script_phase"] = self._parse_setup_phase_from_logs( + logs, phase + ) # 4. Check /probe endpoint try: probe_result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "curl -s --connect-timeout 2 http://20.20.20.21:5000/probe 2>/dev/null"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "curl -s --connect-timeout 2 http://20.20.20.21:5000/probe 2>/dev/null", + ], + capture_output=True, + text=True, + timeout=10, ) if probe_result.returncode == 0 and probe_result.stdout.strip(): metadata["probe_response"] = probe_result.stdout.strip() @@ -948,11 +1105,20 @@ def to_gb(s): # 5. Check QMP connection (port 7200) try: qmp_result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "nc -z -w2 localhost 7200 2>&1"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "nc -z -w2 localhost 7200 2>&1", + ], + capture_output=True, + text=True, + timeout=10, ) metadata["qmp_connected"] = qmp_result.returncode == 0 except Exception: @@ -985,7 +1151,12 @@ def _parse_setup_phase_from_logs(self, logs: str, current_phase: str) -> str: return "Windows installation in progress" elif current_phase == "booting": return "Booting Windows" - elif current_phase in ["downloading", "extracting", "configuring", "building"]: + elif current_phase in [ + "downloading", + "extracting", + "configuring", + "building", + ]: return "Preparing Windows VM" else: return "Initializing..." @@ -1015,17 +1186,23 @@ def _parse_dependencies_from_logs(self, logs: str, phase: str) -> list[dict]: logs_lower = logs.lower() # Check for installation patterns - if "python" in logs_lower and ("installing python" in logs_lower or "python.exe" in logs_lower): + if "python" in logs_lower and ( + "installing python" in logs_lower or "python.exe" in logs_lower + ): dependencies[0]["status"] = "installing" elif "python" in logs_lower and "installed" in logs_lower: dependencies[0]["status"] = "complete" - if "chrome" in logs_lower and ("downloading" in logs_lower or "installing" in logs_lower): + if "chrome" in logs_lower and ( + "downloading" in logs_lower or "installing" in logs_lower + ): dependencies[1]["status"] = "installing" elif "chrome" in logs_lower and "installed" in logs_lower: dependencies[1]["status"] = "complete" - if "libreoffice" in logs_lower and ("downloading" in logs_lower or "installing" in logs_lower): + if "libreoffice" in logs_lower and ( + "downloading" in logs_lower or "installing" in logs_lower + ): dependencies[2]["status"] = "installing" elif "libreoffice" in logs_lower and "installed" in logs_lower: dependencies[2]["status"] = "complete" @@ -1059,31 +1236,43 @@ def _fetch_background_tasks(self): if env_vm_ip: # Use environment variable - VM IP was provided directly vm_ip = env_vm_ip - tasks.append({ - "task_id": "azure-vm-waa", - "task_type": "vm_provision", - "status": "completed", - "phase": "ready", # Match status to prevent "Starting" + "completed" conflict - "title": "Azure VM Host", - "description": f"Linux host running at {vm_ip}", - "progress_percent": 100.0, - "elapsed_seconds": 0, - "metadata": { - "vm_name": "waa-eval-vm", - "ip_address": vm_ip, - "internal_ip": env_internal_ip + tasks.append( + { + "task_id": "azure-vm-waa", + "task_type": "vm_provision", + "status": "completed", + "phase": "ready", # Match status to prevent "Starting" + "completed" conflict + "title": "Azure VM Host", + "description": f"Linux host running at {vm_ip}", + "progress_percent": 100.0, + "elapsed_seconds": 0, + "metadata": { + "vm_name": "waa-eval-vm", + "ip_address": vm_ip, + "internal_ip": env_internal_ip, + }, } - }) + ) else: # Query Azure CLI for VM status try: result = subprocess.run( - ["az", "vm", "get-instance-view", - "--name", "waa-eval-vm", - "--resource-group", "openadapt-agents", - "--query", "instanceView.statuses", - "-o", "json"], - capture_output=True, text=True, timeout=10 + [ + "az", + "vm", + "get-instance-view", + "--name", + "waa-eval-vm", + "--resource-group", + "openadapt-agents", + "--query", + "instanceView.statuses", + "-o", + "json", + ], + capture_output=True, + text=True, + timeout=10, ) if result.returncode == 0: statuses = json.loads(result.stdout) @@ -1094,31 +1283,49 @@ def _fetch_background_tasks(self): # Get VM IP ip_result = subprocess.run( - ["az", "vm", "list-ip-addresses", - "--name", "waa-eval-vm", - "--resource-group", "openadapt-agents", - "--query", "[0].virtualMachine.network.publicIpAddresses[0].ipAddress", - "-o", "tsv"], - capture_output=True, text=True, timeout=10 + [ + "az", + "vm", + "list-ip-addresses", + "--name", + "waa-eval-vm", + "--resource-group", + "openadapt-agents", + "--query", + "[0].virtualMachine.network.publicIpAddresses[0].ipAddress", + "-o", + "tsv", + ], + capture_output=True, + text=True, + timeout=10, + ) + vm_ip = ( + ip_result.stdout.strip() + if ip_result.returncode == 0 + else None ) - vm_ip = ip_result.stdout.strip() if ip_result.returncode == 0 else None if power_state == "running": - tasks.append({ - "task_id": "azure-vm-waa", - "task_type": "vm_provision", - "status": "completed", - "phase": "ready", # Match status to prevent "Starting" + "completed" conflict - "title": "Azure VM Host", - "description": f"Linux host running at {vm_ip}" if vm_ip else "Linux host running", - "progress_percent": 100.0, - "elapsed_seconds": 0, - "metadata": { - "vm_name": "waa-eval-vm", - "ip_address": vm_ip - # No VNC link - that's for the Windows container + tasks.append( + { + "task_id": "azure-vm-waa", + "task_type": "vm_provision", + "status": "completed", + "phase": "ready", # Match status to prevent "Starting" + "completed" conflict + "title": "Azure VM Host", + "description": f"Linux host running at {vm_ip}" + if vm_ip + else "Linux host running", + "progress_percent": 100.0, + "elapsed_seconds": 0, + "metadata": { + "vm_name": "waa-eval-vm", + "ip_address": vm_ip, + # No VNC link - that's for the Windows container + }, } - }) + ) except subprocess.TimeoutExpired: pass except Exception: @@ -1128,30 +1335,59 @@ def _fetch_background_tasks(self): if vm_ip: try: docker_result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "docker ps --format '{{.Names}}|{{.Status}}|{{.Image}}'"], - capture_output=True, text=True, timeout=15 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "docker ps --format '{{.Names}}|{{.Status}}|{{.Image}}'", + ], + capture_output=True, + text=True, + timeout=15, ) if docker_result.returncode == 0 and docker_result.stdout.strip(): - for line in docker_result.stdout.strip().split('\n'): - parts = line.split('|') + for line in docker_result.stdout.strip().split("\n"): + parts = line.split("|") if len(parts) >= 3: - container_name, status, image = parts[0], parts[1], parts[2] + container_name, status, image = ( + parts[0], + parts[1], + parts[2], + ) # Parse "Up X minutes" to determine if healthy # Check for Windows VM specifically - if "windows" in image.lower() or container_name == "winarena": + if ( + "windows" in image.lower() + or container_name == "winarena" + ): # Get detailed progress from docker logs log_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - f"docker logs {container_name} 2>&1 | tail -30"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + f"docker logs {container_name} 2>&1 | tail -30", + ], + capture_output=True, + text=True, + timeout=10, + ) + logs = ( + log_check.stdout + if log_check.returncode == 0 + else "" ) - logs = log_check.stdout if log_check.returncode == 0 else "" # Parse progress from logs phase = "unknown" @@ -1162,17 +1398,32 @@ def _fetch_background_tasks(self): # Check if WAA server is ready via Docker port forwarding # See docs/waa_network_architecture.md - always use localhost server_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "curl -s --connect-timeout 2 http://localhost:5000/probe 2>/dev/null"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "curl -s --connect-timeout 2 http://localhost:5000/probe 2>/dev/null", + ], + capture_output=True, + text=True, + timeout=10, + ) + waa_ready = ( + server_check.returncode == 0 + and "Service is operational" + in server_check.stdout ) - waa_ready = server_check.returncode == 0 and "Service is operational" in server_check.stdout if waa_ready: phase = "ready" progress = 100.0 - description = "WAA Server ready - benchmarks can run" + description = ( + "WAA Server ready - benchmarks can run" + ) else: phase = "oobe" progress = 80.0 # Phase 5/6 - VM install in progress @@ -1181,10 +1432,15 @@ def _fetch_background_tasks(self): phase = "booting" progress = 70.0 # Phase 4/6 description = "Phase 4/6: Booting Windows from installer..." - elif "Building Windows" in logs or "Creating a" in logs: + elif ( + "Building Windows" in logs + or "Creating a" in logs + ): phase = "building" progress = 60.0 # Phase 3/6 - description = "Phase 3/6: Building Windows VM disk..." + description = ( + "Phase 3/6: Building Windows VM disk..." + ) elif "Adding" in logs and "image" in logs: phase = "configuring" progress = 50.0 # Phase 2/6 @@ -1192,15 +1448,22 @@ def _fetch_background_tasks(self): elif "Extracting" in logs: phase = "extracting" progress = 35.0 # Phase 1/6 (after download) - description = "Phase 1/6: Extracting Windows ISO..." + description = ( + "Phase 1/6: Extracting Windows ISO..." + ) else: # Check for download progress (e.g., "1234K ........ 45% 80M 30s") import re - download_match = re.search(r'(\d+)%\s+[\d.]+[KMG]\s+(\d+)s', logs) + + download_match = re.search( + r"(\d+)%\s+[\d.]+[KMG]\s+(\d+)s", logs + ) if download_match: phase = "downloading" dl_pct = float(download_match.group(1)) - progress = dl_pct * 0.30 # 0-30% for download phase + progress = ( + dl_pct * 0.30 + ) # 0-30% for download phase eta = download_match.group(2) description = f"Phase 0/6: Downloading Windows 11... {download_match.group(1)}% ({eta}s left)" @@ -1213,39 +1476,60 @@ def _fetch_background_tasks(self): progress = 90.0 # Get detailed metadata for VM Details panel - vm_metadata = self._get_vm_detailed_metadata(vm_ip, container_name, logs, phase) - - tasks.append({ - "task_id": f"docker-{container_name}", - "task_type": "docker_container", - "status": "completed" if phase == "ready" else "running", - "title": "Windows 11 + WAA Server", - "description": description, - "progress_percent": progress, - "elapsed_seconds": 0, - "phase": phase, - "metadata": { - "container": container_name, - "image": image, - "status": status, + vm_metadata = self._get_vm_detailed_metadata( + vm_ip, container_name, logs, phase + ) + + tasks.append( + { + "task_id": f"docker-{container_name}", + "task_type": "docker_container", + "status": "completed" + if phase == "ready" + else "running", + "title": "Windows 11 + WAA Server", + "description": description, + "progress_percent": progress, + "elapsed_seconds": 0, "phase": phase, - "windows_ready": phase in ["oobe", "ready"], - "waa_server_ready": phase == "ready", - # Use localhost - SSH tunnel handles routing to VM - # See docs/waa_network_architecture.md - "vnc_url": "http://localhost:8006", - "windows_username": "Docker", - "windows_password": "admin", - "recent_logs": logs[-500:] if logs else "", - # Enhanced VM details - "disk_usage_gb": vm_metadata["disk_usage_gb"], - "memory_usage_mb": vm_metadata["memory_usage_mb"], - "setup_script_phase": vm_metadata["setup_script_phase"], - "probe_response": vm_metadata["probe_response"], - "qmp_connected": vm_metadata["qmp_connected"], - "dependencies": vm_metadata["dependencies"], + "metadata": { + "container": container_name, + "image": image, + "status": status, + "phase": phase, + "windows_ready": phase + in ["oobe", "ready"], + "waa_server_ready": phase == "ready", + # Use localhost - SSH tunnel handles routing to VM + # See docs/waa_network_architecture.md + "vnc_url": "http://localhost:8006", + "windows_username": "Docker", + "windows_password": "admin", + "recent_logs": logs[-500:] + if logs + else "", + # Enhanced VM details + "disk_usage_gb": vm_metadata[ + "disk_usage_gb" + ], + "memory_usage_mb": vm_metadata[ + "memory_usage_mb" + ], + "setup_script_phase": vm_metadata[ + "setup_script_phase" + ], + "probe_response": vm_metadata[ + "probe_response" + ], + "qmp_connected": vm_metadata[ + "qmp_connected" + ], + "dependencies": vm_metadata[ + "dependencies" + ], + }, } - }) + ) except Exception: # SSH failed, VM might still be starting pass @@ -1256,16 +1540,24 @@ def _fetch_background_tasks(self): try: progress = json.loads(progress_file.read_text()) if progress.get("status") == "running": - tasks.append({ - "task_id": "benchmark-local", - "task_type": "benchmark_run", - "status": "running", - "title": f"{progress.get('provider', 'API').upper()} Benchmark", - "description": progress.get("message", "Running benchmark..."), - "progress_percent": (progress.get("tasks_complete", 0) / max(progress.get("tasks_total", 1), 1)) * 100, - "elapsed_seconds": 0, - "metadata": progress - }) + tasks.append( + { + "task_id": "benchmark-local", + "task_type": "benchmark_run", + "status": "running", + "title": f"{progress.get('provider', 'API').upper()} Benchmark", + "description": progress.get( + "message", "Running benchmark..." + ), + "progress_percent": ( + progress.get("tasks_complete", 0) + / max(progress.get("tasks_total", 1), 1) + ) + * 100, + "elapsed_seconds": 0, + "metadata": progress, + } + ) except Exception: pass @@ -1301,7 +1593,9 @@ def _fetch_vm_registry(self): vnc_url = f"http://{vm['ssh_host']}:{vm['vnc_port']}" result = subprocess.run( ["curl", "-I", "-s", "--connect-timeout", "3", vnc_url], - capture_output=True, text=True, timeout=5 + capture_output=True, + text=True, + timeout=5, ) if result.returncode == 0 and "200" in result.stdout: vm["vnc_reachable"] = True @@ -1315,13 +1609,25 @@ def _fetch_vm_registry(self): waa_port = vm.get("waa_port", 5000) ssh_cmd = f"curl -s --connect-timeout 2 http://localhost:{waa_port}/probe 2>/dev/null" result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"{vm['ssh_user']}@{vm['ssh_host']}", - ssh_cmd], - capture_output=True, text=True, timeout=5 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=3", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"{vm['ssh_user']}@{vm['ssh_host']}", + ssh_cmd, + ], + capture_output=True, + text=True, + timeout=5, + ) + probe_success = ( + result.returncode == 0 + and "Service is operational" in result.stdout ) - probe_success = result.returncode == 0 and "Service is operational" in result.stdout if probe_success: vm["waa_probe_status"] = "ready" vm["status"] = "online" @@ -1333,7 +1639,11 @@ def _fetch_vm_registry(self): ssh_user=vm.get("ssh_user", "azureuser"), ) vm["tunnels"] = { - name: {"active": s.active, "local_port": s.local_port, "error": s.error} + name: { + "active": s.active, + "local_port": s.local_port, + "error": s.error, + } for name, s in tunnel_status.items() } except Exception as e: @@ -1379,12 +1689,22 @@ def _probe_vm(self) -> dict: # First get VM IP try: ip_result = subprocess.run( - ["az", "vm", "list-ip-addresses", - "--name", "waa-eval-vm", - "--resource-group", "openadapt-agents", - "--query", "[0].virtualMachine.network.publicIpAddresses[0].ipAddress", - "-o", "tsv"], - capture_output=True, text=True, timeout=10 + [ + "az", + "vm", + "list-ip-addresses", + "--name", + "waa-eval-vm", + "--resource-group", + "openadapt-agents", + "--query", + "[0].virtualMachine.network.publicIpAddresses[0].ipAddress", + "-o", + "tsv", + ], + capture_output=True, + text=True, + timeout=10, ) if ip_result.returncode == 0 and ip_result.stdout.strip(): vm_ip = ip_result.stdout.strip() @@ -1393,11 +1713,20 @@ def _probe_vm(self) -> dict: # Try to probe WAA server via SSH # Use the correct internal IP for the Windows VM inside Docker probe_result = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "docker exec waa-container curl -s --connect-timeout 3 http://172.30.0.2:5000/probe 2>/dev/null || echo 'probe_failed'"], - capture_output=True, text=True, timeout=15 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "docker exec waa-container curl -s --connect-timeout 3 http://172.30.0.2:5000/probe 2>/dev/null || echo 'probe_failed'", + ], + capture_output=True, + text=True, + timeout=15, ) result["container"] = "waa-container" @@ -1410,7 +1739,9 @@ def _probe_vm(self) -> dict: else: result["probe_result"] = "WAA server not responding" else: - result["probe_result"] = f"SSH/Docker error: {probe_result.stderr[:200]}" + result["probe_result"] = ( + f"SSH/Docker error: {probe_result.stderr[:200]}" + ) else: result["probe_result"] = "Could not get VM IP" except subprocess.TimeoutExpired: @@ -1459,8 +1790,12 @@ def _get_current_run(self) -> dict: result["running"] = True result["type"] = "local" result["model"] = progress.get("provider", "unknown") - result["progress"]["tasks_completed"] = progress.get("tasks_complete", 0) - result["progress"]["total_tasks"] = progress.get("tasks_total", 0) + result["progress"]["tasks_completed"] = progress.get( + "tasks_complete", 0 + ) + result["progress"]["total_tasks"] = progress.get( + "tasks_total", 0 + ) return result except Exception: pass @@ -1469,12 +1804,22 @@ def _get_current_run(self) -> dict: try: # Get VM IP ip_result = subprocess.run( - ["az", "vm", "list-ip-addresses", - "--name", "waa-eval-vm", - "--resource-group", "openadapt-agents", - "--query", "[0].virtualMachine.network.publicIpAddresses[0].ipAddress", - "-o", "tsv"], - capture_output=True, text=True, timeout=10 + [ + "az", + "vm", + "list-ip-addresses", + "--name", + "waa-eval-vm", + "--resource-group", + "openadapt-agents", + "--query", + "[0].virtualMachine.network.publicIpAddresses[0].ipAddress", + "-o", + "tsv", + ], + capture_output=True, + text=True, + timeout=10, ) if ip_result.returncode == 0 and ip_result.stdout.strip(): @@ -1482,42 +1827,73 @@ def _get_current_run(self) -> dict: # Check if benchmark process is running process_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "docker exec waa-container pgrep -f 'python.*run.py' 2>/dev/null && echo 'RUNNING' || echo 'NOT_RUNNING'"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "docker exec waa-container pgrep -f 'python.*run.py' 2>/dev/null && echo 'RUNNING' || echo 'NOT_RUNNING'", + ], + capture_output=True, + text=True, + timeout=10, ) - if process_check.returncode == 0 and "RUNNING" in process_check.stdout: + if ( + process_check.returncode == 0 + and "RUNNING" in process_check.stdout + ): result["running"] = True result["type"] = "azure_vm" # Get log file for more details log_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "tail -100 /tmp/waa_benchmark.log 2>/dev/null || echo ''"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "tail -100 /tmp/waa_benchmark.log 2>/dev/null || echo ''", + ], + capture_output=True, + text=True, + timeout=10, ) if log_check.returncode == 0 and log_check.stdout.strip(): logs = log_check.stdout # Parse model from logs - model_match = re.search(r'model[=:\s]+([^\s,]+)', logs, re.IGNORECASE) + model_match = re.search( + r"model[=:\s]+([^\s,]+)", logs, re.IGNORECASE + ) if model_match: result["model"] = model_match.group(1) # Parse progress - task_match = re.search(r'Task\s+(\d+)/(\d+)', logs) + task_match = re.search(r"Task\s+(\d+)/(\d+)", logs) if task_match: - result["progress"]["tasks_completed"] = int(task_match.group(1)) - result["progress"]["total_tasks"] = int(task_match.group(2)) + result["progress"]["tasks_completed"] = int( + task_match.group(1) + ) + result["progress"]["total_tasks"] = int( + task_match.group(2) + ) # Parse current task - task_id_match = re.search(r'(?:Running|Processing|task)[:\s]+([a-f0-9-]+)', logs, re.IGNORECASE) + task_id_match = re.search( + r"(?:Running|Processing|task)[:\s]+([a-f0-9-]+)", + logs, + re.IGNORECASE, + ) if task_id_match: result["current_task"] = task_id_match.group(1) @@ -1526,7 +1902,9 @@ def _get_current_run(self) -> dict: return result - async def _detect_running_benchmark(self, vm_ip: str, container_name: str = "winarena") -> dict: + async def _detect_running_benchmark( + self, vm_ip: str, container_name: str = "winarena" + ) -> dict: """Detect if a benchmark is running on the VM and extract progress. SSH into VM and check: @@ -1557,11 +1935,20 @@ async def _detect_running_benchmark(self, vm_ip: str, container_name: str = "win try: # Check if benchmark process is running process_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - f"docker exec {container_name} pgrep -f 'python.*run.py' 2>/dev/null || echo ''"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + f"docker exec {container_name} pgrep -f 'python.*run.py' 2>/dev/null || echo ''", + ], + capture_output=True, + text=True, + timeout=10, ) if process_check.returncode == 0 and process_check.stdout.strip(): @@ -1569,11 +1956,20 @@ async def _detect_running_benchmark(self, vm_ip: str, container_name: str = "win # Get benchmark log log_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "tail -100 /tmp/waa_benchmark.log 2>/dev/null || echo ''"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "tail -100 /tmp/waa_benchmark.log 2>/dev/null || echo ''", + ], + capture_output=True, + text=True, + timeout=10, ) if log_check.returncode == 0 and log_check.stdout.strip(): @@ -1582,20 +1978,26 @@ async def _detect_running_benchmark(self, vm_ip: str, container_name: str = "win # Parse progress from logs # Look for patterns like "Task 5/30" or "Completed: 5, Remaining: 25" - task_match = re.search(r'Task\s+(\d+)/(\d+)', logs) + task_match = re.search(r"Task\s+(\d+)/(\d+)", logs) if task_match: - result["progress"]["tasks_completed"] = int(task_match.group(1)) + result["progress"]["tasks_completed"] = int( + task_match.group(1) + ) result["progress"]["total_tasks"] = int(task_match.group(2)) # Extract current task ID - task_id_match = re.search(r'(?:Running|Processing) task:\s*(\S+)', logs) + task_id_match = re.search( + r"(?:Running|Processing) task:\s*(\S+)", logs + ) if task_id_match: result["current_task"] = task_id_match.group(1) # Extract step info - step_match = re.search(r'Step\s+(\d+)', logs) + step_match = re.search(r"Step\s+(\d+)", logs) if step_match: - result["progress"]["current_step"] = int(step_match.group(1)) + result["progress"]["current_step"] = int( + step_match.group(1) + ) except Exception: # SSH or parsing failed - leave defaults @@ -1621,13 +2023,13 @@ def _parse_task_result(self, log_lines: list[str], task_id: str) -> dict: # Search backwards from most recent for line in reversed(log_lines): # Check for explicit result - if 'Result: PASS' in line or 'completed successfully' in line: + if "Result: PASS" in line or "completed successfully" in line: success = True - elif 'Result: FAIL' in line or 'failed' in line.lower(): + elif "Result: FAIL" in line or "failed" in line.lower(): success = False # Check for score - score_match = re.search(r'Score:\s*([\d.]+)', line) + score_match = re.search(r"Score:\s*([\d.]+)", line) if score_match: try: score = float(score_match.group(1)) @@ -1636,9 +2038,9 @@ def _parse_task_result(self, log_lines: list[str], task_id: str) -> dict: # Check for task-specific completion if task_id in line: - if 'success' in line.lower() or 'pass' in line.lower(): + if "success" in line.lower() or "pass" in line.lower(): success = True - elif 'fail' in line.lower() or 'error' in line.lower(): + elif "fail" in line.lower() or "error" in line.lower(): success = False # Default to True if no explicit failure found (backwards compatible) @@ -1668,11 +2070,11 @@ def _stream_benchmark_updates(self, interval: int): # Set SSE headers self.send_response(200) - self.send_header('Content-Type', 'text/event-stream') - self.send_header('Cache-Control', 'no-cache') - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Connection', 'keep-alive') - self.send_header('X-Accel-Buffering', 'no') # Disable nginx buffering + self.send_header("Content-Type", "text/event-stream") + self.send_header("Cache-Control", "no-cache") + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Connection", "keep-alive") + self.send_header("X-Accel-Buffering", "no") # Disable nginx buffering self.end_headers() # Track connection state @@ -1685,7 +2087,7 @@ def send_event(event_type: str, data: dict) -> bool: return False try: event_str = f"event: {event_type}\ndata: {json.dumps(data)}\n\n" - self.wfile.write(event_str.encode('utf-8')) + self.wfile.write(event_str.encode("utf-8")) self.wfile.flush() return True except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError): @@ -1728,11 +2130,10 @@ def check_client_connected() -> bool: recent_log_lines = [] # Send initial connected event - if not send_event("connected", { - "timestamp": time.time(), - "interval": interval, - "version": "1.0" - }): + if not send_event( + "connected", + {"timestamp": time.time(), "interval": interval, "version": "1.0"}, + ): return try: @@ -1757,17 +2158,25 @@ def check_client_connected() -> bool: tasks = self._fetch_background_tasks() # Send VM status event - vm_task = next((t for t in tasks if t.get("task_type") == "docker_container"), None) + vm_task = next( + (t for t in tasks if t.get("task_type") == "docker_container"), + None, + ) if vm_task: vm_data = { "type": "vm_status", - "connected": vm_task.get("status") in ["running", "completed"], + "connected": vm_task.get("status") + in ["running", "completed"], "phase": vm_task.get("phase", "unknown"), - "waa_ready": vm_task.get("metadata", {}).get("waa_server_ready", False), + "waa_ready": vm_task.get("metadata", {}).get( + "waa_server_ready", False + ), "probe": { - "status": vm_task.get("metadata", {}).get("probe_response", "unknown"), + "status": vm_task.get("metadata", {}).get( + "probe_response", "unknown" + ), "vnc_url": vm_task.get("metadata", {}).get("vnc_url"), - } + }, } if not send_event("status", vm_data): @@ -1777,27 +2186,47 @@ def check_client_connected() -> bool: if vm_data["waa_ready"]: # Get VM IP from tasks vm_ip = None - azure_vm = next((t for t in tasks if t.get("task_type") == "vm_provision"), None) + azure_vm = next( + ( + t + for t in tasks + if t.get("task_type") == "vm_provision" + ), + None, + ) if azure_vm: vm_ip = azure_vm.get("metadata", {}).get("ip_address") if vm_ip: # Detect running benchmark using sync version benchmark_status = self._detect_running_benchmark_sync( - vm_ip, vm_task.get("metadata", {}).get("container", "winarena") + vm_ip, + vm_task.get("metadata", {}).get( + "container", "winarena" + ), ) if benchmark_status["running"]: # Store log lines for result parsing if benchmark_status.get("recent_logs"): - recent_log_lines = benchmark_status["recent_logs"].split('\n') + recent_log_lines = benchmark_status[ + "recent_logs" + ].split("\n") # Send progress event progress_data = { - "tasks_completed": benchmark_status["progress"]["tasks_completed"], - "total_tasks": benchmark_status["progress"]["total_tasks"], - "current_task": benchmark_status["current_task"], - "current_step": benchmark_status["progress"]["current_step"], + "tasks_completed": benchmark_status["progress"][ + "tasks_completed" + ], + "total_tasks": benchmark_status["progress"][ + "total_tasks" + ], + "current_task": benchmark_status[ + "current_task" + ], + "current_step": benchmark_status["progress"][ + "current_step" + ], } if not send_event("progress", progress_data): @@ -1808,13 +2237,17 @@ def check_client_connected() -> bool: if current_task and current_task != last_task: if last_task is not None: # Previous task completed - parse result from logs - result = self._parse_task_result(recent_log_lines, last_task) + result = self._parse_task_result( + recent_log_lines, last_task + ) complete_data = { "task_id": last_task, "success": result["success"], "score": result["score"], } - if not send_event("task_complete", complete_data): + if not send_event( + "task_complete", complete_data + ): break last_task = current_task @@ -1826,7 +2259,9 @@ def check_client_connected() -> bool: progress = json.loads(progress_file.read_text()) if progress.get("status") == "running": progress_data = { - "tasks_completed": progress.get("tasks_complete", 0), + "tasks_completed": progress.get( + "tasks_complete", 0 + ), "total_tasks": progress.get("tasks_total", 0), "current_task": progress.get("provider", "unknown"), } @@ -1852,7 +2287,9 @@ def check_client_connected() -> bool: # Cleanup - connection is ending client_connected = False - def _detect_running_benchmark_sync(self, vm_ip: str, container_name: str = "winarena") -> dict: + def _detect_running_benchmark_sync( + self, vm_ip: str, container_name: str = "winarena" + ) -> dict: """Synchronous version of _detect_running_benchmark. Avoids creating a new event loop on each call which causes issues @@ -1875,11 +2312,20 @@ def _detect_running_benchmark_sync(self, vm_ip: str, container_name: str = "wina try: # Check if benchmark process is running process_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - f"docker exec {container_name} pgrep -f 'python.*run.py' 2>/dev/null || echo ''"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + f"docker exec {container_name} pgrep -f 'python.*run.py' 2>/dev/null || echo ''", + ], + capture_output=True, + text=True, + timeout=10, ) if process_check.returncode == 0 and process_check.stdout.strip(): @@ -1887,11 +2333,20 @@ def _detect_running_benchmark_sync(self, vm_ip: str, container_name: str = "wina # Get benchmark log log_check = subprocess.run( - ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", - "-i", str(Path.home() / ".ssh" / "id_rsa"), - f"azureuser@{vm_ip}", - "tail -100 /tmp/waa_benchmark.log 2>/dev/null || echo ''"], - capture_output=True, text=True, timeout=10 + [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=5", + "-i", + str(Path.home() / ".ssh" / "id_rsa"), + f"azureuser@{vm_ip}", + "tail -100 /tmp/waa_benchmark.log 2>/dev/null || echo ''", + ], + capture_output=True, + text=True, + timeout=10, ) if log_check.returncode == 0 and log_check.stdout.strip(): @@ -1899,20 +2354,26 @@ def _detect_running_benchmark_sync(self, vm_ip: str, container_name: str = "wina result["recent_logs"] = logs[-500:] # Last 500 chars # Parse progress from logs - task_match = re.search(r'Task\s+(\d+)/(\d+)', logs) + task_match = re.search(r"Task\s+(\d+)/(\d+)", logs) if task_match: - result["progress"]["tasks_completed"] = int(task_match.group(1)) + result["progress"]["tasks_completed"] = int( + task_match.group(1) + ) result["progress"]["total_tasks"] = int(task_match.group(2)) # Extract current task ID - task_id_match = re.search(r'(?:Running|Processing) task:\s*(\S+)', logs) + task_id_match = re.search( + r"(?:Running|Processing) task:\s*(\S+)", logs + ) if task_id_match: result["current_task"] = task_id_match.group(1) # Extract step info - step_match = re.search(r'Step\s+(\d+)', logs) + step_match = re.search(r"Step\s+(\d+)", logs) if step_match: - result["progress"]["current_step"] = int(step_match.group(1)) + result["progress"]["current_step"] = int( + step_match.group(1) + ) except Exception: # SSH or parsing failed - leave defaults @@ -1943,7 +2404,7 @@ def _register_vm(self, vm_data): "vnc_port": vm_data.get("vnc_port", 8006), "waa_port": vm_data.get("waa_port", 5000), "docker_container": vm_data.get("docker_container", "win11-waa"), - "internal_ip": vm_data.get("internal_ip", "20.20.20.21") + "internal_ip": vm_data.get("internal_ip", "20.20.20.21"), } vms.append(new_vm) @@ -1951,7 +2412,7 @@ def _register_vm(self, vm_data): # Save registry try: registry_file.parent.mkdir(parents=True, exist_ok=True) - with open(registry_file, 'w') as f: + with open(registry_file, "w") as f: json.dump(vms, f, indent=2) return {"status": "success", "vm": new_vm} except Exception as e: @@ -1984,12 +2445,20 @@ def _start_benchmark_run(self, params: dict) -> dict: # Build CLI command cmd = [ - "uv", "run", "python", "-m", "openadapt_ml.benchmarks.cli", - "vm", "run-waa", - "--num-tasks", str(params.get("num_tasks", 5)), - "--model", params.get("model", "gpt-4o"), - "--agent", params.get("agent", "navi"), - "--no-open" # Don't open viewer (already open) + "uv", + "run", + "python", + "-m", + "openadapt_ml.benchmarks.cli", + "vm", + "run-waa", + "--num-tasks", + str(params.get("num_tasks", 5)), + "--model", + params.get("model", "gpt-4o"), + "--agent", + params.get("agent", "navi"), + "--no-open", # Don't open viewer (already open) ] # Add task selection args @@ -2010,7 +2479,9 @@ def _start_benchmark_run(self, params: dict) -> dict: num_tasks = params.get("num_tasks", 5) agent = params.get("agent", "navi") - print(f"\n[Benchmark] Starting WAA benchmark: model={model}, tasks={num_tasks}, agent={agent}") + print( + f"\n[Benchmark] Starting WAA benchmark: model={model}, tasks={num_tasks}, agent={agent}" + ) print(f"[Benchmark] Task selection: {task_selection}") if task_selection == "domain": print(f"[Benchmark] Domain: {params.get('domain', 'general')}") @@ -2018,15 +2489,19 @@ def _start_benchmark_run(self, params: dict) -> dict: print(f"[Benchmark] Task IDs: {params.get('task_ids', [])}") print(f"[Benchmark] Command: {' '.join(cmd)}") - progress_file.write_text(json.dumps({ - "status": "running", - "model": model, - "num_tasks": num_tasks, - "agent": agent, - "task_selection": task_selection, - "tasks_complete": 0, - "message": f"Starting {model} benchmark with {num_tasks} tasks..." - })) + progress_file.write_text( + json.dumps( + { + "status": "running", + "model": model, + "num_tasks": num_tasks, + "agent": agent, + "task_selection": task_selection, + "tasks_complete": 0, + "message": f"Starting {model} benchmark with {num_tasks} tasks...", + } + ) + ) # Copy environment with loaded vars env = os.environ.copy() @@ -2034,11 +2509,7 @@ def _start_benchmark_run(self, params: dict) -> dict: # Run in background thread def run(): result = subprocess.run( - cmd, - capture_output=True, - text=True, - cwd=str(project_root), - env=env + cmd, capture_output=True, text=True, cwd=str(project_root), env=env ) print(f"\n[Benchmark] Output:\n{result.stdout}") @@ -2047,23 +2518,33 @@ def run(): if result.returncode == 0: print("[Benchmark] Complete. Regenerating viewer...") - progress_file.write_text(json.dumps({ - "status": "complete", - "model": model, - "num_tasks": num_tasks, - "message": "Benchmark complete. Refresh to see results." - })) + progress_file.write_text( + json.dumps( + { + "status": "complete", + "model": model, + "num_tasks": num_tasks, + "message": "Benchmark complete. Refresh to see results.", + } + ) + ) # Regenerate benchmark viewer _regenerate_benchmark_viewer_if_available(serve_dir) else: - error_msg = result.stderr[:200] if result.stderr else "Unknown error" + error_msg = ( + result.stderr[:200] if result.stderr else "Unknown error" + ) print(f"[Benchmark] Failed: {error_msg}") - progress_file.write_text(json.dumps({ - "status": "error", - "model": model, - "num_tasks": num_tasks, - "message": f"Benchmark failed: {error_msg}" - })) + progress_file.write_text( + json.dumps( + { + "status": "error", + "model": model, + "num_tasks": num_tasks, + "message": f"Benchmark failed: {error_msg}", + } + ) + ) threading.Thread(target=run, daemon=True).start() @@ -2072,9 +2553,9 @@ def run(): def do_OPTIONS(self): # Handle CORS preflight self.send_response(200) - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS') - self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): @@ -2128,7 +2609,9 @@ def cmd_viewer(args: argparse.Namespace) -> int: state.learning_rate = data.get("learning_rate", 0) state.losses = data.get("losses", []) state.status = data.get("status", "completed") - state.elapsed_time = data.get("elapsed_time", 0.0) # Load elapsed time for completed training + state.elapsed_time = data.get( + "elapsed_time", 0.0 + ) # Load elapsed time for completed training state.goal = data.get("goal", "") state.config_path = data.get("config_path", "") state.capture_path = data.get("capture_path", "") @@ -2143,6 +2626,7 @@ def cmd_viewer(args: argparse.Namespace) -> int: if not state.model_name and state.config_path: try: import yaml + # Try relative to project root first, then as absolute path project_root = Path(__file__).parent.parent.parent config_file = project_root / state.config_path @@ -2174,7 +2658,9 @@ def cmd_viewer(args: argparse.Namespace) -> int: if viewer_path: print(f"\nGenerated: {viewer_path}") else: - print("\nNo comparison data found. Run comparison first or copy from capture directory.") + print( + "\nNo comparison data found. Run comparison first or copy from capture directory." + ) # Also regenerate benchmark viewer from latest benchmark results _regenerate_benchmark_viewer_if_available(current_dir) @@ -2194,9 +2680,9 @@ def cmd_benchmark_viewer(args: argparse.Namespace) -> int: print(f"Error: Benchmark directory not found: {benchmark_dir}") return 1 - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("GENERATING BENCHMARK VIEWER") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Benchmark dir: {benchmark_dir}") print() @@ -2211,6 +2697,7 @@ def cmd_benchmark_viewer(args: argparse.Namespace) -> int: except Exception as e: print(f"Error generating benchmark viewer: {e}") import traceback + traceback.print_exc() return 1 @@ -2227,16 +2714,19 @@ def cmd_compare(args: argparse.Namespace) -> int: print(f"Error: Checkpoint not found: {checkpoint}") return 1 - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print("RUNNING COMPARISON") - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Capture: {capture_path}") print(f"Checkpoint: {checkpoint or 'None (capture only)'}") print() cmd = [ - sys.executable, "-m", "openadapt_ml.scripts.compare", - "--capture", str(capture_path), + sys.executable, + "-m", + "openadapt_ml.scripts.compare", + "--capture", + str(capture_path), ] if checkpoint: @@ -2275,7 +2765,7 @@ def main(): # Run comparison uv run python -m openadapt_ml.cloud.local compare --capture ~/captures/my-workflow --checkpoint checkpoints/model -""" +""", ) subparsers = parser.add_subparsers(dest="command", help="Command") @@ -2287,9 +2777,15 @@ def main(): # train p_train = subparsers.add_parser("train", help="Run training locally") p_train.add_argument("--capture", required=True, help="Path to capture directory") - p_train.add_argument("--goal", help="Task goal (default: derived from capture name)") - p_train.add_argument("--config", help="Config file (default: auto-select based on device)") - p_train.add_argument("--open", action="store_true", help="Open dashboard in browser") + p_train.add_argument( + "--goal", help="Task goal (default: derived from capture name)" + ) + p_train.add_argument( + "--config", help="Config file (default: auto-select based on device)" + ) + p_train.add_argument( + "--open", action="store_true", help="Open dashboard in browser" + ) p_train.set_defaults(func=cmd_train) # check @@ -2300,11 +2796,21 @@ def main(): p_serve = subparsers.add_parser("serve", help="Start web server for dashboard") p_serve.add_argument("--port", type=int, default=8765, help="Port number") p_serve.add_argument("--open", action="store_true", help="Open in browser") - p_serve.add_argument("--quiet", "-q", action="store_true", help="Suppress request logging") - p_serve.add_argument("--no-regenerate", action="store_true", - help="Skip regenerating dashboard/viewer (serve existing files)") - p_serve.add_argument("--benchmark", help="Serve benchmark results directory instead of training output") - p_serve.add_argument("--start-page", help="Override default start page (e.g., benchmark.html)") + p_serve.add_argument( + "--quiet", "-q", action="store_true", help="Suppress request logging" + ) + p_serve.add_argument( + "--no-regenerate", + action="store_true", + help="Skip regenerating dashboard/viewer (serve existing files)", + ) + p_serve.add_argument( + "--benchmark", + help="Serve benchmark results directory instead of training output", + ) + p_serve.add_argument( + "--start-page", help="Override default start page (e.g., benchmark.html)" + ) p_serve.set_defaults(func=cmd_serve) # viewer @@ -2313,9 +2819,15 @@ def main(): p_viewer.set_defaults(func=cmd_viewer) # benchmark_viewer - p_benchmark = subparsers.add_parser("benchmark-viewer", help="Generate benchmark viewer") - p_benchmark.add_argument("benchmark_dir", help="Path to benchmark results directory") - p_benchmark.add_argument("--open", action="store_true", help="Open viewer in browser") + p_benchmark = subparsers.add_parser( + "benchmark-viewer", help="Generate benchmark viewer" + ) + p_benchmark.add_argument( + "benchmark_dir", help="Path to benchmark results directory" + ) + p_benchmark.add_argument( + "--open", action="store_true", help="Open viewer in browser" + ) p_benchmark.set_defaults(func=cmd_benchmark_viewer) # compare diff --git a/openadapt_ml/cloud/ssh_tunnel.py b/openadapt_ml/cloud/ssh_tunnel.py index 5c4ac92..d401d6b 100644 --- a/openadapt_ml/cloud/ssh_tunnel.py +++ b/openadapt_ml/cloud/ssh_tunnel.py @@ -124,7 +124,9 @@ def __init__( self._current_vm_ip: str | None = None self._current_ssh_user: str | None = None self._auto_reconnect = auto_reconnect - self._reconnect_attempts: dict[str, int] = {} # Track reconnect attempts per tunnel + self._reconnect_attempts: dict[ + str, int + ] = {} # Track reconnect attempts per tunnel def start_tunnels_for_vm( self, @@ -197,7 +199,9 @@ def _start_tunnel( pid=None, # We don't know the PID of the external tunnel ) else: - logger.warning(f"Port {config.local_port} already in use by unknown process") + logger.warning( + f"Port {config.local_port} already in use by unknown process" + ) return TunnelStatus( name=config.name, active=False, @@ -212,16 +216,25 @@ def _start_tunnel( # TCPKeepAlive=yes: Enable TCP-level keepalive as additional safeguard ssh_cmd = [ "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "LogLevel=ERROR", - "-o", "ServerAliveInterval=60", - "-o", "ServerAliveCountMax=10", - "-o", "TCPKeepAlive=yes", - "-o", "ExitOnForwardFailure=yes", - "-i", str(self.ssh_key_path), + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "LogLevel=ERROR", + "-o", + "ServerAliveInterval=60", + "-o", + "ServerAliveCountMax=10", + "-o", + "TCPKeepAlive=yes", + "-o", + "ExitOnForwardFailure=yes", + "-i", + str(self.ssh_key_path), "-N", # Don't execute remote command - "-L", f"{config.local_port}:{config.remote_host}:{config.remote_port}", + "-L", + f"{config.local_port}:{config.remote_host}:{config.remote_port}", f"{ssh_user}@{vm_ip}", ] @@ -252,7 +265,9 @@ def _start_tunnel( # Tunnel started successfully self._active_tunnels[config.name] = (config, proc) - logger.info(f"Started tunnel {config.name}: localhost:{config.local_port} -> {vm_ip}:{config.remote_port}") + logger.info( + f"Started tunnel {config.name}: localhost:{config.local_port} -> {vm_ip}:{config.remote_port}" + ) return TunnelStatus( name=config.name, @@ -339,24 +354,36 @@ def get_tunnel_status(self, auto_restart: bool = True) -> dict[str, TunnelStatus name=config.name, active=True, local_port=config.local_port, - remote_endpoint=f"{self._current_vm_ip}:{config.remote_port}" if self._current_vm_ip else "unknown", + remote_endpoint=f"{self._current_vm_ip}:{config.remote_port}" + if self._current_vm_ip + else "unknown", pid=proc.pid, ) else: # Process died - but check if port is still working # (could be another tunnel on the same port) del self._active_tunnels[config.name] - if self._is_port_in_use(config.local_port) and self._check_tunnel_works(config.local_port, config.remote_port): + if self._is_port_in_use( + config.local_port + ) and self._check_tunnel_works( + config.local_port, config.remote_port + ): results[config.name] = TunnelStatus( name=config.name, active=True, local_port=config.local_port, - remote_endpoint=f"{self._current_vm_ip}:{config.remote_port}" if self._current_vm_ip else "external", + remote_endpoint=f"{self._current_vm_ip}:{config.remote_port}" + if self._current_vm_ip + else "external", pid=None, # External tunnel, PID unknown ) else: # Tunnel is dead - mark for restart if auto_reconnect enabled - if self._auto_reconnect and auto_restart and self._current_vm_ip: + if ( + self._auto_reconnect + and auto_restart + and self._current_vm_ip + ): tunnels_to_restart.append(config) results[config.name] = TunnelStatus( name=config.name, @@ -368,13 +395,19 @@ def get_tunnel_status(self, auto_restart: bool = True) -> dict[str, TunnelStatus else: # Not tracked internally - but check if an external tunnel exists # This handles tunnels started by other processes or after manager restart - if self._is_port_in_use(config.local_port) and self._check_tunnel_works(config.local_port, config.remote_port): - logger.debug(f"Found working external tunnel on port {config.local_port}") + if self._is_port_in_use(config.local_port) and self._check_tunnel_works( + config.local_port, config.remote_port + ): + logger.debug( + f"Found working external tunnel on port {config.local_port}" + ) results[config.name] = TunnelStatus( name=config.name, active=True, local_port=config.local_port, - remote_endpoint=f"{self._current_vm_ip}:{config.remote_port}" if self._current_vm_ip else "external", + remote_endpoint=f"{self._current_vm_ip}:{config.remote_port}" + if self._current_vm_ip + else "external", pid=None, # External tunnel, PID unknown ) else: @@ -389,16 +422,22 @@ def get_tunnel_status(self, auto_restart: bool = True) -> dict[str, TunnelStatus for config in tunnels_to_restart: attempts = self._reconnect_attempts.get(config.name, 0) if attempts < self.MAX_RECONNECT_ATTEMPTS: - logger.info(f"Auto-reconnecting tunnel {config.name} (attempt {attempts + 1}/{self.MAX_RECONNECT_ATTEMPTS})") + logger.info( + f"Auto-reconnecting tunnel {config.name} (attempt {attempts + 1}/{self.MAX_RECONNECT_ATTEMPTS})" + ) time.sleep(self.RECONNECT_DELAY_SECONDS) self._reconnect_attempts[config.name] = attempts + 1 - status = self._start_tunnel(config, self._current_vm_ip, self._current_ssh_user or "azureuser") + status = self._start_tunnel( + config, self._current_vm_ip, self._current_ssh_user or "azureuser" + ) results[config.name] = status if status.active: logger.info(f"Successfully reconnected tunnel {config.name}") self._reconnect_attempts[config.name] = 0 # Reset on success else: - logger.warning(f"Tunnel {config.name} exceeded max reconnect attempts ({self.MAX_RECONNECT_ATTEMPTS})") + logger.warning( + f"Tunnel {config.name} exceeded max reconnect attempts ({self.MAX_RECONNECT_ATTEMPTS})" + ) results[config.name] = TunnelStatus( name=config.name, active=False, @@ -454,7 +493,9 @@ def ensure_tunnels_for_vm( """ # If VM changed, stop old tunnels and reset reconnect attempts if self._current_vm_ip and self._current_vm_ip != vm_ip: - logger.info(f"VM IP changed from {self._current_vm_ip} to {vm_ip}, restarting tunnels") + logger.info( + f"VM IP changed from {self._current_vm_ip} to {vm_ip}, restarting tunnels" + ) self.stop_all_tunnels() self.reset_reconnect_attempts() # Fresh start for new VM diff --git a/openadapt_ml/datasets/next_action.py b/openadapt_ml/datasets/next_action.py index ebd7dbc..9ad9e8b 100644 --- a/openadapt_ml/datasets/next_action.py +++ b/openadapt_ml/datasets/next_action.py @@ -19,7 +19,7 @@ "- Example: An element in the middle of the screen would be approximately x=0.5, y=0.5\n\n" "ALLOWED ACTIONS (use exactly this format):\n" "- CLICK(x=0.XX, y=0.XX) → click at normalized coordinates\n" - "- TYPE(text=\"...\") → type text into the currently focused field\n" + '- TYPE(text="...") → type text into the currently focused field\n' "- WAIT() → wait for UI to update\n" "- DONE() → task is complete\n\n" "RESPONSE FORMAT (required):\n" @@ -41,14 +41,14 @@ "[3] = Login button\n\n" "ALLOWED ACTIONS (use exactly this format):\n" "- CLICK([N]) → click element with number N to focus/activate it\n" - "- TYPE([N], \"text\") → type text into element N (e.g., TYPE([2], \"hello\"))\n" + '- TYPE([N], "text") → type text into element N (e.g., TYPE([2], "hello"))\n' "- WAIT() → wait for UI to update\n" "- DONE() → task is complete\n\n" "ACTION SEQUENCE FOR LOGIN:\n" "1. CLICK([1]) to focus username field\n" - "2. TYPE([1], \"username\") to enter username\n" + '2. TYPE([1], "username") to enter username\n' "3. CLICK([2]) to focus password field\n" - "4. TYPE([2], \"password\") to enter password\n" + '4. TYPE([2], "password") to enter password\n' "5. CLICK([3]) to submit login\n" "6. DONE() when login is complete\n\n" "RESPONSE FORMAT (required):\n" @@ -73,20 +73,20 @@ "[6] = Register button\n\n" "ALLOWED ACTIONS (use exactly this format):\n" "- CLICK([N]) → click element with number N to focus/activate it\n" - "- TYPE([N], \"text\") → type text into element N (e.g., TYPE([2], \"hello\"))\n" + '- TYPE([N], "text") → type text into element N (e.g., TYPE([2], "hello"))\n' "- WAIT() → wait for UI to update\n" "- DONE() → task is complete\n\n" "ACTION SEQUENCE FOR REGISTRATION:\n" "1. CLICK([1]) to focus first name field\n" - "2. TYPE([1], \"name\") to enter first name\n" + '2. TYPE([1], "name") to enter first name\n' "3. CLICK([2]) to focus last name field\n" - "4. TYPE([2], \"name\") to enter last name\n" + '4. TYPE([2], "name") to enter last name\n' "5. CLICK([3]) to focus email field\n" - "6. TYPE([3], \"email\") to enter email\n" + '6. TYPE([3], "email") to enter email\n' "7. CLICK([4]) to focus password field\n" - "8. TYPE([4], \"pass\") to enter password\n" + '8. TYPE([4], "pass") to enter password\n' "9. CLICK([5]) to focus confirm password field\n" - "10. TYPE([5], \"pass\") to enter confirmation\n" + '10. TYPE([5], "pass") to enter confirmation\n' "11. CLICK([6]) to submit registration\n" "12. DONE() when registration is complete\n\n" "RESPONSE FORMAT (required):\n" @@ -126,12 +126,12 @@ def format_action(action: Action, use_som: bool = False) -> str: if t == ActionType.CLICK and element_id is not None: return f"CLICK([{element_id}])" if t == ActionType.TYPE and action.text is not None: - escaped = action.text.replace("\\", "\\\\").replace("\"", "\\\"") + escaped = action.text.replace("\\", "\\\\").replace('"', '\\"') if element_id is not None: - return f"TYPE([{element_id}], \"{escaped}\")" + return f'TYPE([{element_id}], "{escaped}")' else: # Fallback: TYPE without element reference (for focused field) - return f"TYPE(\"{escaped}\")" + return f'TYPE("{escaped}")' if t == ActionType.WAIT: return "WAIT()" if t == ActionType.DONE: @@ -144,8 +144,8 @@ def format_action(action: Action, use_som: bool = False) -> str: x, y = action.normalized_coordinates return f"CLICK(x={x:.2f}, y={y:.2f})" if t == ActionType.TYPE and action.text is not None: - escaped = action.text.replace("\\", "\\\\").replace("\"", "\\\"") - return f"TYPE(text=\"{escaped}\")" + escaped = action.text.replace("\\", "\\\\").replace('"', '\\"') + return f'TYPE(text="{escaped}")' if t == ActionType.WAIT: return "WAIT()" if t == ActionType.DONE: @@ -180,13 +180,15 @@ def parse_action_som(text: str) -> Action: match = re.match(r'TYPE\(\[(\d+)\],\s*["\'](.*)["\']\)', text, re.DOTALL) if match: idx = match.group(1) - content = match.group(2).replace("\\\"", "\"").replace("\\\\", "\\") - return Action(type=ActionType.TYPE, text=content, element=UIElement(element_id=idx)) + content = match.group(2).replace('\\"', '"').replace("\\\\", "\\") + return Action( + type=ActionType.TYPE, text=content, element=UIElement(element_id=idx) + ) # TYPE("text") - no element index match = re.match(r'TYPE\(["\'](.*)["\']\)', text, re.DOTALL) if match: - content = match.group(1).replace("\\\"", "\"").replace("\\\\", "\\") + content = match.group(1).replace('\\"', '"').replace("\\\\", "\\") return Action(type=ActionType.TYPE, text=content) # WAIT() @@ -201,7 +203,9 @@ def parse_action_som(text: str) -> Action: return Action(type=ActionType.FAIL, raw={"text": text}) -def _generate_generic_thought(step_index: int, step: Step, goal: str, total_steps: int) -> str: +def _generate_generic_thought( + step_index: int, step: Step, goal: str, total_steps: int +) -> str: """Generate a thought for real captures (non-synthetic scenarios). This creates action-appropriate thoughts that teach the model to output @@ -238,7 +242,9 @@ def _generate_generic_thought(step_index: int, step: Step, goal: str, total_step return f"{progress} I need to scroll to reveal more content or reach the target element for '{goal}'." if t == ActionType.DRAG: - return f"{progress} I need to drag an element to complete this part of '{goal}'." + return ( + f"{progress} I need to drag an element to complete this part of '{goal}'." + ) if t == ActionType.KEY: return f"{progress} I need to press a key to continue the workflow." @@ -268,7 +274,6 @@ def _generate_thought_for_step( actions back to the stated objective. """ - if scenario == "registration": return _generate_registration_thought(step_index, step, goal, total_steps) elif scenario == "login" and total_steps <= 7: @@ -279,7 +284,9 @@ def _generate_thought_for_step( return _generate_generic_thought(step_index, step, goal, total_steps) -def _generate_login_thought(step_index: int, step: Step, goal: str, total_steps: int) -> str: +def _generate_login_thought( + step_index: int, step: Step, goal: str, total_steps: int +) -> str: """Generate thought for login scenario (6 steps).""" action = step.action t = action.type @@ -333,7 +340,9 @@ def _generate_login_thought(step_index: int, step: Step, goal: str, total_steps: ) -def _generate_registration_thought(step_index: int, step: Step, goal: str, total_steps: int) -> str: +def _generate_registration_thought( + step_index: int, step: Step, goal: str, total_steps: int +) -> str: """Generate thought for registration scenario (12 steps).""" action = step.action t = action.type @@ -466,7 +475,9 @@ def build_next_action_sft_samples( history_text += f" {i}. {action_text}\n" history_text += f"\nThis is step {step_index + 1} of {total_steps}. " else: - history_text = f"This is step 1 of {total_steps} (no actions completed yet). " + history_text = ( + f"This is step 1 of {total_steps} (no actions completed yet). " + ) if use_som: user_content = ( @@ -474,7 +485,7 @@ def build_next_action_sft_samples( f"{history_text}" "Look at the screenshot and determine the NEXT action.\n\n" "Thought: [which numbered element to interact with and why]\n" - "Action: [CLICK([N]) or TYPE([N], \"text\") or WAIT() or DONE()]" + 'Action: [CLICK([N]) or TYPE([N], "text") or WAIT() or DONE()]' ) else: user_content = ( @@ -482,13 +493,15 @@ def build_next_action_sft_samples( f"{history_text}" "Look at the screenshot and determine the NEXT action.\n\n" "Thought: [what element to interact with and why]\n" - "Action: [CLICK(x=..., y=...) or TYPE(text=\"...\") or WAIT() or DONE()]" + 'Action: [CLICK(x=..., y=...) or TYPE(text="...") or WAIT() or DONE()]' ) # Provide a deterministic, semantically meaningful Thought while supervising # the exact DSL Action. action_text = format_action(step.action, use_som=use_som) - thought_text = _generate_thought_for_step(step_index, step, goal, scenario, total_steps) + thought_text = _generate_thought_for_step( + step_index, step, goal, scenario, total_steps + ) assistant_content = f"Thought: {thought_text}\nAction: {action_text}" sample = { diff --git a/openadapt_ml/evals/grounding.py b/openadapt_ml/evals/grounding.py index e189078..daf26eb 100644 --- a/openadapt_ml/evals/grounding.py +++ b/openadapt_ml/evals/grounding.py @@ -221,7 +221,9 @@ def evaluate_grounder_on_episode( action = step.action # Get action type as string for comparison - action_type_str = action.type.value if isinstance(action.type, ActionType) else action.type + action_type_str = ( + action.type.value if isinstance(action.type, ActionType) else action.type + ) # Only evaluate clicks with bboxes if action_type_str not in ("click", "double_click"): @@ -251,7 +253,9 @@ def evaluate_grounder_on_episode( if action.normalized_coordinates: coords_x, coords_y = action.normalized_coordinates if coords_x is not None and coords_y is not None: - target_desc = step.reasoning or f"element at ({coords_x:.2f}, {coords_y:.2f})" + target_desc = ( + step.reasoning or f"element at ({coords_x:.2f}, {coords_y:.2f})" + ) else: target_desc = step.reasoning or "target element" diff --git a/openadapt_ml/evals/plot_eval_metrics.py b/openadapt_ml/evals/plot_eval_metrics.py index f9be94a..b865983 100644 --- a/openadapt_ml/evals/plot_eval_metrics.py +++ b/openadapt_ml/evals/plot_eval_metrics.py @@ -73,7 +73,7 @@ def plot_eval_metrics( fig.suptitle( "VLM Model Comparison (Offline fine-tuned vs API models)", fontsize=12, - fontweight='bold', + fontweight="bold", ) if num_metrics == 1: axes = [axes] @@ -96,36 +96,38 @@ def plot_eval_metrics( hatches.append(hatch) x = range(num_models) - bars = ax.bar(x, values, tick_label=labels, color=colors, edgecolor='black', linewidth=1.2) + bars = ax.bar( + x, values, tick_label=labels, color=colors, edgecolor="black", linewidth=1.2 + ) # Apply hatch patterns for bar, hatch in zip(bars, hatches): bar.set_hatch(hatch) - ax.set_title(title, fontsize=11, fontweight='bold') + ax.set_title(title, fontsize=11, fontweight="bold") ax.set_ylabel(key, fontsize=9) ax.set_ylim(bottom=0.0) # Rotate x-axis labels to prevent crowding - ax.tick_params(axis='x', labelrotation=45, labelsize=8) + ax.tick_params(axis="x", labelrotation=45, labelsize=8) # Align labels to the right for better readability when rotated for tick in ax.get_xticklabels(): - tick.set_horizontalalignment('right') + tick.set_horizontalalignment("right") fig.tight_layout() # Add legend explaining color coding and hatch patterns legend_elements = [ - Patch(facecolor='#4A90E2', edgecolor='black', label='Qwen3-VL-2B'), - Patch(facecolor='#2E5C8A', edgecolor='black', label='Qwen3-VL-8B'), - Patch(facecolor='#FF6B35', edgecolor='black', label='Claude (API)'), - Patch(facecolor='#C1121F', edgecolor='black', label='GPT (API)'), - Patch(facecolor='gray', edgecolor='black', hatch='///', label='Fine-tuned'), - Patch(facecolor='gray', edgecolor='black', label='Base/Pretrained'), + Patch(facecolor="#4A90E2", edgecolor="black", label="Qwen3-VL-2B"), + Patch(facecolor="#2E5C8A", edgecolor="black", label="Qwen3-VL-8B"), + Patch(facecolor="#FF6B35", edgecolor="black", label="Claude (API)"), + Patch(facecolor="#C1121F", edgecolor="black", label="GPT (API)"), + Patch(facecolor="gray", edgecolor="black", hatch="///", label="Fine-tuned"), + Patch(facecolor="gray", edgecolor="black", label="Base/Pretrained"), ] fig.legend( handles=legend_elements, - loc='lower center', + loc="lower center", bbox_to_anchor=(0.5, -0.05), ncol=3, fontsize=9, @@ -133,7 +135,7 @@ def plot_eval_metrics( ) output_path.parent.mkdir(parents=True, exist_ok=True) - fig.savefig(output_path, dpi=150, bbox_inches='tight') + fig.savefig(output_path, dpi=150, bbox_inches="tight") plt.close(fig) diff --git a/openadapt_ml/evals/trajectory_matching.py b/openadapt_ml/evals/trajectory_matching.py index 8550300..34a3f46 100644 --- a/openadapt_ml/evals/trajectory_matching.py +++ b/openadapt_ml/evals/trajectory_matching.py @@ -15,10 +15,15 @@ class MilestoneSpec: A milestone is achieved when, at a specific step, the predicted action matches certain criteria (type match + optional coord threshold). """ + name: str step_index: int # Which step in the episode (0-indexed) - expected_type: str # Expected ground truth action type ("click", "type", "done", etc.) - coord_threshold: Optional[float] = None # If set, coord error must be < this for clicks + expected_type: ( + str # Expected ground truth action type ("click", "type", "done", etc.) + ) + coord_threshold: Optional[float] = ( + None # If set, coord error must be < this for clicks + ) # Predefined milestone specs per scenario @@ -28,7 +33,9 @@ class MilestoneSpec: LOGIN_MILESTONES = [ MilestoneSpec("typed_username", step_index=1, expected_type="type"), MilestoneSpec("typed_password", step_index=3, expected_type="type"), - MilestoneSpec("clicked_login", step_index=4, expected_type="click", coord_threshold=0.10), + MilestoneSpec( + "clicked_login", step_index=4, expected_type="click", coord_threshold=0.10 + ), MilestoneSpec("emitted_done", step_index=5, expected_type="done"), ] @@ -81,14 +88,22 @@ class AggregateMetrics: action_type_accuracy: float mean_coord_error: Optional[float] coord_error_count: int - episode_success_rate: Optional[float] # Strict: all steps must match (renamed from success_pred) + episode_success_rate: Optional[ + float + ] # Strict: all steps must match (renamed from success_pred) click_hit_rate: Optional[float] # Point-based: within 5% of center - mean_episode_progress: Optional[float] # Partial credit: avg(step_matches/step_total) + mean_episode_progress: Optional[ + float + ] # Partial credit: avg(step_matches/step_total) # New partial-credit metrics - mean_episode_step_score: Optional[float] # Strict partial: avg(full_step_correct/step_total) + mean_episode_step_score: Optional[ + float + ] # Strict partial: avg(full_step_correct/step_total) weak_episode_success_rate: Optional[float] # Semantic milestones all achieved state_success_rate: Optional[float] = None # From model's State: {"success": true} - bbox_hit_rate: Optional[float] = None # Bbox-based: click anywhere in element bounds + bbox_hit_rate: Optional[float] = ( + None # Bbox-based: click anywhere in element bounds + ) element_accuracy: Optional[float] = None # SoM element index accuracy @@ -122,12 +137,7 @@ def compute_coordinate_error(pred_action: Action, gt_action: Action) -> Optional pred_x, pred_y = _get_normalized_coords(pred_action) gt_x, gt_y = _get_normalized_coords(gt_action) - if ( - pred_x is None - or pred_y is None - or gt_x is None - or gt_y is None - ): + if pred_x is None or pred_y is None or gt_x is None or gt_y is None: return None dx = pred_x - gt_x @@ -212,7 +222,9 @@ def evaluate_episode( sample = samples[sample_idx] sample_idx += 1 - pred_action, _thought, pred_state, raw_text = policy.predict_action_from_sample(sample) + pred_action, _thought, pred_state, raw_text = policy.predict_action_from_sample( + sample + ) gt_action = step.action # Get action types as strings for comparison @@ -289,11 +301,17 @@ def _get_element_index(action: Action) -> Optional[int]: # Track semantic milestones using the milestone spec for milestone in milestones: - if step_idx == milestone.step_index and gt_type_str == milestone.expected_type: + if ( + step_idx == milestone.step_index + and gt_type_str == milestone.expected_type + ): if pred_type_str == milestone.expected_type: # Check coord threshold if specified (for click actions) if milestone.coord_threshold is not None: - if coord_error is not None and coord_error < milestone.coord_threshold: + if ( + coord_error is not None + and coord_error < milestone.coord_threshold + ): milestones_achieved[milestone.name] = True else: # No coord threshold - type match is sufficient @@ -426,18 +444,16 @@ def aggregate_metrics(episodes_metrics: List[EpisodeMetrics]) -> AggregateMetric # Partial credit: average episode progress (step_matches / step_total per episode) if eval_episodes: - episode_progress_scores = [ - m.step_matches / m.step_total for m in eval_episodes - ] - mean_episode_progress = sum(episode_progress_scores) / len(episode_progress_scores) + episode_progress_scores = [m.step_matches / m.step_total for m in eval_episodes] + mean_episode_progress = sum(episode_progress_scores) / len( + episode_progress_scores + ) else: mean_episode_progress = None # Strict partial: avg(full_step_correct / step_total) - requires type match + click hit if eval_episodes: - step_scores = [ - m.full_step_correct / m.step_total for m in eval_episodes - ] + step_scores = [m.full_step_correct / m.step_total for m in eval_episodes] mean_episode_step_score = sum(step_scores) / len(step_scores) else: mean_episode_step_score = None @@ -445,7 +461,8 @@ def aggregate_metrics(episodes_metrics: List[EpisodeMetrics]) -> AggregateMetric # Weak episode success: all milestones achieved if eval_episodes: weak_success_count = sum( - 1 for m in eval_episodes + 1 + for m in eval_episodes if m.milestones_achieved and all(m.milestones_achieved.values()) ) weak_episode_success_rate = weak_success_count / len(eval_episodes) diff --git a/openadapt_ml/experiments/demo_prompt/format_demo.py b/openadapt_ml/experiments/demo_prompt/format_demo.py index a64471d..88b3cf5 100644 --- a/openadapt_ml/experiments/demo_prompt/format_demo.py +++ b/openadapt_ml/experiments/demo_prompt/format_demo.py @@ -19,7 +19,7 @@ def format_action(action: "Action") -> str: String representation like "CLICK(0.5, 0.3)" or "TYPE('hello')". """ # Get action type value (handle both enum and string) - action_type = action.type.value if hasattr(action.type, 'value') else action.type + action_type = action.type.value if hasattr(action.type, "value") else action.type if action_type == "click": if action.normalized_coordinates is not None: @@ -53,7 +53,10 @@ def format_action(action: "Action") -> str: return f"SCROLL({direction})" elif action_type == "drag": - if action.normalized_coordinates is not None and action.normalized_end is not None: + if ( + action.normalized_coordinates is not None + and action.normalized_end is not None + ): x, y = action.normalized_coordinates end_x, end_y = action.normalized_end return f"DRAG({x:.3f}, {y:.3f}, {end_x:.3f}, {end_y:.3f})" @@ -112,7 +115,11 @@ def format_episode_as_demo( lines.append(format_step(step, i)) # Optionally include screenshot reference - if include_screenshots and step.observation and step.observation.screenshot_path: + if ( + include_screenshots + and step.observation + and step.observation.screenshot_path + ): lines.append(f" [Screenshot: {step.observation.screenshot_path}]") lines.append("") @@ -167,9 +174,12 @@ def format_episode_verbose( if next_step.observation and next_step.observation.window_title: if ( not step.observation - or next_step.observation.window_title != step.observation.window_title + or next_step.observation.window_title + != step.observation.window_title ): - lines.append(f" [Result: Window changed to {next_step.observation.window_title}]") + lines.append( + f" [Result: Window changed to {next_step.observation.window_title}]" + ) lines.append("") diff --git a/openadapt_ml/experiments/demo_prompt/run_experiment.py b/openadapt_ml/experiments/demo_prompt/run_experiment.py index 13cf853..3c38579 100644 --- a/openadapt_ml/experiments/demo_prompt/run_experiment.py +++ b/openadapt_ml/experiments/demo_prompt/run_experiment.py @@ -136,14 +136,16 @@ def _call_api( if Path(path).exists(): with open(path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") - content.append({ - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": image_b64, - }, - }) + content.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": image_b64, + }, + } + ) # Add text content.append({"type": "text", "text": user_content}) @@ -156,7 +158,11 @@ def _call_api( ) parts = getattr(response, "content", []) - texts = [getattr(p, "text", "") for p in parts if getattr(p, "type", "") == "text"] + texts = [ + getattr(p, "text", "") + for p in parts + if getattr(p, "type", "") == "text" + ] return "\n".join([t for t in texts if t]).strip() elif self.provider == "openai": @@ -168,10 +174,14 @@ def _call_api( if Path(path).exists(): with open(path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") - user_content_parts.append({ - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{image_b64}"}, - }) + user_content_parts.append( + { + "type": "image_url", + "image_url": { + "url": f"data:image/png;base64,{image_b64}" + }, + } + ) # Add text user_content_parts.append({"type": "text", "text": user_content}) @@ -444,7 +454,9 @@ def run_experiment( output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) - results_file = output_path / f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + results_file = ( + output_path / f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + ) with open(results_file, "w") as f: json.dump( { diff --git a/openadapt_ml/experiments/representation_shootout/conditions.py b/openadapt_ml/experiments/representation_shootout/conditions.py index bde4c46..256f952 100644 --- a/openadapt_ml/experiments/representation_shootout/conditions.py +++ b/openadapt_ml/experiments/representation_shootout/conditions.py @@ -79,7 +79,9 @@ def to_prompt_text(self, max_elements: int | None = None) -> str: [e17] textfield "Username" at (0.3, 0.4)-(0.7, 0.45) """ lines = [] - elements_to_show = self.elements[:max_elements] if max_elements else self.elements + elements_to_show = ( + self.elements[:max_elements] if max_elements else self.elements + ) for el in elements_to_show: name_part = f' "{el.name}"' if el.name else "" x1, y1, x2, y2 = el.bbox @@ -340,9 +342,13 @@ def parse_output(self, model_output: str) -> ParsedAction: pass # Check for TYPE action - type_match = re.search(r'TYPE\s*\(\s*["\'](.+?)["\']\s*\)', model_output, re.IGNORECASE) + type_match = re.search( + r'TYPE\s*\(\s*["\'](.+?)["\']\s*\)', model_output, re.IGNORECASE + ) if type_match: - return ParsedAction(type="type", text=type_match.group(1), raw_output=model_output) + return ParsedAction( + type="type", text=type_match.group(1), raw_output=model_output + ) # Check for DONE action if re.search(r"DONE\s*\(\s*\)", model_output, re.IGNORECASE): @@ -411,11 +417,16 @@ def prepare_input( prompt = self._build_base_prompt(goal, history) augmented_path = observation.screenshot_path - metadata: dict[str, Any] = {"condition": "coords_cues", "is_training": is_training} + metadata: dict[str, Any] = { + "condition": "coords_cues", + "is_training": is_training, + } if is_training and target_coords: # Add visual cues for training - prompt += "\n\nThe red marker and zoomed inset show the target click location." + prompt += ( + "\n\nThe red marker and zoomed inset show the target click location." + ) prompt += "\nLearn to identify this location based on the UI context." # Augment screenshot (placeholder - actual implementation would use PIL/cv2) @@ -521,7 +532,9 @@ def prepare_input( prompt += "\n\nNo UI elements detected." prompt += "\n\nWhich element should be clicked?" - prompt += "\nRespond with: ACTION: CLICK([element_id]) e.g., ACTION: CLICK([e17])" + prompt += ( + "\nRespond with: ACTION: CLICK([element_id]) e.g., ACTION: CLICK([e17])" + ) # Augment screenshot with marks overlay augmented_path = self._add_marks_overlay( @@ -535,7 +548,9 @@ def prepare_input( prompt=prompt, metadata={ "condition": "marks", - "num_elements": len(observation.ui_elements.elements) if observation.ui_elements else 0, + "num_elements": len(observation.ui_elements.elements) + if observation.ui_elements + else 0, }, ) @@ -595,7 +610,9 @@ def parse_output(self, model_output: str) -> ParsedAction: # Normalize element ID format if not element_id.startswith("e"): element_id = f"e{element_id}" - return ParsedAction(type="click", element_id=element_id, raw_output=model_output) + return ParsedAction( + type="click", element_id=element_id, raw_output=model_output + ) # Try looser patterns element_match = re.search( @@ -607,19 +624,27 @@ def parse_output(self, model_output: str) -> ParsedAction: element_id = element_match.group(1) if not element_id.startswith("e"): element_id = f"e{element_id}" - return ParsedAction(type="click", element_id=element_id, raw_output=model_output) + return ParsedAction( + type="click", element_id=element_id, raw_output=model_output + ) # Check for element mentioned in text (e.g., "click element e17") text_match = re.search(r"\b[eE](\d+)\b", model_output) if text_match: return ParsedAction( - type="click", element_id=f"e{text_match.group(1)}", raw_output=model_output + type="click", + element_id=f"e{text_match.group(1)}", + raw_output=model_output, ) # Check for TYPE action - type_match = re.search(r'TYPE\s*\(\s*["\'](.+?)["\']\s*\)', model_output, re.IGNORECASE) + type_match = re.search( + r'TYPE\s*\(\s*["\'](.+?)["\']\s*\)', model_output, re.IGNORECASE + ) if type_match: - return ParsedAction(type="type", text=type_match.group(1), raw_output=model_output) + return ParsedAction( + type="type", text=type_match.group(1), raw_output=model_output + ) # Check for DONE action if re.search(r"DONE\s*\(\s*\)", model_output, re.IGNORECASE): diff --git a/openadapt_ml/experiments/representation_shootout/config.py b/openadapt_ml/experiments/representation_shootout/config.py index a1a8817..51c2189 100644 --- a/openadapt_ml/experiments/representation_shootout/config.py +++ b/openadapt_ml/experiments/representation_shootout/config.py @@ -32,7 +32,9 @@ class MetricName(str, Enum): """Metrics computed during evaluation.""" CLICK_HIT_RATE = "click_hit_rate" # Clicks within target bbox - GROUNDING_TOP1_ACCURACY = "grounding_top1_accuracy" # Correct element ID (marks only) + GROUNDING_TOP1_ACCURACY = ( + "grounding_top1_accuracy" # Correct element ID (marks only) + ) EPISODE_SUCCESS_RATE = "episode_success_rate" # Episodes reaching goal COORD_DISTANCE = "coord_distance" # L2 distance to target (normalized) ROBUSTNESS_SCORE = "robustness_score" # Performance ratio: drift / canonical @@ -85,12 +87,19 @@ class MarksConfig: overlay_enabled: bool = True font_size: int = 12 - label_background: tuple[int, int, int, int] = (0, 0, 255, 200) # Blue, semi-transparent + label_background: tuple[int, int, int, int] = ( + 0, + 0, + 255, + 200, + ) # Blue, semi-transparent label_text_color: tuple[int, int, int] = (255, 255, 255) # White max_elements: int = 50 include_roles: list[str] | None = None # None = include all - exclude_roles: list[str] = field(default_factory=lambda: ["group", "generic", "static_text"]) + exclude_roles: list[str] = field( + default_factory=lambda: ["group", "generic", "static_text"] + ) @dataclass @@ -117,7 +126,9 @@ class ConditionConfig: marks: MarksConfig | None = None # Training config - loss_type: str = "mse" # "mse" for coordinate regression, "cross_entropy" for classification + loss_type: str = ( + "mse" # "mse" for coordinate regression, "cross_entropy" for classification + ) @classmethod def raw_coords(cls, **kwargs: Any) -> ConditionConfig: @@ -196,7 +207,12 @@ class DriftConfig: name: str drift_type: DriftType - params: ResolutionDriftParams | TranslationDriftParams | ThemeDriftParams | ScrollDriftParams + params: ( + ResolutionDriftParams + | TranslationDriftParams + | ThemeDriftParams + | ScrollDriftParams + ) is_canonical: bool = False @classmethod @@ -369,8 +385,6 @@ def validate(self) -> list[str]: if MetricName.GROUNDING_TOP1_ACCURACY in self.metrics: has_marks = any(c.name == ConditionName.MARKS for c in self.conditions) if not has_marks: - issues.append( - "GROUNDING_TOP1_ACCURACY metric requires MARKS condition" - ) + issues.append("GROUNDING_TOP1_ACCURACY metric requires MARKS condition") return issues diff --git a/openadapt_ml/experiments/representation_shootout/evaluator.py b/openadapt_ml/experiments/representation_shootout/evaluator.py index f286520..94a428d 100644 --- a/openadapt_ml/experiments/representation_shootout/evaluator.py +++ b/openadapt_ml/experiments/representation_shootout/evaluator.py @@ -138,19 +138,27 @@ def apply_drift( if drift_config.drift_type == DriftType.RESOLUTION: return DriftTransformer._apply_resolution_drift( - observation, ground_truth, drift_config.params # type: ignore + observation, + ground_truth, + drift_config.params, # type: ignore ) elif drift_config.drift_type == DriftType.TRANSLATION: return DriftTransformer._apply_translation_drift( - observation, ground_truth, drift_config.params # type: ignore + observation, + ground_truth, + drift_config.params, # type: ignore ) elif drift_config.drift_type == DriftType.THEME: return DriftTransformer._apply_theme_drift( - observation, ground_truth, drift_config.params # type: ignore + observation, + ground_truth, + drift_config.params, # type: ignore ) elif drift_config.drift_type == DriftType.SCROLL: return DriftTransformer._apply_scroll_drift( - observation, ground_truth, drift_config.params # type: ignore + observation, + ground_truth, + drift_config.params, # type: ignore ) else: logger.warning(f"Unknown drift type: {drift_config.drift_type}") @@ -372,7 +380,9 @@ def compute_metrics( if gt_el_id: pred_id = prediction.element_id.lower().replace("e", "") gt_id = str(gt_el_id).lower().replace("e", "") - metrics[MetricName.GROUNDING_TOP1_ACCURACY.value] = 1.0 if pred_id == gt_id else 0.0 + metrics[MetricName.GROUNDING_TOP1_ACCURACY.value] = ( + 1.0 if pred_id == gt_id else 0.0 + ) else: metrics[MetricName.GROUNDING_TOP1_ACCURACY.value] = 0.0 @@ -382,7 +392,9 @@ def compute_metrics( if gt_x is not None and gt_y is not None: if prediction.x is not None and prediction.y is not None: - distance = math.sqrt((prediction.x - gt_x) ** 2 + (prediction.y - gt_y) ** 2) + distance = math.sqrt( + (prediction.x - gt_x) ** 2 + (prediction.y - gt_y) ** 2 + ) else: # If prediction failed or is element-based, compute distance from element center if prediction.element_id and ui_elements: @@ -568,7 +580,9 @@ def compute_robustness_scores( robustness_scores[condition][r.drift] = 1.0 else: drift_value = r.metrics.get(primary_metric, 0) - robustness_scores[condition][r.drift] = drift_value / canonical_value + robustness_scores[condition][r.drift] = ( + drift_value / canonical_value + ) return robustness_scores @@ -622,7 +636,7 @@ def make_recommendation( if coords_cues_avg >= marks_avg - tolerance: recommended = "COORDINATES" reason = ( - f"Coords+Cues ({coords_cues_avg:.1%}) is within {tolerance*100}% of " + f"Coords+Cues ({coords_cues_avg:.1%}) is within {tolerance * 100}% of " f"Marks ({marks_avg:.1%}) under drift. Coordinates approach is simpler " "and doesn't require element detection pipeline." ) @@ -631,7 +645,7 @@ def make_recommendation( gap = marks_avg - coords_cues_avg reason = ( f"Marks ({marks_avg:.1%}) outperforms Coords+Cues ({coords_cues_avg:.1%}) " - f"by {gap:.1%} (>{tolerance*100}%) under drift. Element-based approach " + f"by {gap:.1%} (>{tolerance * 100}%) under drift. Element-based approach " "provides better robustness to UI changes." ) diff --git a/openadapt_ml/experiments/representation_shootout/runner.py b/openadapt_ml/experiments/representation_shootout/runner.py index db18f83..cc3647b 100644 --- a/openadapt_ml/experiments/representation_shootout/runner.py +++ b/openadapt_ml/experiments/representation_shootout/runner.py @@ -260,7 +260,9 @@ def get_model_predictions( # Scaffolding: Generate plausible mock predictions import random - random.seed(self.config.seed + hash(condition.name.value) + hash(drift_config.name)) + random.seed( + self.config.seed + hash(condition.name.value) + hash(drift_config.name) + ) predictions = [] for sample in samples: @@ -273,7 +275,9 @@ def get_model_predictions( if random.random() < error_rate: # Make an error - pick wrong element if sample.observation.ui_elements: - wrong_el = random.choice(sample.observation.ui_elements.elements) + wrong_el = random.choice( + sample.observation.ui_elements.elements + ) predictions.append(f"ACTION: CLICK([{wrong_el.element_id}])") else: predictions.append("ACTION: CLICK([e1])") @@ -486,7 +490,9 @@ def print_summary(self, recommendation: Recommendation) -> None: for r in results: hit_rate = r.metrics.get(MetricName.CLICK_HIT_RATE.value, 0) distance = r.metrics.get(MetricName.COORD_DISTANCE.value, 0) - print(f"{condition:<15} {r.drift:<25} {hit_rate:>10.1%} {distance:>10.4f}") + print( + f"{condition:<15} {r.drift:<25} {hit_rate:>10.1%} {distance:>10.4f}" + ) print() print("-" * 70) @@ -584,13 +590,16 @@ def main() -> int: help="Random seed for reproducibility", ) run_parser.add_argument( - "-v", "--verbose", + "-v", + "--verbose", action="store_true", help="Verbose output", ) # Recommend command (analyze existing results) - rec_parser = subparsers.add_parser("recommend", help="Generate recommendation from results") + rec_parser = subparsers.add_parser( + "recommend", help="Generate recommendation from results" + ) rec_parser.add_argument( "--results", required=True, @@ -635,6 +644,7 @@ def main() -> int: logger.error(f"Experiment failed: {e}") if args.verbose: import traceback + traceback.print_exc() return 1 diff --git a/openadapt_ml/experiments/waa_demo/runner.py b/openadapt_ml/experiments/waa_demo/runner.py index 27f1da9..77fa630 100644 --- a/openadapt_ml/experiments/waa_demo/runner.py +++ b/openadapt_ml/experiments/waa_demo/runner.py @@ -38,7 +38,11 @@ ) if TYPE_CHECKING: - from openadapt_ml.benchmarks.base import BenchmarkAction, BenchmarkObservation, BenchmarkTask + from openadapt_ml.benchmarks.base import ( + BenchmarkAction, + BenchmarkObservation, + BenchmarkTask, + ) logger = logging.getLogger(__name__) @@ -70,7 +74,9 @@ def cmd_list(args: argparse.Namespace) -> int: print() print("Tasks needing recorded demos on Windows:") for task in get_recorded_tasks(): - print(f" - #{list(TASKS.keys())[list(TASKS.values()).index(task)]}: {task.instruction}") + print( + f" - #{list(TASKS.keys())[list(TASKS.values()).index(task)]}: {task.instruction}" + ) return 0 @@ -119,7 +125,9 @@ def cmd_prompt(args: argparse.Namespace) -> int: else: print(f"Task: {task.instruction}") print() - print("Analyze the screenshot and provide the next action to complete this task.") + print( + "Analyze the screenshot and provide the next action to complete this task." + ) if demo and "[PLACEHOLDER" in demo: print() print("[Note: Demo not available - this would be zero-shot]") @@ -205,6 +213,7 @@ def _get_adapter(self): """Lazily initialize the API adapter.""" if self._adapter is None: from openadapt_ml.models.api_adapter import ApiVLMAdapter + self._adapter = ApiVLMAdapter( provider=self.provider, api_key=self.api_key, @@ -322,7 +331,9 @@ def _build_sample( history_str = self._format_history(history) content_parts.append(f"Previous actions:\n{history_str}") - content_parts.append("\nAnalyze the current screenshot and provide the next action.") + content_parts.append( + "\nAnalyze the current screenshot and provide the next action." + ) sample: dict[str, Any] = { "messages": [ @@ -454,7 +465,9 @@ def _parse_response( r"TYPE\s*\(\s*[\"'](.+?)[\"']\s*\)", action_line, re.IGNORECASE ) if type_match: - return BenchmarkAction(type="type", text=type_match.group(1), raw_action=raw_action) + return BenchmarkAction( + type="type", text=type_match.group(1), raw_action=raw_action + ) # Parse KEY key_match = re.match(r"KEY\s*\(\s*(.+?)\s*\)", action_line, re.IGNORECASE) @@ -503,7 +516,10 @@ def cmd_run(args: argparse.Namespace) -> int: WAAMockAdapter, compute_metrics, ) - from openadapt_ml.benchmarks.runner import EvaluationConfig, evaluate_agent_on_benchmark + from openadapt_ml.benchmarks.runner import ( + EvaluationConfig, + evaluate_agent_on_benchmark, + ) print("WAA Demo-Conditioned Experiment Runner") print("=" * 80) @@ -536,7 +552,7 @@ def cmd_run(args: argparse.Namespace) -> int: print(f"Running {len(task_ids)} tasks with complete demos") # Check for mock mode or real WAA - use_mock = getattr(args, 'mock', False) + use_mock = getattr(args, "mock", False) if use_mock: print("Using mock adapter (no Windows required)") @@ -596,7 +612,11 @@ def cmd_run(args: argparse.Namespace) -> int: except Exception as e: print(f"Error during evaluation: {e}") if "API key" in str(e) or "api_key" in str(e).lower(): - key_name = "ANTHROPIC_API_KEY" if args.provider == "anthropic" else "OPENAI_API_KEY" + key_name = ( + "ANTHROPIC_API_KEY" + if args.provider == "anthropic" + else "OPENAI_API_KEY" + ) print(f"\nMake sure {key_name} is set in your environment or .env file.") return 1 diff --git a/openadapt_ml/export/parquet.py b/openadapt_ml/export/parquet.py index 296ba67..095f0aa 100644 --- a/openadapt_ml/export/parquet.py +++ b/openadapt_ml/export/parquet.py @@ -72,7 +72,11 @@ def to_parquet( # Extract action type value (enum -> string) action_type = None if step.action: - action_type = step.action.type.value if hasattr(step.action.type, 'value') else step.action.type + action_type = ( + step.action.type.value + if hasattr(step.action.type, "value") + else step.action.type + ) row = { "episode_id": episode.episode_id, @@ -83,8 +87,12 @@ def to_parquet( "action_type": action_type, "x": x, "y": y, - "end_x": step.action.normalized_end[0] if step.action and step.action.normalized_end else None, - "end_y": step.action.normalized_end[1] if step.action and step.action.normalized_end else None, + "end_x": step.action.normalized_end[0] + if step.action and step.action.normalized_end + else None, + "end_y": step.action.normalized_end[1] + if step.action and step.action.normalized_end + else None, "text": getattr(step.action, "text", None) if step.action else None, "key": getattr(step.action, "key", None) if step.action else None, "scroll_direction": ( @@ -130,33 +138,37 @@ def _write_summary(episodes: list[Episode], output_path: str) -> None: for episode in episodes: first_t = episode.steps[0].timestamp if episode.steps else None last_t = episode.steps[-1].timestamp if episode.steps else None - duration = (last_t - first_t) if first_t is not None and last_t is not None else None + duration = ( + (last_t - first_t) if first_t is not None and last_t is not None else None + ) # Extract action type values (enum -> string) first_action_type = None last_action_type = None if episode.steps and episode.steps[0].action: t = episode.steps[0].action.type - first_action_type = t.value if hasattr(t, 'value') else t + first_action_type = t.value if hasattr(t, "value") else t if episode.steps and episode.steps[-1].action: t = episode.steps[-1].action.type - last_action_type = t.value if hasattr(t, 'value') else t - - summary_rows.append({ - "episode_id": episode.episode_id, - "instruction": episode.instruction, - "task_id": getattr(episode, "task_id", None), - "step_count": len(episode.steps), - "duration": duration, - "success": getattr(episode, "success", None), - "first_action_type": first_action_type, - "last_action_type": last_action_type, - "metadata": ( - json.dumps(episode.metadata) - if hasattr(episode, "metadata") and episode.metadata - else None - ), - }) + last_action_type = t.value if hasattr(t, "value") else t + + summary_rows.append( + { + "episode_id": episode.episode_id, + "instruction": episode.instruction, + "task_id": getattr(episode, "task_id", None), + "step_count": len(episode.steps), + "duration": duration, + "success": getattr(episode, "success", None), + "first_action_type": first_action_type, + "last_action_type": last_action_type, + "metadata": ( + json.dumps(episode.metadata) + if hasattr(episode, "metadata") and episode.metadata + else None + ), + } + ) summary_table = pa.Table.from_pylist(summary_rows) summary_path = str(output_path).replace(".parquet", "_summary.parquet") @@ -254,7 +266,8 @@ def from_parquet(parquet_path: str) -> list[Episode]: episode = Episode( episode_id=str(episode_id), - instruction=group.iloc[0].get("instruction") or group.iloc[0].get("goal", ""), + instruction=group.iloc[0].get("instruction") + or group.iloc[0].get("goal", ""), steps=steps, task_id=group.iloc[0].get("task_id"), metadata=metadata, diff --git a/openadapt_ml/grounding/detector.py b/openadapt_ml/grounding/detector.py index 8af6c31..8d957e2 100644 --- a/openadapt_ml/grounding/detector.py +++ b/openadapt_ml/grounding/detector.py @@ -104,7 +104,7 @@ def _parse_bbox_response( # Try to parse JSON from the response # Look for JSON array or object in the response - json_match = re.search(r'\[[\s\S]*\]|\{[\s\S]*\}', response_text) + json_match = re.search(r"\[[\s\S]*\]|\{[\s\S]*\}", response_text) if not json_match: return candidates @@ -340,11 +340,11 @@ def extract_ui_elements( response_text = response.text # Try to extract JSON array from response - json_match = re.search(r'\[[\s\S]*\]', response_text) + json_match = re.search(r"\[[\s\S]*\]", response_text) if not json_match: # Maybe it's just a plain array - if response_text.strip().startswith('['): - json_match = re.match(r'.*', response_text) + if response_text.strip().startswith("["): + json_match = re.match(r".*", response_text) else: return [] @@ -369,13 +369,18 @@ def extract_ui_elements( max(0, min(1, y2 / screenshot.height)), ] - normalized_elements.append({ - "id": elem.get("id", len(normalized_elements) + 1), - "label": elem.get("label", f"Element {elem.get('id', len(normalized_elements) + 1)}"), - "bbox": norm_bbox, - "type": elem.get("type", "other"), - "text": elem.get("text", ""), - }) + normalized_elements.append( + { + "id": elem.get("id", len(normalized_elements) + 1), + "label": elem.get( + "label", + f"Element {elem.get('id', len(normalized_elements) + 1)}", + ), + "bbox": norm_bbox, + "type": elem.get("type", "other"), + "text": elem.get("text", ""), + } + ) return normalized_elements @@ -549,8 +554,7 @@ def __init__( self._backend = GeminiGrounder(**kwargs) elif backend == "omniparser": raise NotImplementedError( - "OmniParser backend not yet implemented. " - "Use backend='gemini' for now." + "OmniParser backend not yet implemented. Use backend='gemini' for now." ) else: raise ValueError(f"Unknown backend: {backend}") diff --git a/openadapt_ml/ingest/__init__.py b/openadapt_ml/ingest/__init__.py index f11a8b2..3475f75 100644 --- a/openadapt_ml/ingest/__init__.py +++ b/openadapt_ml/ingest/__init__.py @@ -33,10 +33,12 @@ load_captures_as_sessions, ) - __all__.extend([ - "capture_to_episode", - "capture_to_session", - "load_captures_as_sessions", - ]) + __all__.extend( + [ + "capture_to_episode", + "capture_to_session", + "load_captures_as_sessions", + ] + ) except ImportError: pass diff --git a/openadapt_ml/ingest/capture.py b/openadapt_ml/ingest/capture.py index 5a4f4f3..55ce0af 100644 --- a/openadapt_ml/ingest/capture.py +++ b/openadapt_ml/ingest/capture.py @@ -134,7 +134,9 @@ def capture_to_episode( dir_name = capture_path.name if dir_name and dir_name != "capture": # Convert kebab-case/snake_case to readable text - instruction = dir_name.replace("-", " ").replace("_", " ").strip().capitalize() + instruction = ( + dir_name.replace("-", " ").replace("_", " ").strip().capitalize() + ) else: instruction = "Complete the recorded workflow" @@ -154,9 +156,7 @@ def capture_to_episode( screenshot_path = _save_screenshot(screenshot, output_dir, episode_id, idx) # Normalize coordinates - norm_coords = _normalize_coords( - action.x, action.y, screen_width, screen_height - ) + norm_coords = _normalize_coords(action.x, action.y, screen_width, screen_height) # Map event type to openadapt-ml ActionType event_type = action.type @@ -173,15 +173,15 @@ def capture_to_episode( if isinstance(action.event, MouseDragEvent): end_x = action.event.x + action.event.dx end_y = action.event.y + action.event.dy - norm_end = _normalize_coords( - end_x, end_y, screen_width, screen_height + norm_end = _normalize_coords(end_x, end_y, screen_width, screen_height) + ml_action = ml_action.model_copy( + update={ + "normalized_end": norm_end, + "raw": { + "button": action.event.button, + }, + } ) - ml_action = ml_action.model_copy(update={ - "normalized_end": norm_end, - "raw": { - "button": action.event.button, - }, - }) # Handle scroll events if isinstance(action.event, MouseScrollEvent): @@ -196,13 +196,15 @@ def capture_to_episode( elif action.event.dx < 0: scroll_direction = "left" - ml_action = ml_action.model_copy(update={ - "scroll_direction": scroll_direction, - "raw": { - "dx": action.event.dx, - "dy": action.event.dy, - }, - }) + ml_action = ml_action.model_copy( + update={ + "scroll_direction": scroll_direction, + "raw": { + "dx": action.event.dx, + "dy": action.event.dy, + }, + } + ) # Handle keyboard events - include key names for special keys if action.keys: @@ -226,7 +228,9 @@ def capture_to_episode( last_step = steps[-1] done_step = Step( step_index=len(steps), - observation=Observation(screenshot_path=last_step.observation.screenshot_path), + observation=Observation( + screenshot_path=last_step.observation.screenshot_path + ), action=Action(type=ActionType.DONE), reasoning="Workflow complete.", timestamp=(last_step.timestamp or 0) + 0.1, diff --git a/openadapt_ml/ingest/loader.py b/openadapt_ml/ingest/loader.py index 5662d6f..10cb959 100644 --- a/openadapt_ml/ingest/loader.py +++ b/openadapt_ml/ingest/loader.py @@ -142,7 +142,8 @@ def _dict_to_episode(data: Dict[str, Any], validate: bool = True) -> Episode: # Parse observation obs_data = step_data.get("observation", {}) observation = Observation( - screenshot_path=obs_data.get("screenshot_path") or obs_data.get("image_path"), + screenshot_path=obs_data.get("screenshot_path") + or obs_data.get("image_path"), raw=obs_data.get("raw") or obs_data.get("meta"), a11y_tree=obs_data.get("a11y_tree") or obs_data.get("accessibility_tree"), dom=obs_data.get("dom") or obs_data.get("dom_html"), @@ -168,7 +169,10 @@ def _dict_to_episode(data: Dict[str, Any], validate: bool = True) -> Episode: normalized_end = None if action_data.get("normalized_end"): normalized_end = tuple(action_data["normalized_end"]) - elif action_data.get("end_x") is not None and action_data.get("end_y") is not None: + elif ( + action_data.get("end_x") is not None + and action_data.get("end_y") is not None + ): normalized_end = (action_data["end_x"], action_data["end_y"]) action = Action( diff --git a/openadapt_ml/ingest/synthetic.py b/openadapt_ml/ingest/synthetic.py index 4a95d77..79da971 100644 --- a/openadapt_ml/ingest/synthetic.py +++ b/openadapt_ml/ingest/synthetic.py @@ -32,7 +32,9 @@ def _normalize(x_px: int, y_px: int) -> Tuple[float, float]: return x_px / IMG_WIDTH, y_px / IMG_HEIGHT -def _text_size(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> Tuple[int, int]: +def _text_size( + draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont +) -> Tuple[int, int]: """Compute text width/height using textbbox for Pillow compatibility.""" left, top, right, bottom = draw.textbbox((0, 0), text, font=font) @@ -313,7 +315,9 @@ def _center(bounds: Tuple[int, int, int, int]) -> Tuple[float, float]: return _normalize(cx, cy) -def _bbox_normalized(bounds: Tuple[int, int, int, int]) -> Tuple[float, float, float, float]: +def _bbox_normalized( + bounds: Tuple[int, int, int, int], +) -> Tuple[float, float, float, float]: """Convert pixel bounds (x, y, w, h) to normalized bbox (x_min, y_min, x_max, y_max).""" x, y, w, h = bounds x_min = x / IMG_WIDTH @@ -426,7 +430,9 @@ def _script_login_episode( # Step 4: password typed -> click login button cx_btn, cy_btn = _center(layout.login_button) - img4, _ = _draw_login_screen(username=username, password=password, layout=layout, jitter=False) + img4, _ = _draw_login_screen( + username=username, password=password, layout=layout, jitter=False + ) img4_path = root / f"{episode_id}_step_4.png" _save_image(img4, img4_path) obs4 = Observation(screenshot_path=str(img4_path)) @@ -670,7 +676,9 @@ class RegistrationUIElements: SOM_REGISTER_BUTTON = 6 -def _compute_registration_layout(max_offset: int = 8, jitter: bool = True) -> RegistrationUIElements: +def _compute_registration_layout( + max_offset: int = 8, jitter: bool = True +) -> RegistrationUIElements: """Compute registration form layout with optional jitter.""" label_x = 180 @@ -683,7 +691,9 @@ def _maybe_jitter(x: int, y: int) -> tuple[int, int]: return x, y dx = random.randint(-max_offset, max_offset) dy = random.randint(-max_offset, max_offset) - return max(20, min(IMG_WIDTH - box_w - 20, x + dx)), max(20, min(IMG_HEIGHT - 60, y + dy)) + return max(20, min(IMG_WIDTH - box_w - 20, x + dx)), max( + 20, min(IMG_HEIGHT - 60, y + dy) + ) # First name fn_x, fn_y = _maybe_jitter(label_x, start_y + 24) @@ -707,7 +717,9 @@ def _maybe_jitter(x: int, y: int) -> tuple[int, int]: # Register button btn_w, btn_h = 160, 45 - btn_x, btn_y = _maybe_jitter((IMG_WIDTH - btn_w) // 2, start_y + 5 * field_spacing + 40) + btn_x, btn_y = _maybe_jitter( + (IMG_WIDTH - btn_w) // 2, start_y + 5 * field_spacing + 40 + ) register_button = (btn_x, btn_y, btn_w, btn_h) return RegistrationUIElements( @@ -766,19 +778,37 @@ def _draw_registration_screen( # Register button btn_x, btn_y, btn_w, btn_h = layout.register_button - draw.rectangle([(btn_x, btn_y), (btn_x + btn_w, btn_y + btn_h)], outline="black", fill="darkblue") + draw.rectangle( + [(btn_x, btn_y), (btn_x + btn_w, btn_y + btn_h)], + outline="black", + fill="darkblue", + ) btn_text = "Register" btw, bth = _text_size(draw, btn_text, FONT) - draw.text((btn_x + (btn_w - btw) // 2, btn_y + (btn_h - bth) // 2), btn_text, fill="white", font=FONT) + draw.text( + (btn_x + (btn_w - btw) // 2, btn_y + (btn_h - bth) // 2), + btn_text, + fill="white", + font=FONT, + ) # Decoy "Clear Form" button decoy_w, decoy_h = 100, 35 decoy_x = IMG_WIDTH - decoy_w - 30 decoy_y = btn_y + 5 - draw.rectangle([(decoy_x, decoy_y), (decoy_x + decoy_w, decoy_y + decoy_h)], outline="gray", fill=(200, 200, 200)) + draw.rectangle( + [(decoy_x, decoy_y), (decoy_x + decoy_w, decoy_y + decoy_h)], + outline="gray", + fill=(200, 200, 200), + ) decoy_text = "Clear" dtw, dth = _text_size(draw, decoy_text, FONT) - draw.text((decoy_x + (decoy_w - dtw) // 2, decoy_y + (decoy_h - dth) // 2), decoy_text, fill="gray", font=FONT) + draw.text( + (decoy_x + (decoy_w - dtw) // 2, decoy_y + (decoy_h - dth) // 2), + decoy_text, + fill="gray", + font=FONT, + ) return img, layout @@ -789,10 +819,17 @@ def _draw_registration_success_screen(first_name: str, email: str) -> Image.Imag draw = ImageDraw.Draw(img) text = f"Welcome, {first_name}!" tw, th = _text_size(draw, text, FONT_TITLE) - draw.text(((IMG_WIDTH - tw) // 2, IMG_HEIGHT // 2 - 40), text, fill="darkgreen", font=FONT_TITLE) + draw.text( + ((IMG_WIDTH - tw) // 2, IMG_HEIGHT // 2 - 40), + text, + fill="darkgreen", + font=FONT_TITLE, + ) subtext = f"Confirmation sent to {email}" stw, sth = _text_size(draw, subtext, FONT) - draw.text(((IMG_WIDTH - stw) // 2, IMG_HEIGHT // 2 + 20), subtext, fill="gray", font=FONT) + draw.text( + ((IMG_WIDTH - stw) // 2, IMG_HEIGHT // 2 + 20), subtext, fill="gray", font=FONT + ) return img @@ -830,10 +867,21 @@ def _script_registration_episode( ("last_name", layout.last_name_box, last_name, SOM_LAST_NAME_FIELD), ("email", layout.email_box, email, SOM_EMAIL_FIELD), ("password", layout.password_box, password, SOM_REG_PASSWORD_FIELD), - ("confirm_password", layout.confirm_password_box, password, SOM_CONFIRM_PASSWORD_FIELD), + ( + "confirm_password", + layout.confirm_password_box, + password, + SOM_CONFIRM_PASSWORD_FIELD, + ), ] - current_values = {"first_name": "", "last_name": "", "email": "", "password": "", "confirm_password": ""} + current_values = { + "first_name": "", + "last_name": "", + "email": "", + "password": "", + "confirm_password": "", + } step_idx = 0 for field_name, box, value, elem_idx in field_sequence: @@ -851,17 +899,19 @@ def _script_registration_episode( ) img_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img, img_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img_path)), - action=Action( - type=ActionType.CLICK, - normalized_coordinates=(cx, cy), - raw={"bbox": bbox, "element_index": elem_idx}, - ), - reasoning=f"Focus the {field_name.replace('_', ' ')} field.", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img_path)), + action=Action( + type=ActionType.CLICK, + normalized_coordinates=(cx, cy), + raw={"bbox": bbox, "element_index": elem_idx}, + ), + reasoning=f"Focus the {field_name.replace('_', ' ')} field.", + ) + ) step_idx += 1 # Type step @@ -876,17 +926,19 @@ def _script_registration_episode( ) img2_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img2, img2_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img2_path)), - action=Action( - type=ActionType.TYPE, - text=value, - raw={"element_index": elem_idx}, - ), - reasoning=f"Type the {field_name.replace('_', ' ')}.", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img2_path)), + action=Action( + type=ActionType.TYPE, + text=value, + raw={"element_index": elem_idx}, + ), + reasoning=f"Type the {field_name.replace('_', ' ')}.", + ) + ) current_values[field_name] = value step_idx += 1 @@ -904,30 +956,34 @@ def _script_registration_episode( ) img_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img, img_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img_path)), - action=Action( - type=ActionType.CLICK, - normalized_coordinates=(cx, cy), - raw={"bbox": bbox, "element_index": SOM_REGISTER_BUTTON}, - ), - reasoning="Submit the registration form.", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img_path)), + action=Action( + type=ActionType.CLICK, + normalized_coordinates=(cx, cy), + raw={"bbox": bbox, "element_index": SOM_REGISTER_BUTTON}, + ), + reasoning="Submit the registration form.", + ) + ) step_idx += 1 # Done step img_done = _draw_registration_success_screen(first_name, email) img_done_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img_done, img_done_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img_done_path)), - action=Action(type=ActionType.DONE), - reasoning="Registration successful; workflow complete.", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img_done_path)), + action=Action(type=ActionType.DONE), + reasoning="Registration successful; workflow complete.", + ) + ) return Episode( episode_id=episode_id, @@ -968,10 +1024,21 @@ def _script_registration_episode_som( ("last_name", layout.last_name_box, last_name, SOM_LAST_NAME_FIELD), ("email", layout.email_box, email, SOM_EMAIL_FIELD), ("password", layout.password_box, password, SOM_REG_PASSWORD_FIELD), - ("confirm_password", layout.confirm_password_box, password, SOM_CONFIRM_PASSWORD_FIELD), + ( + "confirm_password", + layout.confirm_password_box, + password, + SOM_CONFIRM_PASSWORD_FIELD, + ), ] - current_values = {"first_name": "", "last_name": "", "email": "", "password": "", "confirm_password": ""} + current_values = { + "first_name": "", + "last_name": "", + "email": "", + "password": "", + "confirm_password": "", + } step_idx = 0 for field_name, box, value, elem_idx in field_sequence: @@ -990,17 +1057,19 @@ def _script_registration_episode_som( img_som = _overlay_som_marks(img, som_elements) img_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img_som, img_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img_path)), - action=Action( - type=ActionType.CLICK, - normalized_coordinates=(cx, cy), - raw={"bbox": bbox, "element_index": elem_idx}, - ), - reasoning=f"Focus element [{elem_idx}] ({field_name.replace('_', ' ')} field).", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img_path)), + action=Action( + type=ActionType.CLICK, + normalized_coordinates=(cx, cy), + raw={"bbox": bbox, "element_index": elem_idx}, + ), + reasoning=f"Focus element [{elem_idx}] ({field_name.replace('_', ' ')} field).", + ) + ) step_idx += 1 # Type step @@ -1016,17 +1085,19 @@ def _script_registration_episode_som( img2_som = _overlay_som_marks(img2, som_elements) img2_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img2_som, img2_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img2_path)), - action=Action( - type=ActionType.TYPE, - text=value, - raw={"element_index": elem_idx}, - ), - reasoning=f"Type into element [{elem_idx}].", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img2_path)), + action=Action( + type=ActionType.TYPE, + text=value, + raw={"element_index": elem_idx}, + ), + reasoning=f"Type into element [{elem_idx}].", + ) + ) current_values[field_name] = value step_idx += 1 @@ -1045,30 +1116,34 @@ def _script_registration_episode_som( img_som = _overlay_som_marks(img, som_elements) img_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img_som, img_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img_path)), - action=Action( - type=ActionType.CLICK, - normalized_coordinates=(cx, cy), - raw={"bbox": bbox, "element_index": SOM_REGISTER_BUTTON}, - ), - reasoning=f"Click element [{SOM_REGISTER_BUTTON}] to submit registration.", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img_path)), + action=Action( + type=ActionType.CLICK, + normalized_coordinates=(cx, cy), + raw={"bbox": bbox, "element_index": SOM_REGISTER_BUTTON}, + ), + reasoning=f"Click element [{SOM_REGISTER_BUTTON}] to submit registration.", + ) + ) step_idx += 1 # Done step img_done = _draw_registration_success_screen(first_name, email) img_done_path = root / f"{episode_id}_step_{step_idx}.png" _save_image(img_done, img_done_path) - steps.append(Step( - step_index=step_idx, - timestamp=float(step_idx), - observation=Observation(screenshot_path=str(img_done_path)), - action=Action(type=ActionType.DONE), - reasoning="Registration successful; workflow complete.", - )) + steps.append( + Step( + step_index=step_idx, + timestamp=float(step_idx), + observation=Observation(screenshot_path=str(img_done_path)), + action=Action(type=ActionType.DONE), + reasoning="Registration successful; workflow complete.", + ) + ) return Episode( episode_id=episode_id, @@ -1149,15 +1224,29 @@ def generate_synthetic_episodes( if use_som: episode = _script_registration_episode_som( - episode_dir, episode_id_full, first_name, last_name, email, password, jitter=jitter + episode_dir, + episode_id_full, + first_name, + last_name, + email, + password, + jitter=jitter, ) else: episode = _script_registration_episode( - episode_dir, episode_id_full, first_name, last_name, email, password, jitter=jitter + episode_dir, + episode_id_full, + first_name, + last_name, + email, + password, + jitter=jitter, ) else: - raise ValueError(f"Unknown scenario: {scenario}. Options: login, registration") + raise ValueError( + f"Unknown scenario: {scenario}. Options: login, registration" + ) episodes.append(episode) diff --git a/openadapt_ml/models/api_adapter.py b/openadapt_ml/models/api_adapter.py index 3fa85d8..569f7b2 100644 --- a/openadapt_ml/models/api_adapter.py +++ b/openadapt_ml/models/api_adapter.py @@ -50,7 +50,9 @@ def __init__( "Install with `uv sync --extra api`." ) from exc - key = api_key or settings.anthropic_api_key or os.getenv("ANTHROPIC_API_KEY") + key = ( + api_key or settings.anthropic_api_key or os.getenv("ANTHROPIC_API_KEY") + ) if not key: raise RuntimeError( "ANTHROPIC_API_KEY is required but not found. " @@ -87,10 +89,14 @@ def __init__( super().__init__(model=model, processor=processor, device=device) def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type: ignore[override] - raise NotImplementedError("ApiVLMAdapter does not support training (prepare_inputs)") + raise NotImplementedError( + "ApiVLMAdapter does not support training (prepare_inputs)" + ) def compute_loss(self, inputs: Dict[str, Any]) -> torch.Tensor: # type: ignore[override] - raise NotImplementedError("ApiVLMAdapter does not support training (compute_loss)") + raise NotImplementedError( + "ApiVLMAdapter does not support training (compute_loss)" + ) def generate(self, sample: Dict[str, Any], max_new_tokens: int = 64) -> str: # type: ignore[override] images = sample.get("images", []) @@ -138,7 +144,11 @@ def generate(self, sample: Dict[str, Any], max_new_tokens: int = 64) -> str: # # Anthropic messages API returns a list of content blocks. parts = getattr(resp, "content", []) - texts = [getattr(p, "text", "") for p in parts if getattr(p, "type", "") == "text"] + texts = [ + getattr(p, "text", "") + for p in parts + if getattr(p, "type", "") == "text" + ] return "\n".join([t for t in texts if t]).strip() if self.provider == "openai": diff --git a/openadapt_ml/models/base_adapter.py b/openadapt_ml/models/base_adapter.py index 5412d5f..12d98e4 100644 --- a/openadapt_ml/models/base_adapter.py +++ b/openadapt_ml/models/base_adapter.py @@ -14,7 +14,10 @@ def get_default_device() -> torch.device: if torch.cuda.is_available(): return torch.device("cuda") - if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available(): # type: ignore[attr-defined] + if ( + getattr(torch.backends, "mps", None) is not None + and torch.backends.mps.is_available() + ): # type: ignore[attr-defined] return torch.device("mps") return torch.device("cpu") @@ -28,7 +31,12 @@ class BaseVLMAdapter(ABC): - generating assistant text given a single sample at inference time """ - def __init__(self, model: torch.nn.Module, processor: Any, device: Optional[torch.device] = None) -> None: + def __init__( + self, + model: torch.nn.Module, + processor: Any, + device: Optional[torch.device] = None, + ) -> None: self.model = model self.processor = processor self.device = device or get_default_device() diff --git a/openadapt_ml/models/providers/__init__.py b/openadapt_ml/models/providers/__init__.py index c53bfda..ee4bec8 100644 --- a/openadapt_ml/models/providers/__init__.py +++ b/openadapt_ml/models/providers/__init__.py @@ -131,9 +131,7 @@ def get_provider(provider_name: str) -> BaseAPIProvider: provider_class = PROVIDERS.get(provider_name.lower()) if provider_class is None: available = ", ".join(PROVIDERS.keys()) - raise ValueError( - f"Unknown provider: {provider_name}. Available: {available}" - ) + raise ValueError(f"Unknown provider: {provider_name}. Available: {available}") return provider_class() diff --git a/openadapt_ml/models/providers/anthropic.py b/openadapt_ml/models/providers/anthropic.py index fef9690..f948a43 100644 --- a/openadapt_ml/models/providers/anthropic.py +++ b/openadapt_ml/models/providers/anthropic.py @@ -27,10 +27,16 @@ # Supported models with their context windows SUPPORTED_MODELS = { - "claude-opus-4-5-20251101": {"context": 200_000, "description": "SOTA computer use"}, + "claude-opus-4-5-20251101": { + "context": 200_000, + "description": "SOTA computer use", + }, "claude-sonnet-4-5-20250929": {"context": 200_000, "description": "Fast, cheaper"}, "claude-sonnet-4-20250514": {"context": 200_000, "description": "Previous Sonnet"}, - "claude-haiku-3-5-20241022": {"context": 200_000, "description": "Fastest, cheapest"}, + "claude-haiku-3-5-20241022": { + "context": 200_000, + "description": "Fastest, cheapest", + }, } @@ -168,7 +174,9 @@ def send_message( # Map common errors to specific exceptions if "authentication" in error_str or "api_key" in error_str: - raise AuthenticationError(f"Anthropic authentication failed: {e}") from e + raise AuthenticationError( + f"Anthropic authentication failed: {e}" + ) from e elif "rate_limit" in error_str or "429" in error_str: raise RateLimitError(f"Anthropic rate limit exceeded: {e}") from e elif "model_not_found" in error_str or "not found" in error_str: diff --git a/openadapt_ml/models/providers/google.py b/openadapt_ml/models/providers/google.py index 773dc27..6827ebb 100644 --- a/openadapt_ml/models/providers/google.py +++ b/openadapt_ml/models/providers/google.py @@ -192,7 +192,11 @@ def send_message( error_str = str(e).lower() # Map common errors to specific exceptions - if "api_key" in error_str or "authentication" in error_str or "invalid" in error_str: + if ( + "api_key" in error_str + or "authentication" in error_str + or "invalid" in error_str + ): raise AuthenticationError(f"Google authentication failed: {e}") from e elif "quota" in error_str or "rate" in error_str or "429" in error_str: raise RateLimitError(f"Google rate limit/quota exceeded: {e}") from e @@ -354,7 +358,7 @@ def send_with_grounding( import json import re - json_match = re.search(r'\{[\s\S]*\}', text) + json_match = re.search(r"\{[\s\S]*\}", text) if json_match: try: data = json.loads(json_match.group()) diff --git a/openadapt_ml/models/providers/openai.py b/openadapt_ml/models/providers/openai.py index d54eeef..a91f5d0 100644 --- a/openadapt_ml/models/providers/openai.py +++ b/openadapt_ml/models/providers/openai.py @@ -171,7 +171,11 @@ def send_message( error_str = str(e).lower() # Map common errors to specific exceptions - if "authentication" in error_str or "api_key" in error_str or "invalid_api_key" in error_str: + if ( + "authentication" in error_str + or "api_key" in error_str + or "invalid_api_key" in error_str + ): raise AuthenticationError(f"OpenAI authentication failed: {e}") from e elif "rate_limit" in error_str or "429" in error_str: raise RateLimitError(f"OpenAI rate limit exceeded: {e}") from e diff --git a/openadapt_ml/models/qwen_vl.py b/openadapt_ml/models/qwen_vl.py index 044a575..a6e7641 100644 --- a/openadapt_ml/models/qwen_vl.py +++ b/openadapt_ml/models/qwen_vl.py @@ -4,12 +4,18 @@ import torch from peft import LoraConfig, PeftModel, get_peft_model -from transformers import AutoProcessor, Qwen3VLForConditionalGeneration, Qwen2_5_VLForConditionalGeneration +from transformers import ( + AutoProcessor, + Qwen3VLForConditionalGeneration, + Qwen2_5_VLForConditionalGeneration, +) from openadapt_ml.models.base_adapter import BaseVLMAdapter, get_default_device -def _process_vision_info(messages: List[Dict[str, Any]]) -> tuple[list[list[Any]], list[list[Any]]]: +def _process_vision_info( + messages: List[Dict[str, Any]], +) -> tuple[list[list[Any]], list[list[Any]]]: """Minimal stand-in for qwen_vl_utils.process_vision_info. For our use case we only need to extract image/video entries from the @@ -99,10 +105,12 @@ def from_pretrained( processor = AutoProcessor.from_pretrained(model_name) # Configure image resolution for faster training - if max_pixels is not None and hasattr(processor, 'image_processor'): + if max_pixels is not None and hasattr(processor, "image_processor"): processor.image_processor.max_pixels = max_pixels - print(f"Set max_pixels to {max_pixels} ({int(max_pixels**0.5)}x{int(max_pixels**0.5)} approx)") - if min_pixels is not None and hasattr(processor, 'image_processor'): + print( + f"Set max_pixels to {max_pixels} ({int(max_pixels**0.5)}x{int(max_pixels**0.5)} approx)" + ) + if min_pixels is not None and hasattr(processor, "image_processor"): processor.image_processor.min_pixels = min_pixels model_kwargs: Dict[str, Any] = {} @@ -120,7 +128,9 @@ def from_pretrained( if lora_config is not None: if isinstance(lora_config, dict): lora_weights_path = lora_config.get("weights_path") - lora_cfg_clean = {k: v for k, v in lora_config.items() if k != "weights_path"} + lora_cfg_clean = { + k: v for k, v in lora_config.items() if k != "weights_path" + } else: lora_cfg_clean = lora_config @@ -183,10 +193,12 @@ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type }, ] if assistant_text: - qwen_messages_full.append({ - "role": "assistant", - "content": [{"type": "text", "text": assistant_text}], - }) + qwen_messages_full.append( + { + "role": "assistant", + "content": [{"type": "text", "text": assistant_text}], + } + ) batch_messages_full.append(qwen_messages_full) # User-only messages (for label masking) @@ -249,7 +261,11 @@ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type # Padding token is typically 0 or a special value # For Qwen, we look for the first occurrence of pad token pad_token_id = self.processor.tokenizer.pad_token_id - user_ids_no_pad = user_ids[user_ids != pad_token_id] if pad_token_id is not None else user_ids + user_ids_no_pad = ( + user_ids[user_ids != pad_token_id] + if pad_token_id is not None + else user_ids + ) user_len = len(user_ids_no_pad) # Check if user sequence is a prefix of full sequence @@ -260,7 +276,10 @@ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type labels[i, user_len:] = full_ids[user_len:] # Ensure padding tokens are masked in labels - if hasattr(self.processor.tokenizer, 'pad_token_id') and self.processor.tokenizer.pad_token_id is not None: + if ( + hasattr(self.processor.tokenizer, "pad_token_id") + and self.processor.tokenizer.pad_token_id is not None + ): labels[input_ids_full == self.processor.tokenizer.pad_token_id] = -100 inputs_full["labels"] = labels @@ -299,10 +318,12 @@ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type } ] if assistant_text: - qwen_messages.append({ - "role": "assistant", - "content": [{"type": "text", "text": assistant_text}], - }) + qwen_messages.append( + { + "role": "assistant", + "content": [{"type": "text", "text": assistant_text}], + } + ) batch_messages.append(qwen_messages) @@ -338,14 +359,20 @@ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type labels = input_ids.clone() # Mask padding tokens - if hasattr(self.processor.tokenizer, 'pad_token_id') and self.processor.tokenizer.pad_token_id is not None: + if ( + hasattr(self.processor.tokenizer, "pad_token_id") + and self.processor.tokenizer.pad_token_id is not None + ): labels[input_ids == self.processor.tokenizer.pad_token_id] = -100 inputs["labels"] = labels return inputs def compute_loss(self, inputs: Dict[str, Any]) -> torch.Tensor: # type: ignore[override] - inputs = {k: v.to(self.device) if isinstance(v, torch.Tensor) else v for k, v in inputs.items()} + inputs = { + k: v.to(self.device) if isinstance(v, torch.Tensor) else v + for k, v in inputs.items() + } outputs = self.model(**inputs) # Hugging Face causal LM models return `loss` when `labels` are provided. return outputs.loss # type: ignore[no-any-return] @@ -419,6 +446,7 @@ def generate(self, sample: Dict[str, Any], max_new_tokens: int = 64) -> str: # def save_checkpoint(self, path: str) -> None: """Save the LoRA adapter weights to a directory.""" from pathlib import Path + save_path = Path(path) save_path.mkdir(parents=True, exist_ok=True) # Save the PEFT adapter (LoRA weights only, not base model) diff --git a/openadapt_ml/retrieval/demo_retriever.py b/openadapt_ml/retrieval/demo_retriever.py index 6714f16..b543731 100644 --- a/openadapt_ml/retrieval/demo_retriever.py +++ b/openadapt_ml/retrieval/demo_retriever.py @@ -205,11 +205,15 @@ def add_demo( platform = self._detect_platform(episode, app_name, domain) # Extract action types - action_types = list(set( - step.action.type.value if hasattr(step.action.type, 'value') else str(step.action.type) - for step in episode.steps - if step.action - )) + action_types = list( + set( + step.action.type.value + if hasattr(step.action.type, "value") + else str(step.action.type) + for step in episode.steps + if step.action + ) + ) # Extract key elements key_elements = self._extract_key_elements(episode) @@ -296,9 +300,13 @@ def build_index(self, force: bool = False) -> None: return if not self._demos: - raise ValueError("Cannot build index: no demos added. Use add_demo() first.") + raise ValueError( + "Cannot build index: no demos added. Use add_demo() first." + ) - logger.info(f"Building index for {len(self._demos)} demos using {self.embedding_method}...") + logger.info( + f"Building index for {len(self._demos)} demos using {self.embedding_method}..." + ) # Initialize embedder if needed if self._embedder is None: @@ -357,16 +365,21 @@ def save_index(self, path: Union[str, Path]) -> None: } with open(path / "index.json", "w") as f: - json.dump({ - "embedding_method": self.embedding_method, - "embedding_model": self.embedding_model, - "demos": metadata, - "embedder_state": embedder_state, - }, f, indent=2) + json.dump( + { + "embedding_method": self.embedding_method, + "embedding_model": self.embedding_model, + "demos": metadata, + "embedder_state": embedder_state, + }, + f, + indent=2, + ) # Save embeddings as numpy array try: import numpy as np + if self._embeddings_matrix is not None: np.save(path / "embeddings.npy", self._embeddings_matrix) except ImportError: @@ -374,7 +387,11 @@ def save_index(self, path: Union[str, Path]) -> None: logger.info(f"Index saved to {path}") - def load_index(self, path: Union[str, Path], episode_loader: Optional[Callable[[str], Episode]] = None) -> None: + def load_index( + self, + path: Union[str, Path], + episode_loader: Optional[Callable[[str], Episode]] = None, + ) -> None: """Load index from disk. Args: @@ -394,6 +411,7 @@ def load_index(self, path: Union[str, Path], episode_loader: Optional[Callable[[ embeddings = None try: import numpy as np + embeddings_path = path / "embeddings.npy" if embeddings_path.exists(): embeddings = np.load(embeddings_path) @@ -408,11 +426,14 @@ def load_index(self, path: Union[str, Path], episode_loader: Optional[Callable[[ try: episode = episode_loader(meta["file_path"]) except Exception as e: - logger.warning(f"Failed to load episode from {meta['file_path']}: {e}") + logger.warning( + f"Failed to load episode from {meta['file_path']}: {e}" + ) # Create placeholder episode if not loaded if episode is None: from openadapt_ml.schema import Action, ActionType, Observation, Step + episode = Episode( episode_id=meta["demo_id"], instruction=meta["goal"], @@ -453,6 +474,7 @@ def load_index(self, path: Union[str, Path], episode_loader: Optional[Callable[[ embedder_state = data.get("embedder_state", {}) if embedder_state and self.embedding_method == "tfidf": from openadapt_ml.retrieval.embeddings import TFIDFEmbedder + self._embedder = TFIDFEmbedder() self._embedder.vocab = embedder_state.get("vocab", []) self._embedder.vocab_to_idx = embedder_state.get("vocab_to_idx", {}) @@ -511,12 +533,14 @@ def retrieve( bonus = self._compute_context_bonus(demo, app_context, domain_context) total_score = text_score + bonus - results.append(RetrievalResult( - demo=demo, - score=total_score, - text_score=text_score, - domain_bonus=bonus, - )) + results.append( + RetrievalResult( + demo=demo, + score=total_score, + text_score=text_score, + domain_bonus=bonus, + ) + ) # Sort by score (descending) results.sort(key=lambda r: r.score, reverse=True) @@ -617,6 +641,7 @@ def format_for_prompt( def _format_action_minimal(self, action: Any) -> str: """Format action as minimal string.""" from openadapt_ml.experiments.demo_prompt.format_demo import format_action + return format_action(action) # ========================================================================= @@ -627,10 +652,12 @@ def _init_embedder(self) -> None: """Initialize the embedding backend.""" if self.embedding_method == "tfidf": from openadapt_ml.retrieval.embeddings import TFIDFEmbedder + self._embedder = TFIDFEmbedder() elif self.embedding_method == "sentence_transformers": from openadapt_ml.retrieval.embeddings import SentenceTransformerEmbedder + self._embedder = SentenceTransformerEmbedder( model_name=self.embedding_model, cache_dir=self.cache_dir / "st_cache", @@ -638,6 +665,7 @@ def _init_embedder(self) -> None: elif self.embedding_method == "openai": from openadapt_ml.retrieval.embeddings import OpenAIEmbedder + self._embedder = OpenAIEmbedder( model_name=self.embedding_model, cache_dir=self.cache_dir / "openai_cache", @@ -738,8 +766,7 @@ def _get_candidates( if filter_tags: filter_tags_set = set(filter_tags) candidates = [ - d for d in candidates - if filter_tags_set.issubset(set(d.tags)) + d for d in candidates if filter_tags_set.issubset(set(d.tags)) ] return candidates diff --git a/openadapt_ml/retrieval/embeddings.py b/openadapt_ml/retrieval/embeddings.py index 5db6488..e35f1a5 100644 --- a/openadapt_ml/retrieval/embeddings.py +++ b/openadapt_ml/retrieval/embeddings.py @@ -121,7 +121,7 @@ def _tokenize(self, text: str) -> List[str]: Returns: List of tokens. """ - tokens = re.findall(r'\b\w+\b', text.lower()) + tokens = re.findall(r"\b\w+\b", text.lower()) return tokens def _compute_tf(self, tokens: List[str]) -> Dict[str, float]: @@ -169,8 +169,7 @@ def fit(self, documents: List[str]) -> "TFIDFEmbedder": # Compute IDF: log(N / df) + 1 n_docs = max(len(documents), 1) self.idf = { - term: log(n_docs / doc_freq.get(term, 1)) + 1 - for term in self.vocab + term: log(n_docs / doc_freq.get(term, 1)) + 1 for term in self.vocab } self._is_fitted = True @@ -440,6 +439,7 @@ def _load_disk_cache(self) -> None: cached = json.load(f) # Convert lists back to arrays import numpy as np + for key, val in cached.items(): self._embedding_cache[key] = np.array(val, dtype=np.float32) logger.debug(f"Loaded {len(self._embedding_cache)} cached embeddings") @@ -455,8 +455,7 @@ def _save_disk_cache(self) -> None: try: # Convert arrays to lists for JSON cache_data = { - key: val.tolist() - for key, val in self._embedding_cache.items() + key: val.tolist() for key, val in self._embedding_cache.items() } with open(cache_file, "w") as f: json.dump(cache_data, f) @@ -525,7 +524,9 @@ def embed_batch(self, texts: List[str]) -> Any: # Process in batches for batch_start in range(0, len(uncached_texts), self.batch_size): - batch_texts = uncached_texts[batch_start:batch_start + self.batch_size] + batch_texts = uncached_texts[ + batch_start : batch_start + self.batch_size + ] try: response = client.embeddings.create( diff --git a/openadapt_ml/retrieval/retriever.py b/openadapt_ml/retrieval/retriever.py index 2e6bfb3..110b330 100644 --- a/openadapt_ml/retrieval/retriever.py +++ b/openadapt_ml/retrieval/retriever.py @@ -49,7 +49,9 @@ def __init__( if index.is_empty(): raise ValueError("Cannot create retriever from empty index") if not index.is_fitted(): - raise ValueError("Index must be built before retrieval (call index.build())") + raise ValueError( + "Index must be built before retrieval (call index.build())" + ) self.index = index self.domain_bonus = domain_bonus diff --git a/openadapt_ml/runtime/__init__.py b/openadapt_ml/runtime/__init__.py index 063590e..f6e1160 100644 --- a/openadapt_ml/runtime/__init__.py +++ b/openadapt_ml/runtime/__init__.py @@ -25,7 +25,11 @@ log_blocked(assessment.reason) """ -from openadapt_ml.runtime.policy import AgentPolicy, PolicyOutput, parse_thought_state_action +from openadapt_ml.runtime.policy import ( + AgentPolicy, + PolicyOutput, + parse_thought_state_action, +) from openadapt_ml.runtime.safety_gate import ( SafetyAssessment, SafetyConfig, diff --git a/openadapt_ml/runtime/policy.py b/openadapt_ml/runtime/policy.py index af46efd..869f40c 100644 --- a/openadapt_ml/runtime/policy.py +++ b/openadapt_ml/runtime/policy.py @@ -26,13 +26,16 @@ @dataclass class PolicyOutput: """Result of a single policy step.""" + action: Action thought: Optional[str] = None state: Optional[Dict[str, Any]] = None raw_text: str = "" -def parse_thought_state_action(text: str) -> Tuple[Optional[str], Optional[Dict[str, Any]], str]: +def parse_thought_state_action( + text: str, +) -> Tuple[Optional[str], Optional[Dict[str, Any]], str]: """Parse Thought / State / Action blocks from model output. Expected format: @@ -54,12 +57,18 @@ def parse_thought_state_action(text: str) -> Tuple[Optional[str], Optional[Dict[ action_str: str = text.strip() # Extract Thought - find the LAST occurrence (model's response, not template) - thought_matches = list(re.finditer(r"Thought:\s*(.+?)(?=State:|Action:|$)", text, re.DOTALL | re.IGNORECASE)) + thought_matches = list( + re.finditer( + r"Thought:\s*(.+?)(?=State:|Action:|$)", text, re.DOTALL | re.IGNORECASE + ) + ) if thought_matches: thought = thought_matches[-1].group(1).strip() # Extract State (JSON on same line or next line) - last occurrence - state_matches = list(re.finditer(r"State:\s*(\{.*?\})", text, re.DOTALL | re.IGNORECASE)) + state_matches = list( + re.finditer(r"State:\s*(\{.*?\})", text, re.DOTALL | re.IGNORECASE) + ) if state_matches: try: state = json.loads(state_matches[-1].group(1)) @@ -127,7 +136,11 @@ def _parse_action(self, text: str) -> Action: idx = int(m.group(1)) raw_text = m.group(2) unescaped = raw_text.replace('\\"', '"').replace("\\\\", "\\") - return Action(type=ActionType.TYPE, text=unescaped, element=UIElement(element_id=str(idx))) + return Action( + type=ActionType.TYPE, + text=unescaped, + element=UIElement(element_id=str(idx)), + ) # TYPE("text") - SoM style without index m = _TYPE_SOM_SIMPLE_RE.search(text) diff --git a/openadapt_ml/runtime/safety_gate.py b/openadapt_ml/runtime/safety_gate.py index 6c72c26..4b119a9 100644 --- a/openadapt_ml/runtime/safety_gate.py +++ b/openadapt_ml/runtime/safety_gate.py @@ -177,7 +177,9 @@ def __init__(self, config: Optional[SafetyConfig] = None) -> None: # Pre-compile regex patterns for efficiency self._blocklist_re = self._compile_patterns(self.config.blocklist_patterns) - self._irreversible_re = self._compile_patterns(self.config.irreversible_patterns) + self._irreversible_re = self._compile_patterns( + self.config.irreversible_patterns + ) self._credential_re = self._compile_patterns(self.config.credential_patterns) self._credential_allowlist_re = self._compile_patterns( self.config.credential_allowlist @@ -387,9 +389,7 @@ def _check_app_window_mismatch( ) return None - def _check_confidence_threshold( - self, action: Action - ) -> Optional[SafetyAssessment]: + def _check_confidence_threshold(self, action: Action) -> Optional[SafetyAssessment]: """Check if action confidence is below threshold.""" confidence = self._get_action_confidence(action) diff --git a/openadapt_ml/schema/converters.py b/openadapt_ml/schema/converters.py index 555917e..01ae99d 100644 --- a/openadapt_ml/schema/converters.py +++ b/openadapt_ml/schema/converters.py @@ -30,6 +30,7 @@ # WAA (Windows Agent Arena) Converter # ============================================================================ + def _parse_waa_action(action_str: str) -> tuple[ActionType, dict[str, Any]]: """Parse WAA action string into ActionType and parameters. @@ -104,19 +105,25 @@ def _parse_waa_action(action_str: str) -> tuple[ActionType, dict[str, Any]]: if func_name == "click": params = {} if len(cleaned_args) >= 2: - params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1])) + params["coordinates"] = Coordinates( + x=int(cleaned_args[0]), y=int(cleaned_args[1]) + ) return ActionType.CLICK, params elif func_name == "doubleclick": params = {} if len(cleaned_args) >= 2: - params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1])) + params["coordinates"] = Coordinates( + x=int(cleaned_args[0]), y=int(cleaned_args[1]) + ) return ActionType.DOUBLE_CLICK, params elif func_name == "rightclick": params = {} if len(cleaned_args) >= 2: - params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1])) + params["coordinates"] = Coordinates( + x=int(cleaned_args[0]), y=int(cleaned_args[1]) + ) return ActionType.RIGHT_CLICK, params elif func_name in ("write", "typewrite"): @@ -144,7 +151,9 @@ def _parse_waa_action(action_str: str) -> tuple[ActionType, dict[str, Any]]: elif func_name == "moveto": params = {} if len(cleaned_args) >= 2: - params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1])) + params["coordinates"] = Coordinates( + x=int(cleaned_args[0]), y=int(cleaned_args[1]) + ) return ActionType.HOVER, params elif func_name == "drag" or func_name == "dragto": @@ -229,7 +238,20 @@ def from_waa_trajectory( metadata={ "domain": task_info.get("domain"), "difficulty": task_info.get("difficulty"), - **{k: v for k, v in task_info.items() if k not in ["id", "task_id", "instruction", "goal", "success", "domain", "difficulty"]}, + **{ + k: v + for k, v in task_info.items() + if k + not in [ + "id", + "task_id", + "instruction", + "goal", + "success", + "domain", + "difficulty", + ] + }, }, ) @@ -296,12 +318,16 @@ def _action_to_pyautogui(action: Action) -> str: if action.type == ActionType.DOUBLE_CLICK: if action.coordinates: - return f"pyautogui.doubleClick({action.coordinates.x}, {action.coordinates.y})" + return ( + f"pyautogui.doubleClick({action.coordinates.x}, {action.coordinates.y})" + ) return "pyautogui.doubleClick()" if action.type == ActionType.RIGHT_CLICK: if action.coordinates: - return f"pyautogui.rightClick({action.coordinates.x}, {action.coordinates.y})" + return ( + f"pyautogui.rightClick({action.coordinates.x}, {action.coordinates.y})" + ) return "pyautogui.rightClick()" if action.type == ActionType.TYPE: @@ -342,6 +368,7 @@ def _action_to_pyautogui(action: Action) -> str: # Internal Format Converter (openadapt_ml.schemas.sessions) # ============================================================================ + def from_internal_episode( internal_episode: Any, episode_id: Optional[str] = None, @@ -395,7 +422,9 @@ def from_internal_episode( key=step.action.key, modifiers=step.action.modifiers, scroll_direction=step.action.scroll_direction, - scroll_amount=int(step.action.scroll_amount) if step.action.scroll_amount else None, + scroll_amount=int(step.action.scroll_amount) + if step.action.scroll_amount + else None, normalized_end=(step.action.end_x, step.action.end_y) if step.action.end_x is not None and step.action.end_y is not None else None, @@ -403,17 +432,21 @@ def from_internal_episode( element_id=step.action.target_node_id, role=step.action.target_role, name=step.action.target_name, - ) if step.action.target_node_id else None, + ) + if step.action.target_node_id + else None, raw=step.action.raw, ) - steps.append(Step( - step_index=i, - observation=obs, - action=action, - reasoning=step.thought, - timestamp=step.t, - )) + steps.append( + Step( + step_index=i, + observation=obs, + action=action, + reasoning=step.thought, + timestamp=step.t, + ) + ) return Episode( episode_id=episode_id or internal_episode.id, @@ -423,7 +456,9 @@ def from_internal_episode( metadata={ "workflow_id": internal_episode.workflow_id, "summary": internal_episode.summary, - } if internal_episode.workflow_id or internal_episode.summary else None, + } + if internal_episode.workflow_id or internal_episode.summary + else None, ) @@ -468,11 +503,21 @@ def to_internal_episode(episode: Episode) -> dict: "modifiers": step.action.modifiers, "scroll_direction": step.action.scroll_direction, "scroll_amount": step.action.scroll_amount, - "end_x": step.action.normalized_end[0] if step.action.normalized_end else None, - "end_y": step.action.normalized_end[1] if step.action.normalized_end else None, - "target_node_id": step.action.element.element_id if step.action.element else None, - "target_role": step.action.element.role if step.action.element else None, - "target_name": step.action.element.name if step.action.element else None, + "end_x": step.action.normalized_end[0] + if step.action.normalized_end + else None, + "end_y": step.action.normalized_end[1] + if step.action.normalized_end + else None, + "target_node_id": step.action.element.element_id + if step.action.element + else None, + "target_role": step.action.element.role + if step.action.element + else None, + "target_name": step.action.element.name + if step.action.element + else None, "raw": step.action.raw, }, "thought": step.reasoning, @@ -484,7 +529,9 @@ def to_internal_episode(episode: Episode) -> dict: "goal": episode.instruction, "steps": steps, "success": episode.success, - "workflow_id": episode.metadata.get("workflow_id") if episode.metadata else None, + "workflow_id": episode.metadata.get("workflow_id") + if episode.metadata + else None, "summary": episode.metadata.get("summary") if episode.metadata else None, } @@ -519,7 +566,9 @@ def load_waa_result(result_dir: Union[str, Path]) -> Episode: trajectory = data elif isinstance(data, dict): trajectory = data.get("steps", data.get("trajectory", [])) - task_info = {k: v for k, v in data.items() if k not in ["steps", "trajectory"]} + task_info = { + k: v for k, v in data.items() if k not in ["steps", "trajectory"] + } # Try to read result result_file = result_dir / "result.txt" @@ -536,6 +585,4 @@ def load_waa_result(result_dir: Union[str, Path]) -> Episode: if task_id and "task_id" not in task_info: task_info["task_id"] = task_id - return from_waa_trajectory( - trajectory, task_info, episode_id=f"waa_{task_id}" - ) + return from_waa_trajectory(trajectory, task_info, episode_id=f"waa_{task_id}") diff --git a/openadapt_ml/schema/episode.py b/openadapt_ml/schema/episode.py index 92e3d98..4048290 100644 --- a/openadapt_ml/schema/episode.py +++ b/openadapt_ml/schema/episode.py @@ -154,7 +154,9 @@ def center(self) -> Coordinates: class UIElement(BaseModel): """UI element information from accessibility tree or DOM.""" - role: Optional[str] = Field(None, description="Element role (button, textbox, etc.)") + role: Optional[str] = Field( + None, description="Element role (button, textbox, etc.)" + ) name: Optional[str] = Field(None, description="Element accessible name") value: Optional[str] = Field(None, description="Element value (for inputs)") bounds: Optional[BoundingBox] = Field(None, description="Element bounding box") @@ -199,9 +201,15 @@ class Action(BaseModel): # Additional parameters url: Optional[str] = Field(None, description="URL for goto action") app_name: Optional[str] = Field(None, description="Application name for open/close") - duration: Optional[float] = Field(None, description="Duration in seconds (for wait)") - monitor_id: Optional[int] = Field(None, description="Monitor ID for select_monitor action") - window_title: Optional[str] = Field(None, description="Window title for window_focus action") + duration: Optional[float] = Field( + None, description="Duration in seconds (for wait)" + ) + monitor_id: Optional[int] = Field( + None, description="Monitor ID for select_monitor action" + ) + window_title: Optional[str] = Field( + None, description="Window title for window_focus action" + ) # Normalized coordinates (0.0-1.0) - alternative to pixel coordinates # Useful for resolution-independent recordings @@ -223,7 +231,11 @@ class Action(BaseModel): @model_validator(mode="after") def validate_action_params(self) -> "Action": """Validate that required parameters are present for action type.""" - if self.type in {ActionType.CLICK, ActionType.DOUBLE_CLICK, ActionType.RIGHT_CLICK}: + if self.type in { + ActionType.CLICK, + ActionType.DOUBLE_CLICK, + ActionType.RIGHT_CLICK, + }: if self.coordinates is None and self.element is None: # Allow missing coordinates - can be inferred from context pass @@ -259,7 +271,9 @@ class Observation(BaseModel): # Window/screen info window_title: Optional[str] = Field(None, description="Active window title") - app_name: Optional[str] = Field(None, description="Application name (e.g., 'Chrome', 'System Settings')") + app_name: Optional[str] = Field( + None, description="Application name (e.g., 'Chrome', 'System Settings')" + ) url: Optional[str] = Field(None, description="Current URL (for web apps)") screen_size: Optional[tuple[int, int]] = Field( None, description="Screen dimensions (width, height)" @@ -293,7 +307,9 @@ class Step(BaseModel): # Outcome reward: Optional[float] = Field(None, description="Reward signal (if available)") - done: Optional[bool] = Field(None, description="Whether episode ended after this step") + done: Optional[bool] = Field( + None, description="Whether episode ended after this step" + ) # Timing timestamp: Optional[float] = Field(None, description="Unix timestamp of action") @@ -311,8 +327,7 @@ class Episode(BaseModel): # Schema metadata schema_version: str = Field( - default=SCHEMA_VERSION, - description="Schema version for compatibility checking" + default=SCHEMA_VERSION, description="Schema version for compatibility checking" ) # Episode identification @@ -329,21 +344,20 @@ class Episode(BaseModel): steps: list[Step] = Field(..., description="Sequence of steps in the episode") # Outcome - success: Optional[bool] = Field(None, description="Whether task was completed successfully") + success: Optional[bool] = Field( + None, description="Whether task was completed successfully" + ) final_reward: Optional[float] = Field(None, description="Final reward/score") # Provenance source: Optional[BenchmarkSource] = Field( None, description="Source benchmark/dataset" ) - source_file: Optional[str] = Field( - None, description="Original source file path" - ) + source_file: Optional[str] = Field(None, description="Original source file path") # Metadata created_at: Optional[datetime] = Field( - default_factory=datetime.utcnow, - description="When episode was created/recorded" + default_factory=datetime.utcnow, description="When episode was created/recorded" ) agent_model: Optional[str] = Field( None, description="Model that generated this episode (e.g., 'gpt-4o')" @@ -351,9 +365,7 @@ class Episode(BaseModel): environment: Optional[str] = Field( None, description="Environment info (OS, browser, etc.)" ) - tags: Optional[list[str]] = Field( - None, description="Tags for categorization" - ) + tags: Optional[list[str]] = Field(None, description="Tags for categorization") # Extension point for benchmark-specific data metadata: Optional[dict[str, Any]] = Field( @@ -389,6 +401,7 @@ def json_schema(cls) -> dict[str, Any]: # Utility Functions # ============================================================================ + def validate_episode(data: dict[str, Any]) -> tuple[bool, Optional[str]]: """Validate episode data against schema. diff --git a/openadapt_ml/scripts/compare.py b/openadapt_ml/scripts/compare.py index 67b7c1b..ac41701 100644 --- a/openadapt_ml/scripts/compare.py +++ b/openadapt_ml/scripts/compare.py @@ -19,7 +19,10 @@ from openadapt_ml.ingest.capture import capture_to_episode from openadapt_ml.schema import Episode, ActionType from openadapt_ml.datasets.next_action import SYSTEM_PROMPT, format_action -from openadapt_ml.training.trainer import _get_shared_header_css, _generate_shared_header_html +from openadapt_ml.training.trainer import ( + _get_shared_header_css, + _generate_shared_header_html, +) def load_model(checkpoint_path: str | None, config_path: str | None = None): @@ -50,6 +53,7 @@ def load_model(checkpoint_path: str | None, config_path: str | None = None): except Exception as e: print(f"Warning: Could not load model: {e}") import traceback + traceback.print_exc() return None @@ -79,7 +83,9 @@ def predict_action( history_text += f" {i}. {action_text}\n" history_text += f"\nThis is step {step_index + 1} of {total_steps}. " else: - history_text = f"This is step 1 of {total_steps} (no actions completed yet). " + history_text = ( + f"This is step 1 of {total_steps} (no actions completed yet). " + ) # Match training prompt format exactly user_content = ( @@ -87,7 +93,7 @@ def predict_action( f"{history_text}" "Look at the screenshot and determine the NEXT action.\n\n" "Thought: [what element to interact with and why]\n" - "Action: [CLICK(x=..., y=...) or TYPE(text=\"...\") or WAIT() or DONE()]" + 'Action: [CLICK(x=..., y=...) or TYPE(text="...") or WAIT() or DONE()]' ) # Build sample in the format expected by the adapter @@ -107,14 +113,20 @@ def predict_action( # Try to extract coordinates from output # Match patterns like: CLICK(x=0.42, y=0.31) or click at (0.42, 0.31) - click_match = re.search(r'CLICK\s*\(\s*x\s*=\s*([\d.]+)\s*,\s*y\s*=\s*([\d.]+)\s*\)', result, re.IGNORECASE) + click_match = re.search( + r"CLICK\s*\(\s*x\s*=\s*([\d.]+)\s*,\s*y\s*=\s*([\d.]+)\s*\)", + result, + re.IGNORECASE, + ) if not click_match: - click_match = re.search(r'click.*?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*\)', result, re.IGNORECASE) + click_match = re.search( + r"click.*?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*\)", result, re.IGNORECASE + ) if not click_match: # Try to find any two decimal numbers - nums = re.findall(r'(0\.\d+)', result) + nums = re.findall(r"(0\.\d+)", result) if len(nums) >= 2: - click_match = type('Match', (), {'group': lambda s, i: nums[i-1]})() + click_match = type("Match", (), {"group": lambda s, i: nums[i - 1]})() if click_match: action["x"] = float(click_match.group(1)) @@ -124,6 +136,7 @@ def predict_action( return action except Exception as e: import traceback + traceback.print_exc() return {"type": "error", "error": str(e)} @@ -145,7 +158,11 @@ def generate_comparison_data( action_x, action_y = None, None if step.action.normalized_coordinates: action_x, action_y = step.action.normalized_coordinates - action_type_str = step.action.type.value if isinstance(step.action.type, ActionType) else step.action.type + action_type_str = ( + step.action.type.value + if isinstance(step.action.type, ActionType) + else step.action.type + ) step_data = { "index": i, "time": step.step_index, @@ -204,7 +221,7 @@ def generate_comparison_html( comparison_json = json.dumps(comparison_data) # Add comparison panel above screenshot in main content - comparison_panel = ''' + comparison_panel = """

Action Comparison

@@ -223,9 +240,9 @@ def generate_comparison_html(
- ''' + """ - comparison_styles = ''' + comparison_styles = """ - ''' + """ - comparison_script = f''' + comparison_script = f""" - ''' + """ # Insert into HTML # Add shared header CSS and comparison styles before - shared_header_css = f'' - html = base_html.replace('', shared_header_css + comparison_styles + '') + shared_header_css = f"" + html = base_html.replace( + "", shared_header_css + comparison_styles + "" + ) # Add shared header HTML after container div shared_header_html = _generate_shared_header_html("viewer") html = html.replace( - '
', - '
\n' + shared_header_html + '
', '
\n' + shared_header_html ) # Add comparison panel as full-width row BEFORE the main-content/sidebar flex row # Insert right BEFORE
as a sibling html = html.replace( '
', - comparison_panel + '\n
' + comparison_panel + '\n
', ) # Add script before - html = html.replace('', comparison_script + '') + html = html.replace("", comparison_script + "") # Write output - output_path.write_text(html, encoding='utf-8') + output_path.write_text(html, encoding="utf-8") print(f"Generated comparison viewer: {output_path}") except ImportError: @@ -752,20 +770,24 @@ def main(): description="Compare human actions vs model predictions on a capture." ) parser.add_argument( - "--capture", "-c", + "--capture", + "-c", required=True, help="Path to openadapt-capture recording directory", ) parser.add_argument( - "--checkpoint", "-m", + "--checkpoint", + "-m", help="Path to trained model checkpoint (optional)", ) parser.add_argument( - "--output", "-o", + "--output", + "-o", help="Output HTML path (default: capture_dir/comparison.html)", ) parser.add_argument( - "--goal", "-g", + "--goal", + "-g", help="Task goal/description (auto-detected from capture if not provided)", ) parser.add_argument( @@ -797,7 +819,7 @@ def main(): matches = sum(1 for d in comparison_data if d.get("match") is True) total = sum(1 for d in comparison_data if d.get("match") is not None) if total > 0: - print(f"Match rate: {matches}/{total} ({100*matches/total:.1f}%)") + print(f"Match rate: {matches}/{total} ({100 * matches / total:.1f}%)") # Generate HTML output_path = Path(args.output) if args.output else capture_path / "comparison.html" @@ -806,6 +828,7 @@ def main(): # Open in browser if args.open: import webbrowser + webbrowser.open(f"file://{output_path.absolute()}") return 0 @@ -842,11 +865,13 @@ def generate_unified_viewer( capture_id = capture_path.name if capture_path else "unknown" if available_captures is None: - available_captures = [{ - "id": capture_id, - "name": episode.instruction or "Untitled", - "steps": len(episode.steps), - }] + available_captures = [ + { + "id": capture_id, + "name": episode.instruction or "Untitled", + "steps": len(episode.steps), + } + ] # Prepare base capture data (human actions only, no predictions) base_data = [] @@ -855,18 +880,24 @@ def generate_unified_viewer( action_x, action_y = None, None if step.action.normalized_coordinates: action_x, action_y = step.action.normalized_coordinates - action_type_str = step.action.type.value if isinstance(step.action.type, ActionType) else step.action.type - base_data.append({ - "index": i, - "time": step.step_index, - "image_path": step.observation.screenshot_path, - "human_action": { - "type": action_type_str, - "x": action_x, - "y": action_y, - "text": step.action.text, - }, - }) + action_type_str = ( + step.action.type.value + if isinstance(step.action.type, ActionType) + else step.action.type + ) + base_data.append( + { + "index": i, + "time": step.step_index, + "image_path": step.observation.screenshot_path, + "human_action": { + "type": action_type_str, + "x": action_x, + "y": action_y, + "text": step.action.text, + }, + } + ) # JSON encode all data base_data_json = json.dumps(base_data) @@ -875,7 +906,7 @@ def generate_unified_viewer( current_capture_json = json.dumps(capture_id) # Unified viewer styles and controls - unified_styles = ''' + unified_styles = """ - ''' + """ # Comparison panel HTML - comparison_panel = ''' + comparison_panel = """
Training Example: @@ -1162,10 +1193,10 @@ def generate_unified_viewer(
- ''' + """ # Unified viewer script - unified_script = f''' + unified_script = f""" - ''' + """ # Inject into HTML - html = base_html.replace('', unified_styles + '') + html = base_html.replace("", unified_styles + "") html = html.replace( '
', - comparison_panel + '\n
' + comparison_panel + '\n
', ) - html = html.replace('', unified_script + '') + html = html.replace("", unified_script + "") # Write output - output_path.write_text(html, encoding='utf-8') + output_path.write_text(html, encoding="utf-8") print(f"Generated unified viewer: {output_path}") except ImportError: diff --git a/openadapt_ml/scripts/demo_policy.py b/openadapt_ml/scripts/demo_policy.py index 4aa0f0a..9014db0 100644 --- a/openadapt_ml/scripts/demo_policy.py +++ b/openadapt_ml/scripts/demo_policy.py @@ -20,7 +20,9 @@ def main() -> None: args = parser.parse_args() # Use synthetic data to build one SFT-style sample - sessions = generate_synthetic_sessions(num_sessions=1, seed=99, output_dir="synthetic/demo") + sessions = generate_synthetic_sessions( + num_sessions=1, seed=99, output_dir="synthetic/demo" + ) episodes = [ep for sess in sessions for ep in sess.episodes] samples = build_next_action_sft_samples(episodes) @@ -58,5 +60,6 @@ def main() -> None: print("State:", state) print("Raw output:", raw_text) + if __name__ == "__main__": main() diff --git a/openadapt_ml/scripts/eval_policy.py b/openadapt_ml/scripts/eval_policy.py index 153b026..2c43474 100644 --- a/openadapt_ml/scripts/eval_policy.py +++ b/openadapt_ml/scripts/eval_policy.py @@ -199,7 +199,9 @@ def _log(record: Dict[str, Any]) -> None: "mean_episode_step_score": metrics.mean_episode_step_score, "weak_episode_success_rate": metrics.weak_episode_success_rate, "state_success_rate": metrics.state_success_rate, - "element_accuracy": metrics.element_accuracy if hasattr(metrics, 'element_accuracy') else None, + "element_accuracy": metrics.element_accuracy + if hasattr(metrics, "element_accuracy") + else None, }, } out_path = Path(output_json) @@ -210,8 +212,12 @@ def _log(record: Dict[str, Any]) -> None: if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Evaluate a policy on synthetic episodes.") - parser.add_argument("--config", type=str, required=True, help="Path to YAML config file.") + parser = argparse.ArgumentParser( + description="Evaluate a policy on synthetic episodes." + ) + parser.add_argument( + "--config", type=str, required=True, help="Path to YAML config file." + ) parser.add_argument( "--backend", type=str, @@ -248,19 +254,19 @@ def _log(record: Dict[str, Any]) -> None: choices=["coord", "som"], default="coord", help="DSL mode: 'coord' for coordinate-based (CLICK(x=..., y=...)), " - "'som' for Set-of-Marks index-based (CLICK([1])). Default: coord.", + "'som' for Set-of-Marks index-based (CLICK([1])). Default: coord.", ) parser.add_argument( "--overfit", action="store_true", help="Evaluate on training data to check memorization/overfitting. " - "If not set, generates fresh data to test generalization.", + "If not set, generates fresh data to test generalization.", ) parser.add_argument( "--no-jitter", action="store_true", help="Disable jitter for deterministic UI layouts. " - "Useful for testing memorization of fixed layouts.", + "Useful for testing memorization of fixed layouts.", ) parser.add_argument( "--scenario", @@ -268,7 +274,7 @@ def _log(record: Dict[str, Any]) -> None: choices=["login", "registration"], default=None, help="Scenario type: 'login' (6 steps, 3 elements) or 'registration' (12 steps, 6 elements). " - "Overrides config if provided.", + "Overrides config if provided.", ) args = parser.parse_args() diff --git a/openadapt_ml/scripts/prepare_synthetic.py b/openadapt_ml/scripts/prepare_synthetic.py index 5eebb70..4dc0581 100644 --- a/openadapt_ml/scripts/prepare_synthetic.py +++ b/openadapt_ml/scripts/prepare_synthetic.py @@ -8,7 +8,9 @@ def main() -> None: output_dir = Path("synthetic") / "debug" - episodes = generate_synthetic_episodes(num_episodes=2, seed=42, output_dir=output_dir) + episodes = generate_synthetic_episodes( + num_episodes=2, seed=42, output_dir=output_dir + ) print(f"Generated {len(episodes)} episodes into {output_dir.resolve()}") diff --git a/openadapt_ml/scripts/train.py b/openadapt_ml/scripts/train.py index 7924a2b..e49a515 100644 --- a/openadapt_ml/scripts/train.py +++ b/openadapt_ml/scripts/train.py @@ -126,6 +126,7 @@ def main( # Disable Unsloth if requested if not use_unsloth: import os + os.environ["OPENADAPT_DISABLE_UNSLOTH"] = "1" base_path = Path(capture_path).parent if capture_path else None @@ -142,6 +143,7 @@ def main( # Open dashboard in browser if requested if open_dashboard: import webbrowser + dashboard_path = Path(output_dir) / "dashboard.html" if dashboard_path.exists(): webbrowser.open(f"file://{dashboard_path.absolute()}") @@ -153,22 +155,32 @@ def main( parser = argparse.ArgumentParser( description="Train Qwen-VL adapter on synthetic data or openadapt-capture recordings." ) - parser.add_argument("--config", type=str, required=True, help="Path to YAML config file.") - parser.add_argument("--capture", type=str, help="Path to openadapt-capture recording directory.") - parser.add_argument("--goal", type=str, help="Task goal/description (overrides recording's task description).") - parser.add_argument("--output-dir", type=str, help="Output directory for logs and dashboard.") - parser.add_argument("--open", action="store_true", help="Open training dashboard in browser.") + parser.add_argument( + "--config", type=str, required=True, help="Path to YAML config file." + ) + parser.add_argument( + "--capture", type=str, help="Path to openadapt-capture recording directory." + ) + parser.add_argument( + "--goal", + type=str, + help="Task goal/description (overrides recording's task description).", + ) + parser.add_argument( + "--output-dir", type=str, help="Output directory for logs and dashboard." + ) + parser.add_argument( + "--open", action="store_true", help="Open training dashboard in browser." + ) parser.add_argument( "--use-unsloth", action="store_true", default=True, - help="Enable Unsloth optimizations (default)." + help="Enable Unsloth optimizations (default).", ) parser.add_argument( - "--no-unsloth", - action="store_true", - help="Disable Unsloth optimizations." + "--no-unsloth", action="store_true", help="Disable Unsloth optimizations." ) args = parser.parse_args() diff --git a/openadapt_ml/training/benchmark_viewer.py b/openadapt_ml/training/benchmark_viewer.py index 7fdfc79..5f41905 100644 --- a/openadapt_ml/training/benchmark_viewer.py +++ b/openadapt_ml/training/benchmark_viewer.py @@ -12,7 +12,7 @@ def _get_background_tasks_panel_css() -> str: """Return CSS for background tasks panel.""" - return ''' + return """ .tasks-panel { background: linear-gradient(135deg, rgba(100, 100, 255, 0.1) 0%, rgba(100, 100, 255, 0.05) 100%); border: 1px solid rgba(100, 100, 255, 0.3); @@ -287,12 +287,12 @@ def _get_background_tasks_panel_css() -> str: border-radius: 3px; transition: width 0.5s ease; } - ''' + """ def _get_background_tasks_panel_html() -> str: """Return HTML for background tasks panel with JS polling and improved styling.""" - return ''' + return """
@@ -701,12 +701,12 @@ def _get_background_tasks_panel_html() -> str: fetchBackgroundTasks(); setInterval(fetchBackgroundTasks, 10000); - ''' + """ def _get_live_evaluation_panel_css() -> str: """Return CSS for live evaluation progress panel.""" - return ''' + return """ .live-eval-panel { background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0.05) 100%); border: 1px solid rgba(139, 92, 246, 0.3); @@ -859,12 +859,12 @@ def _get_live_evaluation_panel_css() -> str: 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } - ''' + """ def _get_live_evaluation_panel_html() -> str: """Return HTML for live evaluation panel with SSE and polling fallback.""" - return ''' + return """
@@ -1237,12 +1237,12 @@ class BenchmarkSSEManager { if (window.sseManager) window.sseManager.disconnect(); }); - ''' + """ def _get_azure_jobs_panel_css() -> str: """Return CSS for the Azure jobs status panel with color-coded status indicators.""" - return ''' + return """ .azure-jobs-panel { background: linear-gradient(135deg, rgba(0, 120, 212, 0.15) 0%, rgba(0, 120, 212, 0.05) 100%); border: 1px solid rgba(0, 120, 212, 0.3); @@ -1535,7 +1535,7 @@ def _get_azure_jobs_panel_css() -> str: @keyframes spin { to { transform: rotate(360deg); } } - ''' + """ def _get_azure_jobs_panel_html() -> str: @@ -1545,7 +1545,7 @@ def _get_azure_jobs_panel_html() -> str: is used for training jobs, not for WAA benchmarks (which require nested virtualization that managed compute doesn't support). """ - return ''' + return """