You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1032 lines
40 KiB
Python

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"
)