diff --git a/api/app/controllers/prompt_optimizer_controller.py b/api/app/controllers/prompt_optimizer_controller.py index dba52d0b..61195deb 100644 --- a/api/app/controllers/prompt_optimizer_controller.py +++ b/api/app/controllers/prompt_optimizer_controller.py @@ -1,5 +1,5 @@ -import uuid import json +import uuid from fastapi import APIRouter, Depends, Path from sqlalchemy.orm import Session @@ -8,9 +8,13 @@ from starlette.responses import StreamingResponse from app.core.logging_config import get_api_logger from app.core.response_utils import success from app.dependencies import get_current_user, get_db -from app.models.prompt_optimizer_model import RoleType -from app.schemas.prompt_optimizer_schema import PromptOptMessage, PromptOptModelSet, CreateSessionResponse, \ - OptimizePromptResponse, SessionHistoryResponse, SessionMessage +from app.schemas.prompt_optimizer_schema import ( + PromptOptMessage, + CreateSessionResponse, + SessionHistoryResponse, + SessionMessage, + PromptSaveRequest +) from app.schemas.response_schema import ApiResponse from app.services.prompt_optimizer_service import PromptOptimizerService @@ -135,3 +139,109 @@ async def get_prompt_opt( "X-Accel-Buffering": "no" } ) + + +@router.post( + "/releases", + summary="Get prompt optimization", + response_model=ApiResponse +) +def save_prompt( + data: PromptSaveRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + """ + Save a prompt release for the current tenant. + + Args: + data (PromptSaveRequest): Request body containing session_id, title, and prompt. + db (Session): SQLAlchemy database session, injected via dependency. + current_user: Currently authenticated user object, injected via dependency. + + Returns: + ApiResponse: Standard API response containing the saved prompt release info: + - id: UUID of the prompt release + - session_id: associated session + - title: prompt title + - prompt: prompt content + - created_at: timestamp of creation + + Raises: + Any database or service exceptions are propagated to the global exception handler. + """ + service = PromptOptimizerService(db) + prompt_info = service.save_prompt( + tenant_id=current_user.tenant_id, + session_id=data.session_id, + title=data.title, + prompt=data.prompt + ) + return success(data=prompt_info) + + +@router.delete( + "/releases/{prompt_id}", + summary="Delete prompt (soft delete)", + response_model=ApiResponse +) +def delete_prompt( + prompt_id: uuid.UUID = Path(..., description="Prompt ID"), + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + """ + Soft delete a prompt release. + + Args: + prompt_id + db (Session): Database session + current_user: Current logged-in user + + Returns: + ApiResponse: Success message confirming deletion + """ + service = PromptOptimizerService(db) + service.delete_prompt( + tenant_id=current_user.tenant_id, + prompt_id=prompt_id + ) + return success(msg="Prompt deleted successfully") + + +@router.get( + "/releases/list", + summary="Get paginated list of released prompts with optional filter", + response_model=ApiResponse +) +def get_release_list( + page: int = 1, + page_size: int = 20, + keyword: str | None = None, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + """ + Retrieve paginated list of released prompts for the current tenant. + Optionally filter by keyword in title. + + Args: + page (int): Page number (starting from 1) + page_size (int): Number of items per page (max 100) + keyword (str | None): Optional keyword to filter prompt titles + db (Session): Database session + current_user: Current logged-in user + + Returns: + ApiResponse: Contains paginated list of prompt releases with metadata + """ + service = PromptOptimizerService(db) + result = service.get_release_list( + tenant_id=current_user.tenant_id, + page=max(1, page), + page_size=min(max(1, page_size), 100), + filter_keyword=keyword + ) + return success(data=result) + + diff --git a/api/app/models/prompt_optimizer_model.py b/api/app/models/prompt_optimizer_model.py index 39845ee7..f96b0a66 100644 --- a/api/app/models/prompt_optimizer_model.py +++ b/api/app/models/prompt_optimizer_model.py @@ -2,7 +2,7 @@ import datetime import uuid from enum import StrEnum -from sqlalchemy import Column, ForeignKey, Text, DateTime, String, Index +from sqlalchemy import Column, ForeignKey, Text, DateTime, String, Index, Boolean from sqlalchemy.dialects.postgresql import UUID from app.db import Base @@ -121,10 +121,33 @@ class PromptOptimizerSessionHistory(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, comment="Tenant ID") # app_id = Column(UUID(as_uuid=True), ForeignKey("apps.id"), nullable=False, comment="Application ID") - session_id = Column(UUID(as_uuid=True), ForeignKey("prompt_opt_session_list.id"),nullable=False, comment="Session ID") + session_id = Column( + UUID(as_uuid=True), + ForeignKey("prompt_opt_session_list.id"), + nullable=False, + comment="Session ID" + ) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, comment="User ID") role = Column(String, nullable=False, comment="Message Role") content = Column(Text, nullable=False, comment="Message Content") # prompt = Column(Text, nullable=False, comment="Prompt") created_at = Column(DateTime, default=datetime.datetime.now, comment="Creation Time", index=True) + + +class PromptHistory(Base): + __tablename__ = "prompt_history" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) + tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, comment="Tenant ID") + + session_id = Column( + UUID(as_uuid=True), + ForeignKey("prompt_opt_session_list.id"), + nullable=False, + comment="Session ID" + ) + title = Column(String, nullable=False, comment="Title") + prompt = Column(Text, nullable=False, comment="Prompt") + created_at = Column(DateTime, default=datetime.datetime.now, comment="Creation Time", index=True) + is_delete = Column(Boolean, default=False, comment="Delete") diff --git a/api/app/repositories/prompt_optimizer_repository.py b/api/app/repositories/prompt_optimizer_repository.py index ba65257a..e73ab513 100644 --- a/api/app/repositories/prompt_optimizer_repository.py +++ b/api/app/repositories/prompt_optimizer_repository.py @@ -4,7 +4,10 @@ from sqlalchemy.orm import Session from app.core.logging_config import get_db_logger from app.models.prompt_optimizer_model import ( - PromptOptimizerSession, PromptOptimizerSessionHistory, RoleType + PromptOptimizerSession, + PromptOptimizerSessionHistory, + RoleType, + PromptHistory ) db_logger = get_db_logger() @@ -16,6 +19,12 @@ class PromptOptimizerSessionRepository: def __init__(self, db: Session): self.db = db + def get_session_by_id(self, session_id: uuid.UUID) -> PromptOptimizerSession | None: + session = self.db.query(PromptOptimizerSession).filter( + PromptOptimizerSession.id == session_id, + ).first() + return session + def create_session( self, tenant_id: uuid.UUID, @@ -38,12 +47,9 @@ class PromptOptimizerSessionRepository: user_id=user_id, ) self.db.add(session) - self.db.commit() - self.db.refresh(session) - db_logger.debug(f"Prompt optimization session created: ID:{session.id}") return session except Exception as e: - db_logger.error(f"Error creating prompt optimization session: user_id={user_id} - {str(e)}") + db_logger.error(f"Error creating prompt optimization session: - {str(e)}") raise def get_session_history( @@ -71,10 +77,10 @@ class PromptOptimizerSessionRepository: PromptOptimizerSession.id == session_id, PromptOptimizerSession.user_id == user_id ).first() - + if not session: return [] - + history = self.db.query(PromptOptimizerSessionHistory).filter( PromptOptimizerSessionHistory.session_id == session.id, PromptOptimizerSessionHistory.user_id == user_id @@ -104,11 +110,11 @@ class PromptOptimizerSessionRepository: PromptOptimizerSession.user_id == user_id, PromptOptimizerSession.tenant_id == tenant_id ).first() - + if not session: db_logger.error(f"Session {session_id} not found for user {user_id}") raise ValueError(f"Session {session_id} not found for user {user_id}") - + message = PromptOptimizerSessionHistory( tenant_id=tenant_id, session_id=session.id, @@ -117,8 +123,199 @@ class PromptOptimizerSessionRepository: content=content, ) self.db.add(message) - self.db.commit() + return message except Exception as e: db_logger.error(f"Error creating prompt optimization session history: session_id={session_id} - {str(e)}") raise + + def get_first_user_message(self, session_id: uuid.UUID) -> str | None: + """ + Get the first user message from a session. + + Args: + session_id (uuid.UUID): The session ID. + + Returns: + str | None: The content of the first user message, or None if not found. + """ + try: + message = self.db.query(PromptOptimizerSessionHistory).filter( + PromptOptimizerSessionHistory.session_id == session_id, + PromptOptimizerSessionHistory.role == RoleType.USER.value + ).order_by( + PromptOptimizerSessionHistory.created_at.asc() + ).first() + + return message.content if message else None + except Exception as e: + db_logger.error(f"Error getting first user message: session_id={session_id} - {str(e)}") + raise + + +class PromptReleaseRepository: + def __init__(self, db: Session): + self.db = db + + def get_prompt_by_session_id(self, session_id: uuid.UUID) -> PromptHistory | None: + prompt_obj = self.db.query(PromptHistory).filter( + PromptHistory.session_id == session_id, + PromptHistory.is_delete.is_(False) + ).first() + return prompt_obj + + def create_prompt_release( + self, + tenant_id: uuid.UUID, + title: str, + session_id: uuid.UUID, + prompt: str, + ) -> PromptHistory: + try: + prompt_obj = PromptHistory( + tenant_id=tenant_id, + title=title, + session_id=session_id, + prompt=prompt, + ) + self.db.add(prompt_obj) + return prompt_obj + except Exception as e: + db_logger.error(f"Error creating prompt release: session_id={session_id} - {str(e)}") + raise + + def soft_delete_prompt(self, prompt_obj: PromptHistory) -> None: + """ + Soft delete a prompt release by setting is_delete flag to True. + + Args: + prompt_obj (PromptHistory): The prompt release object to delete. + """ + try: + prompt_obj.is_delete = True + db_logger.debug(f"Soft deleted prompt release: id={prompt_obj.id}, session_id={prompt_obj.session_id}") + except Exception as e: + db_logger.error(f"Error soft deleting prompt release: id={prompt_obj.id} - {str(e)}") + raise + + def get_prompt_by_id(self, prompt_id: uuid.UUID) -> PromptHistory | None: + """ + Get a prompt release by its ID. + + Args: + prompt_id (uuid.UUID): The prompt release ID. + + Returns: + PromptHistory | None: The prompt release object or None if not found. + """ + try: + prompt_obj = self.db.query(PromptHistory).filter( + PromptHistory.id == prompt_id + ).first() + return prompt_obj + except Exception as e: + db_logger.error(f"Error getting prompt release by id: id={prompt_id} - {str(e)}") + raise + + def count_prompts(self, tenant_id: uuid.UUID) -> int: + """ + Count total number of non-deleted prompts for a tenant. + + Args: + tenant_id (uuid.UUID): The tenant ID. + + Returns: + int: Total count of prompts. + """ + try: + count = self.db.query(PromptHistory).filter( + PromptHistory.tenant_id == tenant_id, + PromptHistory.is_delete.is_(False) + ).count() + return count + except Exception as e: + db_logger.error(f"Error counting prompts: tenant_id={tenant_id} - {str(e)}") + raise + + def get_prompts_paginated( + self, + tenant_id: uuid.UUID, + offset: int, + limit: int + ) -> list[PromptHistory]: + """ + Get paginated list of prompt releases for a tenant. + + Args: + tenant_id (uuid.UUID): The tenant ID. + offset (int): Number of records to skip. + limit (int): Maximum number of records to return. + + Returns: + list[PromptHistory]: List of prompt releases. + """ + try: + prompts = self.db.query(PromptHistory).filter( + PromptHistory.tenant_id == tenant_id, + PromptHistory.is_delete.is_(False) + ).order_by( + PromptHistory.created_at.desc() + ).offset(offset).limit(limit).all() + return prompts + except Exception as e: + db_logger.error(f"Error getting paginated prompts: tenant_id={tenant_id} - {str(e)}") + raise + + def count_prompts_by_keyword(self, tenant_id: uuid.UUID, keyword: str) -> int: + """ + Count total number of non-deleted prompts matching keyword for a tenant. + + Args: + tenant_id (uuid.UUID): The tenant ID. + keyword (str): Search keyword for title. + + Returns: + int: Total count of matching prompts. + """ + try: + count = self.db.query(PromptHistory).filter( + PromptHistory.tenant_id == tenant_id, + PromptHistory.is_delete.is_(False), + PromptHistory.title.ilike(f"%{keyword}%") + ).count() + return count + except Exception as e: + db_logger.error(f"Error counting prompts by keyword: tenant_id={tenant_id}, keyword={keyword} - {str(e)}") + raise + + def search_prompts_paginated( + self, + tenant_id: uuid.UUID, + keyword: str, + offset: int, + limit: int + ) -> list[PromptHistory]: + """ + Search prompt releases by keyword in title with pagination. + + Args: + tenant_id (uuid.UUID): The tenant ID. + keyword (str): Search keyword for title. + offset (int): Number of records to skip. + limit (int): Maximum number of records to return. + + Returns: + list[PromptHistory]: List of matching prompt releases. + """ + try: + prompts = self.db.query(PromptHistory).filter( + PromptHistory.tenant_id == tenant_id, + PromptHistory.is_delete.is_(False), + PromptHistory.title.ilike(f"%{keyword}%") + ).order_by( + PromptHistory.created_at.desc() + ).offset(offset).limit(limit).all() + return prompts + except Exception as e: + db_logger.error(f"Error searching prompts: tenant_id={tenant_id}, keyword={keyword} - {str(e)}") + raise diff --git a/api/app/schemas/prompt_optimizer_schema.py b/api/app/schemas/prompt_optimizer_schema.py index e1f27be0..08a11317 100644 --- a/api/app/schemas/prompt_optimizer_schema.py +++ b/api/app/schemas/prompt_optimizer_schema.py @@ -22,6 +22,23 @@ class PromptOptMessage(BaseModel): ) +class PromptSaveRequest(BaseModel): + session_id: UUID = Field( + ..., + description="Session ID" + ) + + title: str = Field( + ..., + description="Prompt Title" + ) + + prompt: str = Field( + ..., + description="Optimized prompt content" + ) + + class PromptOptModelSet(BaseModel): id: UUID | None = Field( default=None, diff --git a/api/app/services/prompt_optimizer_service.py b/api/app/services/prompt_optimizer_service.py index c6142c01..123c6af5 100644 --- a/api/app/services/prompt_optimizer_service.py +++ b/api/app/services/prompt_optimizer_service.py @@ -18,7 +18,8 @@ from app.models.prompt_optimizer_model import ( ) from app.repositories.model_repository import ModelConfigRepository from app.repositories.prompt_optimizer_repository import ( - PromptOptimizerSessionRepository + PromptOptimizerSessionRepository, + PromptReleaseRepository ) from app.schemas.prompt_optimizer_schema import OptimizePromptResult @@ -28,6 +29,8 @@ logger = get_business_logger() class PromptOptimizerService: def __init__(self, db: Session): self.db = db + self.optim_repo = PromptOptimizerSessionRepository(self.db) + self.release_repo = PromptReleaseRepository(self.db) def get_model_config( self, @@ -78,10 +81,12 @@ class PromptOptimizerService: Returns: PromptOptimzerSession: The newly created prompt optimization session. """ - session = PromptOptimizerSessionRepository(self.db).create_session( + session = self.optim_repo.create_session( tenant_id=tenant_id, user_id=user_id ) + self.db.commit() + self.db.refresh(session) return session def get_session_message_history( @@ -106,7 +111,7 @@ class PromptOptimizerService: - role (str): The role of the message sender, e.g., 'system', 'user', or 'assistant'. - content (str): The content of the message. """ - history = PromptOptimizerSessionRepository(self.db).get_session_history( + history = self.optim_repo.get_session_history( session_id=session_id, user_id=user_id ) @@ -295,4 +300,165 @@ class PromptOptimizerService: role=role, content=content ) + self.db.commit() + self.db.refresh(message) return message + + def save_prompt( + self, + tenant_id: uuid.UUID, + session_id: uuid.UUID, + title: str, + prompt: str + ) -> dict: + """ + Create and save a new prompt release for a given session. + + Args: + tenant_id (uuid.UUID): The ID of the tenant owning the prompt. + session_id (uuid.UUID): The ID of the session to associate with this prompt. + title (str): The title of the prompt release. + prompt (str): The content of the prompt. + + Returns: + dict: A dictionary containing: + - id (UUID): The unique ID of the created prompt release. + - session_id (UUID): The session ID linked to the release. + - title (str): The title of the prompt. + - prompt (str): The prompt content. + - created_at (int): Timestamp (in milliseconds) of when the prompt was created. + + Raises: + BusinessException: If a prompt release already exists for the given session. + """ + session = self.optim_repo.get_session_by_id(session_id) + if session is None or session.tenant_id != tenant_id: + raise BusinessException( + "Session does not exist or the current user has no access", + BizCode.BAD_REQUEST + ) + + if self.release_repo.get_prompt_by_session_id(session_id): + raise BusinessException( + "A release already exists for the current session", + BizCode.BAD_REQUEST + ) + + prompt_obj = self.release_repo.create_prompt_release( + tenant_id=tenant_id, + title=title, + session_id=session_id, + prompt=prompt + ) + self.db.commit() + self.db.refresh(prompt_obj) + return { + "id": prompt_obj.id, + "session_id": prompt_obj.session_id, + "title": prompt_obj.title, + "prompt": prompt_obj.prompt, + "created_at": int(prompt_obj.created_at.timestamp() * 1000) + } + + def delete_prompt( + self, + tenant_id: uuid.UUID, + prompt_id: uuid.UUID + ) -> None: + """ + Soft delete a prompt release by prompt_id. + + Args: + tenant_id (uuid.UUID): Tenant identifier. + prompt_id (uuid.UUID): Prompt identifier. + + Raises: + BusinessException: If the prompt does not exist or already deleted. + """ + prompt_obj = self.release_repo.get_prompt_by_id(prompt_id) + if not prompt_obj or prompt_obj.is_delete: + raise BusinessException( + "Prompt does not exist or has already been deleted", + BizCode.NOT_FOUND + ) + + if prompt_obj.tenant_id != tenant_id: + raise BusinessException( + "No permission to delete this prompt", + BizCode.FORBIDDEN + ) + + self.release_repo.soft_delete_prompt(prompt_obj) + self.db.commit() + logger.info(f"Prompt soft deleted, prompt_id={prompt_id}, tenant_id={tenant_id}") + + def get_release_list( + self, + tenant_id: uuid.UUID, + page: int, + page_size: int, + filter_keyword: str | None = None + ) -> dict[str, int | list[Any]]: + """ + Get paginated list of prompt releases with optional filter. + + Args: + tenant_id (uuid.UUID): Tenant identifier. + page (int): Page number (starting from 1). + page_size (int): Number of items per page. + filter_keyword (str | None): Optional keyword to filter by title. + + Returns: + dict: Contains total count, pagination info, and list of releases. + """ + offset = (page - 1) * page_size + + # Get total count and releases based on filter + if filter_keyword: + total = self.release_repo.count_prompts_by_keyword(tenant_id, filter_keyword) + releases = self.release_repo.search_prompts_paginated( + tenant_id=tenant_id, + keyword=filter_keyword, + offset=offset, + limit=page_size + ) + else: + total = self.release_repo.count_prompts(tenant_id) + releases = self.release_repo.get_prompts_paginated( + tenant_id=tenant_id, + offset=offset, + limit=page_size + ) + + items = [] + for release in releases: + # Get first user message from session + first_message = self.optim_repo.get_first_user_message( + session_id=release.session_id + ) + + items.append({ + "id": release.id, + "title": release.title, + "prompt": release.prompt, + "created_at": int(release.created_at.timestamp() * 1000), + "first_message": first_message + }) + + log_msg = f"Retrieved {len(items)} prompt releases, page={page}, tenant_id={tenant_id}" + if filter_keyword: + log_msg += f", filter='{filter_keyword}'" + logger.info(log_msg) + + result = { + "page": { + "total": total, + "page": page, + "page_size": page_size, + "hasnext": page * page_size < total + }, + "keyword": filter_keyword, + "items": items + } + + return result