diff --git a/alembic/versions/2025_08_07_0449-0135ee8b36b0_rename_project_projects_script_scripts.py b/alembic/versions/2025_08_07_0449-0135ee8b36b0_rename_project_projects_script_scripts.py new file mode 100644 index 00000000..f6fe1e74 --- /dev/null +++ b/alembic/versions/2025_08_07_0449-0135ee8b36b0_rename_project_projects_script_scripts.py @@ -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 ### diff --git a/skyvern/exceptions.py b/skyvern/exceptions.py index 10fdf734..3169b939 100644 --- a/skyvern/exceptions.py +++ b/skyvern/exceptions.py @@ -56,11 +56,6 @@ class TaskNotFound(SkyvernHTTPException): 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): def __init__(self, selector: str | None = None, element_id: str | None = None): super().__init__( @@ -746,6 +741,6 @@ class ElementOutOfCurrentViewport(SkyvernException): super().__init__(f"Element {element_id} is out of current viewport") -class ProjectNotFound(SkyvernHTTPException): - def __init__(self, project_id: str) -> None: - super().__init__(f"Project {project_id} not found") +class ScriptNotFound(SkyvernHTTPException): + def __init__(self, script_id: str) -> None: + super().__init__(f"Script {script_id} not found") diff --git a/skyvern/forge/sdk/artifact/manager.py b/skyvern/forge/sdk/artifact/manager.py index 4467790c..6a8c2f1a 100644 --- a/skyvern/forge/sdk/artifact/manager.py +++ b/skyvern/forge/sdk/artifact/manager.py @@ -238,38 +238,38 @@ class ArtifactManager: path=path, ) - async def create_project_file_artifact( + async def create_script_file_artifact( self, *, organization_id: str, - project_id: str, - project_version: int, + script_id: str, + script_version: int, file_path: str, data: bytes, ) -> str: - """Create an artifact for a project file. + """Create an artifact for a script file. Args: organization_id: The organization ID - project_id: The project ID - project_version: The project version - file_path: The file path relative to project root + script_id: The script ID + script_version: The script version + file_path: The file path relative to script root data: The file content as bytes Returns: The 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, - project_id=project_id, - project_version=project_version, + script_id=script_id, + script_version=script_version, file_path=file_path, ) 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_type=ArtifactType.PROJECT_FILE, + artifact_type=ArtifactType.SCRIPT_FILE, uri=uri, organization_id=organization_id, data=data, diff --git a/skyvern/forge/sdk/artifact/models.py b/skyvern/forge/sdk/artifact/models.py index ff6fc27f..2250e860 100644 --- a/skyvern/forge/sdk/artifact/models.py +++ b/skyvern/forge/sdk/artifact/models.py @@ -49,8 +49,8 @@ class ArtifactType(StrEnum): TRACE = "trace" HAR = "har" - # Project files - PROJECT_FILE = "project_file" + # Script files + SCRIPT_FILE = "script_file" class Artifact(BaseModel): diff --git a/skyvern/forge/sdk/artifact/storage/base.py b/skyvern/forge/sdk/artifact/storage/base.py index 9de3770f..7e0e87d5 100644 --- a/skyvern/forge/sdk/artifact/storage/base.py +++ b/skyvern/forge/sdk/artifact/storage/base.py @@ -82,8 +82,8 @@ class BaseStorage(ABC): pass @abstractmethod - def build_project_file_uri( - self, *, organization_id: str, project_id: str, project_version: int, file_path: str + def build_script_file_uri( + self, *, organization_id: str, script_id: str, script_version: int, file_path: str ) -> str: pass diff --git a/skyvern/forge/sdk/artifact/storage/local.py b/skyvern/forge/sdk/artifact/storage/local.py index 0255ea8d..b97642d0 100644 --- a/skyvern/forge/sdk/artifact/storage/local.py +++ b/skyvern/forge/sdk/artifact/storage/local.py @@ -78,10 +78,10 @@ class LocalStorage(BaseStorage): 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}" - def build_project_file_uri( - self, *, organization_id: str, project_id: str, project_version: int, file_path: str + def build_script_file_uri( + self, *, organization_id: str, script_id: str, script_version: int, file_path: 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: file_path = None diff --git a/skyvern/forge/sdk/artifact/storage/s3.py b/skyvern/forge/sdk/artifact/storage/s3.py index 3d0ffe1a..92792a9a 100644 --- a/skyvern/forge/sdk/artifact/storage/s3.py +++ b/skyvern/forge/sdk/artifact/storage/s3.py @@ -84,21 +84,21 @@ class S3Storage(BaseStorage): 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}" - def build_project_file_uri( - self, *, organization_id: str, project_id: str, project_version: int, file_path: str + def build_script_file_uri( + self, *, organization_id: str, script_id: str, script_version: int, file_path: str ) -> str: - """Build the S3 URI for a project file. + """Build the S3 URI for a script file. Args: organization_id: The organization ID - project_id: The project ID - project_version: The project version - file_path: The file path relative to project root + script_id: The script ID + script_version: The script version + file_path: The file path relative to script root 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: sc = await self._get_storage_class_for_org(artifact.organization_id) diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index 02196aea..55fe401c 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -29,8 +29,8 @@ from skyvern.forge.sdk.db.models import ( OrganizationModel, OutputParameterModel, PersistentBrowserSessionModel, - ProjectFileModel, - ProjectModel, + ScriptFileModel, + ScriptModel, StepModel, TaskGenerationModel, TaskModel, @@ -54,8 +54,8 @@ from skyvern.forge.sdk.db.utils import ( convert_to_organization, convert_to_organization_auth_token, convert_to_output_parameter, - convert_to_project, - convert_to_project_file, + convert_to_script, + convert_to_script_file, convert_to_step, convert_to_task, convert_to_workflow, @@ -102,8 +102,8 @@ from skyvern.forge.sdk.workflow.models.workflow import ( WorkflowRunStatus, WorkflowStatus, ) -from skyvern.schemas.projects import Project, ProjectFile 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.models import AgentStepOutput @@ -3546,134 +3546,134 @@ class AgentDB: return DebugSession.model_validate(debug_session) - async def create_project( + async def create_script( self, organization_id: str, run_id: str | None = None, - project_id: str | None = None, + script_id: str | None = None, version: int | None = None, - ) -> Project: + ) -> Script: try: async with self.Session() as session: - project = ProjectModel( + script = ScriptModel( organization_id=organization_id, run_id=run_id, ) - if project_id: - project.project_id = project_id + if script_id: + script.script_id = script_id if version: - project.version = version - session.add(project) + script.version = version + session.add(script) await session.commit() - await session.refresh(project) - return convert_to_project(project) + await session.refresh(script) + return convert_to_script(script) except SQLAlchemyError: LOG.error("SQLAlchemyError", exc_info=True) raise - async def update_project( + async def update_script( self, - project_revision_id: str, + script_revision_id: str, organization_id: str, artifact_id: str | None = None, run_id: str | None = None, version: int | None = None, - ) -> Project: + ) -> Script: try: async with self.Session() as session: - get_project_query = ( - select(ProjectModel) + get_script_query = ( + select(ScriptModel) .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: - project.artifact_id = artifact_id + script.artifact_id = artifact_id if run_id: - project.run_id = run_id + script.run_id = run_id if version: - project.version = version + script.version = version await session.commit() - await session.refresh(project) - return convert_to_project(project) + await session.refresh(script) + return convert_to_script(script) else: - raise NotFoundError("Project not found") + raise NotFoundError("Script not found") except SQLAlchemyError: LOG.error("SQLAlchemyError", exc_info=True) raise 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 except Exception: LOG.error("UnexpectedError", exc_info=True) raise - async def get_projects( + async def get_scripts( self, organization_id: str, page: int = 1, page_size: int = 10, - ) -> list[Project]: + ) -> list[Script]: try: async with self.Session() as session: # Calculate offset for pagination 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 = ( - 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(ProjectModel.deleted_at.is_(None)) - .group_by(ProjectModel.project_id) + .filter(ScriptModel.deleted_at.is_(None)) + .group_by(ScriptModel.script_id) .subquery() ) - # Main query to get projects with their latest versions - get_projects_query = ( - select(ProjectModel) + # Main query to get scripts with their latest versions + get_scripts_query = ( + select(ScriptModel) .join( latest_versions_subquery, and_( - ProjectModel.project_id == latest_versions_subquery.c.project_id, - ProjectModel.version == latest_versions_subquery.c.latest_version, + ScriptModel.script_id == latest_versions_subquery.c.script_id, + ScriptModel.version == latest_versions_subquery.c.latest_version, ), ) .filter_by(organization_id=organization_id) - .filter(ProjectModel.deleted_at.is_(None)) - .order_by(ProjectModel.created_at.desc()) + .filter(ScriptModel.deleted_at.is_(None)) + .order_by(ScriptModel.created_at.desc()) .limit(page_size) .offset(offset) ) - projects = (await session.scalars(get_projects_query)).all() - return [convert_to_project(project) for project in projects] + scripts = (await session.scalars(get_scripts_query)).all() + return [convert_to_script(script) for script in scripts] except SQLAlchemyError: LOG.error("SQLAlchemyError", exc_info=True) raise - async def get_project( + async def get_script( self, - project_id: str, + script_id: str, organization_id: str, version: int | None = None, - ) -> Project | None: - """Get a specific project by ID and optionally by version.""" + ) -> Script | None: + """Get a specific script by ID and optionally by version.""" try: async with self.Session() as session: - get_project_query = ( - select(ProjectModel) - .filter_by(project_id=project_id) + get_script_query = ( + select(ScriptModel) + .filter_by(script_id=script_id) .filter_by(organization_id=organization_id) - .filter(ProjectModel.deleted_at.is_(None)) + .filter(ScriptModel.deleted_at.is_(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: # 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(): - return convert_to_project(project) + if script := (await session.scalars(get_script_query)).first(): + return convert_to_script(script) return None except SQLAlchemyError: LOG.error("SQLAlchemyError", exc_info=True) @@ -3682,21 +3682,21 @@ class AgentDB: LOG.error("UnexpectedError", exc_info=True) 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: - project = ( + script = ( await session.scalars( - select(ProjectModel) - .filter_by(project_revision_id=project_revision_id) + select(ScriptModel) + .filter_by(script_revision_id=script_revision_id) .filter_by(organization_id=organization_id) ) ).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, - project_revision_id: str, - project_id: str, + script_revision_id: str, + script_id: str, organization_id: str, file_path: str, file_name: str, @@ -3707,12 +3707,12 @@ class AgentDB: encoding: str = "utf-8", artifact_id: str | None = None, ) -> None: - """Create a project file record.""" + """Create a script file record.""" try: async with self.Session() as session: - project_file = ProjectFileModel( - project_revision_id=project_revision_id, - project_id=project_id, + script_file = ScriptFileModel( + script_revision_id=script_revision_id, + script_id=script_id, organization_id=organization_id, file_path=file_path, file_name=file_name, @@ -3723,7 +3723,7 @@ class AgentDB: encoding=encoding, artifact_id=artifact_id, ) - session.add(project_file) + session.add(script_file) await session.commit() except SQLAlchemyError: LOG.error("SQLAlchemyError", exc_info=True) @@ -3732,13 +3732,13 @@ class AgentDB: LOG.error("UnexpectedError", exc_info=True) 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: - project_files = ( + script_files = ( await session.scalars( - select(ProjectFileModel) - .filter_by(project_revision_id=project_revision_id) + select(ScriptFileModel) + .filter_by(script_revision_id=script_revision_id) .filter_by(organization_id=organization_id) ) ).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] diff --git a/skyvern/forge/sdk/db/id.py b/skyvern/forge/sdk/db/id.py index 51a0e15a..a1dc1f63 100644 --- a/skyvern/forge/sdk/db/id.py +++ b/skyvern/forge/sdk/db/id.py @@ -45,9 +45,9 @@ ORGANIZATION_AUTH_TOKEN_PREFIX = "oat" ORG_PREFIX = "o" OUTPUT_PARAMETER_PREFIX = "op" PERSISTENT_BROWSER_SESSION_ID = "pbs" -PROJECT_FILE_PREFIX = "pf" -PROJECT_REVISION_PREFIX = "pv" -PROJECT_PREFIX = "p" +SCRIPT_FILE_PREFIX = "sf" +SCRIPT_REVISION_PREFIX = "sr" +SCRIPT_PREFIX = "s" STEP_PREFIX = "stp" TASK_GENERATION_PREFIX = "tg" TASK_PREFIX = "tsk" @@ -212,19 +212,19 @@ def generate_organization_bitwarden_collection_id() -> str: return f"{ORGANIZATION_BITWARDEN_COLLECTION_PREFIX}_{int_id}" -def generate_project_id() -> str: +def generate_script_id() -> str: 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() - 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() - return f"{PROJECT_FILE_PREFIX}_{int_id}" + return f"{SCRIPT_FILE_PREFIX}_{int_id}" ############# Helper functions below ############## diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index 99cce713..62ff0de8 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -35,9 +35,9 @@ from skyvern.forge.sdk.db.id import ( generate_organization_bitwarden_collection_id, generate_output_parameter_id, generate_persistent_browser_session_id, - generate_project_file_id, - generate_project_id, - generate_project_revision_id, + generate_script_file_id, + generate_script_id, + generate_script_revision_id, generate_step_id, generate_task_generation_id, generate_task_id, @@ -778,18 +778,18 @@ class DebugSessionModel(Base): status = Column(String, nullable=False, default="created") -class ProjectModel(Base): - __tablename__ = "projects" +class ScriptModel(Base): + __tablename__ = "scripts" __table_args__ = ( - Index("project_org_created_at_index", "organization_id", "created_at"), - Index("project_org_run_id_index", "organization_id", "run_id"), - UniqueConstraint("organization_id", "project_id", "version", name="uc_org_project_version"), + Index("script_org_created_at_index", "organization_id", "created_at"), + Index("script_org_run_id_index", "organization_id", "run_id"), + UniqueConstraint("organization_id", "script_id", "version", name="uc_org_script_version"), ) - project_revision_id = Column(String, primary_key=True, default=generate_project_revision_id) - project_id = Column(String, default=generate_project_id, nullable=False) # User-facing, consistent across versions + script_revision_id = Column(String, primary_key=True, default=generate_script_revision_id) + script_id = Column(String, default=generate_script_id, nullable=False) # User-facing, consistent across versions 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) version = Column(Integer, default=1, 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) -class ProjectFileModel(Base): - __tablename__ = "project_files" +class ScriptFileModel(Base): + __tablename__ = "script_files" __table_args__ = ( - Index("file_project_path_index", "project_revision_id", "file_path"), - UniqueConstraint("project_revision_id", "file_path", name="unique_project_file_path"), + Index("file_script_path_index", "script_revision_id", "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) - project_revision_id = Column(String, nullable=False) - project_id = Column(String, nullable=False) + file_id = Column(String, primary_key=True, default=generate_script_file_id) + script_revision_id = Column(String, nullable=False) + script_id = Column(String, nullable=False) organization_id = Column(String, nullable=False) file_path = Column(String, nullable=False) # e.g., "src/utils.py" diff --git a/skyvern/forge/sdk/db/utils.py b/skyvern/forge/sdk/db/utils.py index 15468995..d6dc100a 100644 --- a/skyvern/forge/sdk/db/utils.py +++ b/skyvern/forge/sdk/db/utils.py @@ -15,8 +15,8 @@ from skyvern.forge.sdk.db.models import ( OrganizationAuthTokenModel, OrganizationModel, OutputParameterModel, - ProjectFileModel, - ProjectModel, + ScriptFileModel, + ScriptModel, StepModel, TaskModel, WorkflowModel, @@ -50,8 +50,8 @@ from skyvern.forge.sdk.workflow.models.workflow import ( WorkflowRunStatus, WorkflowStatus, ) -from skyvern.schemas.projects import Project, ProjectFile from skyvern.schemas.runs import ProxyLocation +from skyvern.schemas.scripts import Script, ScriptFile from skyvern.webeye.actions.actions import ( Action, ActionType, @@ -506,35 +506,35 @@ def convert_to_workflow_run_block( return block -def convert_to_project(project_model: ProjectModel) -> Project: - return Project( - project_revision_id=project_model.project_revision_id, - project_id=project_model.project_id, - organization_id=project_model.organization_id, - run_id=project_model.run_id, - version=project_model.version, - created_at=project_model.created_at, - modified_at=project_model.modified_at, - deleted_at=project_model.deleted_at, +def convert_to_script(script_model: ScriptModel) -> Script: + return Script( + script_revision_id=script_model.script_revision_id, + script_id=script_model.script_id, + organization_id=script_model.organization_id, + run_id=script_model.run_id, + version=script_model.version, + created_at=script_model.created_at, + modified_at=script_model.modified_at, + deleted_at=script_model.deleted_at, ) -def convert_to_project_file(project_file_model: ProjectFileModel) -> ProjectFile: - return ProjectFile( - file_id=project_file_model.file_id, - project_revision_id=project_file_model.project_revision_id, - project_id=project_file_model.project_id, - organization_id=project_file_model.organization_id, - file_path=project_file_model.file_path, - file_name=project_file_model.file_name, - file_type=project_file_model.file_type, - content_hash=project_file_model.content_hash, - file_size=project_file_model.file_size, - mime_type=project_file_model.mime_type, - encoding=project_file_model.encoding, - artifact_id=project_file_model.artifact_id, - created_at=project_file_model.created_at, - modified_at=project_file_model.modified_at, +def convert_to_script_file(script_file_model: ScriptFileModel) -> ScriptFile: + return ScriptFile( + file_id=script_file_model.file_id, + script_revision_id=script_file_model.script_revision_id, + script_id=script_file_model.script_id, + organization_id=script_file_model.organization_id, + file_path=script_file_model.file_path, + file_name=script_file_model.file_name, + file_type=script_file_model.file_type, + content_hash=script_file_model.content_hash, + file_size=script_file_model.file_size, + mime_type=script_file_model.mime_type, + encoding=script_file_model.encoding, + artifact_id=script_file_model.artifact_id, + created_at=script_file_model.created_at, + modified_at=script_file_model.modified_at, ) diff --git a/skyvern/forge/sdk/executor/async_executor.py b/skyvern/forge/sdk/executor/async_executor.py index 55057f42..3ff41146 100644 --- a/skyvern/forge/sdk/executor/async_executor.py +++ b/skyvern/forge/sdk/executor/async_executor.py @@ -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.workflow.models.workflow import WorkflowRunStatus 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 LOG = structlog.get_logger() @@ -62,10 +62,10 @@ class AsyncExecutor(abc.ABC): pass @abc.abstractmethod - async def execute_project( + async def execute_script( self, request: Request | None, - project_id: str, + script_id: str, organization_id: str, background_tasks: BackgroundTasks | None, **kwargs: dict, @@ -209,18 +209,18 @@ class BackgroundTaskExecutor(AsyncExecutor): browser_session_id=browser_session_id, ) - async def execute_project( + async def execute_script( self, request: Request | None, - project_id: str, + script_id: str, organization_id: str, background_tasks: BackgroundTasks | None, **kwargs: dict, ) -> None: if background_tasks: background_tasks.add_task( - project_service.execute_project, - project_id=project_id, + script_service.execute_script, + script_id=script_id, organization_id=organization_id, background_tasks=background_tasks, ) diff --git a/skyvern/forge/sdk/routes/__init__.py b/skyvern/forge/sdk/routes/__init__.py index da8b1099..a164cd4d 100644 --- a/skyvern/forge/sdk/routes/__init__.py +++ b/skyvern/forge/sdk/routes/__init__.py @@ -1,9 +1,9 @@ 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 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 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_commands # noqa: F401 from skyvern.forge.sdk.routes import streaming_vnc # noqa: F401 diff --git a/skyvern/forge/sdk/routes/projects.py b/skyvern/forge/sdk/routes/projects.py deleted file mode 100644 index 0ec1dc8d..00000000 --- a/skyvern/forge/sdk/routes/projects.py +++ /dev/null @@ -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, - ) diff --git a/skyvern/forge/sdk/routes/scripts.py b/skyvern/forge/sdk/routes/scripts.py new file mode 100644 index 00000000..12c0236a --- /dev/null +++ b/skyvern/forge/sdk/routes/scripts.py @@ -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, + ) diff --git a/skyvern/schemas/projects.py b/skyvern/schemas/scripts.py similarity index 72% rename from skyvern/schemas/projects.py rename to skyvern/schemas/scripts.py index 518733a2..18513797 100644 --- a/skyvern/schemas/projects.py +++ b/skyvern/schemas/scripts.py @@ -14,10 +14,10 @@ class FileEncoding(StrEnum): UTF8 = "utf-8" -class ProjectFile(BaseModel): +class ScriptFile(BaseModel): file_id: str - project_revision_id: str - project_id: str + script_revision_id: str + script_id: str organization_id: str file_path: str # e.g., "src/utils.py" @@ -41,21 +41,21 @@ class ProjectFile(BaseModel): return self.content -class ProjectFileCreate(BaseModel): - """Model representing a file in a project.""" +class ScriptFileCreate(BaseModel): + """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") encoding: FileEncoding = Field(default=FileEncoding.BASE64, description="Content encoding") 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") run_id: str | None = Field(default=None, description="Associated run ID") - files: list[ProjectFileCreate] | None = Field( + files: list[ScriptFileCreate] | None = Field( default=None, - description="Array of files to include in the project", + description="Array of files to include in the script", examples=[ { "path": "main.py", @@ -84,12 +84,12 @@ class FileNode(BaseModel): children: dict[str, FileNode] | None = Field(default=None, description="Child nodes for directories") -class DeployProjectRequest(BaseModel): - """Request model for deploying a project with updated files.""" +class DeployScriptRequest(BaseModel): + """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=[ { "path": "src/main.py", @@ -101,27 +101,27 @@ class DeployProjectRequest(BaseModel): ) -class CreateProjectResponse(BaseModel): - project_id: str = Field(..., description="Unique project identifier", examples=["proj_abc123"]) - version: int = Field(..., description="Project version number", examples=[1]) +class CreateScriptResponse(BaseModel): + script_id: str = Field(..., description="Unique script identifier", examples=["s_abc123"]) + version: int = Field(..., description="Script version number", examples=[1]) 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") - 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) - project_revision_id: str = Field(description="Unique identifier for this specific project revision") - project_id: str = Field(description="User-facing project identifier, consistent across versions") - organization_id: str = Field(description="ID of the organization that owns this project") + script_revision_id: str = Field(description="Unique identifier for this specific script revision") + script_id: str = Field(description="User-facing script identifier, consistent across versions") + organization_id: str = Field(description="ID of the organization that owns this script") 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") - created_at: datetime = Field(description="Timestamp when the project was created") - modified_at: datetime = Field(description="Timestamp when the project was last modified") - deleted_at: datetime | None = Field(default=None, description="Timestamp when the project was soft deleted") + version: int = Field(description="Version number of the script") + created_at: datetime = Field(description="Timestamp when the script was created") + modified_at: datetime = Field(description="Timestamp when the script was last modified") + deleted_at: datetime | None = Field(default=None, description="Timestamp when the script was soft deleted") diff --git a/skyvern/services/project_service.py b/skyvern/services/script_service.py similarity index 67% rename from skyvern/services/project_service.py rename to skyvern/services/script_service.py index 09bf557f..322fb54e 100644 --- a/skyvern/services/project_service.py +++ b/skyvern/services/script_service.py @@ -7,19 +7,19 @@ from datetime import datetime import structlog from fastapi import BackgroundTasks -from skyvern.exceptions import ProjectNotFound +from skyvern.exceptions import ScriptNotFound from skyvern.forge import app -from skyvern.schemas.projects import FileNode, ProjectFileCreate +from skyvern.schemas.scripts import FileNode, ScriptFileCreate LOG = structlog.get_logger(__name__) async def build_file_tree( - files: list[ProjectFileCreate], + files: list[ScriptFileCreate], organization_id: str, - project_id: str, - project_version: int, - project_revision_id: str, + script_id: str, + script_version: int, + script_revision_id: str, ) -> dict[str, FileNode]: """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] = {} @@ -32,24 +32,24 @@ async def build_file_tree( # Create artifact and upload to S3 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, - project_id=project_id, - project_version=project_version, + script_id=script_id, + script_version=script_version, file_path=file.path, data=content_bytes, ) LOG.debug( - "Created project file artifact", + "Created script file artifact", artifact_id=artifact_id, file_path=file.path, - project_id=project_id, - project_version=project_version, + script_id=script_id, + script_version=script_version, ) - # create a project file record - await app.DATABASE.create_project_file( - project_revision_id=project_revision_id, - project_id=project_id, + # create a script file record + await app.DATABASE.create_script_file( + script_revision_id=script_revision_id, + script_id=script_id, organization_id=organization_id, file_path=file.path, file_name=file.path.split("/")[-1], @@ -61,11 +61,11 @@ async def build_file_tree( ) except Exception: LOG.exception( - "Failed to create project file artifact", + "Failed to create script file artifact", file_path=file.path, - project_id=project_id, - project_version=project_version, - project_revision_id=project_revision_id, + script_id=script_id, + script_version=script_version, + script_revision_id=script_revision_id, ) raise @@ -96,43 +96,43 @@ async def build_file_tree( return file_tree -async def execute_project( - project_id: str, +async def execute_script( + script_id: str, organization_id: str, background_tasks: BackgroundTasks | None = None, ) -> None: - # TODO: assume the project only has one ProjectFile called main.py - # step 1: get the project revision - # step 2: get the project files - # step 3: copy the project files to the local directory - # step 4: execute the project + # TODO: assume the script only has one ScriptFile called main.py + # step 1: get the script revision + # step 2: get the script files + # step 3: copy the script files to the local directory + # step 4: execute the script - # step 1: get the project revision - project = await app.DATABASE.get_project( - project_id=project_id, + # step 1: get the script revision + script = await app.DATABASE.get_script( + script_id=script_id, organization_id=organization_id, ) - if not project: - raise ProjectNotFound(project_id=project_id) + if not script: + raise ScriptNotFound(script_id=script_id) - # step 2: get the project files - project_files = await app.DATABASE.get_project_files( - project_revision_id=project.project_revision_id, organization_id=organization_id + # step 2: get the script files + script_files = await app.DATABASE.get_script_files( + script_revision_id=script.script_revision_id, organization_id=organization_id ) - # step 3: copy the project files to the local directory - for file in project_files: + # step 3: copy the script files to the local directory + for file in script_files: # retrieve the artifact if not file.artifact_id: continue artifact = await app.DATABASE.get_artifact_by_id(file.artifact_id, organization_id) 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 file_content = await app.ARTIFACT_MANAGER.retrieve_artifact(artifact) if not file_content: 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 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: f.write(file_content) - # step 4: execute the project + # step 4: execute the script if background_tasks: - background_tasks.add_task(subprocess.run, ["python", f"{project.project_id}/main.py"]) - LOG.info("Project executed successfully", project_id=project_id) + background_tasks.add_task(subprocess.run, ["python", f"{script.script_id}/main.py"]) + LOG.info("Script executed successfully", script_id=script_id)