webpilot-engine 2.0.0__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 (47) hide show
  1. adapters/__init__.py +14 -0
  2. adapters/base.py +34 -0
  3. adapters/browser_adapter.py +300 -0
  4. adapters/dom_scripts.py +518 -0
  5. adapters/envelope_builder.py +108 -0
  6. adapters/live_session_manager.py +326 -0
  7. adapters/upload_adapter.py +77 -0
  8. builder.py +83 -0
  9. cli.py +341 -0
  10. config/__init__.py +23 -0
  11. config/settings.py +339 -0
  12. constants/__init__.py +165 -0
  13. constants/contracts.py +95 -0
  14. constants/selectors.py +27 -0
  15. constants/timeouts.py +25 -0
  16. core/__init__.py +45 -0
  17. core/exceptions.py +33 -0
  18. core/models.py +273 -0
  19. engine.py +199 -0
  20. exceptions.py +23 -0
  21. mcp_server.py +347 -0
  22. models.py +29 -0
  23. orchestration/__init__.py +19 -0
  24. orchestration/context.py +71 -0
  25. orchestration/flow_orchestrator.py +358 -0
  26. orchestration/session_coordinator.py +88 -0
  27. services/__init__.py +17 -0
  28. services/auth_navigation_service.py +129 -0
  29. services/field_interaction_service.py +174 -0
  30. services/form_filler_service.py +106 -0
  31. services/inspection_formatter_service.py +68 -0
  32. services/inspection_service.py +40 -0
  33. services/reactive_interaction_service.py +174 -0
  34. supervisor/__init__.py +13 -0
  35. supervisor/client.py +171 -0
  36. supervisor/contracts.py +62 -0
  37. supervisor/master_daemon.py +303 -0
  38. supervisor/shell.py +194 -0
  39. supervisor/worker_process.py +400 -0
  40. validators/__init__.py +9 -0
  41. validators/form_validator.py +28 -0
  42. validators/payload_validator.py +25 -0
  43. webpilot_engine-2.0.0.dist-info/METADATA +299 -0
  44. webpilot_engine-2.0.0.dist-info/RECORD +47 -0
  45. webpilot_engine-2.0.0.dist-info/WHEEL +5 -0
  46. webpilot_engine-2.0.0.dist-info/entry_points.txt +6 -0
  47. webpilot_engine-2.0.0.dist-info/top_level.txt +14 -0
