Skip to content

Commit 90096bc

Browse files
authored
script gen - support skyvern.loop & cleaner interfaces for generated code (no need to pass context.parameters, implicit template rendering) (#3542)
1 parent 8c54475 commit 90096bc

File tree

7 files changed

+336
-161
lines changed

7 files changed

+336
-161
lines changed

skyvern/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ def process_trace(self, trace: list[Span]) -> list[Span] | None:
3434
download, # noqa: E402
3535
extract, # noqa: E402
3636
http_request, # noqa: E402
37-
generate_text, # noqa: E402
3837
goto, # noqa: E402
3938
login, # noqa: E402
39+
loop, # noqa: E402
4040
parse_file, # noqa: E402
4141
prompt, # noqa: E402
4242
render_list, # noqa: E402
@@ -59,9 +59,9 @@ def process_trace(self, trace: list[Span]) -> list[Span] | None:
5959
"download",
6060
"extract",
6161
"http_request",
62-
"generate_text",
6362
"goto",
6463
"login",
64+
"loop",
6565
"parse_file",
6666
"prompt",
6767
"render_list",

skyvern/core/script_generations/generate_script.py

Lines changed: 37 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -168,57 +168,6 @@ def _render_value(
168168
return _value(prompt_text)
169169

170170

171-
def _generate_text_call(text_value: str, intention: str, parameter_key: str) -> cst.BaseExpression:
172-
"""Create a generate_text function call CST expression."""
173-
return cst.Await(
174-
expression=cst.Call(
175-
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("generate_text")),
176-
whitespace_before_args=cst.ParenthesizedWhitespace(
177-
indent=True,
178-
last_line=cst.SimpleWhitespace(DOUBLE_INDENT),
179-
),
180-
args=[
181-
# First positional argument: context.parameters['parameter_key']
182-
cst.Arg(
183-
value=cst.Subscript(
184-
value=cst.Attribute(
185-
value=cst.Name("context"),
186-
attr=cst.Name("parameters"),
187-
),
188-
slice=[cst.SubscriptElement(slice=cst.Index(value=_value(parameter_key)))],
189-
),
190-
whitespace_after_arg=cst.ParenthesizedWhitespace(
191-
indent=True,
192-
last_line=cst.SimpleWhitespace(DOUBLE_INDENT),
193-
),
194-
),
195-
# intention keyword argument
196-
cst.Arg(
197-
keyword=cst.Name("intention"),
198-
value=_value(intention),
199-
whitespace_after_arg=cst.ParenthesizedWhitespace(
200-
indent=True,
201-
last_line=cst.SimpleWhitespace(DOUBLE_INDENT),
202-
),
203-
),
204-
# data keyword argument
205-
cst.Arg(
206-
keyword=cst.Name("data"),
207-
value=cst.Attribute(
208-
value=cst.Name("context"),
209-
attr=cst.Name("parameters"),
210-
),
211-
whitespace_after_arg=cst.ParenthesizedWhitespace(
212-
indent=True,
213-
last_line=cst.SimpleWhitespace(INDENT),
214-
),
215-
comma=cst.Comma(),
216-
),
217-
],
218-
)
219-
)
220-
221-
222171
# --------------------------------------------------------------------- #
223172
# 2. utility builders #
224173
# --------------------------------------------------------------------- #
@@ -434,7 +383,7 @@ def _action_to_stmt(act: dict[str, Any], task: dict[str, Any], assign_to_output:
434383
args.append(
435384
cst.Arg(
436385
keyword=cst.Name("prompt"),
437-
value=_render_value(act["data_extraction_goal"]),
386+
value=_value(act["data_extraction_goal"]),
438387
whitespace_after_arg=cst.ParenthesizedWhitespace(
439388
indent=True,
440389
last_line=cst.SimpleWhitespace(INDENT),
@@ -459,14 +408,6 @@ def _action_to_stmt(act: dict[str, Any], task: dict[str, Any], assign_to_output:
459408
cst.Arg(
460409
keyword=cst.Name("intention"),
461410
value=_value(act.get("intention") or act.get("reasoning") or ""),
462-
whitespace_after_arg=cst.ParenthesizedWhitespace(
463-
indent=True,
464-
last_line=cst.SimpleWhitespace(INDENT),
465-
),
466-
),
467-
cst.Arg(
468-
keyword=cst.Name("data"),
469-
value=cst.Attribute(value=cst.Name("context"), attr=cst.Name("parameters")),
470411
whitespace_after_arg=cst.ParenthesizedWhitespace(indent=True),
471412
comma=cst.Comma(),
472413
),
@@ -646,7 +587,7 @@ def _build_download_statement(
646587
args = [
647588
cst.Arg(
648589
keyword=cst.Name("prompt"),
649-
value=_render_value(block.get("navigation_goal") or "", data_variable_name=data_variable_name),
590+
value=_value(block.get("navigation_goal") or ""),
650591
whitespace_after_arg=cst.ParenthesizedWhitespace(
651592
indent=True,
652593
last_line=cst.SimpleWhitespace(INDENT),
@@ -657,7 +598,7 @@ def _build_download_statement(
657598
args.append(
658599
cst.Arg(
659600
keyword=cst.Name("download_suffix"),
660-
value=_render_value(block.get("download_suffix"), data_variable_name=data_variable_name),
601+
value=_value(block.get("download_suffix")),
661602
whitespace_after_arg=cst.ParenthesizedWhitespace(
662603
indent=True,
663604
last_line=cst.SimpleWhitespace(INDENT),
@@ -694,7 +635,7 @@ def _build_action_statement(
694635
args = [
695636
cst.Arg(
696637
keyword=cst.Name("prompt"),
697-
value=_render_value(block.get("navigation_goal", ""), data_variable_name=data_variable_name),
638+
value=_value(block.get("navigation_goal", "")),
698639
whitespace_after_arg=cst.ParenthesizedWhitespace(
699640
indent=True,
700641
last_line=cst.SimpleWhitespace(INDENT),
@@ -746,7 +687,7 @@ def _build_extract_statement(
746687
args = [
747688
cst.Arg(
748689
keyword=cst.Name("prompt"),
749-
value=_render_value(block.get("data_extraction_goal", ""), data_variable_name=data_variable_name),
690+
value=_value(block.get("data_extraction_goal", "")),
750691
whitespace_after_arg=cst.ParenthesizedWhitespace(
751692
indent=True,
752693
last_line=cst.SimpleWhitespace(INDENT),
@@ -870,7 +811,7 @@ def _build_validate_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
870811
args = [
871812
cst.Arg(
872813
keyword=cst.Name("prompt"),
873-
value=_render_value(block.get("navigation_goal", "")),
814+
value=_value(block.get("navigation_goal", "")),
874815
whitespace_after_arg=cst.ParenthesizedWhitespace(
875816
indent=True,
876817
),
@@ -896,6 +837,14 @@ def _build_wait_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
896837
cst.Arg(
897838
keyword=cst.Name("seconds"),
898839
value=_value(block.get("wait_sec", 1)),
840+
whitespace_after_arg=cst.ParenthesizedWhitespace(
841+
indent=True,
842+
last_line=cst.SimpleWhitespace(INDENT),
843+
),
844+
),
845+
cst.Arg(
846+
keyword=cst.Name("label"),
847+
value=_value(block.get("label")),
899848
whitespace_after_arg=cst.ParenthesizedWhitespace(
900849
indent=True,
901850
),
@@ -920,7 +869,7 @@ def _build_goto_statement(block: dict[str, Any], data_variable_name: str | None
920869
args = [
921870
cst.Arg(
922871
keyword=cst.Name("url"),
923-
value=_render_value(block.get("url", ""), data_variable_name=data_variable_name),
872+
value=_value(block.get("url", "")),
924873
whitespace_after_arg=cst.ParenthesizedWhitespace(
925874
indent=True,
926875
last_line=cst.SimpleWhitespace(INDENT),
@@ -1212,7 +1161,7 @@ def _build_prompt_statement(block: dict[str, Any]) -> cst.SimpleStatementLine:
12121161
args = [
12131162
cst.Arg(
12141163
keyword=cst.Name("prompt"),
1215-
value=_render_value(block.get("prompt", "")),
1164+
value=_value(block.get("prompt", "")),
12161165
whitespace_after_arg=cst.ParenthesizedWhitespace(
12171166
indent=True,
12181167
last_line=cst.SimpleWhitespace(INDENT),
@@ -1275,7 +1224,7 @@ def _build_for_loop_statement(block_title: str, block: dict[str, Any]) -> cst.Fo
12751224
12761225
An example of a for loop statement:
12771226
```
1278-
for current_value in context.parameters["urls"]:
1227+
async for current_value in skyvern.loop(context.parameters["urls"]):
12791228
await skyvern.goto(
12801229
url=current_value,
12811230
label="block_4",
@@ -1309,28 +1258,28 @@ def _build_for_loop_statement(block_title: str, block: dict[str, Any]) -> cst.Fo
13091258
body_statements = []
13101259

13111260
# Add loop_data assignment as the first statement
1312-
loop_data_variable_name = "loop_data"
1313-
loop_data_assignment = cst.SimpleStatementLine(
1314-
[
1315-
cst.Assign(
1316-
targets=[cst.AssignTarget(target=cst.Name(loop_data_variable_name))],
1317-
value=cst.Dict(
1318-
[cst.DictElement(key=cst.SimpleString('"current_value"'), value=cst.Name("current_value"))]
1319-
),
1320-
)
1321-
]
1322-
)
1323-
body_statements.append(loop_data_assignment)
1324-
13251261
for loop_block in loop_blocks:
1326-
stmt = _build_block_statement(loop_block, data_variable_name=loop_data_variable_name)
1262+
stmt = _build_block_statement(loop_block)
13271263
body_statements.append(stmt)
13281264

1329-
# Create the for loop
1265+
# create skyvern.loop(loop_over_parameter_key, label=block_title)
1266+
loop_call_args = [cst.Arg(keyword=cst.Name("values"), value=_value(loop_over_parameter_key))]
1267+
if block.get("complete_if_empty"):
1268+
loop_call_args.append(
1269+
cst.Arg(keyword=cst.Name("complete_if_empty"), value=_value(block.get("complete_if_empty")))
1270+
)
1271+
loop_call_args.append(cst.Arg(keyword=cst.Name("label"), value=_value(block_title)))
1272+
loop_call = cst.Call(
1273+
func=cst.Attribute(value=cst.Name("skyvern"), attr=cst.Name("loop")),
1274+
args=loop_call_args,
1275+
)
1276+
1277+
# Create the async for loop
13301278
for_loop = cst.For(
13311279
target=target,
1332-
iter=_render_value(loop_over_parameter_key, render_func_name="render_list"),
1280+
iter=loop_call,
13331281
body=cst.IndentedBlock(body=body_statements),
1282+
asynchronous=cst.Asynchronous(),
13341283
whitespace_after_for=cst.SimpleWhitespace(" "),
13351284
whitespace_before_in=cst.SimpleWhitespace(" "),
13361285
whitespace_after_in=cst.SimpleWhitespace(" "),
@@ -1405,7 +1354,7 @@ def __build_base_task_statement(
14051354
args = [
14061355
cst.Arg(
14071356
keyword=cst.Name("prompt"),
1408-
value=_render_value(prompt, data_variable_name=data_variable_name),
1357+
value=_value(prompt),
14091358
whitespace_after_arg=cst.ParenthesizedWhitespace(
14101359
indent=True,
14111360
last_line=cst.SimpleWhitespace(INDENT),
@@ -1416,7 +1365,7 @@ def __build_base_task_statement(
14161365
args.append(
14171366
cst.Arg(
14181367
keyword=cst.Name("url"),
1419-
value=_render_value(block.get("url", "")),
1368+
value=_value(block.get("url", "")),
14201369
whitespace_after_arg=cst.ParenthesizedWhitespace(
14211370
indent=True,
14221371
last_line=cst.SimpleWhitespace(INDENT),
@@ -1439,7 +1388,7 @@ def __build_base_task_statement(
14391388
args.append(
14401389
cst.Arg(
14411390
keyword=cst.Name("totp_identifier"),
1442-
value=_render_value(block.get("totp_identifier", "")),
1391+
value=_value(block.get("totp_identifier", "")),
14431392
whitespace_after_arg=cst.ParenthesizedWhitespace(
14441393
indent=True,
14451394
last_line=cst.SimpleWhitespace(INDENT),
@@ -1450,7 +1399,7 @@ def __build_base_task_statement(
14501399
args.append(
14511400
cst.Arg(
14521401
keyword=cst.Name("totp_url"),
1453-
value=_render_value(block.get("totp_verification_url", "")),
1402+
value=_value(block.get("totp_verification_url", "")),
14541403
whitespace_after_arg=cst.ParenthesizedWhitespace(
14551404
indent=True,
14561405
last_line=cst.SimpleWhitespace(INDENT),

skyvern/core/script_generations/run_initializer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ async def setup(
2626
parameter = parameters_in_workflow_context[key]
2727
if parameter.workflow_parameter_type == WorkflowParameterType.CREDENTIAL_ID:
2828
parameters[key] = workflow_run_context.values[key]
29+
context.script_run_parameters.update(parameters)
2930
skyvern_page = await SkyvernPage.create(browser_session_id=browser_session_id)
3031
run_context = RunContext(
3132
parameters=parameters,

skyvern/core/script_generations/skyvern_page.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,29 @@ async def _get_element_id_by_xpath(xpath: str, page: Page) -> str | None:
6464
return element_id
6565

6666

67+
def _get_context_data(data: str | dict[str, Any] | None = None) -> dict[str, Any] | str | None:
68+
context = skyvern_context.current()
69+
global_context_data = context.script_run_parameters if context else None
70+
if not data:
71+
return global_context_data
72+
result: dict[str, Any] | str | None
73+
if isinstance(data, dict):
74+
result = {k: v for k, v in data.items() if v}
75+
if global_context_data:
76+
result.update(global_context_data)
77+
else:
78+
global_context_data_str = json.dumps(global_context_data) if global_context_data else ""
79+
result = f"{data}\n{global_context_data_str}"
80+
return result
81+
82+
6783
def render_template(template: str, data: dict[str, Any] | None = None) -> str:
6884
"""
6985
Refer to Block.format_block_parameter_template_from_workflow_run_context
7086
7187
TODO: complete this function so that block code shares the same template rendering logic
7288
"""
73-
template_data = data or {}
89+
template_data = data.copy() if data else {}
7490
jinja_template = jinja_sandbox_env.from_string(template)
7591
context = skyvern_context.current()
7692
if context and context.workflow_run_id:
@@ -355,7 +371,7 @@ async def click(self, xpath: str, intention: str | None = None, data: str | dict
355371
try:
356372
# Build the element tree of the current page for the prompt
357373
context = skyvern_context.ensure_context()
358-
payload_str = json.dumps(data) if isinstance(data, (dict, list)) else (data or "")
374+
payload_str = _get_context_data(data)
359375
refreshed_page = await self.scraped_page.generate_scraped_page_without_screenshots()
360376
element_tree = refreshed_page.build_element_tree()
361377
single_click_prompt = prompt_engine.load_prompt(
@@ -463,9 +479,7 @@ async def _input_text(
463479
if ai_infer and intention:
464480
try:
465481
prompt = context.prompt if context else None
466-
# Build the element tree of the current page for the prompt
467-
# clean up empty data values
468-
data = {k: v for k, v in data.items() if v} if isinstance(data, dict) else (data or "")
482+
data = _get_context_data(data)
469483
if (totp_identifier or totp_url) and context and organization_id and task_id:
470484
verification_code = await poll_verification_code(
471485
organization_id=organization_id,
@@ -488,11 +502,10 @@ async def _input_text(
488502
self.scraped_page = refreshed_page
489503
# get the element_id by the xpath
490504
element_id = await _get_element_id_by_xpath(xpath, self.page)
491-
payload_str = json.dumps(data) if isinstance(data, (dict, list)) else (data or "")
492505
script_generation_input_text_prompt = prompt_engine.load_prompt(
493506
template="script-generation-input-text-generatiion",
494507
intention=intention,
495-
data=payload_str,
508+
data=data,
496509
goal=prompt,
497510
)
498511
json_response = await app.SINGLE_INPUT_AGENT_LLM_API_HANDLER(
@@ -539,12 +552,11 @@ async def upload_file(
539552
try:
540553
context = skyvern_context.current()
541554
prompt = context.prompt if context else None
542-
data = {k: v for k, v in data.items() if v} if isinstance(data, dict) else (data or "")
543-
payload_str = json.dumps(data) if isinstance(data, (dict, list)) else (data or "")
555+
data = _get_context_data(data)
544556
script_generation_file_url_prompt = prompt_engine.load_prompt(
545557
template="script-generation-file-url-generation",
546558
intention=intention,
547-
data=payload_str,
559+
data=data,
548560
goal=prompt,
549561
)
550562
json_response = await app.SINGLE_INPUT_AGENT_LLM_API_HANDLER(
@@ -578,15 +590,14 @@ async def select_option(
578590
if ai_infer and intention and task and step:
579591
try:
580592
prompt = context.prompt if context else None
581-
data = {k: v for k, v in data.items() if v} if isinstance(data, dict) else (data or "")
582-
payload_str = json.dumps(data) if isinstance(data, (dict, list)) else (data or "")
593+
data = _get_context_data(data)
583594
refreshed_page = await self.scraped_page.generate_scraped_page_without_screenshots()
584595
self.scraped_page = refreshed_page
585596
element_tree = refreshed_page.build_element_tree()
586597
merged_goal = SELECT_OPTION_GOAL.format(intention=intention, prompt=prompt)
587598
single_select_prompt = prompt_engine.load_prompt(
588599
template="single-select-action",
589-
navigation_payload_str=payload_str,
600+
navigation_payload_str=data,
590601
navigation_goal=merged_goal,
591602
current_url=self.page.url,
592603
elements=element_tree,

0 commit comments

Comments
 (0)