intuned-browser 0.1.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 (90) hide show
  1. intuned_browser/__init__.py +35 -0
  2. intuned_browser/ai/__init__.py +19 -0
  3. intuned_browser/ai/extract_structured_data.py +409 -0
  4. intuned_browser/ai/extract_structured_data_using_ai.py +269 -0
  5. intuned_browser/ai/is_page_loaded.py +97 -0
  6. intuned_browser/ai/prompts/extract_structured_data_prompt.py +107 -0
  7. intuned_browser/ai/tests/test_extract_structured_data.py +1229 -0
  8. intuned_browser/ai/tests/test_is_page_loaded.py +297 -0
  9. intuned_browser/ai/tests/test_matching.py +296 -0
  10. intuned_browser/ai/tests/test_validate_schema.py +259 -0
  11. intuned_browser/ai/tools/extraction_tools.py +31 -0
  12. intuned_browser/ai/types/__init__.py +27 -0
  13. intuned_browser/ai/types/content_items.py +161 -0
  14. intuned_browser/ai/types/data_extraction_errors.py +7 -0
  15. intuned_browser/ai/types/extract_data_output.py +18 -0
  16. intuned_browser/ai/types/json_schema.py +111 -0
  17. intuned_browser/ai/types/mathcing_types.py +25 -0
  18. intuned_browser/ai/types/models.py +77 -0
  19. intuned_browser/ai/types/running_environment.py +6 -0
  20. intuned_browser/ai/utils/__init__.py +31 -0
  21. intuned_browser/ai/utils/build_images.py +104 -0
  22. intuned_browser/ai/utils/collect_strings.py +30 -0
  23. intuned_browser/ai/utils/convert_string_spaces.py +5 -0
  24. intuned_browser/ai/utils/create_messages.py +46 -0
  25. intuned_browser/ai/utils/matching/__init__.py +17 -0
  26. intuned_browser/ai/utils/matching/matching.py +321 -0
  27. intuned_browser/ai/utils/safe_json_loads.py +119 -0
  28. intuned_browser/ai/utils/validate_schema.py +114 -0
  29. intuned_browser/common/__init__.py +13 -0
  30. intuned_browser/common/browser_scripts.js +2596 -0
  31. intuned_browser/common/ensure_browser_scripts.py +14 -0
  32. intuned_browser/common/get_ai_tracking_headers.py +46 -0
  33. intuned_browser/common/get_environment_variable.py +9 -0
  34. intuned_browser/common/hash_object.py +40 -0
  35. intuned_browser/common/tests/test_ensure_browser_scripts.py +22 -0
  36. intuned_browser/common/types.py +19 -0
  37. intuned_browser/helpers/__init__.py +35 -0
  38. intuned_browser/helpers/download_file.py +142 -0
  39. intuned_browser/helpers/extract_markdown.py +19 -0
  40. intuned_browser/helpers/filter_empty_values.py +55 -0
  41. intuned_browser/helpers/go_to_url.py +109 -0
  42. intuned_browser/helpers/process_dates.py +15 -0
  43. intuned_browser/helpers/resolve_url.py +98 -0
  44. intuned_browser/helpers/sanitize_html.py +74 -0
  45. intuned_browser/helpers/save_file_to_s3.py +40 -0
  46. intuned_browser/helpers/scroll_to_load_content.py +55 -0
  47. intuned_browser/helpers/tests/conftest.py +40 -0
  48. intuned_browser/helpers/tests/fixtures/test_matches.html +142 -0
  49. intuned_browser/helpers/tests/test_download_upload_files.py +162 -0
  50. intuned_browser/helpers/tests/test_extract_markdown.py +79 -0
  51. intuned_browser/helpers/tests/test_filter_empty_json_fields.py +49 -0
  52. intuned_browser/helpers/tests/test_get_simplified_html.py +275 -0
  53. intuned_browser/helpers/tests/test_go_to_url.py +71 -0
  54. intuned_browser/helpers/tests/test_process_dates.py +41 -0
  55. intuned_browser/helpers/tests/test_resolve_url.py +217 -0
  56. intuned_browser/helpers/tests/test_sanitize_html.py +305 -0
  57. intuned_browser/helpers/tests/test_upload_file_to_s3_test_e2e.py +77 -0
  58. intuned_browser/helpers/tests/test_validate_data_using_schema.py +153 -0
  59. intuned_browser/helpers/tests/test_wait_for_network_idle.py +108 -0
  60. intuned_browser/helpers/tests/test_with_dom_settled_wait.py +332 -0
  61. intuned_browser/helpers/types/__init__.py +8 -0
  62. intuned_browser/helpers/types/converter.py +151 -0
  63. intuned_browser/helpers/types/custom_type_registry.py +57 -0
  64. intuned_browser/helpers/types/uploaded_file.py +132 -0
  65. intuned_browser/helpers/types/validation_error.py +9 -0
  66. intuned_browser/helpers/upload_file.py +231 -0
  67. intuned_browser/helpers/utils/__init__.py +5 -0
  68. intuned_browser/helpers/utils/get_mode.py +5 -0
  69. intuned_browser/helpers/utils/get_s3_client.py +41 -0
  70. intuned_browser/helpers/utils/get_simplified_html.py +267 -0
  71. intuned_browser/helpers/validate_data_using_schema.py +44 -0
  72. intuned_browser/helpers/wait_for_dom_settled.py +314 -0
  73. intuned_browser/helpers/wait_for_network_settled.py +211 -0
  74. intuned_browser/intuned_services/__init__.py +7 -0
  75. intuned_browser/intuned_services/api_gateways/__init__.py +3 -0
  76. intuned_browser/intuned_services/api_gateways/ai_api_gateway.py +129 -0
  77. intuned_browser/intuned_services/api_gateways/factory.py +11 -0
  78. intuned_browser/intuned_services/api_gateways/models.py +9 -0
  79. intuned_browser/intuned_services/api_gateways/tests/test_api_gateway.py +156 -0
  80. intuned_browser/intuned_services/api_gateways/types.py +35 -0
  81. intuned_browser/intuned_services/blob_storage/__init__.py +0 -0
  82. intuned_browser/intuned_services/blob_storage/blob_storage.py +0 -0
  83. intuned_browser/intuned_services/cache/__init__.py +4 -0
  84. intuned_browser/intuned_services/cache/cache.py +68 -0
  85. intuned_browser/intuned_services/cache/tests/test_cache.py +98 -0
  86. intuned_browser/intuned_services/cache/types.py +15 -0
  87. intuned_browser-0.1.0.dist-info/METADATA +45 -0
  88. intuned_browser-0.1.0.dist-info/RECORD +90 -0
  89. intuned_browser-0.1.0.dist-info/WHEEL +4 -0
  90. intuned_browser-0.1.0.dist-info/licenses/LICENSE +42 -0
@@ -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