core/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """Core package for universal web applier."""
2
+
3
+ from core.models import (
4
+ ProxyConfig,
5
+ DeviceProfile,
6
+ InputField,
7
+ DropdownField,
8
+ ChoiceGroup,
9
+ FileUploadField,
10
+ ActionButton,
11
+ InspectionResult,
12
+ FillSummary,
13
+ ReactiveClickOutcome,
14
+ )
15
+ from core.exceptions import (
16
+ UniversalApplierError,
17
+ ConfigurationError,
18
+ ValidationError,
19
+ AutomationError,
20
+ ElementNotFoundError,
21
+ InteractionTimeoutError,
22
+ DocumentUploadError,
23
+ AuthenticationError,
24
+ )
25
+
26
+ __all__ = [
27
+ "ProxyConfig",
28
+ "DeviceProfile",
29
+ "InputField",
30
+ "DropdownField",
31
+ "ChoiceGroup",
32
+ "FileUploadField",
33
+ "ActionButton",
34
+ "InspectionResult",
35
+ "FillSummary",
36
+ "ReactiveClickOutcome",
37
+ "UniversalApplierError",
38
+ "ConfigurationError",
39
+ "ValidationError",
40
+ "AutomationError",
41
+ "ElementNotFoundError",
42
+ "InteractionTimeoutError",
43
+ "DocumentUploadError",
44
+ "AuthenticationError",
45
+ ]
core/exceptions.py ADDED
@@ -0,0 +1,33 @@
1
+ """Typed exception hierarchy for Universal Web Applier."""
2
+
3
+ class UniversalApplierError(Exception):
4
+ """Base exception for all universal web applier domain errors."""
5
+ pass
6
+
7
+ class ConfigurationError(UniversalApplierError):
8
+ """Raised when configuration values, files, or device profiles are missing or invalid."""
9
+ pass
10
+
11
+ class ValidationError(UniversalApplierError):
12
+ """Raised when input data, payloads, or schema validation fails."""
13
+ pass
14
+
15
+ class AutomationError(UniversalApplierError):
16
+ """Raised when web automation actions fail unexpectedly."""
17
+ pass
18
+
19
+ class ElementNotFoundError(AutomationError):
20
+ """Raised when an expected DOM element cannot be located."""
21
+ pass
22
+
23
+ class InteractionTimeoutError(AutomationError):
24
+ """Raised when an action or wait condition exceeds its deadline."""
25
+ pass
26
+
27
+ class DocumentUploadError(AutomationError):
28
+ """Raised when document attachment fails."""
29
+ pass
30
+
31
+ class AuthenticationError(AutomationError):
32
+ """Raised when login or credential submission fails."""
33
+ pass
core/models.py ADDED
@@ -0,0 +1,273 @@
1
+ """Shared domain Transfer Objects (DTOs) and data models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional, Tuple, Union
7
+ from core.exceptions import ValidationError
8
+
9
+
10
+ @dataclass
11
+ class ProxyConfig:
12
+ """Network proxy configuration loaded from settings."""
13
+ server: str
14
+ username: Optional[str] = None
15
+ password: Optional[str] = None
16
+
17
+ def to_playwright_dict(self) -> Dict[str, str]:
18
+ """Convert to Playwright-compatible proxy dictionary."""
19
+ d: Dict[str, str] = {"server": self.server}
20
+ if self.username:
21
+ d["username"] = self.username
22
+ if self.password:
23
+ d["password"] = self.password
24
+ return d
25
+
26
+
27
+ @dataclass
28
+ class DeviceProfile:
29
+ """Device & browser profile parameters for browser context emulation."""
30
+ device_name: str
31
+ user_agent: str
32
+ viewport_width: int
33
+ viewport_height: int
34
+ device_scale_factor: float = 1.0
35
+ is_mobile: bool = False
36
+ has_touch: bool = False
37
+ locale: str = "en-US"
38
+ timezone_id: str = "UTC"
39
+ geolocation_latitude: Optional[float] = None
40
+ geolocation_longitude: Optional[float] = None
41
+ geolocation_accuracy: Optional[float] = None
42
+ permissions: List[str] = field(default_factory=list)
43
+ platform: str = "Win32"
44
+ vendor: str = "Google Inc."
45
+
46
+ def viewport_dict(self) -> Dict[str, int]:
47
+ """Returns the viewport dimensions dictionary."""
48
+ return {"width": self.viewport_width, "height": self.viewport_height}
49
+
50
+ def geolocation_dict(self) -> Optional[Dict[str, float]]:
51
+ """Returns geolocation dictionary if valid coordinates are present."""
52
+ if self.geolocation_latitude is not None and self.geolocation_longitude is not None:
53
+ return {
54
+ "latitude": float(self.geolocation_latitude),
55
+ "longitude": float(self.geolocation_longitude),
56
+ "accuracy": float(self.geolocation_accuracy or 10.0),
57
+ }
58
+ return None
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: Dict[str, Any]) -> "DeviceProfile":
62
+ """Instantiate a DeviceProfile strictly from parsed dictionary data."""
63
+ if not isinstance(data, dict):
64
+ raise ValidationError(f"Expected dict for DeviceProfile, got {type(data).__name__}")
65
+
66
+ viewport = data.get("viewport") or {}
67
+ vp_width = data.get("viewport_width") or viewport.get("width")
68
+ vp_height = data.get("viewport_height") or viewport.get("height")
69
+
70
+ geo = data.get("geolocation") or {}
71
+ geo_lat = data.get("geolocation_latitude", geo.get("latitude"))
72
+ geo_lon = data.get("geolocation_longitude", geo.get("longitude"))
73
+ geo_acc = data.get("geolocation_accuracy", geo.get("accuracy"))
74
+
75
+ return cls(
76
+ device_name=str(data.get("device_name", "")),
77
+ user_agent=str(data.get("user_agent", "")),
78
+ viewport_width=int(vp_width) if vp_width is not None else 0,
79
+ viewport_height=int(vp_height) if vp_height is not None else 0,
80
+ device_scale_factor=float(data.get("device_scale_factor", 1.0)),
81
+ is_mobile=bool(data.get("is_mobile", False)),
82
+ has_touch=bool(data.get("has_touch", False)),
83
+ locale=str(data.get("locale", "en-US")),
84
+ timezone_id=str(data.get("timezone_id", "UTC")),
85
+ geolocation_latitude=float(geo_lat) if geo_lat is not None else None,
86
+ geolocation_longitude=float(geo_lon) if geo_lon is not None else None,
87
+ geolocation_accuracy=float(geo_acc) if geo_acc is not None else None,
88
+ permissions=list(data.get("permissions", [])),
89
+ platform=str(data.get("platform", "Win32")),
90
+ vendor=str(data.get("vendor", "Google Inc.")),
91
+ )
92
+
93
+ def to_dict(self) -> Dict[str, Any]:
94
+ """Convert DeviceProfile to serializable dictionary."""
95
+ d: Dict[str, Any] = {
96
+ "device_name": self.device_name,
97
+ "user_agent": self.user_agent,
98
+ "viewport": self.viewport_dict(),
99
+ "device_scale_factor": self.device_scale_factor,
100
+ "is_mobile": self.is_mobile,
101
+ "has_touch": self.has_touch,
102
+ "locale": self.locale,
103
+ "timezone_id": self.timezone_id,
104
+ "permissions": self.permissions,
105
+ "platform": self.platform,
106
+ "vendor": self.vendor,
107
+ }
108
+ geo = self.geolocation_dict()
109
+ if geo:
110
+ d["geolocation"] = geo
111
+ return d
112
+
113
+
114
+ @dataclass
115
+ class InputField:
116
+ """Inspected text or numeric input element."""
117
+ id: str
118
+ label: str
119
+ type: str = "text"
120
+ required: bool = False
121
+ value: str = ""
122
+
123
+
124
+ @dataclass
125
+ class DropdownField:
126
+ """Inspected select dropdown or combobox picklist."""
127
+ id: str
128
+ label: str
129
+ type: str = "select"
130
+ required: bool = False
131
+ value: str = ""
132
+ trigger_id: str = ""
133
+ sample_options: List[str] = field(default_factory=list)
134
+ options_count: int = 0
135
+
136
+
137
+ @dataclass
138
+ class ChoiceGroup:
139
+ """Inspected radio or checkbox group for a question."""
140
+ question: str
141
+ type: str = "radio"
142
+ required: bool = False
143
+ options: List[str] = field(default_factory=list)
144
+ selected: Optional[str] = None
145
+
146
+
147
+ @dataclass
148
+ class FileUploadField:
149
+ """Inspected file upload zone or attachment input."""
150
+ id: str
151
+ label: str
152
+ required: bool = False
153
+
154
+
155
+ @dataclass
156
+ class ActionButton:
157
+ """Inspected form action button."""
158
+ id: str
159
+ text: str
160
+ action: str = "action"
161
+
162
+
163
+ @dataclass
164
+ class InspectionResult:
165
+ """Consolidated form inspection schema."""
166
+ title: str = ""
167
+ url: str = ""
168
+ sections: List[str] = field(default_factory=list)
169
+ inputs: List[InputField] = field(default_factory=list)
170
+ dropdowns: List[DropdownField] = field(default_factory=list)
171
+ choices: List[ChoiceGroup] = field(default_factory=list)
172
+ file_uploads: List[FileUploadField] = field(default_factory=list)
173
+ already_uploaded_files: List[str] = field(default_factory=list)
174
+ buttons: List[ActionButton] = field(default_factory=list)
175
+
176
+ def to_dict(self) -> Dict[str, Any]:
177
+ """Convert to JSON-serializable dictionary matching legacy schema format."""
178
+ return {
179
+ "title": self.title,
180
+ "url": self.url,
181
+ "sections": self.sections,
182
+ "inputs": [vars(i) for i in self.inputs],
183
+ "dropdowns": [vars(d) for d in self.dropdowns],
184
+ "choices": [vars(c) for c in self.choices],
185
+ "file_uploads": [vars(f) for f in self.file_uploads],
186
+ "already_uploaded_files": self.already_uploaded_files,
187
+ "buttons": [vars(b) for b in self.buttons],
188
+ }
189
+
190
+ @classmethod
191
+ def from_dict(cls, data: Dict[str, Any]) -> "InspectionResult":
192
+ """Instantiate DTO from raw inspection dictionary."""
193
+ return cls(
194
+ title=data.get("title", ""),
195
+ url=data.get("url", ""),
196
+ sections=list(data.get("sections", [])),
197
+ inputs=[InputField(**i) if isinstance(i, dict) else i for i in data.get("inputs", [])],
198
+ dropdowns=[DropdownField(**d) if isinstance(d, dict) else d for d in data.get("dropdowns", [])],
199
+ choices=[ChoiceGroup(**c) if isinstance(c, dict) else c for c in data.get("choices", [])],
200
+ file_uploads=[FileUploadField(**f) if isinstance(f, dict) else f for f in data.get("file_uploads", [])],
201
+ already_uploaded_files=list(data.get("already_uploaded_files", [])),
202
+ buttons=[ActionButton(**b) if isinstance(b, dict) else b for b in data.get("buttons", [])],
203
+ )
204
+
205
+
206
+ @dataclass
207
+ class FillSummary:
208
+ """Summary record for batch sequential form filling."""
209
+ confirmed: List[Tuple[str, Any]] = field(default_factory=list)
210
+ unconfirmed: List[Tuple[str, Any]] = field(default_factory=list)
211
+ failed: List[Tuple[str, Any]] = field(default_factory=list)
212
+
213
+ def to_dict(self) -> Dict[str, List[Tuple[str, Any]]]:
214
+ """Return standard dictionary format matching legacy API."""
215
+ return {
216
+ "confirmed": list(self.confirmed),
217
+ "unconfirmed": list(self.unconfirmed),
218
+ "failed": list(self.failed),
219
+ }
220
+
221
+
222
+ @dataclass
223
+ class ReactiveClickOutcome:
224
+ """Outcome and reactive environment mutations triggered by a button press."""
225
+ success: bool
226
+ target: str
227
+ redirected: bool = False
228
+ new_window: bool = False
229
+ new_url: Optional[str] = None
230
+ modal_opened: bool = False
231
+ modal_text: str = ""
232
+ new_inputs_count: int = 0
233
+ schema: Optional[Dict[str, Any]] = None
234
+ alerts: List[str] = field(default_factory=list)
235
+
236
+ def to_dict(self) -> Dict[str, Any]:
237
+ """Convert to standard dictionary format matching legacy API."""
238
+ return {
239
+ "success": self.success,
240
+ "target": self.target,
241
+ "redirected": self.redirected,
242
+ "new_window": self.new_window,
243
+ "new_url": self.new_url,
244
+ "modal_opened": self.modal_opened,
245
+ "modal_text": self.modal_text,
246
+ "new_inputs_count": self.new_inputs_count,
247
+ "schema": self.schema,
248
+ "alerts": self.alerts,
249
+ }
250
+
251
+
252
+ @dataclass
253
+ class ApplyExecutionResult:
254
+ """Structured result returned upon completion of an apply flow."""
255
+ confirmed_fields: List[Tuple[str, Any]] = field(default_factory=list)
256
+ unconfirmed_fields: List[Tuple[str, Any]] = field(default_factory=list)
257
+ failed_fields: List[Tuple[str, Any]] = field(default_factory=list)
258
+ final_url: str = ""
259
+ validation_errors: List[str] = field(default_factory=list)
260
+ submitted: bool = False
261
+ confirmation_screenshot: Optional[str] = None
262
+
263
+ def to_dict(self) -> Dict[str, Any]:
264
+ """Convert to dictionary matching standard serialization."""
265
+ return {
266
+ "confirmed_fields": list(self.confirmed_fields),
267
+ "unconfirmed_fields": list(self.unconfirmed_fields),
268
+ "failed_fields": list(self.failed_fields),
269
+ "final_url": self.final_url,
270
+ "validation_errors": list(self.validation_errors),
271
+ "submitted": self.submitted,
272
+ "confirmation_screenshot": self.confirmation_screenshot,
273
+ }
engine.py ADDED
@@ -0,0 +1,199 @@
1
+ """Universal Form Engine Facade.
2
+
3
+ Maintains 100% backwards-compatible external behavior while delegating to
4
+ cohesive domain services and adapters.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+ from playwright.sync_api import Browser, BrowserContext, Page
11
+
12
+ from adapters.browser_adapter import BrowserAdapter
13
+ from adapters.upload_adapter import UploadAdapter
14
+ from config.settings import Settings, get_settings, load_device_profile
15
+ from core.models import DeviceProfile
16
+ from services.auth_navigation_service import AuthNavigationService
17
+ from services.field_interaction_service import FieldInteractionService
18
+ from services.form_filler_service import FormFillerService
19
+ from services.inspection_formatter_service import InspectionFormatterService
20
+ from services.inspection_service import InspectionService
21
+ from services.reactive_interaction_service import ReactiveInteractionService
22
+ from validators.form_validator import FormValidator
23
+
24
+
25
+ class UniversalFormEngine:
26
+ """Universal Form Engine facade coordinating browser lifecycle, inspection, and interaction services."""
27
+
28
+ def __init__(
29
+ self,
30
+ headless: Optional[bool] = None,
31
+ settings: Optional[Settings] = None,
32
+ device_profile: Optional[DeviceProfile] = None,
33
+ viewport: Optional[dict] = None,
34
+ user_agent: Optional[str] = None,
35
+ ):
36
+ self.settings = settings or get_settings()
37
+ self.device_profile = device_profile or load_device_profile()
38
+
39
+ if viewport:
40
+ self.device_profile.viewport_width = viewport.get("width", self.device_profile.viewport_width)
41
+ self.device_profile.viewport_height = viewport.get("height", self.device_profile.viewport_height)
42
+ if user_agent:
43
+ self.device_profile.user_agent = user_agent
44
+
45
+ self.headless = headless if headless is not None else self.settings.headless
46
+ self.viewport = self.device_profile.viewport_dict()
47
+ self.user_agent = self.device_profile.user_agent
48
+
49
+ self.browser_adapter = BrowserAdapter(
50
+ headless=self.headless,
51
+ settings=self.settings,
52
+ device_profile=self.device_profile,
53
+ )
54
+
55
+ # Service instances initialized upon start()
56
+ self.auth_service: Optional[AuthNavigationService] = None
57
+ self.inspection_service: Optional[InspectionService] = None
58
+ self.field_service: Optional[FieldInteractionService] = None
59
+ self.filler_service: Optional[FormFillerService] = None
60
+ self.upload_adapter: Optional[UploadAdapter] = None
61
+ self.validator: Optional[FormValidator] = None
62
+ self.reactive_service: Optional[ReactiveInteractionService] = None
63
+
64
+ @property
65
+ def browser(self) -> Optional[Browser]:
66
+ """Underlying Playwright Browser."""
67
+ return self.browser_adapter.browser
68
+
69
+ @property
70
+ def context(self) -> Optional[BrowserContext]:
71
+ """Active Playwright BrowserContext."""
72
+ return self.browser_adapter.context
73
+
74
+ @property
75
+ def page(self) -> Optional[Page]:
76
+ """Active Playwright Page."""
77
+ return self.browser_adapter.page
78
+
79
+ @page.setter
80
+ def page(self, p: Page) -> None:
81
+ self.browser_adapter.page = p
82
+ if self._services_ready():
83
+ self._bind_services(p)
84
+
85
+ def _services_ready(self) -> bool:
86
+ return self.auth_service is not None
87
+
88
+ def _bind_services(self, active_page: Page) -> None:
89
+ """Binds active page to services and adapters."""
90
+ ctx = self.context
91
+ assert ctx is not None
92
+ self.auth_service = AuthNavigationService(active_page, ctx)
93
+ self.inspection_service = InspectionService(active_page)
94
+ self.field_service = FieldInteractionService(active_page)
95
+ self.filler_service = FormFillerService(active_page, self.field_service)
96
+ self.upload_adapter = UploadAdapter(active_page)
97
+ self.validator = FormValidator(active_page)
98
+ self.reactive_service = ReactiveInteractionService(
99
+ page=active_page,
100
+ context=ctx,
101
+ inspection_service=self.inspection_service,
102
+ form_validator=self.validator,
103
+ on_active_page_change=self._on_active_page_change,
104
+ )
105
+
106
+ def _on_active_page_change(self, new_page: Page) -> None:
107
+ self.browser_adapter.page = new_page
108
+ self._bind_services(new_page)
109
+
110
+ def start(self, cookies_path: Optional[str] = None) -> None:
111
+ """Starts Playwright browser instance, initializes context, and prepares services."""
112
+ self.browser_adapter.start(cookies_path=cookies_path)
113
+ assert self.page is not None
114
+ self._bind_services(self.page)
115
+
116
+ def stop(self) -> None:
117
+ """Closes browser context and Playwright instance."""
118
+ self.browser_adapter.stop()
119
+
120
+ def navigate(self, url: str, wait_timeout: Optional[int] = None) -> None:
121
+ """Navigates to URL and waits for DOM content loaded."""
122
+ self.browser_adapter.navigate(url=url, wait_timeout=wait_timeout)
123
+
124
+ def handle_login_if_needed(self, email: str, password: str, cookies_save_path: Optional[str] = None) -> bool:
125
+ """Detects if page is a login/sign-in page and completes authentication automatically."""
126
+ assert self.auth_service is not None, "Engine not started."
127
+ return self.auth_service.handle_login_if_needed(email, password, cookies_save_path)
128
+
129
+ def bypass_job_landing_portal(self) -> bool:
130
+ """Generic helper: Clicks Apply Now on portal landing pages to enter ATS application."""
131
+ assert self.auth_service is not None, "Engine not started."
132
+ res = self.auth_service.bypass_job_landing_portal()
133
+ if self.page != self.auth_service.page:
134
+ self.page = self.auth_service.page
135
+ return res
136
+
137
+ def wait_for_form_ready(self, timeout_ms: Optional[int] = None) -> None:
138
+ """Waits for form elements to be rendered in DOM, dismissing 'Loading...' state."""
139
+ assert self.auth_service is not None, "Engine not started."
140
+ effective_timeout = timeout_ms if timeout_ms is not None else self.settings.form_ready_timeout_ms
141
+ self.auth_service.wait_for_form_ready(timeout_ms=effective_timeout)
142
+
143
+ def expand_all_sections(self) -> int:
144
+ """Safely expands any collapsed form sections without toggling already-open ones."""
145
+ assert self.auth_service is not None, "Engine not started."
146
+ return self.auth_service.expand_all_sections(wait_ms=self.settings.expand_section_wait_ms)
147
+
148
+ def fill_form_sequential(self, field_mappings: List[Tuple[str, Any]]) -> Dict[str, List[Tuple[str, Any]]]:
149
+ """Universal sequential form filler (section-by-section + global fallback)."""
150
+ assert self.filler_service is not None, "Engine not started."
151
+ return self.filler_service.fill_form_sequential(field_mappings)
152
+
153
+ def inspect(self, unpack_options: bool = True) -> Dict[str, Any]:
154
+ """Deep inspection of the active page form components and schema."""
155
+ assert self.auth_service is not None and self.inspection_service is not None, "Engine not started."
156
+ self.wait_for_form_ready()
157
+ self.expand_all_sections()
158
+ return self.inspection_service.inspect(unpack_options=unpack_options)
159
+
160
+ def format_inspection_summary(self, data: Dict[str, Any]) -> str:
161
+ """Returns a concise, human-readable summary of the inspected form."""
162
+ return InspectionFormatterService.format_summary(data)
163
+
164
+ def set_field(self, target_identifier: str, value: Any) -> bool:
165
+ """Sets form field value across text, picklist, radio, checkbox widgets."""
166
+ assert self.field_service is not None, "Engine not started."
167
+ return self.field_service.set_field(target_identifier, value)
168
+
169
+ def _playwright_click_fallback(self, target: str, value: Any) -> bool:
170
+ """Fallback setter using Playwright native locators (click / fill)."""
171
+ assert self.field_service is not None, "Engine not started."
172
+ return self.field_service._playwright_click_fallback(target, value)
173
+
174
+ def verify_field_value(self, target: str, expected: Any) -> bool:
175
+ """Reads back current DOM value of field and checks if it matches expected."""
176
+ assert self.field_service is not None, "Engine not started."
177
+ return self.field_service.verify_field_value(target, expected)
178
+
179
+ def upload_document(self, file_path: str, document_type_keyword: str = "") -> bool:
180
+ """Universally attaches a document to file inputs or custom ATS chooser triggers."""
181
+ assert self.upload_adapter is not None, "Engine not started."
182
+ return self.upload_adapter.upload_document(file_path, document_type_keyword)
183
+
184
+ def click_button(self, button_text_or_purpose: str) -> Dict[str, Any]:
185
+ """Intelligently clicks a button/link and monitors reactive environmental mutations."""
186
+ assert self.reactive_service is not None, "Engine not started."
187
+ outcome = self.reactive_service.click_button(button_text_or_purpose)
188
+ if outcome.get("new_window") and self.page != self.reactive_service.page:
189
+ self.page = self.reactive_service.page
190
+ return outcome
191
+
192
+ def get_validation_errors(self) -> List[str]:
193
+ """Collects any error alerts, validation messages, or warning texts currently shown."""
194
+ assert self.validator is not None, "Engine not started."
195
+ return self.validator.get_validation_errors()
196
+
197
+ def capture_screenshot(self, output_path: str, full_page: bool = False) -> None:
198
+ """Saves a screenshot of the current page."""
199
+ self.browser_adapter.capture_screenshot(output_path=output_path, full_page=full_page)
exceptions.py ADDED
@@ -0,0 +1,23 @@
1
+ """Backwards compatibility shim for exceptions."""
2
+
3
+ from core.exceptions import (
4
+ UniversalApplierError,
5
+ ConfigurationError,
6
+ ValidationError,
7
+ AutomationError,
8
+ ElementNotFoundError,
9
+ InteractionTimeoutError,
10
+ DocumentUploadError,
11
+ AuthenticationError,
12
+ )
13
+
14
+ __all__ = [
15
+ "UniversalApplierError",
16
+ "ConfigurationError",
17
+ "ValidationError",
18
+ "AutomationError",
19
+ "ElementNotFoundError",
20
+ "InteractionTimeoutError",
21
+ "DocumentUploadError",
22
+ "AuthenticationError",
23
+ ]