diff --git a/agenta-backend/agenta_backend/migrations/postgres/versions/5c29a64204f4_added_modified_by_id_column_to_apps_db.py b/agenta-backend/agenta_backend/migrations/postgres/versions/5c29a64204f4_added_modified_by_id_column_to_apps_db.py new file mode 100644 index 0000000000..e8a444b102 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/postgres/versions/5c29a64204f4_added_modified_by_id_column_to_apps_db.py @@ -0,0 +1,33 @@ +"""Added modified_by_id column to apps_db table + +Revision ID: 5c29a64204f4 +Revises: b80c708c21bb +Create Date: 2024-08-25 17:56:11.732929 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "5c29a64204f4" +down_revision: Union[str, None] = "b80c708c21bb" +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.add_column("app_db", sa.Column("modified_by_id", sa.UUID(), nullable=True)) + op.create_foreign_key(None, "app_db", "users", ["modified_by_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "app_db", type_="foreignkey") + op.drop_constraint(None, "app_db", type_="unique") + op.drop_column("app_db", "modified_by_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 c9d2444f2c..353ab3e47d 100644 --- a/agenta-backend/agenta_backend/models/api/api_models.py +++ b/agenta-backend/agenta_backend/models/api/api_models.py @@ -222,6 +222,7 @@ class URI(BaseModel): class App(BaseModel): app_id: str app_name: str + updated_at: str class RemoveApp(BaseModel): diff --git a/agenta-backend/agenta_backend/models/converters.py b/agenta-backend/agenta_backend/models/converters.py index 66ee111dba..41b3ae0caa 100644 --- a/agenta-backend/agenta_backend/models/converters.py +++ b/agenta-backend/agenta_backend/models/converters.py @@ -448,7 +448,11 @@ def base_db_to_pydantic(base_db: VariantBaseDB) -> BaseOutput: def app_db_to_pydantic(app_db: AppDB) -> App: - return App(app_name=app_db.app_name, app_id=str(app_db.id)) + return App( + app_name=app_db.app_name, + app_id=str(app_db.id), + updated_at=str(app_db.updated_at), + ) def image_db_to_pydantic(image_db: ImageDB) -> ImageExtended: diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index 374d2a80e4..6edc481b24 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -77,6 +77,7 @@ class AppDB(Base): ) 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) ) @@ -84,7 +85,8 @@ class AppDB(Base): DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - user = relationship("UserDB") + 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" ) diff --git a/agenta-backend/agenta_backend/routers/environment_router.py b/agenta-backend/agenta_backend/routers/environment_router.py index 50af65bf01..3fe69188ea 100644 --- a/agenta-backend/agenta_backend/routers/environment_router.py +++ b/agenta-backend/agenta_backend/routers/environment_router.py @@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse from fastapi import Request, HTTPException -from agenta_backend.services import db_manager +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 @@ -53,6 +53,14 @@ async def deploy_to_environment( variant_id=payload.variant_id, user_uid=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.variant_id, + object_type="variant", + ) + logger.debug("Successfully updated last_modified_by app information") except Exception as e: logger.exception(f"An error occurred: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) diff --git a/agenta-backend/agenta_backend/routers/evaluation_router.py b/agenta-backend/agenta_backend/routers/evaluation_router.py index db213c829e..e7d57b196c 100644 --- a/agenta-backend/agenta_backend/routers/evaluation_router.py +++ b/agenta-backend/agenta_backend/routers/evaluation_router.py @@ -8,7 +8,6 @@ from agenta_backend.models import converters from agenta_backend.tasks.evaluations import evaluate from agenta_backend.utils.common import APIRouter, isCloudEE -from agenta_backend.services import evaluation_service, db_manager from agenta_backend.models.api.evaluation_model import ( Evaluation, EvaluationScenario, @@ -18,6 +17,7 @@ from agenta_backend.services.evaluator_manager import ( check_ai_critique_inputs, ) +from agenta_backend.services import evaluation_service, db_manager, app_manager if isCloudEE(): from agenta_backend.commons.models.shared_models import Permission @@ -139,6 +139,14 @@ async def create_evaluation( ) evaluations.append(evaluation) + # 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 evaluations except KeyError: raise HTTPException( @@ -411,6 +419,14 @@ 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(payload.evaluations_ids), + object_type="evaluation", + ) + logger.debug("Successfully updated last_modified_by app information") + await evaluation_service.delete_evaluations(payload.evaluations_ids) return Response(status_code=status.HTTP_204_NO_CONTENT) except Exception as exc: diff --git a/agenta-backend/agenta_backend/routers/evaluators_router.py b/agenta-backend/agenta_backend/routers/evaluators_router.py index 932bc5a6f8..8806035247 100644 --- a/agenta-backend/agenta_backend/routers/evaluators_router.py +++ b/agenta-backend/agenta_backend/routers/evaluators_router.py @@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse from agenta_backend.utils.common import APIRouter, isCloudEE -from agenta_backend.services import evaluator_manager, db_manager +from agenta_backend.services import evaluator_manager, db_manager, app_manager from agenta_backend.models.api.evaluation_model import ( Evaluator, @@ -150,8 +150,20 @@ async def create_new_evaluator_config(payload: NewEvaluatorConfig, request: Requ 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 + + traceback.print_exc() raise HTTPException( status_code=500, detail=f"Error creating evaluator configuration: {str(e)}" ) @@ -186,6 +198,14 @@ 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", + ) + logger.debug("Successfully updated last_modified_by app information") return evaluators_configs except Exception as e: import traceback @@ -222,6 +242,14 @@ 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 cc35bdae14..16672358d9 100644 --- a/agenta-backend/agenta_backend/routers/human_evaluation_router.py +++ b/agenta-backend/agenta_backend/routers/human_evaluation_router.py @@ -1,11 +1,12 @@ import random +import logging from typing import List, Dict from fastapi import HTTPException, Body, Request, status, Response from agenta_backend.models import converters -from agenta_backend.services import db_manager from agenta_backend.services import results_service from agenta_backend.services import evaluation_service +from agenta_backend.services import db_manager, app_manager from agenta_backend.utils.common import APIRouter, isCloudEE from agenta_backend.models.api.evaluation_model import ( @@ -35,6 +36,8 @@ ) # noqa pylint: disable-all router = APIRouter() +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) @router.post( @@ -72,6 +75,15 @@ 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", + ) + logger.debug("Successfully updated last_modified_by app information") + return await converters.human_evaluation_db_to_simple_evaluation_output( new_human_evaluation_db ) @@ -253,6 +265,15 @@ 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: @@ -308,6 +329,15 @@ 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 @@ -379,7 +409,7 @@ async def update_evaluation_scenario_score_router( None: 204 No Content status code upon successful update. """ try: - evaluation_scenario = db_manager.fetch_evaluation_scenario_by_id( + evaluation_scenario = await db_manager.fetch_evaluation_scenario_by_id( evaluation_scenario_id ) if evaluation_scenario is None: @@ -406,6 +436,14 @@ async def update_evaluation_scenario_score_router( values_to_update=payload.dict(), ) + # 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 @@ -494,6 +532,14 @@ 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 ) diff --git a/agenta-backend/agenta_backend/routers/variants_router.py b/agenta-backend/agenta_backend/routers/variants_router.py index c0b4f595b7..e3bf09c396 100644 --- a/agenta-backend/agenta_backend/routers/variants_router.py +++ b/agenta-backend/agenta_backend/routers/variants_router.py @@ -98,6 +98,15 @@ async def add_variant_from_base_and_config( user_uid=request.state.user_id, ) logger.debug(f"Successfully added new variant: {db_app_variant}") + + # Update last_modified_by app information + await app_manager.update_last_modified_by( + user_uid=request.state.user_id, + object_id=str(db_app_variant.app_id), + object_type="app", + ) + 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) ) @@ -142,6 +151,14 @@ async def remove_variant( status_code=403, ) + # Update last_modified_by app information + await app_manager.update_last_modified_by( + user_uid=request.state.user_id, + object_id=variant_id, + object_type="variant", + ) + logger.debug("Successfully updated last_modified_by app information") + await app_manager.terminate_and_remove_app_variant(app_variant_id=variant_id) except DockerException as e: detail = f"Docker error while trying to remove the app variant: {str(e)}" @@ -198,6 +215,14 @@ async def update_variant_parameters( parameters=payload.parameters, user_uid=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=variant_id, + object_type="variant", + ) + logger.debug("Successfully updated last_modified_by app information") except ValueError as e: detail = f"Error while trying to update the app variant: {str(e)}" raise HTTPException(status_code=500, detail=detail) @@ -253,6 +278,14 @@ async def update_variant_image( await app_manager.update_variant_image( db_app_variant, image, 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=str(db_app_variant.app_id), + object_type="app", + ) + logger.debug("Successfully updated last_modified_by app information") except ValueError as e: import traceback diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py index 5349464b51..337b1b675d 100644 --- a/agenta-backend/agenta_backend/services/app_manager.py +++ b/agenta-backend/agenta_backend/services/app_manager.py @@ -5,6 +5,7 @@ import uuid import logging from urllib.parse import urlparse +from datetime import datetime, timezone from typing import List, Any, Dict, Optional from agenta_backend.models.api.api_models import ( @@ -183,6 +184,64 @@ async def update_variant_image( await start_variant(app_variant_db) +async def update_last_modified_by( + user_uid: str, + object_id: str, + object_type: str, +) -> None: + """Updates the last_modified_by field in the app variant table. + + Args: + 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. + """ + + async def get_appdb_str_by_id(object_id: str, object_type: str) -> str: + if object_type == "app": + return object_id + elif object_type == "variant": + app_variant_db = await db_manager.fetch_app_variant_by_id(object_id) + 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 == "evaluation": + evaluation_db = await db_manager.fetch_evaluation_by_id(object_id) + if evaluation_db is None: + raise db_manager.NoResultFound( + 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}") + + 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) + await db_manager.update_app( + app_id=app_id, + values_to_update={ + "modified_by_id": user.id, + "updated_at": datetime.now(timezone.utc), + }, + ) + + async def terminate_and_remove_app_variant( app_variant_id: Optional[str] = None, app_variant_db: Optional[AppVariantDB] = None ) -> None: diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py index dced8269ac..699406a324 100644 --- a/agenta-backend/agenta_backend/services/db_manager.py +++ b/agenta-backend/agenta_backend/services/db_manager.py @@ -2649,7 +2649,9 @@ async def fetch_app_by_name_and_parameters( workspace_id=workspace_id, ) else: - query = base_query.join(UserDB).filter(UserDB.uid == user_uid) + query = base_query.join(UserDB, AppDB.user_id == UserDB.id).filter( + UserDB.uid == user_uid + ) result = await session.execute(query) app_db = result.unique().scalars().first() diff --git a/agenta-web/src/components/AppSelector/AppCard.tsx b/agenta-web/src/components/AppSelector/AppCard.tsx index 2e8f405fd0..64f2a95f85 100644 --- a/agenta-web/src/components/AppSelector/AppCard.tsx +++ b/agenta-web/src/components/AppSelector/AppCard.tsx @@ -140,7 +140,7 @@ const AppCard: React.FC<{
Last modified: - {formatDay(new Date().getTime())} + {formatDay(app.updated_at)}
diff --git a/agenta-web/src/lib/Types.ts b/agenta-web/src/lib/Types.ts index 127643d793..9be734c611 100644 --- a/agenta-web/src/lib/Types.ts +++ b/agenta-web/src/lib/Types.ts @@ -21,6 +21,7 @@ export interface TestSet { export interface ListAppsItem { app_id: string app_name: string + updated_at: string } export interface AppVariant {