import uuid import aiohttp import requests import time import asyncio import json from datetime import datetime from fastapi import HTTPException from sqlalchemy import select, Delete from src.ircfa_interaction.schema import ChatRequest, ChatResponse, ChatMessage, Performance, ChatHistory, Conversation, ClassifierResponse, ConversationExportUpdate from src.ircfa_interaction.model import ChatHistory as ChatHistoryModel, Conversation as ConversationModel, LLMConfig, ClassifierConfig, VLLMConfig from src.rag.service import create as chat_create from src.database.core import DbSession, CollectorDbSession from src.database.service import CommonParameters from src.auth.service import CurrentUser from src.database.service import search_filter_sort_paginate from typing import Optional from langchain_core.prompts import ChatPromptTemplate from src.rag.lib.prompt_template_list import ( llm_only_prompt, failure_function_call_prompt, failure_hari_ini, default_prompt ) from langchain_core.chat_history import BaseChatMessageHistory from langchain_postgres import ( PGVectorStore, PGEngine, Column, PostgresChatMessageHistory, ) from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, BaseMessage from langchain_core.runnables.history import RunnableWithMessageHistory from src.rag.lib.init_vector_table import ( create_vector_table ) from typing import List, Dict, Optional, Union, Any from langchain_core.runnables.config import RunnableConfig from src.ircfa_alert.service import ircfa_alert from src.equipments.service import get_history_maintainance async def create(db_session: DbSession, request: ChatRequest, current_user: CurrentUser, location_tag: Optional[str] = None, conversation_id: Optional[str] = None): """Stream chat response based on the request and user context. Yields: str: Streamed response chunks. """ try: print('Deteksi Intent...', '\nMessage: ', request.message) intent_result = await ClassifierConfig.chain.ainvoke({"question": request.message}) print(intent_result) conversation_id = request.conversation_id if conversation_id is None: conversation_id = uuid.uuid4() conversation_title = request.message[:50] + "..." if len(request.message) > 30 else request.message conversation = ConversationModel( id=conversation_id, user_id=current_user.user_id, conversation_id=conversation_id, conversation_title=conversation_title, created_by=current_user.name, updated_by=current_user.name ) db_session.add(conversation) await db_session.commit() # Determine the response based on intent_result response_message = None if (intent_result.intent if hasattr(intent_result, "intent") else None) == "Lainnya": response_message = ( "Saya adalah AI yang dirancang untuk memberikan informasi dan analisis teknis terkait operasi dan pemeliharaan pembangkit listrik. \n" "Saya tidak dapat menjawab pertanyaan Anda karena tidak memiliki pengetahuan terkait topik tersebut." ) elif ( (request.attachments and len(request.attachments) > 0) or (intent_result.intent if hasattr(intent_result, "intent") else None) == "FileAttachment" or (intent_result.intent if hasattr(intent_result, "intent") else None) == "Mendeskripsikan" or (intent_result.intent if hasattr(intent_result, "intent") else None) == "JelaskanGambar" ): response_message = None elif (intent_result.confidence < 0.8 and intent_result.equipment_name) or (not intent_result.equipment_name): response_message = "Pertanyaan kurang dapat dipahami, mohon masukkan pertanyaan lebih spesifik!" else: response_message = None # Let the normal flow continue if response_message is not None: chat_history = ChatHistoryModel( id=request.message_id or uuid.uuid4(), conversation_id=str(conversation_id), user_id=str(current_user.user_id), message=request.message, attachments=[], attachments_filepath=[], response=response_message, context=[], sources=[], sources_filepath=[], faithfullness=None, answer_relevancy=None, context_recall=None, context_relevancy=None, chat_model=LLMConfig.llm.model, embedding_model="", ocr_model="", message_timestamp=datetime.now(), response_timestamp=datetime.now(), time_taken=1, prompt_tokens=0, response_tokens=0, tokens_used=0, created_by=str(current_user.name), updated_by=str(current_user.name), ) db_session.add(chat_history) await db_session.commit() yield f"data: {response_message}\n\n" yield f"event: conversation_id\ndata: {conversation_id}\n\n" yield f"event: usage_metadata\ndata: {json.dumps({'prompt_tokens': 0, 'response_tokens': 0, 'tokens_used': 0, 'time_taken': 1})}\n\n" rag_request = ChatRequest( conversation_id=conversation_id, message=request.message, attachments=request.attachments if request.attachments else None, flow_type=None, equipment_id=request.equipment_id if request.equipment_id else None, questions_batch=None ) print('\nMessage: ', request.message, '\nintent: ', intent_result) rag_result = chat_create(db_session=db_session, request=rag_request, conversation_id=conversation_id, current_user=current_user, intent_result=intent_result) response_chunks = [] # If rag_result.message is a generator/stream, yield chunks if hasattr(rag_result, "__aiter__"): async for chunk in rag_result: # print(chunk, end='', flush=True) text = chunk if isinstance(chunk, str) else str(chunk) response_chunks.append(text) yield f"{text}\n\n" await asyncio.sleep(0.0125) response_message = ''.join(response_chunks) else: response_message = rag_result.message if hasattr(rag_result, "message") else "" yield f"data: {response_message}\n\n" # yield f"event: conversation_id\ndata: {conversation_id}\n\n" # usage_metadata = { # "prompt_tokens": rag_result.prompt_tokens if hasattr(rag_result, "prompt_tokens") else 0, # "response_tokens": rag_result.response_tokens if hasattr(rag_result, "response_tokens") else 0, # "tokens_used": rag_result.tokens_used if hasattr(rag_result, "tokens_used") else 0, # "time_taken": rag_result.time_taken if hasattr(rag_result, "time_taken") else 0 # } # yield f"event: usage_metadata\ndata: {json.dumps(usage_metadata)}\n\n" # yield f"event: context\ndata: {json.dumps(rag_result.context if hasattr(rag_result, 'context') else [])}\n\n" except Exception as e: print("Gagal mendapatkan jawaban LLM:", e) yield f"event: error\ndata: {str(e)}\n\n" # Optionally, raise HTTPException if you want to propagate error # raise HTTPException(status_code=500, detail=f"Failed to continue conversation: {str(e)}") async def get_conversation( *, db_session: DbSession, current_user: CurrentUser, search: Optional[str] = None, page: Optional[int] = 1, items_per_page: Optional[int] = 50, is_exported: Optional[bool] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, common ) -> list[Conversation]: """ Get all conversations for the current user. """ try: query = select(ConversationModel).where(ConversationModel.user_id == current_user.user_id).order_by(ConversationModel.updated_at.desc()) if search: query = query.filter(ConversationModel.conversation_title.ilike(f"%{search}%")) if is_exported is not None: query = query.filter(ConversationModel.is_exported == is_exported) if start_date: try: start_dt = datetime.strptime(start_date, "%Y-%m-%d") query = query.filter(ConversationModel.updated_at >= start_dt) except (ValueError, TypeError): pass if end_date: try: # Set to end of day to be inclusive end_dt = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=999999) query = query.filter(ConversationModel.updated_at <= end_dt) except (ValueError, TypeError): pass common["items_per_page"] = items_per_page common["page"] = page results = await search_filter_sort_paginate(model=query, **common) return results except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get conversations: {str(e)}") async def get_chat_by_id(db_session: DbSession, chat_history_id: str, conversation_id: str, current_user: CurrentUser) -> ChatHistory: """ Get a chat by conversation ID for the current user. """ try: chat = await db_session.execute( select(ChatHistoryModel) .where(ChatHistoryModel.conversation_id == conversation_id) .where(ChatHistoryModel.user_id == current_user.user_id) ) return chat.scalar() except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get chat: {str(e)}") async def get_conversation_by_id(db_session: DbSession, conversation_id: str, current_user: CurrentUser) -> Conversation | None: """ Get a conversation by ID for the current user. """ try: conversation = await db_session.execute( select(ConversationModel) .where(ConversationModel.conversation_id == conversation_id) .where(ConversationModel.user_id == current_user.user_id) ) return conversation.scalar_one_or_none() except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get conversation: {str(e)}") async def get_conversation_message(db_session: DbSession, conversation_id: str, current_user: CurrentUser) -> list[ChatHistory]: """ Get chat history for a specific conversation. """ try: chat_history = await db_session.execute( select(ChatHistoryModel) .where(ChatHistoryModel.conversation_id == conversation_id) .where(ChatHistoryModel.user_id == current_user.user_id) .order_by(ChatHistoryModel.created_at.asc()) ) chat_history_records = chat_history.scalars().all() return [ChatHistory.model_validate(record.__dict__) for record in chat_history_records] except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get conversation history: {str(e)}") async def continue_conversation(db_session: DbSession, conversation_id: str, request: ChatRequest, current_user: CurrentUser) -> ChatResponse: """ Continue an existing conversation with a new message. """ try: # Get existing conversation history chat_records = await get_conversation_message(db_session, conversation_id, current_user) # Build conversation history conversation_history = [] for record in chat_records: conversation_history.append(ChatMessage(role="user", content=record.message)) conversation_history.append(ChatMessage(role="assistant", content=record.response)) # Create request with complete history updated_request = ChatRequest( message=request.message, conversationHistory=conversation_history ) print('Deteksi Intent...', '\nMessage: ', request.message) intent_result = await ClassifierConfig.chain.ainvoke(updated_request) rag_request = ChatRequestRAG( conversation_id=None, message=request.message, conversation_history=[], attachments=None, flow_type=None, equipment_id=None, questions_batch=None ) rag_result = await chat_create(db_session=db_session, request=rag_request, current_user=current_user) if len(rag_result.context) == 0: print('Melakukan FT...', '\nMessage: ', request.message, '\nintent: ', intent_result) ft_result = await LLMConfig.chain.ainvoke({"question": request.message}) response = ft_result.content else: print('Melakukan RAG...', '\nMessage: ', request.message, '\nintent: ', intent_result) response = rag_result.message # Update conversation history updated_history = conversation_history.copy() updated_history.append(ChatMessage(role="user", content=request.message)) updated_history.append(ChatMessage(role="assistant", content=response)) chat_history = ChatHistory( user_id=current_user.user_id, conversation_id=conversation_id, message=request.message, response=response, created_by=current_user.name, updated_by=current_user.name ) db_session.add(chat_history) await db_session.commit() return ChatResponseRAG( message=response, userMessage=request.message, conversationHistory=updated_history, timestamp=datetime.now().isoformat(), model=LLMConfig.llm.model, time_taken=rag_result.time_taken, prompt_tokens= rag_result.prompt_tokens, response_tokens= rag_result.response_tokens, tokens_used= rag_result.tokens_used ) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to continue conversation: {str(e)}") async def update_conversation_title(db_session: DbSession, conversation_id: str, title: str, current_user: CurrentUser) -> Conversation: """Updates covnersation title.""" new_title = title query = select(ConversationModel).where( ConversationModel.conversation_id == conversation_id, ConversationModel.user_id == current_user.user_id ) result = await db_session.execute(query) conversation = result.scalars().one_or_none() if not conversation: raise HTTPException(status_code=404, detail="Conversation not found") if len(new_title) > 30: new_title = new_title[:30] + "..." conversation.conversation_title = new_title conversation.updated_by = current_user.name await db_session.commit() return conversation async def update_chat_history_feedback( db_session: DbSession, chat_history_id: str, feedback: str, current_user: CurrentUser, comment: Optional[str] = None, screenshot: Optional[str] = None ): """ Update feedback (rating + optional comment + optional screenshot) for a chat history entry. """ try: chat_history = await db_session.execute( select(ChatHistoryModel) .where(ChatHistoryModel.id == chat_history_id) .where(ChatHistoryModel.user_id == current_user.user_id) ) chat_history = chat_history.scalar_one_or_none() if not chat_history: raise HTTPException(status_code=404, detail="Chat history not found") chat_history.feedback = feedback chat_history.feedback_comment = comment chat_history.feedback_screenshot = screenshot chat_history.updated_by = current_user.name db_session.add(chat_history) await db_session.commit() response = ChatHistory( id = str(chat_history.id), user_id = str(chat_history.user_id), message = str(chat_history.message), attachments = chat_history.attachments if chat_history.attachments else None, attachments_filepath = chat_history.attachments_filepath if chat_history.attachments_filepath else None, response = str(chat_history.response), context = chat_history.context if chat_history.context else None, sources = chat_history.sources if chat_history.sources else None, sources_filepath = chat_history.sources_filepath if chat_history.sources_filepath else None, time_taken = chat_history.time_taken if chat_history.time_taken else None, prompt_tokens = chat_history.prompt_tokens if chat_history.prompt_tokens else None, response_tokens = chat_history.response_tokens if chat_history.response_tokens else None, tokens_used = chat_history.tokens_used if chat_history.tokens_used else None, timestamp = chat_history.message_timestamp if chat_history.message_timestamp else None, response_timestamp = chat_history.response_timestamp if chat_history.response_timestamp else None, conversation_id = str(chat_history.conversation_id), chat_model = chat_history.chat_model if chat_history.chat_model else None, embedding_model = chat_history.embedding_model if chat_history.embedding_model else None, ocr_model = chat_history.ocr_model if chat_history.ocr_model else None, feedback = chat_history.feedback if chat_history.feedback else None, feedback_comment = chat_history.feedback_comment if chat_history.feedback_comment else None, feedback_screenshot = chat_history.feedback_screenshot if chat_history.feedback_screenshot else None, updated_at = chat_history.updated_at if chat_history.updated_at else None, created_at = chat_history.created_at if chat_history.created_at else None, ) return response except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to update chat history feedback: {str(e)}") async def get_feedback_analytics( db_session: DbSession, current_user: CurrentUser, start_date: Optional[str] = None, end_date: Optional[str] = None, ): """ Aggregate feedback analytics across all entries for the evaluation dashboard. Admins see all users' feedback; regular users see only their own. """ try: is_admin = getattr(current_user, "role", None) in ("Admin", "Super Admin", "Application Administrator") query = select(ChatHistoryModel).where(ChatHistoryModel.feedback.isnot(None)) if not is_admin: query = query.where(ChatHistoryModel.user_id == current_user.user_id) if start_date: try: start_dt = datetime.strptime(start_date, "%Y-%m-%d") query = query.where(ChatHistoryModel.response_timestamp >= start_dt) except (ValueError, TypeError): pass if end_date: try: end_dt = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59) query = query.where(ChatHistoryModel.response_timestamp <= end_dt) except (ValueError, TypeError): pass result = await db_session.execute(query) records = result.scalars().all() good_count = sum(1 for r in records if r.feedback == "good") bad_count = sum(1 for r in records if r.feedback == "bad") with_comments = sum(1 for r in records if r.feedback_comment) with_screenshots = sum(1 for r in records if r.feedback_screenshot) total = len(records) # Group by day day_map: dict = {} for r in records: if not r.response_timestamp: continue day = r.response_timestamp.strftime("%Y-%m-%d") if day not in day_map: day_map[day] = {"date": day, "good": 0, "bad": 0} if r.feedback == "good": day_map[day]["good"] += 1 elif r.feedback == "bad": day_map[day]["bad"] += 1 by_day = sorted(day_map.values(), key=lambda x: x["date"]) # Group by model model_map: dict = {} for r in records: m = r.chat_model or "Unknown" if m not in model_map: model_map[m] = {"model": m, "good": 0, "bad": 0, "total": 0} model_map[m]["total"] += 1 if r.feedback == "good": model_map[m]["good"] += 1 elif r.feedback == "bad": model_map[m]["bad"] += 1 by_model = sorted(model_map.values(), key=lambda x: x["total"], reverse=True) # Recent feedback entries (paginated-like, last 50) recent = sorted(records, key=lambda x: x.response_timestamp or datetime.min, reverse=True)[:50] recent_feedback = [ { "id": str(r.id), "conversation_id": str(r.conversation_id), "feedback": r.feedback, "feedback_comment": r.feedback_comment, "has_screenshot": bool(r.feedback_screenshot), "screenshot": r.feedback_screenshot, "message_snippet": (r.message[:120] + "...") if r.message and len(r.message) > 120 else r.message, "response_snippet": (r.response[:180] + "...") if r.response and len(r.response) > 180 else r.response, "timestamp": r.response_timestamp.isoformat() if r.response_timestamp else None, "chat_model": r.chat_model, "user_id": str(r.user_id), } for r in recent ] return { "summary": { "total_feedback": total, "good_count": good_count, "bad_count": bad_count, "with_comments": with_comments, "with_screenshots": with_screenshots, "good_rate": round(good_count / total * 100, 1) if total > 0 else 0, }, "by_day": by_day, "by_model": by_model, "recent_feedback": recent_feedback, } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get feedback analytics: {str(e)}") async def delete_conversation(db_session: DbSession, conversation_id: str, current_user: CurrentUser) -> dict: """ Cascade delete a conversation and its chat history for the current user. """ try: # Delete chat history records first (cascade) # await db_session.execute( # ChatHistory.__table__.delete().where( # (ChatHistory.conversation_id == conversation_id) & # (ChatHistory.user_id == current_user.user_id) # ) # ) # await db_session.execute( # ChatHistory.__table__.delete().where( # (ChatHistory.conversation_id == conversation_id) & # (ChatHistory.user_id == current_user.user_id) # ) # ) # Delete conversation record query = Delete(ConversationModel).where(ConversationModel.conversation_id == conversation_id) await db_session.execute(query) await db_session.commit() return {"detail": "Conversation and related chat history deleted successfully."} except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to cascade delete conversation: {str(e)}") async def delete_chat_by_id(db_session: DbSession, chat_history_id: str, conversation_id: str, current_user: CurrentUser) -> dict: """ Delete a specific chat history entry by its ID for the current user. """ try: query = Delete(ChatHistoryModel).where( (ChatHistoryModel.id == chat_history_id) & (ChatHistoryModel.conversation_id == conversation_id) & (ChatHistoryModel.user_id == current_user.user_id) ) result = await db_session.execute(query) await db_session.commit() if result.rowcount == 0: raise HTTPException(status_code=404, detail="Chat history not found or not authorized to delete.") return { "chat_history_id": chat_history_id, "conversation_id": conversation_id } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to delete chat history: {str(e)}") async def update_conversation_export_status(db_session: DbSession, conversation_id: str, update_data: ConversationExportUpdate, current_user: CurrentUser) -> ConversationModel: """Updates conversation export status.""" query = select(ConversationModel).where( ConversationModel.conversation_id == conversation_id, ConversationModel.user_id == current_user.user_id ) result = await db_session.execute(query) conversation = result.scalars().one_or_none() if not conversation: raise HTTPException(status_code=404, detail="Conversation not found") conversation.is_exported = update_data.is_exported conversation.report_name = update_data.report_name conversation.report_content = update_data.report_content conversation.exported_at = datetime.now() conversation.updated_by = current_user.name await db_session.commit() await db_session.refresh(conversation) return conversation async def init_db_models(db_session): """Populates the rcfa_model_config table on startup if it's empty.""" from src.ircfa_interaction.model import ModelConfig from src.ircfa_interaction.config import MODELS_DATA as STATIC_MODELS_DATA, DEFAULT_LLM_CONFIG as STATIC_DEFAULT_LLM_CONFIG # Check if table already has entries result = await db_session.execute(select(ModelConfig)) existing = result.scalars().all() if existing: # Table already initialized. Just reload in-memory cache await refresh_in_memory_configs(db_session) return print("Initializing rcfa_model_config table with default models...") for m in STATIC_MODELS_DATA: db_model = ModelConfig( name=m.name, display_name=m.display_name, model=m.model, model_server=m.model_server, api_key=m.api_key, provider=m.provider, description=m.description, stream=m.stream, enabled=m.enabled, is_default=(m.name == STATIC_DEFAULT_LLM_CONFIG), generate_cfg=m.generate_cfg, created_by="system", updated_by="system" ) db_session.add(db_model) await db_session.commit() # Sync memory cache await refresh_in_memory_configs(db_session) async def refresh_in_memory_configs(db_session): """Queries configurations from the database and refreshes the in-memory cache.""" from src.ircfa_interaction.model import ModelConfig from src.ircfa_interaction.config import LLMModelConfig, update_models_config result = await db_session.execute(select(ModelConfig).order_by(ModelConfig.name)) db_models = result.scalars().all() if not db_models: return models_list = [] default_name = "icon-qwen" for m in db_models: cfg = LLMModelConfig( name=m.name, display_name=m.display_name, model=m.model, model_server=m.model_server, api_key=m.api_key, provider=m.provider, description=m.description, stream=m.stream, enabled=m.enabled, generate_cfg=m.generate_cfg or { "temperature": 0.3, "top_p": 0.9, "max_tokens": 16382 * 2 } ) models_list.append(cfg) if m.is_default: default_name = m.name # Update in-memory lists in config.py update_models_config(models_list, default_name) async def toggle_model_service(db_session, name: str, enabled: bool, current_user: CurrentUser): """Toggles enablement status of a model config in the database and updates memory cache.""" from src.ircfa_interaction.model import ModelConfig result = await db_session.execute(select(ModelConfig).where(ModelConfig.name == name)) model = result.scalar_one_or_none() if not model: raise HTTPException(status_code=404, detail="Model not found") model.enabled = enabled model.updated_by = current_user.name await db_session.commit() await refresh_in_memory_configs(db_session) return model async def set_default_model_service(db_session, name: str, current_user: CurrentUser): """Selects a model as the default config in the database and updates memory cache.""" from src.ircfa_interaction.model import ModelConfig from sqlalchemy import update result = await db_session.execute(select(ModelConfig).where(ModelConfig.name == name)) model = result.scalar_one_or_none() if not model: raise HTTPException(status_code=404, detail="Model not found") # Unset all defaults await db_session.execute( update(ModelConfig).values(is_default=False) ) model.is_default = True model.updated_by = current_user.name await db_session.commit() await refresh_in_memory_configs(db_session) return model async def get_all_models_service(db_session): """Retrieves all model configurations from the database.""" from src.ircfa_interaction.model import ModelConfig result = await db_session.execute(select(ModelConfig).order_by(ModelConfig.name)) return result.scalars().all() async def create_model_service(db_session, data, current_user: CurrentUser): """Creates a new model configuration in the database and refreshes memory cache.""" from src.ircfa_interaction.model import ModelConfig # Check if name already exists result = await db_session.execute(select(ModelConfig).where(ModelConfig.name == data.name)) if result.scalar_one_or_none(): raise HTTPException(status_code=400, detail=f"Model config with name '{data.name}' already exists.") model = ModelConfig( name=data.name, display_name=data.display_name, model=data.model, model_server=data.model_server, api_key=data.api_key or "not-needed", provider=data.provider or "openai", description=data.description, stream=data.stream if data.stream is not None else True, enabled=data.enabled if data.enabled is not None else True, is_default=False, generate_cfg=data.generate_cfg, created_by=current_user.name, updated_by=current_user.name ) db_session.add(model) await db_session.commit() await refresh_in_memory_configs(db_session) return model async def update_model_service(db_session, name: str, data, current_user: CurrentUser): """Updates an existing model configuration in the database and refreshes memory cache.""" from src.ircfa_interaction.model import ModelConfig result = await db_session.execute(select(ModelConfig).where(ModelConfig.name == name)) model = result.scalar_one_or_none() if not model: raise HTTPException(status_code=404, detail="Model not found") model.display_name = data.display_name model.model = data.model model.model_server = data.model_server model.api_key = data.api_key if data.api_key is not None else model.api_key model.provider = data.provider if data.provider is not None else model.provider model.description = data.description model.stream = data.stream if data.stream is not None else model.stream model.enabled = data.enabled if data.enabled is not None else model.enabled if data.generate_cfg is not None: model.generate_cfg = data.generate_cfg model.updated_by = current_user.name await db_session.commit() await refresh_in_memory_configs(db_session) return model async def delete_model_service(db_session, name: str, current_user: CurrentUser): """Deletes a model configuration from the database and refreshes memory cache.""" from src.ircfa_interaction.model import ModelConfig result = await db_session.execute(select(ModelConfig).where(ModelConfig.name == name)) model = result.scalar_one_or_none() if not model: raise HTTPException(status_code=404, detail="Model not found") if model.is_default: raise HTTPException(status_code=400, detail="Cannot delete the default model config. Please set another model as default first.") await db_session.delete(model) await db_session.commit() await refresh_in_memory_configs(db_session) return {"detail": f"Model config '{name}' deleted successfully."}