multi-selection history (#813)
This commit is contained in:
@@ -5,7 +5,8 @@ You can identify the matching element based on the following guidelines:
|
|||||||
2. If no option is a perfect match, and there is a fallback option such as "Others" or "None of the above" in the DOM elements, you can consider it a match.
|
2. If no option is a perfect match, and there is a fallback option such as "Others" or "None of the above" in the DOM elements, you can consider it a match.
|
||||||
3. If a field is required, do not leave it blank.
|
3. If a field is required, do not leave it blank.
|
||||||
4. If a field is required, do not select a placeholder value, such as "Please select", "-", or "Select...".
|
4. If a field is required, do not select a placeholder value, such as "Please select", "-", or "Select...".
|
||||||
5. Exclude loading indicators like "loading more results" as valid options.
|
5. Exclude loading indicators like "loading more results" as valid options.{% if select_history %}
|
||||||
|
6. The selection history displays the previously selected values for the multi-level selection. Continue to complete the entire selection process.{% endif %}
|
||||||
|
|
||||||
MAKE SURE YOU OUTPUT VALID JSON. No text before or after JSON, no trailing commas, no comments (//), no unnecessary quotes, etc.
|
MAKE SURE YOU OUTPUT VALID JSON. No text before or after JSON, no trailing commas, no comments (//), no unnecessary quotes, etc.
|
||||||
Each interactable element is tagged with an ID.
|
Each interactable element is tagged with an ID.
|
||||||
@@ -42,4 +43,10 @@ User details:
|
|||||||
HTML elements:
|
HTML elements:
|
||||||
```
|
```
|
||||||
{{ elements }}
|
{{ elements }}
|
||||||
```
|
```
|
||||||
|
{% if select_history %}
|
||||||
|
Select History:
|
||||||
|
```
|
||||||
|
{{ select_history }}
|
||||||
|
```
|
||||||
|
{% endif %}
|
||||||
@@ -83,6 +83,7 @@ COMMON_INPUT_TAGS = {"input", "textarea", "select"}
|
|||||||
|
|
||||||
class CustomSingleSelectResult:
|
class CustomSingleSelectResult:
|
||||||
def __init__(self, skyvern_frame: SkyvernFrame) -> None:
|
def __init__(self, skyvern_frame: SkyvernFrame) -> None:
|
||||||
|
self.reasoning: str | None = None
|
||||||
self.action_result: ActionResult | None = None
|
self.action_result: ActionResult | None = None
|
||||||
self.value: str | None = None
|
self.value: str | None = None
|
||||||
self.dropdown_menu: SkyvernElement | None = None
|
self.dropdown_menu: SkyvernElement | None = None
|
||||||
@@ -1339,7 +1340,7 @@ async def sequentially_select_from_dropdown(
|
|||||||
# TODO: only suport the third-level dropdown selection now
|
# TODO: only suport the third-level dropdown selection now
|
||||||
MAX_SELECT_DEPTH = 3
|
MAX_SELECT_DEPTH = 3
|
||||||
values: list[str | None] = []
|
values: list[str | None] = []
|
||||||
single_select_result = CustomSingleSelectResult(skyvern_frame=skyvern_frame)
|
select_history: list[CustomSingleSelectResult] = []
|
||||||
|
|
||||||
check_exist_funcs: list[CheckExistIDFunc] = [dom.check_id_in_dom]
|
check_exist_funcs: list[CheckExistIDFunc] = [dom.check_id_in_dom]
|
||||||
for i in range(MAX_SELECT_DEPTH):
|
for i in range(MAX_SELECT_DEPTH):
|
||||||
@@ -1352,9 +1353,11 @@ async def sequentially_select_from_dropdown(
|
|||||||
check_exist_funcs=check_exist_funcs,
|
check_exist_funcs=check_exist_funcs,
|
||||||
step=step,
|
step=step,
|
||||||
task=task,
|
task=task,
|
||||||
|
select_history=select_history,
|
||||||
force_select=force_select,
|
force_select=force_select,
|
||||||
should_relevant=should_relevant,
|
should_relevant=should_relevant,
|
||||||
)
|
)
|
||||||
|
select_history.append(single_select_result)
|
||||||
values.append(single_select_result.value)
|
values.append(single_select_result.value)
|
||||||
# wait 1s until DOM finished updating
|
# wait 1s until DOM finished updating
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
@@ -1399,7 +1402,21 @@ async def sequentially_select_from_dropdown(
|
|||||||
)
|
)
|
||||||
return single_select_result.action_result, values[-1] if len(values) > 0 else None
|
return single_select_result.action_result, values[-1] if len(values) > 0 else None
|
||||||
|
|
||||||
return single_select_result.action_result, values[-1] if len(values) > 0 else None
|
return select_history[-1].action_result if len(select_history) > 0 else None, values[-1] if len(
|
||||||
|
values
|
||||||
|
) > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def build_sequential_select_history(history_list: list[CustomSingleSelectResult]) -> list[dict[str, Any]]:
|
||||||
|
result = [
|
||||||
|
{
|
||||||
|
"reasoning": select_result.reasoning,
|
||||||
|
"value": select_result.value,
|
||||||
|
"result": "success" if isinstance(select_result.action_result, ActionSuccess) else "failed",
|
||||||
|
}
|
||||||
|
for select_result in history_list
|
||||||
|
]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def select_from_dropdown(
|
async def select_from_dropdown(
|
||||||
@@ -1411,6 +1428,7 @@ async def select_from_dropdown(
|
|||||||
check_exist_funcs: list[CheckExistIDFunc],
|
check_exist_funcs: list[CheckExistIDFunc],
|
||||||
step: Step,
|
step: Step,
|
||||||
task: Task,
|
task: Task,
|
||||||
|
select_history: list[CustomSingleSelectResult] | None = None,
|
||||||
force_select: bool = False,
|
force_select: bool = False,
|
||||||
should_relevant: bool = True,
|
should_relevant: bool = True,
|
||||||
) -> CustomSingleSelectResult:
|
) -> CustomSingleSelectResult:
|
||||||
@@ -1421,6 +1439,7 @@ async def select_from_dropdown(
|
|||||||
1. force_select is false and no dropdown menu popped
|
1. force_select is false and no dropdown menu popped
|
||||||
2. force_select is false and match value is not relevant to the target value
|
2. force_select is false and match value is not relevant to the target value
|
||||||
"""
|
"""
|
||||||
|
select_history = [] if select_history is None else select_history
|
||||||
single_select_result = CustomSingleSelectResult(skyvern_frame=skyvern_frame)
|
single_select_result = CustomSingleSelectResult(skyvern_frame=skyvern_frame)
|
||||||
|
|
||||||
timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
|
timeout = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
|
||||||
@@ -1471,6 +1490,7 @@ async def select_from_dropdown(
|
|||||||
navigation_goal=task.navigation_goal,
|
navigation_goal=task.navigation_goal,
|
||||||
navigation_payload_str=json.dumps(task.navigation_payload),
|
navigation_payload_str=json.dumps(task.navigation_payload),
|
||||||
elements=html,
|
elements=html,
|
||||||
|
select_history=json.dumps(build_sequential_select_history(select_history)) if select_history else "",
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
@@ -1481,6 +1501,8 @@ async def select_from_dropdown(
|
|||||||
json_response = await llm_handler(prompt=prompt, step=step)
|
json_response = await llm_handler(prompt=prompt, step=step)
|
||||||
value: str | None = json_response.get("value", None)
|
value: str | None = json_response.get("value", None)
|
||||||
single_select_result.value = value
|
single_select_result.value = value
|
||||||
|
select_reason: str | None = json_response.get("reasoning", None)
|
||||||
|
single_select_result.reasoning = select_reason
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"LLM response for the matched element",
|
"LLM response for the matched element",
|
||||||
@@ -1657,6 +1679,31 @@ async def locate_dropdown_menu(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
ul_or_listbox_element_id = await head_element.find_children_element_id_by_callback(
|
||||||
|
cb=is_ul_or_listbox_element_factory(incremental_scraped=incremental_scraped, task=task, step=step),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ul_or_listbox_element_id:
|
||||||
|
try:
|
||||||
|
await SkyvernElement.create_from_incremental(incremental_scraped, ul_or_listbox_element_id)
|
||||||
|
LOG.info(
|
||||||
|
"Confirm it's an opened dropdown menu since it includes <ul> or <role='listbox'>",
|
||||||
|
step_id=step.step_id,
|
||||||
|
task_id=task.task_id,
|
||||||
|
element_id=element_id,
|
||||||
|
)
|
||||||
|
return await SkyvernElement.create_from_incremental(
|
||||||
|
incre_page=incremental_scraped, element_id=element_id
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOG.debug(
|
||||||
|
"Failed to get <ul> or <role='listbox'> element in the incremental page",
|
||||||
|
element_id=element_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
task_id=task.task_id,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
screenshot = await head_element.get_locator().screenshot(
|
screenshot = await head_element.get_locator().screenshot(
|
||||||
timeout=SettingsManager.get_settings().BROWSER_SCREENSHOT_TIMEOUT_MS
|
timeout=SettingsManager.get_settings().BROWSER_SCREENSHOT_TIMEOUT_MS
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user