projects -> scripts (#3123)

This commit is contained in:
Shuchang Zheng
2025-08-06 22:23:38 -07:00
committed by GitHub
parent 75eadef0e1
commit 1a4bf1df1a
17 changed files with 660 additions and 538 deletions

View File

@@ -0,0 +1,127 @@
"""rename project/projects -> script/scripts
Revision ID: 0135ee8b36b0
Revises: 2fe3e908a028
Create Date: 2025-08-07 04:49:14.257089+00:00
"""
from typing import Sequence, Union
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "0135ee8b36b0"
down_revision: Union[str, None] = "2fe3e908a028"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"script_files",
sa.Column("file_id", sa.String(), nullable=False),
sa.Column("script_revision_id", sa.String(), nullable=False),
sa.Column("script_id", sa.String(), nullable=False),
sa.Column("organization_id", sa.String(), nullable=False),
sa.Column("file_path", sa.String(), nullable=False),
sa.Column("file_name", sa.String(), nullable=False),
sa.Column("file_type", sa.String(), nullable=False),
sa.Column("content_hash", sa.String(), nullable=True),
sa.Column("file_size", sa.Integer(), nullable=True),
sa.Column("mime_type", sa.String(), nullable=True),
sa.Column("encoding", sa.String(), nullable=True),
sa.Column("artifact_id", sa.String(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("modified_at", sa.DateTime(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("file_id"),
sa.UniqueConstraint("script_revision_id", "file_path", name="unique_script_file_path"),
)
op.create_index("file_script_path_index", "script_files", ["script_revision_id", "file_path"], unique=False)
op.create_table(
"scripts",
sa.Column("script_revision_id", sa.String(), nullable=False),
sa.Column("script_id", sa.String(), nullable=False),
sa.Column("organization_id", sa.String(), nullable=False),
sa.Column("run_id", sa.String(), nullable=True),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("modified_at", sa.DateTime(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("script_revision_id"),
sa.UniqueConstraint("organization_id", "script_id", "version", name="uc_org_script_version"),
)
op.create_index("script_org_created_at_index", "scripts", ["organization_id", "created_at"], unique=False)
op.create_index("script_org_run_id_index", "scripts", ["organization_id", "run_id"], unique=False)
op.drop_index(op.f("file_project_path_index"), table_name="project_files")
op.drop_table("project_files")
op.drop_index(op.f("project_org_created_at_index"), table_name="projects")
op.drop_index(op.f("project_org_run_id_index"), table_name="projects")
op.drop_table("projects")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"projects",
sa.Column("project_revision_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("project_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("organization_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("version", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("created_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column("modified_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column("deleted_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.Column("run_id", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint("project_revision_id", name=op.f("projects_pkey")),
sa.UniqueConstraint(
"organization_id",
"project_id",
"version",
name=op.f("uc_org_project_version"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
)
op.create_index(op.f("project_org_run_id_index"), "projects", ["organization_id", "run_id"], unique=False)
op.create_index(op.f("project_org_created_at_index"), "projects", ["organization_id", "created_at"], unique=False)
op.create_table(
"project_files",
sa.Column("file_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("project_revision_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("project_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("organization_id", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("file_path", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("file_name", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("file_type", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("content_hash", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("file_size", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("mime_type", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("encoding", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("artifact_id", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("created_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column("modified_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column("deleted_at", postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint("file_id", name=op.f("project_files_pkey")),
sa.UniqueConstraint(
"project_revision_id",
"file_path",
name=op.f("unique_project_file_path"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
)
op.create_index(
op.f("file_project_path_index"), "project_files", ["project_revision_id", "file_path"], unique=False
)
op.drop_index("script_org_run_id_index", table_name="scripts")
op.drop_index("script_org_created_at_index", table_name="scripts")
op.drop_table("scripts")
op.drop_index("file_script_path_index", table_name="script_files")
op.drop_table("script_files")
# ### end Alembic commands ###

View File

@@ -56,11 +56,6 @@ class TaskNotFound(SkyvernHTTPException):
super().__init__(f"Task {task_id} not found", status_code=status.HTTP_404_NOT_FOUND) super().__init__(f"Task {task_id} not found", status_code=status.HTTP_404_NOT_FOUND)
class ScriptNotFound(SkyvernException):
def __init__(self, script_name: str | None = None):
super().__init__(f"Script {script_name} not found. Has the script been registered?")
class MissingElement(SkyvernException): class MissingElement(SkyvernException):
def __init__(self, selector: str | None = None, element_id: str | None = None): def __init__(self, selector: str | None = None, element_id: str | None = None):
super().__init__( super().__init__(
@@ -746,6 +741,6 @@ class ElementOutOfCurrentViewport(SkyvernException):
super().__init__(f"Element {element_id} is out of current viewport") super().__init__(f"Element {element_id} is out of current viewport")
class ProjectNotFound(SkyvernHTTPException): class ScriptNotFound(SkyvernHTTPException):
def __init__(self, project_id: str) -> None: def __init__(self, script_id: str) -> None:
super().__init__(f"Project {project_id} not found") super().__init__(f"Script {script_id} not found")

View File

@@ -238,38 +238,38 @@ class ArtifactManager:
path=path, path=path,
) )
async def create_project_file_artifact( async def create_script_file_artifact(
self, self,
*, *,
organization_id: str, organization_id: str,
project_id: str, script_id: str,
project_version: int, script_version: int,
file_path: str, file_path: str,
data: bytes, data: bytes,
) -> str: ) -> str:
"""Create an artifact for a project file. """Create an artifact for a script file.
Args: Args:
organization_id: The organization ID organization_id: The organization ID
project_id: The project ID script_id: The script ID
project_version: The project version script_version: The script version
file_path: The file path relative to project root file_path: The file path relative to script root
data: The file content as bytes data: The file content as bytes
Returns: Returns:
The artifact ID The artifact ID
""" """
artifact_id = generate_artifact_id() artifact_id = generate_artifact_id()
uri = app.STORAGE.build_project_file_uri( uri = app.STORAGE.build_script_file_uri(
organization_id=organization_id, organization_id=organization_id,
project_id=project_id, script_id=script_id,
project_version=project_version, script_version=script_version,
file_path=file_path, file_path=file_path,
) )
return await self._create_artifact( return await self._create_artifact(
aio_task_primary_key=f"{project_id}_{project_version}", aio_task_primary_key=f"{script_id}_{script_version}",
artifact_id=artifact_id, artifact_id=artifact_id,
artifact_type=ArtifactType.PROJECT_FILE, artifact_type=ArtifactType.SCRIPT_FILE,
uri=uri, uri=uri,
organization_id=organization_id, organization_id=organization_id,
data=data, data=data,

View File

@@ -49,8 +49,8 @@ class ArtifactType(StrEnum):
TRACE = "trace" TRACE = "trace"
HAR = "har" HAR = "har"
# Project files # Script files
PROJECT_FILE = "project_file" SCRIPT_FILE = "script_file"
class Artifact(BaseModel): class Artifact(BaseModel):

View File

@@ -82,8 +82,8 @@ class BaseStorage(ABC):
pass pass
@abstractmethod @abstractmethod
def build_project_file_uri( def build_script_file_uri(
self, *, organization_id: str, project_id: str, project_version: int, file_path: str self, *, organization_id: str, script_id: str, script_version: int, file_path: str
) -> str: ) -> str:
pass pass

View File

@@ -78,10 +78,10 @@ class LocalStorage(BaseStorage):
file_ext = FILE_EXTENTSION_MAP[artifact_type] file_ext = FILE_EXTENTSION_MAP[artifact_type]
return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}"
def build_project_file_uri( def build_script_file_uri(
self, *, organization_id: str, project_id: str, project_version: int, file_path: str self, *, organization_id: str, script_id: str, script_version: int, file_path: str
) -> str: ) -> str:
return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/projects/{project_id}/{project_version}/{file_path}" return f"file://{self.artifact_path}/{settings.ENV}/{organization_id}/scripts/{script_id}/{script_version}/{file_path}"
async def store_artifact(self, artifact: Artifact, data: bytes) -> None: async def store_artifact(self, artifact: Artifact, data: bytes) -> None:
file_path = None file_path = None

View File

@@ -84,21 +84,21 @@ class S3Storage(BaseStorage):
file_ext = FILE_EXTENTSION_MAP[artifact_type] file_ext = FILE_EXTENTSION_MAP[artifact_type]
return f"{self._build_base_uri(organization_id)}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}" return f"{self._build_base_uri(organization_id)}/ai_suggestions/{ai_suggestion.ai_suggestion_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}"
def build_project_file_uri( def build_script_file_uri(
self, *, organization_id: str, project_id: str, project_version: int, file_path: str self, *, organization_id: str, script_id: str, script_version: int, file_path: str
) -> str: ) -> str:
"""Build the S3 URI for a project file. """Build the S3 URI for a script file.
Args: Args:
organization_id: The organization ID organization_id: The organization ID
project_id: The project ID script_id: The script ID
project_version: The project version script_version: The script version
file_path: The file path relative to project root file_path: The file path relative to script root
Returns: Returns:
The S3 URI for the project file The S3 URI for the script file
""" """
return f"{self._build_base_uri(organization_id)}/projects/{project_id}/{project_version}/{file_path}" return f"{self._build_base_uri(organization_id)}/scripts/{script_id}/{script_version}/{file_path}"
async def store_artifact(self, artifact: Artifact, data: bytes) -> None: async def store_artifact(self, artifact: Artifact, data: bytes) -> None:
sc = await self._get_storage_class_for_org(artifact.organization_id) sc = await self._get_storage_class_for_org(artifact.organization_id)

View File

@@ -29,8 +29,8 @@ from skyvern.forge.sdk.db.models import (
OrganizationModel, OrganizationModel,
OutputParameterModel, OutputParameterModel,
PersistentBrowserSessionModel, PersistentBrowserSessionModel,
ProjectFileModel, ScriptFileModel,
ProjectModel, ScriptModel,
StepModel, StepModel,
TaskGenerationModel, TaskGenerationModel,
TaskModel, TaskModel,
@@ -54,8 +54,8 @@ from skyvern.forge.sdk.db.utils import (
convert_to_organization, convert_to_organization,
convert_to_organization_auth_token, convert_to_organization_auth_token,
convert_to_output_parameter, convert_to_output_parameter,
convert_to_project, convert_to_script,
convert_to_project_file, convert_to_script_file,
convert_to_step, convert_to_step,
convert_to_task, convert_to_task,
convert_to_workflow, convert_to_workflow,
@@ -102,8 +102,8 @@ from skyvern.forge.sdk.workflow.models.workflow import (
WorkflowRunStatus, WorkflowRunStatus,
WorkflowStatus, WorkflowStatus,
) )
from skyvern.schemas.projects import Project, ProjectFile
from skyvern.schemas.runs import ProxyLocation, RunEngine, RunType from skyvern.schemas.runs import ProxyLocation, RunEngine, RunType
from skyvern.schemas.scripts import Script, ScriptFile
from skyvern.webeye.actions.actions import Action from skyvern.webeye.actions.actions import Action
from skyvern.webeye.actions.models import AgentStepOutput from skyvern.webeye.actions.models import AgentStepOutput
@@ -3546,134 +3546,134 @@ class AgentDB:
return DebugSession.model_validate(debug_session) return DebugSession.model_validate(debug_session)
async def create_project( async def create_script(
self, self,
organization_id: str, organization_id: str,
run_id: str | None = None, run_id: str | None = None,
project_id: str | None = None, script_id: str | None = None,
version: int | None = None, version: int | None = None,
) -> Project: ) -> Script:
try: try:
async with self.Session() as session: async with self.Session() as session:
project = ProjectModel( script = ScriptModel(
organization_id=organization_id, organization_id=organization_id,
run_id=run_id, run_id=run_id,
) )
if project_id: if script_id:
project.project_id = project_id script.script_id = script_id
if version: if version:
project.version = version script.version = version
session.add(project) session.add(script)
await session.commit() await session.commit()
await session.refresh(project) await session.refresh(script)
return convert_to_project(project) return convert_to_script(script)
except SQLAlchemyError: except SQLAlchemyError:
LOG.error("SQLAlchemyError", exc_info=True) LOG.error("SQLAlchemyError", exc_info=True)
raise raise
async def update_project( async def update_script(
self, self,
project_revision_id: str, script_revision_id: str,
organization_id: str, organization_id: str,
artifact_id: str | None = None, artifact_id: str | None = None,
run_id: str | None = None, run_id: str | None = None,
version: int | None = None, version: int | None = None,
) -> Project: ) -> Script:
try: try:
async with self.Session() as session: async with self.Session() as session:
get_project_query = ( get_script_query = (
select(ProjectModel) select(ScriptModel)
.filter_by(organization_id=organization_id) .filter_by(organization_id=organization_id)
.filter_by(project_revision_id=project_revision_id) .filter_by(script_revision_id=script_revision_id)
) )
if project := (await session.scalars(get_project_query)).first(): if script := (await session.scalars(get_script_query)).first():
if artifact_id: if artifact_id:
project.artifact_id = artifact_id script.artifact_id = artifact_id
if run_id: if run_id:
project.run_id = run_id script.run_id = run_id
if version: if version:
project.version = version script.version = version
await session.commit() await session.commit()
await session.refresh(project) await session.refresh(script)
return convert_to_project(project) return convert_to_script(script)
else: else:
raise NotFoundError("Project not found") raise NotFoundError("Script not found")
except SQLAlchemyError: except SQLAlchemyError:
LOG.error("SQLAlchemyError", exc_info=True) LOG.error("SQLAlchemyError", exc_info=True)
raise raise
except NotFoundError: except NotFoundError:
LOG.error("No project found to update", project_revision_id=project_revision_id) LOG.error("No script found to update", script_revision_id=script_revision_id)
raise raise
except Exception: except Exception:
LOG.error("UnexpectedError", exc_info=True) LOG.error("UnexpectedError", exc_info=True)
raise raise
async def get_projects( async def get_scripts(
self, self,
organization_id: str, organization_id: str,
page: int = 1, page: int = 1,
page_size: int = 10, page_size: int = 10,
) -> list[Project]: ) -> list[Script]:
try: try:
async with self.Session() as session: async with self.Session() as session:
# Calculate offset for pagination # Calculate offset for pagination
offset = (page - 1) * page_size offset = (page - 1) * page_size
# Subquery to get the latest version of each project # Subquery to get the latest version of each script
latest_versions_subquery = ( latest_versions_subquery = (
select(ProjectModel.project_id, func.max(ProjectModel.version).label("latest_version")) select(ScriptModel.script_id, func.max(ScriptModel.version).label("latest_version"))
.filter_by(organization_id=organization_id) .filter_by(organization_id=organization_id)
.filter(ProjectModel.deleted_at.is_(None)) .filter(ScriptModel.deleted_at.is_(None))
.group_by(ProjectModel.project_id) .group_by(ScriptModel.script_id)
.subquery() .subquery()
) )
# Main query to get projects with their latest versions # Main query to get scripts with their latest versions
get_projects_query = ( get_scripts_query = (
select(ProjectModel) select(ScriptModel)
.join( .join(
latest_versions_subquery, latest_versions_subquery,
and_( and_(
ProjectModel.project_id == latest_versions_subquery.c.project_id, ScriptModel.script_id == latest_versions_subquery.c.script_id,
ProjectModel.version == latest_versions_subquery.c.latest_version, ScriptModel.version == latest_versions_subquery.c.latest_version,
), ),
) )
.filter_by(organization_id=organization_id) .filter_by(organization_id=organization_id)
.filter(ProjectModel.deleted_at.is_(None)) .filter(ScriptModel.deleted_at.is_(None))
.order_by(ProjectModel.created_at.desc()) .order_by(ScriptModel.created_at.desc())
.limit(page_size) .limit(page_size)
.offset(offset) .offset(offset)
) )
projects = (await session.scalars(get_projects_query)).all() scripts = (await session.scalars(get_scripts_query)).all()
return [convert_to_project(project) for project in projects] return [convert_to_script(script) for script in scripts]
except SQLAlchemyError: except SQLAlchemyError:
LOG.error("SQLAlchemyError", exc_info=True) LOG.error("SQLAlchemyError", exc_info=True)
raise raise
async def get_project( async def get_script(
self, self,
project_id: str, script_id: str,
organization_id: str, organization_id: str,
version: int | None = None, version: int | None = None,
) -> Project | None: ) -> Script | None:
"""Get a specific project by ID and optionally by version.""" """Get a specific script by ID and optionally by version."""
try: try:
async with self.Session() as session: async with self.Session() as session:
get_project_query = ( get_script_query = (
select(ProjectModel) select(ScriptModel)
.filter_by(project_id=project_id) .filter_by(script_id=script_id)
.filter_by(organization_id=organization_id) .filter_by(organization_id=organization_id)
.filter(ProjectModel.deleted_at.is_(None)) .filter(ScriptModel.deleted_at.is_(None))
) )
if version is not None: if version is not None:
get_project_query = get_project_query.filter_by(version=version) get_script_query = get_script_query.filter_by(version=version)
else: else:
# Get the latest version # Get the latest version
get_project_query = get_project_query.order_by(ProjectModel.version.desc()).limit(1) get_script_query = get_script_query.order_by(ScriptModel.version.desc()).limit(1)
if project := (await session.scalars(get_project_query)).first(): if script := (await session.scalars(get_script_query)).first():
return convert_to_project(project) return convert_to_script(script)
return None return None
except SQLAlchemyError: except SQLAlchemyError:
LOG.error("SQLAlchemyError", exc_info=True) LOG.error("SQLAlchemyError", exc_info=True)
@@ -3682,21 +3682,21 @@ class AgentDB:
LOG.error("UnexpectedError", exc_info=True) LOG.error("UnexpectedError", exc_info=True)
raise raise
async def get_project_revision(self, project_revision_id: str, organization_id: str) -> Project | None: async def get_script_revision(self, script_revision_id: str, organization_id: str) -> Script | None:
async with self.Session() as session: async with self.Session() as session:
project = ( script = (
await session.scalars( await session.scalars(
select(ProjectModel) select(ScriptModel)
.filter_by(project_revision_id=project_revision_id) .filter_by(script_revision_id=script_revision_id)
.filter_by(organization_id=organization_id) .filter_by(organization_id=organization_id)
) )
).first() ).first()
return convert_to_project(project) if project else None return convert_to_script(script) if script else None
async def create_project_file( async def create_script_file(
self, self,
project_revision_id: str, script_revision_id: str,
project_id: str, script_id: str,
organization_id: str, organization_id: str,
file_path: str, file_path: str,
file_name: str, file_name: str,
@@ -3707,12 +3707,12 @@ class AgentDB:
encoding: str = "utf-8", encoding: str = "utf-8",
artifact_id: str | None = None, artifact_id: str | None = None,
) -> None: ) -> None:
"""Create a project file record.""" """Create a script file record."""
try: try:
async with self.Session() as session: async with self.Session() as session:
project_file = ProjectFileModel( script_file = ScriptFileModel(
project_revision_id=project_revision_id, script_revision_id=script_revision_id,
project_id=project_id, script_id=script_id,
organization_id=organization_id, organization_id=organization_id,
file_path=file_path, file_path=file_path,
file_name=file_name, file_name=file_name,
@@ -3723,7 +3723,7 @@ class AgentDB:
encoding=encoding, encoding=encoding,
artifact_id=artifact_id, artifact_id=artifact_id,
) )
session.add(project_file) session.add(script_file)
await session.commit() await session.commit()
except SQLAlchemyError: except SQLAlchemyError:
LOG.error("SQLAlchemyError", exc_info=True) LOG.error("SQLAlchemyError", exc_info=True)
@@ -3732,13 +3732,13 @@ class AgentDB:
LOG.error("UnexpectedError", exc_info=True) LOG.error("UnexpectedError", exc_info=True)
raise raise
async def get_project_files(self, project_revision_id: str, organization_id: str) -> list[ProjectFile]: async def get_script_files(self, script_revision_id: str, organization_id: str) -> list[ScriptFile]:
async with self.Session() as session: async with self.Session() as session:
project_files = ( script_files = (
await session.scalars( await session.scalars(
select(ProjectFileModel) select(ScriptFileModel)
.filter_by(project_revision_id=project_revision_id) .filter_by(script_revision_id=script_revision_id)
.filter_by(organization_id=organization_id) .filter_by(organization_id=organization_id)
) )
).all() ).all()
return [convert_to_project_file(project_file) for project_file in project_files] return [convert_to_script_file(script_file) for script_file in script_files]

View File

@@ -45,9 +45,9 @@ ORGANIZATION_AUTH_TOKEN_PREFIX = "oat"
ORG_PREFIX = "o" ORG_PREFIX = "o"
OUTPUT_PARAMETER_PREFIX = "op" OUTPUT_PARAMETER_PREFIX = "op"
PERSISTENT_BROWSER_SESSION_ID = "pbs" PERSISTENT_BROWSER_SESSION_ID = "pbs"
PROJECT_FILE_PREFIX = "pf" SCRIPT_FILE_PREFIX = "sf"
PROJECT_REVISION_PREFIX = "pv" SCRIPT_REVISION_PREFIX = "sr"
PROJECT_PREFIX = "p" SCRIPT_PREFIX = "s"
STEP_PREFIX = "stp" STEP_PREFIX = "stp"
TASK_GENERATION_PREFIX = "tg" TASK_GENERATION_PREFIX = "tg"
TASK_PREFIX = "tsk" TASK_PREFIX = "tsk"
@@ -212,19 +212,19 @@ def generate_organization_bitwarden_collection_id() -> str:
return f"{ORGANIZATION_BITWARDEN_COLLECTION_PREFIX}_{int_id}" return f"{ORGANIZATION_BITWARDEN_COLLECTION_PREFIX}_{int_id}"
def generate_project_id() -> str: def generate_script_id() -> str:
int_id = generate_id() int_id = generate_id()
return f"{PROJECT_PREFIX}_{int_id}" return f"{SCRIPT_PREFIX}_{int_id}"
def generate_project_revision_id() -> str: def generate_script_revision_id() -> str:
int_id = generate_id() int_id = generate_id()
return f"{PROJECT_REVISION_PREFIX}_{int_id}" return f"{SCRIPT_REVISION_PREFIX}_{int_id}"
def generate_project_file_id() -> str: def generate_script_file_id() -> str:
int_id = generate_id() int_id = generate_id()
return f"{PROJECT_FILE_PREFIX}_{int_id}" return f"{SCRIPT_FILE_PREFIX}_{int_id}"
############# Helper functions below ############## ############# Helper functions below ##############

View File

@@ -35,9 +35,9 @@ from skyvern.forge.sdk.db.id import (
generate_organization_bitwarden_collection_id, generate_organization_bitwarden_collection_id,
generate_output_parameter_id, generate_output_parameter_id,
generate_persistent_browser_session_id, generate_persistent_browser_session_id,
generate_project_file_id, generate_script_file_id,
generate_project_id, generate_script_id,
generate_project_revision_id, generate_script_revision_id,
generate_step_id, generate_step_id,
generate_task_generation_id, generate_task_generation_id,
generate_task_id, generate_task_id,
@@ -778,18 +778,18 @@ class DebugSessionModel(Base):
status = Column(String, nullable=False, default="created") status = Column(String, nullable=False, default="created")
class ProjectModel(Base): class ScriptModel(Base):
__tablename__ = "projects" __tablename__ = "scripts"
__table_args__ = ( __table_args__ = (
Index("project_org_created_at_index", "organization_id", "created_at"), Index("script_org_created_at_index", "organization_id", "created_at"),
Index("project_org_run_id_index", "organization_id", "run_id"), Index("script_org_run_id_index", "organization_id", "run_id"),
UniqueConstraint("organization_id", "project_id", "version", name="uc_org_project_version"), UniqueConstraint("organization_id", "script_id", "version", name="uc_org_script_version"),
) )
project_revision_id = Column(String, primary_key=True, default=generate_project_revision_id) script_revision_id = Column(String, primary_key=True, default=generate_script_revision_id)
project_id = Column(String, default=generate_project_id, nullable=False) # User-facing, consistent across versions script_id = Column(String, default=generate_script_id, nullable=False) # User-facing, consistent across versions
organization_id = Column(String, nullable=False) organization_id = Column(String, nullable=False)
# The workflow run or task run id that this project is generated # The workflow run or task run id that this script is generated
run_id = Column(String, nullable=True) run_id = Column(String, nullable=True)
version = Column(Integer, default=1, nullable=False) version = Column(Integer, default=1, nullable=False)
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
@@ -802,16 +802,16 @@ class ProjectModel(Base):
deleted_at = Column(DateTime, nullable=True) deleted_at = Column(DateTime, nullable=True)
class ProjectFileModel(Base): class ScriptFileModel(Base):
__tablename__ = "project_files" __tablename__ = "script_files"
__table_args__ = ( __table_args__ = (
Index("file_project_path_index", "project_revision_id", "file_path"), Index("file_script_path_index", "script_revision_id", "file_path"),
UniqueConstraint("project_revision_id", "file_path", name="unique_project_file_path"), UniqueConstraint("script_revision_id", "file_path", name="unique_script_file_path"),
) )
file_id = Column(String, primary_key=True, default=generate_project_file_id) file_id = Column(String, primary_key=True, default=generate_script_file_id)
project_revision_id = Column(String, nullable=False) script_revision_id = Column(String, nullable=False)
project_id = Column(String, nullable=False) script_id = Column(String, nullable=False)
organization_id = Column(String, nullable=False) organization_id = Column(String, nullable=False)
file_path = Column(String, nullable=False) # e.g., "src/utils.py" file_path = Column(String, nullable=False) # e.g., "src/utils.py"

View File

@@ -15,8 +15,8 @@ from skyvern.forge.sdk.db.models import (
OrganizationAuthTokenModel, OrganizationAuthTokenModel,
OrganizationModel, OrganizationModel,
OutputParameterModel, OutputParameterModel,
ProjectFileModel, ScriptFileModel,
ProjectModel, ScriptModel,
StepModel, StepModel,
TaskModel, TaskModel,
WorkflowModel, WorkflowModel,
@@ -50,8 +50,8 @@ from skyvern.forge.sdk.workflow.models.workflow import (
WorkflowRunStatus, WorkflowRunStatus,
WorkflowStatus, WorkflowStatus,
) )
from skyvern.schemas.projects import Project, ProjectFile
from skyvern.schemas.runs import ProxyLocation from skyvern.schemas.runs import ProxyLocation
from skyvern.schemas.scripts import Script, ScriptFile
from skyvern.webeye.actions.actions import ( from skyvern.webeye.actions.actions import (
Action, Action,
ActionType, ActionType,
@@ -506,35 +506,35 @@ def convert_to_workflow_run_block(
return block return block
def convert_to_project(project_model: ProjectModel) -> Project: def convert_to_script(script_model: ScriptModel) -> Script:
return Project( return Script(
project_revision_id=project_model.project_revision_id, script_revision_id=script_model.script_revision_id,
project_id=project_model.project_id, script_id=script_model.script_id,
organization_id=project_model.organization_id, organization_id=script_model.organization_id,
run_id=project_model.run_id, run_id=script_model.run_id,
version=project_model.version, version=script_model.version,
created_at=project_model.created_at, created_at=script_model.created_at,
modified_at=project_model.modified_at, modified_at=script_model.modified_at,
deleted_at=project_model.deleted_at, deleted_at=script_model.deleted_at,
) )
def convert_to_project_file(project_file_model: ProjectFileModel) -> ProjectFile: def convert_to_script_file(script_file_model: ScriptFileModel) -> ScriptFile:
return ProjectFile( return ScriptFile(
file_id=project_file_model.file_id, file_id=script_file_model.file_id,
project_revision_id=project_file_model.project_revision_id, script_revision_id=script_file_model.script_revision_id,
project_id=project_file_model.project_id, script_id=script_file_model.script_id,
organization_id=project_file_model.organization_id, organization_id=script_file_model.organization_id,
file_path=project_file_model.file_path, file_path=script_file_model.file_path,
file_name=project_file_model.file_name, file_name=script_file_model.file_name,
file_type=project_file_model.file_type, file_type=script_file_model.file_type,
content_hash=project_file_model.content_hash, content_hash=script_file_model.content_hash,
file_size=project_file_model.file_size, file_size=script_file_model.file_size,
mime_type=project_file_model.mime_type, mime_type=script_file_model.mime_type,
encoding=project_file_model.encoding, encoding=script_file_model.encoding,
artifact_id=project_file_model.artifact_id, artifact_id=script_file_model.artifact_id,
created_at=project_file_model.created_at, created_at=script_file_model.created_at,
modified_at=project_file_model.modified_at, modified_at=script_file_model.modified_at,
) )

View File

@@ -12,7 +12,7 @@ from skyvern.forge.sdk.schemas.task_v2 import TaskV2Status
from skyvern.forge.sdk.schemas.tasks import TaskStatus from skyvern.forge.sdk.schemas.tasks import TaskStatus
from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus
from skyvern.schemas.runs import RunEngine, RunType from skyvern.schemas.runs import RunEngine, RunType
from skyvern.services import project_service, task_v2_service from skyvern.services import script_service, task_v2_service
from skyvern.utils.files import initialize_skyvern_state_file from skyvern.utils.files import initialize_skyvern_state_file
LOG = structlog.get_logger() LOG = structlog.get_logger()
@@ -62,10 +62,10 @@ class AsyncExecutor(abc.ABC):
pass pass
@abc.abstractmethod @abc.abstractmethod
async def execute_project( async def execute_script(
self, self,
request: Request | None, request: Request | None,
project_id: str, script_id: str,
organization_id: str, organization_id: str,
background_tasks: BackgroundTasks | None, background_tasks: BackgroundTasks | None,
**kwargs: dict, **kwargs: dict,
@@ -209,18 +209,18 @@ class BackgroundTaskExecutor(AsyncExecutor):
browser_session_id=browser_session_id, browser_session_id=browser_session_id,
) )
async def execute_project( async def execute_script(
self, self,
request: Request | None, request: Request | None,
project_id: str, script_id: str,
organization_id: str, organization_id: str,
background_tasks: BackgroundTasks | None, background_tasks: BackgroundTasks | None,
**kwargs: dict, **kwargs: dict,
) -> None: ) -> None:
if background_tasks: if background_tasks:
background_tasks.add_task( background_tasks.add_task(
project_service.execute_project, script_service.execute_script,
project_id=project_id, script_id=script_id,
organization_id=organization_id, organization_id=organization_id,
background_tasks=background_tasks, background_tasks=background_tasks,
) )

View File

@@ -1,9 +1,9 @@
from skyvern.forge.sdk.routes import agent_protocol # noqa: F401 from skyvern.forge.sdk.routes import agent_protocol # noqa: F401
from skyvern.forge.sdk.routes import browser_sessions # noqa: F401 from skyvern.forge.sdk.routes import browser_sessions # noqa: F401
from skyvern.forge.sdk.routes import credentials # noqa: F401 from skyvern.forge.sdk.routes import credentials # noqa: F401
from skyvern.forge.sdk.routes import projects # noqa: F401
from skyvern.forge.sdk.routes import pylon # noqa: F401 from skyvern.forge.sdk.routes import pylon # noqa: F401
from skyvern.forge.sdk.routes import run_blocks # noqa: F401 from skyvern.forge.sdk.routes import run_blocks # noqa: F401
from skyvern.forge.sdk.routes import scripts # noqa: F401
from skyvern.forge.sdk.routes import streaming # noqa: F401 from skyvern.forge.sdk.routes import streaming # noqa: F401
from skyvern.forge.sdk.routes import streaming_commands # noqa: F401 from skyvern.forge.sdk.routes import streaming_commands # noqa: F401
from skyvern.forge.sdk.routes import streaming_vnc # noqa: F401 from skyvern.forge.sdk.routes import streaming_vnc # noqa: F401

View File

@@ -1,295 +0,0 @@
import base64
import hashlib
import structlog
from fastapi import BackgroundTasks, Depends, HTTPException, Path, Query, Request
from skyvern.forge import app
from skyvern.forge.sdk.executor.factory import AsyncExecutorFactory
from skyvern.forge.sdk.routes.routers import base_router
from skyvern.forge.sdk.schemas.organizations import Organization
from skyvern.forge.sdk.services import org_auth_service
from skyvern.schemas.projects import CreateProjectRequest, CreateProjectResponse, DeployProjectRequest, Project
from skyvern.services import project_service
LOG = structlog.get_logger()
@base_router.post(
"/projects",
response_model=CreateProjectResponse,
summary="Create project",
description="Create a new project with optional files and metadata",
tags=["Projects"],
openapi_extra={
"x-fern-sdk-method-name": "create_project",
},
)
@base_router.post(
"/projects/",
response_model=CreateProjectResponse,
include_in_schema=False,
)
async def create_project(
data: CreateProjectRequest,
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> CreateProjectResponse:
"""Create a new project with optional files and metadata."""
organization_id = current_org.organization_id
LOG.info(
"Creating project",
organization_id=organization_id,
file_count=len(data.files) if data.files else 0,
)
if data.run_id:
if not await app.DATABASE.get_run(run_id=data.run_id, organization_id=organization_id):
raise HTTPException(status_code=404, detail=f"Run_id {data.run_id} not found")
try:
# Create the project in the database
project = await app.DATABASE.create_project(
organization_id=organization_id,
run_id=data.run_id,
)
# Process files if provided
file_tree = {}
file_count = 0
if data.files:
file_tree = await project_service.build_file_tree(
data.files,
organization_id=organization_id,
project_id=project.project_id,
project_version=project.version,
project_revision_id=project.project_revision_id,
)
file_count = len(data.files)
return CreateProjectResponse(
project_id=project.project_id,
version=project.version,
run_id=project.run_id,
file_count=file_count,
created_at=project.created_at,
file_tree=file_tree,
)
except Exception as e:
LOG.error("Failed to create project", error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Failed to create project")
@base_router.get(
"/projects/{project_id}",
response_model=Project,
summary="Get project by ID",
description="Retrieves a specific project by its ID",
tags=["Projects"],
openapi_extra={
"x-fern-sdk-method-name": "get_project",
},
)
@base_router.get(
"/projects/{project_id}/",
response_model=Project,
include_in_schema=False,
)
async def get_project(
project_id: str = Path(
...,
description="The unique identifier of the project",
examples=["proj_abc123"],
),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> Project:
"""Get a project by its ID."""
LOG.info(
"Getting project",
organization_id=current_org.organization_id,
project_id=project_id,
)
project = await app.DATABASE.get_project(
project_id=project_id,
organization_id=current_org.organization_id,
)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
@base_router.get(
"/projects",
response_model=list[Project],
summary="Get all projects",
description="Retrieves a paginated list of projects for the current organization",
tags=["Projects"],
openapi_extra={
"x-fern-sdk-method-name": "get_projects",
},
)
@base_router.get(
"/projects/",
response_model=list[Project],
include_in_schema=False,
)
async def get_projects(
current_org: Organization = Depends(org_auth_service.get_current_org),
page: int = Query(
1,
ge=1,
description="Page number for pagination",
examples=[1],
),
page_size: int = Query(
10,
ge=1,
description="Number of items per page",
examples=[10],
),
) -> list[Project]:
"""Get all projects for the current organization."""
LOG.info(
"Getting projects",
organization_id=current_org.organization_id,
page=page,
page_size=page_size,
)
projects = await app.DATABASE.get_projects(
organization_id=current_org.organization_id,
page=page,
page_size=page_size,
)
return projects
@base_router.post(
"/projects/{project_id}/deploy",
response_model=CreateProjectResponse,
summary="Deploy project",
description="Deploy a project with updated files, creating a new version",
tags=["Projects"],
openapi_extra={
"x-fern-sdk-method-name": "deploy_project",
},
)
@base_router.post(
"/projects/{project_id}/deploy/",
response_model=CreateProjectResponse,
include_in_schema=False,
)
async def deploy_project(
data: DeployProjectRequest,
project_id: str = Path(
...,
description="The unique identifier of the project",
examples=["proj_abc123"],
),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> CreateProjectResponse:
"""Deploy a project with updated files, creating a new version."""
LOG.info(
"Deploying project",
organization_id=current_org.organization_id,
project_id=project_id,
file_count=len(data.files) if data.files else 0,
)
try:
# Get the latest version of the project
latest_project = await app.DATABASE.get_project(
project_id=project_id,
organization_id=current_org.organization_id,
)
if not latest_project:
raise HTTPException(status_code=404, detail="Project not found")
# Create a new version of the project
new_version = latest_project.version + 1
new_project_revision = await app.DATABASE.create_project(
organization_id=current_org.organization_id,
run_id=latest_project.run_id,
project_id=project_id, # Use the same project_id for versioning
version=new_version,
)
# Process files if provided
file_tree = {}
file_count = 0
if data.files:
file_tree = await project_service.build_file_tree(
data.files,
organization_id=current_org.organization_id,
project_id=new_project_revision.project_id,
project_version=new_project_revision.version,
project_revision_id=new_project_revision.project_revision_id,
)
file_count = len(data.files)
# Create project file records
for file in data.files:
content_bytes = base64.b64decode(file.content)
content_hash = hashlib.sha256(content_bytes).hexdigest()
file_size = len(content_bytes)
# Extract file name from path
file_name = file.path.split("/")[-1]
await app.DATABASE.create_project_file(
project_revision_id=new_project_revision.project_revision_id,
project_id=new_project_revision.project_id,
organization_id=new_project_revision.organization_id,
file_path=file.path,
file_name=file_name,
file_type="file",
content_hash=f"sha256:{content_hash}",
file_size=file_size,
mime_type=file.mime_type,
encoding=file.encoding,
)
return CreateProjectResponse(
project_id=new_project_revision.project_id,
version=new_project_revision.version,
run_id=new_project_revision.run_id,
file_count=file_count,
created_at=new_project_revision.created_at,
file_tree=file_tree,
)
except HTTPException:
raise
except Exception as e:
LOG.error("Failed to deploy project", error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Failed to deploy project")
@base_router.post(
"/projects/{project_id}/run",
summary="Run project",
description="Run a project",
tags=["Projects"],
)
async def run_project(
request: Request,
background_tasks: BackgroundTasks,
project_id: str = Path(
...,
description="The unique identifier of the project",
examples=["proj_abc123"],
),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> None:
"""Run a project."""
# await project_service.execute_project(
# project_id=project_id,
# organization_id=current_org.organization_id,
# background_tasks=background_tasks,
# )
await AsyncExecutorFactory.get_executor().execute_project(
request=request,
project_id=project_id,
organization_id=current_org.organization_id,
background_tasks=background_tasks,
)

View File

@@ -0,0 +1,295 @@
import base64
import hashlib
import structlog
from fastapi import BackgroundTasks, Depends, HTTPException, Path, Query, Request
from skyvern.forge import app
from skyvern.forge.sdk.executor.factory import AsyncExecutorFactory
from skyvern.forge.sdk.routes.routers import base_router
from skyvern.forge.sdk.schemas.organizations import Organization
from skyvern.forge.sdk.services import org_auth_service
from skyvern.schemas.scripts import CreateScriptRequest, CreateScriptResponse, DeployScriptRequest, Script
from skyvern.services import script_service
LOG = structlog.get_logger()
@base_router.post(
"/scripts",
response_model=CreateScriptResponse,
summary="Create script",
description="Create a new script with optional files and metadata",
tags=["Scripts"],
openapi_extra={
"x-fern-sdk-method-name": "create_script",
},
)
@base_router.post(
"/scripts/",
response_model=CreateScriptResponse,
include_in_schema=False,
)
async def create_script(
data: CreateScriptRequest,
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> CreateScriptResponse:
"""Create a new script with optional files and metadata."""
organization_id = current_org.organization_id
LOG.info(
"Creating script",
organization_id=organization_id,
file_count=len(data.files) if data.files else 0,
)
if data.run_id:
if not await app.DATABASE.get_run(run_id=data.run_id, organization_id=organization_id):
raise HTTPException(status_code=404, detail=f"Run_id {data.run_id} not found")
try:
# Create the script in the database
script = await app.DATABASE.create_script(
organization_id=organization_id,
run_id=data.run_id,
)
# Process files if provided
file_tree = {}
file_count = 0
if data.files:
file_tree = await script_service.build_file_tree(
data.files,
organization_id=organization_id,
script_id=script.script_id,
script_version=script.version,
script_revision_id=script.script_revision_id,
)
file_count = len(data.files)
return CreateScriptResponse(
script_id=script.script_id,
version=script.version,
run_id=script.run_id,
file_count=file_count,
created_at=script.created_at,
file_tree=file_tree,
)
except Exception as e:
LOG.error("Failed to create script", error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Failed to create script")
@base_router.get(
"/scripts/{script_id}",
response_model=Script,
summary="Get script by ID",
description="Retrieves a specific script by its ID",
tags=["Scripts"],
openapi_extra={
"x-fern-sdk-method-name": "get_script",
},
)
@base_router.get(
"/scripts/{script_id}/",
response_model=Script,
include_in_schema=False,
)
async def get_script(
script_id: str = Path(
...,
description="The unique identifier of the script",
examples=["s_abc123"],
),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> Script:
"""Get a script by its ID."""
LOG.info(
"Getting script",
organization_id=current_org.organization_id,
script_id=script_id,
)
script = await app.DATABASE.get_script(
script_id=script_id,
organization_id=current_org.organization_id,
)
if not script:
raise HTTPException(status_code=404, detail="Script not found")
return script
@base_router.get(
"/scripts",
response_model=list[Script],
summary="Get all scripts",
description="Retrieves a paginated list of scripts for the current organization",
tags=["Scripts"],
openapi_extra={
"x-fern-sdk-method-name": "get_scripts",
},
)
@base_router.get(
"/scripts/",
response_model=list[Script],
include_in_schema=False,
)
async def get_scripts(
current_org: Organization = Depends(org_auth_service.get_current_org),
page: int = Query(
1,
ge=1,
description="Page number for pagination",
examples=[1],
),
page_size: int = Query(
10,
ge=1,
description="Number of items per page",
examples=[10],
),
) -> list[Script]:
"""Get all scripts for the current organization."""
LOG.info(
"Getting scripts",
organization_id=current_org.organization_id,
page=page,
page_size=page_size,
)
scripts = await app.DATABASE.get_scripts(
organization_id=current_org.organization_id,
page=page,
page_size=page_size,
)
return scripts
@base_router.post(
"/scripts/{script_id}/deploy",
response_model=CreateScriptResponse,
summary="Deploy script",
description="Deploy a script with updated files, creating a new version",
tags=["Scripts"],
openapi_extra={
"x-fern-sdk-method-name": "deploy_script",
},
)
@base_router.post(
"/scripts/{script_id}/deploy/",
response_model=CreateScriptResponse,
include_in_schema=False,
)
async def deploy_script(
data: DeployScriptRequest,
script_id: str = Path(
...,
description="The unique identifier of the script",
examples=["s_abc123"],
),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> CreateScriptResponse:
"""Deploy a script with updated files, creating a new version."""
LOG.info(
"Deploying script",
organization_id=current_org.organization_id,
script_id=script_id,
file_count=len(data.files) if data.files else 0,
)
try:
# Get the latest version of the script
latest_script = await app.DATABASE.get_script(
script_id=script_id,
organization_id=current_org.organization_id,
)
if not latest_script:
raise HTTPException(status_code=404, detail="Script not found")
# Create a new version of the script
new_version = latest_script.version + 1
new_script_revision = await app.DATABASE.create_script(
organization_id=current_org.organization_id,
run_id=latest_script.run_id,
script_id=script_id, # Use the same script_id for versioning
version=new_version,
)
# Process files if provided
file_tree = {}
file_count = 0
if data.files:
file_tree = await script_service.build_file_tree(
data.files,
organization_id=current_org.organization_id,
script_id=new_script_revision.script_id,
script_version=new_script_revision.version,
script_revision_id=new_script_revision.script_revision_id,
)
file_count = len(data.files)
# Create script file records
for file in data.files:
content_bytes = base64.b64decode(file.content)
content_hash = hashlib.sha256(content_bytes).hexdigest()
file_size = len(content_bytes)
# Extract file name from path
file_name = file.path.split("/")[-1]
await app.DATABASE.create_script_file(
script_revision_id=new_script_revision.script_revision_id,
script_id=new_script_revision.script_id,
organization_id=new_script_revision.organization_id,
file_path=file.path,
file_name=file_name,
file_type="file",
content_hash=f"sha256:{content_hash}",
file_size=file_size,
mime_type=file.mime_type,
encoding=file.encoding,
)
return CreateScriptResponse(
script_id=new_script_revision.script_id,
version=new_script_revision.version,
run_id=new_script_revision.run_id,
file_count=file_count,
created_at=new_script_revision.created_at,
file_tree=file_tree,
)
except HTTPException:
raise
except Exception as e:
LOG.error("Failed to deploy script", error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail="Failed to deploy script")
@base_router.post(
"/scripts/{script_id}/run",
summary="Run script",
description="Run a script",
tags=["Scripts"],
)
async def run_script(
request: Request,
background_tasks: BackgroundTasks,
script_id: str = Path(
...,
description="The unique identifier of the script",
examples=["s_abc123"],
),
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> None:
"""Run a script."""
# await script_service.execute_script(
# script_id=script_id,
# organization_id=current_org.organization_id,
# background_tasks=background_tasks,
# )
await AsyncExecutorFactory.get_executor().execute_script(
request=request,
script_id=script_id,
organization_id=current_org.organization_id,
background_tasks=background_tasks,
)

View File

@@ -14,10 +14,10 @@ class FileEncoding(StrEnum):
UTF8 = "utf-8" UTF8 = "utf-8"
class ProjectFile(BaseModel): class ScriptFile(BaseModel):
file_id: str file_id: str
project_revision_id: str script_revision_id: str
project_id: str script_id: str
organization_id: str organization_id: str
file_path: str # e.g., "src/utils.py" file_path: str # e.g., "src/utils.py"
@@ -41,21 +41,21 @@ class ProjectFile(BaseModel):
return self.content return self.content
class ProjectFileCreate(BaseModel): class ScriptFileCreate(BaseModel):
"""Model representing a file in a project.""" """Model representing a file in a script."""
path: str = Field(..., description="File path relative to project root", examples=["src/main.py"]) path: str = Field(..., description="File path relative to script root", examples=["src/main.py"])
content: str = Field(..., description="Base64 encoded file content") content: str = Field(..., description="Base64 encoded file content")
encoding: FileEncoding = Field(default=FileEncoding.BASE64, description="Content encoding") encoding: FileEncoding = Field(default=FileEncoding.BASE64, description="Content encoding")
mime_type: str | None = Field(default=None, description="MIME type (auto-detected if not provided)") mime_type: str | None = Field(default=None, description="MIME type (auto-detected if not provided)")
class CreateProjectRequest(BaseModel): class CreateScriptRequest(BaseModel):
workflow_id: str | None = Field(default=None, description="Associated workflow ID") workflow_id: str | None = Field(default=None, description="Associated workflow ID")
run_id: str | None = Field(default=None, description="Associated run ID") run_id: str | None = Field(default=None, description="Associated run ID")
files: list[ProjectFileCreate] | None = Field( files: list[ScriptFileCreate] | None = Field(
default=None, default=None,
description="Array of files to include in the project", description="Array of files to include in the script",
examples=[ examples=[
{ {
"path": "main.py", "path": "main.py",
@@ -84,12 +84,12 @@ class FileNode(BaseModel):
children: dict[str, FileNode] | None = Field(default=None, description="Child nodes for directories") children: dict[str, FileNode] | None = Field(default=None, description="Child nodes for directories")
class DeployProjectRequest(BaseModel): class DeployScriptRequest(BaseModel):
"""Request model for deploying a project with updated files.""" """Request model for deploying a script with updated files."""
files: list[ProjectFileCreate] = Field( files: list[ScriptFileCreate] = Field(
..., ...,
description="Array of files to include in the project", description="Array of files to include in the script",
examples=[ examples=[
{ {
"path": "src/main.py", "path": "src/main.py",
@@ -101,27 +101,27 @@ class DeployProjectRequest(BaseModel):
) )
class CreateProjectResponse(BaseModel): class CreateScriptResponse(BaseModel):
project_id: str = Field(..., description="Unique project identifier", examples=["proj_abc123"]) script_id: str = Field(..., description="Unique script identifier", examples=["s_abc123"])
version: int = Field(..., description="Project version number", examples=[1]) version: int = Field(..., description="Script version number", examples=[1])
run_id: str | None = Field( run_id: str | None = Field(
default=None, description="ID of the workflow run or task run that generated this project" default=None, description="ID of the workflow run or task run that generated this script"
) )
file_count: int = Field(..., description="Total number of files in the project") file_count: int = Field(..., description="Total number of files in the script")
file_tree: dict[str, FileNode] = Field(..., description="Hierarchical file tree structure") file_tree: dict[str, FileNode] = Field(..., description="Hierarchical file tree structure")
created_at: datetime = Field(..., description="Timestamp when the project was created") created_at: datetime = Field(..., description="Timestamp when the script was created")
class Project(BaseModel): class Script(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
project_revision_id: str = Field(description="Unique identifier for this specific project revision") script_revision_id: str = Field(description="Unique identifier for this specific script revision")
project_id: str = Field(description="User-facing project identifier, consistent across versions") script_id: str = Field(description="User-facing script identifier, consistent across versions")
organization_id: str = Field(description="ID of the organization that owns this project") organization_id: str = Field(description="ID of the organization that owns this script")
run_id: str | None = Field( run_id: str | None = Field(
default=None, description="ID of the workflow run or task run that generated this project" default=None, description="ID of the workflow run or task run that generated this script"
) )
version: int = Field(description="Version number of the project") version: int = Field(description="Version number of the script")
created_at: datetime = Field(description="Timestamp when the project was created") created_at: datetime = Field(description="Timestamp when the script was created")
modified_at: datetime = Field(description="Timestamp when the project was last modified") modified_at: datetime = Field(description="Timestamp when the script was last modified")
deleted_at: datetime | None = Field(default=None, description="Timestamp when the project was soft deleted") deleted_at: datetime | None = Field(default=None, description="Timestamp when the script was soft deleted")

View File

@@ -7,19 +7,19 @@ from datetime import datetime
import structlog import structlog
from fastapi import BackgroundTasks from fastapi import BackgroundTasks
from skyvern.exceptions import ProjectNotFound from skyvern.exceptions import ScriptNotFound
from skyvern.forge import app from skyvern.forge import app
from skyvern.schemas.projects import FileNode, ProjectFileCreate from skyvern.schemas.scripts import FileNode, ScriptFileCreate
LOG = structlog.get_logger(__name__) LOG = structlog.get_logger(__name__)
async def build_file_tree( async def build_file_tree(
files: list[ProjectFileCreate], files: list[ScriptFileCreate],
organization_id: str, organization_id: str,
project_id: str, script_id: str,
project_version: int, script_version: int,
project_revision_id: str, script_revision_id: str,
) -> dict[str, FileNode]: ) -> dict[str, FileNode]:
"""Build a hierarchical file tree from a list of files and upload the files to s3 with the same tree structure.""" """Build a hierarchical file tree from a list of files and upload the files to s3 with the same tree structure."""
file_tree: dict[str, FileNode] = {} file_tree: dict[str, FileNode] = {}
@@ -32,24 +32,24 @@ async def build_file_tree(
# Create artifact and upload to S3 # Create artifact and upload to S3
try: try:
artifact_id = await app.ARTIFACT_MANAGER.create_project_file_artifact( artifact_id = await app.ARTIFACT_MANAGER.create_script_file_artifact(
organization_id=organization_id, organization_id=organization_id,
project_id=project_id, script_id=script_id,
project_version=project_version, script_version=script_version,
file_path=file.path, file_path=file.path,
data=content_bytes, data=content_bytes,
) )
LOG.debug( LOG.debug(
"Created project file artifact", "Created script file artifact",
artifact_id=artifact_id, artifact_id=artifact_id,
file_path=file.path, file_path=file.path,
project_id=project_id, script_id=script_id,
project_version=project_version, script_version=script_version,
) )
# create a project file record # create a script file record
await app.DATABASE.create_project_file( await app.DATABASE.create_script_file(
project_revision_id=project_revision_id, script_revision_id=script_revision_id,
project_id=project_id, script_id=script_id,
organization_id=organization_id, organization_id=organization_id,
file_path=file.path, file_path=file.path,
file_name=file.path.split("/")[-1], file_name=file.path.split("/")[-1],
@@ -61,11 +61,11 @@ async def build_file_tree(
) )
except Exception: except Exception:
LOG.exception( LOG.exception(
"Failed to create project file artifact", "Failed to create script file artifact",
file_path=file.path, file_path=file.path,
project_id=project_id, script_id=script_id,
project_version=project_version, script_version=script_version,
project_revision_id=project_revision_id, script_revision_id=script_revision_id,
) )
raise raise
@@ -96,43 +96,43 @@ async def build_file_tree(
return file_tree return file_tree
async def execute_project( async def execute_script(
project_id: str, script_id: str,
organization_id: str, organization_id: str,
background_tasks: BackgroundTasks | None = None, background_tasks: BackgroundTasks | None = None,
) -> None: ) -> None:
# TODO: assume the project only has one ProjectFile called main.py # TODO: assume the script only has one ScriptFile called main.py
# step 1: get the project revision # step 1: get the script revision
# step 2: get the project files # step 2: get the script files
# step 3: copy the project files to the local directory # step 3: copy the script files to the local directory
# step 4: execute the project # step 4: execute the script
# step 1: get the project revision # step 1: get the script revision
project = await app.DATABASE.get_project( script = await app.DATABASE.get_script(
project_id=project_id, script_id=script_id,
organization_id=organization_id, organization_id=organization_id,
) )
if not project: if not script:
raise ProjectNotFound(project_id=project_id) raise ScriptNotFound(script_id=script_id)
# step 2: get the project files # step 2: get the script files
project_files = await app.DATABASE.get_project_files( script_files = await app.DATABASE.get_script_files(
project_revision_id=project.project_revision_id, organization_id=organization_id script_revision_id=script.script_revision_id, organization_id=organization_id
) )
# step 3: copy the project files to the local directory # step 3: copy the script files to the local directory
for file in project_files: for file in script_files:
# retrieve the artifact # retrieve the artifact
if not file.artifact_id: if not file.artifact_id:
continue continue
artifact = await app.DATABASE.get_artifact_by_id(file.artifact_id, organization_id) artifact = await app.DATABASE.get_artifact_by_id(file.artifact_id, organization_id)
if not artifact: if not artifact:
LOG.error("Artifact not found", artifact_id=file.artifact_id, project_id=project_id) LOG.error("Artifact not found", artifact_id=file.artifact_id, script_id=script_id)
continue continue
file_content = await app.ARTIFACT_MANAGER.retrieve_artifact(artifact) file_content = await app.ARTIFACT_MANAGER.retrieve_artifact(artifact)
if not file_content: if not file_content:
continue continue
file_path = os.path.join(project.project_id, file.file_path) file_path = os.path.join(script.script_id, file.file_path)
# create the directory if it doesn't exist # create the directory if it doesn't exist
os.makedirs(os.path.dirname(file_path), exist_ok=True) os.makedirs(os.path.dirname(file_path), exist_ok=True)
@@ -154,7 +154,7 @@ async def execute_project(
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(file_content) f.write(file_content)
# step 4: execute the project # step 4: execute the script
if background_tasks: if background_tasks:
background_tasks.add_task(subprocess.run, ["python", f"{project.project_id}/main.py"]) background_tasks.add_task(subprocess.run, ["python", f"{script.script_id}/main.py"])
LOG.info("Project executed successfully", project_id=project_id) LOG.info("Script executed successfully", script_id=script_id)