mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-01 18:20:06 +00:00
adopt ruff as the replacement for python black (#332)
This commit is contained in:
parent
7a2be7e355
commit
2466897158
44 changed files with 1081 additions and 321 deletions
|
@ -13,19 +13,21 @@ repos:
|
|||
- id: check-symlinks
|
||||
- id: debug-statements
|
||||
- id: detect-private-key
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
language_version: python3.11
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.11
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Create tables
|
||||
|
||||
Revision ID: 99423c1dec60
|
||||
Revises:
|
||||
Revises:
|
||||
Create Date: 2024-03-01 05:37:31.862957+00:00
|
||||
|
||||
"""
|
||||
|
@ -31,12 +31,21 @@ def upgrade() -> None:
|
|||
sa.Column("modified_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("organization_id"),
|
||||
)
|
||||
op.create_index(op.f("ix_organizations_organization_id"), "organizations", ["organization_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_organizations_organization_id"),
|
||||
"organizations",
|
||||
["organization_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"organization_auth_tokens",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("organization_id", sa.String(), nullable=False),
|
||||
sa.Column("token_type", sa.Enum("api", name="organizationauthtokentype"), nullable=False),
|
||||
sa.Column(
|
||||
"token_type",
|
||||
sa.Enum("api", name="organizationauthtokentype"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("token", sa.String(), nullable=False),
|
||||
sa.Column("valid", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
|
@ -48,14 +57,24 @@ def upgrade() -> None:
|
|||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_organization_auth_tokens_id"), "organization_auth_tokens", ["id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_organization_auth_tokens_id"),
|
||||
"organization_auth_tokens",
|
||||
["id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_auth_tokens_organization_id"),
|
||||
"organization_auth_tokens",
|
||||
["organization_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_organization_auth_tokens_token"), "organization_auth_tokens", ["token"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_organization_auth_tokens_token"),
|
||||
"organization_auth_tokens",
|
||||
["token"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"workflows",
|
||||
sa.Column("workflow_id", sa.String(), nullable=False),
|
||||
|
@ -96,7 +115,10 @@ def upgrade() -> None:
|
|||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_aws_secret_parameters_workflow_id"), "aws_secret_parameters", ["workflow_id"], unique=False
|
||||
op.f("ix_aws_secret_parameters_workflow_id"),
|
||||
"aws_secret_parameters",
|
||||
["workflow_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"workflow_parameters",
|
||||
|
@ -115,7 +137,12 @@ def upgrade() -> None:
|
|||
),
|
||||
sa.PrimaryKeyConstraint("workflow_parameter_id"),
|
||||
)
|
||||
op.create_index(op.f("ix_workflow_parameters_workflow_id"), "workflow_parameters", ["workflow_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_workflow_parameters_workflow_id"),
|
||||
"workflow_parameters",
|
||||
["workflow_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_workflow_parameters_workflow_parameter_id"),
|
||||
"workflow_parameters",
|
||||
|
@ -129,7 +156,16 @@ def upgrade() -> None:
|
|||
sa.Column("status", sa.String(), nullable=False),
|
||||
sa.Column(
|
||||
"proxy_location",
|
||||
sa.Enum("US_CA", "US_NY", "US_TX", "US_FL", "US_WA", "RESIDENTIAL", "NONE", name="proxylocation"),
|
||||
sa.Enum(
|
||||
"US_CA",
|
||||
"US_NY",
|
||||
"US_TX",
|
||||
"US_FL",
|
||||
"US_WA",
|
||||
"RESIDENTIAL",
|
||||
"NONE",
|
||||
name="proxylocation",
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("webhook_callback_url", sa.String(), nullable=True),
|
||||
|
@ -141,7 +177,12 @@ def upgrade() -> None:
|
|||
),
|
||||
sa.PrimaryKeyConstraint("workflow_run_id"),
|
||||
)
|
||||
op.create_index(op.f("ix_workflow_runs_workflow_run_id"), "workflow_runs", ["workflow_run_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_workflow_runs_workflow_run_id"),
|
||||
"workflow_runs",
|
||||
["workflow_run_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"tasks",
|
||||
sa.Column("task_id", sa.String(), nullable=False),
|
||||
|
@ -156,7 +197,16 @@ def upgrade() -> None:
|
|||
sa.Column("failure_reason", sa.String(), nullable=True),
|
||||
sa.Column(
|
||||
"proxy_location",
|
||||
sa.Enum("US_CA", "US_NY", "US_TX", "US_FL", "US_WA", "RESIDENTIAL", "NONE", name="proxylocation"),
|
||||
sa.Enum(
|
||||
"US_CA",
|
||||
"US_NY",
|
||||
"US_TX",
|
||||
"US_FL",
|
||||
"US_WA",
|
||||
"RESIDENTIAL",
|
||||
"NONE",
|
||||
name="proxylocation",
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("extracted_information_schema", sa.JSON(), nullable=True),
|
||||
|
@ -199,7 +249,10 @@ def upgrade() -> None:
|
|||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_workflow_run_parameters_workflow_run_id"), "workflow_run_parameters", ["workflow_run_id"], unique=False
|
||||
op.f("ix_workflow_run_parameters_workflow_run_id"),
|
||||
"workflow_run_parameters",
|
||||
["workflow_run_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"steps",
|
||||
|
@ -261,23 +314,38 @@ def downgrade() -> None:
|
|||
op.drop_table("artifacts")
|
||||
op.drop_index(op.f("ix_steps_step_id"), table_name="steps")
|
||||
op.drop_table("steps")
|
||||
op.drop_index(op.f("ix_workflow_run_parameters_workflow_run_id"), table_name="workflow_run_parameters")
|
||||
op.drop_index(op.f("ix_workflow_run_parameters_workflow_parameter_id"), table_name="workflow_run_parameters")
|
||||
op.drop_index(
|
||||
op.f("ix_workflow_run_parameters_workflow_run_id"),
|
||||
table_name="workflow_run_parameters",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_workflow_run_parameters_workflow_parameter_id"),
|
||||
table_name="workflow_run_parameters",
|
||||
)
|
||||
op.drop_table("workflow_run_parameters")
|
||||
op.drop_index(op.f("ix_tasks_task_id"), table_name="tasks")
|
||||
op.drop_table("tasks")
|
||||
op.drop_index(op.f("ix_workflow_runs_workflow_run_id"), table_name="workflow_runs")
|
||||
op.drop_table("workflow_runs")
|
||||
op.drop_index(op.f("ix_workflow_parameters_workflow_parameter_id"), table_name="workflow_parameters")
|
||||
op.drop_index(
|
||||
op.f("ix_workflow_parameters_workflow_parameter_id"),
|
||||
table_name="workflow_parameters",
|
||||
)
|
||||
op.drop_index(op.f("ix_workflow_parameters_workflow_id"), table_name="workflow_parameters")
|
||||
op.drop_table("workflow_parameters")
|
||||
op.drop_index(op.f("ix_aws_secret_parameters_workflow_id"), table_name="aws_secret_parameters")
|
||||
op.drop_index(op.f("ix_aws_secret_parameters_aws_secret_parameter_id"), table_name="aws_secret_parameters")
|
||||
op.drop_index(
|
||||
op.f("ix_aws_secret_parameters_aws_secret_parameter_id"),
|
||||
table_name="aws_secret_parameters",
|
||||
)
|
||||
op.drop_table("aws_secret_parameters")
|
||||
op.drop_index(op.f("ix_workflows_workflow_id"), table_name="workflows")
|
||||
op.drop_table("workflows")
|
||||
op.drop_index(op.f("ix_organization_auth_tokens_token"), table_name="organization_auth_tokens")
|
||||
op.drop_index(op.f("ix_organization_auth_tokens_organization_id"), table_name="organization_auth_tokens")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_auth_tokens_organization_id"),
|
||||
table_name="organization_auth_tokens",
|
||||
)
|
||||
op.drop_index(op.f("ix_organization_auth_tokens_id"), table_name="organization_auth_tokens")
|
||||
op.drop_table("organization_auth_tokens")
|
||||
op.drop_index(op.f("ix_organizations_organization_id"), table_name="organizations")
|
||||
|
|
|
@ -37,9 +37,17 @@ def upgrade() -> None:
|
|||
sa.PrimaryKeyConstraint("output_parameter_id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_output_parameters_output_parameter_id"), "output_parameters", ["output_parameter_id"], unique=False
|
||||
op.f("ix_output_parameters_output_parameter_id"),
|
||||
"output_parameters",
|
||||
["output_parameter_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_output_parameters_workflow_id"),
|
||||
"output_parameters",
|
||||
["workflow_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(op.f("ix_output_parameters_workflow_id"), "output_parameters", ["workflow_id"], unique=False)
|
||||
op.create_table(
|
||||
"workflow_run_output_parameters",
|
||||
sa.Column("workflow_run_id", sa.String(), nullable=False),
|
||||
|
@ -74,10 +82,12 @@ def upgrade() -> None:
|
|||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(
|
||||
op.f("ix_workflow_run_output_parameters_workflow_run_id"), table_name="workflow_run_output_parameters"
|
||||
op.f("ix_workflow_run_output_parameters_workflow_run_id"),
|
||||
table_name="workflow_run_output_parameters",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_workflow_run_output_parameters_output_parameter_id"), table_name="workflow_run_output_parameters"
|
||||
op.f("ix_workflow_run_output_parameters_output_parameter_id"),
|
||||
table_name="workflow_run_output_parameters",
|
||||
)
|
||||
op.drop_table("workflow_run_output_parameters")
|
||||
op.drop_index(op.f("ix_output_parameters_workflow_id"), table_name="output_parameters")
|
||||
|
|
|
@ -58,7 +58,8 @@ def upgrade() -> None:
|
|||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(
|
||||
op.f("ix_bitwarden_login_credential_parameters_workflow_id"), table_name="bitwarden_login_credential_parameters"
|
||||
op.f("ix_bitwarden_login_credential_parameters_workflow_id"),
|
||||
table_name="bitwarden_login_credential_parameters",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_bitwarden_login_credential_parameters_bitwarden_login_credential_parameter_id"),
|
||||
|
|
|
@ -19,7 +19,12 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index("org_task_step_index", "artifacts", ["organization_id", "task_id", "step_id"], unique=False)
|
||||
op.create_index(
|
||||
"org_task_step_index",
|
||||
"artifacts",
|
||||
["organization_id", "task_id", "step_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
|
|
|
@ -23,10 +23,22 @@ def upgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("workflows", "workflow_permanent_id", existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column("workflows", "version", existing_type=sa.INTEGER(), nullable=False)
|
||||
op.create_index(op.f("ix_workflows_workflow_permanent_id"), "workflows", ["workflow_permanent_id"], unique=False)
|
||||
op.create_index("permanent_id_version_idx", "workflows", ["workflow_permanent_id", "version"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_workflows_workflow_permanent_id"),
|
||||
"workflows",
|
||||
["workflow_permanent_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"permanent_id_version_idx",
|
||||
"workflows",
|
||||
["workflow_permanent_id", "version"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"uc_org_permanent_id_version", "workflows", ["organization_id", "workflow_permanent_id", "version"]
|
||||
"uc_org_permanent_id_version",
|
||||
"workflows",
|
||||
["organization_id", "workflow_permanent_id", "version"],
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
68
poetry.lock
generated
68
poetry.lock
generated
|
@ -583,50 +583,6 @@ charset-normalizer = ["charset-normalizer"]
|
|||
html5lib = ["html5lib"]
|
||||
lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "23.12.1"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"},
|
||||
{file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"},
|
||||
{file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"},
|
||||
{file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"},
|
||||
{file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"},
|
||||
{file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"},
|
||||
{file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"},
|
||||
{file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"},
|
||||
{file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"},
|
||||
{file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"},
|
||||
{file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"},
|
||||
{file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"},
|
||||
{file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"},
|
||||
{file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"},
|
||||
{file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"},
|
||||
{file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"},
|
||||
{file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"},
|
||||
{file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"},
|
||||
{file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"},
|
||||
{file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"},
|
||||
{file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"},
|
||||
{file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "bleach"
|
||||
version = "6.1.0"
|
||||
|
@ -3075,6 +3031,7 @@ files = [
|
|||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"},
|
||||
|
@ -4215,17 +4172,6 @@ files = [
|
|||
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
|
||||
testing = ["docopt", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pexpect"
|
||||
version = "4.9.0"
|
||||
|
@ -5252,6 +5198,7 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
|
@ -5259,8 +5206,15 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
|
@ -5277,6 +5231,7 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
|
@ -5284,6 +5239,7 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
|
@ -7234,4 +7190,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11,<3.12"
|
||||
content-hash = "d2db23b7c08b0c9225fe746d22a029a372d2848b4895106612888fc1a0041e79"
|
||||
content-hash = "0dc27842c8de420ea8e4d4573e2e062f4a99d27b0e8e4c398abea4b7137b7e2d"
|
||||
|
|
|
@ -57,8 +57,7 @@ fpdf = "^1.7.2"
|
|||
pypdf = "^4.2.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
isort = "^5.12.0"
|
||||
black = "^23.3.0"
|
||||
isort = "^5.13.2"
|
||||
pre-commit = "^3.3.3"
|
||||
mypy = "^1.4.1"
|
||||
flake8 = "^6.0.0"
|
||||
|
@ -84,11 +83,38 @@ clipboard = "^0.0.4"
|
|||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
"alembic/env.py",
|
||||
]
|
||||
line-length = 120
|
||||
target-version = ['py311']
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '(/dist|/.venv|/venv|/build)/'
|
||||
target-version = "py311"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
|
|
@ -13,7 +13,10 @@ async def create_org(org_name: str, webhook_callback_url: str | None = None) ->
|
|||
await create_org_api_token(organization.organization_id)
|
||||
|
||||
|
||||
def main(org_name: str, webhook_callback_url: Annotated[Optional[str], typer.Argument()] = None) -> None:
|
||||
def main(
|
||||
org_name: str,
|
||||
webhook_callback_url: Annotated[Optional[str], typer.Argument()] = None,
|
||||
) -> None:
|
||||
asyncio.run(create_org(org_name, webhook_callback_url))
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,12 @@ class InvalidOpenAIResponseFormat(SkyvernException):
|
|||
|
||||
|
||||
class FailedToSendWebhook(SkyvernException):
|
||||
def __init__(self, task_id: str | None = None, workflow_run_id: str | None = None, workflow_id: str | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str | None = None,
|
||||
workflow_run_id: str | None = None,
|
||||
workflow_id: str | None = None,
|
||||
):
|
||||
workflow_run_str = f"workflow_run_id={workflow_run_id}" if workflow_run_id else ""
|
||||
workflow_str = f"workflow_id={workflow_id}" if workflow_id else ""
|
||||
task_str = f"task_id={task_id}" if task_id else ""
|
||||
|
@ -122,7 +127,10 @@ class WorkflowNotFound(SkyvernHTTPException):
|
|||
else:
|
||||
workflow_repr = f"workflow_permanent_id={workflow_permanent_id}"
|
||||
|
||||
super().__init__(f"Workflow not found. {workflow_repr}", status_code=status.HTTP_404_NOT_FOUND)
|
||||
super().__init__(
|
||||
f"Workflow not found. {workflow_repr}",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowRunNotFound(SkyvernException):
|
||||
|
@ -144,7 +152,10 @@ class MissingValueForParameter(SkyvernException):
|
|||
|
||||
class WorkflowParameterNotFound(SkyvernHTTPException):
|
||||
def __init__(self, workflow_parameter_id: str) -> None:
|
||||
super().__init__(f"Workflow parameter {workflow_parameter_id} not found", status_code=status.HTTP_404_NOT_FOUND)
|
||||
super().__init__(
|
||||
f"Workflow parameter {workflow_parameter_id} not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
class FailedToNavigateToUrl(SkyvernException):
|
||||
|
@ -188,7 +199,10 @@ class BrowserStateMissingPage(SkyvernException):
|
|||
|
||||
class OrganizationNotFound(SkyvernHTTPException):
|
||||
def __init__(self, organization_id: str) -> None:
|
||||
super().__init__(f"Organization {organization_id} not found", status_code=status.HTTP_404_NOT_FOUND)
|
||||
super().__init__(
|
||||
f"Organization {organization_id} not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
class StepNotFound(SkyvernHTTPException):
|
||||
|
|
|
@ -17,4 +17,10 @@ if __name__ == "__main__":
|
|||
load_dotenv()
|
||||
|
||||
reload = SettingsManager.get_settings().ENV == "local"
|
||||
uvicorn.run("skyvern.forge.api_app:app", host="0.0.0.0", port=port, log_level="info", reload=reload)
|
||||
uvicorn.run(
|
||||
"skyvern.forge.api_app:app",
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
log_level="info",
|
||||
reload=reload,
|
||||
)
|
||||
|
|
|
@ -54,7 +54,10 @@ class ForgeAgent:
|
|||
for module in SettingsManager.get_settings().ADDITIONAL_MODULES:
|
||||
LOG.info("Loading additional module", module=module)
|
||||
__import__(module)
|
||||
LOG.info("Additional modules loaded", modules=SettingsManager.get_settings().ADDITIONAL_MODULES)
|
||||
LOG.info(
|
||||
"Additional modules loaded",
|
||||
modules=SettingsManager.get_settings().ADDITIONAL_MODULES,
|
||||
)
|
||||
LOG.info(
|
||||
"Initializing ForgeAgent",
|
||||
env=SettingsManager.get_settings().ENV,
|
||||
|
@ -115,7 +118,10 @@ class ForgeAgent:
|
|||
if task_url is None:
|
||||
browser_state = await app.BROWSER_MANAGER.get_or_create_for_workflow_run(workflow_run=workflow_run)
|
||||
if not browser_state.page:
|
||||
LOG.error("BrowserState has no page", workflow_run_id=workflow_run.workflow_run_id)
|
||||
LOG.error(
|
||||
"BrowserState has no page",
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
)
|
||||
raise MissingBrowserStatePage(workflow_run_id=workflow_run.workflow_run_id)
|
||||
|
||||
if browser_state.page.url == "about:blank":
|
||||
|
@ -155,7 +161,9 @@ class ForgeAgent:
|
|||
)
|
||||
# Update task status to running
|
||||
task = await app.DATABASE.update_task(
|
||||
task_id=task.task_id, organization_id=task.organization_id, status=TaskStatus.running
|
||||
task_id=task.task_id,
|
||||
organization_id=task.organization_id,
|
||||
status=TaskStatus.running,
|
||||
)
|
||||
step = await app.DATABASE.create_step(
|
||||
task.task_id,
|
||||
|
@ -215,7 +223,11 @@ class ForgeAgent:
|
|||
try:
|
||||
# Check some conditions before executing the step, throw an exception if the step can't be executed
|
||||
await self.validate_step_execution(task, step)
|
||||
step, browser_state, detailed_output = await self._initialize_execution_state(task, step, workflow_run)
|
||||
(
|
||||
step,
|
||||
browser_state,
|
||||
detailed_output,
|
||||
) = await self._initialize_execution_state(task, step, workflow_run)
|
||||
|
||||
if browser_state.page:
|
||||
self.register_async_operations(organization, task, browser_state.page)
|
||||
|
@ -242,9 +254,11 @@ class ForgeAgent:
|
|||
return step, detailed_output, None
|
||||
elif step.status == StepStatus.completed:
|
||||
# TODO (kerem): keep the task object uptodate at all times so that send_task_response can just use it
|
||||
is_task_completed, maybe_last_step, maybe_next_step = await self.handle_completed_step(
|
||||
organization, task, step
|
||||
)
|
||||
(
|
||||
is_task_completed,
|
||||
maybe_last_step,
|
||||
maybe_next_step,
|
||||
) = await self.handle_completed_step(organization, task, step)
|
||||
if is_task_completed is not None and maybe_last_step:
|
||||
last_step = maybe_last_step
|
||||
await self.send_task_response(
|
||||
|
@ -358,7 +372,10 @@ class ForgeAgent:
|
|||
step_retry=step.retry_index,
|
||||
)
|
||||
step = await self.update_step(step=step, status=StepStatus.running)
|
||||
scraped_page, extract_action_prompt = await self._build_and_record_step_prompt(
|
||||
(
|
||||
scraped_page,
|
||||
extract_action_prompt,
|
||||
) = await self._build_and_record_step_prompt(
|
||||
task,
|
||||
step,
|
||||
browser_state,
|
||||
|
@ -380,7 +397,8 @@ class ForgeAgent:
|
|||
else:
|
||||
actions = [
|
||||
CompleteAction(
|
||||
reasoning="Task has no navigation goal.", data_extraction_goal=task.data_extraction_goal
|
||||
reasoning="Task has no navigation goal.",
|
||||
data_extraction_goal=task.data_extraction_goal,
|
||||
)
|
||||
]
|
||||
detailed_agent_step_output.actions = actions
|
||||
|
@ -393,7 +411,9 @@ class ForgeAgent:
|
|||
step_retry=step.retry_index,
|
||||
)
|
||||
step = await self.update_step(
|
||||
step=step, status=StepStatus.failed, output=detailed_agent_step_output.to_agent_step_output()
|
||||
step=step,
|
||||
status=StepStatus.failed,
|
||||
output=detailed_agent_step_output.to_agent_step_output(),
|
||||
)
|
||||
detailed_agent_step_output = DetailedAgentStepOutput(
|
||||
scraped_page=scraped_page,
|
||||
|
@ -426,7 +446,11 @@ class ForgeAgent:
|
|||
# if there are wait actions and there are other actions in the list, skip wait actions
|
||||
if wait_actions_len > 0 and wait_actions_len < len(actions):
|
||||
actions = [action for action in actions if action.action_type != ActionType.WAIT]
|
||||
LOG.info("Skipping wait actions", wait_actions_to_skip=wait_actions_to_skip, actions=actions)
|
||||
LOG.info(
|
||||
"Skipping wait actions",
|
||||
wait_actions_to_skip=wait_actions_to_skip,
|
||||
actions=actions,
|
||||
)
|
||||
|
||||
# initialize list of tuples and set actions as the first element of each tuple so that in the case
|
||||
# of an exception, we can still see all the actions
|
||||
|
@ -436,13 +460,19 @@ class ForgeAgent:
|
|||
for action_idx, action in enumerate(actions):
|
||||
if isinstance(action, WebAction):
|
||||
if action.element_id in web_action_element_ids:
|
||||
LOG.error("Duplicate action element id. Action handling stops", action=action)
|
||||
LOG.error(
|
||||
"Duplicate action element id. Action handling stops",
|
||||
action=action,
|
||||
)
|
||||
break
|
||||
web_action_element_ids.add(action.element_id)
|
||||
|
||||
self.async_operation_pool.run_operation(task.task_id, AgentPhase.action)
|
||||
results = await ActionHandler.handle_action(scraped_page, task, step, browser_state, action)
|
||||
detailed_agent_step_output.actions_and_results[action_idx] = (action, results)
|
||||
detailed_agent_step_output.actions_and_results[action_idx] = (
|
||||
action,
|
||||
results,
|
||||
)
|
||||
# wait random time between actions to avoid detection
|
||||
await asyncio.sleep(random.uniform(1.0, 2.0))
|
||||
await self.record_artifacts_after_action(task, step, browser_state)
|
||||
|
@ -467,7 +497,10 @@ class ForgeAgent:
|
|||
# for now, we're being optimistic and assuming that
|
||||
# js call doesn't have impact on the following actions
|
||||
if results[-1].javascript_triggered:
|
||||
LOG.info("Action triggered javascript. Stop executing reamaining actions.", action=action)
|
||||
LOG.info(
|
||||
"Action triggered javascript. Stop executing reamaining actions.",
|
||||
action=action,
|
||||
)
|
||||
# stop executing the rest actions
|
||||
break
|
||||
else:
|
||||
|
@ -484,7 +517,9 @@ class ForgeAgent:
|
|||
)
|
||||
# if the action failed, don't execute the rest of the actions, mark the step as failed, and retry
|
||||
failed_step = await self.update_step(
|
||||
step=step, status=StepStatus.failed, output=detailed_agent_step_output.to_agent_step_output()
|
||||
step=step,
|
||||
status=StepStatus.failed,
|
||||
output=detailed_agent_step_output.to_agent_step_output(),
|
||||
)
|
||||
return failed_step, detailed_agent_step_output
|
||||
|
||||
|
@ -498,7 +533,9 @@ class ForgeAgent:
|
|||
)
|
||||
# If no action errors return the agent state and output
|
||||
completed_step = await self.update_step(
|
||||
step=step, status=StepStatus.completed, output=detailed_agent_step_output.to_agent_step_output()
|
||||
step=step,
|
||||
status=StepStatus.completed,
|
||||
output=detailed_agent_step_output.to_agent_step_output(),
|
||||
)
|
||||
return completed_step, detailed_agent_step_output
|
||||
except Exception:
|
||||
|
@ -510,7 +547,9 @@ class ForgeAgent:
|
|||
step_retry=step.retry_index,
|
||||
)
|
||||
failed_step = await self.update_step(
|
||||
step=step, status=StepStatus.failed, output=detailed_agent_step_output.to_agent_step_output()
|
||||
step=step,
|
||||
status=StepStatus.failed,
|
||||
output=detailed_agent_step_output.to_agent_step_output(),
|
||||
)
|
||||
return failed_step, detailed_agent_step_output
|
||||
|
||||
|
@ -645,7 +684,7 @@ class ForgeAgent:
|
|||
):
|
||||
element_tree_format = ElementTreeFormat.HTML
|
||||
LOG.info(
|
||||
f"Building element tree",
|
||||
"Building element tree",
|
||||
task_id=task.task_id,
|
||||
workflow_run_id=task.workflow_run_id,
|
||||
format=element_tree_format,
|
||||
|
@ -660,7 +699,7 @@ class ForgeAgent:
|
|||
elements=scraped_page.build_element_tree(element_tree_format),
|
||||
data_extraction_goal=task.data_extraction_goal,
|
||||
action_history=actions_and_results_str,
|
||||
error_code_mapping_str=json.dumps(task.error_code_mapping) if task.error_code_mapping else None,
|
||||
error_code_mapping_str=(json.dumps(task.error_code_mapping) if task.error_code_mapping else None),
|
||||
utc_datetime=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
@ -791,7 +830,11 @@ class ForgeAgent:
|
|||
LOG.error("Failed to get task from db when sending task response")
|
||||
raise TaskNotFound(task_id=task.task_id)
|
||||
except Exception as e:
|
||||
LOG.error("Failed to get task from db when sending task response", task_id=task.task_id, error=e)
|
||||
LOG.error(
|
||||
"Failed to get task from db when sending task response",
|
||||
task_id=task.task_id,
|
||||
error=e,
|
||||
)
|
||||
raise TaskNotFound(task_id=task.task_id) from e
|
||||
task = refreshed_task
|
||||
# log the task status as an event
|
||||
|
@ -843,7 +886,11 @@ class ForgeAgent:
|
|||
await self.execute_task_webhook(task=task, last_step=last_step, api_key=api_key)
|
||||
|
||||
async def execute_task_webhook(
|
||||
self, task: Task, last_step: Step, api_key: str | None, skip_artifacts: bool = False
|
||||
self,
|
||||
task: Task,
|
||||
last_step: Step,
|
||||
api_key: str | None,
|
||||
skip_artifacts: bool = False,
|
||||
) -> None:
|
||||
if not api_key:
|
||||
LOG.warning(
|
||||
|
@ -1106,7 +1153,11 @@ class ForgeAgent:
|
|||
)
|
||||
last_step = await self.update_step(step, is_last=True)
|
||||
extracted_information = await self.get_extracted_information_for_task(task)
|
||||
await self.update_task(task, status=TaskStatus.completed, extracted_information=extracted_information)
|
||||
await self.update_task(
|
||||
task,
|
||||
status=TaskStatus.completed,
|
||||
extracted_information=extracted_information,
|
||||
)
|
||||
return True, last_step, None
|
||||
if step.is_terminated():
|
||||
LOG.info(
|
||||
|
@ -1193,5 +1244,7 @@ class ForgeAgent:
|
|||
task_errors.extend([error.model_dump() for error in step_errors])
|
||||
|
||||
return await app.DATABASE.update_task(
|
||||
task_id=task.task_id, organization_id=task.organization_id, errors=task_errors
|
||||
task_id=task.task_id,
|
||||
organization_id=task.organization_id,
|
||||
errors=task_errors,
|
||||
)
|
||||
|
|
|
@ -102,7 +102,8 @@ def get_agent_app(router: APIRouter = base_router) -> FastAPI:
|
|||
LOG.info("Loading additional module to set up api app", module=module)
|
||||
__import__(module)
|
||||
LOG.info(
|
||||
"Additional modules loaded to set up api app", modules=SettingsManager.get_settings().ADDITIONAL_MODULES
|
||||
"Additional modules loaded to set up api app",
|
||||
modules=SettingsManager.get_settings().ADDITIONAL_MODULES,
|
||||
)
|
||||
|
||||
if forge_app.setup_api_app:
|
||||
|
|
|
@ -31,7 +31,8 @@ tracer.configure(
|
|||
setup_logger()
|
||||
SETTINGS_MANAGER = SettingsManager.get_settings()
|
||||
DATABASE = AgentDB(
|
||||
SettingsManager.get_settings().DATABASE_STRING, debug_enabled=SettingsManager.get_settings().DEBUG_MODE
|
||||
SettingsManager.get_settings().DATABASE_STRING,
|
||||
debug_enabled=SettingsManager.get_settings().DEBUG_MODE,
|
||||
)
|
||||
STORAGE = StorageFactory.get_storage()
|
||||
ARTIFACT_MANAGER = ArtifactManager()
|
||||
|
|
|
@ -50,7 +50,7 @@ class AsyncOperation:
|
|||
def run(self) -> asyncio.Task | None:
|
||||
if self.aio_task is not None and not self.aio_task.done():
|
||||
LOG.warning(
|
||||
f"Task already running",
|
||||
"Task already running",
|
||||
task_id=self.task_id,
|
||||
operation_type=self.type,
|
||||
agent_phase=self.agent_phase,
|
||||
|
@ -113,7 +113,7 @@ class AsyncOperationPool:
|
|||
aio_task = self._aio_tasks[task_id][operation_type]
|
||||
if not aio_task.done():
|
||||
LOG.info(
|
||||
f"aio task already running",
|
||||
"aio task already running",
|
||||
task_id=task_id,
|
||||
operation_type=operation_type,
|
||||
agent_phase=agent_phase,
|
||||
|
@ -130,6 +130,9 @@ class AsyncOperationPool:
|
|||
async with asyncio.timeout(30):
|
||||
await asyncio.gather(*[aio_task for aio_task in self.get_aio_tasks(task_id) if not aio_task.done()])
|
||||
except asyncio.TimeoutError:
|
||||
LOG.error(f"Timeout (30s) while waiting for pending async tasks for task_id={task_id}", task_id=task_id)
|
||||
LOG.error(
|
||||
f"Timeout (30s) while waiting for pending async tasks for task_id={task_id}",
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
self.remove_operations(task_id)
|
||||
|
|
|
@ -44,7 +44,7 @@ class LLMAPIHandlerFactory:
|
|||
),
|
||||
num_retries=llm_config.num_retries,
|
||||
retry_after=llm_config.retry_delay_seconds,
|
||||
set_verbose=False if SettingsManager.get_settings().is_cloud_environment() else llm_config.set_verbose,
|
||||
set_verbose=(False if SettingsManager.get_settings().is_cloud_environment() else llm_config.set_verbose),
|
||||
enable_pre_call_checks=True,
|
||||
)
|
||||
main_model_group = llm_config.main_model_group
|
||||
|
@ -101,7 +101,11 @@ class LLMAPIHandlerFactory:
|
|||
except openai.OpenAIError as e:
|
||||
raise LLMProviderError(llm_key) from e
|
||||
except Exception as e:
|
||||
LOG.exception("LLM request failed unexpectedly", llm_key=llm_key, model=main_model_group)
|
||||
LOG.exception(
|
||||
"LLM request failed unexpectedly",
|
||||
llm_key=llm_key,
|
||||
model=main_model_group,
|
||||
)
|
||||
raise LLMProviderError(llm_key) from e
|
||||
|
||||
if step:
|
||||
|
|
|
@ -58,35 +58,58 @@ if not any(
|
|||
if SettingsManager.get_settings().ENABLE_OPENAI:
|
||||
LLMConfigRegistry.register_config(
|
||||
"OPENAI_GPT4_TURBO",
|
||||
LLMConfig("gpt-4-turbo", ["OPENAI_API_KEY"], supports_vision=False, add_assistant_prefix=False),
|
||||
LLMConfig(
|
||||
"gpt-4-turbo",
|
||||
["OPENAI_API_KEY"],
|
||||
supports_vision=False,
|
||||
add_assistant_prefix=False,
|
||||
),
|
||||
)
|
||||
LLMConfigRegistry.register_config(
|
||||
"OPENAI_GPT4V", LLMConfig("gpt-4-turbo", ["OPENAI_API_KEY"], supports_vision=True, add_assistant_prefix=False)
|
||||
"OPENAI_GPT4V",
|
||||
LLMConfig(
|
||||
"gpt-4-turbo",
|
||||
["OPENAI_API_KEY"],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=False,
|
||||
),
|
||||
)
|
||||
|
||||
if SettingsManager.get_settings().ENABLE_ANTHROPIC:
|
||||
LLMConfigRegistry.register_config(
|
||||
"ANTHROPIC_CLAUDE3",
|
||||
LLMConfig(
|
||||
"anthropic/claude-3-sonnet-20240229", ["ANTHROPIC_API_KEY"], supports_vision=True, add_assistant_prefix=True
|
||||
"anthropic/claude-3-sonnet-20240229",
|
||||
["ANTHROPIC_API_KEY"],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=True,
|
||||
),
|
||||
)
|
||||
LLMConfigRegistry.register_config(
|
||||
"ANTHROPIC_CLAUDE3_OPUS",
|
||||
LLMConfig(
|
||||
"anthropic/claude-3-opus-20240229", ["ANTHROPIC_API_KEY"], supports_vision=True, add_assistant_prefix=True
|
||||
"anthropic/claude-3-opus-20240229",
|
||||
["ANTHROPIC_API_KEY"],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=True,
|
||||
),
|
||||
)
|
||||
LLMConfigRegistry.register_config(
|
||||
"ANTHROPIC_CLAUDE3_SONNET",
|
||||
LLMConfig(
|
||||
"anthropic/claude-3-sonnet-20240229", ["ANTHROPIC_API_KEY"], supports_vision=True, add_assistant_prefix=True
|
||||
"anthropic/claude-3-sonnet-20240229",
|
||||
["ANTHROPIC_API_KEY"],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=True,
|
||||
),
|
||||
)
|
||||
LLMConfigRegistry.register_config(
|
||||
"ANTHROPIC_CLAUDE3_HAIKU",
|
||||
LLMConfig(
|
||||
"anthropic/claude-3-haiku-20240307", ["ANTHROPIC_API_KEY"], supports_vision=True, add_assistant_prefix=True
|
||||
"anthropic/claude-3-haiku-20240307",
|
||||
["ANTHROPIC_API_KEY"],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -125,7 +148,12 @@ if SettingsManager.get_settings().ENABLE_AZURE:
|
|||
"AZURE_OPENAI_GPT4V",
|
||||
LLMConfig(
|
||||
f"azure/{SettingsManager.get_settings().AZURE_DEPLOYMENT}",
|
||||
["AZURE_DEPLOYMENT", "AZURE_API_KEY", "AZURE_API_BASE", "AZURE_API_VERSION"],
|
||||
[
|
||||
"AZURE_DEPLOYMENT",
|
||||
"AZURE_API_KEY",
|
||||
"AZURE_API_BASE",
|
||||
"AZURE_API_VERSION",
|
||||
],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=False,
|
||||
),
|
||||
|
|
|
@ -33,7 +33,10 @@ async def llm_messages_builder(
|
|||
)
|
||||
# Anthropic models seems to struggle to always output a valid json object so we need to prefill the response to force it:
|
||||
if add_assistant_prefix:
|
||||
return [{"role": "user", "content": messages}, {"role": "assistant", "content": "{"}]
|
||||
return [
|
||||
{"role": "user", "content": messages},
|
||||
{"role": "assistant", "content": "{"},
|
||||
]
|
||||
return [{"role": "user", "content": messages}]
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,11 @@ class ArtifactManager:
|
|||
upload_aiotasks_map: dict[str, list[asyncio.Task[None]]] = defaultdict(list)
|
||||
|
||||
async def create_artifact(
|
||||
self, step: Step, artifact_type: ArtifactType, data: bytes | None = None, path: str | None = None
|
||||
self,
|
||||
step: Step,
|
||||
artifact_type: ArtifactType,
|
||||
data: bytes | None = None,
|
||||
path: str | None = None,
|
||||
) -> str:
|
||||
# TODO (kerem): Which is better?
|
||||
# current: (disadvantage: we create the artifact_id UUID here)
|
||||
|
@ -87,7 +91,10 @@ class ArtifactManager:
|
|||
duration=time.time() - st,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
LOG.error(f"Timeout (30s) while waiting for upload tasks for task_id={task_id}", task_id=task_id)
|
||||
LOG.error(
|
||||
f"Timeout (30s) while waiting for upload tasks for task_id={task_id}",
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
del self.upload_aiotasks_map[task_id]
|
||||
|
||||
|
@ -109,7 +116,10 @@ class ArtifactManager:
|
|||
duration=time.time() - st,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
LOG.error(f"Timeout (30s) while waiting for upload tasks for task_ids={task_ids}", task_ids=task_ids)
|
||||
LOG.error(
|
||||
f"Timeout (30s) while waiting for upload tasks for task_ids={task_ids}",
|
||||
task_ids=task_ids,
|
||||
)
|
||||
|
||||
for task_id in task_ids:
|
||||
del self.upload_aiotasks_map[task_id]
|
||||
|
|
|
@ -28,7 +28,11 @@ class LocalStorage(BaseStorage):
|
|||
with open(file_path, "wb") as f:
|
||||
f.write(data)
|
||||
except Exception:
|
||||
LOG.exception("Failed to store artifact locally.", file_path=file_path, artifact=artifact)
|
||||
LOG.exception(
|
||||
"Failed to store artifact locally.",
|
||||
file_path=file_path,
|
||||
artifact=artifact,
|
||||
)
|
||||
|
||||
async def store_artifact_from_path(self, artifact: Artifact, path: str) -> None:
|
||||
file_path = None
|
||||
|
@ -37,7 +41,11 @@ class LocalStorage(BaseStorage):
|
|||
self._create_directories_if_not_exists(file_path)
|
||||
Path(path).replace(file_path)
|
||||
except Exception:
|
||||
LOG.exception("Failed to store artifact locally.", file_path=file_path, artifact=artifact)
|
||||
LOG.exception(
|
||||
"Failed to store artifact locally.",
|
||||
file_path=file_path,
|
||||
artifact=artifact,
|
||||
)
|
||||
|
||||
async def retrieve_artifact(self, artifact: Artifact) -> bytes | None:
|
||||
file_path = None
|
||||
|
@ -46,7 +54,11 @@ class LocalStorage(BaseStorage):
|
|||
with open(file_path, "rb") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
LOG.exception("Failed to retrieve local artifact.", file_path=file_path, artifact=artifact)
|
||||
LOG.exception(
|
||||
"Failed to retrieve local artifact.",
|
||||
file_path=file_path,
|
||||
artifact=artifact,
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_share_link(self, artifact: Artifact) -> str:
|
||||
|
|
|
@ -184,7 +184,11 @@ class AgentDB:
|
|||
).first():
|
||||
return convert_to_task(task_obj, self.debug_enabled)
|
||||
else:
|
||||
LOG.info("Task not found", task_id=task_id, organization_id=organization_id)
|
||||
LOG.info(
|
||||
"Task not found",
|
||||
task_id=task_id,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
return None
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
|
@ -266,7 +270,11 @@ class AgentDB:
|
|||
).first():
|
||||
return convert_to_step(step, debug_enabled=self.debug_enabled)
|
||||
else:
|
||||
LOG.info("Latest step not found", task_id=task_id, organization_id=organization_id)
|
||||
LOG.info(
|
||||
"Latest step not found",
|
||||
task_id=task_id,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
return None
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
|
@ -812,7 +820,10 @@ class AgentDB:
|
|||
)
|
||||
.where(WorkflowModel.organization_id == organization_id)
|
||||
.where(WorkflowModel.deleted_at.is_(None))
|
||||
.group_by(WorkflowModel.organization_id, WorkflowModel.workflow_permanent_id)
|
||||
.group_by(
|
||||
WorkflowModel.organization_id,
|
||||
WorkflowModel.workflow_permanent_id,
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
main_query = (
|
||||
|
@ -924,7 +935,10 @@ class AgentDB:
|
|||
await session.commit()
|
||||
await session.refresh(workflow_run)
|
||||
return convert_to_workflow_run(workflow_run)
|
||||
LOG.error("WorkflowRun not found, nothing to update", workflow_run_id=workflow_run_id)
|
||||
LOG.error(
|
||||
"WorkflowRun not found, nothing to update",
|
||||
workflow_run_id=workflow_run_id,
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_workflow_run(self, workflow_run_id: str) -> WorkflowRun | None:
|
||||
|
@ -1066,7 +1080,10 @@ class AgentDB:
|
|||
raise
|
||||
|
||||
async def create_workflow_run_output_parameter(
|
||||
self, workflow_run_id: str, output_parameter_id: str, value: dict[str, Any] | list | str | None
|
||||
self,
|
||||
workflow_run_id: str,
|
||||
output_parameter_id: str,
|
||||
value: dict[str, Any] | list | str | None,
|
||||
) -> WorkflowRunOutputParameter:
|
||||
try:
|
||||
async with self.Session() as session:
|
||||
|
@ -1149,7 +1166,9 @@ class AgentDB:
|
|||
(
|
||||
workflow_parameter,
|
||||
convert_to_workflow_run_parameter(
|
||||
workflow_run_parameter, workflow_parameter, self.debug_enabled
|
||||
workflow_run_parameter,
|
||||
workflow_parameter,
|
||||
self.debug_enabled,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -63,7 +63,11 @@ class TaskModel(Base):
|
|||
max_steps_per_run = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False, index=True)
|
||||
modified_at = Column(
|
||||
DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False, index=True
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
|
||||
|
@ -80,7 +84,12 @@ class StepModel(Base):
|
|||
is_last = Column(Boolean, default=False)
|
||||
retry_index = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
input_token_count = Column(Integer, default=0)
|
||||
output_token_count = Column(Integer, default=0)
|
||||
step_cost = Column(Numeric, default=0)
|
||||
|
@ -96,7 +105,12 @@ class OrganizationModel(Base):
|
|||
max_retries_per_step = Column(Integer, nullable=True)
|
||||
domain = Column(String, nullable=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
class OrganizationAuthTokenModel(Base):
|
||||
|
@ -115,7 +129,12 @@ class OrganizationAuthTokenModel(Base):
|
|||
valid = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
|
@ -130,13 +149,23 @@ class ArtifactModel(Base):
|
|||
artifact_type = Column(String)
|
||||
uri = Column(String)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowModel(Base):
|
||||
__tablename__ = "workflows"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("organization_id", "workflow_permanent_id", "version", name="uc_org_permanent_id_version"),
|
||||
UniqueConstraint(
|
||||
"organization_id",
|
||||
"workflow_permanent_id",
|
||||
"version",
|
||||
name="uc_org_permanent_id_version",
|
||||
),
|
||||
Index("permanent_id_version_idx", "workflow_permanent_id", "version"),
|
||||
)
|
||||
|
||||
|
@ -149,7 +178,12 @@ class WorkflowModel(Base):
|
|||
webhook_callback_url = Column(String)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
workflow_permanent_id = Column(String, nullable=False, default=generate_workflow_permanent_id, index=True)
|
||||
|
@ -166,7 +200,12 @@ class WorkflowRunModel(Base):
|
|||
webhook_callback_url = Column(String)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowParameterModel(Base):
|
||||
|
@ -179,7 +218,12 @@ class WorkflowParameterModel(Base):
|
|||
workflow_id = Column(String, ForeignKey("workflows.workflow_id"), index=True, nullable=False)
|
||||
default_value = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
|
@ -191,7 +235,12 @@ class OutputParameterModel(Base):
|
|||
description = Column(String, nullable=True)
|
||||
workflow_id = Column(String, ForeignKey("workflows.workflow_id"), index=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
|
@ -204,7 +253,12 @@ class AWSSecretParameterModel(Base):
|
|||
description = Column(String, nullable=True)
|
||||
aws_key = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
|
@ -212,7 +266,10 @@ class BitwardenLoginCredentialParameterModel(Base):
|
|||
__tablename__ = "bitwarden_login_credential_parameters"
|
||||
|
||||
bitwarden_login_credential_parameter_id = Column(
|
||||
String, primary_key=True, index=True, default=generate_bitwarden_login_credential_parameter_id
|
||||
String,
|
||||
primary_key=True,
|
||||
index=True,
|
||||
default=generate_bitwarden_login_credential_parameter_id,
|
||||
)
|
||||
workflow_id = Column(String, ForeignKey("workflows.workflow_id"), index=True, nullable=False)
|
||||
key = Column(String, nullable=False)
|
||||
|
@ -222,16 +279,29 @@ class BitwardenLoginCredentialParameterModel(Base):
|
|||
bitwarden_master_password_aws_secret_key = Column(String, nullable=False)
|
||||
url_parameter_key = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
class WorkflowRunParameterModel(Base):
|
||||
__tablename__ = "workflow_run_parameters"
|
||||
|
||||
workflow_run_id = Column(String, ForeignKey("workflow_runs.workflow_run_id"), primary_key=True, index=True)
|
||||
workflow_run_id = Column(
|
||||
String,
|
||||
ForeignKey("workflow_runs.workflow_run_id"),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
)
|
||||
workflow_parameter_id = Column(
|
||||
String, ForeignKey("workflow_parameters.workflow_parameter_id"), primary_key=True, index=True
|
||||
String,
|
||||
ForeignKey("workflow_parameters.workflow_parameter_id"),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
)
|
||||
# Can be bool | int | float | str | dict | list depending on the workflow parameter type
|
||||
value = Column(String, nullable=False)
|
||||
|
@ -241,9 +311,17 @@ class WorkflowRunParameterModel(Base):
|
|||
class WorkflowRunOutputParameterModel(Base):
|
||||
__tablename__ = "workflow_run_output_parameters"
|
||||
|
||||
workflow_run_id = Column(String, ForeignKey("workflow_runs.workflow_run_id"), primary_key=True, index=True)
|
||||
workflow_run_id = Column(
|
||||
String,
|
||||
ForeignKey("workflow_runs.workflow_run_id"),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
)
|
||||
output_parameter_id = Column(
|
||||
String, ForeignKey("output_parameters.output_parameter_id"), primary_key=True, index=True
|
||||
String,
|
||||
ForeignKey("output_parameters.output_parameter_id"),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
)
|
||||
value = Column(JSON, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
|
|
|
@ -67,7 +67,7 @@ def convert_to_task(task_obj: TaskModel, debug_enabled: bool = False) -> Task:
|
|||
extracted_information=task_obj.extracted_information,
|
||||
failure_reason=task_obj.failure_reason,
|
||||
organization_id=task_obj.organization_id,
|
||||
proxy_location=ProxyLocation(task_obj.proxy_location) if task_obj.proxy_location else None,
|
||||
proxy_location=(ProxyLocation(task_obj.proxy_location) if task_obj.proxy_location else None),
|
||||
extracted_information_schema=task_obj.extracted_information_schema,
|
||||
workflow_run_id=task_obj.workflow_run_id,
|
||||
order=task_obj.order,
|
||||
|
@ -112,7 +112,9 @@ def convert_to_organization(org_model: OrganizationModel) -> Organization:
|
|||
)
|
||||
|
||||
|
||||
def convert_to_organization_auth_token(org_auth_token: OrganizationAuthTokenModel) -> OrganizationAuthToken:
|
||||
def convert_to_organization_auth_token(
|
||||
org_auth_token: OrganizationAuthTokenModel,
|
||||
) -> OrganizationAuthToken:
|
||||
return OrganizationAuthToken(
|
||||
id=org_auth_token.id,
|
||||
organization_id=org_auth_token.organization_id,
|
||||
|
@ -126,7 +128,10 @@ def convert_to_organization_auth_token(org_auth_token: OrganizationAuthTokenMode
|
|||
|
||||
def convert_to_artifact(artifact_model: ArtifactModel, debug_enabled: bool = False) -> Artifact:
|
||||
if debug_enabled:
|
||||
LOG.debug("Converting ArtifactModel to Artifact", artifact_id=artifact_model.artifact_id)
|
||||
LOG.debug(
|
||||
"Converting ArtifactModel to Artifact",
|
||||
artifact_id=artifact_model.artifact_id,
|
||||
)
|
||||
|
||||
return Artifact(
|
||||
artifact_id=artifact_model.artifact_id,
|
||||
|
@ -142,7 +147,10 @@ def convert_to_artifact(artifact_model: ArtifactModel, debug_enabled: bool = Fal
|
|||
|
||||
def convert_to_workflow(workflow_model: WorkflowModel, debug_enabled: bool = False) -> Workflow:
|
||||
if debug_enabled:
|
||||
LOG.debug("Converting WorkflowModel to Workflow", workflow_id=workflow_model.workflow_id)
|
||||
LOG.debug(
|
||||
"Converting WorkflowModel to Workflow",
|
||||
workflow_id=workflow_model.workflow_id,
|
||||
)
|
||||
|
||||
return Workflow(
|
||||
workflow_id=workflow_model.workflow_id,
|
||||
|
@ -150,7 +158,7 @@ def convert_to_workflow(workflow_model: WorkflowModel, debug_enabled: bool = Fal
|
|||
title=workflow_model.title,
|
||||
workflow_permanent_id=workflow_model.workflow_permanent_id,
|
||||
webhook_callback_url=workflow_model.webhook_callback_url,
|
||||
proxy_location=ProxyLocation(workflow_model.proxy_location) if workflow_model.proxy_location else None,
|
||||
proxy_location=(ProxyLocation(workflow_model.proxy_location) if workflow_model.proxy_location else None),
|
||||
version=workflow_model.version,
|
||||
description=workflow_model.description,
|
||||
workflow_definition=WorkflowDefinition.model_validate(workflow_model.workflow_definition),
|
||||
|
@ -162,13 +170,18 @@ def convert_to_workflow(workflow_model: WorkflowModel, debug_enabled: bool = Fal
|
|||
|
||||
def convert_to_workflow_run(workflow_run_model: WorkflowRunModel, debug_enabled: bool = False) -> WorkflowRun:
|
||||
if debug_enabled:
|
||||
LOG.debug("Converting WorkflowRunModel to WorkflowRun", workflow_run_id=workflow_run_model.workflow_run_id)
|
||||
LOG.debug(
|
||||
"Converting WorkflowRunModel to WorkflowRun",
|
||||
workflow_run_id=workflow_run_model.workflow_run_id,
|
||||
)
|
||||
|
||||
return WorkflowRun(
|
||||
workflow_run_id=workflow_run_model.workflow_run_id,
|
||||
workflow_id=workflow_run_model.workflow_id,
|
||||
status=WorkflowRunStatus[workflow_run_model.status],
|
||||
proxy_location=ProxyLocation(workflow_run_model.proxy_location) if workflow_run_model.proxy_location else None,
|
||||
proxy_location=(
|
||||
ProxyLocation(workflow_run_model.proxy_location) if workflow_run_model.proxy_location else None
|
||||
),
|
||||
webhook_callback_url=workflow_run_model.webhook_callback_url,
|
||||
created_at=workflow_run_model.created_at,
|
||||
modified_at=workflow_run_model.modified_at,
|
||||
|
@ -221,7 +234,8 @@ def convert_to_aws_secret_parameter(
|
|||
|
||||
|
||||
def convert_to_bitwarden_login_credential_parameter(
|
||||
bitwarden_login_credential_parameter_model: BitwardenLoginCredentialParameterModel, debug_enabled: bool = False
|
||||
bitwarden_login_credential_parameter_model: BitwardenLoginCredentialParameterModel,
|
||||
debug_enabled: bool = False,
|
||||
) -> BitwardenLoginCredentialParameter:
|
||||
if debug_enabled:
|
||||
LOG.debug(
|
||||
|
|
|
@ -91,7 +91,10 @@ class BackgroundTaskExecutor(AsyncExecutor):
|
|||
api_key: str | None,
|
||||
**kwargs: dict,
|
||||
) -> None:
|
||||
LOG.info("Executing workflow using background task executor", workflow_run_id=workflow_run_id)
|
||||
LOG.info(
|
||||
"Executing workflow using background task executor",
|
||||
workflow_run_id=workflow_run_id,
|
||||
)
|
||||
background_tasks.add_task(
|
||||
app.WORKFLOW_SERVICE.execute_workflow,
|
||||
workflow_run_id=workflow_run_id,
|
||||
|
|
|
@ -53,7 +53,12 @@ class Step(BaseModel):
|
|||
output_token_count: int = 0
|
||||
step_cost: float = 0
|
||||
|
||||
def validate_update(self, status: StepStatus | None, output: AgentStepOutput | None, is_last: bool | None) -> None:
|
||||
def validate_update(
|
||||
self,
|
||||
status: StepStatus | None,
|
||||
output: AgentStepOutput | None,
|
||||
is_last: bool | None,
|
||||
) -> None:
|
||||
old_status = self.status
|
||||
|
||||
if status and not old_status.can_update_to(status):
|
||||
|
|
|
@ -78,7 +78,12 @@ class PromptEngine:
|
|||
matches = get_close_matches(target, model_dirs, n=1, cutoff=0.1)
|
||||
return matches[0]
|
||||
except Exception:
|
||||
LOG.error("Failed to get closest match.", target=target, model_dirs=model_dirs, exc_info=True)
|
||||
LOG.error(
|
||||
"Failed to get closest match.",
|
||||
target=target,
|
||||
model_dirs=model_dirs,
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
def load_prompt(self, template: str, **kwargs: Any) -> str:
|
||||
|
@ -97,7 +102,12 @@ class PromptEngine:
|
|||
jinja_template = self.env.get_template(f"{template}.j2")
|
||||
return jinja_template.render(**kwargs)
|
||||
except Exception:
|
||||
LOG.error("Failed to load prompt.", template=template, kwargs_keys=kwargs.keys(), exc_info=True)
|
||||
LOG.error(
|
||||
"Failed to load prompt.",
|
||||
template=template,
|
||||
kwargs_keys=kwargs.keys(),
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
def load_prompt_from_string(self, template: str, **kwargs: Any) -> str:
|
||||
|
@ -115,5 +125,10 @@ class PromptEngine:
|
|||
jinja_template = self.env.from_string(template)
|
||||
return jinja_template.render(**kwargs)
|
||||
except Exception:
|
||||
LOG.error("Failed to load prompt from string.", template=template, kwargs_keys=kwargs.keys(), exc_info=True)
|
||||
LOG.error(
|
||||
"Failed to load prompt from string.",
|
||||
template=template,
|
||||
kwargs_keys=kwargs.keys(),
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
|
|
@ -54,7 +54,10 @@ async def webhook(
|
|||
x_skyvern_timestamp=x_skyvern_timestamp,
|
||||
payload=payload,
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing webhook signature or timestamp")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Missing webhook signature or timestamp",
|
||||
)
|
||||
|
||||
generated_signature = generate_skyvern_signature(
|
||||
payload.decode("utf-8"),
|
||||
|
@ -82,7 +85,12 @@ async def check_server_status() -> Response:
|
|||
|
||||
|
||||
@base_router.post("/tasks", tags=["agent"], response_model=CreateTaskResponse)
|
||||
@base_router.post("/tasks/", tags=["agent"], response_model=CreateTaskResponse, include_in_schema=False)
|
||||
@base_router.post(
|
||||
"/tasks/",
|
||||
tags=["agent"],
|
||||
response_model=CreateTaskResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def create_agent_task(
|
||||
background_tasks: BackgroundTasks,
|
||||
task: TaskRequest,
|
||||
|
@ -342,13 +350,21 @@ async def get_agent_tasks(
|
|||
"""
|
||||
analytics.capture("skyvern-oss-agent-tasks-get")
|
||||
tasks = await app.DATABASE.get_tasks(
|
||||
page, page_size, task_status=task_status, organization_id=current_org.organization_id
|
||||
page,
|
||||
page_size,
|
||||
task_status=task_status,
|
||||
organization_id=current_org.organization_id,
|
||||
)
|
||||
return ORJSONResponse([task.to_task_response().model_dump() for task in tasks])
|
||||
|
||||
|
||||
@base_router.get("/internal/tasks", tags=["agent"], response_model=list[Task])
|
||||
@base_router.get("/internal/tasks/", tags=["agent"], response_model=list[Task], include_in_schema=False)
|
||||
@base_router.get(
|
||||
"/internal/tasks/",
|
||||
tags=["agent"],
|
||||
response_model=list[Task],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def get_agent_tasks_internal(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1),
|
||||
|
@ -367,7 +383,12 @@ async def get_agent_tasks_internal(
|
|||
|
||||
|
||||
@base_router.get("/tasks/{task_id}/steps", tags=["agent"], response_model=list[Step])
|
||||
@base_router.get("/tasks/{task_id}/steps/", tags=["agent"], response_model=list[Step], include_in_schema=False)
|
||||
@base_router.get(
|
||||
"/tasks/{task_id}/steps/",
|
||||
tags=["agent"],
|
||||
response_model=list[Step],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def get_agent_task_steps(
|
||||
task_id: str,
|
||||
current_org: Organization = Depends(org_auth_service.get_current_org),
|
||||
|
@ -382,7 +403,11 @@ async def get_agent_task_steps(
|
|||
return ORJSONResponse([step.model_dump() for step in steps])
|
||||
|
||||
|
||||
@base_router.get("/tasks/{task_id}/steps/{step_id}/artifacts", tags=["agent"], response_model=list[Artifact])
|
||||
@base_router.get(
|
||||
"/tasks/{task_id}/steps/{step_id}/artifacts",
|
||||
tags=["agent"],
|
||||
response_model=list[Artifact],
|
||||
)
|
||||
@base_router.get(
|
||||
"/tasks/{task_id}/steps/{step_id}/artifacts/",
|
||||
tags=["agent"],
|
||||
|
@ -412,7 +437,11 @@ async def get_agent_task_step_artifacts(
|
|||
for i, artifact in enumerate(artifacts):
|
||||
artifact.signed_url = signed_urls[i]
|
||||
else:
|
||||
LOG.warning("Failed to get signed urls for artifacts", task_id=task_id, step_id=step_id)
|
||||
LOG.warning(
|
||||
"Failed to get signed urls for artifacts",
|
||||
task_id=task_id,
|
||||
step_id=step_id,
|
||||
)
|
||||
return ORJSONResponse([artifact.model_dump() for artifact in artifacts])
|
||||
|
||||
|
||||
|
@ -424,7 +453,11 @@ class ActionResultTmp(BaseModel):
|
|||
|
||||
|
||||
@base_router.get("/tasks/{task_id}/actions", response_model=list[ActionResultTmp])
|
||||
@base_router.get("/tasks/{task_id}/actions/", response_model=list[ActionResultTmp], include_in_schema=False)
|
||||
@base_router.get(
|
||||
"/tasks/{task_id}/actions/",
|
||||
response_model=list[ActionResultTmp],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def get_task_actions(
|
||||
task_id: str,
|
||||
current_org: Organization = Depends(org_auth_service.get_current_org),
|
||||
|
@ -441,7 +474,11 @@ async def get_task_actions(
|
|||
|
||||
|
||||
@base_router.post("/workflows/{workflow_id}/run", response_model=RunWorkflowResponse)
|
||||
@base_router.post("/workflows/{workflow_id}/run/", response_model=RunWorkflowResponse, include_in_schema=False)
|
||||
@base_router.post(
|
||||
"/workflows/{workflow_id}/run/",
|
||||
response_model=RunWorkflowResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def execute_workflow(
|
||||
background_tasks: BackgroundTasks,
|
||||
workflow_id: str,
|
||||
|
@ -476,7 +513,10 @@ async def execute_workflow(
|
|||
)
|
||||
|
||||
|
||||
@base_router.get("/workflows/{workflow_id}/runs/{workflow_run_id}", response_model=WorkflowRunStatusResponse)
|
||||
@base_router.get(
|
||||
"/workflows/{workflow_id}/runs/{workflow_run_id}",
|
||||
response_model=WorkflowRunStatusResponse,
|
||||
)
|
||||
@base_router.get(
|
||||
"/workflows/{workflow_id}/runs/{workflow_run_id}/",
|
||||
response_model=WorkflowRunStatusResponse,
|
||||
|
|
|
@ -82,13 +82,27 @@ class TaskStatus(StrEnum):
|
|||
completed = "completed"
|
||||
|
||||
def is_final(self) -> bool:
|
||||
return self in {TaskStatus.failed, TaskStatus.terminated, TaskStatus.completed, TaskStatus.timed_out}
|
||||
return self in {
|
||||
TaskStatus.failed,
|
||||
TaskStatus.terminated,
|
||||
TaskStatus.completed,
|
||||
TaskStatus.timed_out,
|
||||
}
|
||||
|
||||
def can_update_to(self, new_status: TaskStatus) -> bool:
|
||||
allowed_transitions: dict[TaskStatus, set[TaskStatus]] = {
|
||||
TaskStatus.created: {TaskStatus.queued, TaskStatus.running, TaskStatus.timed_out},
|
||||
TaskStatus.created: {
|
||||
TaskStatus.queued,
|
||||
TaskStatus.running,
|
||||
TaskStatus.timed_out,
|
||||
},
|
||||
TaskStatus.queued: {TaskStatus.running, TaskStatus.timed_out},
|
||||
TaskStatus.running: {TaskStatus.completed, TaskStatus.failed, TaskStatus.terminated, TaskStatus.timed_out},
|
||||
TaskStatus.running: {
|
||||
TaskStatus.completed,
|
||||
TaskStatus.failed,
|
||||
TaskStatus.terminated,
|
||||
TaskStatus.timed_out,
|
||||
},
|
||||
TaskStatus.failed: set(),
|
||||
TaskStatus.terminated: set(),
|
||||
TaskStatus.completed: set(),
|
||||
|
|
|
@ -53,7 +53,11 @@ class BitwardenService:
|
|||
"""
|
||||
# Step 1: Set up environment variables and log in
|
||||
try:
|
||||
env = {"BW_CLIENTID": client_id, "BW_CLIENTSECRET": client_secret, "BW_PASSWORD": master_password}
|
||||
env = {
|
||||
"BW_CLIENTID": client_id,
|
||||
"BW_CLIENTSECRET": client_secret,
|
||||
"BW_PASSWORD": master_password,
|
||||
}
|
||||
login_command = ["bw", "login", "--apikey"]
|
||||
login_result = BitwardenService.run_command(login_command, env)
|
||||
|
||||
|
@ -81,7 +85,15 @@ class BitwardenService:
|
|||
raise BitwardenUnlockError("Session key is empty.")
|
||||
|
||||
# Step 3: Retrieve the items
|
||||
list_command = ["bw", "list", "items", "--url", url, "--session", session_key]
|
||||
list_command = [
|
||||
"bw",
|
||||
"list",
|
||||
"items",
|
||||
"--url",
|
||||
url,
|
||||
"--session",
|
||||
session_key,
|
||||
]
|
||||
items_result = BitwardenService.run_command(list_command)
|
||||
|
||||
if items_result.stderr and "Event post failed" not in items_result.stderr:
|
||||
|
@ -100,7 +112,11 @@ class BitwardenService:
|
|||
totp_result = BitwardenService.run_command(totp_command)
|
||||
|
||||
if totp_result.stderr and "Event post failed" not in totp_result.stderr:
|
||||
LOG.warning("Bitwarden TOTP Error", error=totp_result.stderr, e=BitwardenTOTPError(totp_result.stderr))
|
||||
LOG.warning(
|
||||
"Bitwarden TOTP Error",
|
||||
error=totp_result.stderr,
|
||||
e=BitwardenTOTPError(totp_result.stderr),
|
||||
)
|
||||
totp_code = totp_result.stdout
|
||||
|
||||
credentials: list[dict[str, str]] = [
|
||||
|
|
|
@ -39,7 +39,9 @@ async def get_current_org(
|
|||
)
|
||||
|
||||
|
||||
async def get_current_org_with_api_key(x_api_key: Annotated[str | None, Header()] = None) -> Organization:
|
||||
async def get_current_org_with_api_key(
|
||||
x_api_key: Annotated[str | None, Header()] = None,
|
||||
) -> Organization:
|
||||
if not x_api_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
|
@ -48,7 +50,9 @@ async def get_current_org_with_api_key(x_api_key: Annotated[str | None, Header()
|
|||
return await _get_current_org_cached(x_api_key, app.DATABASE)
|
||||
|
||||
|
||||
async def get_current_org_with_authentication(authorization: Annotated[str | None, Header()] = None) -> Organization:
|
||||
async def get_current_org_with_authentication(
|
||||
authorization: Annotated[str | None, Header()] = None,
|
||||
) -> Organization:
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
|
|
|
@ -35,5 +35,5 @@ async def create_org_api_token(org_id: str) -> OrganizationAuthToken:
|
|||
token=api_key,
|
||||
token_type=OrganizationAuthTokenType.api,
|
||||
)
|
||||
LOG.info(f"Created API token for organization", organization_id=org_id)
|
||||
LOG.info("Created API token for organization", organization_id=org_id)
|
||||
return org_auth_token
|
||||
|
|
|
@ -93,7 +93,7 @@ class WorkflowRunContext:
|
|||
assume it's an actual parameter value and return it.
|
||||
|
||||
"""
|
||||
if type(secret_id_or_value) is str:
|
||||
if isinstance(secret_id_or_value, str):
|
||||
return self.secrets.get(secret_id_or_value)
|
||||
return None
|
||||
|
||||
|
@ -149,7 +149,7 @@ class WorkflowRunContext:
|
|||
url = self.values[parameter.url_parameter_key]
|
||||
else:
|
||||
LOG.error(f"URL parameter {parameter.url_parameter_key} not found or has no value")
|
||||
raise ValueError(f"URL parameter for Bitwarden login credentials not found or has no value")
|
||||
raise ValueError("URL parameter for Bitwarden login credentials not found or has no value")
|
||||
|
||||
try:
|
||||
secret_credentials = BitwardenService.get_secret_value_from_url(
|
||||
|
@ -224,7 +224,9 @@ class WorkflowRunContext:
|
|||
await self.set_parameter_values_for_output_parameter_dependent_blocks(parameter, value)
|
||||
|
||||
async def set_parameter_values_for_output_parameter_dependent_blocks(
|
||||
self, output_parameter: OutputParameter, value: dict[str, Any] | list | str | None
|
||||
self,
|
||||
output_parameter: OutputParameter,
|
||||
value: dict[str, Any] | list | str | None,
|
||||
) -> None:
|
||||
for key, parameter in self.parameters.items():
|
||||
if (
|
||||
|
@ -268,7 +270,7 @@ class WorkflowRunContext:
|
|||
isinstance(x, ContextParameter),
|
||||
# This makes sure that ContextParameters witha ContextParameter source are processed after all other
|
||||
# ContextParameters
|
||||
isinstance(x.source, ContextParameter) if isinstance(x, ContextParameter) else False,
|
||||
(isinstance(x.source, ContextParameter) if isinstance(x, ContextParameter) else False),
|
||||
isinstance(x, BitwardenLoginCredentialParameter),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -81,16 +81,20 @@ class Block(BaseModel, abc.ABC):
|
|||
value=value,
|
||||
)
|
||||
LOG.info(
|
||||
f"Registered output parameter value",
|
||||
"Registered output parameter value",
|
||||
output_parameter_id=self.output_parameter.output_parameter_id,
|
||||
workflow_run_id=workflow_run_id,
|
||||
)
|
||||
|
||||
def build_block_result(
|
||||
self, success: bool, output_parameter_value: dict[str, Any] | list | str | None = None
|
||||
self,
|
||||
success: bool,
|
||||
output_parameter_value: dict[str, Any] | list | str | None = None,
|
||||
) -> BlockResult:
|
||||
return BlockResult(
|
||||
success=success, output_parameter=self.output_parameter, output_parameter_value=output_parameter_value
|
||||
success=success,
|
||||
output_parameter=self.output_parameter,
|
||||
output_parameter_value=output_parameter_value,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -236,11 +240,14 @@ class TaskBlock(Block):
|
|||
workflow_run=workflow_run, url=self.url
|
||||
)
|
||||
if not browser_state.page:
|
||||
LOG.error("BrowserState has no page", workflow_run_id=workflow_run.workflow_run_id)
|
||||
LOG.error(
|
||||
"BrowserState has no page",
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
)
|
||||
raise MissingBrowserStatePage(workflow_run_id=workflow_run.workflow_run_id)
|
||||
|
||||
LOG.info(
|
||||
f"Navigating to page",
|
||||
"Navigating to page",
|
||||
url=self.url,
|
||||
workflow_run_id=workflow_run_id,
|
||||
task_id=task.task_id,
|
||||
|
@ -253,7 +260,12 @@ class TaskBlock(Block):
|
|||
await browser_state.page.goto(self.url, timeout=settings.BROWSER_LOADING_TIMEOUT_MS)
|
||||
|
||||
try:
|
||||
await app.agent.execute_step(organization=organization, task=task, step=step, workflow_run=workflow_run)
|
||||
await app.agent.execute_step(
|
||||
organization=organization,
|
||||
task=task,
|
||||
step=step,
|
||||
workflow_run=workflow_run,
|
||||
)
|
||||
except Exception as e:
|
||||
# Make sure the task is marked as failed in the database before raising the exception
|
||||
await app.DATABASE.update_task(
|
||||
|
@ -273,7 +285,7 @@ class TaskBlock(Block):
|
|||
|
||||
if updated_task.status == TaskStatus.completed or updated_task.status == TaskStatus.terminated:
|
||||
LOG.info(
|
||||
f"Task completed",
|
||||
"Task completed",
|
||||
task_id=updated_task.task_id,
|
||||
task_status=updated_task.status,
|
||||
workflow_run_id=workflow_run_id,
|
||||
|
@ -400,7 +412,7 @@ class ForLoopBlock(Block):
|
|||
)
|
||||
if not loop_over_values or len(loop_over_values) == 0:
|
||||
LOG.info(
|
||||
f"No loop_over values found",
|
||||
"No loop_over values found",
|
||||
block_type=self.block_type,
|
||||
workflow_run_id=workflow_run_id,
|
||||
num_loop_over_values=len(loop_over_values),
|
||||
|
@ -519,7 +531,11 @@ class TextPromptBlock(Block):
|
|||
+ json.dumps(self.json_schema, indent=2)
|
||||
+ "\n```\n\n"
|
||||
)
|
||||
LOG.info("TextPromptBlock: Sending prompt to LLM", prompt=prompt, llm_key=self.llm_key)
|
||||
LOG.info(
|
||||
"TextPromptBlock: Sending prompt to LLM",
|
||||
prompt=prompt,
|
||||
llm_key=self.llm_key,
|
||||
)
|
||||
response = await llm_api_handler(prompt=prompt)
|
||||
LOG.info("TextPromptBlock: Received response from LLM", response=response)
|
||||
return response
|
||||
|
@ -692,7 +708,12 @@ class SendEmailBlock(Block):
|
|||
workflow_run_id: str,
|
||||
) -> list[PARAMETER_TYPE]:
|
||||
workflow_run_context = self.get_workflow_run_context(workflow_run_id)
|
||||
parameters = [self.smtp_host, self.smtp_port, self.smtp_username, self.smtp_password]
|
||||
parameters = [
|
||||
self.smtp_host,
|
||||
self.smtp_port,
|
||||
self.smtp_username,
|
||||
self.smtp_password,
|
||||
]
|
||||
|
||||
if self.file_attachments:
|
||||
for file_path in self.file_attachments:
|
||||
|
@ -732,7 +753,12 @@ class SendEmailBlock(Block):
|
|||
if email_config_problems:
|
||||
raise InvalidEmailClientConfiguration(email_config_problems)
|
||||
|
||||
return smtp_host_value, smtp_port_value, smtp_username_value, smtp_password_value
|
||||
return (
|
||||
smtp_host_value,
|
||||
smtp_port_value,
|
||||
smtp_username_value,
|
||||
smtp_password_value,
|
||||
)
|
||||
|
||||
def _get_file_paths(self, workflow_run_context: WorkflowRunContext, workflow_run_id: str) -> list[str]:
|
||||
file_paths = []
|
||||
|
@ -846,7 +872,12 @@ class SendEmailBlock(Block):
|
|||
subtype=subtype,
|
||||
)
|
||||
with open(path, "rb") as fp:
|
||||
msg.add_attachment(fp.read(), maintype=maintype, subtype=subtype, filename=attachment_filename)
|
||||
msg.add_attachment(
|
||||
fp.read(),
|
||||
maintype=maintype,
|
||||
subtype=subtype,
|
||||
filename=attachment_filename,
|
||||
)
|
||||
finally:
|
||||
if path:
|
||||
os.unlink(path)
|
||||
|
@ -884,6 +915,12 @@ class SendEmailBlock(Block):
|
|||
|
||||
|
||||
BlockSubclasses = Union[
|
||||
ForLoopBlock, TaskBlock, CodeBlock, TextPromptBlock, DownloadToS3Block, UploadToS3Block, SendEmailBlock
|
||||
ForLoopBlock,
|
||||
TaskBlock,
|
||||
CodeBlock,
|
||||
TextPromptBlock,
|
||||
DownloadToS3Block,
|
||||
UploadToS3Block,
|
||||
SendEmailBlock,
|
||||
]
|
||||
BlockTypeVar = Annotated[BlockSubclasses, Field(discriminator="block_type")]
|
||||
|
|
|
@ -114,6 +114,10 @@ class OutputParameter(Parameter):
|
|||
|
||||
|
||||
ParameterSubclasses = Union[
|
||||
WorkflowParameter, ContextParameter, AWSSecretParameter, BitwardenLoginCredentialParameter, OutputParameter
|
||||
WorkflowParameter,
|
||||
ContextParameter,
|
||||
AWSSecretParameter,
|
||||
BitwardenLoginCredentialParameter,
|
||||
OutputParameter,
|
||||
]
|
||||
PARAMETER_TYPE = Annotated[ParameterSubclasses, Field(discriminator="parameter_type")]
|
||||
|
|
|
@ -166,7 +166,10 @@ class WorkflowService:
|
|||
wp_wps_tuples = await self.get_workflow_run_parameter_tuples(workflow_run_id=workflow_run.workflow_run_id)
|
||||
workflow_output_parameters = await self.get_workflow_output_parameters(workflow_id=workflow.workflow_id)
|
||||
app.WORKFLOW_CONTEXT_MANAGER.initialize_workflow_run_context(
|
||||
workflow_run_id, wp_wps_tuples, workflow_output_parameters, context_parameters
|
||||
workflow_run_id,
|
||||
wp_wps_tuples,
|
||||
workflow_output_parameters,
|
||||
context_parameters,
|
||||
)
|
||||
# Execute workflow blocks
|
||||
blocks = workflow.workflow_definition.blocks
|
||||
|
@ -203,7 +206,11 @@ class WorkflowService:
|
|||
)
|
||||
else:
|
||||
await self.mark_workflow_run_as_failed(workflow_run_id=workflow_run.workflow_run_id)
|
||||
await self.send_workflow_response(workflow=workflow, workflow_run=workflow_run, api_key=api_key)
|
||||
await self.send_workflow_response(
|
||||
workflow=workflow,
|
||||
workflow_run=workflow_run,
|
||||
api_key=api_key,
|
||||
)
|
||||
return workflow_run
|
||||
|
||||
except Exception:
|
||||
|
@ -224,13 +231,21 @@ class WorkflowService:
|
|||
|
||||
# Create a mapping of status to (action, log_func, log_message)
|
||||
status_action_mapping = {
|
||||
TaskStatus.running: (None, LOG.error, "has running tasks, this should not happen"),
|
||||
TaskStatus.running: (
|
||||
None,
|
||||
LOG.error,
|
||||
"has running tasks, this should not happen",
|
||||
),
|
||||
TaskStatus.terminated: (
|
||||
self.mark_workflow_run_as_terminated,
|
||||
LOG.warning,
|
||||
"has terminated tasks, marking as terminated",
|
||||
),
|
||||
TaskStatus.failed: (self.mark_workflow_run_as_failed, LOG.warning, "has failed tasks, marking as failed"),
|
||||
TaskStatus.failed: (
|
||||
self.mark_workflow_run_as_failed,
|
||||
LOG.warning,
|
||||
"has failed tasks, marking as failed",
|
||||
),
|
||||
TaskStatus.completed: (
|
||||
self.mark_workflow_run_as_completed,
|
||||
LOG.info,
|
||||
|
@ -333,7 +348,7 @@ class WorkflowService:
|
|||
title=title,
|
||||
organization_id=organization_id,
|
||||
description=description,
|
||||
workflow_definition=workflow_definition.model_dump() if workflow_definition else None,
|
||||
workflow_definition=(workflow_definition.model_dump() if workflow_definition else None),
|
||||
)
|
||||
|
||||
async def delete_workflow_by_permanent_id(
|
||||
|
@ -529,7 +544,10 @@ class WorkflowService:
|
|||
for task in workflow_run_tasks[::-1]:
|
||||
screenshot_artifact = await app.DATABASE.get_latest_artifact(
|
||||
task_id=task.task_id,
|
||||
artifact_types=[ArtifactType.SCREENSHOT_ACTION, ArtifactType.SCREENSHOT_FINAL],
|
||||
artifact_types=[
|
||||
ArtifactType.SCREENSHOT_ACTION,
|
||||
ArtifactType.SCREENSHOT_FINAL,
|
||||
],
|
||||
organization_id=organization_id,
|
||||
)
|
||||
if screenshot_artifact:
|
||||
|
@ -541,17 +559,19 @@ class WorkflowService:
|
|||
|
||||
recording_url = None
|
||||
recording_artifact = await app.DATABASE.get_artifact_for_workflow_run(
|
||||
workflow_run_id=workflow_run_id, artifact_type=ArtifactType.RECORDING, organization_id=organization_id
|
||||
workflow_run_id=workflow_run_id,
|
||||
artifact_type=ArtifactType.RECORDING,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
if recording_artifact:
|
||||
recording_url = await app.ARTIFACT_MANAGER.get_share_link(recording_artifact)
|
||||
|
||||
workflow_parameter_tuples = await app.DATABASE.get_workflow_run_parameters(workflow_run_id=workflow_run_id)
|
||||
parameters_with_value = {wfp.key: wfrp.value for wfp, wfrp in workflow_parameter_tuples}
|
||||
output_parameter_tuples: list[tuple[OutputParameter, WorkflowRunOutputParameter]] = (
|
||||
await self.get_output_parameter_workflow_run_output_parameter_tuples(
|
||||
workflow_id=workflow_id, workflow_run_id=workflow_run_id
|
||||
)
|
||||
output_parameter_tuples: list[
|
||||
tuple[OutputParameter, WorkflowRunOutputParameter]
|
||||
] = await self.get_output_parameter_workflow_run_output_parameter_tuples(
|
||||
workflow_id=workflow_id, workflow_run_id=workflow_run_id
|
||||
)
|
||||
if output_parameter_tuples:
|
||||
outputs = {output_parameter.key: output.value for output_parameter, output in output_parameter_tuples}
|
||||
|
@ -587,7 +607,9 @@ class WorkflowService:
|
|||
tasks = await self.get_tasks_by_workflow_run_id(workflow_run.workflow_run_id)
|
||||
all_workflow_task_ids = [task.task_id for task in tasks]
|
||||
browser_state = await app.BROWSER_MANAGER.cleanup_for_workflow_run(
|
||||
workflow_run.workflow_run_id, all_workflow_task_ids, close_browser_on_completion
|
||||
workflow_run.workflow_run_id,
|
||||
all_workflow_task_ids,
|
||||
close_browser_on_completion,
|
||||
)
|
||||
if browser_state:
|
||||
await self.persist_video_data(browser_state, workflow, workflow_run)
|
||||
|
@ -600,7 +622,10 @@ class WorkflowService:
|
|||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
organization_id=workflow.organization_id,
|
||||
)
|
||||
LOG.info("Built workflow run status response", workflow_run_status_response=workflow_run_status_response)
|
||||
LOG.info(
|
||||
"Built workflow run status response",
|
||||
workflow_run_status_response=workflow_run_status_response,
|
||||
)
|
||||
|
||||
if not workflow_run.webhook_callback_url:
|
||||
LOG.warning(
|
||||
|
@ -661,7 +686,8 @@ class WorkflowService:
|
|||
)
|
||||
except Exception as e:
|
||||
raise FailedToSendWebhook(
|
||||
workflow_id=workflow.workflow_id, workflow_run_id=workflow_run.workflow_run_id
|
||||
workflow_id=workflow.workflow_id,
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
) from e
|
||||
|
||||
async def persist_video_data(
|
||||
|
@ -681,10 +707,16 @@ class WorkflowService:
|
|||
)
|
||||
|
||||
async def persist_har_data(
|
||||
self, browser_state: BrowserState, last_step: Step, workflow: Workflow, workflow_run: WorkflowRun
|
||||
self,
|
||||
browser_state: BrowserState,
|
||||
last_step: Step,
|
||||
workflow: Workflow,
|
||||
workflow_run: WorkflowRun,
|
||||
) -> None:
|
||||
har_data = await app.BROWSER_MANAGER.get_har_data(
|
||||
workflow_id=workflow.workflow_id, workflow_run_id=workflow_run.workflow_run_id, browser_state=browser_state
|
||||
workflow_id=workflow.workflow_id,
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
browser_state=browser_state,
|
||||
)
|
||||
if har_data:
|
||||
await app.ARTIFACT_MANAGER.create_artifact(
|
||||
|
@ -703,7 +735,11 @@ class WorkflowService:
|
|||
await app.ARTIFACT_MANAGER.create_artifact(step=last_step, artifact_type=ArtifactType.TRACE, path=trace_path)
|
||||
|
||||
async def persist_debug_artifacts(
|
||||
self, browser_state: BrowserState, last_task: Task, workflow: Workflow, workflow_run: WorkflowRun
|
||||
self,
|
||||
browser_state: BrowserState,
|
||||
last_task: Task,
|
||||
workflow: Workflow,
|
||||
workflow_run: WorkflowRun,
|
||||
) -> None:
|
||||
last_step = await app.DATABASE.get_latest_step(
|
||||
task_id=last_task.task_id, organization_id=last_task.organization_id
|
||||
|
@ -720,7 +756,11 @@ class WorkflowService:
|
|||
request: WorkflowCreateYAMLRequest,
|
||||
workflow_permanent_id: str | None = None,
|
||||
) -> Workflow:
|
||||
LOG.info("Creating workflow from request", organization_id=organization_id, title=request.title)
|
||||
LOG.info(
|
||||
"Creating workflow from request",
|
||||
organization_id=organization_id,
|
||||
title=request.title,
|
||||
)
|
||||
try:
|
||||
if workflow_permanent_id:
|
||||
existing_latest_workflow = await self.get_workflow_by_permanent_id(
|
||||
|
@ -769,7 +809,8 @@ class WorkflowService:
|
|||
|
||||
# Create output parameters for all blocks
|
||||
block_output_parameters = await WorkflowService._create_all_output_parameters_for_workflow(
|
||||
workflow_id=workflow.workflow_id, block_yamls=request.workflow_definition.blocks
|
||||
workflow_id=workflow.workflow_id,
|
||||
block_yamls=request.workflow_definition.blocks,
|
||||
)
|
||||
for block_output_parameter in block_output_parameters.values():
|
||||
parameters[block_output_parameter.key] = block_output_parameter
|
||||
|
@ -822,7 +863,8 @@ class WorkflowService:
|
|||
for context_parameter in context_parameter_yamls:
|
||||
if context_parameter.source_parameter_key not in parameters:
|
||||
raise ContextParameterSourceNotDefined(
|
||||
context_parameter_key=context_parameter.key, source_key=context_parameter.source_parameter_key
|
||||
context_parameter_key=context_parameter.key,
|
||||
source_key=context_parameter.source_parameter_key,
|
||||
)
|
||||
|
||||
if context_parameter.key in parameters:
|
||||
|
@ -901,7 +943,9 @@ class WorkflowService:
|
|||
|
||||
@staticmethod
|
||||
async def block_yaml_to_block(
|
||||
workflow: Workflow, block_yaml: BLOCK_YAML_TYPES, parameters: dict[str, Parameter]
|
||||
workflow: Workflow,
|
||||
block_yaml: BLOCK_YAML_TYPES,
|
||||
parameters: dict[str, Parameter],
|
||||
) -> BlockTypeVar:
|
||||
output_parameter = parameters[f"{block_yaml.label}_output"]
|
||||
if block_yaml.block_type == BlockType.TASK:
|
||||
|
|
|
@ -157,7 +157,12 @@ def parse_actions(task: Task, json_response: List[Dict[str, Any]]) -> List[Actio
|
|||
reasoning=reasoning,
|
||||
actions=actions,
|
||||
)
|
||||
actions.append(TerminateAction(reasoning=reasoning, errors=action["errors"] if "errors" in action else []))
|
||||
actions.append(
|
||||
TerminateAction(
|
||||
reasoning=reasoning,
|
||||
errors=action["errors"] if "errors" in action else [],
|
||||
)
|
||||
)
|
||||
elif action_type == ActionType.CLICK:
|
||||
file_url = action["file_url"] if "file_url" in action else None
|
||||
actions.append(
|
||||
|
@ -173,11 +178,21 @@ def parse_actions(task: Task, json_response: List[Dict[str, Any]]) -> List[Actio
|
|||
elif action_type == ActionType.UPLOAD_FILE:
|
||||
# TODO: see if the element is a file input element. if it's not, convert this action into a click action
|
||||
|
||||
actions.append(UploadFileAction(element_id=element_id, file_url=action["file_url"], reasoning=reasoning))
|
||||
actions.append(
|
||||
UploadFileAction(
|
||||
element_id=element_id,
|
||||
file_url=action["file_url"],
|
||||
reasoning=reasoning,
|
||||
)
|
||||
)
|
||||
# This action is not used in the current implementation. Click actions are used instead.
|
||||
elif action_type == ActionType.DOWNLOAD_FILE:
|
||||
actions.append(
|
||||
DownloadFileAction(element_id=element_id, file_name=action["file_name"], reasoning=reasoning)
|
||||
DownloadFileAction(
|
||||
element_id=element_id,
|
||||
file_name=action["file_name"],
|
||||
reasoning=reasoning,
|
||||
)
|
||||
)
|
||||
elif action_type == ActionType.SELECT_OPTION:
|
||||
actions.append(
|
||||
|
@ -192,7 +207,13 @@ def parse_actions(task: Task, json_response: List[Dict[str, Any]]) -> List[Actio
|
|||
)
|
||||
)
|
||||
elif action_type == ActionType.CHECKBOX:
|
||||
actions.append(CheckboxAction(element_id=element_id, is_checked=action["is_checked"], reasoning=reasoning))
|
||||
actions.append(
|
||||
CheckboxAction(
|
||||
element_id=element_id,
|
||||
is_checked=action["is_checked"],
|
||||
reasoning=reasoning,
|
||||
)
|
||||
)
|
||||
elif action_type == ActionType.WAIT:
|
||||
actions.append(WaitAction(reasoning=reasoning))
|
||||
elif action_type == ActionType.COMPLETE:
|
||||
|
|
|
@ -38,15 +38,18 @@ LOG = structlog.get_logger()
|
|||
|
||||
class ActionHandler:
|
||||
_handled_action_types: dict[
|
||||
ActionType, Callable[[Action, Page, ScrapedPage, Task, Step], Awaitable[list[ActionResult]]]
|
||||
ActionType,
|
||||
Callable[[Action, Page, ScrapedPage, Task, Step], Awaitable[list[ActionResult]]],
|
||||
] = {}
|
||||
|
||||
_setup_action_types: dict[
|
||||
ActionType, Callable[[Action, Page, ScrapedPage, Task, Step], Awaitable[list[ActionResult]]]
|
||||
ActionType,
|
||||
Callable[[Action, Page, ScrapedPage, Task, Step], Awaitable[list[ActionResult]]],
|
||||
] = {}
|
||||
|
||||
_teardown_action_types: dict[
|
||||
ActionType, Callable[[Action, Page, ScrapedPage, Task, Step], Awaitable[list[ActionResult]]]
|
||||
ActionType,
|
||||
Callable[[Action, Page, ScrapedPage, Task, Step], Awaitable[list[ActionResult]]],
|
||||
] = {}
|
||||
|
||||
@classmethod
|
||||
|
@ -111,10 +114,19 @@ class ActionHandler:
|
|||
return actions_result
|
||||
|
||||
else:
|
||||
LOG.error("Unsupported action type in handler", action=action, type=type(action))
|
||||
LOG.error(
|
||||
"Unsupported action type in handler",
|
||||
action=action,
|
||||
type=type(action),
|
||||
)
|
||||
return [ActionFailure(Exception(f"Unsupported action type: {type(action)}"))]
|
||||
except MissingElement as e:
|
||||
LOG.info("Known exceptions", action=action, exception_type=type(e), exception_message=str(e))
|
||||
LOG.info(
|
||||
"Known exceptions",
|
||||
action=action,
|
||||
exception_type=type(e),
|
||||
exception_message=str(e),
|
||||
)
|
||||
return [ActionFailure(e)]
|
||||
except MultipleElementsFound as e:
|
||||
LOG.exception(
|
||||
|
@ -128,7 +140,11 @@ class ActionHandler:
|
|||
|
||||
|
||||
async def handle_solve_captcha_action(
|
||||
action: actions.SolveCaptchaAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.SolveCaptchaAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
LOG.warning(
|
||||
"Please solve the captcha on the page, you have 30 seconds",
|
||||
|
@ -139,14 +155,22 @@ async def handle_solve_captcha_action(
|
|||
|
||||
|
||||
async def handle_click_action(
|
||||
action: actions.ClickAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.ClickAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
xpath = await validate_actions_in_dom(action, page, scraped_page)
|
||||
await asyncio.sleep(0.3)
|
||||
if action.download:
|
||||
return await handle_click_to_download_file_action(action, page, scraped_page)
|
||||
return await chain_click(
|
||||
task, page, action, xpath, timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
|
||||
task,
|
||||
page,
|
||||
action,
|
||||
xpath,
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
|
||||
|
@ -158,7 +182,9 @@ async def handle_click_to_download_file_action(
|
|||
xpath = await validate_actions_in_dom(action, page, scraped_page)
|
||||
try:
|
||||
await page.click(
|
||||
f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, modifiers=["Alt"]
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
modifiers=["Alt"],
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.exception("ClickAction with download failed", action=action, exc_info=True)
|
||||
|
@ -168,7 +194,11 @@ async def handle_click_to_download_file_action(
|
|||
|
||||
|
||||
async def handle_input_text_action(
|
||||
action: actions.InputTextAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.InputTextAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
xpath = await validate_actions_in_dom(action, page, scraped_page)
|
||||
locator = page.locator(f"xpath={xpath}")
|
||||
|
@ -184,7 +214,11 @@ async def handle_input_text_action(
|
|||
|
||||
|
||||
async def handle_upload_file_action(
|
||||
action: actions.UploadFileAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.UploadFileAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
if not action.file_url:
|
||||
LOG.warning("InputFileAction has no file_url", action=action)
|
||||
|
@ -209,7 +243,8 @@ async def handle_upload_file_action(
|
|||
LOG.info("Taking UploadFileAction. Found file input tag", action=action)
|
||||
if file_path:
|
||||
await page.locator(f"xpath={xpath}").set_input_files(
|
||||
file_path, timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
|
||||
file_path,
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
# Sleep for 10 seconds after uploading a file to let the page process it
|
||||
|
@ -222,13 +257,21 @@ async def handle_upload_file_action(
|
|||
# treat it as a click action
|
||||
action.is_upload_file_tag = False
|
||||
return await chain_click(
|
||||
task, page, action, xpath, timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
|
||||
task,
|
||||
page,
|
||||
action,
|
||||
xpath,
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
|
||||
@deprecated("This function is deprecated. Downloads are handled by the click action handler now.")
|
||||
async def handle_download_file_action(
|
||||
action: actions.DownloadFileAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.DownloadFileAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
xpath = await validate_actions_in_dom(action, page, scraped_page)
|
||||
file_name = f"{action.file_name or uuid.uuid4()}"
|
||||
|
@ -238,7 +281,9 @@ async def handle_download_file_action(
|
|||
async with page.expect_download() as download_info:
|
||||
await asyncio.sleep(0.3)
|
||||
await page.click(
|
||||
f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS, modifiers=["Alt"]
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
modifiers=["Alt"],
|
||||
)
|
||||
|
||||
download = await download_info.value
|
||||
|
@ -260,20 +305,33 @@ async def handle_download_file_action(
|
|||
|
||||
|
||||
async def handle_null_action(
|
||||
action: actions.NullAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.NullAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
return [ActionSuccess()]
|
||||
|
||||
|
||||
async def handle_select_option_action(
|
||||
action: actions.SelectOptionAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.SelectOptionAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
xpath = await validate_actions_in_dom(action, page, scraped_page)
|
||||
|
||||
locator = page.locator(f"xpath={xpath}")
|
||||
tag_name = await get_tag_name_lowercase(locator)
|
||||
element_dict = scraped_page.id_to_element_dict[action.element_id]
|
||||
LOG.info("SelectOptionAction", action=action, tag_name=tag_name, element_dict=element_dict)
|
||||
LOG.info(
|
||||
"SelectOptionAction",
|
||||
action=action,
|
||||
tag_name=tag_name,
|
||||
element_dict=element_dict,
|
||||
)
|
||||
|
||||
# if element is not a select option, prioritize clicking the linked element if any
|
||||
if tag_name != "select" and "linked_element" in element_dict:
|
||||
|
@ -290,7 +348,11 @@ async def handle_select_option_action(
|
|||
linked_element=element_dict["linked_element"],
|
||||
)
|
||||
return [ActionSuccess()]
|
||||
LOG.warning("Failed to click linked element", action=action, linked_element=element_dict["linked_element"])
|
||||
LOG.warning(
|
||||
"Failed to click linked element",
|
||||
action=action,
|
||||
linked_element=element_dict["linked_element"],
|
||||
)
|
||||
|
||||
# check if the element is an a tag first. If yes, click it instead of selecting the option
|
||||
if tag_name == "label":
|
||||
|
@ -360,7 +422,7 @@ async def handle_select_option_action(
|
|||
except Exception as e:
|
||||
LOG.error("Failed to click option", action=action, exc_info=True)
|
||||
return [ActionFailure(e)]
|
||||
return [ActionFailure(Exception(f"SelectOption option index is missing"))]
|
||||
return [ActionFailure(Exception("SelectOption option index is missing"))]
|
||||
elif role_attribute == "option":
|
||||
LOG.info(
|
||||
"SelectOptionAction on an option element. Clicking the option",
|
||||
|
@ -373,7 +435,7 @@ async def handle_select_option_action(
|
|||
LOG.error(
|
||||
"SelectOptionAction on a non-listbox element. Cannot handle this action",
|
||||
)
|
||||
return [ActionFailure(Exception(f"Cannot handle SelectOptionAction on a non-listbox element"))]
|
||||
return [ActionFailure(Exception("Cannot handle SelectOptionAction on a non-listbox element"))]
|
||||
elif tag_name == "input" and element_dict.get("attributes", {}).get("type", None) in ["radio", "checkbox"]:
|
||||
LOG.info(
|
||||
"SelectOptionAction is on <input> checkbox/radio",
|
||||
|
@ -387,13 +449,19 @@ async def handle_select_option_action(
|
|||
return [ActionSuccess()]
|
||||
try:
|
||||
# First click by label (if it matches)
|
||||
await page.click(f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS)
|
||||
await page.click(
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
await page.select_option(
|
||||
xpath,
|
||||
label=action.option.label,
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
await page.click(f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS)
|
||||
await page.click(
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
return [ActionSuccess()]
|
||||
except Exception as e:
|
||||
if action.option.index is not None:
|
||||
|
@ -418,23 +486,35 @@ async def handle_select_option_action(
|
|||
if match:
|
||||
# This means we were trying to select an option xpath, click the option
|
||||
option_index = int(match.group(1))
|
||||
await page.click(f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS)
|
||||
await page.click(
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
await page.select_option(
|
||||
xpath,
|
||||
index=option_index,
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
await page.click(f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS)
|
||||
await page.click(
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
return [ActionSuccess()]
|
||||
else:
|
||||
# This means the supplied index was for the select element, not a reference to the xpath dict
|
||||
await page.click(f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS)
|
||||
await page.click(
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
await page.select_option(
|
||||
xpath,
|
||||
index=action.option.index,
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
await page.click(f"xpath={xpath}", timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS)
|
||||
await page.click(
|
||||
f"xpath={xpath}",
|
||||
timeout=SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||
)
|
||||
return [ActionSuccess()]
|
||||
except Exception as e:
|
||||
LOG.warning("Failed to click on the option by index", action=action, exc_info=True)
|
||||
|
@ -442,7 +522,11 @@ async def handle_select_option_action(
|
|||
|
||||
|
||||
async def handle_checkbox_action(
|
||||
self: actions.CheckboxAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
self: actions.CheckboxAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
"""
|
||||
******* NOT REGISTERED *******
|
||||
|
@ -462,20 +546,32 @@ async def handle_checkbox_action(
|
|||
|
||||
|
||||
async def handle_wait_action(
|
||||
action: actions.WaitAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.WaitAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
await asyncio.sleep(10)
|
||||
return [ActionFailure(exception=Exception("Wait action is treated as a failure"))]
|
||||
|
||||
|
||||
async def handle_terminate_action(
|
||||
action: actions.TerminateAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.TerminateAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
return [ActionSuccess()]
|
||||
|
||||
|
||||
async def handle_complete_action(
|
||||
action: actions.CompleteAction, page: Page, scraped_page: ScrapedPage, task: Task, step: Step
|
||||
action: actions.CompleteAction,
|
||||
page: Page,
|
||||
scraped_page: ScrapedPage,
|
||||
task: Task,
|
||||
step: Step,
|
||||
) -> list[ActionResult]:
|
||||
extracted_data = None
|
||||
if action.data_extraction_goal:
|
||||
|
@ -526,7 +622,11 @@ async def validate_actions_in_dom(action: WebAction, page: Page, scraped_page: S
|
|||
|
||||
num_elements = await locator.count()
|
||||
if num_elements < 1:
|
||||
LOG.warning("No elements found with action xpath. Validation failed.", action=action, xpath=xpath)
|
||||
LOG.warning(
|
||||
"No elements found with action xpath. Validation failed.",
|
||||
action=action,
|
||||
xpath=xpath,
|
||||
)
|
||||
raise MissingElement(xpath=xpath, element_id=action.element_id)
|
||||
elif num_elements > 1:
|
||||
LOG.warning(
|
||||
|
@ -560,10 +660,14 @@ async def chain_click(
|
|||
try:
|
||||
file = await download_file(file_url)
|
||||
except Exception:
|
||||
LOG.exception("Failed to download file, continuing without it", action=action, file_url=file_url)
|
||||
LOG.exception(
|
||||
"Failed to download file, continuing without it",
|
||||
action=action,
|
||||
file_url=file_url,
|
||||
)
|
||||
file = []
|
||||
|
||||
fc_func = lambda fc: fc.set_files(files=file)
|
||||
fc_func = lambda fc: fc.set_files(files=file) # noqa: E731
|
||||
page.on("filechooser", fc_func)
|
||||
LOG.info("Registered file chooser listener", action=action, path=file)
|
||||
|
||||
|
@ -585,13 +689,26 @@ async def chain_click(
|
|||
try:
|
||||
await page.click(f"xpath={xpath}", timeout=timeout)
|
||||
LOG.info("Chain click: main element click succeeded", action=action, xpath=xpath)
|
||||
return [ActionSuccess(javascript_triggered=javascript_triggered, download_triggered=download_triggered)]
|
||||
return [
|
||||
ActionSuccess(
|
||||
javascript_triggered=javascript_triggered,
|
||||
download_triggered=download_triggered,
|
||||
)
|
||||
]
|
||||
except Exception as e:
|
||||
action_results: list[ActionResult] = [
|
||||
ActionFailure(e, javascript_triggered=javascript_triggered, download_triggered=download_triggered)
|
||||
ActionFailure(
|
||||
e,
|
||||
javascript_triggered=javascript_triggered,
|
||||
download_triggered=download_triggered,
|
||||
)
|
||||
]
|
||||
if await is_input_element(page.locator(xpath)):
|
||||
LOG.info("Chain click: it's an input element. going to try sibling click", action=action, xpath=xpath)
|
||||
LOG.info(
|
||||
"Chain click: it's an input element. going to try sibling click",
|
||||
action=action,
|
||||
xpath=xpath,
|
||||
)
|
||||
sibling_action_result = await click_sibling_of_input(page.locator(xpath), timeout=timeout)
|
||||
sibling_action_result.download_triggered = download_triggered
|
||||
action_results.append(sibling_action_result)
|
||||
|
@ -604,7 +721,11 @@ async def chain_click(
|
|||
javascript_triggered = javascript_triggered or parent_javascript_triggered
|
||||
parent_locator = page.locator(xpath).locator("..")
|
||||
await parent_locator.click(timeout=timeout)
|
||||
LOG.info("Chain click: successfully clicked parent element", action=action, parent_xpath=parent_xpath)
|
||||
LOG.info(
|
||||
"Chain click: successfully clicked parent element",
|
||||
action=action,
|
||||
parent_xpath=parent_xpath,
|
||||
)
|
||||
action_results.append(
|
||||
ActionSuccess(
|
||||
javascript_triggered=javascript_triggered,
|
||||
|
@ -613,9 +734,18 @@ async def chain_click(
|
|||
)
|
||||
)
|
||||
except Exception as pe:
|
||||
LOG.warning("Failed to click parent element", action=action, parent_xpath=parent_xpath, exc_info=True)
|
||||
LOG.warning(
|
||||
"Failed to click parent element",
|
||||
action=action,
|
||||
parent_xpath=parent_xpath,
|
||||
exc_info=True,
|
||||
)
|
||||
action_results.append(
|
||||
ActionFailure(pe, javascript_triggered=javascript_triggered, interacted_with_parent=True)
|
||||
ActionFailure(
|
||||
pe,
|
||||
javascript_triggered=javascript_triggered,
|
||||
interacted_with_parent=True,
|
||||
)
|
||||
)
|
||||
# We don't raise exception here because we do log the exception, and return ActionFailure as the last action
|
||||
|
||||
|
@ -765,7 +895,7 @@ async def extract_information_for_navigation_goal(
|
|||
extracted_information_schema=task.extracted_information_schema,
|
||||
current_url=scraped_page.url,
|
||||
extracted_text=scraped_page.extracted_text,
|
||||
error_code_mapping_str=json.dumps(task.error_code_mapping) if task.error_code_mapping else None,
|
||||
error_code_mapping_str=(json.dumps(task.error_code_mapping) if task.error_code_mapping else None),
|
||||
)
|
||||
|
||||
json_response = await app.LLM_API_HANDLER(
|
||||
|
@ -804,7 +934,12 @@ async def click_listbox_option(
|
|||
await page.click(f"xpath={option_xpath}", timeout=1000)
|
||||
return True
|
||||
except Exception:
|
||||
LOG.error("Failed to click on the option", action=action, option_xpath=option_xpath, exc_info=True)
|
||||
LOG.error(
|
||||
"Failed to click on the option",
|
||||
action=action,
|
||||
option_xpath=option_xpath,
|
||||
exc_info=True,
|
||||
)
|
||||
if "children" in child:
|
||||
bfs_queue.extend(child["children"])
|
||||
return False
|
||||
|
|
|
@ -63,6 +63,6 @@ class DetailedAgentStepOutput(BaseModel):
|
|||
def to_agent_step_output(self) -> AgentStepOutput:
|
||||
return AgentStepOutput(
|
||||
action_results=self.action_results if self.action_results else [],
|
||||
actions_and_results=self.actions_and_results if self.actions_and_results else [],
|
||||
actions_and_results=(self.actions_and_results if self.actions_and_results else []),
|
||||
errors=self.extract_errors(),
|
||||
)
|
||||
|
|
|
@ -62,7 +62,10 @@ class BrowserContextFactory:
|
|||
],
|
||||
"record_har_path": har_dir,
|
||||
"record_video_dir": video_dir,
|
||||
"viewport": {"width": settings.BROWSER_WIDTH, "height": settings.BROWSER_HEIGHT},
|
||||
"viewport": {
|
||||
"width": settings.BROWSER_WIDTH,
|
||||
"height": settings.BROWSER_HEIGHT,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -73,7 +76,10 @@ class BrowserContextFactory:
|
|||
traces_dir: str | None = None,
|
||||
) -> BrowserArtifacts:
|
||||
return BrowserArtifacts(
|
||||
video_path=video_path, har_path=har_path, video_artifact_id=video_artifact_id, traces_dir=traces_dir
|
||||
video_path=video_path,
|
||||
har_path=har_path,
|
||||
video_artifact_id=video_artifact_id,
|
||||
traces_dir=traces_dir,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -156,7 +162,10 @@ class BrowserState:
|
|||
LOG.info("playwright is started")
|
||||
if self.browser_context is None:
|
||||
LOG.info("creating browser context")
|
||||
browser_context, browser_artifacts = await BrowserContextFactory.create_browser_context(self.pw, url=url)
|
||||
(
|
||||
browser_context,
|
||||
browser_artifacts,
|
||||
) = await BrowserContextFactory.create_browser_context(self.pw, url=url)
|
||||
self.browser_context = browser_context
|
||||
self.browser_artifacts = browser_artifacts
|
||||
LOG.info("browser context is created")
|
||||
|
@ -179,7 +188,11 @@ class BrowserState:
|
|||
start_time = time.time()
|
||||
await self.page.goto(url, timeout=settings.BROWSER_LOADING_TIMEOUT_MS)
|
||||
end_time = time.time()
|
||||
LOG.info(f"Page loading time", loading_time=end_time - start_time, url=url)
|
||||
LOG.info(
|
||||
"Page loading time",
|
||||
loading_time=end_time - start_time,
|
||||
url=url,
|
||||
)
|
||||
except Error as playright_error:
|
||||
LOG.exception(f"Error while navigating to url: {str(playright_error)}")
|
||||
raise FailedToNavigateToUrl(url=url, error_message=str(playright_error))
|
||||
|
@ -239,7 +252,7 @@ class BrowserState:
|
|||
)
|
||||
end_time = time.time()
|
||||
LOG.info(
|
||||
f"Screenshot taking time",
|
||||
"Screenshot taking time",
|
||||
screenshot_time=end_time - start_time,
|
||||
full_page=full_page,
|
||||
file_path=file_path,
|
||||
|
|
|
@ -27,7 +27,10 @@ class BrowserManager:
|
|||
task_id: str | None = None,
|
||||
) -> BrowserState:
|
||||
pw = await async_playwright().start()
|
||||
browser_context, browser_artifacts = await BrowserContextFactory.create_browser_context(
|
||||
(
|
||||
browser_context,
|
||||
browser_artifacts,
|
||||
) = await BrowserContextFactory.create_browser_context(
|
||||
pw,
|
||||
proxy_location=proxy_location,
|
||||
url=url,
|
||||
|
@ -67,7 +70,10 @@ class BrowserManager:
|
|||
async def get_or_create_for_workflow_run(self, workflow_run: WorkflowRun, url: str | None = None) -> BrowserState:
|
||||
if workflow_run.workflow_run_id in self.pages:
|
||||
return self.pages[workflow_run.workflow_run_id]
|
||||
LOG.info("Creating browser state for workflow run", workflow_run_id=workflow_run.workflow_run_id)
|
||||
LOG.info(
|
||||
"Creating browser state for workflow run",
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
)
|
||||
browser_state = await self._create_browser_state(workflow_run.proxy_location, url=url)
|
||||
|
||||
# The URL here is only used when creating a new page, and not when using an existing page.
|
||||
|
@ -102,7 +108,11 @@ class BrowserManager:
|
|||
raise MissingBrowserState(task_id=task.task_id)
|
||||
|
||||
async def get_video_data(
|
||||
self, browser_state: BrowserState, task_id: str = "", workflow_id: str = "", workflow_run_id: str = ""
|
||||
self,
|
||||
browser_state: BrowserState,
|
||||
task_id: str = "",
|
||||
workflow_id: str = "",
|
||||
workflow_run_id: str = "",
|
||||
) -> bytes:
|
||||
if browser_state:
|
||||
path = browser_state.browser_artifacts.video_path
|
||||
|
@ -113,12 +123,19 @@ class BrowserManager:
|
|||
except FileNotFoundError:
|
||||
pass
|
||||
LOG.warning(
|
||||
"Video data not found for task", task_id=task_id, workflow_id=workflow_id, workflow_run_id=workflow_run_id
|
||||
"Video data not found for task",
|
||||
task_id=task_id,
|
||||
workflow_id=workflow_id,
|
||||
workflow_run_id=workflow_run_id,
|
||||
)
|
||||
return b""
|
||||
|
||||
async def get_har_data(
|
||||
self, browser_state: BrowserState, task_id: str = "", workflow_id: str = "", workflow_run_id: str = ""
|
||||
self,
|
||||
browser_state: BrowserState,
|
||||
task_id: str = "",
|
||||
workflow_id: str = "",
|
||||
workflow_run_id: str = "",
|
||||
) -> bytes:
|
||||
if browser_state:
|
||||
path = browser_state.browser_artifacts.har_path
|
||||
|
@ -126,7 +143,10 @@ class BrowserManager:
|
|||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
LOG.warning(
|
||||
"HAR data not found for task", task_id=task_id, workflow_id=workflow_id, workflow_run_id=workflow_run_id
|
||||
"HAR data not found for task",
|
||||
task_id=task_id,
|
||||
workflow_id=workflow_id,
|
||||
workflow_run_id=workflow_run_id,
|
||||
)
|
||||
return b""
|
||||
|
||||
|
@ -154,7 +174,10 @@ class BrowserManager:
|
|||
return browser_state_to_close
|
||||
|
||||
async def cleanup_for_workflow_run(
|
||||
self, workflow_run_id: str, task_ids: list[str], close_browser_on_completion: bool = True
|
||||
self,
|
||||
workflow_run_id: str,
|
||||
task_ids: list[str],
|
||||
close_browser_on_completion: bool = True,
|
||||
) -> BrowserState | None:
|
||||
LOG.info("Cleaning up for workflow run")
|
||||
browser_state_to_close = self.pages.pop(workflow_run_id, None)
|
||||
|
|
|
@ -241,7 +241,11 @@ async def scrape_web_unsafe(
|
|||
scroll_y_px_old = scroll_y_px
|
||||
LOG.info("Scrolling to next page", url=url, num_screenshots=len(screenshots))
|
||||
scroll_y_px = await scroll_to_next_page(page, drow_boxes=True)
|
||||
LOG.info("Scrolled to next page", scroll_y_px=scroll_y_px, scroll_y_px_old=scroll_y_px_old)
|
||||
LOG.info(
|
||||
"Scrolled to next page",
|
||||
scroll_y_px=scroll_y_px,
|
||||
scroll_y_px_old=scroll_y_px_old,
|
||||
)
|
||||
await remove_bounding_boxes(page)
|
||||
await scroll_to_top(page, drow_boxes=False)
|
||||
|
||||
|
|
|
@ -64,6 +64,18 @@ def streamlit_show_recording(st_obj: Any, uri: str) -> None:
|
|||
content = read_artifact_safe(uri, is_webm=True) # type: ignore
|
||||
if content:
|
||||
random_key = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
||||
st_obj.download_button("Download recording", content, f"recording{uri.split('/')[-1]}.webm", key=random_key)
|
||||
st_obj.download_button(
|
||||
"Download recording",
|
||||
content,
|
||||
f"recording{uri.split('/')[-1]}.webm",
|
||||
key=random_key,
|
||||
)
|
||||
|
||||
streamlit_content_safe(st_obj, st_obj.video, content, "No recording available.", format="video/webm", start_time=0)
|
||||
streamlit_content_safe(
|
||||
st_obj,
|
||||
st_obj.video,
|
||||
content,
|
||||
"No recording available.",
|
||||
format="video/webm",
|
||||
start_time=0,
|
||||
)
|
||||
|
|
|
@ -199,9 +199,18 @@ geico_sample_data = SampleTaskRequest(
|
|||
"additionalProperties": False,
|
||||
"description": "The vehicle that the collision and comprehensive coverage is for",
|
||||
"properties": {
|
||||
"make": {"description": "The make of the vehicle", "type": "string"},
|
||||
"model": {"description": "The model of the vehicle", "type": "string"},
|
||||
"year": {"description": "The year of the vehicle", "type": "string"},
|
||||
"make": {
|
||||
"description": "The make of the vehicle",
|
||||
"type": "string",
|
||||
},
|
||||
"model": {
|
||||
"description": "The model of the vehicle",
|
||||
"type": "string",
|
||||
},
|
||||
"year": {
|
||||
"description": "The year of the vehicle",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
|
@ -225,4 +234,9 @@ geico_sample_data = SampleTaskRequest(
|
|||
)
|
||||
|
||||
|
||||
supported_examples = [geico_sample_data, finditparts_sample_data, california_edd_sample_data, bci_seguros_sample_data]
|
||||
supported_examples = [
|
||||
geico_sample_data,
|
||||
finditparts_sample_data,
|
||||
california_edd_sample_data,
|
||||
bci_seguros_sample_data,
|
||||
]
|
||||
|
|
|
@ -50,16 +50,18 @@ for config in CONFIGS_DICT:
|
|||
st.sidebar.markdown("#### **Settings**")
|
||||
select_env = st.sidebar.selectbox("Environment", list(SETTINGS.keys()), on_change=reset_session_state)
|
||||
select_org = st.sidebar.selectbox(
|
||||
"Organization", list(SETTINGS[select_env]["orgs"].keys()), on_change=reset_session_state
|
||||
"Organization",
|
||||
list(SETTINGS[select_env]["orgs"].keys()),
|
||||
on_change=reset_session_state,
|
||||
)
|
||||
|
||||
# Hack the sidebar size to be a little bit smaller
|
||||
st.markdown(
|
||||
f"""
|
||||
"""
|
||||
<style>
|
||||
.sidebar .sidebar-content {{
|
||||
.sidebar .sidebar-content {
|
||||
width: 375px;
|
||||
}}
|
||||
}
|
||||
</style>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
|
@ -68,7 +70,8 @@ st.markdown(
|
|||
# Initialize session state
|
||||
if "client" not in st.session_state:
|
||||
st.session_state.client = SkyvernClient(
|
||||
base_url=SETTINGS[select_env]["host"], credentials=SETTINGS[select_env]["orgs"][select_org]
|
||||
base_url=SETTINGS[select_env]["host"],
|
||||
credentials=SETTINGS[select_env]["orgs"][select_org],
|
||||
)
|
||||
if "repository" not in st.session_state:
|
||||
st.session_state.repository = TaskRepository(st.session_state.client)
|
||||
|
@ -133,7 +136,8 @@ def copy_curl_to_clipboard(task_request_body: TaskRequest) -> None:
|
|||
with execute_tab:
|
||||
# Streamlit doesn't support "focusing" on a tab, so this is a workaround to make the requested tab be the "first" tab
|
||||
sorted_supported_examples = sorted(
|
||||
supported_examples, key=lambda x: (-1 if x.name.lower() == tab_name.lower() else 0)
|
||||
supported_examples,
|
||||
key=lambda x: (-1 if x.name.lower() == tab_name.lower() else 0),
|
||||
)
|
||||
example_tabs = st.tabs([supported_example.name for supported_example in sorted_supported_examples])
|
||||
|
||||
|
@ -157,7 +161,9 @@ with execute_tab:
|
|||
# Create all the fields to create a TaskRequest object
|
||||
st_url = st.text_input("URL*", value=example.url, key=f"url_{unique_key}")
|
||||
st_webhook_callback_url = st.text_input(
|
||||
"Webhook Callback URL", key=f"webhook_{unique_key}", placeholder="Optional"
|
||||
"Webhook Callback URL",
|
||||
key=f"webhook_{unique_key}",
|
||||
placeholder="Optional",
|
||||
)
|
||||
st_navigation_goal = st.text_area(
|
||||
"Navigation Goal",
|
||||
|
@ -252,11 +258,11 @@ with visualizer_tab:
|
|||
|
||||
col_tasks, _, col_steps, _, col_artifacts = st.columns([4, 1, 6, 1, 18])
|
||||
|
||||
col_tasks.markdown(f"#### Tasks")
|
||||
col_steps.markdown(f"#### Steps")
|
||||
col_tasks.markdown("#### Tasks")
|
||||
col_steps.markdown("#### Steps")
|
||||
col_artifacts.markdown("#### Artifacts")
|
||||
tasks_response = repository.get_tasks(task_page_number)
|
||||
if type(tasks_response) is not list:
|
||||
if not isinstance(tasks_response, list):
|
||||
st.error("Failed to fetch tasks.")
|
||||
st.error(tasks_response)
|
||||
st.error(
|
||||
|
@ -282,7 +288,7 @@ with visualizer_tab:
|
|||
on_click=select_task,
|
||||
args=(task,),
|
||||
use_container_width=True,
|
||||
type="primary" if selected_task and task_id == selected_task["task_id"] else "secondary",
|
||||
type=("primary" if selected_task and task_id == selected_task["task_id"] else "secondary"),
|
||||
)
|
||||
for task_id, task in tasks.items()
|
||||
}
|
||||
|
@ -339,10 +345,16 @@ with visualizer_tab:
|
|||
if task_steps:
|
||||
col_steps_prev, _, col_steps_next = col_steps.columns([3, 1, 3])
|
||||
col_steps_prev.button(
|
||||
"prev", on_click=go_to_previous_step, key="previous_step_button", use_container_width=True
|
||||
"prev",
|
||||
on_click=go_to_previous_step,
|
||||
key="previous_step_button",
|
||||
use_container_width=True,
|
||||
)
|
||||
col_steps_next.button(
|
||||
"next", on_click=go_to_next_step, key="next_step_button", use_container_width=True
|
||||
"next",
|
||||
on_click=go_to_next_step,
|
||||
key="next_step_button",
|
||||
use_container_width=True,
|
||||
)
|
||||
|
||||
step_id_buttons = {
|
||||
|
@ -351,7 +363,7 @@ with visualizer_tab:
|
|||
on_click=select_step,
|
||||
args=(step,),
|
||||
use_container_width=True,
|
||||
type="primary" if selected_step and step["step_id"] == selected_step["step_id"] else "secondary",
|
||||
type=("primary" if selected_step and step["step_id"] == selected_step["step_id"] else "secondary"),
|
||||
)
|
||||
for step in task_steps
|
||||
}
|
||||
|
@ -439,7 +451,10 @@ with visualizer_tab:
|
|||
# tab_llm_prompt.text_area("collapsed", value=content, label_visibility="collapsed", height=1000)
|
||||
elif file_name.endswith("llm_request.json"):
|
||||
streamlit_content_safe(
|
||||
tab_llm_request, tab_llm_request.json, read_artifact_safe(uri), "No LLM request available."
|
||||
tab_llm_request,
|
||||
tab_llm_request.json,
|
||||
read_artifact_safe(uri),
|
||||
"No LLM request available.",
|
||||
)
|
||||
elif file_name.endswith("llm_response_parsed.json"):
|
||||
streamlit_content_safe(
|
||||
|
@ -456,8 +471,18 @@ with visualizer_tab:
|
|||
"No raw LLM response available.",
|
||||
)
|
||||
elif file_name.endswith("html_scrape.html"):
|
||||
streamlit_content_safe(tab_html, tab_html.text, read_artifact_safe(uri), "No html available.")
|
||||
streamlit_content_safe(
|
||||
tab_html,
|
||||
tab_html.text,
|
||||
read_artifact_safe(uri),
|
||||
"No html available.",
|
||||
)
|
||||
elif file_name.endswith("html_action.html"):
|
||||
streamlit_content_safe(tab_html, tab_html.text, read_artifact_safe(uri), "No html available.")
|
||||
streamlit_content_safe(
|
||||
tab_html,
|
||||
tab_html.text,
|
||||
read_artifact_safe(uri),
|
||||
"No html available.",
|
||||
)
|
||||
else:
|
||||
st.write(f"Artifact {file_name} not supported.")
|
||||
|
|
Loading…
Add table
Reference in a new issue