import uuid import json as json_module from typing import Optional, List from fastapi import APIRouter, HTTPException, status, Query, File, UploadFile, Request from fastapi.responses import StreamingResponse from src.ircfa_interaction.schema import ChatResponse, Conversation, ConversationUpdate, ConversationDataPagination, UpdateFeedback, RootCauseRequest, RootCauseResponse, ConversationExportUpdate, ChatRequest, FlowType, ChatHistory, ChatHistoryResponse, ModelRead, AdminModelRead, ModelConfigCreate, ModelConfigUpdate, MemoryLabSnapshot, MemoryRead from src.ircfa_interaction.tools import RootCauseSPLNTool from src.ircfa_interaction.model import ClassifierConfig, Conversation as ConversationModel, RCFAMemory from src.rag.service import get_rag_histories_by_conversation_id, create as rag_create from src.ircfa_interaction.service import create, get_conversation, continue_conversation, delete_conversation, update_conversation_title, get_conversation_by_id, get_chat_by_id, update_chat_history_feedback, delete_chat_by_id, update_conversation_export_status, get_all_models_service, toggle_model_service, set_default_model_service, create_model_service, update_model_service, delete_model_service, get_feedback_analytics from src.database.service import CommonParameters, search_filter_sort_paginate from src.database.core import DbSession, CollectorDbSession from src.auth.service import CurrentUser, Token from src.ircfa_interaction.config import MODELS_DATA, MAX_FILE_SIZE from src.ircfa_interaction.stream import stream_create from src.ircfa_interaction.stream2 import stream_create as stream_create_v2 from src.models import StandardResponse from src.rag.lib.prompt_template_list import searchRAGKeywords from uuid import UUID from pydantic import Field, BaseModel from sqlalchemy import func, select router = APIRouter() class ToolInvocation(BaseModel): toolCallId: str toolName: str args: dict result: dict class ClientAttachment(BaseModel): name: str contentType: str url: str class ClientMessage(BaseModel): role: str content: str experimental_attachments: Optional[List[ClientAttachment]] = None toolInvocations: Optional[List[ToolInvocation]] = None def _memory_to_read(memory: RCFAMemory) -> MemoryRead: """Expose only safe, user-owned fields to the Memory Lab.""" return MemoryRead( id=memory.id, conversation_id=memory.conversation_id, source_type=memory.source_type, title=memory.title, summary=memory.summary, excerpt=memory.excerpt, location_tag=memory.location_tag, equipment=memory.equipment, confidence=memory.confidence, created_at=memory.created_at, ) def data_uri_to_upload_file(file_dict): from io import BytesIO import base64 import re import os import json from fastapi import HTTPException # 1. Sanitize filename and check extension filename = os.path.basename(file_dict.get('filename', '')) ext = os.path.splitext(filename)[1].lower() allowed_extensions = {".jpg", ".jpeg", ".png", ".pdf", ".json"} if ext not in allowed_extensions: raise HTTPException( status_code=400, detail=f"Format file '{ext}' tidak diizinkan. Hanya menerima .jpg, .jpeg, .png, .pdf, .json" ) match = re.match(r'data:(.*?);base64,(.*)', file_dict['url']) if not match: raise HTTPException(status_code=400, detail="Data URI tidak valid") content_type, b64_data = match.groups() try: file_bytes = base64.b64decode(b64_data) except Exception: raise HTTPException(status_code=400, detail="Gagal mendecode data Base64") # 2. Limit size to MAX_FILE_SIZE (100MB) MAX_SIZE = MAX_FILE_SIZE if len(file_bytes) > MAX_SIZE: raise HTTPException( status_code=400, detail=f"Ukuran file '{filename}' melebihi batas 100MB" ) # 3. Magic bytes validation is_valid = False if ext in [".jpg", ".jpeg"]: is_valid = file_bytes.startswith(b"\xff\xd8\xff") elif ext == ".png": is_valid = file_bytes.startswith(b"\x89PNG\r\n\x1a\n") elif ext == ".pdf": is_valid = file_bytes.startswith(b"%PDF-") elif ext == ".json": try: json.loads(file_bytes.decode("utf-8")) is_valid = True except Exception: pass if not is_valid: raise HTTPException( status_code=400, detail=f"Verifikasi konten file '{filename}' gagal. Format tidak sesuai dengan ekstensi '{ext}'." ) file_obj = BytesIO(file_bytes) return UploadFile( file=file_obj, filename=filename, size=len(file_bytes), headers={"content-type": content_type} ) def get_pre_uploaded_file(conversation_id: str, file_dict: dict) -> UploadFile: import os from io import BytesIO from fastapi import HTTPException url = file_dict.get('url', '') filename = os.path.basename(file_dict.get('filename', '')) # Enforce strict path structure: src/uploads/{conversation_id}/{filename} expected_path = f"src/uploads/{conversation_id}/{filename}" if url != expected_path: raise HTTPException( status_code=400, detail=f"Jalur file '{url}' tidak valid atau tidak cocok dengan sesi percakapan." ) # Check if the file actually exists if not os.path.exists(url): raise HTTPException( status_code=404, detail=f"File '{filename}' tidak ditemukan di server." ) # Check that extension is in allowed extensions ext = os.path.splitext(filename)[1].lower() allowed_extensions = {".jpg", ".jpeg", ".png", ".pdf", ".json"} if ext not in allowed_extensions: raise HTTPException( status_code=400, detail=f"Format file '{ext}' tidak diizinkan." ) # Return UploadFile instance so stream2.py can read from it with open(url, "rb") as f: content = f.read() file_obj = BytesIO(content) uf = UploadFile( file=file_obj, filename=filename, size=len(content), headers={"content-type": file_dict.get("mediaType", "application/octet-stream")} ) uf.url = url return uf # Endpoint to handle available models requests route => /chat/models @router.get("/models", response_model=StandardResponse[List[ModelRead]], summary="Get available models") async def get_available_models(current_user: CurrentUser): models = [ ModelRead( name=m.name, display_name=m.display_name, description=m.description, provider=m.provider ) for m in MODELS_DATA if m.enabled ] return StandardResponse(data=models, message="Models retrieved successfully") # Admin endpoints for LLM models configuration management @router.get("/admin/models", response_model=StandardResponse[List[AdminModelRead]], summary="Get all models config (Admin)") async def get_all_models_admin(db_session: DbSession, current_user: CurrentUser): if current_user.role not in ("Admin", "Super Admin", "Application Administrator"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) db_models = await get_all_models_service(db_session) models = [ AdminModelRead( 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.is_default, generate_cfg=m.generate_cfg ) for m in db_models ] return StandardResponse(data=models, message="All models configuration retrieved successfully") @router.put("/admin/models/{name}/toggle", response_model=StandardResponse[AdminModelRead], summary="Toggle model configuration enabled status (Admin)") async def toggle_model_admin(name: str, enabled: bool, db_session: DbSession, current_user: CurrentUser): if current_user.role not in ("Admin", "Super Admin", "Application Administrator"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) m = await toggle_model_service(db_session, name, enabled, current_user) response_data = AdminModelRead( 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.is_default, generate_cfg=m.generate_cfg ) return StandardResponse(data=response_data, message=f"Model '{name}' enabled status toggled to {enabled} successfully") @router.put("/admin/models/{name}/default", response_model=StandardResponse[AdminModelRead], summary="Set model as default (Admin)") async def set_default_model_admin(name: str, db_session: DbSession, current_user: CurrentUser): if current_user.role not in ("Admin", "Super Admin", "Application Administrator"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) m = await set_default_model_service(db_session, name, current_user) response_data = AdminModelRead( 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.is_default, generate_cfg=m.generate_cfg ) return StandardResponse(data=response_data, message=f"Model '{name}' set as the default model successfully") @router.post("/admin/models", response_model=StandardResponse[AdminModelRead], summary="Create new model configuration (Admin)") async def create_model_admin(data: ModelConfigCreate, db_session: DbSession, current_user: CurrentUser): if current_user.role not in ("Admin", "Super Admin", "Application Administrator"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) m = await create_model_service(db_session, data, current_user) response_data = AdminModelRead( 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.is_default, generate_cfg=m.generate_cfg ) return StandardResponse(data=response_data, message=f"Model config '{m.name}' created successfully") @router.put("/admin/models/{name}", response_model=StandardResponse[AdminModelRead], summary="Update model configuration (Admin)") async def update_model_admin(name: str, data: ModelConfigUpdate, db_session: DbSession, current_user: CurrentUser): if current_user.role not in ("Admin", "Super Admin", "Application Administrator"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) m = await update_model_service(db_session, name, data, current_user) response_data = AdminModelRead( 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.is_default, generate_cfg=m.generate_cfg ) return StandardResponse(data=response_data, message=f"Model config '{name}' updated successfully") @router.delete("/admin/models/{name}", response_model=StandardResponse[dict], summary="Delete model configuration (Admin)") async def delete_model_admin(name: str, db_session: DbSession, current_user: CurrentUser): if current_user.role not in ("Admin", "Super Admin", "Application Administrator"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this resource." ) res = await delete_model_service(db_session, name, current_user) return StandardResponse(data=res, message=f"Model config '{name}' deleted successfully") # class Request(BaseModel): # messages: List[ClientMessage] # Endpoint to handle delete conversation requests route => /chat/delete/{conversation_id} @router.api_route("/delete/{conversation_id}", methods=["GET", "POST"], response_model=StandardResponse[str], summary="Delete conversation") async def remove_conversation( conversation_id: UUID, db_session: DbSession, current_user: CurrentUser ): await delete_conversation(db_session=db_session, conversation_id=str(conversation_id), current_user=current_user) return StandardResponse(data=str(conversation_id), message="Conversation deleted successfully") @router.api_route("/delete/{conversation_id}/{chat_history_id}", methods=["GET", "POST"], response_model=StandardResponse[str], summary="Delete chat history") async def remove_chat_history( conversation_id: UUID, chat_history_id: UUID, db_session: DbSession, current_user: CurrentUser ): chat_history_data = await get_chat_by_id(db_session=db_session, chat_history_id=chat_history_id, conversation_id=conversation_id, current_user=current_user) if not chat_history_data: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="A data with this id does not exist.", ) deleted_chat = await delete_chat_by_id(db_session=db_session, chat_history_id=chat_history_id, conversation_id=conversation_id, current_user=current_user) return StandardResponse(data=str(deleted_chat), message="Chat history deleted successfully") # Endpoint to handle convesations lists requests route => /chat @router.get("", response_model=StandardResponse[ConversationDataPagination]) async def get_conversations( db_session: DbSession, current_user: CurrentUser, common: CommonParameters, search: Optional[str] = Query(None), page: Optional[int] = Query(1), items_per_page: Optional[int] = Query(100), is_exported: Optional[bool] = Query(None), start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), ): conversations = await get_conversation( db_session=db_session, current_user=current_user, search=search, page=page, items_per_page=items_per_page, is_exported=is_exported, start_date=start_date, end_date=end_date, common=common ) return StandardResponse(data=conversations, message="Conversations retrieved successfully") # Analytics endpoint for feedback evaluation dashboard @router.get("/feedback/analytics", summary="Get feedback analytics for evaluation dashboard") async def get_feedback_analytics_route( db_session: DbSession, current_user: CurrentUser, start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), ): result = await get_feedback_analytics( db_session=db_session, current_user=current_user, start_date=start_date, end_date=end_date, ) return StandardResponse(data=result, message="Feedback analytics retrieved successfully") @router.get("/memory-lab", response_model=StandardResponse[MemoryLabSnapshot], summary="Get private Memory Lab records") async def get_memory_lab( db_session: DbSession, current_user: CurrentUser, ): filters = ( RCFAMemory.user_id == current_user.user_id, RCFAMemory.scope == "user", ) memory_result = await db_session.execute( select(RCFAMemory) .where(*filters) .order_by(RCFAMemory.created_at.desc()) .limit(50) ) memories = list(memory_result.scalars()) total = await db_session.scalar(select(func.count(RCFAMemory.id)).where(*filters)) or 0 snapshot = MemoryLabSnapshot( items=[_memory_to_read(memory) for memory in memories], total=total, retrieval_limit=50, ) return StandardResponse(data=snapshot, message="Private memories retrieved successfully") @router.delete("/memory-lab/{memory_id}", response_model=StandardResponse[str], summary="Delete one private Memory Lab record") async def delete_memory_lab_record( memory_id: UUID, db_session: DbSession, current_user: CurrentUser, ): memory = await db_session.scalar( select(RCFAMemory).where( RCFAMemory.id == memory_id, RCFAMemory.user_id == current_user.user_id, RCFAMemory.scope == "user", ) ) if memory is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="This private memory does not exist.", ) await db_session.delete(memory) await db_session.commit() return StandardResponse(data=str(memory_id), message="Private memory deleted successfully") @router.get("/{conversation_id}", response_model=StandardResponse[ChatHistoryResponse], summary="Get all chats") async def get_rag_chats(db_session: DbSession, conversation_id: UUID, current_user: CurrentUser): result = await get_rag_histories_by_conversation_id( db_session=db_session, conversation_id=conversation_id ) chats = [ ChatHistory( id=str(chat.id), user_id=str(current_user.user_id), message=str(chat.message), attachments=chat.attachments if chat.attachments else None, attachments_filepath=chat.attachments_filepath if chat.attachments_filepath else None, response=str(chat.response), context=chat.context if chat.context else None, sources=chat.sources if chat.sources else None, sources_filepath=chat.sources_filepath if chat.sources_filepath else None, timestamp=chat.message_timestamp, response_timestamp=chat.response_timestamp, time_taken=chat.time_taken, prompt_tokens=chat.prompt_tokens, response_tokens=chat.response_tokens, tokens_used=chat.tokens_used, conversation_id=str(chat.conversation_id), chat_model=chat.chat_model, embedding_model=chat.embedding_model, ocr_model=chat.ocr_model, created_at=chat.created_at, feedback=chat.feedback if chat.feedback else None, feedback_comment=chat.feedback_comment if chat.feedback_comment else None, feedback_screenshot=chat.feedback_screenshot if chat.feedback_screenshot else None, last_location_tag=chat.last_location_tag, context_info=chat.context_info if chat.context_info else None, references=(chat.context_info or {}).get("references") if isinstance(chat.context_info, dict) else None, updated_at=chat.updated_at ) for chat in result ] # Fetch conversation metadata for RCFA phase/state conversation = await get_conversation_by_id(db_session=db_session, conversation_id=str(conversation_id), current_user=current_user) return StandardResponse( data={ "messages": chats, "metadata": { "rcfa_phase": conversation.rcfa_phase if conversation else "1. Failure Prediction", "rcfa_state": conversation.rcfa_state if conversation else {}, "title": conversation.conversation_title if conversation else "" } }, message="Chat history retrieved successfully" ) # Endpoint to handle chat requests route => /chat @router.post("") async def chat( db_session: DbSession, current_user: CurrentUser, request: Request, ): data = await request.json() last_msg = data["messages"][-1] if "parts" in last_msg: message = last_msg['parts'][0]['text'] if len(last_msg['parts']) < 2 else last_msg['parts'][1]['text'] files = next((x for x in last_msg['parts'] if x['type'] == 'file'), None) else: message = last_msg.get("content", "") files = None location_tag = data.get('locationTag') or data.get('location_tag') or None flow_type = FlowType(data.get('flow_type', FlowType.ORIGINAL)) equipment_id = UUID(data['equipment_id']) if data.get('equipment_id') else None questions_batch = data.get('questions_batch') parsed_questions_batch = None if flow_type == FlowType.BATCH_SUMMARIZE: if not questions_batch: raise HTTPException( status_code=422, detail="questions_batch parameter is required for batch_summarize flow type" ) if not isinstance(questions_batch, list) or len(questions_batch) != 5: raise HTTPException( status_code=422, detail="questions_batch must be a JSON array of exactly 5 questions" ) parsed_questions_batch = questions_batch upload_file = data_uri_to_upload_file(files) if files else None chat_req = ChatRequest( message=message, conversation_id=None, message_id=data.get('message_id'), attachments=[upload_file] if upload_file else None, equipment_id=equipment_id, flow_type=flow_type, questions_batch=parsed_questions_batch, model=data.get('model'), context_state=data.get('context_state'), enable_background_analysis=data.get('enable_background_analysis', True) ) # try: # response = await create(db_session=db_session, request=request, current_user=current_user) # return StandardResponse(data=response, message="Chat request processed successfully") # except Exception as e: # raise HTTPException(status_code=500, detail=str(e)) headers = { "Content-Type": "text/event-stream", "Content-type": "text/event-stream", "Cache-Control": "no-cache, no-transform", "Connection": "keep-alive", "X-Content-Type-Options": "nosniff", "Content-Encoding": "none" } stream_response = create(db_session=db_session, request=chat_req, current_user=current_user, location_tag=location_tag) return StreamingResponse(stream_response, media_type="text/event-stream", headers=headers) @router.post("/upload/{conversation_id}") async def upload_file( conversation_id: UUID, current_user: CurrentUser, file: UploadFile = File(...), ): import os import re import json from fastapi import HTTPException # 1. Sanitize filename and check extension filename = os.path.basename(file.filename) ext = os.path.splitext(filename)[1].lower() allowed_extensions = {".jpg", ".jpeg", ".png", ".pdf", ".json"} if ext not in allowed_extensions: raise HTTPException( status_code=400, detail=f"Format file '{ext}' tidak diizinkan. Hanya menerima .jpg, .jpeg, .png, .pdf, .json" ) # 2. Limit size to MAX_FILE_SIZE (100MB) try: content = await file.read() except Exception: raise HTTPException(status_code=400, detail="Gagal membaca konten file") if len(content) > MAX_FILE_SIZE: raise HTTPException( status_code=400, detail=f"Ukuran file '{filename}' melebihi batas 100MB" ) # 3. Magic bytes validation is_valid = False if ext in [".jpg", ".jpeg"]: is_valid = content.startswith(b"\xff\xd8\xff") elif ext == ".png": is_valid = content.startswith(b"\x89PNG\r\n\x1a\n") elif ext == ".pdf": is_valid = content.startswith(b"%PDF-") elif ext == ".json": try: json.loads(content.decode("utf-8")) is_valid = True except Exception: pass if not is_valid: raise HTTPException( status_code=400, detail=f"Verifikasi konten file '{filename}' gagal. Format tidak sesuai dengan ekstensi '{ext}'." ) # 4. Save file to src/uploads/{conversation_id}/ upload_dir = f"src/uploads/{conversation_id}" os.makedirs(upload_dir, exist_ok=True) file_path = os.path.join(upload_dir, filename) try: with open(file_path, "wb") as f_out: f_out.write(content) except Exception as e: raise HTTPException( status_code=500, detail=f"Gagal menyimpan file ke disk: {str(e)}" ) return { "url": f"src/uploads/{conversation_id}/{filename}", "name": filename, "contentType": file.content_type or "application/octet-stream" } @router.post("/stream") async def chat_stream(db_session: DbSession, collector_db_session: CollectorDbSession, common: CommonParameters, current_user: CurrentUser, token: Token, request: Request, ): """ Stream chat response from LLM. """ data = await request.json() last_msg = data["messages"][-1] conv_id_str = data.get('conversation_id') or str(uuid.uuid4()) attachments = [] if "parts" in last_msg: text_parts = [p.get("text") for p in last_msg["parts"] if p.get("type") == "text" or "text" in p] message = text_parts[0] if text_parts else last_msg.get("content", "") file_parts = [p for p in last_msg["parts"] if p.get("type") == "file"][:3] for fp in file_parts: uf = get_pre_uploaded_file(conv_id_str, fp) if uf: attachments.append(uf) else: message = last_msg.get("content", "") location_tag = data.get('locationTag') or data.get('location_tag') or None upload_type = data.get('uploadType') or None flow_type = FlowType(data.get('flow_type', FlowType.ORIGINAL)) equipment_id = UUID(data['equipment_id']) if data.get('equipment_id') else None chat_req = ChatRequest( message=message, conversation_id=UUID(conv_id_str) if isinstance(conv_id_str, str) else conv_id_str, message_id=data.get('message_id'), attachments=attachments if attachments else None, equipment_id=equipment_id, flow_type=flow_type, model=data.get('model'), context_state=data.get('context_state'), enable_background_analysis=data.get('enable_background_analysis', True) ) streaming = stream_create_v2( db_session=db_session, collector_db_session=collector_db_session, request=chat_req, upload_type=upload_type, location_tag=location_tag, current_user=current_user, token=token, common=common ) # if any(k.lower() in message.lower() for k in searchRAGKeywords): # intent_result = await ClassifierConfig.chain.ainvoke({"question": request.message}) # # Only create a new conversation_id if not present in request # new_conversation_id = uuid.uuid4() # conversation_title = ( # request.message[:50] + "..." if len(request.message) > 50 else request.message # ) # conversation = ConversationModel( # user_id=current_user.user_id, # conversation_id=new_conversation_id, # conversation_title=conversation_title, # created_by=current_user.name, # updated_by=current_user.name # ) # db_session.add(conversation) # await db_session.commit() # print("Detected RAG keyword, switching to RAG flow") # async def rag_stream(): # async for chunk in rag_create( # db_session=db_session, # collector_db_session=collector_db_session, # request=request, # conversation_id=new_conversation_id, # current_user=current_user, # intent_result=intent_result # ): # yield chunk # streaming = rag_stream() # else: # # streaming = stream_create( # # db_session=db_session, # # collector_db_session=collector_db_session, # # request=request, # # location_tag=location_tag, # # current_user=current_user, # # token=token, # # common=common # # ) # streaming = stream_create_v2( # db_session=db_session, # collector_db_session=collector_db_session, # request=request, # location_tag=location_tag, # current_user=current_user, # token=token, # common=common # ) headers = { "Content-Type": "text/plain", "Content-type": "text/plain", "Cache-Control": "no-cache, no-transform", "Connection": "keep-alive", "X-Content-Type-Options": "nosniff", "Content-Encoding": "none" } stream_response = StreamingResponse(streaming, media_type="text/plain", headers=headers) return stream_response @router.post("/stream/{conversation_id}") async def chat_stream_conversation(db_session: DbSession, collector_db_session: CollectorDbSession, common: CommonParameters, current_user: CurrentUser, token: Token, request: Request, conversation_id: UUID, ): """ Stream chat response from LLM. """ data = await request.json() last_msg = data["messages"][-1] attachments = [] if "parts" in last_msg: text_parts = [p.get("text") for p in last_msg["parts"] if p.get("type") == "text" or "text" in p] message = text_parts[0] if text_parts else last_msg.get("content", "") file_parts = [p for p in last_msg["parts"] if p.get("type") == "file"][:3] for fp in file_parts: uf = get_pre_uploaded_file(str(conversation_id), fp) if uf: attachments.append(uf) else: message = last_msg.get("content", "") location_tag = data.get('locationTag') or data.get('location_tag') or None upload_type = data.get('uploadType') or None flow_type = FlowType(data.get('flow_type', FlowType.ORIGINAL)) equipment_id = UUID(data['equipment_id']) if data.get('equipment_id') else None chat_req = ChatRequest( message=message, conversation_id=conversation_id, message_id=data.get('message_id'), attachments=attachments if attachments else None, equipment_id=equipment_id, flow_type=flow_type, model=data.get('model'), context_state=data.get('context_state'), enable_background_analysis=data.get('enable_background_analysis', True) ) streaming = stream_create_v2( db_session=db_session, collector_db_session=collector_db_session, request=chat_req, upload_type=upload_type, location_tag=location_tag, current_user=current_user, token=token, common=common ) # if any(k.lower() in message.lower() for k in searchRAGKeywords): # intent_result = await ClassifierConfig.chain.ainvoke({"question": request.message}) # # Only create a new conversation_id if not present in request # if conversation_id is None: # new_conversation_id = uuid.uuid4() # conversation_title = ( # request.message[:50] + "..." if len(request.message) > 50 else request.message # ) # conversation = ConversationModel( # user_id=current_user.user_id, # conversation_id=new_conversation_id, # conversation_title=conversation_title, # created_by=current_user.name, # updated_by=current_user.name # ) # db_session.add(conversation) # await db_session.commit() # print("Detected RAG keyword, switching to RAG flow") # async def rag_stream(): # async for chunk in rag_create( # db_session=db_session, # collector_db_session=collector_db_session, # request=request, # conversation_id=conversation_id, # current_user=current_user, # intent_result=intent_result # ): # yield chunk # streaming = rag_stream() # else: # streaming = stream_create_v2( # db_session=db_session, # collector_db_session=collector_db_session, # request=request, # location_tag=location_tag, # current_user=current_user, # token=token, # common=common # ) headers = { "Content-Type": "text/event-stream", "Content-type": "text/event-stream", "Cache-Control": "no-cache, no-transform", "Connection": "keep-alive", "X-Content-Type-Options": "nosniff", "Content-Encoding": "none" } stream_response = StreamingResponse(streaming, media_type="text/event-stream", headers=headers) return stream_response @router.post("/root-cause", response_model=StandardResponse[RootCauseResponse], summary="Search root cause from SPLN") async def search_root_cause( request: RootCauseRequest, current_user: CurrentUser ): try: tool = RootCauseSPLNTool() params = { "equipment_type": request.equipment_type, "problem": request.problem } result_str = tool.call(params) # Check if result_str is a plain error message or JSON if result_str.strip().startswith("{") and result_str.strip().endswith("}"): result_data = json_module.loads(result_str) return StandardResponse(data=result_data, message="Root cause mapping retrieved successfully") else: # It's an error message string raise HTTPException(status_code=400, detail=result_str) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(e)}") # Endpoint to handle continue chat conversations requests route => /chat/{conversation_id} @router.post("/{conversation_id}", response_model=StandardResponse[ChatResponse], summary="Continue conversation") async def continue_chat( db_session: DbSession, current_user: CurrentUser, conversation_id: UUID, request: Request, ): """ Process a chat request with RAG (Retrieval-Augmented Generation) for a specific conversation. This endpoint handles the chat request with different processing flows: - original: Standard RAG flow - llm_first: LLM processes question first, then uses retrieval - batch_summarize: Processes 5 questions in batch (requires questions_batch parameter) """ # Parse questions_batch if provided for batch_summarize flow data = await request.json() last_msg = data["messages"][-1] attachments = [] if "parts" in last_msg: text_parts = [p.get("text") for p in last_msg["parts"] if p.get("type") == "text" or "text" in p] message = text_parts[0] if text_parts else last_msg.get("content", "") file_parts = [p for p in last_msg["parts"] if p.get("type") == "file"][:3] for fp in file_parts: uf = data_uri_to_upload_file(fp) if uf: attachments.append(uf) else: message = last_msg.get("content", "") location_tag = data.get('locationTag') or data.get('location_tag') or None flow_type = FlowType(data.get('flow_type', FlowType.ORIGINAL)) equipment_id = UUID(data['equipment_id']) if data.get('equipment_id') else None questions_batch = data.get('questions_batch') parsed_questions_batch = None if flow_type == FlowType.BATCH_SUMMARIZE: if not questions_batch: raise HTTPException( status_code=422, detail="questions_batch parameter is required for batch_summarize flow type" ) if not isinstance(questions_batch, list) or len(questions_batch) != 5: raise HTTPException( status_code=422, detail="questions_batch must be a JSON array of exactly 5 questions" ) parsed_questions_batch = questions_batch chat_req = ChatRequest( message=message, conversation_id=conversation_id, message_id=data.get('message_id'), attachments=attachments if attachments else None, equipment_id=equipment_id, flow_type=flow_type, questions_batch=parsed_questions_batch, model=data.get('model'), context_state=data.get('context_state'), enable_background_analysis=data.get('enable_background_analysis', True) ) # try: # response = await create(db_session=db_session, request=request, current_user=current_user) # return StandardResponse(data=response, message="Chat request processed successfully") # except Exception as e: # raise HTTPException(status_code=500, detail=str(e)) streaming = stream_create_v2( db_session=db_session, collector_db_session=collector_db_session, request=chat_req, upload_type=upload_type, location_tag=location_tag, current_user=current_user, token=token, common=common ) return StreamingResponse(streaming, media_type="text/event-stream") @router.put("/{conversation_id}", response_model=StandardResponse[Conversation]) async def update_conversation( db_session: DbSession, conversation_id: str, update: ConversationUpdate, current_user: CurrentUser, ): conversation = await get_conversation_by_id(db_session=db_session, conversation_id=conversation_id, current_user=current_user) new_title = update.conversation_title if not conversation: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="A data with this id does not exist.", ) updated_title = await update_conversation_title( db_session=db_session, conversation_id=conversation_id, title=new_title, current_user=current_user ) return StandardResponse( data=updated_title, message="Data updated successfully", ) @router.put("/export/{conversation_id}", response_model=StandardResponse[Conversation]) async def mark_conversation_exported( db_session: DbSession, conversation_id: str, update: ConversationExportUpdate, current_user: CurrentUser, ): updated_conversation = await update_conversation_export_status( db_session=db_session, conversation_id=conversation_id, update_data=update, current_user=current_user ) return StandardResponse( data=updated_conversation, message="Conversation marked as exported" ) @router.put("/feedback/{conversation_id}/{chat_history_id}", response_model=StandardResponse[ChatHistory]) async def update_feedback( db_session: DbSession, conversation_id: UUID, chat_history_id: UUID, current_user: CurrentUser, update: UpdateFeedback, ): chat_history_data = await get_chat_by_id(db_session=db_session, chat_history_id=chat_history_id, conversation_id=conversation_id, current_user=current_user) if not chat_history_data: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="A data with this id does not exist.", ) response = await update_chat_history_feedback( db_session=db_session, chat_history_id=chat_history_id, feedback=update.update, current_user=current_user, comment=update.comment, screenshot=update.screenshot, ) return StandardResponse( data=response, message="Feedback updated successfully" )