diff --git a/agenta-backend/agenta_backend/migrations/postgres/data_migrations/applications.py b/agenta-backend/agenta_backend/migrations/postgres/data_migrations/applications.py new file mode 100644 index 000000000..56a842c47 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/postgres/data_migrations/applications.py @@ -0,0 +1,73 @@ +import os +import uuid +import traceback +from typing import Optional + + +import click +from sqlalchemy.future import select +from sqlalchemy import create_engine, delete +from sqlalchemy.orm import sessionmaker, Session + +from agenta_backend.models.deprecated_models import ( + DeprecatedEvaluatorConfigDB, + DeprecatedAppDB, +) + + +BATCH_SIZE = 1000 + + +def get_app_db(session: Session, app_id: str) -> Optional[DeprecatedAppDB]: + query = session.execute(select(DeprecatedAppDB).filter_by(id=uuid.UUID(app_id))) + return query.scalars().first() + + +def update_evaluators_with_app_name(): + engine = create_engine(os.getenv("POSTGRES_URI")) + sync_session = sessionmaker(engine, expire_on_commit=False) + + with sync_session() as session: + try: + offset = 0 + while True: + records = ( + session.execute( + select(DeprecatedEvaluatorConfigDB) + .filter(DeprecatedEvaluatorConfigDB.app_id.isnot(None)) + .offset(offset) + .limit(BATCH_SIZE) + ) + .scalars() + .all() + ) + if not records: + break + + # Update records with app_name as prefix + for record in records: + evaluator_config_app = get_app_db( + session=session, app_id=str(record.app_id) + ) + if record.app_id is not None and evaluator_config_app is not None: + record.name = f"{record.name} ({evaluator_config_app.app_name})" + + session.commit() + offset += BATCH_SIZE + + # Delete deprecated evaluator configs with app_id as None + session.execute( + delete(DeprecatedEvaluatorConfigDB).where( + DeprecatedEvaluatorConfigDB.app_id.is_(None) + ) + ) + session.commit() + except Exception as e: + session.rollback() + click.echo( + click.style( + f"ERROR updating evaluator config names: {traceback.format_exc()}", + fg="red", + ) + ) + raise e diff --git a/agenta-backend/agenta_backend/migrations/postgres/data_migrations/projects.py b/agenta-backend/agenta_backend/migrations/postgres/data_migrations/projects.py index 6e567099e..5ba0acc0f 100644 --- a/agenta-backend/agenta_backend/migrations/postgres/data_migrations/projects.py +++ b/agenta-backend/agenta_backend/migrations/postgres/data_migrations/projects.py @@ -1,13 +1,49 @@ import os import traceback +from typing import Sequence import click from sqlalchemy.future import select from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from agenta_backend.models.db_models import ProjectDB +from sqlalchemy.orm import sessionmaker, Session + +from agenta_backend.models.db_models import ( + ProjectDB, + AppDB, + AppVariantDB, + AppVariantRevisionsDB, + VariantBaseDB, + DeploymentDB, + ImageDB, + AppEnvironmentDB, + AppEnvironmentRevisionDB, + EvaluationScenarioDB, + EvaluationDB, + EvaluatorConfigDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + TestSetDB, +) + + +BATCH_SIZE = 1000 +MODELS = [ + AppDB, + AppVariantDB, + AppVariantRevisionsDB, + VariantBaseDB, + DeploymentDB, + ImageDB, + AppEnvironmentDB, + AppEnvironmentRevisionDB, + EvaluationScenarioDB, + EvaluationDB, + EvaluatorConfigDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + TestSetDB, +] def get_default_projects(session): @@ -15,6 +51,15 @@ def get_default_projects(session): return query.scalars().all() +def check_for_multiple_default_projects(session: Session) -> Sequence[ProjectDB]: + default_projects = get_default_projects(session) + if len(default_projects) > 1: + raise ValueError( + "Multiple default projects found. Please ensure only one exists." + ) + return default_projects + + def create_default_project(): PROJECT_NAME = "Default Project" engine = create_engine(os.getenv("POSTGRES_URI")) @@ -22,12 +67,7 @@ def create_default_project(): with sync_session() as session: try: - default_projects = get_default_projects(session) - if len(default_projects) > 1: - raise ValueError( - "Multiple default projects found. Please ensure only one exists." - ) - + default_projects = check_for_multiple_default_projects(session) if len(default_projects) == 0: new_project = ProjectDB(project_name=PROJECT_NAME, is_default=True) session.add(new_project) @@ -35,7 +75,12 @@ def create_default_project(): except Exception as e: session.rollback() - click.echo(click.style(f"ERROR: {traceback.format_exc()}", fg="red")) + click.echo( + click.style( + f"ERROR creating default project: {traceback.format_exc()}", + fg="red", + ) + ) raise e @@ -45,18 +90,13 @@ def remove_default_project(): with sync_session() as session: try: - default_projects = get_default_projects(session) + default_projects = check_for_multiple_default_projects(session) if len(default_projects) == 0: click.echo( click.style("No default project found to remove.", fg="yellow") ) return - if len(default_projects) > 1: - raise ValueError( - "Multiple default projects found. Please ensure only one exists." - ) - session.delete(default_projects[0]) session.commit() click.echo(click.style("Default project removed successfully.", fg="green")) @@ -65,3 +105,84 @@ def remove_default_project(): session.rollback() click.echo(click.style(f"ERROR: {traceback.format_exc()}", fg="red")) raise e + + +def add_project_id_to_db_entities(): + engine = create_engine(os.getenv("POSTGRES_URI")) + sync_session = sessionmaker(engine, expire_on_commit=False) + + with sync_session() as session: + try: + default_project = check_for_multiple_default_projects(session)[0] + for model in MODELS: + offset = 0 + while True: + records = ( + session.execute( + select(model) + .where(model.project_id == None) + .offset(offset) + .limit(BATCH_SIZE) + ) + .scalars() + .all() + ) + if not records: + break + + # Update records with default project_id + for record in records: + record.project_id = default_project.id + + session.commit() + offset += BATCH_SIZE + + except Exception as e: + session.rollback() + click.echo( + click.style( + f"ERROR adding project_id to db entities: {traceback.format_exc()}", + fg="red", + ) + ) + raise e + + +def remove_project_id_from_db_entities(): + engine = create_engine(os.getenv("POSTGRES_URI")) + sync_session = sessionmaker(engine, expire_on_commit=False) + + with sync_session() as session: + try: + for model in MODELS: + offset = 0 + while True: + records = ( + session.execute( + select(model) + .where(model.project_id != None) + .offset(offset) + .limit(BATCH_SIZE) + ) + .scalars() + .all() + ) + if not records: + break + + # Update records project_id column with None + for record in records: + record.project_id = None + + session.commit() + offset += BATCH_SIZE + + except Exception as e: + session.rollback() + click.echo( + click.style( + f"ERROR removing project_id to db entities: {traceback.format_exc()}", + fg="red", + ) + ) + raise e diff --git a/agenta-backend/agenta_backend/migrations/postgres/utils.py b/agenta-backend/agenta_backend/migrations/postgres/utils.py index 3666175a3..a327e32f5 100644 --- a/agenta-backend/agenta_backend/migrations/postgres/utils.py +++ b/agenta-backend/agenta_backend/migrations/postgres/utils.py @@ -5,13 +5,14 @@ import click import asyncpg + +from sqlalchemy import inspect, text, Engine from sqlalchemy.exc import ProgrammingError +from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine from alembic import command from alembic.config import Config -from sqlalchemy import inspect, text from alembic.script import ScriptDirectory -from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine from agenta_backend.utils.common import isCloudEE, isCloudDev @@ -173,3 +174,31 @@ async def check_if_templates_table_exist(): await engine.dispose() return True + + +def unique_constraint_exists( + engine: Engine, table_name: str, constraint_name: str +) -> bool: + """ + The function checks if a unique constraint with a specific name exists on a table in a PostgreSQL + database. + + Args: + - engine (Engine): instance of a database engine that represents a connection to a database. + - table_name (str): name of the table to check the existence of the unique constraint. + - constraint_name (str): name of the unique constraint to check for existence. + + Returns: + - returns a boolean value indicating whether a unique constraint with the specified `constraint_name` exists in the table. + """ + + with engine.connect() as conn: + result = conn.execute( + text( + f""" + SELECT conname FROM pg_constraint + WHERE conname = '{constraint_name}' AND conrelid = '{table_name}'::regclass; + """ + ) + ) + return result.fetchone() is not None diff --git a/agenta-backend/agenta_backend/migrations/postgres/versions/22d29365f5fc_update_evaluators_names_with_app_name_.py b/agenta-backend/agenta_backend/migrations/postgres/versions/22d29365f5fc_update_evaluators_names_with_app_name_.py new file mode 100644 index 000000000..f817b805d --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/postgres/versions/22d29365f5fc_update_evaluators_names_with_app_name_.py @@ -0,0 +1,32 @@ +"""Update evaluators names with app name as prefix + +Revision ID: 22d29365f5fc +Revises: 6cfe239894fb +Create Date: 2024-09-16 11:38:33.886908 + +""" + +from typing import Sequence, Union + +from agenta_backend.migrations.postgres.data_migrations.applications import ( + update_evaluators_with_app_name, +) + + +# revision identifiers, used by Alembic. +revision: str = "22d29365f5fc" +down_revision: Union[str, None] = "6cfe239894fb" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### custom command ### + update_evaluators_with_app_name() + # ### end custom command ### + + +def downgrade() -> None: + # ### custom command ### + pass + # ### end custom command ### diff --git a/agenta-backend/agenta_backend/migrations/postgres/versions/55bdd2e9a465_add_default_project_to_scoped_model_.py b/agenta-backend/agenta_backend/migrations/postgres/versions/55bdd2e9a465_add_default_project_to_scoped_model_.py new file mode 100644 index 000000000..3cb0054b1 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/postgres/versions/55bdd2e9a465_add_default_project_to_scoped_model_.py @@ -0,0 +1,36 @@ +"""add default project to scoped model entities + +Revision ID: 55bdd2e9a465 +Revises: c00a326c625a +Create Date: 2024-09-12 21:56:38.701088 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +from agenta_backend.migrations.postgres.data_migrations.projects import ( + add_project_id_to_db_entities, + remove_project_id_from_db_entities, +) + + +# revision identifiers, used by Alembic. +revision: str = "55bdd2e9a465" +down_revision: Union[str, None] = "c00a326c625a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### custom command ### + add_project_id_to_db_entities() + # ### end custom command ### + + +def downgrade() -> None: + # ### custom command ### + remove_project_id_from_db_entities() + # ### end custom command ### diff --git a/agenta-backend/agenta_backend/migrations/postgres/versions/6cfe239894fb_set_user_id_column_in_db_entities_to_be_.py b/agenta-backend/agenta_backend/migrations/postgres/versions/6cfe239894fb_set_user_id_column_in_db_entities_to_be_.py new file mode 100644 index 000000000..8f742b791 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/postgres/versions/6cfe239894fb_set_user_id_column_in_db_entities_to_be_.py @@ -0,0 +1,68 @@ +"""Set user_id column in db entities to be optional --- prep for project_id scoping + +Revision ID: 6cfe239894fb +Revises: 362gbs21a2ee +Create Date: 2024-09-12 15:25:29.462793 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "6cfe239894fb" +down_revision: Union[str, None] = "362gbs21a2ee" +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.alter_column("docker_images", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column("app_db", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column("deployments", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column("bases", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column("app_variants", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column("environments", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column("testsets", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column( + "evaluators_configs", "user_id", existing_type=sa.UUID, nullable=True + ) + op.alter_column( + "human_evaluations", "user_id", existing_type=sa.UUID, nullable=True + ) + op.alter_column( + "human_evaluations_scenarios", "user_id", existing_type=sa.UUID, nullable=True + ) + op.alter_column("evaluations", "user_id", existing_type=sa.UUID, nullable=True) + op.alter_column( + "evaluation_scenarios", "user_id", existing_type=sa.UUID, nullable=True + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("docker_images", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column("app_db", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column("deployments", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column("bases", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column("app_variants", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column("environments", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column("testsets", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column( + "evaluators_configs", "user_id", existing_type=sa.UUID, nullable=False + ) + op.alter_column( + "human_evaluations", "user_id", existing_type=sa.UUID, nullable=False + ) + op.alter_column( + "human_evaluations_scenarios", "user_id", existing_type=sa.UUID, nullable=False + ) + op.alter_column("evaluations", "user_id", existing_type=sa.UUID, nullable=False) + op.alter_column( + "evaluation_scenarios", "user_id", existing_type=sa.UUID, nullable=False + ) + # ### end Alembic commands ### diff --git a/agenta-backend/agenta_backend/migrations/postgres/versions/c00a326c625a_scope_project_id_to_db_models_entities.py b/agenta-backend/agenta_backend/migrations/postgres/versions/c00a326c625a_scope_project_id_to_db_models_entities.py new file mode 100644 index 000000000..0bf5ab761 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/postgres/versions/c00a326c625a_scope_project_id_to_db_models_entities.py @@ -0,0 +1,353 @@ +"""scope project_id to db models/entities + +Revision ID: c00a326c625a +Revises: 22d29365f5fc +Create Date: 2024-09-12 20:34:16.175845 + +""" + +import os +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +from agenta_backend.migrations.postgres import utils + + +# revision identifiers, used by Alembic. +revision: str = "c00a326c625a" +down_revision: Union[str, None] = "22d29365f5fc" +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! ### + engine = sa.create_engine(os.getenv("POSTGRES_URI")) + op.add_column("app_db", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint("app_db_user_id_fkey", "app_db", type_="foreignkey") + op.create_foreign_key( + "app_db_projects_fkey", + "app_db", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("app_db", "user_id") + op.add_column( + "app_variant_revisions", sa.Column("project_id", sa.UUID(), nullable=True) + ) + op.create_foreign_key( + "app_variant_revisions_projects_fkey", + "app_variant_revisions", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.add_column("app_variants", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint("app_variants_user_id_fkey", "app_variants", type_="foreignkey") + op.create_foreign_key( + "app_variants_projects_fkey", + "app_variants", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("app_variants", "user_id") + op.add_column("bases", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint("bases_app_id_fkey", "bases", type_="foreignkey") + op.drop_constraint("bases_user_id_fkey", "bases", type_="foreignkey") + op.create_foreign_key( + "bases_projects_fkey", + "bases", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("bases", "user_id") + op.add_column("deployments", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint("deployments_user_id_fkey", "deployments", type_="foreignkey") + op.create_foreign_key( + "deployments_projects_fkey", + "deployments", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("deployments", "user_id") + op.add_column("docker_images", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint( + "docker_images_user_id_fkey", "docker_images", type_="foreignkey" + ) + op.create_foreign_key( + "docker_images_projects_fkey", + "docker_images", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("docker_images", "user_id") + op.add_column("environments", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint("environments_user_id_fkey", "environments", type_="foreignkey") + op.create_foreign_key( + "environments_projects_fkey", + "environments", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("environments", "user_id") + op.add_column( + "environments_revisions", sa.Column("project_id", sa.UUID(), nullable=True) + ) + op.create_foreign_key( + "environments_revisions_projects_fkey", + "environments_revisions", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.add_column( + "evaluation_scenarios", sa.Column("project_id", sa.UUID(), nullable=True) + ) + op.drop_constraint( + "evaluation_scenarios_user_id_fkey", "evaluation_scenarios", type_="foreignkey" + ) + op.create_foreign_key( + "evaluation_scenarios_projects_fkey", + "evaluation_scenarios", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("evaluation_scenarios", "user_id") + op.add_column("evaluations", sa.Column("project_id", sa.UUID(), nullable=True)) + op.drop_constraint("evaluations_user_id_fkey", "evaluations", type_="foreignkey") + op.create_foreign_key( + "evaluations_projects_fkey", + "evaluations", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("evaluations", "user_id") + op.add_column( + "evaluators_configs", sa.Column("project_id", sa.UUID(), nullable=True) + ) + op.drop_constraint( + "evaluators_configs_user_id_fkey", "evaluators_configs", type_="foreignkey" + ) + op.drop_constraint( + "evaluators_configs_app_id_fkey", "evaluators_configs", type_="foreignkey" + ) + op.create_foreign_key( + "evaluators_configs_projects_fkey", + "evaluators_configs", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("evaluators_configs", "app_id") + op.drop_column("evaluators_configs", "user_id") + op.add_column( + "human_evaluations", sa.Column("project_id", sa.UUID(), nullable=True) + ) + op.drop_constraint( + "human_evaluations_user_id_fkey", "human_evaluations", type_="foreignkey" + ) + op.create_foreign_key( + "human_evaluations_projects_fkey", + "human_evaluations", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("human_evaluations", "user_id") + op.add_column( + "human_evaluations_scenarios", sa.Column("project_id", sa.UUID(), nullable=True) + ) + op.drop_constraint( + "human_evaluations_scenarios_user_id_fkey", + "human_evaluations_scenarios", + type_="foreignkey", + ) + op.create_foreign_key( + "human_evaluations_scenarios_projects_fkey", + "human_evaluations_scenarios", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("human_evaluations_scenarios", "user_id") + op.alter_column("projects", "is_default", existing_type=sa.BOOLEAN(), nullable=True) + op.add_column("testsets", sa.Column("project_id", sa.UUID(), nullable=True)) + if not utils.unique_constraint_exists(engine, "testsets", "testsets_user_id_fkey"): + op.drop_constraint("testsets_user_id_fkey", "testsets", type_="foreignkey") + op.drop_constraint("testsets_app_id_fkey", "testsets", type_="foreignkey") + + op.create_foreign_key( + "testsets_projects_fkey", + "testsets", + "projects", + ["project_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_column("testsets", "app_id") + op.drop_column("testsets", "user_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "testsets", sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True) + ) + op.add_column( + "testsets", sa.Column("app_id", sa.UUID(), autoincrement=False, nullable=True) + ) + op.create_foreign_key( + "testsets_app_id_fkey", + "testsets", + "app_db", + ["app_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + "testsets_user_id_fkey", "testsets", "users", ["user_id"], ["id"] + ) + op.drop_column("testsets", "project_id") + op.alter_column( + "projects", "is_default", existing_type=sa.BOOLEAN(), nullable=False + ) + op.add_column( + "human_evaluations_scenarios", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "human_evaluations_scenarios_user_id_fkey", + "human_evaluations_scenarios", + "users", + ["user_id"], + ["id"], + ) + op.drop_column("human_evaluations_scenarios", "project_id") + op.add_column( + "human_evaluations", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "human_evaluations_user_id_fkey", + "human_evaluations", + "users", + ["user_id"], + ["id"], + ) + op.drop_column("human_evaluations", "project_id") + op.add_column( + "evaluators_configs", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.add_column( + "evaluators_configs", + sa.Column("app_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "evaluators_configs_app_id_fkey", + "evaluators_configs", + "app_db", + ["app_id"], + ["id"], + ondelete="SET NULL", + ) + op.create_foreign_key( + "evaluators_configs_user_id_fkey", + "evaluators_configs", + "users", + ["user_id"], + ["id"], + ) + op.drop_column("evaluators_configs", "project_id") + op.add_column( + "evaluations", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "evaluations_user_id_fkey", "evaluations", "users", ["user_id"], ["id"] + ) + op.drop_column("evaluations", "project_id") + op.add_column( + "evaluation_scenarios", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "evaluation_scenarios_user_id_fkey", + "evaluation_scenarios", + "users", + ["user_id"], + ["id"], + ) + op.drop_column("evaluation_scenarios", "project_id") + op.drop_column("environments_revisions", "project_id") + op.add_column( + "environments", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "environments_user_id_fkey", "environments", "users", ["user_id"], ["id"] + ) + op.drop_column("environments", "project_id") + op.add_column( + "docker_images", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "docker_images_user_id_fkey", "docker_images", "users", ["user_id"], ["id"] + ) + op.drop_column("docker_images", "project_id") + op.add_column( + "deployments", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "deployments_user_id_fkey", "deployments", "users", ["user_id"], ["id"] + ) + op.drop_column("deployments", "project_id") + op.add_column( + "bases", sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True) + ) + op.create_foreign_key("bases_user_id_fkey", "bases", "users", ["user_id"], ["id"]) + op.create_foreign_key( + "bases_app_id_fkey", "bases", "app_db", ["app_id"], ["id"], ondelete="CASCADE" + ) + op.drop_column("bases", "project_id") + op.add_column( + "app_variants", + sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True), + ) + op.create_foreign_key( + "app_variants_user_id_fkey", "app_variants", "users", ["user_id"], ["id"] + ) + op.drop_column("app_variants", "project_id") + op.drop_column("app_variant_revisions", "project_id") + op.add_column( + "app_db", sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=True) + ) + op.create_foreign_key("app_db_user_id_fkey", "app_db", "users", ["user_id"], ["id"]) + op.drop_column("app_db", "project_id") + # ### end Alembic commands ### diff --git a/agenta-backend/agenta_backend/models/api/api_models.py b/agenta-backend/agenta_backend/models/api/api_models.py index 353ab3e47..472734c9c 100644 --- a/agenta-backend/agenta_backend/models/api/api_models.py +++ b/agenta-backend/agenta_backend/models/api/api_models.py @@ -58,6 +58,8 @@ class VariantAction(BaseModel): class CreateApp(BaseModel): app_name: str + project_id: Optional[str] = None + workspace_id: Optional[str] = None class CreateAppOutput(BaseModel): @@ -84,6 +86,7 @@ class UpdateVariantParameterPayload(BaseModel): class AppVariant(BaseModel): app_id: str app_name: str + project_id: Optional[str] = None variant_name: str parameters: Optional[Dict[str, Any]] previous_variant_name: Optional[str] @@ -100,8 +103,8 @@ class AppVariantResponse(BaseModel): app_name: str variant_id: str variant_name: str + project_id: str parameters: Optional[Dict[str, Any]] - user_id: str base_name: str base_id: str config_name: str @@ -138,6 +141,7 @@ class AppVariantOutputExtended(BaseModel): class EnvironmentOutput(BaseModel): name: str app_id: str + project_id: str deployed_app_variant_id: Optional[str] deployed_variant_name: Optional[str] deployed_app_variant_revision_id: Optional[str] @@ -197,6 +201,7 @@ class AddVariantFromImagePayload(BaseModel): class ImageExtended(Image): # includes the mongodb image id id: str + project_id: Optional[str] = None class TemplateImageInfo(BaseModel): @@ -236,6 +241,8 @@ class DockerEnvVars(BaseModel): class CreateAppVariant(BaseModel): app_name: str template_id: str + project_id: Optional[str] = None + workspace_id: Optional[str] = None env_vars: Dict[str, str] diff --git a/agenta-backend/agenta_backend/models/api/evaluation_model.py b/agenta-backend/agenta_backend/models/api/evaluation_model.py index f75194581..0f2b1b364 100644 --- a/agenta-backend/agenta_backend/models/api/evaluation_model.py +++ b/agenta-backend/agenta_backend/models/api/evaluation_model.py @@ -21,6 +21,7 @@ class Evaluator(BaseModel): class EvaluatorConfig(BaseModel): id: str name: str + project_id: str evaluator_key: str settings_values: Optional[Dict[str, Any]] = None created_at: str @@ -67,8 +68,7 @@ class AppOutput(BaseModel): class Evaluation(BaseModel): id: str app_id: str - user_id: str - user_username: str + project_id: str variant_ids: List[str] variant_names: List[str] variant_revision_ids: List[str] @@ -145,8 +145,7 @@ class HumanEvaluationScenarioOutput(BaseModel): class HumanEvaluation(BaseModel): id: str app_id: str - user_id: str - user_username: str + project_id: str evaluation_type: str variant_ids: List[str] variant_names: List[str] diff --git a/agenta-backend/agenta_backend/models/converters.py b/agenta-backend/agenta_backend/models/converters.py index 41b3ae0ca..180d3bbd2 100644 --- a/agenta-backend/agenta_backend/models/converters.py +++ b/agenta-backend/agenta_backend/models/converters.py @@ -107,6 +107,7 @@ async def human_evaluation_db_to_simple_evaluation_output( return SimpleEvaluationOutput( id=str(human_evaluation_db.id), app_id=str(human_evaluation_db.app_id), + project_id=str(human_evaluation_db.project_id), status=human_evaluation_db.status, # type: ignore evaluation_type=human_evaluation_db.evaluation_type, # type: ignore variant_ids=[ @@ -131,8 +132,7 @@ async def evaluation_db_to_pydantic( return Evaluation( id=str(evaluation_db.id), app_id=str(evaluation_db.app_id), - user_id=str(evaluation_db.user_id), - user_username=evaluation_db.user.username or "", + project_id=str(evaluation_db.project_id), status=evaluation_db.status, variant_ids=[str(evaluation_db.variant_id)], variant_revision_ids=[str(evaluation_db.variant_revision_id)], @@ -179,8 +179,7 @@ async def human_evaluation_db_to_pydantic( return HumanEvaluation( id=str(evaluation_db.id), app_id=str(evaluation_db.app_id), - user_id=str(evaluation_db.user_id), - user_username=evaluation_db.user.username or "", + project_id=str(evaluation_db.project_id), status=evaluation_db.status, # type: ignore evaluation_type=evaluation_db.evaluation_type, # type: ignore variant_ids=variants_ids, @@ -275,17 +274,13 @@ def app_variant_db_to_pydantic( app_variant = AppVariant( app_id=str(app_variant_db.app.id), app_name=app_variant_db.app.app_name, + project_id=str(app_variant_db.project_id), variant_name=app_variant_db.variant_name, parameters=app_variant_db.config.parameters, previous_variant_name=app_variant_db.previous_variant_name, base_name=app_variant_db.base_name, config_name=app_variant_db.config_name, ) - - if isCloudEE(): - app_variant.organization_id = str(app_variant_db.organization_id) - app_variant.workspace_id = str(app_variant_db.workspace_id) - return app_variant @@ -301,9 +296,9 @@ async def app_variant_db_to_output(app_variant_db: AppVariantDB) -> AppVariantRe variant_response = AppVariantResponse( app_id=str(app_variant_db.app_id), app_name=str(app_variant_db.app.app_name), + project_id=str(app_variant_db.project_id), variant_name=app_variant_db.variant_name, # type: ignore variant_id=str(app_variant_db.id), - user_id=str(app_variant_db.user_id), parameters=app_variant_db.config_parameters, # type: ignore base_name=app_variant_db.base_name, # type: ignore base_id=str(app_variant_db.base_id), @@ -314,11 +309,6 @@ async def app_variant_db_to_output(app_variant_db: AppVariantDB) -> AppVariantRe updated_at=str(app_variant_db.created_at), modified_by_id=str(app_variant_db.modified_by_id), ) - - if isCloudEE(): - variant_response.organization_id = str(app_variant_db.organization_id) - variant_response.workspace_id = str(app_variant_db.workspace_id) - return variant_response @@ -367,7 +357,7 @@ async def environment_db_to_output( ) if deployed_app_variant_id: deployed_app_variant = await db_manager.get_app_variant_instance_by_id( - deployed_app_variant_id + deployed_app_variant_id, str(environment_db.project_id) ) deployed_variant_name = deployed_app_variant.variant_name revision = deployed_app_variant.revision @@ -378,6 +368,7 @@ async def environment_db_to_output( environment_output = EnvironmentOutput( name=environment_db.name, app_id=str(environment_db.app_id), + project_id=str(environment_db.project_id), deployed_app_variant_id=deployed_app_variant_id, deployed_variant_name=deployed_variant_name, deployed_app_variant_revision_id=str( @@ -385,10 +376,6 @@ async def environment_db_to_output( ), revision=revision, ) - - if isCloudEE(): - environment_output.organization_id = str(environment_db.organization_id) - environment_output.workspace_id = str(environment_db.workspace_id) return environment_output @@ -403,7 +390,7 @@ async def environment_db_and_revision_to_extended_output( ) if deployed_app_variant_id: deployed_app_variant = await db_manager.get_app_variant_instance_by_id( - deployed_app_variant_id + deployed_app_variant_id, str(environment_db.project_id) ) deployed_variant_name = deployed_app_variant.variant_name else: @@ -434,12 +421,6 @@ async def environment_db_and_revision_to_extended_output( revision=environment_db.revision, revisions=app_environment_revisions, ) - - if isCloudEE(): - environment_output_extended.organization_id = str( - environment_db.organization_id - ) - environment_output_extended.workspace_id = str(environment_db.workspace_id) return environment_output_extended @@ -458,14 +439,10 @@ def app_db_to_pydantic(app_db: AppDB) -> App: def image_db_to_pydantic(image_db: ImageDB) -> ImageExtended: image = ImageExtended( docker_id=image_db.docker_id, + project_id=str(image_db.project_id), tags=image_db.tags, id=str(image_db.id), ) - - if isCloudEE(): - image.organization_id = str(image_db.organization_id) - image.workspace_id = str(image_db.workspace_id) - return image @@ -513,12 +490,13 @@ def user_db_to_pydantic(user_db: UserDB) -> User: uid=user_db.uid, username=user_db.username, email=user_db.email, - ).dict(exclude_unset=True) + ).model_dump(exclude_unset=True) def evaluator_config_db_to_pydantic(evaluator_config: EvaluatorConfigDB): return EvaluatorConfig( id=str(evaluator_config.id), + project_id=str(evaluator_config.project_id), name=evaluator_config.name, evaluator_key=evaluator_config.evaluator_key, settings_values=evaluator_config.settings_values, diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index 2508a027f..339f554eb 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -18,6 +18,9 @@ from agenta_backend.models.shared_models import TemplateType +CASCADE_ALL_DELETE = "all, delete-orphan" + + class UserDB(Base): __tablename__ = "users" @@ -58,6 +61,13 @@ class ProjectDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) + image = relationship("ImageDB", cascade=CASCADE_ALL_DELETE, backref="project") + app = relationship("AppDB", cascade=CASCADE_ALL_DELETE, backref="project") + evaluator_config = relationship( + "EvaluatorConfigDB", cascade=CASCADE_ALL_DELETE, backref="project" + ) + testset = relationship("TestSetDB", cascade=CASCADE_ALL_DELETE, backref="project") + class ImageDB(Base): __tablename__ = "docker_images" @@ -74,8 +84,9 @@ class ImageDB(Base): docker_id = Column(String, nullable=True, index=True) tags = Column(String, nullable=True) deletable = Column(Boolean, default=True) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) - user = relationship("UserDB") + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) @@ -95,7 +106,9 @@ class AppDB(Base): nullable=False, ) app_name = Column(String) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) modified_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) @@ -104,23 +117,16 @@ class AppDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB", foreign_keys=[user_id]) modified_by = relationship("UserDB", foreign_keys=[modified_by_id]) variant = relationship( - "AppVariantDB", cascade="all, delete-orphan", back_populates="app" + "AppVariantDB", cascade=CASCADE_ALL_DELETE, back_populates="app" ) - testset = relationship("TestSetDB", cascade="all, delete-orphan", backref="app") deployment = relationship( - "DeploymentDB", cascade="all, delete-orphan", back_populates="app" - ) - base = relationship( - "VariantBaseDB", cascade="all, delete-orphan", back_populates="app" - ) - evaluation = relationship( - "EvaluationDB", cascade="all, delete-orphan", backref="app" + "DeploymentDB", cascade=CASCADE_ALL_DELETE, back_populates="app" ) + evaluation = relationship("EvaluationDB", cascade=CASCADE_ALL_DELETE, backref="app") human_evaluation = relationship( - "HumanEvaluationDB", cascade="all, delete-orphan", backref="app" + "HumanEvaluationDB", cascade=CASCADE_ALL_DELETE, backref="app" ) @@ -135,7 +141,9 @@ class DeploymentDB(Base): nullable=False, ) app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="CASCADE")) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) container_name = Column(String) container_id = Column(String) uri = Column(String) @@ -147,7 +155,7 @@ class DeploymentDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") + project = relationship("ProjectDB") app = relationship("AppDB", back_populates="deployment") @@ -162,7 +170,9 @@ class VariantBaseDB(Base): nullable=False, ) app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="CASCADE")) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) base_name = Column(String) image_id = Column( UUID(as_uuid=True), ForeignKey("docker_images.id", ondelete="SET NULL") @@ -177,10 +187,10 @@ class VariantBaseDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") + app = relationship("AppDB") image = relationship("ImageDB") deployment = relationship("DeploymentDB") - app = relationship("AppDB", back_populates="base") + project = relationship("ProjectDB") class AppVariantDB(Base): @@ -201,7 +211,9 @@ class AppVariantDB(Base): ForeignKey("docker_images.id", ondelete="SET NULL"), nullable=True, ) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) modified_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) base_name = Column(String) base_id = Column(UUID(as_uuid=True), ForeignKey("bases.id")) @@ -218,12 +230,12 @@ class AppVariantDB(Base): image = relationship("ImageDB") app = relationship("AppDB", back_populates="variant") - user = relationship("UserDB", foreign_keys=[user_id]) + project = relationship("ProjectDB") modified_by = relationship("UserDB", foreign_keys=[modified_by_id]) base = relationship("VariantBaseDB") variant_revision = relationship( "AppVariantRevisionsDB", - cascade="all, delete-orphan", + cascade=CASCADE_ALL_DELETE, backref="variant_revision", ) @@ -242,6 +254,9 @@ class AppVariantRevisionsDB(Base): UUID(as_uuid=True), ForeignKey("app_variants.id", ondelete="CASCADE") ) revision = Column(Integer) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) modified_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) base_id = Column(UUID(as_uuid=True), ForeignKey("bases.id")) config_name = Column(String, nullable=False) @@ -255,6 +270,7 @@ class AppVariantRevisionsDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) + project = relationship("ProjectDB") modified_by = relationship("UserDB") base = relationship("VariantBaseDB") @@ -274,7 +290,9 @@ class AppEnvironmentDB(Base): ) app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="CASCADE")) name = Column(String) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) revision = Column(Integer) deployed_app_variant_id = Column( UUID(as_uuid=True), ForeignKey("app_variants.id", ondelete="SET NULL") @@ -289,9 +307,9 @@ class AppEnvironmentDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") + project = relationship("ProjectDB") environment_revisions = relationship( - "AppEnvironmentRevisionDB", cascade="all, delete-orphan", backref="environment" + "AppEnvironmentRevisionDB", cascade=CASCADE_ALL_DELETE, backref="environment" ) deployed_app_variant = relationship("AppVariantDB") deployed_app_variant_revision = relationship("AppVariantRevisionsDB") @@ -310,6 +328,9 @@ class AppEnvironmentRevisionDB(Base): environment_id = Column( UUID(as_uuid=True), ForeignKey("environments.id", ondelete="CASCADE") ) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) revision = Column(Integer) modified_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) deployed_app_variant_revision_id = Column( @@ -322,6 +343,7 @@ class AppEnvironmentRevisionDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) + project = relationship("ProjectDB") modified_by = relationship("UserDB") @@ -360,9 +382,10 @@ class TestSetDB(Base): nullable=False, ) name = Column(String) - app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="CASCADE")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) csvdata = Column(mutable_json_type(dbtype=JSONB, nested=True)) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) @@ -370,8 +393,6 @@ class TestSetDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") - class EvaluatorConfigDB(Base): __tablename__ = "evaluators_configs" @@ -384,8 +405,9 @@ class EvaluatorConfigDB(Base): nullable=False, ) - app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="SET NULL")) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) name = Column(String) evaluator_key = Column(String) settings_values = Column(mutable_json_type(dbtype=JSONB, nested=True), default=dict) @@ -396,8 +418,6 @@ class EvaluatorConfigDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") - class HumanEvaluationVariantDB(Base): __tablename__ = "human_evaluation_variants" @@ -436,7 +456,9 @@ class HumanEvaluationDB(Base): nullable=False, ) app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="CASCADE")) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) status = Column(String) evaluation_type = Column(String) testset_id = Column(UUID(as_uuid=True), ForeignKey("testsets.id")) @@ -447,16 +469,15 @@ class HumanEvaluationDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") testset = relationship("TestSetDB") evaluation_variant = relationship( "HumanEvaluationVariantDB", - cascade="all, delete-orphan", + cascade=CASCADE_ALL_DELETE, backref="human_evaluation", ) evaluation_scenario = relationship( "HumanEvaluationScenarioDB", - cascade="all, delete-orphan", + cascade=CASCADE_ALL_DELETE, backref="evaluation_scenario", ) @@ -471,7 +492,9 @@ class HumanEvaluationScenarioDB(Base): unique=True, nullable=False, ) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) evaluation_id = Column( UUID(as_uuid=True), ForeignKey("human_evaluations.id", ondelete="CASCADE") ) @@ -545,7 +568,9 @@ class EvaluationDB(Base): nullable=False, ) app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="CASCADE")) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) status = Column(mutable_json_type(dbtype=JSONB, nested=True)) # Result testset_id = Column( UUID(as_uuid=True), ForeignKey("testsets.id", ondelete="SET NULL") @@ -566,21 +591,21 @@ class EvaluationDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") + project = relationship("ProjectDB") testset = relationship("TestSetDB") variant = relationship("AppVariantDB") variant_revision = relationship("AppVariantRevisionsDB") aggregated_results = relationship( "EvaluationAggregatedResultDB", - cascade="all, delete-orphan", + cascade=CASCADE_ALL_DELETE, backref="evaluation", ) evaluation_scenarios = relationship( - "EvaluationScenarioDB", cascade="all, delete-orphan", backref="evaluation" + "EvaluationScenarioDB", cascade=CASCADE_ALL_DELETE, backref="evaluation" ) evaluator_configs = relationship( "EvaluationEvaluatorConfigDB", - cascade="all, delete-orphan", + cascade=CASCADE_ALL_DELETE, backref="evaluation", ) @@ -617,7 +642,9 @@ class EvaluationScenarioDB(Base): unique=True, nullable=False, ) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + project_id = Column( + UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE") + ) evaluation_id = Column( UUID(as_uuid=True), ForeignKey("evaluations.id", ondelete="CASCADE") ) @@ -644,11 +671,11 @@ class EvaluationScenarioDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") + project = relationship("ProjectDB") variant = relationship("AppVariantDB") results = relationship( "EvaluationScenarioResultDB", - cascade="all, delete-orphan", + cascade=CASCADE_ALL_DELETE, backref="evaluation_scenario", ) diff --git a/agenta-backend/agenta_backend/models/deprecated_models.py b/agenta-backend/agenta_backend/models/deprecated_models.py new file mode 100644 index 000000000..4bd4373b4 --- /dev/null +++ b/agenta-backend/agenta_backend/models/deprecated_models.py @@ -0,0 +1,61 @@ +from datetime import datetime, timezone + +import uuid_utils.compat as uuid +from sqlalchemy import ( + Column, + String, + DateTime, + ForeignKey, +) +from sqlalchemy_json import mutable_json_type +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.ext.declarative import declarative_base + + +DeprecatedBase = declarative_base() + + +class DeprecatedAppDB(DeprecatedBase): + __tablename__ = "app_db" + __table_args__ = {"extend_existing": True} + + id = Column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid7, + unique=True, + nullable=False, + ) + app_name = Column(String) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) + modified_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + created_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + updated_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + + +class DeprecatedEvaluatorConfigDB(DeprecatedBase): + __tablename__ = "evaluators_configs" + __table_args__ = {"extend_existing": True} + + id = Column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid7, + unique=True, + nullable=False, + ) + + app_id = Column(UUID(as_uuid=True), ForeignKey("app_db.id", ondelete="SET NULL")) + name = Column(String) + evaluator_key = Column(String) + settings_values = Column(mutable_json_type(dbtype=JSONB, nested=True), default=dict) + created_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + updated_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) diff --git a/agenta-backend/agenta_backend/routers/app_router.py b/agenta-backend/agenta_backend/routers/app_router.py index bede0cb24..ae89ecae0 100644 --- a/agenta-backend/agenta_backend/routers/app_router.py +++ b/agenta-backend/agenta_backend/routers/app_router.py @@ -102,11 +102,11 @@ async def list_app_variants( List[AppVariantResponse]: A list of app variants for the given app ID. """ try: + app = await db_manager.get_app_instance_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=str(app.project_id), permission=Permission.VIEW_APPLICATION, ) logger.debug(f"User has Permission to list app variants: {has_permission}") @@ -155,11 +155,11 @@ async def get_variant_by_env( AppVariantResponse: The retrieved app variant. """ try: + app = await db_manager.get_app_instance_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=str(app.project_id), permission=Permission.VIEW_APPLICATION, ) logger.debug( @@ -229,37 +229,9 @@ async def create_app( detail="Failed to get user org and workspace data", ) - if payload.organization_id: - organization_id = payload.organization_id - organization = await db_manager_ee.get_organization(organization_id) - else: - organization = await get_user_own_org( - user_org_workspace_data["uid"] - ) - organization_id = str(organization.id) - - if not organization: - raise HTTPException( - status_code=400, - detail="User Organization not found", - ) - - if payload.workspace_id: - workspace_id = payload.workspace_id - workspace = db_manager_ee.get_workspace(workspace_id) - else: - workspace = await get_org_default_workspace(organization) - - if not workspace: - raise HTTPException( - status_code=400, - detail="User Organization not found", - ) - has_permission = await check_rbac_permission( user_org_workspace_data=user_org_workspace_data, - workspace_id=str(workspace.id), - organization=organization, + project_id=payload.project_id or request.state.project_id, permission=Permission.CREATE_APPLICATION, ) logger.debug( @@ -276,9 +248,8 @@ async def create_app( app_db = await db_manager.create_app_and_envs( payload.app_name, - request.state.user_id, - organization_id if isCloudEE() else None, - str(workspace.id) if isCloudEE() else None, + project_id=payload.project_id or request.state.project_id, + workspace_id=payload.workspace_id, ) return CreateAppOutput(app_id=str(app_db.id), app_name=str(app_db.app_name)) except Exception as e: @@ -312,7 +283,7 @@ async def update_app( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.EDIT_APPLICATION, ) logger.debug(f"User has Permission to update app: {has_permission}") @@ -335,7 +306,6 @@ async def update_app( async def list_apps( request: Request, app_name: Optional[str] = None, - org_id: Optional[str] = None, workspace_id: Optional[str] = None, ) -> List[App]: """ @@ -354,9 +324,9 @@ async def list_apps( """ try: apps = await db_manager.list_apps( + project_id=request.state.project_id, user_uid=request.state.user_id, app_name=app_name, - org_id=org_id, workspace_id=workspace_id, ) return apps @@ -402,11 +372,10 @@ async def add_variant_from_image( try: app = await db_manager.fetch_app_by_id(app_id) - if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.CREATE_APPLICATION, ) logger.debug( @@ -421,6 +390,7 @@ async def add_variant_from_image( variant_db = await app_manager.add_variant_based_on_image( app=app, + project_id=str(app.project_id), variant_name=payload.variant_name, docker_id_or_template_uri=payload.docker_id, tags=payload.tags, @@ -432,7 +402,9 @@ async def add_variant_from_image( app_variant_db = await db_manager.fetch_app_variant_by_id(str(variant_db.id)) logger.debug("Step 8: We create ready-to use evaluators") - await evaluator_manager.create_ready_to_use_evaluators(app=app) + await evaluator_manager.create_ready_to_use_evaluators( + app_name=app.app_name, project_id=str(app.project_id) + ) return await converters.app_variant_db_to_output(app_variant_db) except Exception as e: @@ -441,7 +413,10 @@ async def add_variant_from_image( @router.delete("/{app_id}/", operation_id="remove_app") -async def remove_app(app_id: str, request: Request): +async def remove_app( + app_id: str, + request: Request, +): """Remove app, all its variant, containers and images Arguments: @@ -449,11 +424,10 @@ async def remove_app(app_id: str, request: Request): """ try: app = await db_manager.fetch_app_by_id(app_id) - if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.DELETE_APPLICATION, ) logger.debug(f"User has Permission to delete app: {has_permission}") @@ -506,19 +480,19 @@ async def create_app_and_variant_from_template( request.state.user_id ) - logger.debug( - "Step 2: Checking that workspace ID and organization ID are provided" - ) - if payload.organization_id is None or payload.workspace_id is None: + logger.debug("Step 2: Checking that Project ID is provided") + if request.state.project_id is None: raise Exception( - "Organization ID and Workspace ID must be provided to create app from template", + "Project ID must be provided to create app from template", ) logger.debug("Step 3: Checking user has permission to create app") + project = await db_manager_ee.get_project_by_workspace( + workspace_id=payload.workspace_id + ) has_permission = await check_rbac_permission( user_org_workspace_data=user_org_workspace_data, - workspace_id=payload.workspace_id, - organization_id=payload.organization_id, + project_id=str(project.id), permission=Permission.CREATE_APPLICATION, ) logger.debug( @@ -540,9 +514,8 @@ async def create_app_and_variant_from_template( app_name = payload.app_name.lower() app = await db_manager.fetch_app_by_name_and_parameters( app_name, - request.state.user_id, - payload.organization_id if isCloudEE() else None, # type: ignore - payload.workspace_id if isCloudEE() else None, # type: ignore + workspace_id=payload.workspace_id, + project_id=payload.project_id or request.state.project_id, ) if app is not None: raise Exception( @@ -557,9 +530,8 @@ async def create_app_and_variant_from_template( if app is None: app = await db_manager.create_app_and_envs( app_name, - request.state.user_id, - payload.organization_id if isCloudEE() else None, # type: ignore - payload.workspace_id if isCloudEE() else None, # type: ignore + project_id=payload.project_id or request.state.project_id, + workspace_id=payload.workspace_id, ) logger.debug( @@ -578,6 +550,7 @@ async def create_app_and_variant_from_template( ) app_variant_db = await app_manager.add_variant_based_on_image( app=app, + project_id=str(app.project_id), variant_name="app.default", docker_id_or_template_uri=( # type: ignore template_db.template_uri if isCloudProd() else template_db.digest @@ -595,12 +568,9 @@ async def create_app_and_variant_from_template( else "Step 5: Creating testset for app variant" ) await db_manager.add_testset_to_app_variant( - app_id=str(app.id), - org_id=payload.organization_id if isCloudEE() else None, # type: ignore - workspace_id=payload.workspace_id if isCloudEE() else None, # type: ignore template_name=template_db.name, # type: ignore app_name=app.app_name, # type: ignore - user_uid=request.state.user_id, + project_id=str(app.project_id), ) logger.debug( @@ -608,7 +578,9 @@ async def create_app_and_variant_from_template( if isCloudEE() else "Step 6: We create ready-to use evaluators" ) - await evaluator_manager.create_ready_to_use_evaluators(app=app) + await evaluator_manager.create_ready_to_use_evaluators( + app_name=app.app_name, project_id=str(app.project_id) + ) logger.debug( "Step 10: Starting variant and injecting environment variables" @@ -645,7 +617,12 @@ async def create_app_and_variant_from_template( envvars[key] = os.environ[key] else: envvars = {} if payload.env_vars is None else payload.env_vars - await app_manager.start_variant(app_variant_db, envvars) + await app_manager.start_variant( + app_variant_db, + str(app.project_id), + envvars, + user_uid=request.state.user_id, + ) logger.debug("End: Successfully created app and variant") return await converters.app_variant_db_to_output(app_variant_db) @@ -682,8 +659,7 @@ async def list_environments( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=request.state.project_id, permission=Permission.VIEW_APPLICATION, ) logger.debug(f"User has Permission to list environments: {has_permission}") @@ -694,7 +670,9 @@ async def list_environments( status_code=403, ) - environments_db = await db_manager.list_environments(app_id=app_id) + environments_db = await db_manager.list_environments( + app_id=app_id, project_id=request.state.project_id + ) logger.debug(f"environments_db: {environments_db}") return [ await converters.environment_db_to_output(env) for env in environments_db @@ -710,7 +688,9 @@ async def list_environments( response_model=EnvironmentOutputExtended, ) async def list_app_environment_revisions( - request: Request, app_id: str, environment_name + request: Request, + app_id: str, + environment_name, ): logger.debug("getting environment " + environment_name) user_org_workspace_data: dict = await get_user_org_and_workspace_id( @@ -720,8 +700,7 @@ async def list_app_environment_revisions( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=request.state.project_id, permission=Permission.VIEW_APPLICATION, ) logger.debug(f"User has Permission to list environments: {has_permission}") diff --git a/agenta-backend/agenta_backend/routers/bases_router.py b/agenta-backend/agenta_backend/routers/bases_router.py index 6e98a0665..e698f9e6f 100644 --- a/agenta-backend/agenta_backend/routers/bases_router.py +++ b/agenta-backend/agenta_backend/routers/bases_router.py @@ -40,11 +40,11 @@ async def list_bases( HTTPException: If there was an error retrieving the bases. """ try: + app = await db_manager.fetch_app_by_id(app_id=app_id) if isCloudEE() and app_id is not None: has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=str(app.project_i), permission=Permission.VIEW_APPLICATION, ) if not has_permission: diff --git a/agenta-backend/agenta_backend/routers/configs_router.py b/agenta-backend/agenta_backend/routers/configs_router.py index 03afc928b..b72c0f6a6 100644 --- a/agenta-backend/agenta_backend/routers/configs_router.py +++ b/agenta-backend/agenta_backend/routers/configs_router.py @@ -3,8 +3,9 @@ from typing import Optional from fastapi.responses import JSONResponse from fastapi import Request, HTTPException -from agenta_backend.utils.common import APIRouter, isCloudEE + +from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.models.api.api_models import ( SaveConfigPayload, GetConfigResponse, @@ -35,7 +36,7 @@ async def save_config( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=base_db, + project_id=str(base_db.project_id), permission=Permission.MODIFY_VARIANT_CONFIGURATIONS, ) if not has_permission: @@ -60,6 +61,7 @@ async def save_config( app_variant_id=str(variant_to_overwrite.id), parameters=payload.parameters, user_uid=request.state.user_id, + project_id=str(base_db.project_id), ) logger.debug("Deploying to production environment") @@ -82,6 +84,7 @@ async def save_config( new_config_name=payload.config_name, parameters=payload.parameters, user_uid=request.state.user_id, + project_id=str(base_db.project_id), ) except HTTPException as e: @@ -110,7 +113,7 @@ async def get_config( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=base_db, + project_id=str(base_db.project_id), permission=Permission.MODIFY_VARIANT_CONFIGURATIONS, ) if not has_permission: @@ -124,7 +127,7 @@ async def get_config( # in case environment_name is provided, find the variant deployed if environment_name: app_environments = await db_manager.list_environments( - app_id=str(base_db.app_id) # type: ignore + app_id=str(base_db.app_id), # type: ignore ) found_variant_revision = next( ( @@ -195,7 +198,10 @@ async def get_config( "/deployment/{deployment_revision_id}/", operation_id="get_config_deployment_revision", ) -async def get_config_deployment_revision(request: Request, deployment_revision_id: str): +async def get_config_deployment_revision( + request: Request, + deployment_revision_id: str, +): try: environment_revision = await db_manager.fetch_app_environment_revision( deployment_revision_id @@ -231,7 +237,10 @@ async def get_config_deployment_revision(request: Request, deployment_revision_i "/deployment/{deployment_revision_id}/revert/", operation_id="revert_deployment_revision", ) -async def revert_deployment_revision(request: Request, deployment_revision_id: str): +async def revert_deployment_revision( + request: Request, + deployment_revision_id: str, +): environment_revision = await db_manager.fetch_app_environment_revision( deployment_revision_id ) @@ -244,7 +253,7 @@ async def revert_deployment_revision(request: Request, deployment_revision_id: s if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=environment_revision, + project_id=str(environment_revision.project_id), permission=Permission.EDIT_APP_ENVIRONMENT_DEPLOYMENT, ) if not has_permission: diff --git a/agenta-backend/agenta_backend/routers/container_router.py b/agenta-backend/agenta_backend/routers/container_router.py index 4dfea8a01..77fed296b 100644 --- a/agenta-backend/agenta_backend/routers/container_router.py +++ b/agenta-backend/agenta_backend/routers/container_router.py @@ -1,10 +1,10 @@ -import uuid import logging from typing import List, Optional, Union from fastapi.responses import JSONResponse from fastapi import Request, UploadFile, HTTPException + from agenta_backend.services import db_manager from agenta_backend.utils.common import ( APIRouter, @@ -66,12 +66,10 @@ async def build_image( """ try: app_db = await db_manager.fetch_app_by_id(app_id) - - # Check app access if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app_db, + project_id=str(app_db.project_id), permission=Permission.CREATE_APPLICATION, ) if not has_permission: @@ -104,6 +102,7 @@ async def restart_docker_container( payload (RestartAppContainer) -- the required data (app_name and variant_name) """ logger.debug(f"Restarting container for variant {payload.variant_id}") + app_variant_db = await db_manager.fetch_app_variant_by_id(payload.variant_id) try: deployment = await db_manager.get_deployment_by_id( @@ -183,7 +182,7 @@ async def construct_app_container_url( if isCloudEE() and object_db is not None: has_permission = await check_action_access( user_uid=request.state.user_id, - object=object_db, + project_id=str(object_db.project_id), permission=Permission.VIEW_APPLICATION, ) if not has_permission: diff --git a/agenta-backend/agenta_backend/routers/environment_router.py b/agenta-backend/agenta_backend/routers/environment_router.py index 3fe69188e..554ffec22 100644 --- a/agenta-backend/agenta_backend/routers/environment_router.py +++ b/agenta-backend/agenta_backend/routers/environment_router.py @@ -1,8 +1,10 @@ import logging +from typing import Optional from fastapi.responses import JSONResponse from fastapi import Request, HTTPException + from agenta_backend.services import db_manager, app_manager from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.models.api.api_models import DeployToEnvironmentPayload @@ -32,11 +34,13 @@ async def deploy_to_environment( HTTPException: If the deployment fails. """ try: + variant = await db_manager.fetch_app_variant_by_id( + app_variant_id=payload.variant_id + ) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=payload.variant_id, - object_type="app_variant", + project_id=str(variant.project_id), permission=Permission.DEPLOY_APPLICATION, ) logger.debug(f"User has permission deploy to environment: {has_permission}") @@ -59,6 +63,7 @@ async def deploy_to_environment( user_uid=request.state.user_id, object_id=payload.variant_id, object_type="variant", + project_id=str(variant.project_id), ) logger.debug("Successfully updated last_modified_by app information") except Exception as e: diff --git a/agenta-backend/agenta_backend/routers/evaluation_router.py b/agenta-backend/agenta_backend/routers/evaluation_router.py index f25c5d181..d3cd29818 100644 --- a/agenta-backend/agenta_backend/routers/evaluation_router.py +++ b/agenta-backend/agenta_backend/routers/evaluation_router.py @@ -1,12 +1,13 @@ import random import logging -from typing import Any, List +from typing import Any, List, Optional from fastapi.responses import JSONResponse from fastapi import HTTPException, Request, status, Response, Query from agenta_backend.services import helpers from agenta_backend.models import converters + from agenta_backend.tasks.evaluations import evaluate from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.models.api.evaluation_model import ( @@ -51,11 +52,11 @@ async def fetch_evaluation_ids( List[str]: A list of evaluation ids. """ try: + app = await db_manager.fetch_app_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=str(app.project_id), permission=Permission.VIEW_EVALUATION, ) logger.debug( @@ -69,7 +70,7 @@ async def fetch_evaluation_ids( status_code=403, ) evaluations = await db_manager.fetch_evaluations_by_resource( - resource_type, resource_ids + resource_type, str(app.project_id), resource_ids ) return list(map(lambda x: str(x.id), evaluations)) except Exception as exc: @@ -98,7 +99,7 @@ async def create_evaluation( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.CREATE_EVALUATION, ) logger.debug(f"User has permission to create evaluation: {has_permission}") @@ -122,13 +123,14 @@ async def create_evaluation( for variant_id in payload.variant_ids: evaluation = await evaluation_service.create_new_evaluation( app_id=payload.app_id, + project_id=str(app.project_id), variant_id=variant_id, - evaluator_config_ids=payload.evaluators_configs, testset_id=payload.testset_id, ) evaluate.delay( app_id=payload.app_id, + project_id=str(app.project_id), variant_id=variant_id, evaluators_config_ids=payload.evaluators_configs, testset_id=payload.testset_id, @@ -143,6 +145,7 @@ async def create_evaluation( user_uid=request.state.user_id, object_id=payload.app_id, object_type="app", + project_id=str(app.project_id), ) logger.debug("Successfully updated last_modified_by app information") @@ -161,7 +164,10 @@ async def create_evaluation( @router.get("/{evaluation_id}/status/", operation_id="fetch_evaluation_status") -async def fetch_evaluation_status(evaluation_id: str, request: Request): +async def fetch_evaluation_status( + evaluation_id: str, + request: Request, +): """Fetches the status of the evaluation. Args: @@ -177,7 +183,7 @@ async def fetch_evaluation_status(evaluation_id: str, request: Request): if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation, + project_id=str(evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) logger.debug( @@ -197,7 +203,10 @@ async def fetch_evaluation_status(evaluation_id: str, request: Request): @router.get("/{evaluation_id}/results/", operation_id="fetch_evaluation_results") -async def fetch_evaluation_results(evaluation_id: str, request: Request): +async def fetch_evaluation_results( + evaluation_id: str, + request: Request, +): """Fetches the results of the evaluation Args: @@ -213,7 +222,7 @@ async def fetch_evaluation_results(evaluation_id: str, request: Request): if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation, + project_id=str(evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) logger.debug( @@ -266,7 +275,7 @@ async def fetch_evaluation_scenarios( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation, + project_id=str(evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) logger.debug( @@ -282,7 +291,7 @@ async def fetch_evaluation_scenarios( eval_scenarios = ( await evaluation_service.fetch_evaluation_scenarios_for_evaluation( - evaluation_id=str(evaluation.id) + evaluation_id=str(evaluation.id), project_id=str(evaluation.project_id) ) ) return eval_scenarios @@ -313,7 +322,7 @@ async def fetch_list_evaluations( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.VIEW_EVALUATION, ) logger.debug( @@ -327,7 +336,7 @@ async def fetch_list_evaluations( status_code=403, ) - return await evaluation_service.fetch_list_evaluations(app) + return await evaluation_service.fetch_list_evaluations(app, str(app.project_id)) except Exception as exc: import traceback @@ -364,8 +373,7 @@ async def fetch_evaluation( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluation_id, - object_type="evaluation", + project_id=str(evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) logger.debug( @@ -401,12 +409,11 @@ async def delete_evaluations( """ try: + evaluation = await db_manager.fetch_evaluation_by_id(payload.evaluations_ids[0]) if isCloudEE(): - evaluation_id = random.choice(payload.evaluations_ids) has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluation_id, - object_type="evaluation", + project_id=str(evaluation.project_id), permission=Permission.DELETE_EVALUATION, ) logger.debug(f"User has permission to delete evaluation: {has_permission}") @@ -423,9 +430,11 @@ async def delete_evaluations( user_uid=request.state.user_id, object_id=random.choice(payload.evaluations_ids), object_type="evaluation", + project_id=str(evaluation.project_id), ) logger.debug("Successfully updated last_modified_by app information") + logger.debug(f"Deleting evaluations {payload.evaluations_ids}...") await evaluation_service.delete_evaluations(payload.evaluations_ids) return Response(status_code=status.HTTP_204_NO_CONTENT) except Exception as exc: @@ -453,28 +462,26 @@ async def fetch_evaluation_scenarios( """ try: evaluations_ids_list = evaluations_ids.split(",") - + evaluation = await db_manager.fetch_evaluation_by_id(evaluations_ids_list[0]) if isCloudEE(): - for evaluation_id in evaluations_ids_list: - has_permission = await check_action_access( - user_uid=request.state.user_id, - object_id=evaluation_id, - object_type="evaluation", - permission=Permission.VIEW_EVALUATION, - ) - logger.debug( - f"User has permission to get evaluation scenarios: {has_permission}" + has_permission = await check_action_access( + user_uid=request.state.user_id, + project_id=str(evaluation.project_id), + permission=Permission.VIEW_EVALUATION, + ) + logger.debug( + f"User has permission to get evaluation scenarios: {has_permission}" + ) + if not has_permission: + error_msg = f"You do not have permission to perform this action. Please contact your organization admin." + logger.error(error_msg) + return JSONResponse( + {"detail": error_msg}, + status_code=403, ) - if not has_permission: - error_msg = f"You do not have permission to perform this action. Please contact your organization admin." - logger.error(error_msg) - return JSONResponse( - {"detail": error_msg}, - status_code=403, - ) eval_scenarios = await evaluation_service.compare_evaluations_scenarios( - evaluations_ids_list + evaluations_ids_list, str(evaluation.project_id) ) return eval_scenarios diff --git a/agenta-backend/agenta_backend/routers/evaluators_router.py b/agenta-backend/agenta_backend/routers/evaluators_router.py index 743fb3e0f..5ad1a9bf4 100644 --- a/agenta-backend/agenta_backend/routers/evaluators_router.py +++ b/agenta-backend/agenta_backend/routers/evaluators_router.py @@ -1,10 +1,11 @@ import logging import traceback -from typing import List +from typing import List, Optional from fastapi import HTTPException, Request from fastapi.responses import JSONResponse + from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.services import ( evaluator_manager, @@ -115,7 +116,10 @@ async def evaluator_run( @router.get("/configs/", response_model=List[EvaluatorConfig]) -async def get_evaluator_configs(app_id: str, request: Request): +async def get_evaluator_configs( + app_id: str, + request: Request, +): """Endpoint to fetch evaluator configurations for a specific app. Args: @@ -126,11 +130,11 @@ async def get_evaluator_configs(app_id: str, request: Request): """ try: + app_db = await db_manager.fetch_app_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=str(app_db.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -141,7 +145,9 @@ async def get_evaluator_configs(app_id: str, request: Request): status_code=403, ) - evaluators_configs = await evaluator_manager.get_evaluators_configs(app_id) + evaluators_configs = await evaluator_manager.get_evaluators_configs( + str(app_db.project_id) + ) return evaluators_configs except Exception as e: raise HTTPException( @@ -150,7 +156,10 @@ async def get_evaluator_configs(app_id: str, request: Request): @router.get("/configs/{evaluator_config_id}/", response_model=EvaluatorConfig) -async def get_evaluator_config(evaluator_config_id: str, request: Request): +async def get_evaluator_config( + evaluator_config_id: str, + request: Request, +): """Endpoint to fetch evaluator configurations for a specific app. Returns: @@ -164,7 +173,7 @@ async def get_evaluator_config(evaluator_config_id: str, request: Request): if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluator_config_db.app, + project_id=str(evaluator_config_db.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -186,7 +195,10 @@ async def get_evaluator_config(evaluator_config_id: str, request: Request): @router.post("/configs/", response_model=EvaluatorConfig) -async def create_new_evaluator_config(payload: NewEvaluatorConfig, request: Request): +async def create_new_evaluator_config( + payload: NewEvaluatorConfig, + request: Request, +): """Endpoint to fetch evaluator configurations for a specific app. Args: @@ -196,11 +208,11 @@ async def create_new_evaluator_config(payload: NewEvaluatorConfig, request: Requ EvaluatorConfigDB: Evaluator configuration api model. """ try: + app_db = await db_manager.get_app_instance_by_id(app_id=payload.app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=payload.app_id, - object_type="app", + project_id=str(app_db.project_id), permission=Permission.CREATE_EVALUATION, ) if not has_permission: @@ -212,20 +224,12 @@ async def create_new_evaluator_config(payload: NewEvaluatorConfig, request: Requ ) evaluator_config = await evaluator_manager.create_evaluator_config( - app_id=payload.app_id, + project_id=str(app_db.project_id), + app_name=app_db.app_name, name=payload.name, evaluator_key=payload.evaluator_key, settings_values=payload.settings_values, ) - - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=payload.app_id, - object_type="app", - ) - logger.debug("Successfully updated last_modified_by app information") - return evaluator_config except Exception as e: import traceback @@ -238,7 +242,9 @@ async def create_new_evaluator_config(payload: NewEvaluatorConfig, request: Requ @router.put("/configs/{evaluator_config_id}/", response_model=EvaluatorConfig) async def update_evaluator_config( - evaluator_config_id: str, payload: UpdateEvaluatorConfig, request: Request + evaluator_config_id: str, + payload: UpdateEvaluatorConfig, + request: Request, ): """Endpoint to update evaluator configurations for a specific app. @@ -247,11 +253,13 @@ async def update_evaluator_config( """ try: + evaluator_config = await db_manager.fetch_evaluator_config( + evaluator_config_id=evaluator_config_id + ) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluator_config_id, - object_type="evaluator_config", + project_id=str(evaluator_config.project_id), permission=Permission.EDIT_EVALUATION, ) if not has_permission: @@ -263,16 +271,8 @@ async def update_evaluator_config( ) evaluators_configs = await evaluator_manager.update_evaluator_config( - evaluator_config_id=evaluator_config_id, updates=payload.dict() - ) - - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=evaluator_config_id, - object_type="evaluator_config", + evaluator_config_id=evaluator_config_id, updates=payload.model_dump() ) - logger.debug("Successfully updated last_modified_by app information") return evaluators_configs except Exception as e: import traceback @@ -284,7 +284,10 @@ async def update_evaluator_config( @router.delete("/configs/{evaluator_config_id}/", response_model=bool) -async def delete_evaluator_config(evaluator_config_id: str, request: Request): +async def delete_evaluator_config( + evaluator_config_id: str, + request: Request, +): """Endpoint to delete a specific evaluator configuration. Args: @@ -294,11 +297,13 @@ async def delete_evaluator_config(evaluator_config_id: str, request: Request): bool: True if deletion was successful, False otherwise. """ try: + evaluator_config = await db_manager.fetch_evaluator_config( + evaluator_config_id=evaluator_config_id + ) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluator_config_id, - object_type="evaluator_config", + project_id=str(evaluator_config.project_id), permission=Permission.DELETE_EVALUATION, ) if not has_permission: @@ -309,14 +314,6 @@ async def delete_evaluator_config(evaluator_config_id: str, request: Request): status_code=403, ) - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=evaluator_config_id, - object_type="evaluator_config", - ) - logger.debug("Successfully updated last_modified_by app information") - success = await evaluator_manager.delete_evaluator_config(evaluator_config_id) return success except Exception as e: diff --git a/agenta-backend/agenta_backend/routers/human_evaluation_router.py b/agenta-backend/agenta_backend/routers/human_evaluation_router.py index 16672358d..a5573923e 100644 --- a/agenta-backend/agenta_backend/routers/human_evaluation_router.py +++ b/agenta-backend/agenta_backend/routers/human_evaluation_router.py @@ -1,9 +1,10 @@ import random import logging -from typing import List, Dict +from typing import List, Dict, Optional from fastapi import HTTPException, Body, Request, status, Response from agenta_backend.models import converters + from agenta_backend.services import results_service from agenta_backend.services import evaluation_service from agenta_backend.services import db_manager, app_manager @@ -61,8 +62,7 @@ async def create_evaluation( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=payload.app_id, - object_type="app", + project_id=str(app.project_id), permission=Permission.CREATE_EVALUATION, ) if not has_permission: @@ -73,17 +73,8 @@ async def create_evaluation( ) new_human_evaluation_db = await evaluation_service.create_new_human_evaluation( - payload, request.state.user_id - ) - - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=payload.app_id, - object_type="app", + payload ) - logger.debug("Successfully updated last_modified_by app information") - return await converters.human_evaluation_db_to_simple_evaluation_output( new_human_evaluation_db ) @@ -115,11 +106,11 @@ async def fetch_list_human_evaluations( """ try: + app = await db_manager.fetch_app_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=app_id, - object_type="app", + project_id=str(app.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -129,7 +120,9 @@ async def fetch_list_human_evaluations( status_code=403, ) - return await evaluation_service.fetch_list_human_evaluations(app_id) + return await evaluation_service.fetch_list_human_evaluations( + app_id, str(app.project_id) + ) except Exception as e: status_code = e.status_code if hasattr(e, "status_code") else 500 # type: ignore raise HTTPException(status_code=status_code, detail=str(e)) from e @@ -156,8 +149,7 @@ async def fetch_human_evaluation( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluation_id, - object_type="human_evaluation", + project_id=str(human_evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -205,8 +197,7 @@ async def fetch_evaluation_scenarios( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluation_id, - object_type="human_evaluation", + project_id=str(human_evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -254,7 +245,7 @@ async def update_human_evaluation( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=human_evaluation, + project_id=str(human_evaluation.project_id), permission=Permission.EDIT_EVALUATION, ) if not has_permission: @@ -265,15 +256,6 @@ async def update_human_evaluation( ) await update_human_evaluation_service(human_evaluation, update_data) - - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=str(human_evaluation.app_id), - object_type="app", - ) - logger.debug("Successfully updated last_modified_by app information") - return Response(status_code=status.HTTP_204_NO_CONTENT) except KeyError: @@ -314,7 +296,7 @@ async def update_evaluation_scenario_router( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation_scenario_db, + project_id=str(evaluation_scenario_db.project_id), permission=Permission.EDIT_EVALUATION, ) if not has_permission: @@ -329,15 +311,6 @@ async def update_evaluation_scenario_router( payload, evaluation_type, ) - - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=str(evaluation_scenario_db.evaluation_id), - object_type="human_evaluation", - ) - logger.debug("Successfully updated last_modified_by app information") - return Response(status_code=status.HTTP_204_NO_CONTENT) except UpdateEvaluationScenarioError as e: import traceback @@ -375,7 +348,7 @@ async def get_evaluation_scenario_score_router( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation_scenario, + project_id=str(evaluation_scenario.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -421,7 +394,7 @@ async def update_evaluation_scenario_score_router( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation_scenario, + project_id=str(evaluation_scenario.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -433,17 +406,8 @@ async def update_evaluation_scenario_score_router( await db_manager.update_human_evaluation_scenario( evaluation_scenario_id=str(evaluation_scenario.id), # type: ignore - values_to_update=payload.dict(), + values_to_update=payload.model_dump(), ) - - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=str(evaluation_scenario.evaluation_id), - object_type="human_evaluation", - ) - logger.debug("Successfully updated last_modified_by app information") - return Response(status_code=status.HTTP_204_NO_CONTENT) except Exception as e: status_code = e.status_code if hasattr(e, "status_code") else 500 # type: ignore @@ -474,7 +438,7 @@ async def fetch_results( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=evaluation, + project_id=str(evaluation.project_id), permission=Permission.VIEW_EVALUATION, ) if not has_permission: @@ -503,26 +467,27 @@ async def fetch_results( @router.delete("/", response_model=List[str]) async def delete_evaluations( - delete_evaluations: DeleteEvaluation, + payload: DeleteEvaluation, request: Request, ): """ Delete specific comparison tables based on their unique IDs. Args: - delete_evaluations (List[str]): The unique identifiers of the comparison tables to delete. + payload (List[str]): The unique identifiers of the comparison tables to delete. Returns: A list of the deleted comparison tables' IDs. """ try: + evaluation = await db_manager.fetch_human_evaluation_by_id( + payload.evaluations_ids[0] + ) if isCloudEE(): - evaluation_id = random.choice(delete_evaluations.evaluations_ids) has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=evaluation_id, - object_type="human_evaluation", + project_id=str(evaluation.project_id), permission=Permission.DELETE_EVALUATION, ) if not has_permission: @@ -532,17 +497,7 @@ async def delete_evaluations( status_code=403, ) - # Update last_modified_by app information - await app_manager.update_last_modified_by( - user_uid=request.state.user_id, - object_id=random.choice(delete_evaluations.evaluations_ids), - object_type="human_evaluation", - ) - logger.debug("Successfully updated last_modified_by app information") - - await evaluation_service.delete_human_evaluations( - delete_evaluations.evaluations_ids - ) + await evaluation_service.delete_human_evaluations(payload.evaluations_ids) return Response(status_code=status.HTTP_204_NO_CONTENT) except Exception as e: import traceback diff --git a/agenta-backend/agenta_backend/routers/testset_router.py b/agenta-backend/agenta_backend/routers/testset_router.py index b6691f496..ddd39bcc2 100644 --- a/agenta-backend/agenta_backend/routers/testset_router.py +++ b/agenta-backend/agenta_backend/routers/testset_router.py @@ -11,6 +11,7 @@ from fastapi.responses import JSONResponse from fastapi import HTTPException, UploadFile, File, Form, Request + from agenta_backend.services import db_manager from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.models.converters import testset_db_to_pydantic @@ -70,7 +71,7 @@ async def upload_file( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.CREATE_TESTSET, ) logger.debug(f"User has Permission to upload Testset: {has_permission}") @@ -113,7 +114,7 @@ async def upload_file( try: testset = await db_manager.create_testset( - app=app, user_uid=request.state.user_id, testset_data=document + app=app, project_id=str(app.project_id), testset_data=document ) return TestSetSimpleResponse( id=str(testset.id), @@ -143,11 +144,12 @@ async def import_testset( Returns: dict: The result of the import process. """ + app = await db_manager.fetch_app_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.CREATE_TESTSET, ) logger.debug(f"User has Permission to import Testset: {has_permission}") @@ -178,7 +180,7 @@ async def import_testset( document["csvdata"].append(row) testset = await db_manager.create_testset( - app=app, user_uid=request.state.user_id, testset_data=document + app=app, project_id=str(app.project_id), testset_data=document ) return TestSetSimpleResponse( id=str(testset.id), @@ -225,7 +227,7 @@ async def create_testset( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.CREATE_TESTSET, ) logger.debug(f"User has Permission to create Testset: {has_permission}") @@ -243,7 +245,7 @@ async def create_testset( "csvdata": csvdata.csvdata, } testset_instance = await db_manager.create_testset( - app=app, user_uid=request.state.user_id, testset_data=testset_data + app=app, project_id=str(app.project_id), testset_data=testset_data ) if testset_instance is not None: return TestSetSimpleResponse( @@ -280,7 +282,7 @@ async def update_testset( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=testset, + project_id=str(testset.project_id), permission=Permission.EDIT_TESTSET, ) logger.debug(f"User has Permission to update Testset: {has_permission}") @@ -324,11 +326,12 @@ async def get_testsets( Raises: - `HTTPException` with status code 404 if no testsets are found. """ + app = await db_manager.fetch_app_by_id(app_id=app_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app, + project_id=str(app.project_id), permission=Permission.VIEW_TESTSET, ) logger.debug(f"User has Permission to view Testsets: {has_permission}") @@ -340,10 +343,9 @@ async def get_testsets( status_code=403, ) - if app is None: - raise HTTPException(status_code=404, detail="App not found") - - testsets = await db_manager.fetch_testsets_by_app_id(app_id=app_id) + testsets = await db_manager.fetch_testsets_by_project_id( + project_id=str(app.project_id) + ) return [ TestSetOutputResponse( _id=str(testset.id), # type: ignore @@ -374,7 +376,7 @@ async def get_single_testset( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=test_set, + project_id=str(test_set.project_id), permission=Permission.VIEW_TESTSET, ) logger.debug(f"User has Permission to view Testset: {has_permission}") @@ -410,21 +412,21 @@ async def delete_testsets( """ if isCloudEE(): - testset_id = random.choice(payload.testset_ids) - has_permission = await check_action_access( - user_uid=request.state.user_id, - object_id=testset_id, - object_type="testset", - permission=Permission.DELETE_TESTSET, - ) - logger.debug(f"User has Permission to delete Testset: {has_permission}") - if not has_permission: - error_msg = f"You do not have permission to perform this action. Please contact your organization admin." - logger.error(error_msg) - return JSONResponse( - {"detail": error_msg}, - status_code=403, + for testset_id in payload.testset_ids: + testset = await db_manager.fetch_testset_by_id(testset_id=testset_id) + has_permission = await check_action_access( + user_uid=request.state.user_id, + project_id=str(testset.project_id), + permission=Permission.DELETE_TESTSET, ) + logger.debug(f"User has Permission to delete Testset: {has_permission}") + if not has_permission: + error_msg = f"You do not have permission to perform this action. Please contact your organization admin." + logger.error(error_msg) + return JSONResponse( + {"detail": error_msg}, + status_code=403, + ) await db_manager.remove_testsets(testset_ids=payload.testset_ids) return payload.testset_ids diff --git a/agenta-backend/agenta_backend/routers/variants_router.py b/agenta-backend/agenta_backend/routers/variants_router.py index e3bf09c39..486d9fb83 100644 --- a/agenta-backend/agenta_backend/routers/variants_router.py +++ b/agenta-backend/agenta_backend/routers/variants_router.py @@ -1,5 +1,3 @@ -import os -import inspect import logging from typing import Any, Optional, Union, List @@ -8,6 +6,7 @@ from fastapi import HTTPException, Request, Body from agenta_backend.models import converters + from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.services import ( app_manager, @@ -76,7 +75,7 @@ async def add_variant_from_base_and_config( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=base_db, + project_id=str(base_db.project_id), permission=Permission.CREATE_APPLICATION, ) logger.debug( @@ -96,6 +95,7 @@ async def add_variant_from_base_and_config( new_config_name=payload.new_config_name, parameters=payload.parameters, user_uid=request.state.user_id, + project_id=str(base_db.project_id), ) logger.debug(f"Successfully added new variant: {db_app_variant}") @@ -104,11 +104,12 @@ async def add_variant_from_base_and_config( user_uid=request.state.user_id, object_id=str(db_app_variant.app_id), object_type="app", + project_id=str(base_db.project_id), ) logger.debug("Successfully updated last_modified_by app information") app_variant_db = await db_manager.get_app_variant_instance_by_id( - str(db_app_variant.id) + str(db_app_variant.id), str(db_app_variant.project_id) ) return await converters.app_variant_db_to_output(app_variant_db) @@ -134,12 +135,13 @@ async def remove_variant( Raises: HTTPException: If there is a problem removing the app variant """ + try: + variant = await db_manager.fetch_app_variant_by_id(variant_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=variant_id, - object_type="app_variant", + project_id=str(variant.project_id), permission=Permission.DELETE_APPLICATION_VARIANT, ) logger.debug(f"User has Permission to delete app variant: {has_permission}") @@ -156,10 +158,13 @@ async def remove_variant( user_uid=request.state.user_id, object_id=variant_id, object_type="variant", + project_id=str(variant.project_id), ) logger.debug("Successfully updated last_modified_by app information") - await app_manager.terminate_and_remove_app_variant(app_variant_id=variant_id) + await app_manager.terminate_and_remove_app_variant( + project_id=str(variant.project_id), app_variant_id=variant_id + ) except DockerException as e: detail = f"Docker error while trying to remove the app variant: {str(e)}" raise HTTPException(status_code=500, detail=detail) @@ -192,11 +197,11 @@ async def update_variant_parameters( JSONResponse: A JSON response containing the updated app variant parameters. """ try: + variant_db = await db_manager.fetch_app_variant_by_id(variant_id) if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object_id=variant_id, - object_type="app_variant", + project_id=str(variant_db.project_id), permission=Permission.MODIFY_VARIANT_CONFIGURATIONS, ) logger.debug( @@ -214,6 +219,7 @@ async def update_variant_parameters( app_variant_id=variant_id, parameters=payload.parameters, user_uid=request.state.user_id, + project_id=str(variant_db.project_id), ) # Update last_modified_by app information @@ -221,6 +227,7 @@ async def update_variant_parameters( user_uid=request.state.user_id, object_id=variant_id, object_type="variant", + project_id=str(variant_db.project_id), ) logger.debug("Successfully updated last_modified_by app information") except ValueError as e: @@ -261,7 +268,7 @@ async def update_variant_image( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=db_app_variant, + project_id=str(db_app_variant.project_id), permission=Permission.CREATE_APPLICATION, ) logger.debug( @@ -276,7 +283,7 @@ async def update_variant_image( ) await app_manager.update_variant_image( - db_app_variant, image, request.state.user_id + db_app_variant, str(db_app_variant.project_id), image, request.state.user_id ) # Update last_modified_by app information @@ -284,6 +291,7 @@ async def update_variant_image( user_uid=request.state.user_id, object_id=str(db_app_variant.app_id), object_type="app", + project_id=str(db_app_variant.project_id), ) logger.debug("Successfully updated last_modified_by app information") except ValueError as e: @@ -328,13 +336,14 @@ async def start_variant( Raises: HTTPException: If the app container cannot be started. """ + app_variant_db = await db_manager.fetch_app_variant_by_id(app_variant_id=variant_id) # Check user has permission to start variant if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app_variant_db, + project_id=str(app_variant_db.project_id), permission=Permission.CREATE_APPLICATION, ) logger.debug(f"User has Permission to start variant: {has_permission}") @@ -352,19 +361,25 @@ async def start_variant( envvars = {} if env_vars is None else env_vars.env_vars if action.action == VariantActionEnum.START: - url: URI = await app_manager.start_variant(app_variant_db, envvars) + url: URI = await app_manager.start_variant( + app_variant_db, str(app_variant_db.project_id), envvars + ) # Deploy to production await db_manager.deploy_to_environment( environment_name="production", variant_id=str(app_variant_db.id), + project_id=str(app_variant_db.project_id), user_uid=request.state.user_id, ) return url @router.get("/{variant_id}/logs/", operation_id="retrieve_variant_logs") -async def retrieve_variant_logs(variant_id: str, request: Request): +async def retrieve_variant_logs( + variant_id: str, + request: Request, +): try: app_variant = await db_manager.fetch_app_variant_by_id(variant_id) deployment = await db_manager.get_deployment_by_appid(str(app_variant.app.id)) @@ -393,7 +408,7 @@ async def get_variant( if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app_variant, + project_id=str(app_variant.project_id), permission=Permission.VIEW_APPLICATION, ) logger.debug(f"User has Permission to get variant: {has_permission}") @@ -416,7 +431,10 @@ async def get_variant( operation_id="get_variant_revisions", response_model=List[AppVariantRevision], ) -async def get_variant_revisions(variant_id: str, request: Request): +async def get_variant_revisions( + variant_id: str, + request: Request, +): logger.debug("getting variant revisions: ", variant_id) try: app_variant = await db_manager.fetch_app_variant_by_id( @@ -426,7 +444,7 @@ async def get_variant_revisions(variant_id: str, request: Request): if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app_variant, + project_id=str(app_variant.project_id), permission=Permission.VIEW_APPLICATION, ) logger.debug(f"User has Permission to get variant: {has_permission}") @@ -439,7 +457,7 @@ async def get_variant_revisions(variant_id: str, request: Request): ) app_variant_revisions = await db_manager.list_app_variant_revisions_by_variant( - app_variant=app_variant + app_variant=app_variant, project_id=str(app_variant.project_id) ) return await converters.app_variant_db_revisions_to_output( app_variant_revisions @@ -454,7 +472,11 @@ async def get_variant_revisions(variant_id: str, request: Request): operation_id="get_variant_revision", response_model=AppVariantRevision, ) -async def get_variant_revision(variant_id: str, revision_number: int, request: Request): +async def get_variant_revision( + variant_id: str, + revision_number: int, + request: Request, +): logger.debug("getting variant revision: ", variant_id, revision_number) try: assert ( @@ -467,7 +489,7 @@ async def get_variant_revision(variant_id: str, revision_number: int, request: R if isCloudEE(): has_permission = await check_action_access( user_uid=request.state.user_id, - object=app_variant, + project_id=str(app_variant.project_id), permission=Permission.VIEW_APPLICATION, ) logger.debug(f"User has Permission to get variant: {has_permission}") diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py index 337b1b675..0a39de23e 100644 --- a/agenta-backend/agenta_backend/services/app_manager.py +++ b/agenta-backend/agenta_backend/services/app_manager.py @@ -54,7 +54,9 @@ async def start_variant( db_app_variant: AppVariantDB, + project_id: str, env_vars: Optional[DockerEnvVars] = None, + user_uid: Optional[str] = None, ) -> URI: """ Starts a Docker container for a given app variant. @@ -64,7 +66,9 @@ async def start_variant( Args: app_variant (AppVariant): The app variant for which a container is to be started. + project_id (str): The ID of the project the app variant belongs to. env_vars (DockerEnvVars): (optional) The environment variables to be passed to the container. + user_uid (str): (optional) The user ID. Returns: URI: The URI of the started Docker container. @@ -81,8 +85,6 @@ async def start_variant( db_app_variant.image.docker_id, db_app_variant.image.tags, db_app_variant.app.app_name, - str(db_app_variant.organization_id) if isCloudEE() else None, - str(db_app_variant.workspace_id) if isCloudEE() else None, ) logger.debug("App name is %s", db_app_variant.app.app_name) # update the env variables @@ -93,6 +95,7 @@ async def start_variant( "http://host.docker.internal" # unclear why this stopped working ) # domain_name = "http://localhost" + env_vars = {} if env_vars is None else env_vars # type: ignore env_vars.update( { @@ -102,16 +105,17 @@ async def start_variant( } ) if isCloudEE(): + user = await db_manager.get_user(user_uid=user_uid) api_key = await api_key_service.create_api_key( - str(db_app_variant.user.uid), - workspace_id=str(db_app_variant.workspace_id), + str(user.id), + project_id=project_id, expiration_date=None, hidden=True, ) env_vars.update({"AGENTA_API_KEY": api_key}) deployment = await deployment_manager.start_service( - app_variant_db=db_app_variant, env_vars=env_vars + app_variant_db=db_app_variant, project_id=project_id, env_vars=env_vars ) await db_manager.update_base( @@ -133,13 +137,15 @@ async def start_variant( async def update_variant_image( - app_variant_db: AppVariantDB, image: Image, user_uid: str + app_variant_db: AppVariantDB, project_id: str, image: Image, user_uid: str ): """Updates the image for app variant in the database. Arguments: - app_variant -- the app variant to update - image -- the image to update + app_variant (AppVariantDB): the app variant to update + project_id (str): The ID of the project + image (Image): the image to update + user_uid (str): The ID of the user updating the image """ valid_image = await deployment_manager.validate_image(image) @@ -155,25 +161,21 @@ async def update_variant_image( if isOss(): await deployment_manager.remove_image(base.image) - await db_manager.remove_image(base.image) + await db_manager.remove_image(base.image, project_id) # Create a new image instance db_image = await db_manager.create_image( image_type="image", + project_id=project_id, tags=image.tags, docker_id=image.docker_id, - user=app_variant_db.user, deletable=True, - organization=( - str(app_variant_db.organization_id) if isCloudEE() else None - ), # noqa - workspace=str(app_variant_db.workspace_id) if isCloudEE() else None, # noqa ) # Update base with new image await db_manager.update_base(str(app_variant_db.base_id), image_id=db_image.id) # Update variant to remove configuration await db_manager.update_variant_parameters( - str(app_variant_db.id), parameters={}, user_uid=user_uid + str(app_variant_db.id), parameters={}, project_id=project_id, user_uid=user_uid ) # Update variant with new image app_variant_db = await db_manager.update_app_variant( @@ -181,13 +183,11 @@ async def update_variant_image( ) # Start variant - await start_variant(app_variant_db) + await start_variant(app_variant_db, project_id) async def update_last_modified_by( - user_uid: str, - object_id: str, - object_type: str, + user_uid: str, object_id: str, object_type: str, project_id: str ) -> None: """Updates the last_modified_by field in the app variant table. @@ -195,6 +195,7 @@ async def update_last_modified_by( object_id (str): The object ID to update. object_type (str): The type of object to update. user_uid (str): The user UID to update. + project_id (str): The project ID. """ async def get_appdb_str_by_id(object_id: str, object_type: str) -> str: @@ -205,6 +206,13 @@ async def get_appdb_str_by_id(object_id: str, object_type: str) -> str: if app_variant_db is None: raise db_manager.NoResultFound(f"Variant with id {object_id} not found") return str(app_variant_db.app_id) + elif object_type == "deployment": + deployment_db = await db_manager.get_deployment_by_id(object_id) + if deployment_db is None: + raise db_manager.NoResultFound( + f"Deployment with id {object_id} not found" + ) + return str(deployment_db.app_id) elif object_type == "evaluation": evaluation_db = await db_manager.fetch_evaluation_by_id(object_id) if evaluation_db is None: @@ -212,27 +220,14 @@ async def get_appdb_str_by_id(object_id: str, object_type: str) -> str: f"Evaluation with id {object_id} not found" ) return str(evaluation_db.app_id) - elif object_type == "human_evaluation": - human_evaluation_db = await db_manager.fetch_human_evaluation_by_id( - object_id - ) - if human_evaluation_db is None: - raise db_manager.NoResultFound( - f"Human Evaluation with id {object_id} not found" - ) - return str(human_evaluation_db.app_id) - elif object_type == "evaluator_config": - evaluator_config_db = await db_manager.fetch_evaluator_config(object_id) - if evaluator_config_db is None: - raise db_manager.NoResultFound( - f"Evaluator Config with id {str(object_id)} not found" - ) - return str(evaluator_config_db.app_id) else: - raise ValueError(f"Unsupported object type: {object_type}") + raise ValueError( + f"Could not update last_modified_by application information. Unsupported type: {object_type}" + ) user = await db_manager.get_user(user_uid=user_uid) app_id = await get_appdb_str_by_id(object_id=object_id, object_type=object_type) + assert app_id is not None, f"app_id in {object_type} cannot be None" await db_manager.update_app( app_id=app_id, values_to_update={ @@ -243,7 +238,9 @@ async def get_appdb_str_by_id(object_id: str, object_type: str) -> str: async def terminate_and_remove_app_variant( - app_variant_id: Optional[str] = None, app_variant_db: Optional[AppVariantDB] = None + project_id: str, + app_variant_id: Optional[str] = None, + app_variant_db: Optional[AppVariantDB] = None, ) -> None: """ Removes app variant from the database. If it's the last one using an image, performs additional operations: @@ -252,6 +249,7 @@ async def terminate_and_remove_app_variant( - Removes the image from the registry. Args: + project_id (str): The ID of the project variant_id (srt): The app variant to remove. Raises: @@ -278,14 +276,16 @@ async def terminate_and_remove_app_variant( try: is_last_variant_for_image = await db_manager.check_is_last_variant_for_image( - app_variant_db + str(app_variant_db.base_id), project_id ) if is_last_variant_for_image: base_db = await db_manager.fetch_base_by_id( base_id=str(app_variant_db.base_id) ) if not base_db: - raise + raise db_manager.NoResultFound( + f"Variant base with the ID {str(app_variant_db.base_id)} not found" + ) image = base_db.image logger.debug(f"is_last_variant_for_image {image}") @@ -320,14 +320,14 @@ async def terminate_and_remove_app_variant( except RuntimeError as e: logger.error(f"Failed to remove image {image} {e}") finally: - await db_manager.remove_image(image) + await db_manager.remove_image(image, project_id) # remove app variant - await db_manager.remove_app_variant_from_db(app_variant_db) + await db_manager.remove_app_variant_from_db(app_variant_db, project_id) else: # remove variant + config logger.debug("remove_app_variant_from_db") - await db_manager.remove_app_variant_from_db(app_variant_db) + await db_manager.remove_app_variant_from_db(app_variant_db, project_id) app_variants = await db_manager.list_app_variants(app_id) logger.debug(f"Count of app variants available: {len(app_variants)}") @@ -335,7 +335,7 @@ async def terminate_and_remove_app_variant( len(app_variants) == 0 ): # remove app related resources if the length of the app variants hit 0 logger.debug("remove_app_related_resources") - await remove_app_related_resources(app_id) + await remove_app_related_resources(app_id, project_id) except Exception as e: logger.error( f"An error occurred while deleting app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name}: {str(e)}" @@ -343,18 +343,19 @@ async def terminate_and_remove_app_variant( raise e from None -async def remove_app_related_resources(app_id: str): +async def remove_app_related_resources(app_id: str, project_id: str): """Removes associated tables with an app after its deletion. When an app or its last variant is deleted, this function ensures that all related resources such as environments and testsets are also deleted. Args: - app_name: The name of the app whose associated resources are to be removed. + app_name (str): The name of the app whose associated resources are to be removed. + project_id (str: The ID of the project) """ try: - await db_manager.remove_app_by_id(app_id) + await db_manager.remove_app_by_id(app_id, project_id) logger.info(f"Successfully remove app object {app_id}.") except Exception as e: logger.error( @@ -368,8 +369,8 @@ async def remove_app(app: AppDB): deletes the image from the db, shutdowns the container, deletes it and remove the image from the registry - Arguments: - app_name -- the app name to remove + Args: + app (AppDB): The application instance to remove from database. """ if app is None: @@ -380,21 +381,23 @@ async def remove_app(app: AppDB): app_variants = await db_manager.list_app_variants(str(app.id)) try: for app_variant_db in app_variants: - await terminate_and_remove_app_variant(app_variant_db=app_variant_db) + await terminate_and_remove_app_variant( + project_id=str(app_variant_db.project_id), app_variant_db=app_variant_db + ) logger.info( f"Successfully deleted app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name}." ) if len(app_variants) == 0: logger.debug("remove_app_related_resources") - await remove_app_related_resources(str(app.id)) + await remove_app_related_resources(str(app.id), str(app.project_id)) except Exception as e: # Failsafe: in case something went wrong, # delete app and its related resources try: logger.debug("remove_app_related_resources") - await remove_app_related_resources(str(app.id)) + await remove_app_related_resources(str(app.id), str(app.project_id)) except Exception as e: logger.error( f"An error occurred while deleting app {app.id} and its associated resources: {str(e)}" @@ -403,14 +406,15 @@ async def remove_app(app: AppDB): async def update_variant_parameters( - app_variant_id: str, parameters: Dict[str, Any], user_uid: str + app_variant_id: str, parameters: Dict[str, Any], user_uid: str, project_id: str ): """Updates the parameters for app variant in the database. Arguments: - app_variant -- the app variant to update - parameters -- the parameters to update - user_uid -- the user uid + app_variant (str): The app variant to update + parameters (dict): The parameters to update + user_uid (str): The UID of the user + project_id (str): The ID of the project """ assert app_variant_id is not None, "app_variant_id must be provided" @@ -418,7 +422,10 @@ async def update_variant_parameters( try: await db_manager.update_variant_parameters( - app_variant_id=app_variant_id, parameters=parameters, user_uid=user_uid + app_variant_id=app_variant_id, + parameters=parameters, + project_id=project_id, + user_uid=user_uid, ) except Exception as e: logger.error(f"Error updating app variant {app_variant_id}") @@ -427,6 +434,7 @@ async def update_variant_parameters( async def add_variant_based_on_image( app: AppDB, + project_id: str, variant_name: str, docker_id_or_template_uri: str, user_uid: str, @@ -440,6 +448,7 @@ async def add_variant_based_on_image( Args: app (AppDB): The app to add the variant to. + project_id (str): The ID of the project. variant_name (str): The name of the new variant. docker_id (str): The ID of the Docker image to use for the new variant. tags (str): The tags associated with the Docker image. @@ -475,7 +484,9 @@ async def add_variant_based_on_image( # Check if app variant already exists logger.debug("Step 2: Checking if app variant already exists") - variants = await db_manager.list_app_variants_for_app_id(app_id=str(app.id)) + variants = await db_manager.list_app_variants_for_app_id( + app_id=str(app.id), project_id=project_id + ) already_exists = any(av for av in variants if av.variant_name == variant_name) # type: ignore if already_exists: logger.error("App variant with the same name already exists") @@ -487,14 +498,10 @@ async def add_variant_based_on_image( if parsed_url.scheme and parsed_url.netloc: db_image = await db_manager.get_orga_image_instance_by_uri( template_uri=docker_id_or_template_uri, - organization_id=str(app.organization_id) if isCloudEE() else None, # type: ignore - workspace_id=str(app.workspace_id) if isCloudEE() else None, # type: ignore ) else: db_image = await db_manager.get_orga_image_instance_by_docker_id( - docker_id=docker_id_or_template_uri, - organization_id=str(app.organization_id) if isCloudEE() else None, # type: ignore - workspace_id=str(app.workspace_id) if isCloudEE() else None, # type: ignore + docker_id=docker_id_or_template_uri, project_id=project_id ) # Create new image if not exists @@ -503,22 +510,18 @@ async def add_variant_based_on_image( if parsed_url.scheme and parsed_url.netloc: db_image = await db_manager.create_image( image_type="zip", + project_id=project_id, template_uri=docker_id_or_template_uri, deletable=not (is_template_image), - user=user_instance, - organization=str(app.organization_id) if isCloudEE() else None, # noqa - workspace=str(app.workspace_id) if isCloudEE() else None, # noqa ) else: docker_id = docker_id_or_template_uri db_image = await db_manager.create_image( image_type="image", + project_id=project_id, docker_id=docker_id, tags=tags, deletable=not (is_template_image), - user=user_instance, - organization=str(app.organization_id) if isCloudEE() else None, # noqa - workspace=str(app.workspace_id) if isCloudEE() else None, # noqa ) # Create config @@ -535,9 +538,7 @@ async def add_variant_based_on_image( ] # TODO: Change this in SDK2 to directly use base_name db_base = await db_manager.create_new_variant_base( app=app, - organization=str(app.organization_id) if isCloudEE() else None, # noqa - workspace=str(app.workspace_id) if isCloudEE() else None, # noqa - user=user_instance, + project_id=project_id, base_name=base_name, # the first variant always has default base image=db_image, ) @@ -546,14 +547,13 @@ async def add_variant_based_on_image( logger.debug("Step 7: Creating app variant") db_app_variant = await db_manager.create_new_app_variant( app=app, + user=user_instance, variant_name=variant_name, + project_id=project_id, image=db_image, - user=user_instance, - organization=str(app.organization_id) if isCloudEE() else None, # noqa - workspace=str(app.workspace_id) if isCloudEE() else None, # noqa - base_name=base_name, base=db_base, config=config_db, + base_name=base_name, ) logger.debug("End: Successfully created db_app_variant: %s", db_app_variant) return db_app_variant diff --git a/agenta-backend/agenta_backend/services/auth_helper.py b/agenta-backend/agenta_backend/services/auth_helper.py index 263de7c4e..ba9f0f9ca 100644 --- a/agenta-backend/agenta_backend/services/auth_helper.py +++ b/agenta-backend/agenta_backend/services/auth_helper.py @@ -1,5 +1,14 @@ +import logging + from fastapi import Request, HTTPException +from agenta_backend.utils.project_utils import retrieve_project_id_from_request +from agenta_backend.services.db_manager import fetch_default_project, NoResultFound + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + class SessionContainer(object): """dummy class""" @@ -19,13 +28,32 @@ def inner_function(): async def authentication_middleware(request: Request, call_next): try: - # Initialize the user_id attribute in the request state if it doesn't exist + # Retrieve project_id from request + project_id_from_request = await retrieve_project_id_from_request( + request=request + ) + + # Set project_id if found or fetch default + if project_id_from_request and not hasattr(request.state, "project_id"): + setattr(request.state, "project_id", project_id_from_request) + elif not project_id_from_request: + logger.info("Retrieving default project from database...") + project = await fetch_default_project() # Fetch the default project + if project is None: + raise NoResultFound("Default project not found.") + + setattr(request.state, "project_id", str(project.id)) + logger.info( + f"Default project fetched: {str(project.id)} and set in request.state" + ) + if not hasattr(request.state, "user_id"): - user_uid_id = "0" - setattr(request.state, "user_id", user_uid_id) + setattr(request.state, "user_id", "0") # Call the next middleware or route handler response = await call_next(request) return response except Exception as e: - raise HTTPException(status_code=401, detail=str(e)) + # Handle exceptions, set status code + status_code = e.status_code if hasattr(e, "status_code") else 500 + raise HTTPException(status_code=status_code, detail=str(e)) diff --git a/agenta-backend/agenta_backend/services/container_manager.py b/agenta-backend/agenta_backend/services/container_manager.py index 7575c1efa..9a8751a3e 100644 --- a/agenta-backend/agenta_backend/services/container_manager.py +++ b/agenta-backend/agenta_backend/services/container_manager.py @@ -30,7 +30,7 @@ async def build_image(app_db: AppDB, base_name: str, tar_file: UploadFile) -> Image: app_name = app_db.app_name - user_id = str(app_db.user_id) + project_id = str(app_db.project_id) image_name = f"agentaai/{app_name.lower()}_{base_name.lower()}:latest" # Get event loop @@ -47,6 +47,7 @@ async def build_image(app_db: AppDB, base_name: str, tar_file: UploadFile) -> Im tar_path = temp_dir / tar_file.filename with tar_path.open("wb") as buffer: buffer.write(await tar_file.read()) + future = loop.run_in_executor( thread_pool, build_image_job, @@ -56,7 +57,7 @@ async def build_image(app_db: AppDB, base_name: str, tar_file: UploadFile) -> Im tar_path, image_name, temp_dir, - user_id, + project_id, ), ) image_result = await asyncio.wrap_future(future) @@ -69,7 +70,7 @@ def build_image_job( tar_path: Path, image_name: str, temp_dir: Path, - user_id: str, + project_id: str, ) -> Image: """Business logic for building a docker image from a tar file @@ -86,7 +87,7 @@ def build_image_job( image that will be built. It is used as the tag for the image temp_dir -- The `temp_dir` parameter is a `Path` object that represents the temporary directory where the contents of the tar file will be extracted - user_id -- The id of the user that owns the app + project_id -- The id of the project that owns the app Raises: HTTPException: _description_ @@ -108,7 +109,7 @@ def build_image_job( image, build_log = client.images.build( path=str(temp_dir), tag=image_name, - buildargs={"ROOT_PATH": f"/{user_id}/{app_name}/{base_name}"}, + buildargs={"ROOT_PATH": f"/{project_id}/{app_name}/{base_name}"}, rm=True, dockerfile=dockerfile, pull=True, diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py index 9bf9b5ab9..70293e23b 100644 --- a/agenta-backend/agenta_backend/services/db_manager.py +++ b/agenta-backend/agenta_backend/services/db_manager.py @@ -28,6 +28,7 @@ AppDB_ as AppDB, UserDB_ as UserDB, ImageDB_ as ImageDB, + ProjectDB_ as ProjectDB, TestSetDB_ as TestSetDB, AppVariantDB_ as AppVariantDB, EvaluationDB_ as EvaluationDB, @@ -49,6 +50,7 @@ AppDB, UserDB, ImageDB, + ProjectDB, TestSetDB, AppVariantDB, EvaluationDB, @@ -93,27 +95,18 @@ async def add_testset_to_app_variant( - app_id: str, - template_name: str, - app_name: str, - user_uid: str, - org_id: Optional[str] = None, - workspace_id: Optional[str] = None, + template_name: str, app_name: str, project_id: str ): """Add testset to app variant. + Args: - app_id (str): The id of the app - org_id (str): The id of the organization template_name (str): The name of the app template image app_name (str): The name of the app - user_uid (str): The uid of the user + project_id (str): The ID of the project """ async with db_engine.get_session() as session: try: - app_db = await get_app_instance_by_id(app_id) - user_db = await get_user(user_uid) - json_path = os.path.join( PARENT_DIRECTORY, "resources", @@ -129,22 +122,9 @@ async def add_testset_to_app_variant( } testset_db = TestSetDB( **testset, - app_id=app_db.id, - user_id=user_db.id, + project_id=uuid.UUID(project_id), ) - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - org_id is not None and workspace_id is not None - ), "organization and workspace must be provided together" - - organization_db = await db_manager_ee.get_organization(org_id) # type: ignore - workspace_db = await db_manager_ee.get_workspace(workspace_id) # type: ignore - - testset_db.organization_id = organization_db.id - testset_db.workspace_id = workspace_db.id - session.add(testset_db) await session.commit() await session.refresh(testset_db) @@ -182,20 +162,12 @@ async def fetch_app_by_id(app_id: str) -> AppDB: app_uuid = await get_object_uuid(object_id=app_id, table_name="app_db") async with db_engine.get_session() as session: base_query = select(AppDB).filter_by(id=uuid.UUID(app_uuid)) - if isCloudEE(): - base_query = base_query.options( - joinedload(AppDB.workspace).joinedload(WorkspaceDB.members), # type: ignore - joinedload(AppDB.organization), - ) - result = await session.execute(base_query) app = result.unique().scalars().first() return app -async def fetch_app_variant_by_id( - app_variant_id: str, -) -> Optional[AppVariantDB]: +async def fetch_app_variant_by_id(app_variant_id: str) -> Optional[AppVariantDB]: """ Fetches an app variant by its ID. @@ -214,13 +186,10 @@ async def fetch_app_variant_by_id( ) if isCloudEE(): query = base_query.options( - joinedload(AppVariantDB.organization), - joinedload(AppVariantDB.user.of_type(UserDB)).load_only(UserDB.uid), # type: ignore joinedload(AppVariantDB.image.of_type(ImageDB)).load_only(ImageDB.docker_id, ImageDB.tags), # type: ignore ) else: query = base_query.options( - joinedload(AppVariantDB.user).load_only(UserDB.uid), # type: ignore joinedload(AppVariantDB.image).load_only(ImageDB.docker_id, ImageDB.tags), # type: ignore ) @@ -276,7 +245,7 @@ async def fetch_app_variant_by_base_id_and_config_name( async def fetch_app_variant_revision_by_variant( - app_variant_id: str, revision: int + app_variant_id: str, project_id: str, revision: int ) -> AppVariantRevisionsDB: """Fetches app variant revision by variant id and revision @@ -294,7 +263,9 @@ async def fetch_app_variant_revision_by_variant( async with db_engine.get_session() as session: result = await session.execute( select(AppVariantRevisionsDB).filter_by( - variant_id=uuid.UUID(app_variant_id), revision=revision + variant_id=uuid.UUID(app_variant_id), + project_id=uuid.UUID(project_id), + revision=revision, ) ) app_variant_revision = result.scalars().first() @@ -308,8 +279,10 @@ async def fetch_app_variant_revision_by_variant( async def fetch_base_by_id(base_id: str) -> Optional[VariantBaseDB]: """ Fetches a base by its ID. + Args: base_id (str): The ID of the base to fetch. + Returns: VariantBaseDB: The fetched base, or None if no base was found. """ @@ -353,20 +326,16 @@ async def fetch_app_variant_by_name_and_appid( async def create_new_variant_base( app: AppDB, - user: UserDB, + project_id: str, base_name: str, image: ImageDB, - organization=None, - workspace=None, ) -> VariantBaseDB: """Create a new base. Args: base_name (str): The name of the base. image (ImageDB): The image of the base. - user (UserDB): The User Object creating the variant. + project_id (str): The ID of the project app (AppDB): The associated App Object. - organization (OrganizationDB): The Organization the variant belongs to. - workspace (WorkspaceDB): The Workspace the variant belongs to. Returns: VariantBaseDB: The created base. """ @@ -375,20 +344,11 @@ async def create_new_variant_base( async with db_engine.get_session() as session: base = VariantBaseDB( app_id=app.id, - user_id=user.id, + project_id=uuid.UUID(project_id), base_name=base_name, image_id=image.id, ) - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - organization is not None and workspace is not None - ), "organization and workspace must be provided together" - - base.organization_id = uuid.UUID(organization) - base.workspace_id = uuid.UUID(workspace) - session.add(base) await session.commit() await session.refresh(base) @@ -420,19 +380,22 @@ async def create_new_app_variant( app: AppDB, user: UserDB, variant_name: str, + project_id: str, image: ImageDB, base: VariantBaseDB, config: ConfigDB, base_name: str, - organization=None, - workspace=None, ) -> AppVariantDB: """Create a new variant. + Args: variant_name (str): The name of the variant. + project_id (str): The ID of the project. image (ImageDB): The image of the variant. base (VariantBaseDB): The base of the variant. config (ConfigDB): The config of the variant. + base_name (str): The name of the variant base. + Returns: AppVariantDB: The created variant. """ @@ -444,7 +407,7 @@ async def create_new_app_variant( async with db_engine.get_session() as session: variant = AppVariantDB( app_id=app.id, - user_id=user.id, + project_id=uuid.UUID(project_id), modified_by_id=user.id, revision=0, variant_name=variant_name, @@ -455,35 +418,22 @@ async def create_new_app_variant( config_parameters=config.parameters, ) - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - organization is not None and workspace is not None - ), "organization and workspace must be provided together" - - variant.organization_id = uuid.UUID(organization) - variant.workspace_id = uuid.UUID(workspace) - session.add(variant) - attributes_to_refresh = [ - "app", - "image", - "user", - "base", - ] - if isCloudEE(): - attributes_to_refresh.extend(["organization", "workspace"]) - await session.commit() await session.refresh( variant, - attribute_names=attributes_to_refresh, + attribute_names=[ + "app", + "image", + "base", + ], ) # Ensures the app, image, user and base relationship are loaded variant_revision = AppVariantRevisionsDB( variant_id=variant.id, revision=0, + project_id=uuid.UUID(project_id), modified_by_id=user.id, base_id=base.id, config_name=config.config_name, @@ -499,22 +449,20 @@ async def create_new_app_variant( async def create_image( image_type: str, - user: UserDB, + project_id: str, deletable: bool, - organization=None, - workspace=None, template_uri: Optional[str] = None, docker_id: Optional[str] = None, tags: Optional[str] = None, ) -> ImageDB: """Create a new image. Args: + image_type (str): The type of image to create. + project_id (str): The ID of the project. docker_id (str): The ID of the image. - tags (str): The tags of the image. - user (UserDB): The user that the image belongs to. deletable (bool): Whether the image can be deleted. - organization (OrganizationDB): The organization that the image belongs to. - workspace (WorkspaceDB): The workspace that the image belongs to. + tags (str): The tags of the image. + Returns: ImageDB: The created image. """ @@ -537,7 +485,7 @@ async def create_image( async with db_engine.get_session() as session: image = ImageDB( deletable=deletable, - user_id=user.id, + project_id=uuid.UUID(project_id), ) image_types = {"zip": TemplateType.ZIP.value, "image": TemplateType.IMAGE.value} @@ -553,15 +501,6 @@ async def create_image( image.tags = tags # type: ignore image.docker_id = docker_id # type: ignore - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - organization is not None and workspace is not None - ), "organization and workspace must be provided together" - - image.organization_id = uuid.UUID(organization) - image.workspace_id = uuid.UUID(workspace) - session.add(image) await session.commit() await session.refresh(image) @@ -571,24 +510,22 @@ async def create_image( async def create_deployment( app_id: str, - user_id: str, + project_id: str, container_name: str, container_id: str, uri: str, status: str, - organization=None, - workspace=None, ) -> DeploymentDB: """Create a new deployment. + Args: app (str): The app to create the deployment for. - organization (OrganizationDB): The organization that the deployment belongs to. - workspace (WorkspaceDB): The Workspace that the deployment belongs to. - user (str): The user that the deployment belongs to. + project_id (str): The ID of the project to create the deployment for. container_name (str): The name of the container. container_id (str): The ID of the container. uri (str): The URI of the container. status (str): The status of the container. + Returns: DeploymentDB: The created deployment. """ @@ -597,17 +534,13 @@ async def create_deployment( try: deployment = DeploymentDB( app_id=uuid.UUID(app_id), - user_id=uuid.UUID(user_id), + project_id=uuid.UUID(project_id), container_name=container_name, container_id=container_id, uri=uri, status=status, ) - if isCloudEE(): - deployment.organization_id = uuid.UUID(organization) - deployment.workspace_id = uuid.UUID(workspace) - session.add(deployment) await session.commit() await session.refresh(deployment) @@ -618,19 +551,14 @@ async def create_deployment( async def create_app_and_envs( - app_name: str, - user_uid: str, - organization_id: Optional[str] = None, - workspace_id: Optional[str] = None, + app_name: str, project_id: Optional[str] = None, workspace_id: Optional[str] = None ) -> AppDB: """ Create a new app with the given name and organization ID. Args: app_name (str): The name of the app to create. - user_uid (str): The UID of the user that the app belongs to. - organization_id (str): The ID of the organization that the app belongs to. - workspace_id (str): The ID of the workspace that the app belongs to. + project_id (str): The ID of the project. Returns: AppDB: The created app. @@ -639,30 +567,18 @@ async def create_app_and_envs( ValueError: If an app with the same name already exists. """ - user = await get_user(user_uid) + if isCloudEE(): + project = await db_manager_ee.get_project_by_workspace(workspace_id) + project_id = str(project.id) + app = await fetch_app_by_name_and_parameters( - app_name, - user_uid, - organization_id, - workspace_id, + app_name=app_name, project_id=project_id ) if app is not None: raise ValueError("App with the same name already exists") async with db_engine.get_session() as session: - app = AppDB(app_name=app_name, user_id=user.id) - - if isCloudEE(): - # assert that if organization_id is provided, workspace_id is also provided, and vice versa - assert ( - organization_id is not None and workspace_id is not None - ), "org_id and workspace_id must be provided together" - - organization_db = await db_manager_ee.get_organization(organization_id) # type: ignore - workspace_db = await db_manager_ee.get_workspace(workspace_id) # type: ignore - - app.organization_id = organization_db.id - app.workspace_id = workspace_db.id + app = AppDB(app_name=app_name, project_id=uuid.UUID(project_id)) session.add(app) await session.commit() @@ -693,9 +609,7 @@ async def update_app(app_id: str, values_to_update: dict) -> None: await session.commit() -async def get_deployment_by_id( - deployment_id: str, -) -> DeploymentDB: +async def get_deployment_by_id(deployment_id: str) -> DeploymentDB: """Get the deployment object from the database with the provided id. Arguments: @@ -732,13 +646,14 @@ async def get_deployment_by_appid(app_id: str) -> DeploymentDB: return deployment -async def list_app_variants_for_app_id( - app_id: str, -): +async def list_app_variants_for_app_id(app_id: str, project_id: str): """ Lists all the app variants from the db + Args: - app_name: if specified, only returns the variants for the app name + app_name (str): if specified, only returns the variants for the app name + project_id (str): The ID of the project. + Returns: List[AppVariant]: List of AppVariant objects """ @@ -746,7 +661,9 @@ async def list_app_variants_for_app_id( assert app_id is not None, "app_id cannot be None" async with db_engine.get_session() as session: result = await session.execute( - select(AppVariantDB).filter_by(app_id=uuid.UUID(app_id)) + select(AppVariantDB).filter_by( + app_id=uuid.UUID(app_id), project_id=uuid.UUID(project_id) + ) ) app_variants = result.scalars().all() return app_variants @@ -777,8 +694,10 @@ async def list_bases_for_app_id(app_id: str, base_name: Optional[str] = None): async def list_variants_for_base(base: VariantBaseDB): """ Lists all the app variants from the db for a base + Args: - base: if specified, only returns the variants for the base + base (VariantBaseDB): if specified, only returns the variants for the base + Returns: List[AppVariant]: List of AppVariant objects """ @@ -900,48 +819,31 @@ async def get_users_by_ids(user_ids: List): async def get_orga_image_instance_by_docker_id( - docker_id: str, - organization_id: Optional[str] = None, - workspace_id: Optional[str] = None, + docker_id: str, project_id: str ) -> ImageDB: """Get the image object from the database with the provided id. Arguments: - organization_id (str): The organization unique identifier docker_id (str): The image id + project_id (str): The ID of project. Returns: ImageDB: instance of image object """ async with db_engine.get_session() as session: - query = select(ImageDB).filter_by(docker_id=docker_id) - - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - organization_id is not None and workspace_id is not None - ), "organization and workspace must be provided together" - - query = query.filter_by( - organization_id=uuid.UUID(organization_id), - workspace_id=workspace_id, - ) - + query = select(ImageDB).filter_by( + docker_id=docker_id, project_id=uuid.UUID(project_id) + ) result = await session.execute(query) image = result.scalars().first() return image -async def get_orga_image_instance_by_uri( - template_uri: str, - organization_id: Optional[str] = None, - workspace_id: Optional[str] = None, -) -> ImageDB: +async def get_orga_image_instance_by_uri(template_uri: str) -> ImageDB: """Get the image object from the database with the provided id. Arguments: - organization_id (str): The organization unique identifier template_uri (url): The image template url Returns: @@ -954,18 +856,6 @@ async def get_orga_image_instance_by_uri( async with db_engine.get_session() as session: query = select(ImageDB).filter_by(template_uri=template_uri) - - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - organization_id is not None and workspace_id is not None - ), "organization and workspace must be provided together" - - query = query.filter_by( - organization_id=uuid.UUID(organization_id), - workspace_id=workspace_id, - ) - result = await session.execute(query) image = result.scalars().first() return image @@ -992,6 +882,7 @@ async def add_variant_from_base_and_config( new_config_name: str, parameters: Dict[str, Any], user_uid: str, + project_id: str, ) -> AppVariantDB: """ Add a new variant to the database based on an existing base and a new configuration. @@ -1001,13 +892,16 @@ async def add_variant_from_base_and_config( new_config_name (str): The name of the new configuration to use for the new variant. parameters (Dict[str, Any]): The parameters to use for the new configuration. user_uid (str): The UID of the user + project_id (str): The ID of the project Returns: AppVariantDB: The newly created app variant. """ new_variant_name = f"{base_db.base_name}.{new_config_name}" - previous_app_variant_db = await find_previous_variant_from_base_id(str(base_db.id)) + previous_app_variant_db = await find_previous_variant_from_base_id( + str(base_db.id), project_id + ) if previous_app_variant_db is None: logger.error("Failed to find the previous app variant in the database.") raise HTTPException(status_code=404, detail="Previous app variant not found") @@ -1027,19 +921,15 @@ async def add_variant_from_base_and_config( app_id=previous_app_variant_db.app_id, variant_name=new_variant_name, image_id=base_db.image_id, - user_id=user_db.id, modified_by_id=user_db.id, revision=1, base_name=base_db.base_name, + project_id=uuid.UUID(project_id), base_id=base_db.id, config_name=new_config_name, config_parameters=parameters, ) - if isCloudEE(): - db_app_variant.organization_id = previous_app_variant_db.organization_id - db_app_variant.workspace_id = previous_app_variant_db.workspace_id - session.add(db_app_variant) await session.commit() await session.refresh(db_app_variant) @@ -1048,6 +938,7 @@ async def add_variant_from_base_and_config( variant_id=db_app_variant.id, revision=1, modified_by_id=user_db.id, + project_id=uuid.UUID(project_id), base_id=base_db.id, config_name=new_config_name, config_parameters=parameters, @@ -1061,9 +952,9 @@ async def add_variant_from_base_and_config( async def list_apps( + project_id: str, user_uid: str, app_name: Optional[str] = None, - org_id: Optional[str] = None, workspace_id: Optional[str] = None, ): """ @@ -1076,37 +967,21 @@ async def list_apps( List[App] """ - user = await get_user(user_uid) - assert user is not None, "User is None" - if app_name is not None: app_db = await fetch_app_by_name_and_parameters( - app_name=app_name, - user_uid=user_uid, - organization_id=org_id, - workspace_id=workspace_id, + app_name=app_name, project_id=project_id ) return [converters.app_db_to_pydantic(app_db)] - elif org_id is not None or workspace_id is not None: - if not isCloudEE(): - raise HTTPException( - status_code=400, - detail={ - "error": "organization and/or workspace is only available in Cloud and EE" - }, - ) - - # assert that if org_id is provided, workspace_id is also provided, and vice versa - assert ( - org_id is not None and workspace_id is not None - ), "org_id and workspace_id must be provided together" + elif isCloudEE(): if isCloudEE(): + project = await db_manager_ee.get_project_by_workspace( + workspace_id=workspace_id + ) user_org_workspace_data = await get_user_org_and_workspace_id(user_uid) # type: ignore has_permission = await check_rbac_permission( # type: ignore user_org_workspace_data=user_org_workspace_data, - workspace_id=workspace_id, - organization_id=org_id, + project_id=str(project.id), permission=Permission.VIEW_APPLICATION, # type: ignore ) logger.debug(f"User has Permission to list apps: {has_permission}") @@ -1118,17 +993,16 @@ async def list_apps( async with db_engine.get_session() as session: result = await session.execute( - select(AppDB).filter_by( - organization_id=org_id, - workspace_id=workspace_id, - ) + select(AppDB).filter_by(project_id=project.id) ) apps = result.unique().scalars().all() return [converters.app_db_to_pydantic(app) for app in apps] else: async with db_engine.get_session() as session: - result = await session.execute(select(AppDB).filter_by(user_id=user.id)) + result = await session.execute( + select(AppDB).filter_by(project_id=uuid.UUID(project_id)) + ) apps = result.unique().scalars().all() return [converters.app_db_to_pydantic(app) for app in apps] @@ -1136,8 +1010,11 @@ async def list_apps( async def list_app_variants(app_id: str): """ Lists all the app variants from the db + Args: - app_name: if specified, only returns the variants for the app name + app_name (str): if specified, only returns the variants for the app name + project_id (str): The ID of the project + Returns: List[AppVariant]: List of AppVariant objects """ @@ -1156,7 +1033,9 @@ async def list_app_variants(app_id: str): return app_variants -async def check_is_last_variant_for_image(db_app_variant: AppVariantDB) -> bool: +async def check_is_last_variant_for_image( + variant_base_id: str, project_id: str +) -> bool: """Checks whether the input variant is the sole variant that uses its linked image. NOTE: This is a helpful function to determine whether to delete the image when removing a variant. Usually many variants will use the same image (these variants would have been created using the UI). We only delete the image and shutdown the container if the variant is the last one using the image @@ -1169,14 +1048,9 @@ async def check_is_last_variant_for_image(db_app_variant: AppVariantDB) -> bool: """ async with db_engine.get_session() as session: - query = select(AppVariantDB).filter_by(base_id=db_app_variant.base_id) - - if isCloudEE(): - query = query.filter( - AppVariantDB.organization_id == db_app_variant.organization_id, - AppVariantDB.workspace_id == db_app_variant.workspace_id, - ) - + query = select(AppVariantDB).filter_by( + base_id=uuid.UUID(variant_base_id), project_id=uuid.UUID(project_id) + ) count_result = await session.execute( query.with_only_columns(func.count()) # type: ignore ) @@ -1224,19 +1098,22 @@ async def list_deployments(app_id: str): return environments -async def remove_app_variant_from_db(app_variant_db: AppVariantDB): +async def remove_app_variant_from_db(app_variant_db: AppVariantDB, project_id: str): """Remove an app variant from the db the logic for removing the image is in app_manager.py - Arguments: - app_variant -- AppVariant to remove + Args: + app_variant (AppVariantDB): the application variant to remove + project_id (str): The ID of the project """ logger.debug("Removing app variant") assert app_variant_db is not None, "app_variant_db is missing" logger.debug("list_app_variants_revisions_by_variant") - app_variant_revisions = await list_app_variant_revisions_by_variant(app_variant_db) + app_variant_revisions = await list_app_variant_revisions_by_variant( + app_variant_db, project_id + ) async with db_engine.get_session() as session: # Delete all the revisions associated with the variant @@ -1267,7 +1144,7 @@ async def deploy_to_environment( app_variant_db = await fetch_app_variant_by_id(variant_id) app_variant_revision_db = await fetch_app_variant_revision_by_variant( - app_variant_id=variant_id, revision=app_variant_db.revision # type: ignore + app_variant_id=variant_id, project_id=str(app_variant_db.project_id), revision=app_variant_db.revision # type: ignore ) if app_variant_db is None: raise ValueError("App variant not found") @@ -1283,7 +1160,9 @@ async def deploy_to_environment( # Find the environment for the given app name and user result = await session.execute( select(AppEnvironmentDB).filter_by( - app_id=app_variant_db.app_id, name=environment_name + app_id=app_variant_db.app_id, + project_id=app_variant_db.project_id, + name=environment_name, ) ) environment_db = result.scalars().first() @@ -1301,6 +1180,7 @@ async def deploy_to_environment( session, environment_db, user, + str(app_variant_db.project_id), deployed_app_variant_revision=app_variant_revision_db, deployment=deployment, ) @@ -1474,7 +1354,7 @@ async def list_environments(app_id: str, **kwargs: dict): AppVariantRevisionsDB.config_parameters, # type: ignore ) ) - .filter_by(app_id=uuid.UUID(app_id)) + .filter_by(app_id=uuid.UUID(app_id), project_id=app_instance.project_id) ) environments_db = result.scalars().all() return environments_db @@ -1515,13 +1395,9 @@ async def create_environment( """ environment_db = AppEnvironmentDB( - app_id=app_db.id, name=name, user_id=app_db.user_id, revision=0 + app_id=app_db.id, name=name, project_id=app_db.project_id, revision=0 ) - if isCloudEE(): - environment_db.organization_id = app_db.organization_id - environment_db.workspace_id = app_db.workspace_id - session.add(environment_db) await session.commit() await session.refresh(environment_db) @@ -1530,13 +1406,18 @@ async def create_environment( async def create_environment_revision( - session: AsyncSession, environment: AppEnvironmentDB, user: UserDB, **kwargs: dict + session: AsyncSession, + environment: AppEnvironmentDB, + user: UserDB, + project_id: str, + **kwargs: dict, ): """Creates a new environment revision. Args: environment (AppEnvironmentDB): The environment to create a revision for. user (UserDB): The user that made the deployment. + project_id (str): The ID of the project. """ assert environment is not None, "environment cannot be None" @@ -1546,6 +1427,7 @@ async def create_environment_revision( environment_id=environment.id, revision=environment.revision, modified_by_id=user.id, + project_id=uuid.UUID(project_id), ) if kwargs: @@ -1572,27 +1454,26 @@ async def create_environment_revision( if deployment is not None: environment_revision.deployment_id = deployment.id # type: ignore - if isCloudEE(): - environment_revision.organization_id = environment.organization_id - environment_revision.workspace_id = environment.workspace_id - session.add(environment_revision) async def list_app_variant_revisions_by_variant( - app_variant: AppVariantDB, + app_variant: AppVariantDB, project_id: str ): """Returns list of app variant revision for the given app variant Args: app_variant (AppVariantDB): The app variant to retrieve environments for. + project_id (str): The ID of the project. Returns: List[AppVariantRevisionsDB]: A list of AppVariantRevisionsDB objects. """ async with db_engine.get_session() as session: - base_query = select(AppVariantRevisionsDB).filter_by(variant_id=app_variant.id) + base_query = select(AppVariantRevisionsDB).filter_by( + variant_id=app_variant.id, project_id=uuid.UUID(project_id) + ) if isCloudEE(): base_query = base_query.options( joinedload(AppVariantRevisionsDB.modified_by.of_type(UserDB)).load_only( @@ -1636,12 +1517,13 @@ async def fetch_app_variant_revision(app_variant: str, revision_number: int): return app_variant_revisions -async def remove_image(image: ImageDB): +async def remove_image(image: ImageDB, project_id: str): """ Removes an image from the database. Args: image (ImageDB): The image to remove from the database. + project_id (str): The ID of the project the image belongs to. Raises: ValueError: If the image is None. @@ -1654,7 +1536,9 @@ async def remove_image(image: ImageDB): raise ValueError("Image is None") async with db_engine.get_session() as session: - result = await session.execute(select(ImageDB).filter_by(id=image.id)) + result = await session.execute( + select(ImageDB).filter_by(id=image.id, project_id=uuid.UUID(project_id)) + ) image = result.scalars().first() await session.delete(image) @@ -1757,12 +1641,13 @@ async def remove_base_from_db(base_id: str): await session.commit() -async def remove_app_by_id(app_id: str): +async def remove_app_by_id(app_id: str, project_id: str): """ Removes an app instance from the database by its ID. Args: app_id (str): The ID of the app instance to remove. + project_id (str): The ID of the project. Raises: AssertionError: If app_id is None or if the app instance could not be found. @@ -1773,7 +1658,11 @@ async def remove_app_by_id(app_id: str): assert app_id is not None, "app_id cannot be None" async with db_engine.get_session() as session: - result = await session.execute(select(AppDB).filter_by(id=uuid.UUID(app_id))) + result = await session.execute( + select(AppDB).filter_by( + id=uuid.UUID(app_id), project_id=uuid.UUID(project_id) + ) + ) app_db = result.scalars().first() if not app_db: raise NoResultFound(f"App with id {app_id} not found") @@ -1783,7 +1672,7 @@ async def remove_app_by_id(app_id: str): async def update_variant_parameters( - app_variant_id: str, parameters: Dict[str, Any], user_uid: str + app_variant_id: str, parameters: Dict[str, Any], project_id: str, user_uid: str ) -> None: """ Update the parameters of an app variant in the database. @@ -1791,6 +1680,7 @@ async def update_variant_parameters( Args: app_variant_id (str): The app variant ID. parameters (Dict[str, Any]): The new parameters to set for the app variant. + project_id (str): The ID of the project. user_uid (str): The UID of the user that is updating the app variant. Raises: @@ -1800,7 +1690,9 @@ async def update_variant_parameters( user = await get_user(user_uid) async with db_engine.get_session() as session: result = await session.execute( - select(AppVariantDB).filter_by(id=uuid.UUID(app_variant_id)) + select(AppVariantDB).filter_by( + id=uuid.UUID(app_variant_id), project_id=uuid.UUID(project_id) + ) ) app_variant_db = result.scalars().first() if not app_variant_db: @@ -1824,6 +1716,7 @@ async def update_variant_parameters( variant_id=app_variant_db.id, revision=app_variant_db.revision, modified_by_id=user.id, + project_id=uuid.UUID(project_id), base_id=app_variant_db.base_id, config_name=app_variant_db.config_name, config_parameters=app_variant_db.config_parameters, @@ -1833,11 +1726,14 @@ async def update_variant_parameters( await session.commit() -async def get_app_variant_instance_by_id(variant_id: str) -> AppVariantDB: +async def get_app_variant_instance_by_id( + variant_id: str, project_id: str +) -> AppVariantDB: """Get the app variant object from the database with the provided id. Arguments: variant_id (str): The app variant unique identifier + project_id (str): The ID of the project Returns: AppVariantDB: instance of app variant object @@ -1850,7 +1746,7 @@ async def get_app_variant_instance_by_id(variant_id: str) -> AppVariantDB: joinedload(AppVariantDB.app.of_type(AppDB)).load_only(AppDB.id, AppDB.app_name), # type: ignore joinedload(AppVariantDB.base.of_type(VariantBaseDB)).joinedload(VariantBaseDB.deployment.of_type(DeploymentDB)).load_only(DeploymentDB.uri), # type: ignore ) - .filter_by(id=uuid.UUID(variant_id)) + .filter_by(id=uuid.UUID(variant_id), project_id=uuid.UUID(project_id)), ) app_variant_db = result.scalars().first() return app_variant_db @@ -1878,25 +1774,21 @@ async def fetch_testset_by_id(testset_id: str) -> Optional[TestSetDB]: return testset -async def create_testset(app: AppDB, user_uid: str, testset_data: Dict[str, Any]): +async def create_testset(app: AppDB, project_id: str, testset_data: Dict[str, Any]): """ Creates a testset. Args: app (AppDB): The app object - user_uid (str): The user uID + project_id (str): The ID of the project testset_data (dict): The data of the testset to create with Returns: returns the newly created TestsetDB """ - user = await get_user(user_uid=user_uid) async with db_engine.get_session() as session: - testset_db = TestSetDB(**testset_data, app_id=app.id, user_id=user.id) - if isCloudEE(): - testset_db.organization_id = app.organization_id - testset_db.workspace_id = app.workspace_id + testset_db = TestSetDB(**testset_data, project_id=uuid.UUID(project_id)) session.add(testset_db) await session.commit() @@ -1928,18 +1820,19 @@ async def update_testset(testset_id: str, values_to_update: dict) -> None: await session.refresh(testset) -async def fetch_testsets_by_app_id(app_id: str): - """Fetches all testsets for a given app. +async def fetch_testsets_by_project_id(project_id: str): + """Fetches all testsets for a given project. + Args: - app_id (str): The ID of the app to fetch testsets for. + project_id (str): The ID of the project. + Returns: List[TestSetDB]: The fetched testsets. """ - assert app_id is not None, "app_id cannot be None" async with db_engine.get_session() as session: result = await session.execute( - select(TestSetDB).filter_by(app_id=uuid.UUID(app_id)) + select(TestSetDB).filter_by(project_id=uuid.UUID(project_id)) ) testsets = result.scalars().all() return testsets @@ -1960,14 +1853,13 @@ async def fetch_evaluation_by_id(evaluation_id: str) -> Optional[EvaluationDB]: base_query = select(EvaluationDB).filter_by(id=uuid.UUID(evaluation_id)) if isCloudEE(): query = base_query.options( - joinedload(EvaluationDB.user.of_type(UserDB)).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(EvaluationDB.testset.of_type(TestSetDB)).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) else: query = base_query.options( - joinedload(EvaluationDB.user).load_only(UserDB.username), # type: ignore joinedload(EvaluationDB.testset).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) + result = await session.execute( query.options( joinedload(EvaluationDB.variant.of_type(AppVariantDB)).load_only(AppVariantDB.id, AppVariantDB.variant_name), # type: ignore @@ -1983,7 +1875,7 @@ async def fetch_evaluation_by_id(evaluation_id: str) -> Optional[EvaluationDB]: return evaluation -async def list_human_evaluations(app_id: str): +async def list_human_evaluations(app_id: str, project_id: str): """ Fetches human evaluations belonging to an App. @@ -1994,17 +1886,15 @@ async def list_human_evaluations(app_id: str): async with db_engine.get_session() as session: base_query = ( select(HumanEvaluationDB) - .filter_by(app_id=uuid.UUID(app_id)) + .filter_by(app_id=uuid.UUID(app_id), project_id=uuid.UUID(project_id)) .filter(HumanEvaluationDB.testset_id.isnot(None)) ) if isCloudEE(): query = base_query.options( - joinedload(HumanEvaluationDB.user.of_type(UserDB)).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(HumanEvaluationDB.testset.of_type(TestSetDB)).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) else: query = base_query.options( - joinedload(HumanEvaluationDB.user).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(HumanEvaluationDB.testset).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) result = await session.execute(query) @@ -2014,7 +1904,6 @@ async def list_human_evaluations(app_id: str): async def create_human_evaluation( app: AppDB, - user_id: str, status: str, evaluation_type: str, testset_id: str, @@ -2025,7 +1914,6 @@ async def create_human_evaluation( Args: app (AppDB: The app object - user_id (id): The ID of the user status (str): The status of the evaluation evaluation_type (str): The evaluation type testset_id (str): The ID of the evaluation testset @@ -2035,14 +1923,11 @@ async def create_human_evaluation( async with db_engine.get_session() as session: human_evaluation = HumanEvaluationDB( app_id=app.id, - user_id=uuid.UUID(user_id), + project_id=app.project_id, status=status, evaluation_type=evaluation_type, testset_id=testset_id, ) - if isCloudEE(): - human_evaluation.organization_id = str(app.organization_id) - human_evaluation.workspace_id = str(app.workspace_id) session.add(human_evaluation) await session.commit() @@ -2050,7 +1935,8 @@ async def create_human_evaluation( # create variants for human evaluation await create_human_evaluation_variants( - human_evaluation_id=str(human_evaluation.id), variants_ids=variants_ids + human_evaluation_id=str(human_evaluation.id), + variants_ids=variants_ids, ) return human_evaluation @@ -2098,6 +1984,7 @@ async def create_human_evaluation_variants( Args: human_evaluation_id (str): The human evaluation identifier variants_ids (List[str]): The variants identifiers + project_id (str): The project ID """ variants_dict = {} @@ -2109,7 +1996,7 @@ async def create_human_evaluation_variants( variants_revisions_dict = {} for variant_id, variant in variants_dict.items(): variant_revision = await fetch_app_variant_revision_by_variant( - app_variant_id=str(variant.id), revision=variant.revision # type: ignore + app_variant_id=str(variant.id), project_id=str(variant.project_id), revision=variant.revision # type: ignore ) if variant_revision: variants_revisions_dict[variant_id] = variant_revision @@ -2134,9 +2021,12 @@ async def create_human_evaluation_variants( async def fetch_human_evaluation_by_id( evaluation_id: str, ) -> Optional[HumanEvaluationDB]: - """Fetches a evaluation by its ID. + """ + Fetches a evaluation by its ID. + Args: evaluation_id (str): The ID of the evaluation to fetch. + Returns: EvaluationDB: The fetched evaluation, or None if no evaluation was found. """ @@ -2146,12 +2036,10 @@ async def fetch_human_evaluation_by_id( base_query = select(HumanEvaluationDB).filter_by(id=uuid.UUID(evaluation_id)) if isCloudEE(): query = base_query.options( - joinedload(HumanEvaluationDB.user.of_type(UserDB)).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(HumanEvaluationDB.testset.of_type(TestSetDB)).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) else: query = base_query.options( - joinedload(HumanEvaluationDB.user).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(HumanEvaluationDB.testset).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) result = await session.execute(query) @@ -2208,8 +2096,7 @@ async def delete_human_evaluation(evaluation_id: str): async def create_human_evaluation_scenario( inputs: List[HumanEvaluationScenarioInput], - user_id: str, - app: AppDB, + project_id: str, evaluation_id: str, evaluation_extend: Dict[str, Any], ): @@ -2218,8 +2105,6 @@ async def create_human_evaluation_scenario( Args: inputs (List[HumanEvaluationScenarioInput]): The inputs. - user_id (str): The user ID. - app (AppDB): The app object. evaluation_id (str): The evaluation identifier. evaluation_extend (Dict[str, any]): An extended required payload for the evaluation scenario. Contains score, vote, and correct_answer. """ @@ -2227,16 +2112,12 @@ async def create_human_evaluation_scenario( async with db_engine.get_session() as session: evaluation_scenario = HumanEvaluationScenarioDB( **evaluation_extend, - user_id=uuid.UUID(user_id), + project_id=uuid.UUID(project_id), evaluation_id=uuid.UUID(evaluation_id), - inputs=[input.dict() for input in inputs], + inputs=[input.model_dump() for input in inputs], outputs=[], ) - if isCloudEE(): - evaluation_scenario.organization_id = str(app.organization_id) - evaluation_scenario.workspace_id = str(app.workspace_id) - session.add(evaluation_scenario) await session.commit() @@ -2295,12 +2176,13 @@ async def fetch_human_evaluation_scenarios(evaluation_id: str): return evaluation_scenarios -async def fetch_evaluation_scenarios(evaluation_id: str): +async def fetch_evaluation_scenarios(evaluation_id: str, project_id: str): """ Fetches evaluation scenarios. Args: evaluation_id (str): The evaluation identifier + project_id (str): The ID of the project Returns: The evaluation scenarios. @@ -2309,7 +2191,9 @@ async def fetch_evaluation_scenarios(evaluation_id: str): async with db_engine.get_session() as session: result = await session.execute( select(EvaluationScenarioDB) - .filter_by(evaluation_id=uuid.UUID(evaluation_id)) + .filter_by( + evaluation_id=uuid.UUID(evaluation_id), project_id=uuid.UUID(project_id) + ) .options(joinedload(EvaluationScenarioDB.results)) ) evaluation_scenarios = result.unique().scalars().all() @@ -2320,8 +2204,10 @@ async def fetch_evaluation_scenario_by_id( evaluation_scenario_id: str, ) -> Optional[EvaluationScenarioDB]: """Fetches and evaluation scenario by its ID. + Args: evaluation_scenario_id (str): The ID of the evaluation scenario to fetch. + Returns: EvaluationScenarioDB: The fetched evaluation scenario, or None if no evaluation scenario was found. """ @@ -2339,8 +2225,10 @@ async def fetch_human_evaluation_scenario_by_id( evaluation_scenario_id: str, ) -> Optional[HumanEvaluationScenarioDB]: """Fetches and evaluation scenario by its ID. + Args: evaluation_scenario_id (str): The ID of the evaluation scenario to fetch. + Returns: EvaluationScenarioDB: The fetched evaluation scenario, or None if no evaluation scenario was found. """ @@ -2378,12 +2266,13 @@ async def fetch_human_evaluation_scenario_by_evaluation_id( async def find_previous_variant_from_base_id( - base_id: str, + base_id: str, project_id: str ) -> Optional[AppVariantDB]: """Find the previous variant from a base id. Args: base_id (str): The base id to search for. + project_id (str): The ID of the project. Returns: Optional[AppVariantDB]: The previous variant, or None if no previous variant was found. @@ -2393,7 +2282,7 @@ async def find_previous_variant_from_base_id( async with db_engine.get_session() as session: result = await session.execute( select(AppVariantDB) - .filter_by(base_id=uuid.UUID(base_id)) + .filter_by(base_id=uuid.UUID(base_id), project_id=uuid.UUID(project_id)) .order_by(AppVariantDB.created_at.desc()) ) last_variant = result.scalars().first() @@ -2601,18 +2490,14 @@ async def update_app_variant( if hasattr(app_variant, key): setattr(app_variant, key, value) - relationships_to_load_in_session = [ - "user", - "app", - "image", - "base", - ] - if isCloudEE(): - relationships_to_load_in_session.append("organization") - await session.commit() await session.refresh( - app_variant, attribute_names=relationships_to_load_in_session + app_variant, + attribute_names=[ + "app", + "image", + "base", + ], ) return app_variant @@ -2620,39 +2505,31 @@ async def update_app_variant( async def fetch_app_by_name_and_parameters( app_name: str, - user_uid: str, - organization_id: Optional[str] = None, workspace_id: Optional[str] = None, + project_id: Optional[str] = None, ): - """Fetch an app by its name, organization id, and workspace_id. + """Fetch an app by its name and project identifier. Args: app_name (str): The name of the app - organization_id (str): The ID of the app organization - workspace_id (str): The ID of the app workspace + workspace_id (str, optional): The ID of the workspace. Defaults to None. + project_id (str, optional): The ID of the project. Defaults to None. Returns: AppDB: the instance of the app """ - async with db_engine.get_session() as session: - base_query = select(AppDB).filter_by(app_name=app_name) - - if isCloudEE(): - # assert that if organization is provided, workspace_id is also provided, and vice versa - assert ( - organization_id is not None and workspace_id is not None - ), "organization_id and workspace_id must be provided together" - - query = base_query.filter_by( - organization_id=uuid.UUID(organization_id), - workspace_id=workspace_id, - ) - else: - query = base_query.join(UserDB, AppDB.user_id == UserDB.id).filter( - UserDB.uid == user_uid - ) + if isCloudEE() and workspace_id is not None: + project = await db_manager_ee.get_project_by_workspace( + workspace_id=workspace_id + ) + query = select(AppDB).filter_by(app_name=app_name, project_id=project.id) + else: + query = select(AppDB).filter_by( + app_name=app_name, project_id=uuid.UUID(project_id) + ) + async with db_engine.get_session() as session: result = await session.execute(query) app_db = result.unique().scalars().first() return app_db @@ -2660,13 +2537,11 @@ async def fetch_app_by_name_and_parameters( async def create_new_evaluation( app: AppDB, - user_id: str, + project_id: str, testset: TestSetDB, status: Result, variant: str, variant_revision: str, - organization=None, - workspace=None, ) -> EvaluationDB: """Create a new evaluation scenario. Returns: @@ -2676,28 +2551,18 @@ async def create_new_evaluation( async with db_engine.get_session() as session: evaluation = EvaluationDB( app_id=app.id, - user_id=uuid.UUID(user_id), + project_id=uuid.UUID(project_id), testset_id=testset.id, - status=status.dict(), + status=status.model_dump(), variant_id=uuid.UUID(variant), variant_revision_id=uuid.UUID(variant_revision), ) - if isCloudEE(): - # assert that if organization is provided, workspace is also provided, and vice versa - assert ( - organization is not None and workspace is not None - ), "organization and workspace must be provided together" - - evaluation.organization_id = uuid.UUID(organization) # type: ignore - evaluation.workspace_id = uuid.UUID(workspace) # type: ignore - session.add(evaluation) await session.commit() await session.refresh( evaluation, attribute_names=[ - "user", "testset", "variant", "variant_revision", @@ -2708,23 +2573,24 @@ async def create_new_evaluation( return evaluation -async def list_evaluations(app_id: str): +async def list_evaluations(app_id: str, project_id: str): """Retrieves evaluations of the specified app from the db. Args: app_id (str): The ID of the app + project_id (str): The ID of the project """ async with db_engine.get_session() as session: - base_query = select(EvaluationDB).filter_by(app_id=uuid.UUID(app_id)) + base_query = select(EvaluationDB).filter_by( + app_id=uuid.UUID(app_id), project_id=uuid.UUID(project_id) + ) if isCloudEE(): query = base_query.options( - joinedload(EvaluationDB.user.of_type(UserDB)).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(EvaluationDB.testset.of_type(TestSetDB)).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) else: query = base_query.options( - joinedload(EvaluationDB.user).load_only(UserDB.id, UserDB.username), # type: ignore joinedload(EvaluationDB.testset).load_only(TestSetDB.id, TestSetDB.name), # type: ignore ) @@ -2743,13 +2609,16 @@ async def list_evaluations(app_id: str): return evaluations -async def fetch_evaluations_by_resource(resource_type: str, resource_ids: List[str]): +async def fetch_evaluations_by_resource( + resource_type: str, project_id: str, resource_ids: List[str] +): """ Fetches an evaluations by resource. Args: - resource_type: The resource type - resource_ids: The resource identifiers + resource_type (str): The resource type + project_id (str): The ID of the project + resource_ids (List[str]): The resource identifiers Returns: The evaluations by resource. @@ -2764,39 +2633,55 @@ async def fetch_evaluations_by_resource(resource_type: str, resource_ids: List[s if resource_type == "variant": result_evaluations = await session.execute( select(EvaluationDB) - .filter(EvaluationDB.variant_id.in_(ids)) + .filter( + EvaluationDB.variant_id.in_(ids), + EvaluationDB.project_id == uuid.UUID(project_id), + ) .options(load_only(EvaluationDB.id)) # type: ignore ) result_human_evaluations = await session.execute( select(HumanEvaluationDB) .join(HumanEvaluationVariantDB) - .filter(HumanEvaluationVariantDB.variant_id.in_(ids)) + .filter( + HumanEvaluationVariantDB.variant_id.in_(ids), + HumanEvaluationDB.project_id == uuid.UUID(project_id), + ) .options(load_only(HumanEvaluationDB.id)) # type: ignore ) res_evaluations = result_evaluations.scalars().all() res_human_evaluations = result_human_evaluations.scalars().all() return res_evaluations + res_human_evaluations - if resource_type == "testset": + elif resource_type == "testset": result_evaluations = await session.execute( select(EvaluationDB) - .filter(EvaluationDB.testset_id.in_(ids)) + .filter( + EvaluationDB.testset_id.in_(ids), + EvaluationDB.project_id == uuid.UUID(project_id), + ) .options(load_only(EvaluationDB.id)) # type: ignore ) result_human_evaluations = await session.execute( select(HumanEvaluationDB) - .filter(HumanEvaluationDB.testset_id.in_(ids)) + .filter( + HumanEvaluationDB.testset_id.in_(ids), + HumanEvaluationDB.project_id + == uuid.UUID(project_id), # Fixed to match HumanEvaluationDB + ) .options(load_only(HumanEvaluationDB.id)) # type: ignore ) res_evaluations = result_evaluations.scalars().all() res_human_evaluations = result_human_evaluations.scalars().all() return res_evaluations + res_human_evaluations - if resource_type == "evaluator_config": + elif resource_type == "evaluator_config": query = ( select(EvaluationDB) .join(EvaluationDB.evaluator_configs) - .filter(EvaluationEvaluatorConfigDB.evaluator_config_id.in_(ids)) + .filter( + EvaluationEvaluatorConfigDB.evaluator_config_id.in_(ids), + EvaluationDB.project_id == uuid.UUID(project_id), + ) ) result = await session.execute(query) res = result.scalars().all() @@ -2825,7 +2710,7 @@ async def delete_evaluations(evaluation_ids: List[str]) -> None: async def create_new_evaluation_scenario( - user_id: str, + project_id: str, evaluation_id: str, variant_id: str, inputs: List[EvaluationScenarioInput], @@ -2834,8 +2719,6 @@ async def create_new_evaluation_scenario( is_pinned: Optional[bool], note: Optional[str], results: List[EvaluationScenarioResult], - organization=None, - workspace=None, ) -> EvaluationScenarioDB: """Create a new evaluation scenario. @@ -2845,13 +2728,13 @@ async def create_new_evaluation_scenario( async with db_engine.get_session() as session: evaluation_scenario = EvaluationScenarioDB( - user_id=uuid.UUID(user_id), + project_id=uuid.UUID(project_id), evaluation_id=uuid.UUID(evaluation_id), variant_id=uuid.UUID(variant_id), - inputs=[input.dict() for input in inputs], - outputs=[output.dict() for output in outputs], + inputs=[input.model_dump() for input in inputs], + outputs=[output.model_dump() for output in outputs], correct_answers=( - [correct_answer.dict() for correct_answer in correct_answers] + [correct_answer.model_dump() for correct_answer in correct_answers] if correct_answers is not None else [] ), @@ -2859,15 +2742,6 @@ async def create_new_evaluation_scenario( note=note, ) - if isCloudEE(): - # assert that if organization is provided, workspace is also provided, and vice versa - assert ( - organization is not None and workspace is not None - ), "organization and workspace must be provided together" - - evaluation_scenario.organization_id = organization # type: ignore - evaluation_scenario.workspace_id = workspace # type: ignore - session.add(evaluation_scenario) await session.commit() await session.refresh(evaluation_scenario) @@ -2877,7 +2751,7 @@ async def create_new_evaluation_scenario( evaluation_scenario_result = EvaluationScenarioResultDB( evaluation_scenario_id=evaluation_scenario.id, evaluator_config_id=uuid.UUID(result.evaluator_config), - result=result.result.dict(), + result=result.result.model_dump(), ) session.add(evaluation_scenario_result) @@ -2896,7 +2770,7 @@ async def update_evaluation_with_aggregated_results( aggregated_result = EvaluationAggregatedResultDB( evaluation_id=uuid.UUID(evaluation_id), evaluator_config_id=uuid.UUID(result.evaluator_config), - result=result.result.dict(), + result=result.result.model_dump(), ) session.add(aggregated_result) @@ -2950,17 +2824,16 @@ async def fetch_eval_aggregated_results(evaluation_id: str): return aggregated_results -async def fetch_evaluators_configs(app_id: str): +async def fetch_evaluators_configs(project_id: str): """Fetches a list of evaluator configurations from the database. Returns: List[EvaluatorConfigDB]: A list of evaluator configuration objects. """ - assert app_id is not None, "evaluation_id cannot be None" async with db_engine.get_session() as session: result = await session.execute( - select(EvaluatorConfigDB).filter_by(app_id=uuid.UUID(app_id)) + select(EvaluatorConfigDB).filter_by(project_id=uuid.UUID(project_id)) ) evaluators_configs = result.scalars().all() return evaluators_configs @@ -2969,6 +2842,9 @@ async def fetch_evaluators_configs(app_id: str): async def fetch_evaluator_config(evaluator_config_id: str): """Fetch evaluator configurations from the database. + Args: + evaluator_config_id (str): The ID of the evaluator configuration. + Returns: EvaluatorConfigDB: the evaluator configuration object. """ @@ -3038,8 +2914,8 @@ async def fetch_evaluator_config_by_appId( async def create_evaluator_config( - app: AppDB, - user_id: str, + project_id: str, + app_name: str, name: str, evaluator_key: str, settings_values: Optional[Dict[str, Any]] = None, @@ -3048,17 +2924,12 @@ async def create_evaluator_config( async with db_engine.get_session() as session: new_evaluator_config = EvaluatorConfigDB( - app_id=app.id, - user_id=uuid.UUID(user_id), - name=name, + project_id=uuid.UUID(project_id), + name=f"{name} ({app_name})", evaluator_key=evaluator_key, settings_values=settings_values, ) - if isCloudEE(): - new_evaluator_config.organization_id = app.organization_id - new_evaluator_config.workspace_id = app.workspace_id - session.add(new_evaluator_config) await session.commit() await session.refresh(new_evaluator_config) @@ -3122,13 +2993,14 @@ async def delete_evaluator_config(evaluator_config_id: str) -> bool: async def update_evaluation( - evaluation_id: str, updates: Dict[str, Any] + evaluation_id: str, project_id: str, updates: Dict[str, Any] ) -> EvaluationDB: """ Update an evaluator configuration in the database with the provided id. Arguments: evaluation_id (str): The ID of the evaluator configuration to be updated. + project_id (str): The ID of the project. updates (Dict[str, Any]): The updates to apply to the evaluator configuration. Returns: @@ -3137,7 +3009,9 @@ async def update_evaluation( async with db_engine.get_session() as session: result = await session.execute( - select(EvaluationDB).filter_by(id=uuid.UUID(evaluation_id)) + select(EvaluationDB).filter_by( + id=uuid.UUID(evaluation_id), project_id=uuid.UUID(project_id) + ) ) evaluation = result.scalars().first() for key, value in updates.items(): @@ -3226,3 +3100,17 @@ async def fetch_corresponding_object_uuid(table_name: str, object_id: str) -> st ) object_mapping = result.scalars().first() return str(object_mapping.uuid) + + +async def fetch_default_project() -> ProjectDB: + """ + Fetch the default project from the database. + + Returns: + ProjectDB: The default project instance. + """ + + async with db_engine.get_session() as session: + result = await session.execute(select(ProjectDB).filter_by(is_default=True)) + default_project = result.scalars().first() + return default_project diff --git a/agenta-backend/agenta_backend/services/deployment_manager.py b/agenta-backend/agenta_backend/services/deployment_manager.py index b44c320cf..9ec391ba5 100644 --- a/agenta-backend/agenta_backend/services/deployment_manager.py +++ b/agenta-backend/agenta_backend/services/deployment_manager.py @@ -16,13 +16,14 @@ async def start_service( - app_variant_db: AppVariantDB, env_vars: Dict[str, str] + app_variant_db: AppVariantDB, project_id: str, env_vars: Dict[str, str] ) -> DeploymentDB: """ Start a service. Args: app_variant_db (AppVariantDB): The app variant to start. + project_id (str): The ID of the project the app variant belongs to. env_vars (Dict[str, str]): The environment variables to pass to the container. Returns: @@ -30,11 +31,11 @@ async def start_service( """ if isCloudEE(): - uri_path = f"{app_variant_db.organization.id}/{app_variant_db.app.app_name}/{app_variant_db.base_name}" - container_name = f"{app_variant_db.app.app_name}-{app_variant_db.base_name}-{app_variant_db.organization.id}" + uri_path = f"{app_variant_db.project_id}/{app_variant_db.app.app_name}/{app_variant_db.base_name}" + container_name = f"{app_variant_db.app.app_name}-{app_variant_db.base_name}-{app_variant_db.project_id}" else: - uri_path = f"{app_variant_db.user.id}/{app_variant_db.app.app_name}/{app_variant_db.base_name}" - container_name = f"{app_variant_db.app.app_name}-{app_variant_db.base_name}-{app_variant_db.user.id}" + uri_path = f"{app_variant_db.project_id}/{app_variant_db.app.app_name}/{app_variant_db.base_name}" + container_name = f"{app_variant_db.app.app_name}-{app_variant_db.base_name}-{app_variant_db.project_id}" logger.debug("Starting service with the following parameters:") logger.debug(f"image_name: {app_variant_db.image.tags}") @@ -59,13 +60,11 @@ async def start_service( deployment = await db_manager.create_deployment( app_id=str(app_variant_db.app.id), - user_id=str(app_variant_db.user.id), + project_id=project_id, container_name=container_name, container_id=container_id, uri=uri, status="running", - organization=str(app_variant_db.organization_id) if isCloudEE() else None, - workspace=str(app_variant_db.workspace_id) if isCloudEE() else None, ) return deployment @@ -138,7 +137,7 @@ async def validate_image(image: Image) -> bool: raise ValueError(msg) if isCloudEE(): - image = Image(**image.model_dump(exclude={"workspace", "organization"})) + image = Image(**image.model_dump()) if not image.tags.startswith(agenta_registry_repo): raise ValueError( diff --git a/agenta-backend/agenta_backend/services/evaluation_service.py b/agenta-backend/agenta_backend/services/evaluation_service.py index d15a4cf1d..7f8f2290f 100644 --- a/agenta-backend/agenta_backend/services/evaluation_service.py +++ b/agenta-backend/agenta_backend/services/evaluation_service.py @@ -67,10 +67,9 @@ class UpdateEvaluationScenarioError(Exception): async def prepare_csvdata_and_create_evaluation_scenario( csvdata: List[Dict[str, str]], payload_inputs: List[str], + project_id: str, evaluation_type: EvaluationType, new_evaluation: HumanEvaluationDB, - user: UserDB, - app: AppDB, ): """ Prepares CSV data and creates evaluation scenarios based on the inputs, evaluation @@ -79,10 +78,9 @@ async def prepare_csvdata_and_create_evaluation_scenario( Args: csvdata: A list of dictionaries representing the CSV data. payload_inputs: A list of strings representing the names of the inputs in the variant. + project_id (str): The ID of the project evaluation_type: The type of evaluation new_evaluation: The instance of EvaluationDB - user: The owner of the evaluation scenario - app: The app the evaluation is going to belong to """ for datum in csvdata: @@ -94,7 +92,7 @@ async def prepare_csvdata_and_create_evaluation_scenario( ] except KeyError: await db_manager.delete_human_evaluation( - evaluation_id=str(new_evaluation.id) + evaluation_id=str(new_evaluation.id), project_id=project_id ) msg = f""" Columns in the test set should match the names of the inputs in the variant. @@ -121,8 +119,7 @@ async def prepare_csvdata_and_create_evaluation_scenario( } await db_manager.create_human_evaluation_scenario( inputs=list_of_scenario_input, - user_id=str(user.id), - app=app, + project_id=project_id, evaluation_id=str(new_evaluation.id), evaluation_extend=evaluation_scenario_extend_payload, ) @@ -141,23 +138,26 @@ async def update_human_evaluation_service( # Update the evaluation await db_manager.update_human_evaluation( - evaluation_id=str(evaluation.id), values_to_update=update_payload.dict() + evaluation_id=str(evaluation.id), values_to_update=update_payload.model_dump() ) -async def fetch_evaluation_scenarios_for_evaluation(evaluation_id: str): +async def fetch_evaluation_scenarios_for_evaluation( + evaluation_id: str, project_id: str +): """ Fetch evaluation scenarios for a given evaluation ID. Args: evaluation_id (str): The ID of the evaluation. + project_id (str): The ID of the project. Returns: List[EvaluationScenario]: A list of evaluation scenarios. """ evaluation_scenarios = await db_manager.fetch_evaluation_scenarios( - evaluation_id=evaluation_id + evaluation_id=evaluation_id, project_id=project_id ) return [ await converters.evaluation_scenario_db_to_pydantic( @@ -213,7 +213,7 @@ async def update_human_evaluation_scenario( """ values_to_update = {} - payload = evaluation_scenario_data.dict(exclude_unset=True) + payload = evaluation_scenario_data.model_dump(exclude_unset=True) if "score" in payload and evaluation_type == EvaluationType.single_model_test: values_to_update["score"] = str(payload["score"]) @@ -226,7 +226,7 @@ async def update_human_evaluation_scenario( HumanEvaluationScenarioOutput( variant_id=output["variant_id"], variant_output=output["variant_output"], - ).dict() + ).model_dump() for output in payload["outputs"] ] values_to_update["outputs"] = new_outputs @@ -236,7 +236,7 @@ async def update_human_evaluation_scenario( HumanEvaluationScenarioInput( input_name=input_item["input_name"], input_value=input_item["input_value"], - ).dict() + ).model_dump() for input_item in payload["inputs"] ] values_to_update["inputs"] = new_inputs @@ -273,20 +273,21 @@ def _extend_with_correct_answer(evaluation_type: EvaluationType, row: dict): return correct_answer -async def fetch_list_evaluations( - app: AppDB, -) -> List[Evaluation]: +async def fetch_list_evaluations(app: AppDB, project_id: str) -> List[Evaluation]: """ Fetches a list of evaluations based on the provided filtering criteria. Args: app (AppDB): An app to filter the evaluations. + project_id (str): The ID of the project Returns: List[Evaluation]: A list of evaluations. """ - evaluations_db = await db_manager.list_evaluations(app_id=str(app.id)) + evaluations_db = await db_manager.list_evaluations( + app_id=str(app.id), project_id=project_id + ) return [ await converters.evaluation_db_to_pydantic(evaluation) for evaluation in evaluations_db @@ -294,19 +295,22 @@ async def fetch_list_evaluations( async def fetch_list_human_evaluations( - app_id: str, + app_id: str, project_id: str ) -> List[HumanEvaluation]: """ Fetches a list of evaluations based on the provided filtering criteria. Args: app_id (Optional[str]): An optional app ID to filter the evaluations. + project_id (str): The ID of the project. Returns: List[Evaluation]: A list of evaluations. """ - evaluations_db = await db_manager.list_human_evaluations(app_id=app_id) + evaluations_db = await db_manager.list_human_evaluations( + app_id=app_id, project_id=project_id + ) return [ await converters.human_evaluation_db_to_pydantic(evaluation) for evaluation in evaluations_db @@ -333,6 +337,7 @@ async def delete_human_evaluations(evaluation_ids: List[str]) -> None: Args: evaluation_ids (List[str]): A list of evaluation IDs. + project_id (str): The ID of the project. Raises: NoResultFound: If evaluation not found or access denied. @@ -356,21 +361,17 @@ async def delete_evaluations(evaluation_ids: List[str]) -> None: await db_manager.delete_evaluations(evaluation_ids=evaluation_ids) -async def create_new_human_evaluation( - payload: NewHumanEvaluation, user_uid: str -) -> HumanEvaluationDB: +async def create_new_human_evaluation(payload: NewHumanEvaluation) -> HumanEvaluationDB: """ Create a new evaluation based on the provided payload and additional arguments. Args: payload (NewEvaluation): The evaluation payload. - user_uid (str): The user_uid of the user Returns: HumanEvaluationDB """ - user = await db_manager.get_user(user_uid) app = await db_manager.fetch_app_by_id(app_id=payload.app_id) if app is None: raise HTTPException( @@ -380,7 +381,6 @@ async def create_new_human_evaluation( human_evaluation = await db_manager.create_human_evaluation( app=app, - user_id=str(user.id), status=payload.status, evaluation_type=payload.evaluation_type, testset_id=payload.testset_id, @@ -394,18 +394,17 @@ async def create_new_human_evaluation( await prepare_csvdata_and_create_evaluation_scenario( human_evaluation.testset.csvdata, payload.inputs, + str(app.project_id), payload.evaluation_type, human_evaluation, - user, - app, ) return human_evaluation async def create_new_evaluation( app_id: str, + project_id: str, variant_id: str, - evaluator_config_ids: List[str], testset_id: str, ) -> Evaluation: """ @@ -413,8 +412,8 @@ async def create_new_evaluation( Args: app_id (str): The ID of the app. + project_id (str): The ID of the project. variant_id (str): The ID of the variant. - evaluator_config_ids (List[str]): The IDs of the evaluator configurations. testset_id (str): The ID of the testset. Returns: @@ -423,22 +422,27 @@ async def create_new_evaluation( app = await db_manager.fetch_app_by_id(app_id=app_id) testset = await db_manager.fetch_testset_by_id(testset_id=testset_id) - variant_db = await db_manager.get_app_variant_instance_by_id(variant_id=variant_id) + variant_db = await db_manager.get_app_variant_instance_by_id( + variant_id=variant_id, project_id=project_id + ) + + assert variant_db is not None, f"App variant with ID {variant_id} cannot be None." + assert ( + variant_db.revision is not None + ), f"Revision of App variant with ID {variant_id} cannot be None" variant_revision = await db_manager.fetch_app_variant_revision_by_variant( - app_variant_id=variant_id, revision=variant_db.revision # type: ignore + app_variant_id=variant_id, project_id=project_id, revision=variant_db.revision # type: ignore ) evaluation_db = await db_manager.create_new_evaluation( app=app, - user_id=str(app.user_id), + project_id=project_id, testset=testset, status=Result( value=EvaluationStatusEnum.EVALUATION_INITIALIZED, type="status", error=None ), variant=variant_id, variant_revision=str(variant_revision.id), - organization=str(app.organization_id) if isCloudEE() else None, - workspace=str(app.workspace_id) if isCloudEE() else None, ) return await converters.evaluation_db_to_pydantic(evaluation_db) @@ -458,9 +462,7 @@ async def retrieve_evaluation_results(evaluation_id: str) -> List[dict]: return await converters.aggregated_result_to_pydantic(evaluation.aggregated_results) -async def compare_evaluations_scenarios( - evaluations_ids: List[str], -): +async def compare_evaluations_scenarios(evaluations_ids: List[str], project_id: str): evaluation = await db_manager.fetch_evaluation_by_id(evaluations_ids[0]) testset = evaluation.testset unique_testset_datapoints = remove_duplicates(testset.csvdata) @@ -471,7 +473,7 @@ async def compare_evaluations_scenarios( for evaluation_id in evaluations_ids: eval_scenarios = await fetch_evaluation_scenarios_for_evaluation( - evaluation_id=evaluation_id + evaluation_id=evaluation_id, project_id=project_id ) all_scenarios.append(eval_scenarios) diff --git a/agenta-backend/agenta_backend/services/evaluator_manager.py b/agenta-backend/agenta_backend/services/evaluator_manager.py index 84dd456e2..bb5d6b19f 100644 --- a/agenta-backend/agenta_backend/services/evaluator_manager.py +++ b/agenta-backend/agenta_backend/services/evaluator_manager.py @@ -31,17 +31,18 @@ def get_evaluators() -> List[Evaluator]: return [Evaluator(**evaluator_dict) for evaluator_dict in evaluators_as_dict] -async def get_evaluators_configs(app_id: str) -> List[EvaluatorConfig]: +async def get_evaluators_configs(project_id: str) -> List[EvaluatorConfig]: """ Get evaluators configs by app_id. Args: - app_id (str): The ID of the app. + project_id (str): The ID of the project. Returns: List[EvaluatorConfig]: A list of evaluator configuration objects. """ - evaluator_configs_db = await db_manager.fetch_evaluators_configs(app_id) + + evaluator_configs_db = await db_manager.fetch_evaluators_configs(project_id) return [ evaluator_config_db_to_pydantic(evaluator_config_db) for evaluator_config_db in evaluator_configs_db @@ -62,7 +63,8 @@ async def get_evaluator_config(evaluator_config: EvaluatorConfig) -> EvaluatorCo async def create_evaluator_config( - app_id: str, + project_id: str, + app_name: str, name: str, evaluator_key: str, settings_values: Optional[Dict[str, Any]] = None, @@ -71,7 +73,8 @@ async def create_evaluator_config( Create a new evaluator configuration for an app. Args: - app_id (str): The ID of the app. + project_id (str): The ID of the project. + app_name (str): The name of the app. name (str): The name of the evaluator config. evaluator_key (str): The key of the evaluator. settings_values (Optional[Dict[str, Any]]): Additional settings for the evaluator. @@ -79,11 +82,10 @@ async def create_evaluator_config( Returns: EvaluatorConfigDB: The newly created evaluator configuration object. """ - app = await db_manager.fetch_app_by_id(app_id) evaluator_config = await db_manager.create_evaluator_config( - app=app, - user_id=str(app.user_id), + project_id=project_id, + app_name=app_name, name=name, evaluator_key=evaluator_key, settings_values=settings_values, @@ -123,7 +125,7 @@ async def delete_evaluator_config(evaluator_config_id: str) -> bool: return await db_manager.delete_evaluator_config(evaluator_config_id) -async def create_ready_to_use_evaluators(app: AppDB): +async def create_ready_to_use_evaluators(app_name: str, project_id: str): """ Create configurations for all evaluators that are marked for direct use. @@ -131,13 +133,12 @@ async def create_ready_to_use_evaluators(app: AppDB): out those marked for direct use, and creates configuration entries for them in the database using the database manager. - Parameters: - - evaluator_manager: The manager object responsible for handling evaluators. - - db_manager: The database manager object used for database operations. - - app: The application context, containing details like organization and user. + Args: + app_name (str): The name of the application. + project_id (str): The ID of the project. Returns: - Nothing. The function works by side effect, modifying the database. + Nothing. The function works by side effect, modifying the database. """ direct_use_evaluators = [ @@ -160,8 +161,8 @@ async def create_ready_to_use_evaluators(app: AppDB): evaluator, "key" ), f"'name' and 'key' does not exist in the evaluator: {evaluator}" await db_manager.create_evaluator_config( - app=app, - user_id=str(app.user_id), + project_id=project_id, + app_name=app_name, name=evaluator.name, evaluator_key=evaluator.key, settings_values=settings_values, diff --git a/agenta-backend/agenta_backend/tasks/evaluations.py b/agenta-backend/agenta_backend/tasks/evaluations.py index b5f36852b..c2388477e 100644 --- a/agenta-backend/agenta_backend/tasks/evaluations.py +++ b/agenta-backend/agenta_backend/tasks/evaluations.py @@ -27,7 +27,6 @@ create_new_evaluation_scenario, fetch_app_by_id, fetch_app_variant_by_id, - fetch_evaluation_by_id, fetch_evaluator_config, fetch_testset_by_id, get_deployment_by_id, @@ -38,16 +37,12 @@ ) from agenta_backend.services.evaluator_manager import get_evaluators -if isCloudEE(): - from agenta_backend.commons.models.db_models import AppDB_ as AppDB -else: - from agenta_backend.models.db_models import AppDB # Set logger logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Fetch all evaluators and precompute ground truth keys +# Fetch all evaluators and pre-compute ground truth keys all_evaluators = get_evaluators() ground_truth_keys_dict = { evaluator.key: [ @@ -63,6 +58,7 @@ def evaluate( self, app_id: str, + project_id: str, variant_id: str, evaluators_config_ids: List[str], testset_id: str, @@ -76,6 +72,7 @@ def evaluate( Args: self: The task instance. app_id (str): The ID of the app. + project_id (str): The ID of the project. variant_id (str): The ID of the app variant. evaluators_config_ids (List[str]): The IDs of the evaluators configurations to be used. testset_id (str): The ID of the testset. @@ -94,6 +91,7 @@ def evaluate( loop.run_until_complete( update_evaluation( evaluation_id, + project_id, { "status": Result( type="status", value=EvaluationStatusEnum.EVALUATION_STARTED @@ -169,7 +167,7 @@ def evaluate( ] logger.debug(f"Inputs: {inputs}") - # 2. We skip the iteration if error invking the llm-app + # 2. We skip the iteration if error invoking the llm-app if app_output.result.error: print("There is an error when invoking the llm app so we need to skip") error_results = [ @@ -189,7 +187,7 @@ def evaluate( loop.run_until_complete( create_new_evaluation_scenario( - user_id=str(app.user_id), + project_id=project_id, evaluation_id=evaluation_id, variant_id=variant_id, inputs=inputs, @@ -209,8 +207,6 @@ def evaluate( is_pinned=False, note="", results=error_results, - organization=str(app.organization_id) if isCloudEE() else None, - workspace=str(app.workspace_id) if isCloudEE() else None, ) ) continue @@ -270,7 +266,7 @@ def evaluate( # 4. We save the result of the eval scenario in the db loop.run_until_complete( create_new_evaluation_scenario( - user_id=str(app.user_id), + project_id=project_id, evaluation_id=evaluation_id, variant_id=variant_id, inputs=inputs, @@ -287,8 +283,6 @@ def evaluate( is_pinned=False, note="", results=evaluators_results, - organization=str(app.organization_id) if isCloudEE() else None, - workspace=str(app.workspace_id) if isCloudEE() else None, ) ) @@ -305,6 +299,7 @@ def evaluate( loop.run_until_complete( update_evaluation( evaluation_id, + project_id, { "average_latency": average_latency.model_dump(), "average_cost": average_cost.model_dump(), @@ -319,6 +314,7 @@ def evaluate( loop.run_until_complete( update_evaluation( evaluation_id, + project_id, { "status": Result( type="status", @@ -361,6 +357,7 @@ def evaluate( loop.run_until_complete( update_evaluation( evaluation_id=evaluation_id, + project_id=project_id, updates={"status": evaluation_status.model_dump()}, ) ) @@ -371,6 +368,7 @@ def evaluate( loop.run_until_complete( update_evaluation( evaluation_id, + project_id, { "status": Result( type="status", diff --git a/agenta-backend/agenta_backend/tests/variants_main_router/conftest.py b/agenta-backend/agenta_backend/tests/variants_main_router/conftest.py index a3459942b..515be6261 100644 --- a/agenta-backend/agenta_backend/tests/variants_main_router/conftest.py +++ b/agenta-backend/agenta_backend/tests/variants_main_router/conftest.py @@ -6,6 +6,7 @@ from agenta_backend.models.db.postgres_engine import db_engine from agenta_backend.models.shared_models import ConfigDB from agenta_backend.models.db_models import ( + ProjectDB, AppDB, UserDB, DeploymentDB, @@ -75,7 +76,12 @@ async def get_first_user_app(get_first_user_object): user = await get_first_user_object async with db_engine.get_session() as session: - app = AppDB(app_name="myapp", user_id=user.id) + project = ProjectDB(project_name="default", is_default=True) + session.add(project) + await session.commit() + await session.refresh(project) + + app = AppDB(app_name="myapp", project_id=project.id) session.add(app) await session.commit() await session.refresh(app) @@ -83,7 +89,7 @@ async def get_first_user_app(get_first_user_object): db_image = ImageDB( docker_id="sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", tags="agentaai/templates_v2:local_test_prompt", - user_id=user.id, + project_id=project.id, ) session.add(db_image) await session.commit() @@ -96,7 +102,7 @@ async def get_first_user_app(get_first_user_object): db_deployment = DeploymentDB( app_id=app.id, - user_id=user.id, + project_id=project.id, container_name="container_a_test", container_id="w243e34red", uri="http://localhost/app/w243e34red", @@ -107,7 +113,7 @@ async def get_first_user_app(get_first_user_object): db_base = VariantBaseDB( base_name="app", image_id=db_image.id, - user_id=user.id, + project_id=project.id, app_id=app.id, deployment_id=db_deployment.id, ) @@ -119,7 +125,7 @@ async def get_first_user_app(get_first_user_object): app_id=app.id, variant_name="app", image_id=db_image.id, - user_id=user.id, + project_id=project.id, config_parameters={}, base_name="app", config_name="default", diff --git a/agenta-backend/agenta_backend/tests/variants_main_router/test_app_variant_router.py b/agenta-backend/agenta_backend/tests/variants_main_router/test_app_variant_router.py index 150ff3f65..29a144f03 100644 --- a/agenta-backend/agenta_backend/tests/variants_main_router/test_app_variant_router.py +++ b/agenta-backend/agenta_backend/tests/variants_main_router/test_app_variant_router.py @@ -1,5 +1,6 @@ import os import httpx +import random import pytest import logging from bson import ObjectId @@ -9,6 +10,7 @@ from agenta_backend.models.db.postgres_engine import db_engine from agenta_backend.models.shared_models import ConfigDB from agenta_backend.models.db_models import ( + ProjectDB, AppDB, DeploymentDB, VariantBaseDB, @@ -113,22 +115,22 @@ async def test_create_app_variant(get_first_user_object): ) app = result.scalars().first() + project_result = await session.execute( + select(ProjectDB).filter_by(is_default=True) + ) + project = project_result.scalars().first() + db_image = ImageDB( docker_id="sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", tags="agentaai/templates_v2:local_test_prompt", - user_id=user.id, + project_id=project.id, ) session.add(db_image) await session.commit() - db_config = ConfigDB( - config_name="default", - parameters={}, - ) - db_deployment = DeploymentDB( app_id=app.id, - user_id=user.id, + project_id=project.id, container_name="container_a_test", container_id="w243e34red", uri="http://localhost/app/w243e34red", @@ -140,7 +142,7 @@ async def test_create_app_variant(get_first_user_object): db_base = VariantBaseDB( base_name="app", app_id=app.id, - user_id=user.id, + project_id=project.id, image_id=db_image.id, deployment_id=db_deployment.id, ) @@ -151,7 +153,7 @@ async def test_create_app_variant(get_first_user_object): app_id=app.id, variant_name="app", image_id=db_image.id, - user_id=user.id, + project_id=project.id, config_parameters={}, base_name="app", config_name="default", diff --git a/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_evaluators_router.py b/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_evaluators_router.py index a2067da77..e8fd22c5e 100644 --- a/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_evaluators_router.py +++ b/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_evaluators_router.py @@ -213,7 +213,7 @@ async def create_evaluation_with_evaluator(evaluator_config_name): app_variant = app_variant_result.scalars().first() testset_result = await session.execute( - select(TestSetDB).filter_by(app_id=app.id) + select(TestSetDB).filter_by(project_id=app.project_id) ) testset = testset_result.scalars().first() @@ -280,7 +280,7 @@ async def test_create_evaluation_with_no_llm_keys(evaluators_requiring_llm_keys) app_variant = app_variant_result.scalars().first() testset_result = await session.execute( - select(TestSetDB).filter_by(app_id=app.id) + select(TestSetDB).filter_by(project_id=app.project_id) ) testset = testset_result.scalars().first() diff --git a/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_testset_router.py b/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_testset_router.py index 7862ebdd9..84f29650c 100644 --- a/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_testset_router.py +++ b/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_testset_router.py @@ -68,7 +68,7 @@ async def test_update_testset(): app = result.scalars().first() testset_result = await session.execute( - select(TestSetDB).filter_by(app_id=app.id) + select(TestSetDB).filter_by(project_id=app.project_id) ) testset = testset_result.scalars().first() @@ -112,7 +112,7 @@ async def test_get_testsets(): ) assert response.status_code == 200 - assert len(response.json()) == 1 + assert len(response.json()) == 2 @pytest.mark.asyncio() @@ -124,7 +124,7 @@ async def test_get_testset(): app = result.scalars().first() testset_result = await session.execute( - select(TestSetDB).filter_by(app_id=app.id) + select(TestSetDB).filter_by(project_id=app.project_id) ) testset = testset_result.scalars().first() @@ -146,7 +146,7 @@ async def test_delete_testsets(): app = result.scalars().first() testset_result = await session.execute( - select(TestSetDB).filter_by(app_id=app.id) + select(TestSetDB).filter_by(project_id=app.project_id) ) testsets = testset_result.scalars().all() diff --git a/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_versioning_deployment.py b/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_versioning_deployment.py index 3b1f733a0..43d9c5ac0 100644 --- a/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_versioning_deployment.py +++ b/agenta-backend/agenta_backend/tests/variants_main_router/test_variant_versioning_deployment.py @@ -8,7 +8,6 @@ from agenta_backend.models.db.postgres_engine import db_engine from agenta_backend.models.db_models import ( AppDB, - TestSetDB, AppVariantDB, ) @@ -18,7 +17,6 @@ timeout = httpx.Timeout(timeout=5, read=None, write=5) # Set global variables -APP_NAME = "evaluation_in_backend" ENVIRONMENT = os.environ.get("ENVIRONMENT") VARIANT_DEPLOY_ENVIRONMENTS = ["development", "staging", "production"] OPEN_AI_KEY = os.environ.get("OPENAI_API_KEY") @@ -31,13 +29,10 @@ @pytest.mark.asyncio async def test_update_app_variant_parameters(app_variant_parameters_updated): async with db_engine.get_session() as session: - result = await session.execute(select(AppDB).filter_by(app_name=APP_NAME)) - app = result.scalars().first() - - testset_result = await session.execute( - select(TestSetDB).filter_by(app_id=app.id) + result = await session.execute( + select(AppDB).filter_by(app_name="evaluation_in_backend") ) - testset = testset_result.scalars().first() + app = result.scalars().first() app_variant_result = await session.execute( select(AppVariantDB).filter_by(app_id=app.id, variant_name="app.default") @@ -49,7 +44,6 @@ async def test_update_app_variant_parameters(app_variant_parameters_updated): parameters["temperature"] = random.uniform(0.9, 1.5) parameters["frequence_penalty"] = random.uniform(0.9, 1.5) parameters["frequence_penalty"] = random.uniform(0.9, 1.5) - parameters["inputs"] = [{"name": list(testset.csvdata[0].keys())[0]}] payload = {"parameters": parameters} response = await test_client.put( @@ -62,7 +56,9 @@ async def test_update_app_variant_parameters(app_variant_parameters_updated): @pytest.mark.asyncio async def test_deploy_to_environment(deploy_to_environment_payload): async with db_engine.get_session() as session: - result = await session.execute(select(AppDB).filter_by(app_name=APP_NAME)) + result = await session.execute( + select(AppDB).filter_by(app_name="evaluation_in_backend") + ) app = result.scalars().first() app_variant_result = await session.execute( diff --git a/agenta-backend/agenta_backend/utils/project_utils.py b/agenta-backend/agenta_backend/utils/project_utils.py new file mode 100644 index 000000000..a1a833ead --- /dev/null +++ b/agenta-backend/agenta_backend/utils/project_utils.py @@ -0,0 +1,39 @@ +import logging +from typing import Optional + +from fastapi import Request + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +async def retrieve_project_id_from_request(request: Request) -> Optional[str]: + """ + Retrieves the `project_id` from an incoming HTTP request. + + This function attempts to extract the `project_id` from various parts of the request: + 1. Path parameters + 2. Query parameters + + Args: + request (Request): The FastAPI `Request` object from which to extract the `project_id`. + + Returns: + Optional[str]: The extracted `project_id` if found; otherwise, `None`. + """ + + logger.info("Retrieving project_id from request...") + + project_id_from_path_params = request.path_params.get("project_id") + if project_id_from_path_params: + logger.info("Project ID found in path params") + return project_id_from_path_params + + project_id_from_query_params = request.query_params.get("project_id") + if project_id_from_query_params: + logger.info("Project ID found in query params") + return project_id_from_query_params + + logger.info("No project ID found in the request") + return None diff --git a/agenta-cli/agenta/client/backend/types/app_variant_response.py b/agenta-cli/agenta/client/backend/types/app_variant_response.py index 79c821ba1..70c819544 100644 --- a/agenta-cli/agenta/client/backend/types/app_variant_response.py +++ b/agenta-cli/agenta/client/backend/types/app_variant_response.py @@ -12,7 +12,7 @@ class AppVariantResponse(UniversalBaseModel): variant_id: str variant_name: str parameters: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None - user_id: str + project_id: str base_name: str base_id: str config_name: str @@ -21,8 +21,6 @@ class AppVariantResponse(UniversalBaseModel): created_at: typing.Optional[str] = None updated_at: typing.Optional[str] = None modified_by_id: typing.Optional[str] = None - organization_id: typing.Optional[str] = None - workspace_id: typing.Optional[str] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( diff --git a/agenta-web/cypress/e2e/eval.scenarios.cy.ts b/agenta-web/cypress/e2e/eval.scenarios.cy.ts index 5c545b13b..9478c51f3 100644 --- a/agenta-web/cypress/e2e/eval.scenarios.cy.ts +++ b/agenta-web/cypress/e2e/eval.scenarios.cy.ts @@ -24,7 +24,7 @@ describe("Evaluation Scenarios Test", function () { it("Should double click on the Evaluation and successfully navigate to the evalaution results page", () => { cy.get(".ant-table-row").eq(0).should("exist") - cy.get(".ant-table-row").click() + cy.get(".ant-table-row").click({force: true}) cy.wait(1000) cy.contains(/Evaluation Results/i) cy.get('[data-cy="evalaution-scenarios-table"]').should("exist") diff --git a/agenta-web/src/components/Evaluations/HumanEvaluationResult.tsx b/agenta-web/src/components/Evaluations/HumanEvaluationResult.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/agenta-web/src/components/HumanEvaluations/AbTestingEvaluation.tsx b/agenta-web/src/components/HumanEvaluations/AbTestingEvaluation.tsx index 29d3c3356..bae2f29c8 100644 --- a/agenta-web/src/components/HumanEvaluations/AbTestingEvaluation.tsx +++ b/agenta-web/src/components/HumanEvaluations/AbTestingEvaluation.tsx @@ -287,33 +287,6 @@ const AbTestingEvaluation = ({viewType}: {viewType: "evaluation" | "overview"}) }, ] - if (isDemo()) { - columns.push({ - title: "User", - dataIndex: ["user", "username"], - key: "username", - onHeaderCell: () => ({ - style: {minWidth: 160}, - }), - render: (_, record: any) => { - return ( - - - {getInitials(record.user.username)} - - {record.user.username} - - ) - }, - }) - } - columns.push( ...([ {