Skip to content

Commit 0eea0a2

Browse files
committed
Improvements in Devices and AI Scriptless
1 parent cea06ef commit 0eea0a2

4 files changed

Lines changed: 78 additions & 30 deletions

File tree

formatters/device.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def format_real_device(devices: dict[str, Any], params: Optional[dict] = None) -
1717
platform_version=d.get("osVersion"),
1818
manufacturer=d.get("manufacturer"),
1919
model=d.get("model"),
20+
location=d.get("location", ""),
21+
description=d.get("description", ""),
2022
status=d.get("status"),
2123
in_use=d.get("inUse", "false"), # When device is on error inUse is None
2224
)

models/device.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class RealDevice(BaseModel):
88
platform_version: str = Field(description="The Platform Version (capability=platformVersion)")
99
manufacturer: str = Field(description="The Manufacturer (capability=manufacturer)")
1010
model: str = Field(description="The Model Name (capability=model)")
11+
location: str = Field(description="The Location Name (capability=location)")
12+
description: str = Field(description="The Device Description")
1113
status: str = Field(description="The Device Status")
1214
in_use: str = Field(description="Whether the device is in use")
1315

tools/ai_scriptless_manager.py

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,57 @@ async def list_filter_values(self, filter_names: list[str]) -> BaseResult:
6767
async def execute_test(self, test_id: str, device_type: str, device_under_test: dict[str, Any]) -> BaseResult:
6868
execute_url = perfecto.get_ai_scriptless_execution_api_url(self.token.cloud_name)
6969

