cudag 0.3.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. cudag/__init__.py +334 -0
  2. cudag/annotation/__init__.py +77 -0
  3. cudag/annotation/codegen.py +648 -0
  4. cudag/annotation/config.py +545 -0
  5. cudag/annotation/loader.py +342 -0
  6. cudag/annotation/scaffold.py +121 -0
  7. cudag/annotation/transcription.py +296 -0
  8. cudag/cli/__init__.py +5 -0
  9. cudag/cli/main.py +315 -0
  10. cudag/cli/new.py +873 -0
  11. cudag/core/__init__.py +364 -0
  12. cudag/core/button.py +137 -0
  13. cudag/core/canvas.py +222 -0
  14. cudag/core/config.py +70 -0
  15. cudag/core/coords.py +233 -0
  16. cudag/core/data_grid.py +804 -0
  17. cudag/core/dataset.py +678 -0
  18. cudag/core/distribution.py +136 -0
  19. cudag/core/drawing.py +75 -0
  20. cudag/core/fonts.py +156 -0
  21. cudag/core/generator.py +163 -0
  22. cudag/core/grid.py +367 -0
  23. cudag/core/grounding_task.py +247 -0
  24. cudag/core/icon.py +207 -0
  25. cudag/core/iconlist_task.py +301 -0
  26. cudag/core/models.py +1251 -0
  27. cudag/core/random.py +130 -0
  28. cudag/core/renderer.py +190 -0
  29. cudag/core/screen.py +402 -0
  30. cudag/core/scroll_task.py +254 -0
  31. cudag/core/scrollable_grid.py +447 -0
  32. cudag/core/state.py +110 -0
  33. cudag/core/task.py +293 -0
  34. cudag/core/taskbar.py +350 -0
  35. cudag/core/text.py +212 -0
  36. cudag/core/utils.py +82 -0
  37. cudag/data/surnames.txt +5000 -0
  38. cudag/modal_apps/__init__.py +4 -0
  39. cudag/modal_apps/archive.py +103 -0
  40. cudag/modal_apps/extract.py +138 -0
  41. cudag/modal_apps/preprocess.py +529 -0
  42. cudag/modal_apps/upload.py +317 -0
  43. cudag/prompts/SYSTEM_PROMPT.txt +104 -0
  44. cudag/prompts/__init__.py +33 -0
  45. cudag/prompts/system.py +43 -0
  46. cudag/prompts/tools.py +382 -0
  47. cudag/py.typed +0 -0
  48. cudag/schemas/filesystem.json +90 -0
  49. cudag/schemas/test_record.schema.json +113 -0
  50. cudag/schemas/train_record.schema.json +90 -0
  51. cudag/server/__init__.py +21 -0
  52. cudag/server/app.py +232 -0
  53. cudag/server/services/__init__.py +9 -0
  54. cudag/server/services/generator.py +128 -0
  55. cudag/templates/scripts/archive.sh +35 -0
  56. cudag/templates/scripts/build.sh +13 -0
  57. cudag/templates/scripts/extract.sh +54 -0
  58. cudag/templates/scripts/generate.sh +116 -0
  59. cudag/templates/scripts/pre-commit.sh +44 -0
  60. cudag/templates/scripts/preprocess.sh +46 -0
  61. cudag/templates/scripts/upload.sh +63 -0
  62. cudag/templates/scripts/verify.py +428 -0
  63. cudag/validation/__init__.py +35 -0
  64. cudag/validation/validate.py +508 -0
  65. cudag-0.3.10.dist-info/METADATA +570 -0
  66. cudag-0.3.10.dist-info/RECORD +69 -0
  67. cudag-0.3.10.dist-info/WHEEL +4 -0
  68. cudag-0.3.10.dist-info/entry_points.txt +2 -0
  69. cudag-0.3.10.dist-info/licenses/LICENSE +66 -0
