intuned-browser 0.1.0__tar.gz

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 (90) hide show
  1. intuned_browser-0.1.0/LICENSE +42 -0
  2. intuned_browser-0.1.0/PKG-INFO +45 -0
  3. intuned_browser-0.1.0/README.md +3 -0
  4. intuned_browser-0.1.0/intuned_browser/__init__.py +35 -0
  5. intuned_browser-0.1.0/intuned_browser/ai/__init__.py +19 -0
  6. intuned_browser-0.1.0/intuned_browser/ai/extract_structured_data.py +409 -0
  7. intuned_browser-0.1.0/intuned_browser/ai/extract_structured_data_using_ai.py +269 -0
  8. intuned_browser-0.1.0/intuned_browser/ai/is_page_loaded.py +97 -0
  9. intuned_browser-0.1.0/intuned_browser/ai/prompts/extract_structured_data_prompt.py +107 -0
  10. intuned_browser-0.1.0/intuned_browser/ai/tests/test_extract_structured_data.py +1229 -0
  11. intuned_browser-0.1.0/intuned_browser/ai/tests/test_is_page_loaded.py +297 -0
  12. intuned_browser-0.1.0/intuned_browser/ai/tests/test_matching.py +296 -0
  13. intuned_browser-0.1.0/intuned_browser/ai/tests/test_validate_schema.py +259 -0
  14. intuned_browser-0.1.0/intuned_browser/ai/tools/extraction_tools.py +31 -0
  15. intuned_browser-0.1.0/intuned_browser/ai/types/__init__.py +27 -0
  16. intuned_browser-0.1.0/intuned_browser/ai/types/content_items.py +161 -0
  17. intuned_browser-0.1.0/intuned_browser/ai/types/data_extraction_errors.py +7 -0
  18. intuned_browser-0.1.0/intuned_browser/ai/types/extract_data_output.py +18 -0
  19. intuned_browser-0.1.0/intuned_browser/ai/types/json_schema.py +111 -0
  20. intuned_browser-0.1.0/intuned_browser/ai/types/mathcing_types.py +25 -0
  21. intuned_browser-0.1.0/intuned_browser/ai/types/models.py +77 -0
  22. intuned_browser-0.1.0/intuned_browser/ai/types/running_environment.py +6 -0
  23. intuned_browser-0.1.0/intuned_browser/ai/utils/__init__.py +31 -0
  24. intuned_browser-0.1.0/intuned_browser/ai/utils/build_images.py +104 -0
  25. intuned_browser-0.1.0/intuned_browser/ai/utils/collect_strings.py +30 -0
  26. intuned_browser-0.1.0/intuned_browser/ai/utils/convert_string_spaces.py +5 -0
  27. intuned_browser-0.1.0/intuned_browser/ai/utils/create_messages.py +46 -0
  28. intuned_browser-0.1.0/intuned_browser/ai/utils/matching/__init__.py +17 -0
  29. intuned_browser-0.1.0/intuned_browser/ai/utils/matching/matching.py +321 -0
  30. intuned_browser-0.1.0/intuned_browser/ai/utils/safe_json_loads.py +119 -0
  31. intuned_browser-0.1.0/intuned_browser/ai/utils/validate_schema.py +114 -0
  32. intuned_browser-0.1.0/intuned_browser/common/__init__.py +13 -0
  33. intuned_browser-0.1.0/intuned_browser/common/browser_scripts.js +2596 -0
  34. intuned_browser-0.1.0/intuned_browser/common/ensure_browser_scripts.py +14 -0
  35. intuned_browser-0.1.0/intuned_browser/common/get_ai_tracking_headers.py +46 -0
  36. intuned_browser-0.1.0/intuned_browser/common/get_environment_variable.py +9 -0
  37. intuned_browser-0.1.0/intuned_browser/common/hash_object.py +40 -0
  38. intuned_browser-0.1.0/intuned_browser/common/tests/test_ensure_browser_scripts.py +22 -0
  39. intuned_browser-0.1.0/intuned_browser/common/types.py +19 -0
  40. intuned_browser-0.1.0/intuned_browser/helpers/__init__.py +35 -0
  41. intuned_browser-0.1.0/intuned_browser/helpers/download_file.py +142 -0
  42. intuned_browser-0.1.0/intuned_browser/helpers/extract_markdown.py +19 -0
  43. intuned_browser-0.1.0/intuned_browser/helpers/filter_empty_values.py +55 -0
  44. intuned_browser-0.1.0/intuned_browser/helpers/go_to_url.py +109 -0
  45. intuned_browser-0.1.0/intuned_browser/helpers/process_dates.py +15 -0
  46. intuned_browser-0.1.0/intuned_browser/helpers/resolve_url.py +98 -0
  47. intuned_browser-0.1.0/intuned_browser/helpers/sanitize_html.py +74 -0
  48. intuned_browser-0.1.0/intuned_browser/helpers/save_file_to_s3.py +40 -0
  49. intuned_browser-0.1.0/intuned_browser/helpers/scroll_to_load_content.py +55 -0
  50. intuned_browser-0.1.0/intuned_browser/helpers/tests/conftest.py +40 -0
  51. intuned_browser-0.1.0/intuned_browser/helpers/tests/fixtures/test_matches.html +142 -0
  52. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_download_upload_files.py +162 -0
  53. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_extract_markdown.py +79 -0
  54. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_filter_empty_json_fields.py +49 -0
  55. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_get_simplified_html.py +275 -0
  56. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_go_to_url.py +71 -0
  57. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_process_dates.py +41 -0
  58. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_resolve_url.py +217 -0
  59. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_sanitize_html.py +305 -0
  60. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_upload_file_to_s3_test_e2e.py +77 -0
  61. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_validate_data_using_schema.py +153 -0
  62. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_wait_for_network_idle.py +108 -0
  63. intuned_browser-0.1.0/intuned_browser/helpers/tests/test_with_dom_settled_wait.py +332 -0
  64. intuned_browser-0.1.0/intuned_browser/helpers/types/__init__.py +8 -0
  65. intuned_browser-0.1.0/intuned_browser/helpers/types/converter.py +151 -0
  66. intuned_browser-0.1.0/intuned_browser/helpers/types/custom_type_registry.py +57 -0
  67. intuned_browser-0.1.0/intuned_browser/helpers/types/uploaded_file.py +132 -0
  68. intuned_browser-0.1.0/intuned_browser/helpers/types/validation_error.py +9 -0
  69. intuned_browser-0.1.0/intuned_browser/helpers/upload_file.py +231 -0
  70. intuned_browser-0.1.0/intuned_browser/helpers/utils/__init__.py +5 -0
  71. intuned_browser-0.1.0/intuned_browser/helpers/utils/get_mode.py +5 -0
  72. intuned_browser-0.1.0/intuned_browser/helpers/utils/get_s3_client.py +41 -0
  73. intuned_browser-0.1.0/intuned_browser/helpers/utils/get_simplified_html.py +267 -0
  74. intuned_browser-0.1.0/intuned_browser/helpers/validate_data_using_schema.py +44 -0
  75. intuned_browser-0.1.0/intuned_browser/helpers/wait_for_dom_settled.py +314 -0
  76. intuned_browser-0.1.0/intuned_browser/helpers/wait_for_network_settled.py +211 -0
  77. intuned_browser-0.1.0/intuned_browser/intuned_services/__init__.py +7 -0
  78. intuned_browser-0.1.0/intuned_browser/intuned_services/api_gateways/__init__.py +3 -0
  79. intuned_browser-0.1.0/intuned_browser/intuned_services/api_gateways/ai_api_gateway.py +129 -0
  80. intuned_browser-0.1.0/intuned_browser/intuned_services/api_gateways/factory.py +11 -0
  81. intuned_browser-0.1.0/intuned_browser/intuned_services/api_gateways/models.py +9 -0
  82. intuned_browser-0.1.0/intuned_browser/intuned_services/api_gateways/tests/test_api_gateway.py +156 -0
  83. intuned_browser-0.1.0/intuned_browser/intuned_services/api_gateways/types.py +35 -0
  84. intuned_browser-0.1.0/intuned_browser/intuned_services/blob_storage/__init__.py +0 -0
  85. intuned_browser-0.1.0/intuned_browser/intuned_services/blob_storage/blob_storage.py +0 -0
  86. intuned_browser-0.1.0/intuned_browser/intuned_services/cache/__init__.py +4 -0
  87. intuned_browser-0.1.0/intuned_browser/intuned_services/cache/cache.py +68 -0
  88. intuned_browser-0.1.0/intuned_browser/intuned_services/cache/tests/test_cache.py +98 -0
  89. intuned_browser-0.1.0/intuned_browser/intuned_services/cache/types.py +15 -0
  90. intuned_browser-0.1.0/pyproject.toml +81 -0
