lumivor 0.1.7__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ import base64
2
+
3
+ import pytest
4
+
5
+ from lumivor.browser.browser import Browser, BrowserConfig
6
+
7
+
8
+ @pytest.fixture
9
+ async def browser():
10
+ browser_service = Browser(config=BrowserConfig(headless=True))
11
+ yield browser_service
12
+
13
+ await browser_service.close()
14
+
15
+
16
+ # @pytest.mark.skip(reason='takes too long')
17
+ def test_take_full_page_screenshot(browser):
18
+ # Go to a test page
19
+ browser.go_to_url('https://example.com')
20
+
21
+ # Take full page screenshot
22
+ screenshot_b64 = browser.take_screenshot(full_page=True)
23
+
24
+ # Verify screenshot is not empty and is valid base64
25
+ assert screenshot_b64 is not None
26
+ assert isinstance(screenshot_b64, str)
27
+ assert len(screenshot_b64) > 0
28
+
29
+ # Test we can decode the base64 string
30
+ try:
31
+ base64.b64decode(screenshot_b64)
32
+ except Exception as e:
33
+ pytest.fail(f'Failed to decode base64 screenshot: {str(e)}')
34
+
35
+
36
+ if __name__ == '__main__':
37
+ test_take_full_page_screenshot(
38
+ Browser(config=BrowserConfig(headless=False)))
@@ -0,0 +1,77 @@
1
+ import asyncio
2
+ import json
3
+
4
+ import pytest
5
+
6
+ from lumivor.browser.browser import Browser, BrowserConfig
7
+ from lumivor.dom.views import ElementTreeSerializer
8
+ from lumivor.utils import time_execution_sync
9
+
10
+
11
+ # run with: pytest lumivor/browser/tests/test_clicks.py
12
+ @pytest.mark.asyncio
13
+ async def test_highlight_elements():
14
+ browser = Browser(config=BrowserConfig(
15
+ headless=False, disable_security=True))
16
+
17
+ async with await browser.new_context() as context:
18
+ page = await context.get_current_page()
19
+ # await page.goto('https://immobilienscout24.de')
20
+ # await page.goto('https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/service-plans')
21
+ # await page.goto('https://google.com/search?q=elon+musk')
22
+ # await page.goto('https://kayak.com')
23
+ # await page.goto('https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_iframe')
24
+ # await page.goto('https://dictionary.cambridge.org')
25
+ # await page.goto('https://github.com')
26
+ await page.goto('https://huggingface.co/')
27
+
28
+ await asyncio.sleep(1)
29
+
30
+ while True:
31
+ try:
32
+ # await asyncio.sleep(10)
33
+ state = await context.get_state()
34
+
35
+ with open('./tmp/page.json', 'w') as f:
36
+ json.dump(
37
+ ElementTreeSerializer.dom_element_node_to_json(
38
+ state.element_tree),
39
+ f,
40
+ indent=1,
41
+ )
42
+
43
+ # await time_execution_sync('highlight_selector_map_elements')(
44
+ # browser.highlight_selector_map_elements
45
+ # )(state.selector_map)
46
+
47
+ # Find and print duplicate XPaths
48
+ xpath_counts = {}
49
+ if not state.selector_map:
50
+ continue
51
+ for selector in state.selector_map.values():
52
+ xpath = selector.xpath
53
+ if xpath in xpath_counts:
54
+ xpath_counts[xpath] += 1
55
+ else:
56
+ xpath_counts[xpath] = 1
57
+
58
+ print('\nDuplicate XPaths found:')
59
+ for xpath, count in xpath_counts.items():
60
+ if count > 1:
61
+ print(f'XPath: {xpath}')
62
+ print(f'Count: {count}\n')
63
+
64
+ print(list(state.selector_map.keys()), 'Selector map keys')
65
+ print(state.element_tree.clickable_elements_to_string())
66
+ action = input('Select next action: ')
67
+
68
+ await time_execution_sync('remove_highlight_elements')(context.remove_highlights)()
69
+
70
+ node_element = state.selector_map[int(action)]
71
+
72
+ # check if index of selector map are the same as index of items in dom_items
73
+
74
+ await context._click_element_node(node_element)
75
+
76
+ except Exception as e:
77
+ print(e)
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from lumivor.dom.history_tree_processor.service import DOMHistoryElement
7
+ from lumivor.dom.views import DOMState
8
+
9
+
10
+ # Pydantic
11
+ class TabInfo(BaseModel):
12
+ """Represents information about a browser tab"""
13
+
14
+ page_id: int
15
+ url: str
16
+ title: str
17
+
18
+
19
+ @dataclass
20
+ class BrowserState(DOMState):
21
+ url: str
22
+ title: str
23
+ tabs: list[TabInfo]
24
+ screenshot: Optional[str] = None
25
+
26
+
27
+ @dataclass
28
+ class BrowserStateHistory:
29
+ url: str
30
+ title: str
31
+ tabs: list[TabInfo]
32
+ interacted_element: list[DOMHistoryElement | None] | list[None]
33
+ screenshot: Optional[str] = None
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ data = {}
37
+ data['tabs'] = [tab.model_dump() for tab in self.tabs]
38
+ data['screenshot'] = self.screenshot
39
+ data['interacted_element'] = [
40
+ el.to_dict() if el else None for el in self.interacted_element
41
+ ]
42
+ data['url'] = self.url
43
+ data['title'] = self.title
44
+ return data
45
+
46
+
47
+ class BrowserError(Exception):
48
+ """Base class for all browser errors"""
@@ -0,0 +1,140 @@
1
+ import asyncio
2
+ from inspect import iscoroutinefunction, signature
3
+ from typing import Any, Callable, Optional, Type
4
+
5
+ from pydantic import BaseModel, create_model
6
+
7
+ from lumivor.browser.context import BrowserContext
8
+ from lumivor.controller.registry.views import (
9
+ ActionModel,
10
+ ActionRegistry,
11
+ RegisteredAction,
12
+ )
13
+ from lumivor.telemetry.service import ProductTelemetry
14
+ from lumivor.telemetry.views import (
15
+ ControllerRegisteredFunctionsTelemetryEvent,
16
+ RegisteredFunction,
17
+ )
18
+
19
+
20
+ class Registry:
21
+ """Service for registering and managing actions"""
22
+
23
+ def __init__(self):
24
+ self.registry = ActionRegistry()
25
+ self.telemetry = ProductTelemetry()
26
+
27
+ def _create_param_model(self, function: Callable) -> Type[BaseModel]:
28
+ """Creates a Pydantic model from function signature"""
29
+ sig = signature(function)
30
+ params = {
31
+ name: (param.annotation, ... if param.default ==
32
+ param.empty else param.default)
33
+ for name, param in sig.parameters.items()
34
+ if name != 'browser'
35
+ }
36
+ # TODO: make the types here work
37
+ return create_model(
38
+ f'{function.__name__}Params',
39
+ __base__=ActionModel,
40
+ **params, # type: ignore
41
+ )
42
+
43
+ def action(
44
+ self,
45
+ description: str,
46
+ param_model: Optional[Type[BaseModel]] = None,
47
+ requires_browser: bool = False,
48
+ ):
49
+ """Decorator for registering actions"""
50
+
51
+ def decorator(func: Callable):
52
+ # Create param model from function if not provided
53
+ actual_param_model = param_model or self._create_param_model(func)
54
+
55
+ # Wrap sync functions to make them async
56
+ if not iscoroutinefunction(func):
57
+
58
+ async def async_wrapper(*args, **kwargs):
59
+ return await asyncio.to_thread(func, *args, **kwargs)
60
+
61
+ # Copy the signature and other metadata from the original function
62
+ async_wrapper.__signature__ = signature(func)
63
+ async_wrapper.__name__ = func.__name__
64
+ async_wrapper.__annotations__ = func.__annotations__
65
+ wrapped_func = async_wrapper
66
+ else:
67
+ wrapped_func = func
68
+
69
+ action = RegisteredAction(
70
+ name=func.__name__,
71
+ description=description,
72
+ function=wrapped_func,
73
+ param_model=actual_param_model,
74
+ requires_browser=requires_browser,
75
+ )
76
+ self.registry.actions[func.__name__] = action
77
+ return func
78
+
79
+ return decorator
80
+
81
+ async def execute_action(
82
+ self, action_name: str, params: dict, browser: Optional[BrowserContext] = None
83
+ ) -> Any:
84
+ """Execute a registered action"""
85
+ if action_name not in self.registry.actions:
86
+ raise ValueError(f'Action {action_name} not found')
87
+
88
+ action = self.registry.actions[action_name]
89
+ try:
90
+ # Create the validated Pydantic model
91
+ validated_params = action.param_model(**params)
92
+
93
+ # Check if the first parameter is a Pydantic model
94
+ sig = signature(action.function)
95
+ parameters = list(sig.parameters.values())
96
+ is_pydantic = parameters and issubclass(
97
+ parameters[0].annotation, BaseModel)
98
+
99
+ # Prepare arguments based on parameter type
100
+ if action.requires_browser:
101
+ if not browser:
102
+ raise ValueError(
103
+ f'Action {
104
+ action_name} requires browser but none provided. This has to be used in combination of `requires_browser=True` when registering the action.'
105
+ )
106
+ if is_pydantic:
107
+ return await action.function(validated_params, browser=browser)
108
+ return await action.function(**validated_params.model_dump(), browser=browser)
109
+
110
+ if is_pydantic:
111
+ return await action.function(validated_params)
112
+ return await action.function(**validated_params.model_dump())
113
+
114
+ except Exception as e:
115
+ raise RuntimeError(f'Error executing action {
116
+ action_name}: {str(e)}') from e
117
+
118
+ def create_action_model(self) -> Type[ActionModel]:
119
+ """Creates a Pydantic model from registered actions"""
120
+ fields = {
121
+ name: (Optional[action.param_model], None)
122
+ for name, action in self.registry.actions.items()
123
+ }
124
+
125
+ self.telemetry.capture(
126
+ ControllerRegisteredFunctionsTelemetryEvent(
127
+ registered_functions=[
128
+ RegisteredFunction(
129
+ name=name, params=action.param_model.model_json_schema())
130
+ for name, action in self.registry.actions.items()
131
+ ]
132
+ )
133
+ )
134
+
135
+ # type:ignore
136
+ return create_model('ActionModel', __base__=ActionModel, **fields)
137
+
138
+ def get_prompt_description(self) -> str:
139
+ """Get a description of all actions for the prompt"""
140
+ return self.registry.get_prompt_description()
@@ -0,0 +1,71 @@
1
+ from typing import Callable, Dict, Type
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class RegisteredAction(BaseModel):
7
+ """Model for a registered action"""
8
+
9
+ name: str
10
+ description: str
11
+ function: Callable
12
+ param_model: Type[BaseModel]
13
+ requires_browser: bool = False
14
+
15
+ model_config = ConfigDict(arbitrary_types_allowed=True)
16
+
17
+ def prompt_description(self) -> str:
18
+ """Get a description of the action for the prompt"""
19
+ skip_keys = ['title']
20
+ s = f'{self.description}: \n'
21
+ s += '{' + str(self.name) + ': '
22
+ s += str(
23
+ {
24
+ k: {sub_k: sub_v for sub_k, sub_v in v.items() if sub_k not in skip_keys}
25
+ for k, v in self.param_model.schema()['properties'].items()
26
+ }
27
+ )
28
+ s += '}'
29
+ return s
30
+
31
+
32
+ class ActionModel(BaseModel):
33
+ """Base model for dynamically created action models"""
34
+
35
+ # this will have all the registered actions, e.g.
36
+ # click_element = param_model = ClickElementParams
37
+ # done = param_model = None
38
+ #
39
+ model_config = ConfigDict(arbitrary_types_allowed=True)
40
+
41
+ def get_index(self) -> int | None:
42
+ """Get the index of the action"""
43
+ # {'clicked_element': {'index':5}}
44
+ params = self.model_dump(exclude_unset=True).values()
45
+ if not params:
46
+ return None
47
+ for param in params:
48
+ if param is not None and 'index' in param:
49
+ return param['index']
50
+ return None
51
+
52
+ def set_index(self, index: int):
53
+ """Overwrite the index of the action"""
54
+ # Get the action name and params
55
+ action_data = self.model_dump(exclude_unset=True)
56
+ action_name = next(iter(action_data.keys()))
57
+ action_params = getattr(self, action_name)
58
+
59
+ # Update the index directly on the model
60
+ if hasattr(action_params, 'index'):
61
+ action_params.index = index
62
+
63
+
64
+ class ActionRegistry(BaseModel):
65
+ """Model representing the action registry"""
66
+
67
+ actions: Dict[str, RegisteredAction] = {}
68
+
69
+ def get_prompt_description(self) -> str:
70
+ """Get a description of all actions for the prompt"""
71
+ return '\n'.join([action.prompt_description() for action in self.actions.values()])