cudag/core/icon.py ADDED
@@ -0,0 +1,207 @@
1
+ # Copyright (c) 2025 Tylt LLC. All rights reserved.
2
+ # CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
3
+ # is strictly prohibited. For licensing inquiries: hello@claimhawk.app
4
+
5
+ """Icon abstraction for clickable UI icons.
6
+
7
+ Provides reusable Icon classes for any icon-based UI element:
8
+ - Desktop icons (large, with labels)
9
+ - Taskbar icons (small, no labels)
10
+ - Application icons
11
+ - Toolbar buttons
12
+
13
+ The Icon class handles:
14
+ - Placement (position, size)
15
+ - Tolerance calculation
16
+ - Center point for clicks
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+
26
+ @dataclass
27
+ class IconSpec:
28
+ """Specification for an icon type.
29
+
30
+ Defines the size and characteristics of a class of icons
31
+ (e.g., all desktop icons are 54x54 with labels).
32
+ """
33
+
34
+ width: int
35
+ """Icon width in pixels."""
36
+
37
+ height: int
38
+ """Icon height in pixels."""
39
+
40
+ has_label: bool = False
41
+ """Whether this icon type has a text label."""
42
+
43
+ label_height: int = 0
44
+ """Height of the label area below icon."""
45
+
46
+ @property
47
+ def total_height(self) -> int:
48
+ """Total height including label."""
49
+ return self.height + (self.label_height if self.has_label else 0)
50
+
51
+ @property
52
+ def tolerance_pixels(self) -> tuple[int, int]:
53
+ """Natural tolerance for this icon size (70% of dimensions)."""
54
+ return (int(self.width * 0.7), int(self.height * 0.7))
55
+
56
+ def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
57
+ """Tolerance in RU units for normalized coordinates.
58
+
59
+ Args:
60
+ image_size: (width, height) of the image in pixels
61
+
62
+ Returns:
63
+ (x_tolerance, y_tolerance) in RU units (0-1000 scale)
64
+ """
65
+ x_ru = (self.width * 0.7 / image_size[0]) * 1000
66
+ y_ru = (self.height * 0.7 / image_size[1]) * 1000
67
+ return (int(x_ru), int(y_ru))
68
+
69
+
70
+ # Common icon specs
71
+ DESKTOP_ICON = IconSpec(width=54, height=54, has_label=True, label_height=20)
72
+ TASKBAR_ICON = IconSpec(width=27, height=28, has_label=False)
73
+ TOOLBAR_ICON = IconSpec(width=24, height=24, has_label=False)
74
+ APP_ICON_LARGE = IconSpec(width=48, height=48, has_label=True, label_height=16)
75
+ APP_ICON_SMALL = IconSpec(width=32, height=32, has_label=False)
76
+
77
+
78
+ @dataclass
79
+ class IconPlacement:
80
+ """An icon placed at a specific location.
81
+
82
+ Represents a single icon instance with its position and metadata.
83
+ """
84
+
85
+ icon_id: str
86
+ """Unique identifier for this icon (e.g., 'chrome', 'od')."""
87
+
88
+ x: int
89
+ """X position of icon top-left corner."""
90
+
91
+ y: int
92
+ """Y position of icon top-left corner."""
93
+
94
+ spec: IconSpec
95
+ """Icon specification (size, etc.)."""
96
+
97
+ label: str = ""
98
+ """Display label for the icon."""
99
+
100
+ image_file: str | Path = ""
101
+ """Path to the icon image file."""
102
+
103
+ metadata: dict[str, Any] = field(default_factory=dict)
104
+ """Additional metadata."""
105
+
106
+ @property
107
+ def width(self) -> int:
108
+ """Icon width."""
109
+ return self.spec.width
110
+
111
+ @property
112
+ def height(self) -> int:
113
+ """Icon height (excluding label)."""
114
+ return self.spec.height
115
+
116
+ @property
117
+ def center(self) -> tuple[int, int]:
118
+ """Center point of the icon (for clicking)."""
119
+ return (
120
+ self.x + self.spec.width // 2,
121
+ self.y + self.spec.height // 2,
122
+ )
123
+
124
+ @property
125
+ def bounds(self) -> tuple[int, int, int, int]:
126
+ """Bounding box as (x, y, width, height)."""
127
+ return (self.x, self.y, self.spec.width, self.spec.height)
128
+
129
+ @property
130
+ def tolerance_pixels(self) -> tuple[int, int]:
131
+ """Natural tolerance for this icon."""
132
+ return self.spec.tolerance_pixels
133
+
134
+ def tolerance_ru(self, image_size: tuple[int, int]) -> tuple[int, int]:
135
+ """Tolerance in RU units."""
136
+ return self.spec.tolerance_ru(image_size)
137
+
138
+
139
+ @dataclass
140
+ class IconLayout:
141
+ """Layout manager for icons in a region.
142
+
143
+ Handles placement of multiple icons in a grid or list layout.
144
+ """
145
+
146
+ spec: IconSpec
147
+ """Icon specification for all icons in this layout."""
148
+
149
+ start_x: int = 0
150
+ """Starting X position."""
151
+
152
+ start_y: int = 0
153
+ """Starting Y position."""
154
+
155
+ padding: int = 10
156
+ """Padding between icons."""
157
+
158
+ direction: str = "vertical"
159
+ """Layout direction: 'vertical', 'horizontal', or 'grid'."""
160
+
161
+ cols: int = 1
162
+ """Number of columns for grid layout."""
163
+
164
+ def place_icons(
165
+ self,
166
+ icon_ids: list[str],
167
+ labels: dict[str, str] | None = None,
168
+ image_files: dict[str, str | Path] | None = None,
169
+ ) -> list[IconPlacement]:
170
+ """Place icons according to layout rules.
171
+
172
+ Args:
173
+ icon_ids: List of icon identifiers to place
174
+ labels: Optional dict mapping icon_id to label
175
+ image_files: Optional dict mapping icon_id to image path
176
+
177
+ Returns:
178
+ List of IconPlacement objects
179
+ """
180
+ labels = labels or {}
181
+ image_files = image_files or {}
182
+ placements: list[IconPlacement] = []
183
+
184
+ for i, icon_id in enumerate(icon_ids):
185
+ if self.direction == "vertical":
186
+ x = self.start_x
187
+ y = self.start_y + i * (self.spec.total_height + self.padding)
188
+ elif self.direction == "horizontal":
189
+ x = self.start_x + i * (self.spec.width + self.padding)
190
+ y = self.start_y
191
+ else: # grid
192
+ row, col = divmod(i, self.cols)
193
+ x = self.start_x + col * (self.spec.width + self.padding)
194
+ y = self.start_y + row * (self.spec.total_height + self.padding)
195
+
196
+ placements.append(
197
+ IconPlacement(
198
+ icon_id=icon_id,
199
+ x=x,
200
+ y=y,
201
+ spec=self.spec,
202
+ label=labels.get(icon_id, ""),
203
+ image_file=image_files.get(icon_id, ""),
204
+ )
205
+ )
206
+
207
+ return placements
@@ -0,0 +1,301 @@
1
+ # Copyright (c) 2025 Tylt LLC. All rights reserved.
2
+ # CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
3
+ # is strictly prohibited. For licensing inquiries: hello@claimhawk.app
4
+
5
+ """Base class for iconlist element tasks.
6
+
7
+ This module provides an abstract base class for tasks that target iconlist
8
+ elements in annotation.json. When a task targets an iconlist, it generates
9
+ one sample per icon in that list.
10
+
11
+ Element type determines generation behavior:
12
+ - iconlist -> generate one sample per icon
13
+ - button -> generate one sample (not handled here)
14
+ - loading -> handled separately by wait tasks
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from abc import abstractmethod
20
+ from dataclasses import dataclass
21
+ from random import Random
22
+ from typing import TYPE_CHECKING, Any, Protocol
23
+
24
+ from cudag.annotation.config import AnnotatedElement, AnnotatedTask, AnnotationConfig
25
+ from cudag.core.task import BaseTask, TaskContext, TaskSample, TestCase
26
+ from cudag.prompts.tools import ToolCall
27
+
28
+ if TYPE_CHECKING:
29
+ from cudag.core.renderer import BaseRenderer
30
+
31
+
32
+ class IconInfo(Protocol):
33
+ """Protocol for icon placement info from state."""
34
+
35
+ icon_id: str
36
+ center: tuple[int, int]
37
+ bounds: tuple[int, int, int, int]
38
+
39
+
40
+ def make_tool_call(
41
+ action: str, coord: tuple[int, int], wait_time: float = 0
42
+ ) -> ToolCall:
43
+ """Create a ToolCall based on action string from annotation.
44
+
45
+ Args:
46
+ action: Action string from annotation (double_click, left_click, etc.)
47
+ coord: (x, y) pixel coordinates
48
+ wait_time: Wait time in seconds for wait actions
49
+
50
+ Returns:
51
+ ToolCall instance for the action
52
+ """
53
+ if action == "double_click":
54
+ return ToolCall.double_click(coord)
55
+ elif action == "left_click":
56
+ return ToolCall.left_click(coord)
57
+ elif action == "right_click":
58
+ return ToolCall.right_click(coord)
59
+ elif action == "wait":
60
+ return ToolCall.wait(wait_time)
61
+ else:
62
+ raise ValueError(f"Unknown action '{action}' - must be one of: double_click, left_click, right_click, wait")
63
+
64
+
65
+ class IconListTaskBase(BaseTask):
66
+ """Abstract base class for tasks targeting iconlist elements.
67
+
68
+ This base class handles the common pattern for iconlist tasks:
69
+ 1. Iterate over tasks in annotation
70
+ 2. For each task targeting an iconlist element, generate one sample per icon
71
+ 3. Use task.prompt_template, task.action, task.task_type from annotation
72
+ 4. Use element.tolerance_x/y from annotation
73
+
74
+ Subclasses must implement:
75
+ - get_annotation_config(): Return the AnnotationConfig
76
+ - get_icons_for_element(): Return icon placements and info for an element
77
+ - generate_state(): Generate the screen state
78
+
79
+ Example:
80
+ class ClickIconTask(IconListTaskBase):
81
+ def get_annotation_config(self) -> AnnotationConfig:
82
+ return ANNOTATION_CONFIG
83
+
84
+ def get_icons_for_element(
85
+ self, element: AnnotatedElement, state: Any
86
+ ) -> tuple[list[IconInfo], dict[str, dict]]:
87
+ if element.label == "desktop":
88
+ return state.desktop_icons, get_desktop_icons()
89
+ elif element.label == "taskbar":
90
+ return state.taskbar_icons, get_taskbar_icons()
91
+ return [], {}
92
+
93
+ def generate_state(self, rng: Random, **kwargs) -> Any:
94
+ return DesktopState.generate(rng=rng, **kwargs)
95
+ """
96
+
97
+ task_type: str = "iconlist"
98
+
99
+ @abstractmethod
100
+ def get_annotation_config(self) -> AnnotationConfig | None:
101
+ """Return the annotation config for this generator.
102
+
103
+ Returns:
104
+ AnnotationConfig loaded from annotation.json, or None
105
+ """
106
+ ...
107
+
108
+ @abstractmethod
109
+ def get_icons_for_element(
110
+ self, element: AnnotatedElement, state: Any
111
+ ) -> tuple[list[Any], dict[str, dict[str, Any]]]:
112
+ """Get icons and their info for an element.
113
+
114
+ Args:
115
+ element: The annotated element (iconlist type)
116
+ state: The generated screen state
117
+
118
+ Returns:
119
+ Tuple of (icon_placements, icons_info_dict)
120
+ - icon_placements: List of IconPlacement-like objects with icon_id, center, bounds
121
+ - icons_info_dict: Dict mapping icon_id to {label, required, ...}
122
+ """
123
+ ...
124
+
125
+ @abstractmethod
126
+ def generate_state(self, rng: Random, **kwargs: Any) -> Any:
127
+ """Generate the screen state.
128
+
129
+ Args:
130
+ rng: Random number generator
131
+ **kwargs: Additional arguments for state generation
132
+
133
+ Returns:
134
+ State object for rendering
135
+ """
136
+ ...
137
+
138
+ def generate_samples(self, ctx: TaskContext) -> list[TaskSample]:
139
+ """Generate samples for ALL iconlist tasks in annotation.
140
+
141
+ Iterates over tasks in annotation.json. For each task targeting an
142
+ iconlist element, generates one sample per icon in that list.
143
+
144
+ Args:
145
+ ctx: Task context with RNG, index, output directory
146
+
147
+ Returns:
148
+ List of TaskSample, one per icon per iconlist task
149
+ """
150
+ config = self.get_annotation_config()
151
+ if config is None:
152
+ return []
153
+
154
+ # Generate state - subclass provides kwargs
155
+ state = self.generate_state(ctx.rng)
156
+
157
+ # Render once - shared by all samples
158
+ image, metadata = self.renderer.render(state)
159
+ image_path = self.save_image(image, ctx)
160
+
161
+ samples: list[TaskSample] = []
162
+
163
+ # Iterate over ALL tasks in annotation (except wait and grounding tasks)
164
+ for task in config.tasks:
165
+ if task.action == "wait":
166
+ continue # Wait tasks handled separately
167
+ if task.action == "grounding":
168
+ continue # Grounding tasks handled by GroundingTask
169
+
170
+ # Get target element
171
+ element = config.get_element(task.target_element_id)
172
+ if element is None:
173
+ continue
174
+
175
+ # Only handle iconlist elements
176
+ if element.element_type != "iconlist":
177
+ continue
178
+
179
+ # Get icons for this element from state
180
+ icons_in_state, icons_info = self.get_icons_for_element(element, state)
181
+ if not icons_in_state:
182
+ continue
183
+
184
+ tolerance = (element.tolerance_x, element.tolerance_y)
185
+
186
+ # Generate a sample for each icon
187
+ for icon in icons_in_state:
188
+ icon_info = icons_info.get(icon.icon_id, {})
189
+ label = icon_info.get("label", icon.icon_id)
190
+
191
+ prompt = task.prompt_template.replace("[icon_label]", label)
192
+ click_x, click_y = icon.center
193
+
194
+ tool_call = make_tool_call(task.action, (click_x, click_y))
195
+
196
+ samples.append(
197
+ TaskSample(
198
+ id=self.build_id(ctx, f"_{element.label}_{icon.icon_id}"),
199
+ image_path=image_path,
200
+ human_prompt=prompt,
201
+ tool_call=tool_call,
202
+ pixel_coords=(click_x, click_y),
203
+ metadata={
204
+ "task_type": task.task_type,
205
+ "element_type": element.element_type,
206
+ "element_label": element.label,
207
+ "icon_id": icon.icon_id,
208
+ "icon_label": label,
209
+ "icon_bounds": icon.bounds,
210
+ "ground_truth": (
211
+ state.to_ground_truth()
212
+ if hasattr(state, "to_ground_truth")
213
+ else {}
214
+ ),
215
+ "tolerance": list(tolerance),
216
+ },
217
+ image_size=config.image_size,
218
+ )
219
+ )
220
+
221
+ return samples
222
+
223
+ def generate_sample(self, ctx: TaskContext) -> TaskSample:
224
+ """Generate samples - returns first sample."""
225
+ samples = self.generate_samples(ctx)
226
+ if not samples:
227
+ raise ValueError("No samples generated")
228
+ return samples[0]
229
+
230
+ def generate_tests(self, ctx: TaskContext) -> list[TestCase]:
231
+ """Generate test cases for required icons in iconlist tasks.
232
+
233
+ Only generates tests for icons marked as required=true in annotation.
234
+
235
+ Args:
236
+ ctx: Task context
237
+
238
+ Returns:
239
+ List of TestCase for required icons
240
+ """
241
+ config = self.get_annotation_config()
242
+ if config is None:
243
+ return []
244
+
245
+ state = self.generate_state(ctx.rng)
246
+ image, metadata = self.renderer.render(state)
247
+ image_path = self.save_image(image, ctx)
248
+
249
+ tests: list[TestCase] = []
250
+
251
+ for task in config.tasks:
252
+ if task.action == "wait":
253
+ continue
254
+
255
+ element = config.get_element(task.target_element_id)
256
+ if element is None or element.element_type != "iconlist":
257
+ continue
258
+
259
+ icons_in_state, icons_info = self.get_icons_for_element(element, state)
260
+ tolerance = (element.tolerance_x, element.tolerance_y)
261
+
262
+ # Test on required icons only
263
+ for icon in icons_in_state:
264
+ icon_info = icons_info.get(icon.icon_id, {})
265
+ if not icon_info.get("required", False):
266
+ continue
267
+
268
+ label = icon_info.get("label", icon.icon_id)
269
+ prompt = task.prompt_template.replace("[icon_label]", label)
270
+ click_x, click_y = icon.center
271
+
272
+ tool_call = make_tool_call(task.action, (click_x, click_y))
273
+
274
+ tests.append(
275
+ TestCase(
276
+ test_id=f"test_{ctx.index:04d}_{element.label}_{icon.icon_id}",
277
+ screenshot=image_path,
278
+ prompt=prompt,
279
+ expected_action=tool_call.to_dict(),
280
+ tolerance=tolerance,
281
+ metadata={
282
+ "task_type": task.task_type,
283
+ "element_type": element.element_type,
284
+ "element_label": element.label,
285
+ "icon_id": icon.icon_id,
286
+ "icon_label": label,
287
+ "icon_bounds": icon.bounds,
288
+ "image_size": image.size,
289
+ },
290
+ pixel_coords=(click_x, click_y),
291
+ )
292
+ )
293
+
294
+ return tests
295
+
296
+ def generate_test(self, ctx: TaskContext) -> TestCase:
297
+ """Generate test - returns first."""
298
+ tests = self.generate_tests(ctx)
299
+ if not tests:
300
+ raise ValueError("No tests generated")
301
+ return tests[0]