@@ -0,0 +1,42 @@
1
+ Acceptance
2
+ By using the software, you agree to all of the terms and conditions below.
3
+
4
+ Copyright License
5
+ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below.
6
+
7
+ Limitations
8
+ You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
9
+
10
+ You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
11
+
12
+ You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
13
+
14
+ Patents
15
+ The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
16
+
17
+ Notices
18
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
19
+
20
+ If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
21
+
22
+ No Other Rights
23
+ These terms do not imply any licenses other than those expressly granted in these terms.
24
+
25
+ Termination
26
+ If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
27
+
28
+ No Liability
29
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
30
+
31
+ Definitions
32
+ The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it.
33
+
34
+ you refers to the individual or entity agreeing to these terms.
35
+
36
+ your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
37
+
38
+ your licenses are all the licenses granted to you for the software under these terms.
39
+
40
+ use means anything you do with the software requiring one of your licenses.
41
+
42
+ trademark means trademarks, service marks, and similar rights.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: intuned-browser
3
+ Version: 0.1.0
4
+ Summary: Intuned Browser SDK
5
+ License: Elastic-2.0
6
+ License-File: LICENSE
7
+ Keywords: sdk,intuned
8
+ Author: Intuned Developers
9
+ Author-email: engineering@intunedhq.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Dist: Levenshtein (==0.26.0)
21
+ Requires-Dist: aioboto3 (>=15.2.0,<16.0.0)
22
+ Requires-Dist: aiofiles (>=24.1.0)
23
+ Requires-Dist: anthropic (>=0.47.1,<0.48.0)
24
+ Requires-Dist: beautifulsoup4 (>=4.12.3)
25
+ Requires-Dist: botocore (>=1.34.70)
26
+ Requires-Dist: diskcache (>=5.6.3)
27
+ Requires-Dist: fuzzysearch (>=0.7.3)
28
+ Requires-Dist: google-generativeai (>=0.8.2)
29
+ Requires-Dist: httpx (==0.27.2)
30
+ Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
31
+ Requires-Dist: litellm (==1.67.2)
32
+ Requires-Dist: mdformat (>=0.7.18,<0.8.0)
33
+ Requires-Dist: openai (>=1.42.0)
34
+ Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
35
+ Requires-Dist: python-dotenv (>=1.0.1)
36
+ Requires-Dist: ruff (>=0.7.2,<0.8.0)
37
+ Requires-Dist: tenacity (==8.5.0)
38
+ Requires-Dist: termcolor (>=2.5.0,<3.0.0)
39
+ Requires-Dist: validators (>=0.34.0)
40
+ Description-Content-Type: text/markdown
41
+
42
+ # Intuned Browser
43
+
44
+ A set of common utilities for Intuned services, useful for web scraping with Playwright and for integration into Intuned platform projects.
45
+
@@ -0,0 +1,3 @@
1
+ # Intuned Browser
2
+
3
+ A set of common utilities for Intuned services, useful for web scraping with Playwright and for integration into Intuned platform projects.
@@ -0,0 +1,35 @@
1
+ from intuned_browser.helpers.download_file import download_file
2
+ from intuned_browser.helpers.extract_markdown import extract_markdown
3
+ from intuned_browser.helpers.filter_empty_values import filter_empty_values
4
+ from intuned_browser.helpers.go_to_url import go_to_url
5
+ from intuned_browser.helpers.process_dates import process_date
6
+ from intuned_browser.helpers.resolve_url import resolve_url
7
+ from intuned_browser.helpers.sanitize_html import sanitize_html
8
+ from intuned_browser.helpers.save_file_to_s3 import save_file_to_s3
9
+ from intuned_browser.helpers.scroll_to_load_content import scroll_to_load_content
10
+ from intuned_browser.helpers.types import Attachment
11
+ from intuned_browser.helpers.types import S3Configs
12
+ from intuned_browser.helpers.types import ValidationError
13
+ from intuned_browser.helpers.upload_file import upload_file_to_s3
14
+ from intuned_browser.helpers.validate_data_using_schema import validate_data_using_schema
15
+ from intuned_browser.helpers.wait_for_dom_settled import wait_for_dom_settled
16
+ from intuned_browser.helpers.wait_for_network_settled import wait_for_network_settled
17
+
18
+ __all__ = [
19
+ "extract_markdown",
20
+ "sanitize_html",
21
+ "resolve_url",
22
+ "download_file",
23
+ "filter_empty_values",
24
+ "go_to_url",
25
+ "scroll_to_load_content",
26
+ "process_date",
27
+ "save_file_to_s3",
28
+ "upload_file_to_s3",
29
+ "validate_data_using_schema",
30
+ "wait_for_network_settled",
31
+ "wait_for_dom_settled",
32
+ "Attachment",
33
+ "S3Configs",
34
+ "ValidationError",
35
+ ]
@@ -0,0 +1,19 @@
1
+ from intuned_browser.ai.extract_structured_data import extract_structured_data
2
+ from intuned_browser.ai.is_page_loaded import is_page_loaded
3
+ from intuned_browser.ai.types import ContentItem
4
+ from intuned_browser.ai.types import DataExtractionError
5
+ from intuned_browser.ai.types import ImageBufferContentItem
6
+ from intuned_browser.ai.types import ImageObject
7
+ from intuned_browser.ai.types import ImageUrlContentItem
8
+ from intuned_browser.ai.types import TextContentItem
9
+
10
+ __all__ = [
11
+ "extract_structured_data",
12
+ "is_page_loaded",
13
+ "TextContentItem",
14
+ "ContentItem",
15
+ "ImageBufferContentItem",
16
+ "ImageObject",
17
+ "ImageUrlContentItem",
18
+ "DataExtractionError",
19
+ ]
@@ -0,0 +1,409 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Literal
4
+ from typing import overload
5
+
6
+ from playwright.async_api import Locator
7
+ from playwright.async_api import Page
8
+
9
+ from intuned_browser.ai.extract_structured_data_using_ai import extract_structured_data_using_ai
10
+ from intuned_browser.ai.types import ContentItem
11
+ from intuned_browser.ai.types import ImageObject
12
+ from intuned_browser.ai.types import JsonSchema # Import TypedDict for autocomplete
13
+ from intuned_browser.ai.types import SUPPORTED_MODELS
14
+ from intuned_browser.ai.utils import compress_string_spaces
15
+ from intuned_browser.ai.utils.build_images import build_images_from_page_or_handle
16
+ from intuned_browser.ai.utils.matching.matching import create_xpath_mapping
17
+ from intuned_browser.ai.utils.matching.matching import validate_xpath_mapping
18
+ from intuned_browser.ai.utils.validate_schema import check_all_types_are_strings
19
+ from intuned_browser.ai.utils.validate_schema import validate_schema
20
+ from intuned_browser.common.hash_object import hash_object
21
+ from intuned_browser.helpers.extract_markdown import extract_markdown
22
+ from intuned_browser.helpers.utils.get_simplified_html import get_simplified_html
23
+ from intuned_browser.intuned_services import cache
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # Overload for page/locator-based extraction
29
+ @overload
30
+ async def extract_structured_data(
31
+ source: Page | Locator,
32
+ data_schema: JsonSchema,
33
+ *,
34
+ prompt: str | None = None,
35
+ strategy: Literal["IMAGE", "MARKDOWN", "HTML"] = "HTML",
36
+ model: SUPPORTED_MODELS = "claude-3-5-haiku-latest",
37
+ api_key: str | None = None,
38
+ enable_dom_matching: bool | None = False,
39
+ enable_cache: bool | None = True,
40
+ max_retires: int | None = 3,
41
+ ) -> Any: ...
42
+
43
+
44
+ # Overload for content-based extraction
45
+ @overload
46
+ async def extract_structured_data(
47
+ content: list[ContentItem] | ContentItem,
48
+ data_schema: JsonSchema,
49
+ *,
50
+ prompt: str | None = None,
51
+ max_retires: int | None = 3,
52
+ enable_cache: bool | None = True,
53
+ model: SUPPORTED_MODELS = "claude-3-5-haiku-latest",
54
+ api_key: str | None = None,
55
+ ) -> Any: ...
56
+
57
+
58
+ async def extract_structured_data(
59
+ source: Page | Locator | None = None,
60
+ data_schema: JsonSchema | None = None,
61
+ *,
62
+ content: list[ContentItem] | ContentItem | None = None,
63
+ prompt: str | None = None,
64
+ strategy: Literal["IMAGE", "MARKDOWN", "HTML"] = "HTML",
65
+ enable_dom_matching: bool | None = False,
66
+ enable_cache: bool | None = True,
67
+ max_retires: int | None = 3,
68
+ model: SUPPORTED_MODELS = "claude-3-5-haiku-latest",
69
+ api_key: str | None = None,
70
+ ) -> Any:
71
+ # Handle content-based extraction
72
+ if content is not None and source is None:
73
+ if data_schema is None:
74
+ raise ValueError("data_schema is required for content-based extraction")
75
+ return await _extract_structured_data_from_content(
76
+ content=content,
77
+ data_schema=data_schema,
78
+ prompt=prompt,
79
+ model=model,
80
+ api_key=api_key,
81
+ max_retires=max_retires,
82
+ enable_cache=enable_cache,
83
+ )
84
+
85
+ # Handle page/locator-based extraction
86
+ if source is None or data_schema is None:
87
+ raise ValueError("source and data_schema are required for page/locator-based extraction")
88
+
89
+ page_or_locator = source
90
+
91
+ # Validate input using pydantic models
92
+ try:
93
+ # Convert TypedDict to Pydantic for validation
94
+ parsed_schema = validate_schema(data_schema)
95
+ if not parsed_schema:
96
+ raise ValueError("Invalid data schema provided.")
97
+ except Exception as e:
98
+ raise ValueError(f"Invalid extract data input: {str(e)}") from e
99
+
100
+ page_object = page_or_locator if isinstance(page_or_locator, Page) else page_or_locator.page
101
+
102
+ # deny DOM matching if the schema is not valid
103
+ if enable_dom_matching and not check_all_types_are_strings(parsed_schema):
104
+ raise ValueError("For DOM matching, all types of the extraction fields must be STRINGS, to match with the DOM.")
105
+
106
+ # We have 3 kind of extractions: image, html and markdown. These 3 strategies will call the same model, the only difference is what we pass to it.
107
+ if strategy == "HTML":
108
+ if not isinstance(page_or_locator, Page):
109
+ container_handle = await page_or_locator.element_handle()
110
+ else:
111
+ container_handle = await page_or_locator.locator("html").element_handle()
112
+
113
+ # Get simplified HTML
114
+ simplified_html = await get_simplified_html(container_handle)
115
+ cache_key = None
116
+ # Create cache key
117
+ if enable_cache:
118
+ cache_key = hash_object(
119
+ {
120
+ "pageUrl": page_object.url,
121
+ "data_schema": parsed_schema.model_dump()
122
+ if hasattr(parsed_schema, "model_dump")
123
+ else parsed_schema,
124
+ "model": model,
125
+ "strategy": strategy,
126
+ "prompt": prompt,
127
+ "search_region": str(page_or_locator) if isinstance(page_or_locator, Locator) else "",
128
+ **({"html": compress_string_spaces(simplified_html)} if not enable_dom_matching else {}),
129
+ },
130
+ treat_arrays_as_unsorted_lists=True,
131
+ )
132
+
133
+ # Check cache
134
+ cached_result = await cache.get(cache_key)
135
+ if cached_result:
136
+ # dom matching is enabled and the result is cached, check if xpaths still have the matched text
137
+ if enable_dom_matching and isinstance(cached_result, dict) and "matchesMapping" in cached_result:
138
+ # Validate DOM matches using XPath mapping
139
+ is_valid = await validate_xpath_mapping(page_object, cached_result["matchesMapping"])
140
+ if is_valid:
141
+ logger.info("Returning cached result with valid DOM matching")
142
+ return cached_result["result"]
143
+ elif not enable_dom_matching:
144
+ logger.info("Returning cached result")
145
+ return cached_result
146
+
147
+ # Extract using AI
148
+ result = await extract_structured_data_using_ai(
149
+ page=page_object,
150
+ api_key=api_key,
151
+ enable_dom_matching=enable_dom_matching,
152
+ json_schema=data_schema,
153
+ model=model,
154
+ content=simplified_html,
155
+ prompt=prompt,
156
+ images=[],
157
+ max_retries=max_retires,
158
+ )
159
+ if not result:
160
+ raise Exception(result["error"]["context"])
161
+
162
+ if enable_cache:
163
+ # Cache result
164
+ if not enable_dom_matching:
165
+ await cache.set(cache_key, result.extracted_data)
166
+ else:
167
+ # Create XPath mapping for DOM validation
168
+ dom_validation_hash = await create_xpath_mapping(page_object, result.extracted_data)
169
+ results_to_cache = {"result": result.extracted_data, "matchesMapping": dom_validation_hash}
170
+ await cache.set(cache_key, results_to_cache)
171
+
172
+ return result.extracted_data
173
+
174
+ elif strategy == "IMAGE":
175
+ # Get container handle
176
+ container_handle = None
177
+ if isinstance(page_or_locator, Locator):
178
+ container_handle = await page_or_locator.element_handle()
179
+
180
+ # Build images
181
+ images = await build_images_from_page_or_handle(page_object, container_handle)
182
+
183
+ # Create cache key
184
+ cache_key = hash_object(
185
+ {
186
+ "pageUrl": page_object.url,
187
+ "data_schema": parsed_schema.model_dump() if hasattr(parsed_schema, "model_dump") else parsed_schema,
188
+ "model": model,
189
+ "strategy": strategy,
190
+ "prompt": prompt,
191
+ "search_region": str(page_or_locator) if isinstance(page_or_locator, Locator) else "",
192
+ **({"html": await page_object.locator("html").inner_html()} if not enable_dom_matching else {}),
193
+ },
194
+ treat_arrays_as_unsorted_lists=True,
195
+ )
196
+
197
+ # Check cache
198
+ if enable_cache:
199
+ cached_result = await cache.get(cache_key)
200
+ if cached_result:
201
+ if enable_dom_matching and isinstance(cached_result, dict) and "matchesMapping" in cached_result:
202
+ # Validate DOM matches using XPath mapping
203
+ is_valid = await validate_xpath_mapping(page_object, cached_result["matchesMapping"])
204
+ if is_valid:
205
+ logger.info("Returning cached result with valid DOM matching")
206
+ return cached_result["result"]
207
+ elif not enable_dom_matching:
208
+ logger.info("Returning cached result")
209
+ return cached_result
210
+
211
+ # Extract using AI
212
+ result = await extract_structured_data_using_ai(
213
+ page=page_object,
214
+ api_key=api_key,
215
+ enable_dom_matching=enable_dom_matching,
216
+ json_schema=data_schema,
217
+ model=model,
218
+ content="Extract structured data from the following images.",
219
+ prompt=prompt,
220
+ images=images,
221
+ max_retries=max_retires,
222
+ )
223
+
224
+ if not result:
225
+ raise Exception(result["error"]["context"])
226
+
227
+ # Cache result
228
+ if enable_cache:
229
+ if not enable_dom_matching:
230
+ await cache.set(cache_key, result.extracted_data)
231
+ else:
232
+ # Create XPath mapping for DOM validation
233
+ dom_validation_hash = await create_xpath_mapping(page_object, result.extracted_data)
234
+ results_to_cache = {"result": result.extracted_data, "matchesMapping": dom_validation_hash}
235
+ await cache.set(cache_key, results_to_cache)
236
+
237
+ return result.extracted_data
238
+
239
+ elif strategy == "MARKDOWN":
240
+ # Get container handle
241
+ container_handle = None
242
+ if isinstance(page_or_locator, Locator):
243
+ container_handle = await page_or_locator.element_handle()
244
+ else:
245
+ container_handle = await page_or_locator.locator("html").element_handle()
246
+
247
+ # Get page metadata
248
+ page_title = await page_object.title()
249
+ page_url = page_object.url
250
+
251
+ # Convert HTML to markdown
252
+ markdown_content = await extract_markdown(page_object)
253
+
254
+ cache_key = hash_object(
255
+ {
256
+ "pageUrl": page_object.url,
257
+ "data_schema": parsed_schema.model_dump() if hasattr(parsed_schema, "model_dump") else parsed_schema,
258
+ "model": model,
259
+ "strategy": strategy,
260
+ "prompt": prompt,
261
+ "search_region": str(page_or_locator) if isinstance(page_or_locator, Locator) else "",
262
+ **(
263
+ {"html": await page_object.locator("html").inner_html(), "markdown": markdown_content}
264
+ if not enable_dom_matching
265
+ else {}
266
+ ),
267
+ },
268
+ treat_arrays_as_unsorted_lists=True,
269
+ )
270
+
271
+ # Check cache
272
+ if enable_cache:
273
+ cached_result = await cache.get(cache_key)
274
+ if cached_result:
275
+ if enable_dom_matching and isinstance(cached_result, dict) and "matchesMapping" in cached_result:
276
+ # Validate DOM matches using XPath mapping
277
+ is_valid = await validate_xpath_mapping(page_object, cached_result["matchesMapping"])
278
+ if is_valid:
279
+ logger.info("Returning cached result with valid DOM matching")
280
+ return cached_result["result"]
281
+ elif not enable_dom_matching:
282
+ logger.info("Returning cached result")
283
+ return cached_result
284
+
285
+ # Extract using AI
286
+ result = await extract_structured_data_using_ai(
287
+ page=page_object,
288
+ api_key=api_key,
289
+ enable_dom_matching=enable_dom_matching,
290
+ json_schema=data_schema,
291
+ model=model,
292
+ content=markdown_content,
293
+ prompt=prompt,
294
+ images=[],
295
+ max_retries=max_retires,
296
+ )
297
+ if not result:
298
+ raise Exception(result["error"]["context"])
299
+
300
+ # Cache result
301
+ if enable_cache:
302
+ if not enable_dom_matching:
303
+ await cache.set(cache_key, result.extracted_data)
304
+ else:
305
+ # Create XPath mapping for DOM validation
306
+ dom_validation_hash = await create_xpath_mapping(page_object, result.extracted_data)
307
+ results_to_cache = {"result": result.extracted_data, "matchesMapping": dom_validation_hash}
308
+ await cache.set(cache_key, results_to_cache)
309
+
310
+ return result.extracted_data
311
+
312
+
313
+ import aiohttp
314
+
315
+
316
+ async def _extract_structured_data_from_content(
317
+ content: list[ContentItem] | ContentItem,
318
+ data_schema: JsonSchema,
319
+ prompt: str | None = None,
320
+ model: SUPPORTED_MODELS = "claude-3-5-haiku-latest",
321
+ api_key: str | None = None,
322
+ max_retires: int | None = 3,
323
+ enable_cache: bool | None = True,
324
+ ) -> Any:
325
+ # Validate schema
326
+ try:
327
+ parsed_schema = validate_schema(data_schema)
328
+ if not parsed_schema:
329
+ raise ValueError("Invalid data schema provided.")
330
+ except Exception as e:
331
+ raise ValueError(f"Invalid extract data input: {str(e)}") from e
332
+
333
+ # Normalize content to list
334
+ content_list = content if isinstance(content, list) else [content]
335
+
336
+ # Process images from buffers - create ImageObject format
337
+ images_from_buffers: list[ImageObject] = [
338
+ {
339
+ "image_type": item["image_type"],
340
+ "data": item["data"],
341
+ }
342
+ for item in content_list
343
+ if item["type"] == "image-buffer"
344
+ ]
345
+
346
+ # Process images from URLs - create ImageObject format
347
+ images_from_urls: list[ImageObject] = []
348
+ for item in content_list:
349
+ if item["type"] == "image-url":
350
+ try:
351
+ async with aiohttp.ClientSession() as session:
352
+ async with session.get(item["data"], ssl=False) as response:
353
+ if response.status == 200:
354
+ buffer = await response.read()
355
+ images_from_urls.append(
356
+ {
357
+ "image_type": item["image_type"],
358
+ "data": buffer,
359
+ }
360
+ )
361
+ else:
362
+ raise Exception(f"HTTP {response.status}")
363
+ except Exception as e:
364
+ raise ValueError(f"Fetching image from URL {item['data']} failed: {e}") from e
365
+
366
+ # Combine all images as list[ImageObject]
367
+ images = images_from_urls + images_from_buffers
368
+
369
+ # Extract text content
370
+ texts = [item["data"] for item in content_list if item["type"] == "text"]
371
+
372
+ # Create cache key if caching is enabled
373
+ cache_key = None
374
+ if enable_cache:
375
+ cache_key = hash_object(
376
+ {
377
+ "system_message": prompt,
378
+ "images": [{"image_type": img["image_type"], "data": img["data"].hex()} for img in images],
379
+ "json_schema": parsed_schema.model_dump() if hasattr(parsed_schema, "model_dump") else parsed_schema,
380
+ "model": model,
381
+ "text": texts,
382
+ },
383
+ treat_arrays_as_unsorted_lists=False,
384
+ )
385
+
386
+ # Check cache
387
+ cached_result = await cache.get(cache_key)
388
+ if cached_result:
389
+ logger.info("Returning cached result")
390
+ return cached_result
391
+
392
+ # Extract using AI
393
+ result = await extract_structured_data_using_ai(
394
+ api_key=api_key,
395
+ enable_dom_matching=False,
396
+ json_schema=data_schema,
397
+ model=model,
398
+ content="\n".join(texts),
399
+ prompt=prompt,
400
+ images=images,
401
+ max_retries=max_retires,
402
+ )
403
+
404
+ # Cache result if caching is enabled
405
+ if enable_cache and cache_key:
406
+ await cache.set(cache_key, result.extracted_data)
407
+ if not result.extracted_data:
408
+ logger.warning("No extracted data found, returning None")
409
+ return result.extracted_data