70+
# This mapping allows us to detect when the AI gets confused and uses Perfecto-style capabilities.
71+
# It also allows for reverse mapping from internal to capabilities from Perfecto.
72+
att_map = {
73+
"real": {
74+
"device_id": "deviceId"
75+
},
76+
"virtual": {
77+
"platform_name": "platformName",
78+
"manufacturer": "manufacturer",
79+
"model": "model",
80+
"platform_version": "platformVersion"
81+
},
82+
"desktop": {
83+
"platform_name": "platformName",
84+
"platform_version": "platformVersion",
85+
"browser_name": "browserName",
86+
"browser_version": "browserVersion",
87+
"resolution": "resolution",
88+
"location": "location"
89+
}
90+
}
91+
7092
dut = None
93+
remapped_device_under_test = {}
94+
# Remap the attributes to Perfecto Capabilities format
95+
if device_type in att_map.keys():
96+
for key in att_map[device_type].keys():
97+
alt_key = att_map[device_type][key]
98+
remapped_device_under_test[alt_key] = device_under_test.get(key, device_under_test.get(alt_key, None))
99+
71100
if device_type == "real":
72-
dut = device_under_test.get("device_id", None)
101+
dut = remapped_device_under_test.get("deviceId", None)
102+
if dut is None:
103+
return BaseResult(
104+
error="Invalid value for device_under_test. The key device_id could not be found."
105+
)
73106
elif device_type in ["virtual", "desktop"]:
74-
dut = json.dumps(device_under_test, separators=(',', ':'))
75-
if dut is not None and len(dut) > 0 :
107+
# Verify if all the needed keys exist on the remapped version
108+
key_not_found = []
109+
for key in att_map[device_type].keys():
110+
alt_key = att_map[device_type][key]
111+
if alt_key not in remapped_device_under_test:
112+
key_not_found.append(key)
113+
if len(key_not_found) == 0:
114+
dut = json.dumps(remapped_device_under_test, separators=(',', ':'))
115+
else:
116+
keys_not_found_str = ",".join(key_not_found)
117+
return BaseResult(
118+
error=f"Invalid value for device_under_test. The keys [{keys_not_found_str}] could not be found."
119+
)
120+
if dut is not None and len(dut) > 0:
76121
body = {
77122
"params": {
78123
"DUT": dut
@@ -83,9 +128,10 @@ async def execute_test(self, test_id: str, device_type: str, device_under_test:
83128
return await api_request(self.token, "POST", endpoint=execute_url, json=body)
84129
else:
85130
return BaseResult(
86-
error=f"Invalid device_type or device_under_test value."
131+
error="Invalid device_type or device_under_test value."
87132
)
88133

134+
89135
def register(mcp, token: Optional[PerfectoToken]):
90136
@mcp.tool(
91137
name=f"{TOOLS_PREFIX}_ai_scriptless",
@@ -98,37 +144,33 @@ def register(mcp, token: Optional[PerfectoToken]):
98144
visibility (str, default='PRIVATE' values=['PUBLIC', 'PRIVATE']): The visibility, PUBLIC=All Public Tests, PRIVATE=My private tests.
99145
owner_list (list[str], values= use first list_filter_values tool with 'owner_list'): The list of users to filter tests (owners).
100146
page_index (int, default=1), The current page number. If the result mention has_next_page in true, asks the user if they want to see the next page.
101-
- list_filter_values: List the values needed for list_report_executions filters
147+
- list_filter_values: List the values needed for list_tests filters.
102148
args(dict): Dictionary with the following required filter parameters:
103149
filter_names (list[str], values=['test_name', 'owner_list']): The filter name list.
104-
- execute_test: Execute a preconfigured AI Scriptless Test, you need to know the test_id of a created and configured test and the real device_id available an not in use to run the test.
150+
- execute_test: Execute a preconfigured AI Scriptless Test.
105151
args(dict): Dictionary with the following required parameters:
106-
test_id (str): The test Id that should be started.
152+
test_id (str): Test ID from list_tests()
107153
device_type (str, default='real', values=['real', 'virtual', 'desktop']: The device type.
108-
device_under_test (dict): The Device Under Test (DUT).
109-
If device_type it's:
110-
- 'real': required device_under_test attributes = [
111-
'device_id':'the real device_id value'
112-
]
113-
- 'virtual': required device_under_test attributes = [
114-
'platformName': 'the virtual device platform name value',
115-
'manufacturer': 'the manufacturer name value',
116-
'model': 'the model name value',
117-
'platformVersion': 'the platform version value'
118-
]
119-
- 'desktop': required device_under_test attributes = [
120-
'platformName': 'the desktop platform name value',
121-
'platformVersion': 'the platform version value',
122-
'browserName': 'the browser name value',
123-
'browserVersion': 'the browser version value',
124-
'resolution': 'the resolution value',
125-
'location': 'the location value'
126-
]
154+
device_under_test (dict, required): Device configuration object.
155+
When device_type='real': {device_id: str} (Get from list_real_devices()).
156+
When device_type='virtual': {platform_name: str, manufacturer: str, model: str, platform_version: str} (Get from list_virtual_devices()).
157+
When device_type='desktop': {platform_name: str, platform_version: str, browser_name: str,
158+
browser_version: str, resolution: str, location: str} (Get from list_desktop_devices()).
127159
Hints:
128160
- IMPORTANT: Always call list_filter_values first to get valid filter values before using any filters in list_tests.
129161
This ensures you're using the correct test name, list of owners users or other filter values that actually exist in the system.
130-
- Always check before running a test_id if the device_type and device_under_test exist and is available, not use device in use or malfunctioning.
131-
- Always monitor a device's operation while it's in use by checking the live executions.
162+
- If in any result has_next_page is true, ask the user if they want to see the next page or access all pages before making a subsequent call.
163+
- Before executing a test, follow this validation workflow:
164+
1. list_tests() (get and validate test_id).
165+
2. Get device configuration based on device_type:
166+
- 'real': list_real_devices() (get device_id).
167+
- 'virtual': list_virtual_devices() (get platform_name, manufacturer, model, platform_version).
168+
- 'desktop': list_desktop_devices() (get platform_name, platform_version, browser_name, browser_version, resolution, location).
169+
3. On real device use read_real_device_info() (verify device is available and not in use).
170+
4. execute_test() (execute the test).
171+
5. list_report_executions() with report name equal to test name and list_live_executions() when the device it's in use (monitor execution progress).
172+
- Always check before running a test_id if the device_type and device_under_test exist and is available (when it's a real device), not use device in use or malfunctioning.
173+
- Always monitor a real device's operation while it's in use by checking the information with read_real_device_info().
132174
- Always stop the execution by stopping the live execution (make sure it's the correct execution, such as the execution name or user ID).
133175
"""
134176
)
@@ -148,8 +190,8 @@ async def ai_scriptless(
148190
return await ai_scriptless_manager.list_filter_values(args.get("filter_names", []))
149191
case "execute_test":
150192
return await ai_scriptless_manager.execute_test(args.get("test_id", ""),
151-
args.get("device_type", ""),
152-
args.get("device_under_test", {}))
193+
args.get("device_type", ""),
194+
args.get("device_under_test", {}))
153195
case _:
154196
return BaseResult(
155197
error=f"Action {action} not found in AI Scriptless manager tool"

tools/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def get_date_time_iso(timestamp: int) -> Optional[str]:
107107
else:
108108
return datetime.fromtimestamp(timestamp).isoformat()
109109

110+
110111
def get_resources_path():
111112
try:
112113
resources_path = resources.files("resources")
@@ -119,6 +120,7 @@ def get_resources_path():
119120
resources_path = Path(base_path) / 'resources'
120121
return resources_path
121122

123+
122124
def get_mcp_icon_uri():
123125
name = "app.png"
124126
icon_path = get_resources_path().joinpath(name)

0 commit comments

Comments
 (0)