Spaces:
Sleeping
Sleeping
backend changes, added new features
Browse files- app/api/v1/chat.py +664 -664
- app/api/v1/conversation_routes.py +535 -0
- app/db/repositories/conversation_repository.py +1075 -542
- app/main.py +335 -20
- app/models/conversation.py +273 -0
- app/services/conversation_service.py +503 -0
app/api/v1/chat.py
CHANGED
|
@@ -1,391 +1,3 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Chat API Endpoints (WITH AUTHENTICATION)
|
| 3 |
-
RESTful API for the Banking RAG Chatbot
|
| 4 |
-
|
| 5 |
-
NOW REQUIRES JWT TOKEN FOR ALL ENDPOINTS!
|
| 6 |
-
|
| 7 |
-
Endpoints:
|
| 8 |
-
- POST /chat - Send a message and get response (PROTECTED)
|
| 9 |
-
- GET /chat/history/{conversation_id} - Get conversation history (PROTECTED)
|
| 10 |
-
- POST /chat/conversation - Create new conversation (PROTECTED)
|
| 11 |
-
- GET /chat/conversations - List user's conversations (PROTECTED)
|
| 12 |
-
- DELETE /chat/conversation/{conversation_id} - Delete conversation (PROTECTED)
|
| 13 |
-
- GET /chat/health - Health check (PUBLIC)
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from fastapi import APIRouter, HTTPException, status, Depends
|
| 17 |
-
from pydantic import BaseModel, Field
|
| 18 |
-
from typing import List, Dict, Optional
|
| 19 |
-
from datetime import datetime
|
| 20 |
-
|
| 21 |
-
from app.services.chat_service import chat_service
|
| 22 |
-
from app.db.repositories.conversation_repository import ConversationRepository
|
| 23 |
-
from app.utils.dependencies import get_current_user # AUTH DEPENDENCY
|
| 24 |
-
from app.models.user import TokenData # USER DATA FROM TOKEN
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
# ============================================================================
|
| 28 |
-
# CREATE ROUTER
|
| 29 |
-
# ============================================================================
|
| 30 |
-
router = APIRouter()
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
# ============================================================================
|
| 34 |
-
# DEPENDENCY: Get ConversationRepository instance
|
| 35 |
-
# ============================================================================
|
| 36 |
-
def get_conversation_repo() -> ConversationRepository:
|
| 37 |
-
"""
|
| 38 |
-
Dependency that provides ConversationRepository instance.
|
| 39 |
-
This ensures MongoDB is connected before repository is used.
|
| 40 |
-
"""
|
| 41 |
-
return ConversationRepository()
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# ============================================================================
|
| 45 |
-
# PYDANTIC MODELS (Request/Response schemas)
|
| 46 |
-
# ============================================================================
|
| 47 |
-
|
| 48 |
-
class ChatRequest(BaseModel):
|
| 49 |
-
"""Request model for chat endpoint"""
|
| 50 |
-
query: str = Field(..., description="User query text", min_length=1, max_length=1000)
|
| 51 |
-
conversation_id: Optional[str] = Field(None, description="Optional conversation ID")
|
| 52 |
-
|
| 53 |
-
class Config:
|
| 54 |
-
json_schema_extra = {
|
| 55 |
-
"example": {
|
| 56 |
-
"query": "What is my account balance?",
|
| 57 |
-
"conversation_id": "conv-123"
|
| 58 |
-
}
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
class ChatResponse(BaseModel):
|
| 63 |
-
"""Response model for chat endpoint"""
|
| 64 |
-
response: str = Field(..., description="Generated response text")
|
| 65 |
-
conversation_id: str = Field(..., description="Conversation ID")
|
| 66 |
-
policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH")
|
| 67 |
-
policy_confidence: float = Field(..., description="Policy confidence score (0-1)")
|
| 68 |
-
documents_retrieved: int = Field(..., description="Number of documents retrieved")
|
| 69 |
-
top_doc_score: Optional[float] = Field(None, description="Best document similarity score")
|
| 70 |
-
total_time_ms: float = Field(..., description="Total processing time in milliseconds")
|
| 71 |
-
timestamp: str = Field(..., description="Response timestamp (ISO format)")
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
class ConversationCreateResponse(BaseModel):
|
| 75 |
-
"""Response after creating a conversation"""
|
| 76 |
-
conversation_id: str = Field(..., description="Created conversation ID")
|
| 77 |
-
created_at: str = Field(..., description="Creation timestamp")
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
class MessageModel(BaseModel):
|
| 81 |
-
"""Single message in conversation history"""
|
| 82 |
-
role: str = Field(..., description="Message role: user or assistant")
|
| 83 |
-
content: str = Field(..., description="Message content")
|
| 84 |
-
timestamp: str = Field(..., description="Message timestamp")
|
| 85 |
-
metadata: Optional[Dict] = Field(None, description="Optional metadata")
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
class ConversationHistoryResponse(BaseModel):
|
| 89 |
-
"""Response containing conversation history"""
|
| 90 |
-
conversation_id: str
|
| 91 |
-
messages: List[MessageModel]
|
| 92 |
-
message_count: int
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
# ============================================================================
|
| 96 |
-
# ENDPOINTS (ALL PROTECTED WITH JWT)
|
| 97 |
-
# ============================================================================
|
| 98 |
-
|
| 99 |
-
@router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 100 |
-
async def chat(
|
| 101 |
-
request: ChatRequest,
|
| 102 |
-
current_user: TokenData = Depends(get_current_user),
|
| 103 |
-
repo: ConversationRepository = Depends(get_conversation_repo) # ← INJECT REPO
|
| 104 |
-
):
|
| 105 |
-
"""
|
| 106 |
-
Main chat endpoint - Send a query and get a response.
|
| 107 |
-
|
| 108 |
-
**REQUIRES AUTHENTICATION** - JWT token must be provided in Authorization header.
|
| 109 |
-
"""
|
| 110 |
-
try:
|
| 111 |
-
# Get user_id from token
|
| 112 |
-
user_id = current_user.user_id
|
| 113 |
-
|
| 114 |
-
# If no conversation_id provided, create a new conversation
|
| 115 |
-
conversation_id = request.conversation_id
|
| 116 |
-
if not conversation_id:
|
| 117 |
-
conversation_id = await repo.create_conversation(user_id=user_id)
|
| 118 |
-
else:
|
| 119 |
-
# Verify user owns this conversation
|
| 120 |
-
conversation = await repo.get_conversation(conversation_id)
|
| 121 |
-
if not conversation:
|
| 122 |
-
raise HTTPException(
|
| 123 |
-
status_code=status.HTTP_404_NOT_FOUND,
|
| 124 |
-
detail="Conversation not found"
|
| 125 |
-
)
|
| 126 |
-
if conversation["user_id"] != user_id:
|
| 127 |
-
raise HTTPException(
|
| 128 |
-
status_code=status.HTTP_403_FORBIDDEN,
|
| 129 |
-
detail="Access denied - you don't own this conversation"
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
# Get conversation history
|
| 133 |
-
history = await repo.get_conversation_history(
|
| 134 |
-
conversation_id=conversation_id,
|
| 135 |
-
max_messages=10
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
# Save user message
|
| 139 |
-
await repo.add_message(
|
| 140 |
-
conversation_id=conversation_id,
|
| 141 |
-
message={
|
| 142 |
-
'role': 'user',
|
| 143 |
-
'content': request.query,
|
| 144 |
-
'timestamp': datetime.now()
|
| 145 |
-
}
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
# Process query through RAG pipeline
|
| 149 |
-
result = await chat_service.process_query(
|
| 150 |
-
query=request.query,
|
| 151 |
-
conversation_history=history,
|
| 152 |
-
user_id=user_id
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
# Save assistant message
|
| 156 |
-
await repo.add_message(
|
| 157 |
-
conversation_id=conversation_id,
|
| 158 |
-
message={
|
| 159 |
-
'role': 'assistant',
|
| 160 |
-
'content': result['response'],
|
| 161 |
-
'timestamp': datetime.now(),
|
| 162 |
-
'metadata': {
|
| 163 |
-
'policy_action': result['policy_action'],
|
| 164 |
-
'policy_confidence': result['policy_confidence'],
|
| 165 |
-
'documents_retrieved': result['documents_retrieved'],
|
| 166 |
-
'top_doc_score': result['top_doc_score']
|
| 167 |
-
}
|
| 168 |
-
}
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
# Log retrieval data for RL training
|
| 172 |
-
await repo.log_retrieval({
|
| 173 |
-
'conversation_id': conversation_id,
|
| 174 |
-
'user_id': user_id,
|
| 175 |
-
'query': request.query,
|
| 176 |
-
'policy_action': result['policy_action'],
|
| 177 |
-
'policy_confidence': result['policy_confidence'],
|
| 178 |
-
'should_retrieve': result['should_retrieve'],
|
| 179 |
-
'documents_retrieved': result['documents_retrieved'],
|
| 180 |
-
'top_doc_score': result['top_doc_score'],
|
| 181 |
-
'response': result['response'],
|
| 182 |
-
'retrieval_time_ms': result['retrieval_time_ms'],
|
| 183 |
-
'generation_time_ms': result['generation_time_ms'],
|
| 184 |
-
'total_time_ms': result['total_time_ms'],
|
| 185 |
-
'retrieved_docs_metadata': result.get('retrieved_docs_metadata', []),
|
| 186 |
-
'timestamp': datetime.now()
|
| 187 |
-
})
|
| 188 |
-
|
| 189 |
-
# Return response
|
| 190 |
-
return ChatResponse(
|
| 191 |
-
response=result['response'],
|
| 192 |
-
conversation_id=conversation_id,
|
| 193 |
-
policy_action=result['policy_action'],
|
| 194 |
-
policy_confidence=result['policy_confidence'],
|
| 195 |
-
documents_retrieved=result['documents_retrieved'],
|
| 196 |
-
top_doc_score=result['top_doc_score'],
|
| 197 |
-
total_time_ms=result['total_time_ms'],
|
| 198 |
-
timestamp=result['timestamp']
|
| 199 |
-
)
|
| 200 |
-
|
| 201 |
-
except HTTPException:
|
| 202 |
-
raise
|
| 203 |
-
except Exception as e:
|
| 204 |
-
print(f"❌ Chat endpoint error: {e}")
|
| 205 |
-
import traceback
|
| 206 |
-
traceback.print_exc()
|
| 207 |
-
raise HTTPException(
|
| 208 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 209 |
-
detail=f"Failed to process chat request: {str(e)}"
|
| 210 |
-
)
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
@router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED)
|
| 214 |
-
async def create_conversation(
|
| 215 |
-
current_user: TokenData = Depends(get_current_user),
|
| 216 |
-
repo: ConversationRepository = Depends(get_conversation_repo)
|
| 217 |
-
):
|
| 218 |
-
"""Create a new conversation"""
|
| 219 |
-
try:
|
| 220 |
-
conversation_id = await repo.create_conversation(user_id=current_user.user_id)
|
| 221 |
-
return ConversationCreateResponse(
|
| 222 |
-
conversation_id=conversation_id,
|
| 223 |
-
created_at=datetime.now().isoformat()
|
| 224 |
-
)
|
| 225 |
-
except Exception as e:
|
| 226 |
-
raise HTTPException(
|
| 227 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 228 |
-
detail=f"Failed to create conversation: {str(e)}"
|
| 229 |
-
)
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
@router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse)
|
| 233 |
-
async def get_conversation_history(
|
| 234 |
-
conversation_id: str,
|
| 235 |
-
current_user: TokenData = Depends(get_current_user),
|
| 236 |
-
repo: ConversationRepository = Depends(get_conversation_repo)
|
| 237 |
-
):
|
| 238 |
-
"""Get conversation history by ID"""
|
| 239 |
-
try:
|
| 240 |
-
conversation = await repo.get_conversation(conversation_id)
|
| 241 |
-
|
| 242 |
-
if not conversation:
|
| 243 |
-
raise HTTPException(
|
| 244 |
-
status_code=status.HTTP_404_NOT_FOUND,
|
| 245 |
-
detail=f"Conversation {conversation_id} not found"
|
| 246 |
-
)
|
| 247 |
-
|
| 248 |
-
if conversation["user_id"] != current_user.user_id:
|
| 249 |
-
raise HTTPException(
|
| 250 |
-
status_code=status.HTTP_403_FORBIDDEN,
|
| 251 |
-
detail="Access denied - you don't own this conversation"
|
| 252 |
-
)
|
| 253 |
-
|
| 254 |
-
messages = []
|
| 255 |
-
for msg in conversation.get('messages', []):
|
| 256 |
-
messages.append(MessageModel(
|
| 257 |
-
role=msg['role'],
|
| 258 |
-
content=msg['content'],
|
| 259 |
-
timestamp=msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'],
|
| 260 |
-
metadata=msg.get('metadata')
|
| 261 |
-
))
|
| 262 |
-
|
| 263 |
-
return ConversationHistoryResponse(
|
| 264 |
-
conversation_id=conversation_id,
|
| 265 |
-
messages=messages,
|
| 266 |
-
message_count=len(messages)
|
| 267 |
-
)
|
| 268 |
-
|
| 269 |
-
except HTTPException:
|
| 270 |
-
raise
|
| 271 |
-
except Exception as e:
|
| 272 |
-
raise HTTPException(
|
| 273 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 274 |
-
detail=f"Failed to fetch conversation history: {str(e)}"
|
| 275 |
-
)
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
@router.get("/conversations")
|
| 279 |
-
async def list_user_conversations(
|
| 280 |
-
limit: int = 10,
|
| 281 |
-
skip: int = 0,
|
| 282 |
-
current_user: TokenData = Depends(get_current_user),
|
| 283 |
-
repo: ConversationRepository = Depends(get_conversation_repo)
|
| 284 |
-
):
|
| 285 |
-
"""List all conversations for the authenticated user"""
|
| 286 |
-
try:
|
| 287 |
-
conversations = await repo.get_user_conversations(
|
| 288 |
-
user_id=current_user.user_id,
|
| 289 |
-
limit=limit,
|
| 290 |
-
skip=skip
|
| 291 |
-
)
|
| 292 |
-
|
| 293 |
-
return {
|
| 294 |
-
"user_id": current_user.user_id,
|
| 295 |
-
"user_email": current_user.email,
|
| 296 |
-
"conversations": [
|
| 297 |
-
{
|
| 298 |
-
"conversation_id": conv['conversation_id'],
|
| 299 |
-
"created_at": conv['created_at'].isoformat() if isinstance(conv['created_at'], datetime) else conv['created_at'],
|
| 300 |
-
"updated_at": conv['updated_at'].isoformat() if isinstance(conv['updated_at'], datetime) else conv['updated_at'],
|
| 301 |
-
"message_count": len(conv.get('messages', []))
|
| 302 |
-
}
|
| 303 |
-
for conv in conversations
|
| 304 |
-
],
|
| 305 |
-
"total": len(conversations)
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
except Exception as e:
|
| 309 |
-
raise HTTPException(
|
| 310 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 311 |
-
detail=f"Failed to fetch conversations: {str(e)}"
|
| 312 |
-
)
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
@router.delete("/conversation/{conversation_id}")
|
| 316 |
-
async def delete_conversation(
|
| 317 |
-
conversation_id: str,
|
| 318 |
-
current_user: TokenData = Depends(get_current_user),
|
| 319 |
-
repo: ConversationRepository = Depends(get_conversation_repo)
|
| 320 |
-
):
|
| 321 |
-
"""Delete a conversation"""
|
| 322 |
-
try:
|
| 323 |
-
conversation = await repo.get_conversation(conversation_id)
|
| 324 |
-
|
| 325 |
-
if not conversation:
|
| 326 |
-
raise HTTPException(
|
| 327 |
-
status_code=status.HTTP_404_NOT_FOUND,
|
| 328 |
-
detail=f"Conversation {conversation_id} not found"
|
| 329 |
-
)
|
| 330 |
-
|
| 331 |
-
if conversation["user_id"] != current_user.user_id:
|
| 332 |
-
raise HTTPException(
|
| 333 |
-
status_code=status.HTTP_403_FORBIDDEN,
|
| 334 |
-
detail="Access denied - you don't own this conversation"
|
| 335 |
-
)
|
| 336 |
-
|
| 337 |
-
success = await repo.delete_conversation(conversation_id)
|
| 338 |
-
|
| 339 |
-
if success:
|
| 340 |
-
return {
|
| 341 |
-
"message": "Conversation deleted successfully",
|
| 342 |
-
"conversation_id": conversation_id
|
| 343 |
-
}
|
| 344 |
-
else:
|
| 345 |
-
raise HTTPException(
|
| 346 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 347 |
-
detail="Failed to delete conversation"
|
| 348 |
-
)
|
| 349 |
-
|
| 350 |
-
except HTTPException:
|
| 351 |
-
raise
|
| 352 |
-
except Exception as e:
|
| 353 |
-
raise HTTPException(
|
| 354 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 355 |
-
detail=f"Failed to delete conversation: {str(e)}"
|
| 356 |
-
)
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
@router.get("/health")
|
| 360 |
-
async def chat_health():
|
| 361 |
-
"""Health check for chat service (PUBLIC)"""
|
| 362 |
-
try:
|
| 363 |
-
health = await chat_service.health_check()
|
| 364 |
-
|
| 365 |
-
return {
|
| 366 |
-
"status": "healthy",
|
| 367 |
-
"service": "chat",
|
| 368 |
-
"components": health['components'],
|
| 369 |
-
"timestamp": datetime.now().isoformat()
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
-
except Exception as e:
|
| 373 |
-
return {
|
| 374 |
-
"status": "unhealthy",
|
| 375 |
-
"service": "chat",
|
| 376 |
-
"error": str(e),
|
| 377 |
-
"timestamp": datetime.now().isoformat()
|
| 378 |
-
}
|
| 379 |
-
# ============================================================================
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
# """
|
| 390 |
# Chat API Endpoints (WITH AUTHENTICATION)
|
| 391 |
# RESTful API for the Banking RAG Chatbot
|
|
@@ -417,8 +29,16 @@ async def chat_health():
|
|
| 417 |
# # ============================================================================
|
| 418 |
# router = APIRouter()
|
| 419 |
|
| 420 |
-
|
| 421 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
|
| 424 |
# # ============================================================================
|
|
@@ -426,17 +46,7 @@ async def chat_health():
|
|
| 426 |
# # ============================================================================
|
| 427 |
|
| 428 |
# class ChatRequest(BaseModel):
|
| 429 |
-
# """
|
| 430 |
-
# Request model for chat endpoint.
|
| 431 |
-
|
| 432 |
-
# NOTE: user_id is now extracted from JWT token, not from request body!
|
| 433 |
-
|
| 434 |
-
# Example:
|
| 435 |
-
# {
|
| 436 |
-
# "query": "What is my account balance?",
|
| 437 |
-
# "conversation_id": "abc-123"
|
| 438 |
-
# }
|
| 439 |
-
# """
|
| 440 |
# query: str = Field(..., description="User query text", min_length=1, max_length=1000)
|
| 441 |
# conversation_id: Optional[str] = Field(None, description="Optional conversation ID")
|
| 442 |
|
|
@@ -450,11 +60,7 @@ async def chat_health():
|
|
| 450 |
|
| 451 |
|
| 452 |
# class ChatResponse(BaseModel):
|
| 453 |
-
# """
|
| 454 |
-
# Response model for chat endpoint.
|
| 455 |
-
|
| 456 |
-
# Contains the generated response plus metadata about the RAG pipeline.
|
| 457 |
-
# """
|
| 458 |
# response: str = Field(..., description="Generated response text")
|
| 459 |
# conversation_id: str = Field(..., description="Conversation ID")
|
| 460 |
# policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH")
|
|
@@ -465,11 +71,6 @@ async def chat_health():
|
|
| 465 |
# timestamp: str = Field(..., description="Response timestamp (ISO format)")
|
| 466 |
|
| 467 |
|
| 468 |
-
# class ConversationCreateRequest(BaseModel):
|
| 469 |
-
# """Request to create a new conversation (no user_id needed - from token)"""
|
| 470 |
-
# pass # Empty - user_id comes from JWT token
|
| 471 |
-
|
| 472 |
-
|
| 473 |
# class ConversationCreateResponse(BaseModel):
|
| 474 |
# """Response after creating a conversation"""
|
| 475 |
# conversation_id: str = Field(..., description="Created conversation ID")
|
|
@@ -498,43 +99,25 @@ async def chat_health():
|
|
| 498 |
# @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 499 |
# async def chat(
|
| 500 |
# request: ChatRequest,
|
| 501 |
-
# current_user: TokenData = Depends(get_current_user)
|
|
|
|
| 502 |
# ):
|
| 503 |
# """
|
| 504 |
# Main chat endpoint - Send a query and get a response.
|
| 505 |
|
| 506 |
# **REQUIRES AUTHENTICATION** - JWT token must be provided in Authorization header.
|
| 507 |
-
|
| 508 |
-
# This endpoint:
|
| 509 |
-
# 1. Extracts user_id from JWT token
|
| 510 |
-
# 2. Processes the query through the RAG pipeline
|
| 511 |
-
# 3. Saves messages to MongoDB
|
| 512 |
-
# 4. Logs retrieval data for RL training
|
| 513 |
-
# 5. Returns response with metadata
|
| 514 |
-
|
| 515 |
-
# Args:
|
| 516 |
-
# request: ChatRequest with query and optional conversation_id
|
| 517 |
-
# current_user: Authenticated user data from JWT token
|
| 518 |
-
|
| 519 |
-
# Returns:
|
| 520 |
-
# ChatResponse: Generated response with metadata
|
| 521 |
-
|
| 522 |
-
# Raises:
|
| 523 |
-
# HTTPException: If processing fails or user not authenticated
|
| 524 |
# """
|
| 525 |
# try:
|
| 526 |
-
# # Get user_id from token
|
| 527 |
# user_id = current_user.user_id
|
| 528 |
|
| 529 |
# # If no conversation_id provided, create a new conversation
|
| 530 |
# conversation_id = request.conversation_id
|
| 531 |
# if not conversation_id:
|
| 532 |
-
# conversation_id = await
|
| 533 |
-
# user_id=user_id
|
| 534 |
-
# )
|
| 535 |
# else:
|
| 536 |
# # Verify user owns this conversation
|
| 537 |
-
# conversation = await
|
| 538 |
# if not conversation:
|
| 539 |
# raise HTTPException(
|
| 540 |
# status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -547,13 +130,13 @@ async def chat_health():
|
|
| 547 |
# )
|
| 548 |
|
| 549 |
# # Get conversation history
|
| 550 |
-
# history = await
|
| 551 |
# conversation_id=conversation_id,
|
| 552 |
-
# max_messages=10
|
| 553 |
# )
|
| 554 |
|
| 555 |
-
# # Save user message
|
| 556 |
-
# await
|
| 557 |
# conversation_id=conversation_id,
|
| 558 |
# message={
|
| 559 |
# 'role': 'user',
|
|
@@ -569,8 +152,8 @@ async def chat_health():
|
|
| 569 |
# user_id=user_id
|
| 570 |
# )
|
| 571 |
|
| 572 |
-
# # Save assistant message
|
| 573 |
-
# await
|
| 574 |
# conversation_id=conversation_id,
|
| 575 |
# message={
|
| 576 |
# 'role': 'assistant',
|
|
@@ -586,7 +169,7 @@ async def chat_health():
|
|
| 586 |
# )
|
| 587 |
|
| 588 |
# # Log retrieval data for RL training
|
| 589 |
-
# await
|
| 590 |
# 'conversation_id': conversation_id,
|
| 591 |
# 'user_id': user_id,
|
| 592 |
# 'query': request.query,
|
|
@@ -616,9 +199,11 @@ async def chat_health():
|
|
| 616 |
# )
|
| 617 |
|
| 618 |
# except HTTPException:
|
| 619 |
-
# raise
|
| 620 |
# except Exception as e:
|
| 621 |
# print(f"❌ Chat endpoint error: {e}")
|
|
|
|
|
|
|
| 622 |
# raise HTTPException(
|
| 623 |
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 624 |
# detail=f"Failed to process chat request: {str(e)}"
|
|
@@ -627,29 +212,16 @@ async def chat_health():
|
|
| 627 |
|
| 628 |
# @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED)
|
| 629 |
# async def create_conversation(
|
| 630 |
-
# current_user: TokenData = Depends(get_current_user)
|
|
|
|
| 631 |
# ):
|
| 632 |
-
# """
|
| 633 |
-
# Create a new conversation.
|
| 634 |
-
|
| 635 |
-
# **REQUIRES AUTHENTICATION** - User ID is extracted from JWT token.
|
| 636 |
-
|
| 637 |
-
# Args:
|
| 638 |
-
# current_user: Authenticated user data from JWT token
|
| 639 |
-
|
| 640 |
-
# Returns:
|
| 641 |
-
# ConversationCreateResponse: Created conversation ID
|
| 642 |
-
# """
|
| 643 |
# try:
|
| 644 |
-
# conversation_id = await
|
| 645 |
-
# user_id=current_user.user_id
|
| 646 |
-
# )
|
| 647 |
-
|
| 648 |
# return ConversationCreateResponse(
|
| 649 |
# conversation_id=conversation_id,
|
| 650 |
# created_at=datetime.now().isoformat()
|
| 651 |
# )
|
| 652 |
-
|
| 653 |
# except Exception as e:
|
| 654 |
# raise HTTPException(
|
| 655 |
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
@@ -660,26 +232,12 @@ async def chat_health():
|
|
| 660 |
# @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse)
|
| 661 |
# async def get_conversation_history(
|
| 662 |
# conversation_id: str,
|
| 663 |
-
# current_user: TokenData = Depends(get_current_user)
|
|
|
|
| 664 |
# ):
|
| 665 |
-
# """
|
| 666 |
-
# Get conversation history by ID.
|
| 667 |
-
|
| 668 |
-
# **REQUIRES AUTHENTICATION** - User can only access their own conversations.
|
| 669 |
-
|
| 670 |
-
# Args:
|
| 671 |
-
# conversation_id: Conversation ID
|
| 672 |
-
# current_user: Authenticated user data from JWT token
|
| 673 |
-
|
| 674 |
-
# Returns:
|
| 675 |
-
# ConversationHistoryResponse: List of messages
|
| 676 |
-
|
| 677 |
-
# Raises:
|
| 678 |
-
# HTTPException: If conversation not found or user doesn't own it
|
| 679 |
-
# """
|
| 680 |
# try:
|
| 681 |
-
#
|
| 682 |
-
# conversation = await conversation_repo.get_conversation(conversation_id)
|
| 683 |
|
| 684 |
# if not conversation:
|
| 685 |
# raise HTTPException(
|
|
@@ -687,14 +245,12 @@ async def chat_health():
|
|
| 687 |
# detail=f"Conversation {conversation_id} not found"
|
| 688 |
# )
|
| 689 |
|
| 690 |
-
# # Verify user owns this conversation
|
| 691 |
# if conversation["user_id"] != current_user.user_id:
|
| 692 |
# raise HTTPException(
|
| 693 |
# status_code=status.HTTP_403_FORBIDDEN,
|
| 694 |
# detail="Access denied - you don't own this conversation"
|
| 695 |
# )
|
| 696 |
|
| 697 |
-
# # Format messages
|
| 698 |
# messages = []
|
| 699 |
# for msg in conversation.get('messages', []):
|
| 700 |
# messages.append(MessageModel(
|
|
@@ -723,29 +279,17 @@ async def chat_health():
|
|
| 723 |
# async def list_user_conversations(
|
| 724 |
# limit: int = 10,
|
| 725 |
# skip: int = 0,
|
| 726 |
-
# current_user: TokenData = Depends(get_current_user)
|
|
|
|
| 727 |
# ):
|
| 728 |
-
# """
|
| 729 |
-
# List all conversations for the authenticated user.
|
| 730 |
-
|
| 731 |
-
# **REQUIRES AUTHENTICATION** - User ID is extracted from JWT token.
|
| 732 |
-
|
| 733 |
-
# Args:
|
| 734 |
-
# limit: Maximum conversations to return (default: 10)
|
| 735 |
-
# skip: Number to skip for pagination (default: 0)
|
| 736 |
-
# current_user: Authenticated user data from JWT token
|
| 737 |
-
|
| 738 |
-
# Returns:
|
| 739 |
-
# dict: List of conversations for current user
|
| 740 |
-
# """
|
| 741 |
# try:
|
| 742 |
-
# conversations = await
|
| 743 |
-
# user_id=current_user.user_id,
|
| 744 |
# limit=limit,
|
| 745 |
# skip=skip
|
| 746 |
# )
|
| 747 |
|
| 748 |
-
# # Format response
|
| 749 |
# return {
|
| 750 |
# "user_id": current_user.user_id,
|
| 751 |
# "user_email": current_user.email,
|
|
@@ -771,26 +315,12 @@ async def chat_health():
|
|
| 771 |
# @router.delete("/conversation/{conversation_id}")
|
| 772 |
# async def delete_conversation(
|
| 773 |
# conversation_id: str,
|
| 774 |
-
# current_user: TokenData = Depends(get_current_user)
|
|
|
|
| 775 |
# ):
|
| 776 |
-
# """
|
| 777 |
-
# Delete a conversation.
|
| 778 |
-
|
| 779 |
-
# **REQUIRES AUTHENTICATION** - User can only delete their own conversations.
|
| 780 |
-
|
| 781 |
-
# Args:
|
| 782 |
-
# conversation_id: Conversation ID to delete
|
| 783 |
-
# current_user: Authenticated user data from JWT token
|
| 784 |
-
|
| 785 |
-
# Returns:
|
| 786 |
-
# dict: Success message
|
| 787 |
-
|
| 788 |
-
# Raises:
|
| 789 |
-
# HTTPException: If conversation not found or user doesn't own it
|
| 790 |
-
# """
|
| 791 |
# try:
|
| 792 |
-
#
|
| 793 |
-
# conversation = await conversation_repo.get_conversation(conversation_id)
|
| 794 |
|
| 795 |
# if not conversation:
|
| 796 |
# raise HTTPException(
|
|
@@ -798,15 +328,13 @@ async def chat_health():
|
|
| 798 |
# detail=f"Conversation {conversation_id} not found"
|
| 799 |
# )
|
| 800 |
|
| 801 |
-
# # Verify user owns this conversation
|
| 802 |
# if conversation["user_id"] != current_user.user_id:
|
| 803 |
# raise HTTPException(
|
| 804 |
# status_code=status.HTTP_403_FORBIDDEN,
|
| 805 |
# detail="Access denied - you don't own this conversation"
|
| 806 |
# )
|
| 807 |
|
| 808 |
-
#
|
| 809 |
-
# success = await conversation_repo.delete_conversation(conversation_id)
|
| 810 |
|
| 811 |
# if success:
|
| 812 |
# return {
|
|
@@ -830,14 +358,7 @@ async def chat_health():
|
|
| 830 |
|
| 831 |
# @router.get("/health")
|
| 832 |
# async def chat_health():
|
| 833 |
-
# """
|
| 834 |
-
# Health check for chat service.
|
| 835 |
-
|
| 836 |
-
# **PUBLIC ENDPOINT** - No authentication required.
|
| 837 |
-
|
| 838 |
-
# Returns:
|
| 839 |
-
# dict: Health status of chat service components
|
| 840 |
-
# """
|
| 841 |
# try:
|
| 842 |
# health = await chat_service.health_check()
|
| 843 |
|
|
@@ -855,68 +376,7 @@ async def chat_health():
|
|
| 855 |
# "error": str(e),
|
| 856 |
# "timestamp": datetime.now().isoformat()
|
| 857 |
# }
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
# # ============================================================================
|
| 861 |
-
# # USAGE DOCUMENTATION
|
| 862 |
# # ============================================================================
|
| 863 |
-
# """
|
| 864 |
-
# === API USAGE EXAMPLES (WITH AUTHENTICATION) ===
|
| 865 |
-
|
| 866 |
-
# ALL ENDPOINTS (except /health) NOW REQUIRE JWT TOKEN IN AUTHORIZATION HEADER!
|
| 867 |
-
|
| 868 |
-
# 1. Register user:
|
| 869 |
-
# POST /api/v1/auth/register
|
| 870 |
-
# Body: {
|
| 871 |
-
# "email": "user@example.com",
|
| 872 |
-
# "password": "SecurePass123",
|
| 873 |
-
# "full_name": "John Doe"
|
| 874 |
-
# }
|
| 875 |
-
# Response: { "access_token": "eyJ...", "user": {...} }
|
| 876 |
-
|
| 877 |
-
# 2. Login:
|
| 878 |
-
# POST /api/v1/auth/login
|
| 879 |
-
# Body: {
|
| 880 |
-
# "email": "user@example.com",
|
| 881 |
-
# "password": "SecurePass123"
|
| 882 |
-
# }
|
| 883 |
-
# Response: { "access_token": "eyJ...", "user": {...} }
|
| 884 |
-
|
| 885 |
-
# 3. Send chat message (WITH TOKEN):
|
| 886 |
-
# POST /api/v1/chat/
|
| 887 |
-
# Headers: { "Authorization": "Bearer eyJ..." }
|
| 888 |
-
# Body: {
|
| 889 |
-
# "query": "What is my account balance?",
|
| 890 |
-
# "conversation_id": "conv_abc" // optional
|
| 891 |
-
# }
|
| 892 |
-
|
| 893 |
-
# 4. Get conversation history (WITH TOKEN):
|
| 894 |
-
# GET /api/v1/chat/history/conv_abc
|
| 895 |
-
# Headers: { "Authorization": "Bearer eyJ..." }
|
| 896 |
-
|
| 897 |
-
# 5. List conversations (WITH TOKEN):
|
| 898 |
-
# GET /api/v1/chat/conversations?limit=10
|
| 899 |
-
# Headers: { "Authorization": "Bearer eyJ..." }
|
| 900 |
-
|
| 901 |
-
# === TESTING WITH CURL ===
|
| 902 |
-
|
| 903 |
-
# # 1. Register
|
| 904 |
-
# TOKEN=$(curl -X POST "http://localhost:8000/api/v1/auth/register" \
|
| 905 |
-
# -H "Content-Type: application/json" \
|
| 906 |
-
# -d '{"email":"test@test.com","password":"test123","full_name":"Test User"}' \
|
| 907 |
-
# | jq -r '.access_token')
|
| 908 |
-
|
| 909 |
-
# # 2. Send chat message with token
|
| 910 |
-
# curl -X POST "http://localhost:8000/api/v1/chat/" \
|
| 911 |
-
# -H "Content-Type: application/json" \
|
| 912 |
-
# -H "Authorization: Bearer $TOKEN" \
|
| 913 |
-
# -d '{"query": "What is my balance?"}'
|
| 914 |
-
# """
|
| 915 |
-
# # ============================================================================
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
|
| 921 |
|
| 922 |
|
|
@@ -926,41 +386,30 @@ async def chat_health():
|
|
| 926 |
|
| 927 |
|
| 928 |
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
# # ======================================================================================================
|
| 942 |
-
# # OLD CODE
|
| 943 |
-
# # ======================================================================================================
|
| 944 |
-
|
| 945 |
# # """
|
| 946 |
-
# # Chat API Endpoints
|
| 947 |
# # RESTful API for the Banking RAG Chatbot
|
| 948 |
|
|
|
|
|
|
|
| 949 |
# # Endpoints:
|
| 950 |
-
# # - POST /chat - Send a message and get response
|
| 951 |
-
# # - GET /chat/history/{conversation_id} - Get conversation history
|
| 952 |
-
# # - POST /chat/conversation - Create new conversation
|
| 953 |
-
# # - GET /chat/conversations - List user's conversations
|
| 954 |
-
# # -
|
|
|
|
| 955 |
# # """
|
| 956 |
|
| 957 |
-
# # from fastapi import APIRouter, HTTPException, status
|
| 958 |
# # from pydantic import BaseModel, Field
|
| 959 |
# # from typing import List, Dict, Optional
|
| 960 |
# # from datetime import datetime
|
| 961 |
|
| 962 |
# # from app.services.chat_service import chat_service
|
| 963 |
# # from app.db.repositories.conversation_repository import ConversationRepository
|
|
|
|
|
|
|
| 964 |
|
| 965 |
|
| 966 |
# # # ============================================================================
|
|
@@ -980,23 +429,22 @@ async def chat_health():
|
|
| 980 |
# # """
|
| 981 |
# # Request model for chat endpoint.
|
| 982 |
|
|
|
|
|
|
|
| 983 |
# # Example:
|
| 984 |
# # {
|
| 985 |
# # "query": "What is my account balance?",
|
| 986 |
-
# # "conversation_id": "abc-123"
|
| 987 |
-
# # "user_id": "user_456"
|
| 988 |
# # }
|
| 989 |
# # """
|
| 990 |
# # query: str = Field(..., description="User query text", min_length=1, max_length=1000)
|
| 991 |
# # conversation_id: Optional[str] = Field(None, description="Optional conversation ID")
|
| 992 |
-
# # user_id: str = Field(..., description="User ID")
|
| 993 |
|
| 994 |
# # class Config:
|
| 995 |
# # json_schema_extra = {
|
| 996 |
# # "example": {
|
| 997 |
# # "query": "What is my account balance?",
|
| 998 |
-
# # "conversation_id": "conv-123"
|
| 999 |
-
# # "user_id": "user-456"
|
| 1000 |
# # }
|
| 1001 |
# # }
|
| 1002 |
|
|
@@ -1018,8 +466,8 @@ async def chat_health():
|
|
| 1018 |
|
| 1019 |
|
| 1020 |
# # class ConversationCreateRequest(BaseModel):
|
| 1021 |
-
# # """Request to create a new conversation"""
|
| 1022 |
-
# # user_id
|
| 1023 |
|
| 1024 |
|
| 1025 |
# # class ConversationCreateResponse(BaseModel):
|
|
@@ -1044,36 +492,59 @@ async def chat_health():
|
|
| 1044 |
|
| 1045 |
|
| 1046 |
# # # ============================================================================
|
| 1047 |
-
# # # ENDPOINTS
|
| 1048 |
# # # ============================================================================
|
| 1049 |
|
| 1050 |
# # @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 1051 |
-
# # async def chat(
|
|
|
|
|
|
|
|
|
|
| 1052 |
# # """
|
| 1053 |
# # Main chat endpoint - Send a query and get a response.
|
| 1054 |
|
|
|
|
|
|
|
| 1055 |
# # This endpoint:
|
| 1056 |
-
# # 1.
|
| 1057 |
-
# # 2.
|
| 1058 |
-
# # 3.
|
| 1059 |
-
# # 4.
|
|
|
|
| 1060 |
|
| 1061 |
# # Args:
|
| 1062 |
-
# # request: ChatRequest with query
|
|
|
|
| 1063 |
|
| 1064 |
# # Returns:
|
| 1065 |
# # ChatResponse: Generated response with metadata
|
| 1066 |
|
| 1067 |
# # Raises:
|
| 1068 |
-
# # HTTPException: If processing fails
|
| 1069 |
# # """
|
| 1070 |
# # try:
|
|
|
|
|
|
|
|
|
|
| 1071 |
# # # If no conversation_id provided, create a new conversation
|
| 1072 |
# # conversation_id = request.conversation_id
|
| 1073 |
# # if not conversation_id:
|
| 1074 |
# # conversation_id = await conversation_repo.create_conversation(
|
| 1075 |
-
# # user_id=
|
| 1076 |
# # )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
|
| 1078 |
# # # Get conversation history
|
| 1079 |
# # history = await conversation_repo.get_conversation_history(
|
|
@@ -1095,7 +566,7 @@ async def chat_health():
|
|
| 1095 |
# # result = await chat_service.process_query(
|
| 1096 |
# # query=request.query,
|
| 1097 |
# # conversation_history=history,
|
| 1098 |
-
# # user_id=
|
| 1099 |
# # )
|
| 1100 |
|
| 1101 |
# # # Save assistant message to database
|
|
@@ -1117,7 +588,7 @@ async def chat_health():
|
|
| 1117 |
# # # Log retrieval data for RL training
|
| 1118 |
# # await conversation_repo.log_retrieval({
|
| 1119 |
# # 'conversation_id': conversation_id,
|
| 1120 |
-
# # 'user_id':
|
| 1121 |
# # 'query': request.query,
|
| 1122 |
# # 'policy_action': result['policy_action'],
|
| 1123 |
# # 'policy_confidence': result['policy_confidence'],
|
|
@@ -1144,6 +615,8 @@ async def chat_health():
|
|
| 1144 |
# # timestamp=result['timestamp']
|
| 1145 |
# # )
|
| 1146 |
|
|
|
|
|
|
|
| 1147 |
# # except Exception as e:
|
| 1148 |
# # print(f"❌ Chat endpoint error: {e}")
|
| 1149 |
# # raise HTTPException(
|
|
@@ -1153,19 +626,23 @@ async def chat_health():
|
|
| 1153 |
|
| 1154 |
|
| 1155 |
# # @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED)
|
| 1156 |
-
# # async def create_conversation(
|
|
|
|
|
|
|
| 1157 |
# # """
|
| 1158 |
# # Create a new conversation.
|
| 1159 |
|
|
|
|
|
|
|
| 1160 |
# # Args:
|
| 1161 |
-
# #
|
| 1162 |
|
| 1163 |
# # Returns:
|
| 1164 |
# # ConversationCreateResponse: Created conversation ID
|
| 1165 |
# # """
|
| 1166 |
# # try:
|
| 1167 |
# # conversation_id = await conversation_repo.create_conversation(
|
| 1168 |
-
# # user_id=
|
| 1169 |
# # )
|
| 1170 |
|
| 1171 |
# # return ConversationCreateResponse(
|
|
@@ -1181,15 +658,24 @@ async def chat_health():
|
|
| 1181 |
|
| 1182 |
|
| 1183 |
# # @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse)
|
| 1184 |
-
# # async def get_conversation_history(
|
|
|
|
|
|
|
|
|
|
| 1185 |
# # """
|
| 1186 |
# # Get conversation history by ID.
|
| 1187 |
|
|
|
|
|
|
|
| 1188 |
# # Args:
|
| 1189 |
# # conversation_id: Conversation ID
|
|
|
|
| 1190 |
|
| 1191 |
# # Returns:
|
| 1192 |
# # ConversationHistoryResponse: List of messages
|
|
|
|
|
|
|
|
|
|
| 1193 |
# # """
|
| 1194 |
# # try:
|
| 1195 |
# # # Get conversation
|
|
@@ -1201,6 +687,13 @@ async def chat_health():
|
|
| 1201 |
# # detail=f"Conversation {conversation_id} not found"
|
| 1202 |
# # )
|
| 1203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
# # # Format messages
|
| 1205 |
# # messages = []
|
| 1206 |
# # for msg in conversation.get('messages', []):
|
|
@@ -1227,28 +720,35 @@ async def chat_health():
|
|
| 1227 |
|
| 1228 |
|
| 1229 |
# # @router.get("/conversations")
|
| 1230 |
-
# # async def list_user_conversations(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
# # """
|
| 1232 |
-
# # List all conversations for
|
|
|
|
|
|
|
| 1233 |
|
| 1234 |
# # Args:
|
| 1235 |
-
# # user_id: User ID
|
| 1236 |
# # limit: Maximum conversations to return (default: 10)
|
| 1237 |
# # skip: Number to skip for pagination (default: 0)
|
|
|
|
| 1238 |
|
| 1239 |
# # Returns:
|
| 1240 |
-
# # dict: List of conversations
|
| 1241 |
# # """
|
| 1242 |
# # try:
|
| 1243 |
# # conversations = await conversation_repo.get_user_conversations(
|
| 1244 |
-
# # user_id=user_id,
|
| 1245 |
# # limit=limit,
|
| 1246 |
# # skip=skip
|
| 1247 |
# # )
|
| 1248 |
|
| 1249 |
# # # Format response
|
| 1250 |
# # return {
|
| 1251 |
-
# # "user_id": user_id,
|
|
|
|
| 1252 |
# # "conversations": [
|
| 1253 |
# # {
|
| 1254 |
# # "conversation_id": conv['conversation_id'],
|
|
@@ -1268,11 +768,73 @@ async def chat_health():
|
|
| 1268 |
# # )
|
| 1269 |
|
| 1270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1271 |
# # @router.get("/health")
|
| 1272 |
# # async def chat_health():
|
| 1273 |
# # """
|
| 1274 |
# # Health check for chat service.
|
| 1275 |
|
|
|
|
|
|
|
| 1276 |
# # Returns:
|
| 1277 |
# # dict: Health status of chat service components
|
| 1278 |
# # """
|
|
@@ -1299,48 +861,486 @@ async def chat_health():
|
|
| 1299 |
# # # USAGE DOCUMENTATION
|
| 1300 |
# # # ============================================================================
|
| 1301 |
# # """
|
| 1302 |
-
# # === API USAGE EXAMPLES ===
|
| 1303 |
|
| 1304 |
-
# #
|
| 1305 |
-
|
|
|
|
|
|
|
| 1306 |
# # Body: {
|
| 1307 |
-
# # "
|
| 1308 |
-
# # "
|
| 1309 |
-
# # "
|
| 1310 |
# # }
|
|
|
|
| 1311 |
|
| 1312 |
-
# # 2.
|
| 1313 |
-
# # POST /api/v1/
|
| 1314 |
# # Body: {
|
| 1315 |
-
# # "
|
|
|
|
| 1316 |
# # }
|
|
|
|
| 1317 |
|
| 1318 |
-
# # 3.
|
| 1319 |
-
# #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1320 |
|
| 1321 |
-
# # 4.
|
| 1322 |
-
# # GET /api/v1/chat/
|
|
|
|
| 1323 |
|
| 1324 |
-
# # 5.
|
| 1325 |
-
# # GET /api/v1/chat/
|
|
|
|
| 1326 |
|
| 1327 |
# # === TESTING WITH CURL ===
|
| 1328 |
|
| 1329 |
-
# # #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1330 |
# # curl -X POST "http://localhost:8000/api/v1/chat/" \
|
| 1331 |
# # -H "Content-Type: application/json" \
|
| 1332 |
-
# # -
|
| 1333 |
-
# #
|
| 1334 |
-
# #
|
| 1335 |
-
# #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1336 |
|
| 1337 |
-
# # # Get history
|
| 1338 |
-
# # curl "http://localhost:8000/api/v1/chat/history/conv_123"
|
| 1339 |
|
| 1340 |
-
# # === TESTING WITH SWAGGER UI ===
|
| 1341 |
|
| 1342 |
-
# # After starting the server, visit:
|
| 1343 |
-
# # http://localhost:8000/docs
|
| 1344 |
|
| 1345 |
-
|
| 1346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# """
|
| 2 |
# Chat API Endpoints (WITH AUTHENTICATION)
|
| 3 |
# RESTful API for the Banking RAG Chatbot
|
|
|
|
| 29 |
# # ============================================================================
|
| 30 |
# router = APIRouter()
|
| 31 |
|
| 32 |
+
|
| 33 |
+
# # ============================================================================
|
| 34 |
+
# # DEPENDENCY: Get ConversationRepository instance
|
| 35 |
+
# # ============================================================================
|
| 36 |
+
# def get_conversation_repo() -> ConversationRepository:
|
| 37 |
+
# """
|
| 38 |
+
# Dependency that provides ConversationRepository instance.
|
| 39 |
+
# This ensures MongoDB is connected before repository is used.
|
| 40 |
+
# """
|
| 41 |
+
# return ConversationRepository()
|
| 42 |
|
| 43 |
|
| 44 |
# # ============================================================================
|
|
|
|
| 46 |
# # ============================================================================
|
| 47 |
|
| 48 |
# class ChatRequest(BaseModel):
|
| 49 |
+
# """Request model for chat endpoint"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# query: str = Field(..., description="User query text", min_length=1, max_length=1000)
|
| 51 |
# conversation_id: Optional[str] = Field(None, description="Optional conversation ID")
|
| 52 |
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
# class ChatResponse(BaseModel):
|
| 63 |
+
# """Response model for chat endpoint"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# response: str = Field(..., description="Generated response text")
|
| 65 |
# conversation_id: str = Field(..., description="Conversation ID")
|
| 66 |
# policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH")
|
|
|
|
| 71 |
# timestamp: str = Field(..., description="Response timestamp (ISO format)")
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# class ConversationCreateResponse(BaseModel):
|
| 75 |
# """Response after creating a conversation"""
|
| 76 |
# conversation_id: str = Field(..., description="Created conversation ID")
|
|
|
|
| 99 |
# @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 100 |
# async def chat(
|
| 101 |
# request: ChatRequest,
|
| 102 |
+
# current_user: TokenData = Depends(get_current_user),
|
| 103 |
+
# repo: ConversationRepository = Depends(get_conversation_repo) # ← INJECT REPO
|
| 104 |
# ):
|
| 105 |
# """
|
| 106 |
# Main chat endpoint - Send a query and get a response.
|
| 107 |
|
| 108 |
# **REQUIRES AUTHENTICATION** - JWT token must be provided in Authorization header.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
# """
|
| 110 |
# try:
|
| 111 |
+
# # Get user_id from token
|
| 112 |
# user_id = current_user.user_id
|
| 113 |
|
| 114 |
# # If no conversation_id provided, create a new conversation
|
| 115 |
# conversation_id = request.conversation_id
|
| 116 |
# if not conversation_id:
|
| 117 |
+
# conversation_id = await repo.create_conversation(user_id=user_id)
|
|
|
|
|
|
|
| 118 |
# else:
|
| 119 |
# # Verify user owns this conversation
|
| 120 |
+
# conversation = await repo.get_conversation(conversation_id)
|
| 121 |
# if not conversation:
|
| 122 |
# raise HTTPException(
|
| 123 |
# status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
| 130 |
# )
|
| 131 |
|
| 132 |
# # Get conversation history
|
| 133 |
+
# history = await repo.get_conversation_history(
|
| 134 |
# conversation_id=conversation_id,
|
| 135 |
+
# max_messages=10
|
| 136 |
# )
|
| 137 |
|
| 138 |
+
# # Save user message
|
| 139 |
+
# await repo.add_message(
|
| 140 |
# conversation_id=conversation_id,
|
| 141 |
# message={
|
| 142 |
# 'role': 'user',
|
|
|
|
| 152 |
# user_id=user_id
|
| 153 |
# )
|
| 154 |
|
| 155 |
+
# # Save assistant message
|
| 156 |
+
# await repo.add_message(
|
| 157 |
# conversation_id=conversation_id,
|
| 158 |
# message={
|
| 159 |
# 'role': 'assistant',
|
|
|
|
| 169 |
# )
|
| 170 |
|
| 171 |
# # Log retrieval data for RL training
|
| 172 |
+
# await repo.log_retrieval({
|
| 173 |
# 'conversation_id': conversation_id,
|
| 174 |
# 'user_id': user_id,
|
| 175 |
# 'query': request.query,
|
|
|
|
| 199 |
# )
|
| 200 |
|
| 201 |
# except HTTPException:
|
| 202 |
+
# raise
|
| 203 |
# except Exception as e:
|
| 204 |
# print(f"❌ Chat endpoint error: {e}")
|
| 205 |
+
# import traceback
|
| 206 |
+
# traceback.print_exc()
|
| 207 |
# raise HTTPException(
|
| 208 |
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 209 |
# detail=f"Failed to process chat request: {str(e)}"
|
|
|
|
| 212 |
|
| 213 |
# @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED)
|
| 214 |
# async def create_conversation(
|
| 215 |
+
# current_user: TokenData = Depends(get_current_user),
|
| 216 |
+
# repo: ConversationRepository = Depends(get_conversation_repo)
|
| 217 |
# ):
|
| 218 |
+
# """Create a new conversation"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
# try:
|
| 220 |
+
# conversation_id = await repo.create_conversation(user_id=current_user.user_id)
|
|
|
|
|
|
|
|
|
|
| 221 |
# return ConversationCreateResponse(
|
| 222 |
# conversation_id=conversation_id,
|
| 223 |
# created_at=datetime.now().isoformat()
|
| 224 |
# )
|
|
|
|
| 225 |
# except Exception as e:
|
| 226 |
# raise HTTPException(
|
| 227 |
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
| 232 |
# @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse)
|
| 233 |
# async def get_conversation_history(
|
| 234 |
# conversation_id: str,
|
| 235 |
+
# current_user: TokenData = Depends(get_current_user),
|
| 236 |
+
# repo: ConversationRepository = Depends(get_conversation_repo)
|
| 237 |
# ):
|
| 238 |
+
# """Get conversation history by ID"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
# try:
|
| 240 |
+
# conversation = await repo.get_conversation(conversation_id)
|
|
|
|
| 241 |
|
| 242 |
# if not conversation:
|
| 243 |
# raise HTTPException(
|
|
|
|
| 245 |
# detail=f"Conversation {conversation_id} not found"
|
| 246 |
# )
|
| 247 |
|
|
|
|
| 248 |
# if conversation["user_id"] != current_user.user_id:
|
| 249 |
# raise HTTPException(
|
| 250 |
# status_code=status.HTTP_403_FORBIDDEN,
|
| 251 |
# detail="Access denied - you don't own this conversation"
|
| 252 |
# )
|
| 253 |
|
|
|
|
| 254 |
# messages = []
|
| 255 |
# for msg in conversation.get('messages', []):
|
| 256 |
# messages.append(MessageModel(
|
|
|
|
| 279 |
# async def list_user_conversations(
|
| 280 |
# limit: int = 10,
|
| 281 |
# skip: int = 0,
|
| 282 |
+
# current_user: TokenData = Depends(get_current_user),
|
| 283 |
+
# repo: ConversationRepository = Depends(get_conversation_repo)
|
| 284 |
# ):
|
| 285 |
+
# """List all conversations for the authenticated user"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
# try:
|
| 287 |
+
# conversations = await repo.get_user_conversations(
|
| 288 |
+
# user_id=current_user.user_id,
|
| 289 |
# limit=limit,
|
| 290 |
# skip=skip
|
| 291 |
# )
|
| 292 |
|
|
|
|
| 293 |
# return {
|
| 294 |
# "user_id": current_user.user_id,
|
| 295 |
# "user_email": current_user.email,
|
|
|
|
| 315 |
# @router.delete("/conversation/{conversation_id}")
|
| 316 |
# async def delete_conversation(
|
| 317 |
# conversation_id: str,
|
| 318 |
+
# current_user: TokenData = Depends(get_current_user),
|
| 319 |
+
# repo: ConversationRepository = Depends(get_conversation_repo)
|
| 320 |
# ):
|
| 321 |
+
# """Delete a conversation"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
# try:
|
| 323 |
+
# conversation = await repo.get_conversation(conversation_id)
|
|
|
|
| 324 |
|
| 325 |
# if not conversation:
|
| 326 |
# raise HTTPException(
|
|
|
|
| 328 |
# detail=f"Conversation {conversation_id} not found"
|
| 329 |
# )
|
| 330 |
|
|
|
|
| 331 |
# if conversation["user_id"] != current_user.user_id:
|
| 332 |
# raise HTTPException(
|
| 333 |
# status_code=status.HTTP_403_FORBIDDEN,
|
| 334 |
# detail="Access denied - you don't own this conversation"
|
| 335 |
# )
|
| 336 |
|
| 337 |
+
# success = await repo.delete_conversation(conversation_id)
|
|
|
|
| 338 |
|
| 339 |
# if success:
|
| 340 |
# return {
|
|
|
|
| 358 |
|
| 359 |
# @router.get("/health")
|
| 360 |
# async def chat_health():
|
| 361 |
+
# """Health check for chat service (PUBLIC)"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
# try:
|
| 363 |
# health = await chat_service.health_check()
|
| 364 |
|
|
|
|
| 376 |
# "error": str(e),
|
| 377 |
# "timestamp": datetime.now().isoformat()
|
| 378 |
# }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
# # ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
|
| 382 |
|
|
|
|
| 386 |
|
| 387 |
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
# # """
|
| 390 |
+
# # Chat API Endpoints (WITH AUTHENTICATION)
|
| 391 |
# # RESTful API for the Banking RAG Chatbot
|
| 392 |
|
| 393 |
+
# # NOW REQUIRES JWT TOKEN FOR ALL ENDPOINTS!
|
| 394 |
+
|
| 395 |
# # Endpoints:
|
| 396 |
+
# # - POST /chat - Send a message and get response (PROTECTED)
|
| 397 |
+
# # - GET /chat/history/{conversation_id} - Get conversation history (PROTECTED)
|
| 398 |
+
# # - POST /chat/conversation - Create new conversation (PROTECTED)
|
| 399 |
+
# # - GET /chat/conversations - List user's conversations (PROTECTED)
|
| 400 |
+
# # - DELETE /chat/conversation/{conversation_id} - Delete conversation (PROTECTED)
|
| 401 |
+
# # - GET /chat/health - Health check (PUBLIC)
|
| 402 |
# # """
|
| 403 |
|
| 404 |
+
# # from fastapi import APIRouter, HTTPException, status, Depends
|
| 405 |
# # from pydantic import BaseModel, Field
|
| 406 |
# # from typing import List, Dict, Optional
|
| 407 |
# # from datetime import datetime
|
| 408 |
|
| 409 |
# # from app.services.chat_service import chat_service
|
| 410 |
# # from app.db.repositories.conversation_repository import ConversationRepository
|
| 411 |
+
# # from app.utils.dependencies import get_current_user # AUTH DEPENDENCY
|
| 412 |
+
# # from app.models.user import TokenData # USER DATA FROM TOKEN
|
| 413 |
|
| 414 |
|
| 415 |
# # # ============================================================================
|
|
|
|
| 429 |
# # """
|
| 430 |
# # Request model for chat endpoint.
|
| 431 |
|
| 432 |
+
# # NOTE: user_id is now extracted from JWT token, not from request body!
|
| 433 |
+
|
| 434 |
# # Example:
|
| 435 |
# # {
|
| 436 |
# # "query": "What is my account balance?",
|
| 437 |
+
# # "conversation_id": "abc-123"
|
|
|
|
| 438 |
# # }
|
| 439 |
# # """
|
| 440 |
# # query: str = Field(..., description="User query text", min_length=1, max_length=1000)
|
| 441 |
# # conversation_id: Optional[str] = Field(None, description="Optional conversation ID")
|
|
|
|
| 442 |
|
| 443 |
# # class Config:
|
| 444 |
# # json_schema_extra = {
|
| 445 |
# # "example": {
|
| 446 |
# # "query": "What is my account balance?",
|
| 447 |
+
# # "conversation_id": "conv-123"
|
|
|
|
| 448 |
# # }
|
| 449 |
# # }
|
| 450 |
|
|
|
|
| 466 |
|
| 467 |
|
| 468 |
# # class ConversationCreateRequest(BaseModel):
|
| 469 |
+
# # """Request to create a new conversation (no user_id needed - from token)"""
|
| 470 |
+
# # pass # Empty - user_id comes from JWT token
|
| 471 |
|
| 472 |
|
| 473 |
# # class ConversationCreateResponse(BaseModel):
|
|
|
|
| 492 |
|
| 493 |
|
| 494 |
# # # ============================================================================
|
| 495 |
+
# # # ENDPOINTS (ALL PROTECTED WITH JWT)
|
| 496 |
# # # ============================================================================
|
| 497 |
|
| 498 |
# # @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 499 |
+
# # async def chat(
|
| 500 |
+
# # request: ChatRequest,
|
| 501 |
+
# # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH!
|
| 502 |
+
# # ):
|
| 503 |
# # """
|
| 504 |
# # Main chat endpoint - Send a query and get a response.
|
| 505 |
|
| 506 |
+
# # **REQUIRES AUTHENTICATION** - JWT token must be provided in Authorization header.
|
| 507 |
+
|
| 508 |
# # This endpoint:
|
| 509 |
+
# # 1. Extracts user_id from JWT token
|
| 510 |
+
# # 2. Processes the query through the RAG pipeline
|
| 511 |
+
# # 3. Saves messages to MongoDB
|
| 512 |
+
# # 4. Logs retrieval data for RL training
|
| 513 |
+
# # 5. Returns response with metadata
|
| 514 |
|
| 515 |
# # Args:
|
| 516 |
+
# # request: ChatRequest with query and optional conversation_id
|
| 517 |
+
# # current_user: Authenticated user data from JWT token
|
| 518 |
|
| 519 |
# # Returns:
|
| 520 |
# # ChatResponse: Generated response with metadata
|
| 521 |
|
| 522 |
# # Raises:
|
| 523 |
+
# # HTTPException: If processing fails or user not authenticated
|
| 524 |
# # """
|
| 525 |
# # try:
|
| 526 |
+
# # # Get user_id from token (NOT from request body!)
|
| 527 |
+
# # user_id = current_user.user_id
|
| 528 |
+
|
| 529 |
# # # If no conversation_id provided, create a new conversation
|
| 530 |
# # conversation_id = request.conversation_id
|
| 531 |
# # if not conversation_id:
|
| 532 |
# # conversation_id = await conversation_repo.create_conversation(
|
| 533 |
+
# # user_id=user_id
|
| 534 |
# # )
|
| 535 |
+
# # else:
|
| 536 |
+
# # # Verify user owns this conversation
|
| 537 |
+
# # conversation = await conversation_repo.get_conversation(conversation_id)
|
| 538 |
+
# # if not conversation:
|
| 539 |
+
# # raise HTTPException(
|
| 540 |
+
# # status_code=status.HTTP_404_NOT_FOUND,
|
| 541 |
+
# # detail="Conversation not found"
|
| 542 |
+
# # )
|
| 543 |
+
# # if conversation["user_id"] != user_id:
|
| 544 |
+
# # raise HTTPException(
|
| 545 |
+
# # status_code=status.HTTP_403_FORBIDDEN,
|
| 546 |
+
# # detail="Access denied - you don't own this conversation"
|
| 547 |
+
# # )
|
| 548 |
|
| 549 |
# # # Get conversation history
|
| 550 |
# # history = await conversation_repo.get_conversation_history(
|
|
|
|
| 566 |
# # result = await chat_service.process_query(
|
| 567 |
# # query=request.query,
|
| 568 |
# # conversation_history=history,
|
| 569 |
+
# # user_id=user_id
|
| 570 |
# # )
|
| 571 |
|
| 572 |
# # # Save assistant message to database
|
|
|
|
| 588 |
# # # Log retrieval data for RL training
|
| 589 |
# # await conversation_repo.log_retrieval({
|
| 590 |
# # 'conversation_id': conversation_id,
|
| 591 |
+
# # 'user_id': user_id,
|
| 592 |
# # 'query': request.query,
|
| 593 |
# # 'policy_action': result['policy_action'],
|
| 594 |
# # 'policy_confidence': result['policy_confidence'],
|
|
|
|
| 615 |
# # timestamp=result['timestamp']
|
| 616 |
# # )
|
| 617 |
|
| 618 |
+
# # except HTTPException:
|
| 619 |
+
# # raise # Re-raise HTTP exceptions
|
| 620 |
# # except Exception as e:
|
| 621 |
# # print(f"❌ Chat endpoint error: {e}")
|
| 622 |
# # raise HTTPException(
|
|
|
|
| 626 |
|
| 627 |
|
| 628 |
# # @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED)
|
| 629 |
+
# # async def create_conversation(
|
| 630 |
+
# # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH!
|
| 631 |
+
# # ):
|
| 632 |
# # """
|
| 633 |
# # Create a new conversation.
|
| 634 |
|
| 635 |
+
# # **REQUIRES AUTHENTICATION** - User ID is extracted from JWT token.
|
| 636 |
+
|
| 637 |
# # Args:
|
| 638 |
+
# # current_user: Authenticated user data from JWT token
|
| 639 |
|
| 640 |
# # Returns:
|
| 641 |
# # ConversationCreateResponse: Created conversation ID
|
| 642 |
# # """
|
| 643 |
# # try:
|
| 644 |
# # conversation_id = await conversation_repo.create_conversation(
|
| 645 |
+
# # user_id=current_user.user_id
|
| 646 |
# # )
|
| 647 |
|
| 648 |
# # return ConversationCreateResponse(
|
|
|
|
| 658 |
|
| 659 |
|
| 660 |
# # @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse)
|
| 661 |
+
# # async def get_conversation_history(
|
| 662 |
+
# # conversation_id: str,
|
| 663 |
+
# # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH!
|
| 664 |
+
# # ):
|
| 665 |
# # """
|
| 666 |
# # Get conversation history by ID.
|
| 667 |
|
| 668 |
+
# # **REQUIRES AUTHENTICATION** - User can only access their own conversations.
|
| 669 |
+
|
| 670 |
# # Args:
|
| 671 |
# # conversation_id: Conversation ID
|
| 672 |
+
# # current_user: Authenticated user data from JWT token
|
| 673 |
|
| 674 |
# # Returns:
|
| 675 |
# # ConversationHistoryResponse: List of messages
|
| 676 |
+
|
| 677 |
+
# # Raises:
|
| 678 |
+
# # HTTPException: If conversation not found or user doesn't own it
|
| 679 |
# # """
|
| 680 |
# # try:
|
| 681 |
# # # Get conversation
|
|
|
|
| 687 |
# # detail=f"Conversation {conversation_id} not found"
|
| 688 |
# # )
|
| 689 |
|
| 690 |
+
# # # Verify user owns this conversation
|
| 691 |
+
# # if conversation["user_id"] != current_user.user_id:
|
| 692 |
+
# # raise HTTPException(
|
| 693 |
+
# # status_code=status.HTTP_403_FORBIDDEN,
|
| 694 |
+
# # detail="Access denied - you don't own this conversation"
|
| 695 |
+
# # )
|
| 696 |
+
|
| 697 |
# # # Format messages
|
| 698 |
# # messages = []
|
| 699 |
# # for msg in conversation.get('messages', []):
|
|
|
|
| 720 |
|
| 721 |
|
| 722 |
# # @router.get("/conversations")
|
| 723 |
+
# # async def list_user_conversations(
|
| 724 |
+
# # limit: int = 10,
|
| 725 |
+
# # skip: int = 0,
|
| 726 |
+
# # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH!
|
| 727 |
+
# # ):
|
| 728 |
# # """
|
| 729 |
+
# # List all conversations for the authenticated user.
|
| 730 |
+
|
| 731 |
+
# # **REQUIRES AUTHENTICATION** - User ID is extracted from JWT token.
|
| 732 |
|
| 733 |
# # Args:
|
|
|
|
| 734 |
# # limit: Maximum conversations to return (default: 10)
|
| 735 |
# # skip: Number to skip for pagination (default: 0)
|
| 736 |
+
# # current_user: Authenticated user data from JWT token
|
| 737 |
|
| 738 |
# # Returns:
|
| 739 |
+
# # dict: List of conversations for current user
|
| 740 |
# # """
|
| 741 |
# # try:
|
| 742 |
# # conversations = await conversation_repo.get_user_conversations(
|
| 743 |
+
# # user_id=current_user.user_id, # From JWT token!
|
| 744 |
# # limit=limit,
|
| 745 |
# # skip=skip
|
| 746 |
# # )
|
| 747 |
|
| 748 |
# # # Format response
|
| 749 |
# # return {
|
| 750 |
+
# # "user_id": current_user.user_id,
|
| 751 |
+
# # "user_email": current_user.email,
|
| 752 |
# # "conversations": [
|
| 753 |
# # {
|
| 754 |
# # "conversation_id": conv['conversation_id'],
|
|
|
|
| 768 |
# # )
|
| 769 |
|
| 770 |
|
| 771 |
+
# # @router.delete("/conversation/{conversation_id}")
|
| 772 |
+
# # async def delete_conversation(
|
| 773 |
+
# # conversation_id: str,
|
| 774 |
+
# # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH!
|
| 775 |
+
# # ):
|
| 776 |
+
# # """
|
| 777 |
+
# # Delete a conversation.
|
| 778 |
+
|
| 779 |
+
# # **REQUIRES AUTHENTICATION** - User can only delete their own conversations.
|
| 780 |
+
|
| 781 |
+
# # Args:
|
| 782 |
+
# # conversation_id: Conversation ID to delete
|
| 783 |
+
# # current_user: Authenticated user data from JWT token
|
| 784 |
+
|
| 785 |
+
# # Returns:
|
| 786 |
+
# # dict: Success message
|
| 787 |
+
|
| 788 |
+
# # Raises:
|
| 789 |
+
# # HTTPException: If conversation not found or user doesn't own it
|
| 790 |
+
# # """
|
| 791 |
+
# # try:
|
| 792 |
+
# # # Get conversation
|
| 793 |
+
# # conversation = await conversation_repo.get_conversation(conversation_id)
|
| 794 |
+
|
| 795 |
+
# # if not conversation:
|
| 796 |
+
# # raise HTTPException(
|
| 797 |
+
# # status_code=status.HTTP_404_NOT_FOUND,
|
| 798 |
+
# # detail=f"Conversation {conversation_id} not found"
|
| 799 |
+
# # )
|
| 800 |
+
|
| 801 |
+
# # # Verify user owns this conversation
|
| 802 |
+
# # if conversation["user_id"] != current_user.user_id:
|
| 803 |
+
# # raise HTTPException(
|
| 804 |
+
# # status_code=status.HTTP_403_FORBIDDEN,
|
| 805 |
+
# # detail="Access denied - you don't own this conversation"
|
| 806 |
+
# # )
|
| 807 |
+
|
| 808 |
+
# # # Delete conversation
|
| 809 |
+
# # success = await conversation_repo.delete_conversation(conversation_id)
|
| 810 |
+
|
| 811 |
+
# # if success:
|
| 812 |
+
# # return {
|
| 813 |
+
# # "message": "Conversation deleted successfully",
|
| 814 |
+
# # "conversation_id": conversation_id
|
| 815 |
+
# # }
|
| 816 |
+
# # else:
|
| 817 |
+
# # raise HTTPException(
|
| 818 |
+
# # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 819 |
+
# # detail="Failed to delete conversation"
|
| 820 |
+
# # )
|
| 821 |
+
|
| 822 |
+
# # except HTTPException:
|
| 823 |
+
# # raise
|
| 824 |
+
# # except Exception as e:
|
| 825 |
+
# # raise HTTPException(
|
| 826 |
+
# # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 827 |
+
# # detail=f"Failed to delete conversation: {str(e)}"
|
| 828 |
+
# # )
|
| 829 |
+
|
| 830 |
+
|
| 831 |
# # @router.get("/health")
|
| 832 |
# # async def chat_health():
|
| 833 |
# # """
|
| 834 |
# # Health check for chat service.
|
| 835 |
|
| 836 |
+
# # **PUBLIC ENDPOINT** - No authentication required.
|
| 837 |
+
|
| 838 |
# # Returns:
|
| 839 |
# # dict: Health status of chat service components
|
| 840 |
# # """
|
|
|
|
| 861 |
# # # USAGE DOCUMENTATION
|
| 862 |
# # # ============================================================================
|
| 863 |
# # """
|
| 864 |
+
# # === API USAGE EXAMPLES (WITH AUTHENTICATION) ===
|
| 865 |
|
| 866 |
+
# # ALL ENDPOINTS (except /health) NOW REQUIRE JWT TOKEN IN AUTHORIZATION HEADER!
|
| 867 |
+
|
| 868 |
+
# # 1. Register user:
|
| 869 |
+
# # POST /api/v1/auth/register
|
| 870 |
# # Body: {
|
| 871 |
+
# # "email": "user@example.com",
|
| 872 |
+
# # "password": "SecurePass123",
|
| 873 |
+
# # "full_name": "John Doe"
|
| 874 |
# # }
|
| 875 |
+
# # Response: { "access_token": "eyJ...", "user": {...} }
|
| 876 |
|
| 877 |
+
# # 2. Login:
|
| 878 |
+
# # POST /api/v1/auth/login
|
| 879 |
# # Body: {
|
| 880 |
+
# # "email": "user@example.com",
|
| 881 |
+
# # "password": "SecurePass123"
|
| 882 |
# # }
|
| 883 |
+
# # Response: { "access_token": "eyJ...", "user": {...} }
|
| 884 |
|
| 885 |
+
# # 3. Send chat message (WITH TOKEN):
|
| 886 |
+
# # POST /api/v1/chat/
|
| 887 |
+
# # Headers: { "Authorization": "Bearer eyJ..." }
|
| 888 |
+
# # Body: {
|
| 889 |
+
# # "query": "What is my account balance?",
|
| 890 |
+
# # "conversation_id": "conv_abc" // optional
|
| 891 |
+
# # }
|
| 892 |
|
| 893 |
+
# # 4. Get conversation history (WITH TOKEN):
|
| 894 |
+
# # GET /api/v1/chat/history/conv_abc
|
| 895 |
+
# # Headers: { "Authorization": "Bearer eyJ..." }
|
| 896 |
|
| 897 |
+
# # 5. List conversations (WITH TOKEN):
|
| 898 |
+
# # GET /api/v1/chat/conversations?limit=10
|
| 899 |
+
# # Headers: { "Authorization": "Bearer eyJ..." }
|
| 900 |
|
| 901 |
# # === TESTING WITH CURL ===
|
| 902 |
|
| 903 |
+
# # # 1. Register
|
| 904 |
+
# # TOKEN=$(curl -X POST "http://localhost:8000/api/v1/auth/register" \
|
| 905 |
+
# # -H "Content-Type: application/json" \
|
| 906 |
+
# # -d '{"email":"test@test.com","password":"test123","full_name":"Test User"}' \
|
| 907 |
+
# # | jq -r '.access_token')
|
| 908 |
+
|
| 909 |
+
# # # 2. Send chat message with token
|
| 910 |
# # curl -X POST "http://localhost:8000/api/v1/chat/" \
|
| 911 |
# # -H "Content-Type: application/json" \
|
| 912 |
+
# # -H "Authorization: Bearer $TOKEN" \
|
| 913 |
+
# # -d '{"query": "What is my balance?"}'
|
| 914 |
+
# # """
|
| 915 |
+
# # # ============================================================================
|
| 916 |
+
|
| 917 |
+
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
|
| 925 |
+
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
|
| 932 |
|
|
|
|
|
|
|
| 933 |
|
|
|
|
| 934 |
|
|
|
|
|
|
|
| 935 |
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
# # # ======================================================================================================
|
| 942 |
+
# # # OLD CODE
|
| 943 |
+
# # # ======================================================================================================
|
| 944 |
+
|
| 945 |
+
# # # """
|
| 946 |
+
# # # Chat API Endpoints
|
| 947 |
+
# # # RESTful API for the Banking RAG Chatbot
|
| 948 |
+
|
| 949 |
+
# # # Endpoints:
|
| 950 |
+
# # # - POST /chat - Send a message and get response
|
| 951 |
+
# # # - GET /chat/history/{conversation_id} - Get conversation history
|
| 952 |
+
# # # - POST /chat/conversation - Create new conversation
|
| 953 |
+
# # # - GET /chat/conversations - List user's conversations
|
| 954 |
+
# # # - GET /chat/health - Health check for chat service
|
| 955 |
+
# # # """
|
| 956 |
+
|
| 957 |
+
# # # from fastapi import APIRouter, HTTPException, status
|
| 958 |
+
# # # from pydantic import BaseModel, Field
|
| 959 |
+
# # # from typing import List, Dict, Optional
|
| 960 |
+
# # # from datetime import datetime
|
| 961 |
+
|
| 962 |
+
# # # from app.services.chat_service import chat_service
|
| 963 |
+
# # # from app.db.repositories.conversation_repository import ConversationRepository
|
| 964 |
+
|
| 965 |
+
|
| 966 |
+
# # # # ============================================================================
|
| 967 |
+
# # # # CREATE ROUTER
|
| 968 |
+
# # # # ============================================================================
|
| 969 |
+
# # # router = APIRouter()
|
| 970 |
+
|
| 971 |
+
# # # # Initialize repository
|
| 972 |
+
# # # conversation_repo = ConversationRepository()
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
# # # # ============================================================================
|
| 976 |
+
# # # # PYDANTIC MODELS (Request/Response schemas)
|
| 977 |
+
# # # # ============================================================================
|
| 978 |
+
|
| 979 |
+
# # # class ChatRequest(BaseModel):
|
| 980 |
+
# # # """
|
| 981 |
+
# # # Request model for chat endpoint.
|
| 982 |
+
|
| 983 |
+
# # # Example:
|
| 984 |
+
# # # {
|
| 985 |
+
# # # "query": "What is my account balance?",
|
| 986 |
+
# # # "conversation_id": "abc-123",
|
| 987 |
+
# # # "user_id": "user_456"
|
| 988 |
+
# # # }
|
| 989 |
+
# # # """
|
| 990 |
+
# # # query: str = Field(..., description="User query text", min_length=1, max_length=1000)
|
| 991 |
+
# # # conversation_id: Optional[str] = Field(None, description="Optional conversation ID")
|
| 992 |
+
# # # user_id: str = Field(..., description="User ID")
|
| 993 |
+
|
| 994 |
+
# # # class Config:
|
| 995 |
+
# # # json_schema_extra = {
|
| 996 |
+
# # # "example": {
|
| 997 |
+
# # # "query": "What is my account balance?",
|
| 998 |
+
# # # "conversation_id": "conv-123",
|
| 999 |
+
# # # "user_id": "user-456"
|
| 1000 |
+
# # # }
|
| 1001 |
+
# # # }
|
| 1002 |
+
|
| 1003 |
+
|
| 1004 |
+
# # # class ChatResponse(BaseModel):
|
| 1005 |
+
# # # """
|
| 1006 |
+
# # # Response model for chat endpoint.
|
| 1007 |
+
|
| 1008 |
+
# # # Contains the generated response plus metadata about the RAG pipeline.
|
| 1009 |
+
# # # """
|
| 1010 |
+
# # # response: str = Field(..., description="Generated response text")
|
| 1011 |
+
# # # conversation_id: str = Field(..., description="Conversation ID")
|
| 1012 |
+
# # # policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH")
|
| 1013 |
+
# # # policy_confidence: float = Field(..., description="Policy confidence score (0-1)")
|
| 1014 |
+
# # # documents_retrieved: int = Field(..., description="Number of documents retrieved")
|
| 1015 |
+
# # # top_doc_score: Optional[float] = Field(None, description="Best document similarity score")
|
| 1016 |
+
# # # total_time_ms: float = Field(..., description="Total processing time in milliseconds")
|
| 1017 |
+
# # # timestamp: str = Field(..., description="Response timestamp (ISO format)")
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
# # # class ConversationCreateRequest(BaseModel):
|
| 1021 |
+
# # # """Request to create a new conversation"""
|
| 1022 |
+
# # # user_id: str = Field(..., description="User ID")
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
# # # class ConversationCreateResponse(BaseModel):
|
| 1026 |
+
# # # """Response after creating a conversation"""
|
| 1027 |
+
# # # conversation_id: str = Field(..., description="Created conversation ID")
|
| 1028 |
+
# # # created_at: str = Field(..., description="Creation timestamp")
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
+
# # # class MessageModel(BaseModel):
|
| 1032 |
+
# # # """Single message in conversation history"""
|
| 1033 |
+
# # # role: str = Field(..., description="Message role: user or assistant")
|
| 1034 |
+
# # # content: str = Field(..., description="Message content")
|
| 1035 |
+
# # # timestamp: str = Field(..., description="Message timestamp")
|
| 1036 |
+
# # # metadata: Optional[Dict] = Field(None, description="Optional metadata")
|
| 1037 |
+
|
| 1038 |
+
|
| 1039 |
+
# # # class ConversationHistoryResponse(BaseModel):
|
| 1040 |
+
# # # """Response containing conversation history"""
|
| 1041 |
+
# # # conversation_id: str
|
| 1042 |
+
# # # messages: List[MessageModel]
|
| 1043 |
+
# # # message_count: int
|
| 1044 |
+
|
| 1045 |
+
|
| 1046 |
+
# # # # ============================================================================
|
| 1047 |
+
# # # # ENDPOINTS
|
| 1048 |
+
# # # # ============================================================================
|
| 1049 |
+
|
| 1050 |
+
# # # @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 1051 |
+
# # # async def chat(request: ChatRequest):
|
| 1052 |
+
# # # """
|
| 1053 |
+
# # # Main chat endpoint - Send a query and get a response.
|
| 1054 |
+
|
| 1055 |
+
# # # This endpoint:
|
| 1056 |
+
# # # 1. Processes the query through the RAG pipeline
|
| 1057 |
+
# # # 2. Saves messages to MongoDB
|
| 1058 |
+
# # # 3. Logs retrieval data for RL training
|
| 1059 |
+
# # # 4. Returns response with metadata
|
| 1060 |
+
|
| 1061 |
+
# # # Args:
|
| 1062 |
+
# # # request: ChatRequest with query, conversation_id, user_id
|
| 1063 |
+
|
| 1064 |
+
# # # Returns:
|
| 1065 |
+
# # # ChatResponse: Generated response with metadata
|
| 1066 |
+
|
| 1067 |
+
# # # Raises:
|
| 1068 |
+
# # # HTTPException: If processing fails
|
| 1069 |
+
# # # """
|
| 1070 |
+
# # # try:
|
| 1071 |
+
# # # # If no conversation_id provided, create a new conversation
|
| 1072 |
+
# # # conversation_id = request.conversation_id
|
| 1073 |
+
# # # if not conversation_id:
|
| 1074 |
+
# # # conversation_id = await conversation_repo.create_conversation(
|
| 1075 |
+
# # # user_id=request.user_id
|
| 1076 |
+
# # # )
|
| 1077 |
+
|
| 1078 |
+
# # # # Get conversation history
|
| 1079 |
+
# # # history = await conversation_repo.get_conversation_history(
|
| 1080 |
+
# # # conversation_id=conversation_id,
|
| 1081 |
+
# # # max_messages=10 # Last 5 turns (10 messages)
|
| 1082 |
+
# # # )
|
| 1083 |
+
|
| 1084 |
+
# # # # Save user message to database
|
| 1085 |
+
# # # await conversation_repo.add_message(
|
| 1086 |
+
# # # conversation_id=conversation_id,
|
| 1087 |
+
# # # message={
|
| 1088 |
+
# # # 'role': 'user',
|
| 1089 |
+
# # # 'content': request.query,
|
| 1090 |
+
# # # 'timestamp': datetime.now()
|
| 1091 |
+
# # # }
|
| 1092 |
+
# # # )
|
| 1093 |
+
|
| 1094 |
+
# # # # Process query through RAG pipeline
|
| 1095 |
+
# # # result = await chat_service.process_query(
|
| 1096 |
+
# # # query=request.query,
|
| 1097 |
+
# # # conversation_history=history,
|
| 1098 |
+
# # # user_id=request.user_id
|
| 1099 |
+
# # # )
|
| 1100 |
+
|
| 1101 |
+
# # # # Save assistant message to database
|
| 1102 |
+
# # # await conversation_repo.add_message(
|
| 1103 |
+
# # # conversation_id=conversation_id,
|
| 1104 |
+
# # # message={
|
| 1105 |
+
# # # 'role': 'assistant',
|
| 1106 |
+
# # # 'content': result['response'],
|
| 1107 |
+
# # # 'timestamp': datetime.now(),
|
| 1108 |
+
# # # 'metadata': {
|
| 1109 |
+
# # # 'policy_action': result['policy_action'],
|
| 1110 |
+
# # # 'policy_confidence': result['policy_confidence'],
|
| 1111 |
+
# # # 'documents_retrieved': result['documents_retrieved'],
|
| 1112 |
+
# # # 'top_doc_score': result['top_doc_score']
|
| 1113 |
+
# # # }
|
| 1114 |
+
# # # }
|
| 1115 |
+
# # # )
|
| 1116 |
+
|
| 1117 |
+
# # # # Log retrieval data for RL training
|
| 1118 |
+
# # # await conversation_repo.log_retrieval({
|
| 1119 |
+
# # # 'conversation_id': conversation_id,
|
| 1120 |
+
# # # 'user_id': request.user_id,
|
| 1121 |
+
# # # 'query': request.query,
|
| 1122 |
+
# # # 'policy_action': result['policy_action'],
|
| 1123 |
+
# # # 'policy_confidence': result['policy_confidence'],
|
| 1124 |
+
# # # 'should_retrieve': result['should_retrieve'],
|
| 1125 |
+
# # # 'documents_retrieved': result['documents_retrieved'],
|
| 1126 |
+
# # # 'top_doc_score': result['top_doc_score'],
|
| 1127 |
+
# # # 'response': result['response'],
|
| 1128 |
+
# # # 'retrieval_time_ms': result['retrieval_time_ms'],
|
| 1129 |
+
# # # 'generation_time_ms': result['generation_time_ms'],
|
| 1130 |
+
# # # 'total_time_ms': result['total_time_ms'],
|
| 1131 |
+
# # # 'retrieved_docs_metadata': result.get('retrieved_docs_metadata', []),
|
| 1132 |
+
# # # 'timestamp': datetime.now()
|
| 1133 |
+
# # # })
|
| 1134 |
+
|
| 1135 |
+
# # # # Return response
|
| 1136 |
+
# # # return ChatResponse(
|
| 1137 |
+
# # # response=result['response'],
|
| 1138 |
+
# # # conversation_id=conversation_id,
|
| 1139 |
+
# # # policy_action=result['policy_action'],
|
| 1140 |
+
# # # policy_confidence=result['policy_confidence'],
|
| 1141 |
+
# # # documents_retrieved=result['documents_retrieved'],
|
| 1142 |
+
# # # top_doc_score=result['top_doc_score'],
|
| 1143 |
+
# # # total_time_ms=result['total_time_ms'],
|
| 1144 |
+
# # # timestamp=result['timestamp']
|
| 1145 |
+
# # # )
|
| 1146 |
+
|
| 1147 |
+
# # # except Exception as e:
|
| 1148 |
+
# # # print(f"❌ Chat endpoint error: {e}")
|
| 1149 |
+
# # # raise HTTPException(
|
| 1150 |
+
# # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 1151 |
+
# # # detail=f"Failed to process chat request: {str(e)}"
|
| 1152 |
+
# # # )
|
| 1153 |
+
|
| 1154 |
+
|
| 1155 |
+
# # # @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED)
|
| 1156 |
+
# # # async def create_conversation(request: ConversationCreateRequest):
|
| 1157 |
+
# # # """
|
| 1158 |
+
# # # Create a new conversation.
|
| 1159 |
+
|
| 1160 |
+
# # # Args:
|
| 1161 |
+
# # # request: ConversationCreateRequest with user_id
|
| 1162 |
+
|
| 1163 |
+
# # # Returns:
|
| 1164 |
+
# # # ConversationCreateResponse: Created conversation ID
|
| 1165 |
+
# # # """
|
| 1166 |
+
# # # try:
|
| 1167 |
+
# # # conversation_id = await conversation_repo.create_conversation(
|
| 1168 |
+
# # # user_id=request.user_id
|
| 1169 |
+
# # # )
|
| 1170 |
+
|
| 1171 |
+
# # # return ConversationCreateResponse(
|
| 1172 |
+
# # # conversation_id=conversation_id,
|
| 1173 |
+
# # # created_at=datetime.now().isoformat()
|
| 1174 |
+
# # # )
|
| 1175 |
+
|
| 1176 |
+
# # # except Exception as e:
|
| 1177 |
+
# # # raise HTTPException(
|
| 1178 |
+
# # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 1179 |
+
# # # detail=f"Failed to create conversation: {str(e)}"
|
| 1180 |
+
# # # )
|
| 1181 |
+
|
| 1182 |
+
|
| 1183 |
+
# # # @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse)
|
| 1184 |
+
# # # async def get_conversation_history(conversation_id: str):
|
| 1185 |
+
# # # """
|
| 1186 |
+
# # # Get conversation history by ID.
|
| 1187 |
+
|
| 1188 |
+
# # # Args:
|
| 1189 |
+
# # # conversation_id: Conversation ID
|
| 1190 |
+
|
| 1191 |
+
# # # Returns:
|
| 1192 |
+
# # # ConversationHistoryResponse: List of messages
|
| 1193 |
+
# # # """
|
| 1194 |
+
# # # try:
|
| 1195 |
+
# # # # Get conversation
|
| 1196 |
+
# # # conversation = await conversation_repo.get_conversation(conversation_id)
|
| 1197 |
+
|
| 1198 |
+
# # # if not conversation:
|
| 1199 |
+
# # # raise HTTPException(
|
| 1200 |
+
# # # status_code=status.HTTP_404_NOT_FOUND,
|
| 1201 |
+
# # # detail=f"Conversation {conversation_id} not found"
|
| 1202 |
+
# # # )
|
| 1203 |
+
|
| 1204 |
+
# # # # Format messages
|
| 1205 |
+
# # # messages = []
|
| 1206 |
+
# # # for msg in conversation.get('messages', []):
|
| 1207 |
+
# # # messages.append(MessageModel(
|
| 1208 |
+
# # # role=msg['role'],
|
| 1209 |
+
# # # content=msg['content'],
|
| 1210 |
+
# # # timestamp=msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'],
|
| 1211 |
+
# # # metadata=msg.get('metadata')
|
| 1212 |
+
# # # ))
|
| 1213 |
+
|
| 1214 |
+
# # # return ConversationHistoryResponse(
|
| 1215 |
+
# # # conversation_id=conversation_id,
|
| 1216 |
+
# # # messages=messages,
|
| 1217 |
+
# # # message_count=len(messages)
|
| 1218 |
+
# # # )
|
| 1219 |
+
|
| 1220 |
+
# # # except HTTPException:
|
| 1221 |
+
# # # raise
|
| 1222 |
+
# # # except Exception as e:
|
| 1223 |
+
# # # raise HTTPException(
|
| 1224 |
+
# # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 1225 |
+
# # # detail=f"Failed to fetch conversation history: {str(e)}"
|
| 1226 |
+
# # # )
|
| 1227 |
+
|
| 1228 |
+
|
| 1229 |
+
# # # @router.get("/conversations")
|
| 1230 |
+
# # # async def list_user_conversations(user_id: str, limit: int = 10, skip: int = 0):
|
| 1231 |
+
# # # """
|
| 1232 |
+
# # # List all conversations for a user.
|
| 1233 |
+
|
| 1234 |
+
# # # Args:
|
| 1235 |
+
# # # user_id: User ID
|
| 1236 |
+
# # # limit: Maximum conversations to return (default: 10)
|
| 1237 |
+
# # # skip: Number to skip for pagination (default: 0)
|
| 1238 |
+
|
| 1239 |
+
# # # Returns:
|
| 1240 |
+
# # # dict: List of conversations
|
| 1241 |
+
# # # """
|
| 1242 |
+
# # # try:
|
| 1243 |
+
# # # conversations = await conversation_repo.get_user_conversations(
|
| 1244 |
+
# # # user_id=user_id,
|
| 1245 |
+
# # # limit=limit,
|
| 1246 |
+
# # # skip=skip
|
| 1247 |
+
# # # )
|
| 1248 |
+
|
| 1249 |
+
# # # # Format response
|
| 1250 |
+
# # # return {
|
| 1251 |
+
# # # "user_id": user_id,
|
| 1252 |
+
# # # "conversations": [
|
| 1253 |
+
# # # {
|
| 1254 |
+
# # # "conversation_id": conv['conversation_id'],
|
| 1255 |
+
# # # "created_at": conv['created_at'].isoformat() if isinstance(conv['created_at'], datetime) else conv['created_at'],
|
| 1256 |
+
# # # "updated_at": conv['updated_at'].isoformat() if isinstance(conv['updated_at'], datetime) else conv['updated_at'],
|
| 1257 |
+
# # # "message_count": len(conv.get('messages', []))
|
| 1258 |
+
# # # }
|
| 1259 |
+
# # # for conv in conversations
|
| 1260 |
+
# # # ],
|
| 1261 |
+
# # # "total": len(conversations)
|
| 1262 |
+
# # # }
|
| 1263 |
+
|
| 1264 |
+
# # # except Exception as e:
|
| 1265 |
+
# # # raise HTTPException(
|
| 1266 |
+
# # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 1267 |
+
# # # detail=f"Failed to fetch conversations: {str(e)}"
|
| 1268 |
+
# # # )
|
| 1269 |
+
|
| 1270 |
+
|
| 1271 |
+
# # # @router.get("/health")
|
| 1272 |
+
# # # async def chat_health():
|
| 1273 |
+
# # # """
|
| 1274 |
+
# # # Health check for chat service.
|
| 1275 |
+
|
| 1276 |
+
# # # Returns:
|
| 1277 |
+
# # # dict: Health status of chat service components
|
| 1278 |
+
# # # """
|
| 1279 |
+
# # # try:
|
| 1280 |
+
# # # health = await chat_service.health_check()
|
| 1281 |
+
|
| 1282 |
+
# # # return {
|
| 1283 |
+
# # # "status": "healthy",
|
| 1284 |
+
# # # "service": "chat",
|
| 1285 |
+
# # # "components": health['components'],
|
| 1286 |
+
# # # "timestamp": datetime.now().isoformat()
|
| 1287 |
+
# # # }
|
| 1288 |
+
|
| 1289 |
+
# # # except Exception as e:
|
| 1290 |
+
# # # return {
|
| 1291 |
+
# # # "status": "unhealthy",
|
| 1292 |
+
# # # "service": "chat",
|
| 1293 |
+
# # # "error": str(e),
|
| 1294 |
+
# # # "timestamp": datetime.now().isoformat()
|
| 1295 |
+
# # # }
|
| 1296 |
+
|
| 1297 |
+
|
| 1298 |
+
# # # # ============================================================================
|
| 1299 |
+
# # # # USAGE DOCUMENTATION
|
| 1300 |
+
# # # # ============================================================================
|
| 1301 |
+
# # # """
|
| 1302 |
+
# # # === API USAGE EXAMPLES ===
|
| 1303 |
+
|
| 1304 |
+
# # # 1. Send a chat message:
|
| 1305 |
+
# # # POST /api/v1/chat/
|
| 1306 |
+
# # # Body: {
|
| 1307 |
+
# # # "query": "What is my account balance?",
|
| 1308 |
+
# # # "user_id": "user_123",
|
| 1309 |
+
# # # "conversation_id": "conv_abc" // optional
|
| 1310 |
+
# # # }
|
| 1311 |
+
|
| 1312 |
+
# # # 2. Create new conversation:
|
| 1313 |
+
# # # POST /api/v1/chat/conversation
|
| 1314 |
+
# # # Body: {
|
| 1315 |
+
# # # "user_id": "user_123"
|
| 1316 |
+
# # # }
|
| 1317 |
+
|
| 1318 |
+
# # # 3. Get conversation history:
|
| 1319 |
+
# # # GET /api/v1/chat/history/conv_abc
|
| 1320 |
+
|
| 1321 |
+
# # # 4. List user's conversations:
|
| 1322 |
+
# # # GET /api/v1/chat/conversations?user_id=user_123&limit=10&skip=0
|
| 1323 |
+
|
| 1324 |
+
# # # 5. Check health:
|
| 1325 |
+
# # # GET /api/v1/chat/health
|
| 1326 |
+
|
| 1327 |
+
# # # === TESTING WITH CURL ===
|
| 1328 |
+
|
| 1329 |
+
# # # # Send chat message
|
| 1330 |
+
# # # curl -X POST "http://localhost:8000/api/v1/chat/" \
|
| 1331 |
+
# # # -H "Content-Type: application/json" \
|
| 1332 |
+
# # # -d '{
|
| 1333 |
+
# # # "query": "What is my balance?",
|
| 1334 |
+
# # # "user_id": "user_123"
|
| 1335 |
+
# # # }'
|
| 1336 |
+
|
| 1337 |
+
# # # # Get history
|
| 1338 |
+
# # # curl "http://localhost:8000/api/v1/chat/history/conv_123"
|
| 1339 |
+
|
| 1340 |
+
# # # === TESTING WITH SWAGGER UI ===
|
| 1341 |
+
|
| 1342 |
+
# # # After starting the server, visit:
|
| 1343 |
+
# # # http://localhost:8000/docs
|
| 1344 |
+
|
| 1345 |
+
# # # Interactive API documentation with "Try it out" buttons!
|
| 1346 |
+
# # # """
|
app/api/v1/conversation_routes.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Conversation & Chat API Endpoints (UNIFIED)
|
| 3 |
+
|
| 4 |
+
Combines:
|
| 5 |
+
- Chat functionality (send message, get response)
|
| 6 |
+
- Conversation management (list, search, rename, archive, delete)
|
| 7 |
+
|
| 8 |
+
All endpoints require JWT authentication.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from fastapi import APIRouter, HTTPException, status, Depends
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
from typing import List, Dict, Optional
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
from app.services.chat_service import chat_service
|
| 17 |
+
from app.services.conversation_service import conversation_service
|
| 18 |
+
from app.db.repositories.conversation_repository import conversation_repository
|
| 19 |
+
from app.utils.dependencies import get_current_user
|
| 20 |
+
from app.models.user import TokenData
|
| 21 |
+
from app.models.conversation import (
|
| 22 |
+
CreateConversationRequest,
|
| 23 |
+
UpdateConversationRequest,
|
| 24 |
+
ConversationResponse,
|
| 25 |
+
ConversationListResult,
|
| 26 |
+
Message
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ============================================================================
|
| 31 |
+
# CREATE ROUTER
|
| 32 |
+
# ============================================================================
|
| 33 |
+
router = APIRouter()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ============================================================================
|
| 37 |
+
# REQUEST/RESPONSE MODELS
|
| 38 |
+
# ============================================================================
|
| 39 |
+
|
| 40 |
+
class ChatRequest(BaseModel):
|
| 41 |
+
"""Request for chat endpoint"""
|
| 42 |
+
query: str = Field(..., min_length=1, max_length=2000)
|
| 43 |
+
conversation_id: Optional[str] = Field(None, description="Optional conversation ID. If not provided, creates new conversation.")
|
| 44 |
+
|
| 45 |
+
class Config:
|
| 46 |
+
schema_extra = {
|
| 47 |
+
"example": {
|
| 48 |
+
"query": "What is my account balance?",
|
| 49 |
+
"conversation_id": "507f1f77bcf86cd799439011"
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ChatResponse(BaseModel):
|
| 55 |
+
"""Response from chat endpoint"""
|
| 56 |
+
response: str
|
| 57 |
+
conversation_id: str
|
| 58 |
+
policy_action: str
|
| 59 |
+
policy_confidence: float
|
| 60 |
+
documents_retrieved: int
|
| 61 |
+
top_doc_score: Optional[float]
|
| 62 |
+
total_time_ms: float
|
| 63 |
+
timestamp: str
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ============================================================================
|
| 67 |
+
# CHAT ENDPOINTS
|
| 68 |
+
# ============================================================================
|
| 69 |
+
|
| 70 |
+
@router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK)
|
| 71 |
+
async def chat(
|
| 72 |
+
request: ChatRequest,
|
| 73 |
+
current_user: TokenData = Depends(get_current_user)
|
| 74 |
+
):
|
| 75 |
+
"""
|
| 76 |
+
💬 Send a message and get AI response.
|
| 77 |
+
|
| 78 |
+
**Main chat endpoint** - processes user query through RAG pipeline.
|
| 79 |
+
|
| 80 |
+
- If conversation_id provided: Adds to existing conversation
|
| 81 |
+
- If no conversation_id: Creates new conversation with auto-generated title
|
| 82 |
+
|
| 83 |
+
Requires JWT authentication.
|
| 84 |
+
"""
|
| 85 |
+
try:
|
| 86 |
+
user_id = current_user.user_id
|
| 87 |
+
|
| 88 |
+
# ====================================================================
|
| 89 |
+
# STEP 1: Get or Create Conversation
|
| 90 |
+
# ====================================================================
|
| 91 |
+
conversation_id = request.conversation_id
|
| 92 |
+
|
| 93 |
+
if conversation_id:
|
| 94 |
+
# Verify conversation exists and user owns it
|
| 95 |
+
conversation = await conversation_repository.get_conversation(conversation_id)
|
| 96 |
+
|
| 97 |
+
if not conversation:
|
| 98 |
+
raise HTTPException(
|
| 99 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 100 |
+
detail="Conversation not found"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
if conversation["user_id"] != user_id:
|
| 104 |
+
raise HTTPException(
|
| 105 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 106 |
+
detail="Access denied - you don't own this conversation"
|
| 107 |
+
)
|
| 108 |
+
else:
|
| 109 |
+
# Create new conversation (auto-title will be generated after first response)
|
| 110 |
+
from app.models.conversation import CreateConversationRequest
|
| 111 |
+
create_req = CreateConversationRequest(
|
| 112 |
+
title=None, # Will be auto-generated
|
| 113 |
+
first_message=request.query
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
new_conversation = await conversation_service.create_conversation(
|
| 117 |
+
user_id=user_id,
|
| 118 |
+
request=create_req,
|
| 119 |
+
llm_manager=None # Can pass llm_manager for smart titles
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
conversation_id = str(new_conversation.id)
|
| 123 |
+
|
| 124 |
+
# ====================================================================
|
| 125 |
+
# STEP 2: Get Conversation History
|
| 126 |
+
# ====================================================================
|
| 127 |
+
history = await conversation_repository.get_conversation_history(
|
| 128 |
+
conversation_id=conversation_id,
|
| 129 |
+
max_messages=10
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# ====================================================================
|
| 133 |
+
# STEP 3: Save User Message
|
| 134 |
+
# ====================================================================
|
| 135 |
+
await conversation_repository.add_message(
|
| 136 |
+
conversation_id=conversation_id,
|
| 137 |
+
message={
|
| 138 |
+
'role': 'user',
|
| 139 |
+
'content': request.query,
|
| 140 |
+
'timestamp': datetime.utcnow(),
|
| 141 |
+
'metadata': None
|
| 142 |
+
}
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# ====================================================================
|
| 146 |
+
# STEP 4: Process Query (RAG Pipeline)
|
| 147 |
+
# ====================================================================
|
| 148 |
+
result = await chat_service.process_query(
|
| 149 |
+
query=request.query,
|
| 150 |
+
conversation_history=history,
|
| 151 |
+
user_id=user_id
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# ====================================================================
|
| 155 |
+
# STEP 5: Save Assistant Response
|
| 156 |
+
# ====================================================================
|
| 157 |
+
await conversation_repository.add_message(
|
| 158 |
+
conversation_id=conversation_id,
|
| 159 |
+
message={
|
| 160 |
+
'role': 'assistant',
|
| 161 |
+
'content': result['response'],
|
| 162 |
+
'timestamp': datetime.utcnow(),
|
| 163 |
+
'metadata': {
|
| 164 |
+
'policy_action': result['policy_action'],
|
| 165 |
+
'policy_confidence': result['policy_confidence'],
|
| 166 |
+
'documents_retrieved': result['documents_retrieved'],
|
| 167 |
+
'top_doc_score': result['top_doc_score'],
|
| 168 |
+
'retrieval_time_ms': result['retrieval_time_ms'],
|
| 169 |
+
'generation_time_ms': result['generation_time_ms']
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
# ====================================================================
|
| 175 |
+
# STEP 6: Log Retrieval Data (for RL training)
|
| 176 |
+
# ====================================================================
|
| 177 |
+
await conversation_repository.log_retrieval({
|
| 178 |
+
'conversation_id': conversation_id,
|
| 179 |
+
'user_id': user_id,
|
| 180 |
+
'query': request.query,
|
| 181 |
+
'policy_action': result['policy_action'],
|
| 182 |
+
'policy_confidence': result['policy_confidence'],
|
| 183 |
+
'should_retrieve': result['should_retrieve'],
|
| 184 |
+
'documents_retrieved': result['documents_retrieved'],
|
| 185 |
+
'top_doc_score': result['top_doc_score'],
|
| 186 |
+
'response': result['response'],
|
| 187 |
+
'retrieval_time_ms': result['retrieval_time_ms'],
|
| 188 |
+
'generation_time_ms': result['generation_time_ms'],
|
| 189 |
+
'total_time_ms': result['total_time_ms'],
|
| 190 |
+
'retrieved_docs_metadata': result.get('retrieved_docs_metadata', []),
|
| 191 |
+
'timestamp': datetime.utcnow()
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
# ====================================================================
|
| 195 |
+
# STEP 7: Return Response
|
| 196 |
+
# ====================================================================
|
| 197 |
+
return ChatResponse(
|
| 198 |
+
response=result['response'],
|
| 199 |
+
conversation_id=conversation_id,
|
| 200 |
+
policy_action=result['policy_action'],
|
| 201 |
+
policy_confidence=result['policy_confidence'],
|
| 202 |
+
documents_retrieved=result['documents_retrieved'],
|
| 203 |
+
top_doc_score=result['top_doc_score'],
|
| 204 |
+
total_time_ms=result['total_time_ms'],
|
| 205 |
+
timestamp=result['timestamp']
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
except HTTPException:
|
| 209 |
+
raise
|
| 210 |
+
except Exception as e:
|
| 211 |
+
print(f"❌ Chat endpoint error: {e}")
|
| 212 |
+
import traceback
|
| 213 |
+
traceback.print_exc()
|
| 214 |
+
raise HTTPException(
|
| 215 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 216 |
+
detail=f"Failed to process chat request: {str(e)}"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# ============================================================================
|
| 221 |
+
# CONVERSATION MANAGEMENT ENDPOINTS
|
| 222 |
+
# ============================================================================
|
| 223 |
+
|
| 224 |
+
@router.post("/conversation", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED)
|
| 225 |
+
async def create_conversation(
|
| 226 |
+
request: CreateConversationRequest = CreateConversationRequest(),
|
| 227 |
+
current_user: TokenData = Depends(get_current_user)
|
| 228 |
+
):
|
| 229 |
+
"""
|
| 230 |
+
🆕 Create a new conversation.
|
| 231 |
+
|
| 232 |
+
Optional parameters:
|
| 233 |
+
- title: Custom title (if not provided, auto-generated)
|
| 234 |
+
- first_message: Optional first message to start conversation
|
| 235 |
+
|
| 236 |
+
Returns full conversation object.
|
| 237 |
+
"""
|
| 238 |
+
try:
|
| 239 |
+
conversation = await conversation_service.create_conversation(
|
| 240 |
+
user_id=current_user.user_id,
|
| 241 |
+
request=request,
|
| 242 |
+
llm_manager=None
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
return ConversationResponse(
|
| 246 |
+
id=str(conversation.id),
|
| 247 |
+
user_id=conversation.user_id,
|
| 248 |
+
title=conversation.title,
|
| 249 |
+
messages=conversation.messages,
|
| 250 |
+
is_archived=conversation.is_archived,
|
| 251 |
+
created_at=conversation.created_at,
|
| 252 |
+
updated_at=conversation.updated_at,
|
| 253 |
+
last_message_at=conversation.last_message_at,
|
| 254 |
+
message_count=conversation.message_count
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
except Exception as e:
|
| 258 |
+
raise HTTPException(
|
| 259 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 260 |
+
detail=f"Failed to create conversation: {str(e)}"
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
@router.get("/conversations", response_model=ConversationListResult)
|
| 265 |
+
async def list_conversations(
|
| 266 |
+
page: int = 1,
|
| 267 |
+
page_size: int = 20,
|
| 268 |
+
include_archived: bool = False,
|
| 269 |
+
current_user: TokenData = Depends(get_current_user)
|
| 270 |
+
):
|
| 271 |
+
"""
|
| 272 |
+
📋 List all conversations for authenticated user.
|
| 273 |
+
|
| 274 |
+
Supports:
|
| 275 |
+
- Pagination (page, page_size)
|
| 276 |
+
- Filter archived conversations
|
| 277 |
+
- Sorted by last message (newest first)
|
| 278 |
+
|
| 279 |
+
Returns lightweight list (without full message history).
|
| 280 |
+
"""
|
| 281 |
+
try:
|
| 282 |
+
result = await conversation_service.list_conversations(
|
| 283 |
+
user_id=current_user.user_id,
|
| 284 |
+
page=page,
|
| 285 |
+
page_size=page_size,
|
| 286 |
+
include_archived=include_archived
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
return result
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
raise HTTPException(
|
| 293 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 294 |
+
detail=f"Failed to list conversations: {str(e)}"
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
@router.get("/conversation/{conversation_id}", response_model=ConversationResponse)
|
| 299 |
+
async def get_conversation(
|
| 300 |
+
conversation_id: str,
|
| 301 |
+
current_user: TokenData = Depends(get_current_user)
|
| 302 |
+
):
|
| 303 |
+
"""
|
| 304 |
+
🔍 Get full conversation by ID.
|
| 305 |
+
|
| 306 |
+
Returns complete conversation with all messages.
|
| 307 |
+
User must own the conversation.
|
| 308 |
+
"""
|
| 309 |
+
try:
|
| 310 |
+
conversation = await conversation_service.get_conversation(
|
| 311 |
+
conversation_id=conversation_id,
|
| 312 |
+
user_id=current_user.user_id
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
if not conversation:
|
| 316 |
+
raise HTTPException(
|
| 317 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 318 |
+
detail="Conversation not found or access denied"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
return ConversationResponse(
|
| 322 |
+
id=str(conversation.id),
|
| 323 |
+
user_id=conversation.user_id,
|
| 324 |
+
title=conversation.title,
|
| 325 |
+
messages=conversation.messages,
|
| 326 |
+
is_archived=conversation.is_archived,
|
| 327 |
+
created_at=conversation.created_at,
|
| 328 |
+
updated_at=conversation.updated_at,
|
| 329 |
+
last_message_at=conversation.last_message_at,
|
| 330 |
+
message_count=conversation.message_count
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
except HTTPException:
|
| 334 |
+
raise
|
| 335 |
+
except Exception as e:
|
| 336 |
+
raise HTTPException(
|
| 337 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 338 |
+
detail=f"Failed to get conversation: {str(e)}"
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
@router.patch("/conversation/{conversation_id}", response_model=ConversationResponse)
|
| 343 |
+
async def update_conversation(
|
| 344 |
+
conversation_id: str,
|
| 345 |
+
request: UpdateConversationRequest,
|
| 346 |
+
current_user: TokenData = Depends(get_current_user)
|
| 347 |
+
):
|
| 348 |
+
"""
|
| 349 |
+
✏️ Update conversation properties.
|
| 350 |
+
|
| 351 |
+
Can update:
|
| 352 |
+
- title: Rename conversation
|
| 353 |
+
- is_archived: Archive/unarchive
|
| 354 |
+
|
| 355 |
+
User must own the conversation.
|
| 356 |
+
"""
|
| 357 |
+
try:
|
| 358 |
+
conversation = await conversation_service.update_conversation(
|
| 359 |
+
conversation_id=conversation_id,
|
| 360 |
+
user_id=current_user.user_id,
|
| 361 |
+
request=request
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
if not conversation:
|
| 365 |
+
raise HTTPException(
|
| 366 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 367 |
+
detail="Conversation not found or access denied"
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
return ConversationResponse(
|
| 371 |
+
id=str(conversation.id),
|
| 372 |
+
user_id=conversation.user_id,
|
| 373 |
+
title=conversation.title,
|
| 374 |
+
messages=conversation.messages,
|
| 375 |
+
is_archived=conversation.is_archived,
|
| 376 |
+
created_at=conversation.created_at,
|
| 377 |
+
updated_at=conversation.updated_at,
|
| 378 |
+
last_message_at=conversation.last_message_at,
|
| 379 |
+
message_count=conversation.message_count
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
except HTTPException:
|
| 383 |
+
raise
|
| 384 |
+
except ValueError as e:
|
| 385 |
+
raise HTTPException(
|
| 386 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 387 |
+
detail=str(e)
|
| 388 |
+
)
|
| 389 |
+
except Exception as e:
|
| 390 |
+
raise HTTPException(
|
| 391 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 392 |
+
detail=f"Failed to update conversation: {str(e)}"
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
@router.delete("/conversation/{conversation_id}")
|
| 397 |
+
async def delete_conversation(
|
| 398 |
+
conversation_id: str,
|
| 399 |
+
permanent: bool = False,
|
| 400 |
+
current_user: TokenData = Depends(get_current_user)
|
| 401 |
+
):
|
| 402 |
+
"""
|
| 403 |
+
🗑️ Delete a conversation.
|
| 404 |
+
|
| 405 |
+
- Default (permanent=False): Soft delete (can be recovered)
|
| 406 |
+
- permanent=True: Hard delete (cannot be recovered)
|
| 407 |
+
|
| 408 |
+
User must own the conversation.
|
| 409 |
+
"""
|
| 410 |
+
try:
|
| 411 |
+
success = await conversation_service.delete_conversation(
|
| 412 |
+
conversation_id=conversation_id,
|
| 413 |
+
user_id=current_user.user_id,
|
| 414 |
+
permanent=permanent
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
if not success:
|
| 418 |
+
raise HTTPException(
|
| 419 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 420 |
+
detail="Conversation not found or access denied"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
return {
|
| 424 |
+
"message": "Conversation deleted successfully",
|
| 425 |
+
"conversation_id": conversation_id,
|
| 426 |
+
"permanent": permanent
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
except HTTPException:
|
| 430 |
+
raise
|
| 431 |
+
except Exception as e:
|
| 432 |
+
raise HTTPException(
|
| 433 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 434 |
+
detail=f"Failed to delete conversation: {str(e)}"
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
@router.get("/conversations/search", response_model=ConversationListResult)
|
| 439 |
+
async def search_conversations(
|
| 440 |
+
query: str,
|
| 441 |
+
page: int = 1,
|
| 442 |
+
page_size: int = 20,
|
| 443 |
+
current_user: TokenData = Depends(get_current_user)
|
| 444 |
+
):
|
| 445 |
+
"""
|
| 446 |
+
🔎 Search conversations by title or message content.
|
| 447 |
+
|
| 448 |
+
Searches in:
|
| 449 |
+
- Conversation titles
|
| 450 |
+
- Message content
|
| 451 |
+
|
| 452 |
+
Returns paginated results.
|
| 453 |
+
"""
|
| 454 |
+
try:
|
| 455 |
+
if not query or len(query.strip()) < 2:
|
| 456 |
+
raise HTTPException(
|
| 457 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 458 |
+
detail="Search query must be at least 2 characters"
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
result = await conversation_service.search_conversations(
|
| 462 |
+
user_id=current_user.user_id,
|
| 463 |
+
query=query,
|
| 464 |
+
page=page,
|
| 465 |
+
page_size=page_size
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
return result
|
| 469 |
+
|
| 470 |
+
except HTTPException:
|
| 471 |
+
raise
|
| 472 |
+
except Exception as e:
|
| 473 |
+
raise HTTPException(
|
| 474 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 475 |
+
detail=f"Failed to search conversations: {str(e)}"
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
@router.get("/conversations/stats")
|
| 480 |
+
async def get_conversation_stats(
|
| 481 |
+
current_user: TokenData = Depends(get_current_user)
|
| 482 |
+
):
|
| 483 |
+
"""
|
| 484 |
+
📊 Get conversation statistics for user.
|
| 485 |
+
|
| 486 |
+
Returns:
|
| 487 |
+
- total: Total conversations
|
| 488 |
+
- active: Non-archived conversations
|
| 489 |
+
- archived: Archived conversations
|
| 490 |
+
"""
|
| 491 |
+
try:
|
| 492 |
+
stats = await conversation_service.get_conversation_stats(
|
| 493 |
+
user_id=current_user.user_id
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
return {
|
| 497 |
+
"user_id": current_user.user_id,
|
| 498 |
+
"stats": stats
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
except Exception as e:
|
| 502 |
+
raise HTTPException(
|
| 503 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 504 |
+
detail=f"Failed to get stats: {str(e)}"
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
# ============================================================================
|
| 509 |
+
# HEALTH CHECK
|
| 510 |
+
# ============================================================================
|
| 511 |
+
|
| 512 |
+
@router.get("/health")
|
| 513 |
+
async def chat_health():
|
| 514 |
+
"""
|
| 515 |
+
🏥 Health check for chat & conversation service.
|
| 516 |
+
|
| 517 |
+
Public endpoint (no auth required).
|
| 518 |
+
"""
|
| 519 |
+
try:
|
| 520 |
+
health = await chat_service.health_check()
|
| 521 |
+
|
| 522 |
+
return {
|
| 523 |
+
"status": "healthy",
|
| 524 |
+
"service": "chat & conversations",
|
| 525 |
+
"components": health.get('components', {}),
|
| 526 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
except Exception as e:
|
| 530 |
+
return {
|
| 531 |
+
"status": "unhealthy",
|
| 532 |
+
"service": "chat & conversations",
|
| 533 |
+
"error": str(e),
|
| 534 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 535 |
+
}
|
app/db/repositories/conversation_repository.py
CHANGED
|
@@ -1,9 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# """
|
| 2 |
# Conversation Repository - MongoDB CRUD operations
|
| 3 |
# Handles storing and retrieving conversations from MongoDB Atlas
|
| 4 |
|
| 5 |
# Repository Pattern: Separates database logic from business logic
|
| 6 |
# This makes code cleaner and easier to test
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# """
|
| 8 |
|
| 9 |
# import uuid
|
|
@@ -22,16 +613,42 @@
|
|
| 22 |
# """
|
| 23 |
# Repository for conversation data in MongoDB.
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
#
|
| 27 |
-
#
|
| 28 |
# """
|
| 29 |
|
| 30 |
# def __init__(self):
|
| 31 |
-
# """
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
# self.db = get_database()
|
| 33 |
-
|
| 34 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
# # ========================================================================
|
| 37 |
# # CONVERSATION CRUD OPERATIONS
|
|
@@ -51,7 +668,12 @@
|
|
| 51 |
|
| 52 |
# Returns:
|
| 53 |
# str: Conversation ID
|
|
|
|
|
|
|
|
|
|
| 54 |
# """
|
|
|
|
|
|
|
| 55 |
# if conversation_id is None:
|
| 56 |
# conversation_id = str(uuid.uuid4())
|
| 57 |
|
|
@@ -77,7 +699,12 @@
|
|
| 77 |
|
| 78 |
# Returns:
|
| 79 |
# dict or None: Conversation document
|
|
|
|
|
|
|
|
|
|
| 80 |
# """
|
|
|
|
|
|
|
| 81 |
# conversation = await self.conversations.find_one(
|
| 82 |
# {"conversation_id": conversation_id}
|
| 83 |
# )
|
|
@@ -88,35 +715,65 @@
|
|
| 88 |
|
| 89 |
# return conversation
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
# async def get_user_conversations(
|
| 92 |
# self,
|
| 93 |
# user_id: str,
|
| 94 |
# limit: int = 10,
|
| 95 |
# skip: int = 0
|
| 96 |
# ) -> List[Dict]:
|
| 97 |
-
# """
|
| 98 |
-
#
|
| 99 |
-
|
| 100 |
-
#
|
| 101 |
-
#
|
| 102 |
-
|
| 103 |
-
# skip: Number of conversations to skip (for pagination)
|
| 104 |
-
|
| 105 |
-
# Returns:
|
| 106 |
-
# list: List of conversation documents
|
| 107 |
-
# """
|
| 108 |
# cursor = self.conversations.find(
|
| 109 |
# {"user_id": user_id, "status": "active"}
|
| 110 |
# ).sort("updated_at", -1).skip(skip).limit(limit)
|
| 111 |
-
|
| 112 |
# conversations = await cursor.to_list(length=limit)
|
| 113 |
-
|
| 114 |
# # Convert ObjectIds to strings
|
| 115 |
# for conv in conversations:
|
| 116 |
# if "_id" in conv:
|
| 117 |
# conv["_id"] = str(conv["_id"])
|
| 118 |
-
|
| 119 |
# return conversations
|
|
|
|
| 120 |
|
| 121 |
# async def add_message(
|
| 122 |
# self,
|
|
@@ -138,7 +795,12 @@
|
|
| 138 |
|
| 139 |
# Returns:
|
| 140 |
# bool: Success status
|
|
|
|
|
|
|
|
|
|
| 141 |
# """
|
|
|
|
|
|
|
| 142 |
# # Ensure timestamp exists
|
| 143 |
# if "timestamp" not in message:
|
| 144 |
# message["timestamp"] = datetime.now()
|
|
@@ -168,7 +830,12 @@
|
|
| 168 |
|
| 169 |
# Returns:
|
| 170 |
# list: List of messages
|
|
|
|
|
|
|
|
|
|
| 171 |
# """
|
|
|
|
|
|
|
| 172 |
# conversation = await self.get_conversation(conversation_id)
|
| 173 |
|
| 174 |
# if not conversation:
|
|
@@ -190,7 +857,12 @@
|
|
| 190 |
|
| 191 |
# Returns:
|
| 192 |
# bool: Success status
|
|
|
|
|
|
|
|
|
|
| 193 |
# """
|
|
|
|
|
|
|
| 194 |
# result = await self.conversations.update_one(
|
| 195 |
# {"conversation_id": conversation_id},
|
| 196 |
# {
|
|
@@ -234,7 +906,12 @@
|
|
| 234 |
|
| 235 |
# Returns:
|
| 236 |
# str: Log ID
|
|
|
|
|
|
|
|
|
|
| 237 |
# """
|
|
|
|
|
|
|
| 238 |
# # Add timestamp if not present
|
| 239 |
# if "timestamp" not in log_data:
|
| 240 |
# log_data["timestamp"] = datetime.now()
|
|
@@ -266,7 +943,12 @@
|
|
| 266 |
|
| 267 |
# Returns:
|
| 268 |
# list: List of log documents
|
|
|
|
|
|
|
|
|
|
| 269 |
# """
|
|
|
|
|
|
|
| 270 |
# # Build query
|
| 271 |
# query = {}
|
| 272 |
# if conversation_id:
|
|
@@ -300,7 +982,12 @@
|
|
| 300 |
|
| 301 |
# Returns:
|
| 302 |
# list: List of log documents suitable for RL training
|
|
|
|
|
|
|
|
|
|
| 303 |
# """
|
|
|
|
|
|
|
| 304 |
# # Build query
|
| 305 |
# query = {
|
| 306 |
# "policy_action": {"$exists": True},
|
|
@@ -334,7 +1021,12 @@
|
|
| 334 |
|
| 335 |
# Returns:
|
| 336 |
# dict: Statistics
|
|
|
|
|
|
|
|
|
|
| 337 |
# """
|
|
|
|
|
|
|
| 338 |
# # Count total conversations
|
| 339 |
# total_conversations = await self.conversations.count_documents({
|
| 340 |
# "user_id": user_id,
|
|
@@ -365,7 +1057,12 @@
|
|
| 365 |
|
| 366 |
# Returns:
|
| 367 |
# dict: Policy statistics
|
|
|
|
|
|
|
|
|
|
| 368 |
# """
|
|
|
|
|
|
|
| 369 |
# # Build query
|
| 370 |
# query = {}
|
| 371 |
# if user_id:
|
|
@@ -444,606 +1141,442 @@
|
|
| 444 |
|
| 445 |
|
| 446 |
|
|
|
|
|
|
|
|
|
|
| 447 |
|
|
|
|
|
|
|
|
|
|
| 448 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
|
|
|
| 450 |
|
| 451 |
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
"""
|
| 502 |
-
Conversation Repository - MongoDB CRUD operations
|
| 503 |
-
Handles storing and retrieving conversations from MongoDB Atlas
|
| 504 |
-
|
| 505 |
-
Repository Pattern: Separates database logic from business logic
|
| 506 |
-
This makes code cleaner and easier to test
|
| 507 |
-
|
| 508 |
-
Collections:
|
| 509 |
-
- conversations: Stores complete conversations with messages
|
| 510 |
-
- retrieval_logs: Logs each retrieval operation (for RL training data)
|
| 511 |
-
"""
|
| 512 |
-
|
| 513 |
-
import uuid
|
| 514 |
-
from datetime import datetime
|
| 515 |
-
from typing import List, Dict, Optional
|
| 516 |
-
from bson import ObjectId
|
| 517 |
-
|
| 518 |
-
from app.db.mongodb import get_database
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
# ============================================================================
|
| 522 |
-
# CONVERSATION REPOSITORY
|
| 523 |
-
# ============================================================================
|
| 524 |
-
|
| 525 |
-
class ConversationRepository:
|
| 526 |
-
"""
|
| 527 |
-
Repository for conversation data in MongoDB.
|
| 528 |
-
|
| 529 |
-
Provides CRUD operations for:
|
| 530 |
-
1. Conversations (user chat sessions)
|
| 531 |
-
2. Retrieval logs (for RL training and analytics)
|
| 532 |
-
"""
|
| 533 |
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
Gracefully handles case where MongoDB is not connected.
|
| 539 |
-
"""
|
| 540 |
-
self.db = get_database()
|
| 541 |
-
|
| 542 |
-
# Graceful handling if MongoDB not connected
|
| 543 |
-
if self.db is None:
|
| 544 |
-
print("⚠️ ConversationRepository: MongoDB not connected")
|
| 545 |
-
print(" Repository will not function until database is connected")
|
| 546 |
-
self.conversations = None
|
| 547 |
-
self.retrieval_logs = None
|
| 548 |
-
else:
|
| 549 |
-
self.conversations = self.db["conversations"]
|
| 550 |
-
self.retrieval_logs = self.db["retrieval_logs"]
|
| 551 |
-
print("✅ ConversationRepository initialized with MongoDB")
|
| 552 |
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
RuntimeError: If MongoDB is not connected
|
| 559 |
-
"""
|
| 560 |
-
if self.db is None or self.conversations is None:
|
| 561 |
-
raise RuntimeError(
|
| 562 |
-
"MongoDB not connected. Cannot perform database operations. "
|
| 563 |
-
"Check MONGODB_URI in .env file."
|
| 564 |
-
)
|
| 565 |
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
Args:
|
| 579 |
-
user_id: User ID who owns this conversation
|
| 580 |
-
conversation_id: Optional custom conversation ID (auto-generated if None)
|
| 581 |
-
|
| 582 |
-
Returns:
|
| 583 |
-
str: Conversation ID
|
| 584 |
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
self._check_connection()
|
| 589 |
|
| 590 |
-
|
| 591 |
-
|
|
|
|
|
|
|
|
|
|
| 592 |
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
|
| 602 |
-
|
| 603 |
|
| 604 |
-
|
| 605 |
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
Args:
|
| 611 |
-
conversation_id: Conversation ID
|
| 612 |
-
|
| 613 |
-
Returns:
|
| 614 |
-
dict or None: Conversation document
|
| 615 |
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
"""
|
| 619 |
-
self._check_connection()
|
| 620 |
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
| 624 |
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
# async def get_user_conversations(
|
| 632 |
-
# self,
|
| 633 |
-
# user_id: str,
|
| 634 |
-
# limit: int = 10,
|
| 635 |
-
# skip: int = 0
|
| 636 |
-
# ) -> List[Dict]:
|
| 637 |
-
# """
|
| 638 |
-
# Get all conversations for a user.
|
| 639 |
-
|
| 640 |
-
# Args:
|
| 641 |
-
# user_id: User ID
|
| 642 |
-
# limit: Maximum number of conversations to return
|
| 643 |
-
# skip: Number of conversations to skip (for pagination)
|
| 644 |
-
|
| 645 |
-
# Returns:
|
| 646 |
-
# list: List of conversation documents
|
| 647 |
-
|
| 648 |
-
# Raises:
|
| 649 |
-
# RuntimeError: If MongoDB not connected
|
| 650 |
-
# """
|
| 651 |
-
# self._check_connection()
|
| 652 |
-
|
| 653 |
-
# cursor = self.conversations.find(
|
| 654 |
-
# {"user_id": user_id, "status": "active"}
|
| 655 |
-
# ).sort("updated_at", -1).skip(skip).limit(limit)
|
| 656 |
-
|
| 657 |
-
# conversations = await cursor.to_list(length=limit)
|
| 658 |
-
|
| 659 |
-
# # Convert ObjectIds to strings
|
| 660 |
-
# for conv in conversations:
|
| 661 |
-
# if "_id" in conv:
|
| 662 |
-
# conv["_id"] = str(conv["_id"])
|
| 663 |
-
|
| 664 |
-
# return conversations
|
| 665 |
-
async def get_user_conversations(
|
| 666 |
-
self,
|
| 667 |
-
user_id: str,
|
| 668 |
-
limit: int = 10,
|
| 669 |
-
skip: int = 0
|
| 670 |
-
) -> List[Dict]:
|
| 671 |
-
"""Get all conversations for a user."""
|
| 672 |
-
# Gracefully return empty list if not connected
|
| 673 |
-
if self.db is None or self.conversations is None:
|
| 674 |
-
print("⚠️ MongoDB not connected - returning empty conversations list")
|
| 675 |
-
return []
|
| 676 |
-
|
| 677 |
-
cursor = self.conversations.find(
|
| 678 |
-
{"user_id": user_id, "status": "active"}
|
| 679 |
-
).sort("updated_at", -1).skip(skip).limit(limit)
|
| 680 |
-
|
| 681 |
-
conversations = await cursor.to_list(length=limit)
|
| 682 |
-
|
| 683 |
-
# Convert ObjectIds to strings
|
| 684 |
-
for conv in conversations:
|
| 685 |
-
if "_id" in conv:
|
| 686 |
-
conv["_id"] = str(conv["_id"])
|
| 687 |
-
|
| 688 |
-
return conversations
|
| 689 |
-
|
| 690 |
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
|
|
|
| 698 |
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
'role': 'user' or 'assistant',
|
| 704 |
-
'content': str,
|
| 705 |
-
'timestamp': datetime,
|
| 706 |
-
'metadata': dict (optional - policy_action, docs_retrieved, etc.)
|
| 707 |
-
}
|
| 708 |
|
| 709 |
-
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
if "timestamp" not in message:
|
| 719 |
-
message["timestamp"] = datetime.now()
|
| 720 |
-
|
| 721 |
-
# Add message to conversation
|
| 722 |
-
result = await self.conversations.update_one(
|
| 723 |
-
{"conversation_id": conversation_id},
|
| 724 |
-
{
|
| 725 |
-
"$push": {"messages": message},
|
| 726 |
-
"$set": {"updated_at": datetime.now()}
|
| 727 |
-
}
|
| 728 |
-
)
|
| 729 |
|
| 730 |
-
|
| 731 |
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
|
| 744 |
-
|
| 745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
|
| 752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
|
| 754 |
-
|
| 755 |
-
|
|
|
|
| 756 |
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
| 758 |
|
| 759 |
-
|
| 760 |
-
|
| 761 |
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
async def delete_conversation(self, conversation_id: str) -> bool:
|
| 765 |
-
"""
|
| 766 |
-
Soft delete a conversation (mark as deleted, don't actually delete).
|
| 767 |
|
| 768 |
-
|
| 769 |
-
|
| 770 |
|
| 771 |
-
|
| 772 |
-
|
|
|
|
|
|
|
|
|
|
| 773 |
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
"""
|
| 777 |
-
self._check_connection()
|
| 778 |
-
|
| 779 |
-
result = await self.conversations.update_one(
|
| 780 |
-
{"conversation_id": conversation_id},
|
| 781 |
-
{
|
| 782 |
-
"$set": {
|
| 783 |
-
"status": "deleted",
|
| 784 |
-
"deleted_at": datetime.now()
|
| 785 |
-
}
|
| 786 |
-
}
|
| 787 |
-
)
|
| 788 |
|
| 789 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
Args:
|
| 803 |
-
log_data: Log data dict
|
| 804 |
-
{
|
| 805 |
-
'conversation_id': str,
|
| 806 |
-
'user_id': str,
|
| 807 |
-
'query': str,
|
| 808 |
-
'policy_action': 'FETCH' or 'NO_FETCH',
|
| 809 |
-
'policy_confidence': float,
|
| 810 |
-
'documents_retrieved': int,
|
| 811 |
-
'top_doc_score': float or None,
|
| 812 |
-
'retrieved_docs_metadata': list,
|
| 813 |
-
'response': str,
|
| 814 |
-
'retrieval_time_ms': float,
|
| 815 |
-
'generation_time_ms': float,
|
| 816 |
-
'total_time_ms': float,
|
| 817 |
-
'timestamp': datetime
|
| 818 |
-
}
|
| 819 |
-
|
| 820 |
-
Returns:
|
| 821 |
-
str: Log ID
|
| 822 |
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
|
|
|
|
|
|
|
|
|
| 831 |
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
|
| 836 |
-
|
| 837 |
-
|
| 838 |
|
| 839 |
-
|
| 840 |
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
Args:
|
| 852 |
-
conversation_id: Optional filter by conversation
|
| 853 |
-
user_id: Optional filter by user
|
| 854 |
-
limit: Maximum number of logs
|
| 855 |
-
skip: Number of logs to skip
|
| 856 |
-
|
| 857 |
-
Returns:
|
| 858 |
-
list: List of log documents
|
| 859 |
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
|
|
|
| 864 |
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
|
|
|
|
|
|
|
|
|
| 871 |
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
|
| 881 |
-
|
| 882 |
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
Args:
|
| 893 |
-
min_date: Optional minimum date for logs
|
| 894 |
-
limit: Maximum number of logs
|
| 895 |
-
|
| 896 |
-
Returns:
|
| 897 |
-
list: List of log documents suitable for RL training
|
| 898 |
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
self._check_connection()
|
| 903 |
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
|
|
|
|
|
|
|
|
|
| 909 |
|
| 910 |
-
|
| 911 |
-
|
| 912 |
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
|
| 922 |
-
|
| 923 |
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
Args:
|
| 933 |
-
user_id: User ID
|
| 934 |
-
|
| 935 |
-
Returns:
|
| 936 |
-
dict: Statistics
|
| 937 |
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
"""
|
| 941 |
-
self._check_connection()
|
| 942 |
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
|
|
|
|
|
|
|
|
|
| 948 |
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
|
| 955 |
-
|
| 956 |
-
|
| 957 |
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
Args:
|
| 969 |
-
user_id: Optional user ID filter
|
| 970 |
-
|
| 971 |
-
Returns:
|
| 972 |
-
dict: Policy statistics
|
| 973 |
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
"""
|
| 977 |
-
self._check_connection()
|
| 978 |
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
|
|
|
|
|
|
|
|
|
| 983 |
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
|
| 995 |
-
|
| 996 |
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
|
| 1005 |
|
| 1006 |
-
# ============================================================================
|
| 1007 |
-
# USAGE EXAMPLE (for reference)
|
| 1008 |
-
# ============================================================================
|
| 1009 |
-
"""
|
| 1010 |
-
# In your service or API endpoint:
|
| 1011 |
|
| 1012 |
-
from app.db.repositories.conversation_repository import ConversationRepository
|
| 1013 |
|
| 1014 |
-
repo = ConversationRepository()
|
| 1015 |
|
| 1016 |
-
# Create conversation
|
| 1017 |
-
conv_id = await repo.create_conversation(user_id="user_123")
|
| 1018 |
|
| 1019 |
-
# Add user message
|
| 1020 |
-
await repo.add_message(conv_id, {
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
})
|
| 1025 |
|
| 1026 |
-
# Add assistant message
|
| 1027 |
-
await repo.add_message(conv_id, {
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
})
|
| 1036 |
|
| 1037 |
-
# Get conversation history
|
| 1038 |
-
history = await repo.get_conversation_history(conv_id)
|
| 1039 |
|
| 1040 |
-
# Log retrieval for RL training
|
| 1041 |
-
await repo.log_retrieval({
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
})
|
| 1049 |
-
"""
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Conversation Repository - MongoDB Operations (UPDATED)
|
| 3 |
+
|
| 4 |
+
NOW COMPATIBLE with existing chat.py!
|
| 5 |
+
|
| 6 |
+
Added methods:
|
| 7 |
+
- get_conversation_history() - For chat.py compatibility
|
| 8 |
+
- log_retrieval() - For RL training data
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import List, Optional, Dict, Any
|
| 13 |
+
from bson import ObjectId
|
| 14 |
+
from pymongo import DESCENDING, ASCENDING
|
| 15 |
+
|
| 16 |
+
from app.db.mongodb import get_database
|
| 17 |
+
from app.models.conversation import (
|
| 18 |
+
Conversation,
|
| 19 |
+
Message,
|
| 20 |
+
ConversationListResponse,
|
| 21 |
+
ConversationListResult
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# CONVERSATION REPOSITORY
|
| 27 |
+
# ============================================================================
|
| 28 |
+
|
| 29 |
+
class ConversationRepository:
|
| 30 |
+
"""
|
| 31 |
+
Repository for conversation operations.
|
| 32 |
+
|
| 33 |
+
All MongoDB queries for conversations go through here.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self):
|
| 37 |
+
"""Initialize repository with database connection"""
|
| 38 |
+
self.db = get_database()
|
| 39 |
+
self.collection_name = "conversations"
|
| 40 |
+
self.retrieval_logs_collection = "retrieval_logs"
|
| 41 |
+
print("✅ ConversationRepository initialized with MongoDB")
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def collection(self):
|
| 45 |
+
"""Get conversations collection"""
|
| 46 |
+
if self.db is None:
|
| 47 |
+
raise RuntimeError("MongoDB database not available")
|
| 48 |
+
return self.db[self.collection_name]
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def retrieval_logs(self):
|
| 52 |
+
"""Get retrieval logs collection"""
|
| 53 |
+
if self.db is None:
|
| 54 |
+
raise RuntimeError("MongoDB database not available")
|
| 55 |
+
return self.db[self.retrieval_logs_collection]
|
| 56 |
+
|
| 57 |
+
# ========================================================================
|
| 58 |
+
# CREATE (UPDATED - Compatible with chat.py)
|
| 59 |
+
# ========================================================================
|
| 60 |
+
|
| 61 |
+
async def create_conversation(
|
| 62 |
+
self,
|
| 63 |
+
user_id: str,
|
| 64 |
+
title: Optional[str] = None,
|
| 65 |
+
first_message: Optional[str] = None
|
| 66 |
+
) -> str:
|
| 67 |
+
"""
|
| 68 |
+
Create a new conversation.
|
| 69 |
+
|
| 70 |
+
UPDATED: Returns conversation_id string (not full object)
|
| 71 |
+
This matches chat.py's expectation!
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
user_id: User ID who owns the conversation
|
| 75 |
+
title: Optional conversation title
|
| 76 |
+
first_message: Optional first user message
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
str: conversation_id (ObjectId as string)
|
| 80 |
+
"""
|
| 81 |
+
now = datetime.utcnow()
|
| 82 |
+
|
| 83 |
+
# Auto-generate title if not provided
|
| 84 |
+
if not title:
|
| 85 |
+
if first_message:
|
| 86 |
+
# Simple title from first 50 chars
|
| 87 |
+
title = first_message[:50] + ("..." if len(first_message) > 50 else "")
|
| 88 |
+
else:
|
| 89 |
+
title = f"Conversation {now.strftime('%Y-%m-%d %H:%M')}"
|
| 90 |
+
|
| 91 |
+
# Create conversation document
|
| 92 |
+
conversation_data = {
|
| 93 |
+
"user_id": user_id,
|
| 94 |
+
"title": title,
|
| 95 |
+
"messages": [],
|
| 96 |
+
"is_archived": False,
|
| 97 |
+
"is_deleted": False,
|
| 98 |
+
"created_at": now,
|
| 99 |
+
"updated_at": now,
|
| 100 |
+
"last_message_at": None,
|
| 101 |
+
"message_count": 0
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
# Add first message if provided
|
| 105 |
+
if first_message:
|
| 106 |
+
message = {
|
| 107 |
+
"role": "user",
|
| 108 |
+
"content": first_message,
|
| 109 |
+
"timestamp": now,
|
| 110 |
+
"metadata": None
|
| 111 |
+
}
|
| 112 |
+
conversation_data["messages"].append(message)
|
| 113 |
+
conversation_data["last_message_at"] = now
|
| 114 |
+
conversation_data["message_count"] = 1
|
| 115 |
+
|
| 116 |
+
# Insert into database
|
| 117 |
+
result = await self.collection.insert_one(conversation_data)
|
| 118 |
+
|
| 119 |
+
# Return conversation_id as string
|
| 120 |
+
return str(result.inserted_id)
|
| 121 |
+
|
| 122 |
+
# ========================================================================
|
| 123 |
+
# READ
|
| 124 |
+
# ========================================================================
|
| 125 |
+
|
| 126 |
+
async def get_conversation(
|
| 127 |
+
self,
|
| 128 |
+
conversation_id: str
|
| 129 |
+
) -> Optional[Dict]:
|
| 130 |
+
"""
|
| 131 |
+
Get conversation by ID (returns raw dict).
|
| 132 |
+
|
| 133 |
+
UPDATED: No user_id verification here (done in service layer)
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
conversation_id: Conversation ID
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
dict or None: Raw conversation document
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
result = await self.collection.find_one({
|
| 143 |
+
"_id": ObjectId(conversation_id),
|
| 144 |
+
"is_deleted": False
|
| 145 |
+
})
|
| 146 |
+
|
| 147 |
+
if result:
|
| 148 |
+
# Add conversation_id field for compatibility
|
| 149 |
+
result['conversation_id'] = str(result['_id'])
|
| 150 |
+
|
| 151 |
+
return result
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
print(f"❌ Error getting conversation: {e}")
|
| 155 |
+
return None
|
| 156 |
+
|
| 157 |
+
async def get_conversation_by_id(
|
| 158 |
+
self,
|
| 159 |
+
conversation_id: str,
|
| 160 |
+
user_id: str
|
| 161 |
+
) -> Optional[Conversation]:
|
| 162 |
+
"""
|
| 163 |
+
Get conversation by ID (with user verification, returns Pydantic model).
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
conversation_id: Conversation ID
|
| 167 |
+
user_id: User ID (for ownership verification)
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
Conversation or None
|
| 171 |
+
"""
|
| 172 |
+
try:
|
| 173 |
+
result = await self.collection.find_one({
|
| 174 |
+
"_id": ObjectId(conversation_id),
|
| 175 |
+
"user_id": user_id,
|
| 176 |
+
"is_deleted": False
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
if result:
|
| 180 |
+
return Conversation(**result)
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print(f"❌ Error getting conversation: {e}")
|
| 185 |
+
return None
|
| 186 |
+
|
| 187 |
+
async def get_conversation_history(
|
| 188 |
+
self,
|
| 189 |
+
conversation_id: str,
|
| 190 |
+
max_messages: int = 10
|
| 191 |
+
) -> List[Dict[str, str]]:
|
| 192 |
+
"""
|
| 193 |
+
Get conversation history for chat.py compatibility.
|
| 194 |
+
|
| 195 |
+
Returns format expected by chat_service.process_query():
|
| 196 |
+
[
|
| 197 |
+
{'role': 'user', 'content': '...', 'metadata': {...}},
|
| 198 |
+
{'role': 'assistant', 'content': '...', 'metadata': {...}}
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
conversation_id: Conversation ID
|
| 203 |
+
max_messages: Maximum messages to return (recent first)
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
List of message dicts
|
| 207 |
+
"""
|
| 208 |
+
try:
|
| 209 |
+
conversation = await self.get_conversation(conversation_id)
|
| 210 |
+
|
| 211 |
+
if not conversation:
|
| 212 |
+
return []
|
| 213 |
+
|
| 214 |
+
messages = conversation.get('messages', [])
|
| 215 |
+
|
| 216 |
+
# Return last N messages
|
| 217 |
+
recent_messages = messages[-max_messages:] if len(messages) > max_messages else messages
|
| 218 |
+
|
| 219 |
+
# Convert to expected format
|
| 220 |
+
history = []
|
| 221 |
+
for msg in recent_messages:
|
| 222 |
+
history.append({
|
| 223 |
+
'role': msg['role'],
|
| 224 |
+
'content': msg['content'],
|
| 225 |
+
'metadata': msg.get('metadata')
|
| 226 |
+
})
|
| 227 |
+
|
| 228 |
+
return history
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
print(f"❌ Error getting history: {e}")
|
| 232 |
+
return []
|
| 233 |
+
|
| 234 |
+
async def get_user_conversations(
|
| 235 |
+
self,
|
| 236 |
+
user_id: str,
|
| 237 |
+
limit: int = 10,
|
| 238 |
+
skip: int = 0
|
| 239 |
+
) -> List[Dict]:
|
| 240 |
+
"""
|
| 241 |
+
Get conversations for a user (for chat.py compatibility).
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
user_id: User ID
|
| 245 |
+
limit: Max conversations to return
|
| 246 |
+
skip: Number to skip (pagination)
|
| 247 |
+
|
| 248 |
+
Returns:
|
| 249 |
+
List of conversation dicts
|
| 250 |
+
"""
|
| 251 |
+
try:
|
| 252 |
+
cursor = self.collection.find({
|
| 253 |
+
"user_id": user_id,
|
| 254 |
+
"is_deleted": False
|
| 255 |
+
}).sort("updated_at", DESCENDING).skip(skip).limit(limit)
|
| 256 |
+
|
| 257 |
+
conversations = []
|
| 258 |
+
async for doc in cursor:
|
| 259 |
+
doc['conversation_id'] = str(doc['_id'])
|
| 260 |
+
conversations.append(doc)
|
| 261 |
+
|
| 262 |
+
return conversations
|
| 263 |
+
|
| 264 |
+
except Exception as e:
|
| 265 |
+
print(f"❌ Error listing conversations: {e}")
|
| 266 |
+
return []
|
| 267 |
+
|
| 268 |
+
async def list_conversations(
|
| 269 |
+
self,
|
| 270 |
+
user_id: str,
|
| 271 |
+
page: int = 1,
|
| 272 |
+
page_size: int = 20,
|
| 273 |
+
include_archived: bool = False,
|
| 274 |
+
search_query: Optional[str] = None
|
| 275 |
+
) -> ConversationListResult:
|
| 276 |
+
"""
|
| 277 |
+
List conversations with pagination and filtering (for new API).
|
| 278 |
+
|
| 279 |
+
Args:
|
| 280 |
+
user_id: User ID
|
| 281 |
+
page: Page number (1-indexed)
|
| 282 |
+
page_size: Items per page
|
| 283 |
+
include_archived: Include archived conversations?
|
| 284 |
+
search_query: Optional search query
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
ConversationListResult: Paginated list
|
| 288 |
+
"""
|
| 289 |
+
# Build query filter
|
| 290 |
+
query_filter = {
|
| 291 |
+
"user_id": user_id,
|
| 292 |
+
"is_deleted": False
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
if not include_archived:
|
| 296 |
+
query_filter["is_archived"] = False
|
| 297 |
+
|
| 298 |
+
if search_query:
|
| 299 |
+
query_filter["$or"] = [
|
| 300 |
+
{"title": {"$regex": search_query, "$options": "i"}},
|
| 301 |
+
{"messages.content": {"$regex": search_query, "$options": "i"}}
|
| 302 |
+
]
|
| 303 |
+
|
| 304 |
+
# Get total count
|
| 305 |
+
total = await self.collection.count_documents(query_filter)
|
| 306 |
+
|
| 307 |
+
# Calculate pagination
|
| 308 |
+
skip = (page - 1) * page_size
|
| 309 |
+
has_more = (skip + page_size) < total
|
| 310 |
+
|
| 311 |
+
# Get conversations
|
| 312 |
+
cursor = self.collection.find(query_filter).sort(
|
| 313 |
+
"last_message_at", DESCENDING
|
| 314 |
+
).skip(skip).limit(page_size)
|
| 315 |
+
|
| 316 |
+
conversations = []
|
| 317 |
+
async for doc in cursor:
|
| 318 |
+
preview = ""
|
| 319 |
+
if doc.get("messages"):
|
| 320 |
+
last_msg = doc["messages"][-1]
|
| 321 |
+
preview = last_msg.get("content", "")[:100]
|
| 322 |
+
if len(last_msg.get("content", "")) > 100:
|
| 323 |
+
preview += "..."
|
| 324 |
+
|
| 325 |
+
conversations.append(ConversationListResponse(
|
| 326 |
+
id=str(doc["_id"]),
|
| 327 |
+
user_id=doc["user_id"],
|
| 328 |
+
title=doc["title"],
|
| 329 |
+
preview=preview,
|
| 330 |
+
is_archived=doc.get("is_archived", False),
|
| 331 |
+
created_at=doc["created_at"],
|
| 332 |
+
updated_at=doc["updated_at"],
|
| 333 |
+
last_message_at=doc.get("last_message_at"),
|
| 334 |
+
message_count=doc.get("message_count", 0)
|
| 335 |
+
))
|
| 336 |
+
|
| 337 |
+
return ConversationListResult(
|
| 338 |
+
conversations=conversations,
|
| 339 |
+
total=total,
|
| 340 |
+
page=page,
|
| 341 |
+
page_size=page_size,
|
| 342 |
+
has_more=has_more
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# ========================================================================
|
| 346 |
+
# UPDATE
|
| 347 |
+
# ========================================================================
|
| 348 |
+
|
| 349 |
+
async def add_message(
|
| 350 |
+
self,
|
| 351 |
+
conversation_id: str,
|
| 352 |
+
message: Dict[str, Any]
|
| 353 |
+
) -> bool:
|
| 354 |
+
"""
|
| 355 |
+
Add a message to conversation (chat.py compatible).
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
conversation_id: Conversation ID
|
| 359 |
+
message: Message dict with role, content, timestamp, metadata
|
| 360 |
+
|
| 361 |
+
Returns:
|
| 362 |
+
bool: True if added successfully
|
| 363 |
+
"""
|
| 364 |
+
try:
|
| 365 |
+
now = datetime.utcnow()
|
| 366 |
+
|
| 367 |
+
# Ensure timestamp is datetime
|
| 368 |
+
if 'timestamp' not in message or not isinstance(message['timestamp'], datetime):
|
| 369 |
+
message['timestamp'] = now
|
| 370 |
+
|
| 371 |
+
result = await self.collection.update_one(
|
| 372 |
+
{
|
| 373 |
+
"_id": ObjectId(conversation_id),
|
| 374 |
+
"is_deleted": False
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"$push": {"messages": message},
|
| 378 |
+
"$set": {
|
| 379 |
+
"updated_at": now,
|
| 380 |
+
"last_message_at": message['timestamp']
|
| 381 |
+
},
|
| 382 |
+
"$inc": {"message_count": 1}
|
| 383 |
+
}
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
return result.modified_count > 0
|
| 387 |
+
|
| 388 |
+
except Exception as e:
|
| 389 |
+
print(f"❌ Error adding message: {e}")
|
| 390 |
+
return False
|
| 391 |
+
|
| 392 |
+
async def update_conversation(
|
| 393 |
+
self,
|
| 394 |
+
conversation_id: str,
|
| 395 |
+
user_id: str,
|
| 396 |
+
update_data: Dict[str, Any]
|
| 397 |
+
) -> Optional[Conversation]:
|
| 398 |
+
"""Update conversation properties."""
|
| 399 |
+
try:
|
| 400 |
+
update_data["updated_at"] = datetime.utcnow()
|
| 401 |
+
|
| 402 |
+
result = await self.collection.update_one(
|
| 403 |
+
{
|
| 404 |
+
"_id": ObjectId(conversation_id),
|
| 405 |
+
"user_id": user_id,
|
| 406 |
+
"is_deleted": False
|
| 407 |
+
},
|
| 408 |
+
{"$set": update_data}
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
if result.modified_count > 0:
|
| 412 |
+
return await self.get_conversation_by_id(conversation_id, user_id)
|
| 413 |
+
return None
|
| 414 |
+
|
| 415 |
+
except Exception as e:
|
| 416 |
+
print(f"❌ Error updating conversation: {e}")
|
| 417 |
+
return None
|
| 418 |
+
|
| 419 |
+
async def rename_conversation(
|
| 420 |
+
self,
|
| 421 |
+
conversation_id: str,
|
| 422 |
+
user_id: str,
|
| 423 |
+
new_title: str
|
| 424 |
+
) -> Optional[Conversation]:
|
| 425 |
+
"""Rename a conversation."""
|
| 426 |
+
return await self.update_conversation(
|
| 427 |
+
conversation_id,
|
| 428 |
+
user_id,
|
| 429 |
+
{"title": new_title}
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
async def archive_conversation(
|
| 433 |
+
self,
|
| 434 |
+
conversation_id: str,
|
| 435 |
+
user_id: str,
|
| 436 |
+
archived: bool = True
|
| 437 |
+
) -> Optional[Conversation]:
|
| 438 |
+
"""Archive or unarchive a conversation."""
|
| 439 |
+
return await self.update_conversation(
|
| 440 |
+
conversation_id,
|
| 441 |
+
user_id,
|
| 442 |
+
{"is_archived": archived}
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
# ========================================================================
|
| 446 |
+
# DELETE
|
| 447 |
+
# ========================================================================
|
| 448 |
+
|
| 449 |
+
async def delete_conversation(
|
| 450 |
+
self,
|
| 451 |
+
conversation_id: str,
|
| 452 |
+
soft_delete: bool = True
|
| 453 |
+
) -> bool:
|
| 454 |
+
"""
|
| 455 |
+
Delete a conversation (chat.py compatible - no user_id check).
|
| 456 |
+
|
| 457 |
+
Args:
|
| 458 |
+
conversation_id: Conversation ID
|
| 459 |
+
soft_delete: If True, mark as deleted. If False, remove from DB.
|
| 460 |
+
|
| 461 |
+
Returns:
|
| 462 |
+
bool: True if deleted
|
| 463 |
+
"""
|
| 464 |
+
try:
|
| 465 |
+
if soft_delete:
|
| 466 |
+
result = await self.collection.update_one(
|
| 467 |
+
{"_id": ObjectId(conversation_id)},
|
| 468 |
+
{
|
| 469 |
+
"$set": {
|
| 470 |
+
"is_deleted": True,
|
| 471 |
+
"updated_at": datetime.utcnow()
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
)
|
| 475 |
+
return result.modified_count > 0
|
| 476 |
+
else:
|
| 477 |
+
result = await self.collection.delete_one({
|
| 478 |
+
"_id": ObjectId(conversation_id)
|
| 479 |
+
})
|
| 480 |
+
return result.deleted_count > 0
|
| 481 |
+
|
| 482 |
+
except Exception as e:
|
| 483 |
+
print(f"❌ Error deleting conversation: {e}")
|
| 484 |
+
return False
|
| 485 |
+
|
| 486 |
+
# ========================================================================
|
| 487 |
+
# RETRIEVAL LOGGING (For RL Training)
|
| 488 |
+
# ========================================================================
|
| 489 |
+
|
| 490 |
+
async def log_retrieval(
|
| 491 |
+
self,
|
| 492 |
+
log_data: Dict[str, Any]
|
| 493 |
+
) -> bool:
|
| 494 |
+
"""
|
| 495 |
+
Log retrieval data for RL training.
|
| 496 |
+
|
| 497 |
+
Stores query, policy decision, retrieval results for model improvement.
|
| 498 |
+
|
| 499 |
+
Args:
|
| 500 |
+
log_data: Dict with retrieval metadata
|
| 501 |
+
|
| 502 |
+
Returns:
|
| 503 |
+
bool: True if logged successfully
|
| 504 |
+
"""
|
| 505 |
+
try:
|
| 506 |
+
# Ensure timestamp
|
| 507 |
+
if 'timestamp' not in log_data:
|
| 508 |
+
log_data['timestamp'] = datetime.utcnow()
|
| 509 |
+
|
| 510 |
+
await self.retrieval_logs.insert_one(log_data)
|
| 511 |
+
return True
|
| 512 |
+
|
| 513 |
+
except Exception as e:
|
| 514 |
+
print(f"❌ Error logging retrieval: {e}")
|
| 515 |
+
return False
|
| 516 |
+
|
| 517 |
+
# ========================================================================
|
| 518 |
+
# SEARCH
|
| 519 |
+
# ========================================================================
|
| 520 |
+
|
| 521 |
+
async def search_conversations(
|
| 522 |
+
self,
|
| 523 |
+
user_id: str,
|
| 524 |
+
query: str,
|
| 525 |
+
page: int = 1,
|
| 526 |
+
page_size: int = 20
|
| 527 |
+
) -> ConversationListResult:
|
| 528 |
+
"""Search conversations by title or content."""
|
| 529 |
+
return await self.list_conversations(
|
| 530 |
+
user_id=user_id,
|
| 531 |
+
page=page,
|
| 532 |
+
page_size=page_size,
|
| 533 |
+
include_archived=True,
|
| 534 |
+
search_query=query
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
# ========================================================================
|
| 538 |
+
# UTILITY
|
| 539 |
+
# ========================================================================
|
| 540 |
+
|
| 541 |
+
async def get_conversation_count(self, user_id: str) -> Dict[str, int]:
|
| 542 |
+
"""Get conversation counts for a user."""
|
| 543 |
+
total = await self.collection.count_documents({
|
| 544 |
+
"user_id": user_id,
|
| 545 |
+
"is_deleted": False
|
| 546 |
+
})
|
| 547 |
+
|
| 548 |
+
archived = await self.collection.count_documents({
|
| 549 |
+
"user_id": user_id,
|
| 550 |
+
"is_deleted": False,
|
| 551 |
+
"is_archived": True
|
| 552 |
+
})
|
| 553 |
+
|
| 554 |
+
return {
|
| 555 |
+
"total": total,
|
| 556 |
+
"active": total - archived,
|
| 557 |
+
"archived": archived
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
async def create_indexes(self):
|
| 561 |
+
"""Create database indexes for better performance."""
|
| 562 |
+
try:
|
| 563 |
+
await self.collection.create_index([
|
| 564 |
+
("user_id", ASCENDING),
|
| 565 |
+
("is_deleted", ASCENDING),
|
| 566 |
+
("last_message_at", DESCENDING)
|
| 567 |
+
])
|
| 568 |
+
|
| 569 |
+
await self.collection.create_index([
|
| 570 |
+
("user_id", ASCENDING),
|
| 571 |
+
("title", "text"),
|
| 572 |
+
("messages.content", "text")
|
| 573 |
+
])
|
| 574 |
+
|
| 575 |
+
print("✅ Conversation indexes created")
|
| 576 |
+
|
| 577 |
+
except Exception as e:
|
| 578 |
+
print(f"⚠️ Failed to create indexes: {e}")
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
# ============================================================================
|
| 582 |
+
# GLOBAL REPOSITORY INSTANCE
|
| 583 |
+
# ============================================================================
|
| 584 |
+
|
| 585 |
+
conversation_repository = ConversationRepository()
|
| 586 |
+
|
| 587 |
+
|
| 588 |
# """
|
| 589 |
# Conversation Repository - MongoDB CRUD operations
|
| 590 |
# Handles storing and retrieving conversations from MongoDB Atlas
|
| 591 |
|
| 592 |
# Repository Pattern: Separates database logic from business logic
|
| 593 |
# This makes code cleaner and easier to test
|
| 594 |
+
|
| 595 |
+
# Collections:
|
| 596 |
+
# - conversations: Stores complete conversations with messages
|
| 597 |
+
# - retrieval_logs: Logs each retrieval operation (for RL training data)
|
| 598 |
# """
|
| 599 |
|
| 600 |
# import uuid
|
|
|
|
| 613 |
# """
|
| 614 |
# Repository for conversation data in MongoDB.
|
| 615 |
|
| 616 |
+
# Provides CRUD operations for:
|
| 617 |
+
# 1. Conversations (user chat sessions)
|
| 618 |
+
# 2. Retrieval logs (for RL training and analytics)
|
| 619 |
# """
|
| 620 |
|
| 621 |
# def __init__(self):
|
| 622 |
+
# """
|
| 623 |
+
# Initialize repository with database connection.
|
| 624 |
+
|
| 625 |
+
# Gracefully handles case where MongoDB is not connected.
|
| 626 |
+
# """
|
| 627 |
# self.db = get_database()
|
| 628 |
+
|
| 629 |
+
# # Graceful handling if MongoDB not connected
|
| 630 |
+
# if self.db is None:
|
| 631 |
+
# print("⚠️ ConversationRepository: MongoDB not connected")
|
| 632 |
+
# print(" Repository will not function until database is connected")
|
| 633 |
+
# self.conversations = None
|
| 634 |
+
# self.retrieval_logs = None
|
| 635 |
+
# else:
|
| 636 |
+
# self.conversations = self.db["conversations"]
|
| 637 |
+
# self.retrieval_logs = self.db["retrieval_logs"]
|
| 638 |
+
# print("✅ ConversationRepository initialized with MongoDB")
|
| 639 |
+
|
| 640 |
+
# def _check_connection(self):
|
| 641 |
+
# """
|
| 642 |
+
# Check if MongoDB is connected.
|
| 643 |
+
|
| 644 |
+
# Raises:
|
| 645 |
+
# RuntimeError: If MongoDB is not connected
|
| 646 |
+
# """
|
| 647 |
+
# if self.db is None or self.conversations is None:
|
| 648 |
+
# raise RuntimeError(
|
| 649 |
+
# "MongoDB not connected. Cannot perform database operations. "
|
| 650 |
+
# "Check MONGODB_URI in .env file."
|
| 651 |
+
# )
|
| 652 |
|
| 653 |
# # ========================================================================
|
| 654 |
# # CONVERSATION CRUD OPERATIONS
|
|
|
|
| 668 |
|
| 669 |
# Returns:
|
| 670 |
# str: Conversation ID
|
| 671 |
+
|
| 672 |
+
# Raises:
|
| 673 |
+
# RuntimeError: If MongoDB not connected
|
| 674 |
# """
|
| 675 |
+
# self._check_connection()
|
| 676 |
+
|
| 677 |
# if conversation_id is None:
|
| 678 |
# conversation_id = str(uuid.uuid4())
|
| 679 |
|
|
|
|
| 699 |
|
| 700 |
# Returns:
|
| 701 |
# dict or None: Conversation document
|
| 702 |
+
|
| 703 |
+
# Raises:
|
| 704 |
+
# RuntimeError: If MongoDB not connected
|
| 705 |
# """
|
| 706 |
+
# self._check_connection()
|
| 707 |
+
|
| 708 |
# conversation = await self.conversations.find_one(
|
| 709 |
# {"conversation_id": conversation_id}
|
| 710 |
# )
|
|
|
|
| 715 |
|
| 716 |
# return conversation
|
| 717 |
|
| 718 |
+
# # async def get_user_conversations(
|
| 719 |
+
# # self,
|
| 720 |
+
# # user_id: str,
|
| 721 |
+
# # limit: int = 10,
|
| 722 |
+
# # skip: int = 0
|
| 723 |
+
# # ) -> List[Dict]:
|
| 724 |
+
# # """
|
| 725 |
+
# # Get all conversations for a user.
|
| 726 |
+
|
| 727 |
+
# # Args:
|
| 728 |
+
# # user_id: User ID
|
| 729 |
+
# # limit: Maximum number of conversations to return
|
| 730 |
+
# # skip: Number of conversations to skip (for pagination)
|
| 731 |
+
|
| 732 |
+
# # Returns:
|
| 733 |
+
# # list: List of conversation documents
|
| 734 |
+
|
| 735 |
+
# # Raises:
|
| 736 |
+
# # RuntimeError: If MongoDB not connected
|
| 737 |
+
# # """
|
| 738 |
+
# # self._check_connection()
|
| 739 |
+
|
| 740 |
+
# # cursor = self.conversations.find(
|
| 741 |
+
# # {"user_id": user_id, "status": "active"}
|
| 742 |
+
# # ).sort("updated_at", -1).skip(skip).limit(limit)
|
| 743 |
+
|
| 744 |
+
# # conversations = await cursor.to_list(length=limit)
|
| 745 |
+
|
| 746 |
+
# # # Convert ObjectIds to strings
|
| 747 |
+
# # for conv in conversations:
|
| 748 |
+
# # if "_id" in conv:
|
| 749 |
+
# # conv["_id"] = str(conv["_id"])
|
| 750 |
+
|
| 751 |
+
# # return conversations
|
| 752 |
# async def get_user_conversations(
|
| 753 |
# self,
|
| 754 |
# user_id: str,
|
| 755 |
# limit: int = 10,
|
| 756 |
# skip: int = 0
|
| 757 |
# ) -> List[Dict]:
|
| 758 |
+
# """Get all conversations for a user."""
|
| 759 |
+
# # Gracefully return empty list if not connected
|
| 760 |
+
# if self.db is None or self.conversations is None:
|
| 761 |
+
# print("⚠️ MongoDB not connected - returning empty conversations list")
|
| 762 |
+
# return []
|
| 763 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
# cursor = self.conversations.find(
|
| 765 |
# {"user_id": user_id, "status": "active"}
|
| 766 |
# ).sort("updated_at", -1).skip(skip).limit(limit)
|
| 767 |
+
|
| 768 |
# conversations = await cursor.to_list(length=limit)
|
| 769 |
+
|
| 770 |
# # Convert ObjectIds to strings
|
| 771 |
# for conv in conversations:
|
| 772 |
# if "_id" in conv:
|
| 773 |
# conv["_id"] = str(conv["_id"])
|
| 774 |
+
|
| 775 |
# return conversations
|
| 776 |
+
|
| 777 |
|
| 778 |
# async def add_message(
|
| 779 |
# self,
|
|
|
|
| 795 |
|
| 796 |
# Returns:
|
| 797 |
# bool: Success status
|
| 798 |
+
|
| 799 |
+
# Raises:
|
| 800 |
+
# RuntimeError: If MongoDB not connected
|
| 801 |
# """
|
| 802 |
+
# self._check_connection()
|
| 803 |
+
|
| 804 |
# # Ensure timestamp exists
|
| 805 |
# if "timestamp" not in message:
|
| 806 |
# message["timestamp"] = datetime.now()
|
|
|
|
| 830 |
|
| 831 |
# Returns:
|
| 832 |
# list: List of messages
|
| 833 |
+
|
| 834 |
+
# Raises:
|
| 835 |
+
# RuntimeError: If MongoDB not connected
|
| 836 |
# """
|
| 837 |
+
# self._check_connection()
|
| 838 |
+
|
| 839 |
# conversation = await self.get_conversation(conversation_id)
|
| 840 |
|
| 841 |
# if not conversation:
|
|
|
|
| 857 |
|
| 858 |
# Returns:
|
| 859 |
# bool: Success status
|
| 860 |
+
|
| 861 |
+
# Raises:
|
| 862 |
+
# RuntimeError: If MongoDB not connected
|
| 863 |
# """
|
| 864 |
+
# self._check_connection()
|
| 865 |
+
|
| 866 |
# result = await self.conversations.update_one(
|
| 867 |
# {"conversation_id": conversation_id},
|
| 868 |
# {
|
|
|
|
| 906 |
|
| 907 |
# Returns:
|
| 908 |
# str: Log ID
|
| 909 |
+
|
| 910 |
+
# Raises:
|
| 911 |
+
# RuntimeError: If MongoDB not connected
|
| 912 |
# """
|
| 913 |
+
# self._check_connection()
|
| 914 |
+
|
| 915 |
# # Add timestamp if not present
|
| 916 |
# if "timestamp" not in log_data:
|
| 917 |
# log_data["timestamp"] = datetime.now()
|
|
|
|
| 943 |
|
| 944 |
# Returns:
|
| 945 |
# list: List of log documents
|
| 946 |
+
|
| 947 |
+
# Raises:
|
| 948 |
+
# RuntimeError: If MongoDB not connected
|
| 949 |
# """
|
| 950 |
+
# self._check_connection()
|
| 951 |
+
|
| 952 |
# # Build query
|
| 953 |
# query = {}
|
| 954 |
# if conversation_id:
|
|
|
|
| 982 |
|
| 983 |
# Returns:
|
| 984 |
# list: List of log documents suitable for RL training
|
| 985 |
+
|
| 986 |
+
# Raises:
|
| 987 |
+
# RuntimeError: If MongoDB not connected
|
| 988 |
# """
|
| 989 |
+
# self._check_connection()
|
| 990 |
+
|
| 991 |
# # Build query
|
| 992 |
# query = {
|
| 993 |
# "policy_action": {"$exists": True},
|
|
|
|
| 1021 |
|
| 1022 |
# Returns:
|
| 1023 |
# dict: Statistics
|
| 1024 |
+
|
| 1025 |
+
# Raises:
|
| 1026 |
+
# RuntimeError: If MongoDB not connected
|
| 1027 |
# """
|
| 1028 |
+
# self._check_connection()
|
| 1029 |
+
|
| 1030 |
# # Count total conversations
|
| 1031 |
# total_conversations = await self.conversations.count_documents({
|
| 1032 |
# "user_id": user_id,
|
|
|
|
| 1057 |
|
| 1058 |
# Returns:
|
| 1059 |
# dict: Policy statistics
|
| 1060 |
+
|
| 1061 |
+
# Raises:
|
| 1062 |
+
# RuntimeError: If MongoDB not connected
|
| 1063 |
# """
|
| 1064 |
+
# self._check_connection()
|
| 1065 |
+
|
| 1066 |
# # Build query
|
| 1067 |
# query = {}
|
| 1068 |
# if user_id:
|
|
|
|
| 1141 |
|
| 1142 |
|
| 1143 |
|
| 1144 |
+
# """
|
| 1145 |
+
# Conversation Repository - MongoDB CRUD operations
|
| 1146 |
+
# Handles storing and retrieving conversations from MongoDB Atlas
|
| 1147 |
|
| 1148 |
+
# Repository Pattern: Separates database logic from business logic
|
| 1149 |
+
# This makes code cleaner and easier to test
|
| 1150 |
+
# """
|
| 1151 |
|
| 1152 |
+
# import uuid
|
| 1153 |
+
# from datetime import datetime
|
| 1154 |
+
# from typing import List, Dict, Optional
|
| 1155 |
+
# from bson import ObjectId
|
| 1156 |
|
| 1157 |
+
# from app.db.mongodb import get_database
|
| 1158 |
|
| 1159 |
|
| 1160 |
+
# # ============================================================================
|
| 1161 |
+
# # CONVERSATION REPOSITORY
|
| 1162 |
+
# # ============================================================================
|
| 1163 |
|
| 1164 |
+
# class ConversationRepository:
|
| 1165 |
+
# """
|
| 1166 |
+
# Repository for conversation data in MongoDB.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1167 |
|
| 1168 |
+
# Collections used:
|
| 1169 |
+
# - conversations: Stores complete conversations with messages
|
| 1170 |
+
# - retrieval_logs: Logs each retrieval operation (for RL training)
|
| 1171 |
+
# """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
|
| 1173 |
+
# def __init__(self):
|
| 1174 |
+
# """Initialize repository with database connection"""
|
| 1175 |
+
# self.db = get_database()
|
| 1176 |
+
# self.conversations = self.db["conversations"]
|
| 1177 |
+
# self.retrieval_logs = self.db["retrieval_logs"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
|
| 1179 |
+
# # ========================================================================
|
| 1180 |
+
# # CONVERSATION CRUD OPERATIONS
|
| 1181 |
+
# # ========================================================================
|
| 1182 |
|
| 1183 |
+
# async def create_conversation(
|
| 1184 |
+
# self,
|
| 1185 |
+
# user_id: str,
|
| 1186 |
+
# conversation_id: Optional[str] = None
|
| 1187 |
+
# ) -> str:
|
| 1188 |
+
# """
|
| 1189 |
+
# Create a new conversation.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1190 |
|
| 1191 |
+
# Args:
|
| 1192 |
+
# user_id: User ID who owns this conversation
|
| 1193 |
+
# conversation_id: Optional custom conversation ID (auto-generated if None)
|
|
|
|
| 1194 |
|
| 1195 |
+
# Returns:
|
| 1196 |
+
# str: Conversation ID
|
| 1197 |
+
# """
|
| 1198 |
+
# if conversation_id is None:
|
| 1199 |
+
# conversation_id = str(uuid.uuid4())
|
| 1200 |
|
| 1201 |
+
# conversation = {
|
| 1202 |
+
# "conversation_id": conversation_id,
|
| 1203 |
+
# "user_id": user_id,
|
| 1204 |
+
# "messages": [], # Will store all messages
|
| 1205 |
+
# "created_at": datetime.now(),
|
| 1206 |
+
# "updated_at": datetime.now(),
|
| 1207 |
+
# "status": "active" # active, archived, deleted
|
| 1208 |
+
# }
|
| 1209 |
|
| 1210 |
+
# await self.conversations.insert_one(conversation)
|
| 1211 |
|
| 1212 |
+
# return conversation_id
|
| 1213 |
|
| 1214 |
+
# async def get_conversation(self, conversation_id: str) -> Optional[Dict]:
|
| 1215 |
+
# """
|
| 1216 |
+
# Get a conversation by ID.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
|
| 1218 |
+
# Args:
|
| 1219 |
+
# conversation_id: Conversation ID
|
|
|
|
|
|
|
| 1220 |
|
| 1221 |
+
# Returns:
|
| 1222 |
+
# dict or None: Conversation document
|
| 1223 |
+
# """
|
| 1224 |
+
# conversation = await self.conversations.find_one(
|
| 1225 |
+
# {"conversation_id": conversation_id}
|
| 1226 |
+
# )
|
| 1227 |
|
| 1228 |
+
# # Convert MongoDB ObjectId to string for JSON serialization
|
| 1229 |
+
# if conversation and "_id" in conversation:
|
| 1230 |
+
# conversation["_id"] = str(conversation["_id"])
|
| 1231 |
|
| 1232 |
+
# return conversation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1233 |
|
| 1234 |
+
# async def get_user_conversations(
|
| 1235 |
+
# self,
|
| 1236 |
+
# user_id: str,
|
| 1237 |
+
# limit: int = 10,
|
| 1238 |
+
# skip: int = 0
|
| 1239 |
+
# ) -> List[Dict]:
|
| 1240 |
+
# """
|
| 1241 |
+
# Get all conversations for a user.
|
| 1242 |
|
| 1243 |
+
# Args:
|
| 1244 |
+
# user_id: User ID
|
| 1245 |
+
# limit: Maximum number of conversations to return
|
| 1246 |
+
# skip: Number of conversations to skip (for pagination)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1247 |
|
| 1248 |
+
# Returns:
|
| 1249 |
+
# list: List of conversation documents
|
| 1250 |
+
# """
|
| 1251 |
+
# cursor = self.conversations.find(
|
| 1252 |
+
# {"user_id": user_id, "status": "active"}
|
| 1253 |
+
# ).sort("updated_at", -1).skip(skip).limit(limit)
|
| 1254 |
|
| 1255 |
+
# conversations = await cursor.to_list(length=limit)
|
| 1256 |
+
|
| 1257 |
+
# # Convert ObjectIds to strings
|
| 1258 |
+
# for conv in conversations:
|
| 1259 |
+
# if "_id" in conv:
|
| 1260 |
+
# conv["_id"] = str(conv["_id"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1261 |
|
| 1262 |
+
# return conversations
|
| 1263 |
|
| 1264 |
+
# async def add_message(
|
| 1265 |
+
# self,
|
| 1266 |
+
# conversation_id: str,
|
| 1267 |
+
# message: Dict
|
| 1268 |
+
# ) -> bool:
|
| 1269 |
+
# """
|
| 1270 |
+
# Add a message to a conversation.
|
| 1271 |
|
| 1272 |
+
# Args:
|
| 1273 |
+
# conversation_id: Conversation ID
|
| 1274 |
+
# message: Message dict
|
| 1275 |
+
# {
|
| 1276 |
+
# 'role': 'user' or 'assistant',
|
| 1277 |
+
# 'content': str,
|
| 1278 |
+
# 'timestamp': datetime,
|
| 1279 |
+
# 'metadata': dict (optional - policy_action, docs_retrieved, etc.)
|
| 1280 |
+
# }
|
| 1281 |
|
| 1282 |
+
# Returns:
|
| 1283 |
+
# bool: Success status
|
| 1284 |
+
# """
|
| 1285 |
+
# # Ensure timestamp exists
|
| 1286 |
+
# if "timestamp" not in message:
|
| 1287 |
+
# message["timestamp"] = datetime.now()
|
| 1288 |
|
| 1289 |
+
# # Add message to conversation
|
| 1290 |
+
# result = await self.conversations.update_one(
|
| 1291 |
+
# {"conversation_id": conversation_id},
|
| 1292 |
+
# {
|
| 1293 |
+
# "$push": {"messages": message},
|
| 1294 |
+
# "$set": {"updated_at": datetime.now()}
|
| 1295 |
+
# }
|
| 1296 |
+
# )
|
| 1297 |
|
| 1298 |
+
# return result.modified_count > 0
|
| 1299 |
+
|
| 1300 |
+
# async def get_conversation_history(
|
| 1301 |
+
# self,
|
| 1302 |
+
# conversation_id: str,
|
| 1303 |
+
# max_messages: int = None
|
| 1304 |
+
# ) -> List[Dict]:
|
| 1305 |
+
# """
|
| 1306 |
+
# Get conversation history (messages only).
|
| 1307 |
|
| 1308 |
+
# Args:
|
| 1309 |
+
# conversation_id: Conversation ID
|
| 1310 |
+
# max_messages: Optional limit on number of messages
|
| 1311 |
|
| 1312 |
+
# Returns:
|
| 1313 |
+
# list: List of messages
|
| 1314 |
+
# """
|
| 1315 |
+
# conversation = await self.get_conversation(conversation_id)
|
| 1316 |
|
| 1317 |
+
# if not conversation:
|
| 1318 |
+
# return []
|
| 1319 |
|
| 1320 |
+
# messages = conversation.get("messages", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1321 |
|
| 1322 |
+
# if max_messages:
|
| 1323 |
+
# messages = messages[-max_messages:]
|
| 1324 |
|
| 1325 |
+
# return messages
|
| 1326 |
+
|
| 1327 |
+
# async def delete_conversation(self, conversation_id: str) -> bool:
|
| 1328 |
+
# """
|
| 1329 |
+
# Soft delete a conversation (mark as deleted, don't actually delete).
|
| 1330 |
|
| 1331 |
+
# Args:
|
| 1332 |
+
# conversation_id: Conversation ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1333 |
|
| 1334 |
+
# Returns:
|
| 1335 |
+
# bool: Success status
|
| 1336 |
+
# """
|
| 1337 |
+
# result = await self.conversations.update_one(
|
| 1338 |
+
# {"conversation_id": conversation_id},
|
| 1339 |
+
# {
|
| 1340 |
+
# "$set": {
|
| 1341 |
+
# "status": "deleted",
|
| 1342 |
+
# "deleted_at": datetime.now()
|
| 1343 |
+
# }
|
| 1344 |
+
# }
|
| 1345 |
+
# )
|
| 1346 |
+
|
| 1347 |
+
# return result.modified_count > 0
|
| 1348 |
|
| 1349 |
+
# # ========================================================================
|
| 1350 |
+
# # RETRIEVAL LOGS (for RL training)
|
| 1351 |
+
# # ========================================================================
|
| 1352 |
|
| 1353 |
+
# async def log_retrieval(
|
| 1354 |
+
# self,
|
| 1355 |
+
# log_data: Dict
|
| 1356 |
+
# ) -> str:
|
| 1357 |
+
# """
|
| 1358 |
+
# Log a retrieval operation (for RL training and analysis).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1359 |
|
| 1360 |
+
# Args:
|
| 1361 |
+
# log_data: Log data dict
|
| 1362 |
+
# {
|
| 1363 |
+
# 'conversation_id': str,
|
| 1364 |
+
# 'user_id': str,
|
| 1365 |
+
# 'query': str,
|
| 1366 |
+
# 'policy_action': 'FETCH' or 'NO_FETCH',
|
| 1367 |
+
# 'policy_confidence': float,
|
| 1368 |
+
# 'documents_retrieved': int,
|
| 1369 |
+
# 'top_doc_score': float or None,
|
| 1370 |
+
# 'retrieved_docs_metadata': list,
|
| 1371 |
+
# 'response': str,
|
| 1372 |
+
# 'retrieval_time_ms': float,
|
| 1373 |
+
# 'generation_time_ms': float,
|
| 1374 |
+
# 'total_time_ms': float,
|
| 1375 |
+
# 'timestamp': datetime
|
| 1376 |
+
# }
|
| 1377 |
|
| 1378 |
+
# Returns:
|
| 1379 |
+
# str: Log ID
|
| 1380 |
+
# """
|
| 1381 |
+
# # Add timestamp if not present
|
| 1382 |
+
# if "timestamp" not in log_data:
|
| 1383 |
+
# log_data["timestamp"] = datetime.now()
|
| 1384 |
|
| 1385 |
+
# # Generate log ID
|
| 1386 |
+
# log_id = str(uuid.uuid4())
|
| 1387 |
+
# log_data["log_id"] = log_id
|
| 1388 |
|
| 1389 |
+
# # Insert log
|
| 1390 |
+
# await self.retrieval_logs.insert_one(log_data)
|
| 1391 |
|
| 1392 |
+
# return log_id
|
| 1393 |
|
| 1394 |
+
# async def get_retrieval_logs(
|
| 1395 |
+
# self,
|
| 1396 |
+
# conversation_id: Optional[str] = None,
|
| 1397 |
+
# user_id: Optional[str] = None,
|
| 1398 |
+
# limit: int = 100,
|
| 1399 |
+
# skip: int = 0
|
| 1400 |
+
# ) -> List[Dict]:
|
| 1401 |
+
# """
|
| 1402 |
+
# Get retrieval logs (for analysis and RL training).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1403 |
|
| 1404 |
+
# Args:
|
| 1405 |
+
# conversation_id: Optional filter by conversation
|
| 1406 |
+
# user_id: Optional filter by user
|
| 1407 |
+
# limit: Maximum number of logs
|
| 1408 |
+
# skip: Number of logs to skip
|
| 1409 |
|
| 1410 |
+
# Returns:
|
| 1411 |
+
# list: List of log documents
|
| 1412 |
+
# """
|
| 1413 |
+
# # Build query
|
| 1414 |
+
# query = {}
|
| 1415 |
+
# if conversation_id:
|
| 1416 |
+
# query["conversation_id"] = conversation_id
|
| 1417 |
+
# if user_id:
|
| 1418 |
+
# query["user_id"] = user_id
|
| 1419 |
|
| 1420 |
+
# # Fetch logs
|
| 1421 |
+
# cursor = self.retrieval_logs.find(query).sort("timestamp", -1).skip(skip).limit(limit)
|
| 1422 |
+
# logs = await cursor.to_list(length=limit)
|
| 1423 |
|
| 1424 |
+
# # Convert ObjectIds to strings
|
| 1425 |
+
# for log in logs:
|
| 1426 |
+
# if "_id" in log:
|
| 1427 |
+
# log["_id"] = str(log["_id"])
|
| 1428 |
|
| 1429 |
+
# return logs
|
| 1430 |
|
| 1431 |
+
# async def get_logs_for_rl_training(
|
| 1432 |
+
# self,
|
| 1433 |
+
# min_date: Optional[datetime] = None,
|
| 1434 |
+
# limit: int = 1000
|
| 1435 |
+
# ) -> List[Dict]:
|
| 1436 |
+
# """
|
| 1437 |
+
# Get logs specifically for RL training.
|
| 1438 |
+
# Filters for logs with both policy decision and retrieval results.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1439 |
|
| 1440 |
+
# Args:
|
| 1441 |
+
# min_date: Optional minimum date for logs
|
| 1442 |
+
# limit: Maximum number of logs
|
|
|
|
| 1443 |
|
| 1444 |
+
# Returns:
|
| 1445 |
+
# list: List of log documents suitable for RL training
|
| 1446 |
+
# """
|
| 1447 |
+
# # Build query
|
| 1448 |
+
# query = {
|
| 1449 |
+
# "policy_action": {"$exists": True},
|
| 1450 |
+
# "response": {"$exists": True}
|
| 1451 |
+
# }
|
| 1452 |
|
| 1453 |
+
# if min_date:
|
| 1454 |
+
# query["timestamp"] = {"$gte": min_date}
|
| 1455 |
|
| 1456 |
+
# # Fetch logs
|
| 1457 |
+
# cursor = self.retrieval_logs.find(query).sort("timestamp", -1).limit(limit)
|
| 1458 |
+
# logs = await cursor.to_list(length=limit)
|
| 1459 |
|
| 1460 |
+
# # Convert ObjectIds
|
| 1461 |
+
# for log in logs:
|
| 1462 |
+
# if "_id" in log:
|
| 1463 |
+
# log["_id"] = str(log["_id"])
|
| 1464 |
|
| 1465 |
+
# return logs
|
| 1466 |
|
| 1467 |
+
# # ========================================================================
|
| 1468 |
+
# # ANALYTICS QUERIES
|
| 1469 |
+
# # ========================================================================
|
| 1470 |
|
| 1471 |
+
# async def get_conversation_stats(self, user_id: str) -> Dict:
|
| 1472 |
+
# """
|
| 1473 |
+
# Get conversation statistics for a user.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1474 |
|
| 1475 |
+
# Args:
|
| 1476 |
+
# user_id: User ID
|
|
|
|
|
|
|
| 1477 |
|
| 1478 |
+
# Returns:
|
| 1479 |
+
# dict: Statistics
|
| 1480 |
+
# """
|
| 1481 |
+
# # Count total conversations
|
| 1482 |
+
# total_conversations = await self.conversations.count_documents({
|
| 1483 |
+
# "user_id": user_id,
|
| 1484 |
+
# "status": "active"
|
| 1485 |
+
# })
|
| 1486 |
|
| 1487 |
+
# # Count total messages
|
| 1488 |
+
# pipeline = [
|
| 1489 |
+
# {"$match": {"user_id": user_id, "status": "active"}},
|
| 1490 |
+
# {"$project": {"message_count": {"$size": "$messages"}}}
|
| 1491 |
+
# ]
|
| 1492 |
|
| 1493 |
+
# result = await self.conversations.aggregate(pipeline).to_list(length=None)
|
| 1494 |
+
# total_messages = sum(doc.get("message_count", 0) for doc in result)
|
| 1495 |
|
| 1496 |
+
# return {
|
| 1497 |
+
# "total_conversations": total_conversations,
|
| 1498 |
+
# "total_messages": total_messages,
|
| 1499 |
+
# "avg_messages_per_conversation": total_messages / total_conversations if total_conversations > 0 else 0
|
| 1500 |
+
# }
|
| 1501 |
|
| 1502 |
+
# async def get_policy_stats(self, user_id: Optional[str] = None) -> Dict:
|
| 1503 |
+
# """
|
| 1504 |
+
# Get policy decision statistics.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1505 |
|
| 1506 |
+
# Args:
|
| 1507 |
+
# user_id: Optional user ID filter
|
|
|
|
|
|
|
| 1508 |
|
| 1509 |
+
# Returns:
|
| 1510 |
+
# dict: Policy statistics
|
| 1511 |
+
# """
|
| 1512 |
+
# # Build query
|
| 1513 |
+
# query = {}
|
| 1514 |
+
# if user_id:
|
| 1515 |
+
# query["user_id"] = user_id
|
| 1516 |
|
| 1517 |
+
# # Count FETCH vs NO_FETCH
|
| 1518 |
+
# fetch_count = await self.retrieval_logs.count_documents({
|
| 1519 |
+
# **query,
|
| 1520 |
+
# "policy_action": "FETCH"
|
| 1521 |
+
# })
|
| 1522 |
|
| 1523 |
+
# no_fetch_count = await self.retrieval_logs.count_documents({
|
| 1524 |
+
# **query,
|
| 1525 |
+
# "policy_action": "NO_FETCH"
|
| 1526 |
+
# })
|
| 1527 |
|
| 1528 |
+
# total = fetch_count + no_fetch_count
|
| 1529 |
|
| 1530 |
+
# return {
|
| 1531 |
+
# "fetch_count": fetch_count,
|
| 1532 |
+
# "no_fetch_count": no_fetch_count,
|
| 1533 |
+
# "total": total,
|
| 1534 |
+
# "fetch_rate": fetch_count / total if total > 0 else 0,
|
| 1535 |
+
# "no_fetch_rate": no_fetch_count / total if total > 0 else 0
|
| 1536 |
+
# }
|
| 1537 |
|
| 1538 |
|
| 1539 |
+
# # ============================================================================
|
| 1540 |
+
# # USAGE EXAMPLE (for reference)
|
| 1541 |
+
# # ============================================================================
|
| 1542 |
+
# """
|
| 1543 |
+
# # In your service or API endpoint:
|
| 1544 |
|
| 1545 |
+
# from app.db.repositories.conversation_repository import ConversationRepository
|
| 1546 |
|
| 1547 |
+
# repo = ConversationRepository()
|
| 1548 |
|
| 1549 |
+
# # Create conversation
|
| 1550 |
+
# conv_id = await repo.create_conversation(user_id="user_123")
|
| 1551 |
|
| 1552 |
+
# # Add user message
|
| 1553 |
+
# await repo.add_message(conv_id, {
|
| 1554 |
+
# 'role': 'user',
|
| 1555 |
+
# 'content': 'What is my balance?',
|
| 1556 |
+
# 'timestamp': datetime.now()
|
| 1557 |
+
# })
|
| 1558 |
|
| 1559 |
+
# # Add assistant message
|
| 1560 |
+
# await repo.add_message(conv_id, {
|
| 1561 |
+
# 'role': 'assistant',
|
| 1562 |
+
# 'content': 'Your balance is $1000',
|
| 1563 |
+
# 'timestamp': datetime.now(),
|
| 1564 |
+
# 'metadata': {
|
| 1565 |
+
# 'policy_action': 'FETCH',
|
| 1566 |
+
# 'documents_retrieved': 3
|
| 1567 |
+
# }
|
| 1568 |
+
# })
|
| 1569 |
|
| 1570 |
+
# # Get conversation history
|
| 1571 |
+
# history = await repo.get_conversation_history(conv_id)
|
| 1572 |
|
| 1573 |
+
# # Log retrieval for RL training
|
| 1574 |
+
# await repo.log_retrieval({
|
| 1575 |
+
# 'conversation_id': conv_id,
|
| 1576 |
+
# 'user_id': 'user_123',
|
| 1577 |
+
# 'query': 'What is my balance?',
|
| 1578 |
+
# 'policy_action': 'FETCH',
|
| 1579 |
+
# 'documents_retrieved': 3,
|
| 1580 |
+
# 'response': 'Your balance is $1000'
|
| 1581 |
+
# })
|
| 1582 |
+
# """
|
app/main.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
FastAPI Main Application Entry Point
|
| 3 |
|
| 4 |
Banking RAG Chatbot API with JWT Authentication
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
3. Connects to MongoDB on startup/shutdown
|
| 10 |
-
4. Includes API routers (auth + chat)
|
| 11 |
-
5. Provides health check endpoints
|
| 12 |
"""
|
| 13 |
|
| 14 |
from fastapi import FastAPI, Request
|
|
@@ -30,6 +27,7 @@ async def lifespan(app: FastAPI):
|
|
| 30 |
|
| 31 |
Startup:
|
| 32 |
- Connect to MongoDB Atlas
|
|
|
|
| 33 |
- ML models load lazily on first use
|
| 34 |
|
| 35 |
Shutdown:
|
|
@@ -49,6 +47,13 @@ async def lifespan(app: FastAPI):
|
|
| 49 |
# Connect to MongoDB
|
| 50 |
await connect_to_mongo()
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
print("\n💡 ML Models Info:")
|
| 53 |
print(" Policy Network: Loads on first chat request (lazy loading)")
|
| 54 |
print(" Retriever Model: Loads on first retrieval (lazy loading)")
|
|
@@ -65,7 +70,6 @@ async def lifespan(app: FastAPI):
|
|
| 65 |
print(f"📖 API Docs: https://eeshanyaj-questrag-backend.hf.space/docs")
|
| 66 |
print(f"🏥 Health Check: https://eeshanyaj-questrag-backend.hf.space/health")
|
| 67 |
print(f"🧠 Backend Link: https://eeshanyaj-questrag-backend.hf.space/")
|
| 68 |
-
# print(f"🔑 Login: POST http://localhost:8000/api/v1/auth/login")
|
| 69 |
print("=" * 80 + "\n")
|
| 70 |
|
| 71 |
yield # Application runs here
|
|
@@ -98,16 +102,18 @@ app = FastAPI(
|
|
| 98 |
- 🧠 RL-based Policy Network (BERT)
|
| 99 |
- 🔍 Custom E5 Retriever
|
| 100 |
- ⚡ Groq LLM with HuggingFace Fallback (Llama 3 models)
|
|
|
|
| 101 |
|
| 102 |
**Capabilities:**
|
| 103 |
- Intelligent document retrieval
|
| 104 |
- Context-aware responses
|
| 105 |
-
- Conversation history
|
| 106 |
-
-
|
|
|
|
| 107 |
- User authentication & authorization
|
| 108 |
- Multi-provider LLM with automatic fallback
|
| 109 |
""",
|
| 110 |
-
version="
|
| 111 |
docs_url="/docs",
|
| 112 |
redoc_url="/redoc",
|
| 113 |
lifespan=lifespan
|
|
@@ -130,10 +136,11 @@ app.add_middleware(
|
|
| 130 |
)
|
| 131 |
|
| 132 |
# ============================================================================
|
| 133 |
-
# INCLUDE API ROUTERS
|
| 134 |
# ============================================================================
|
| 135 |
|
| 136 |
-
from app.api.v1 import
|
|
|
|
| 137 |
|
| 138 |
# Auth router (public endpoints - register, login)
|
| 139 |
app.include_router(
|
|
@@ -142,11 +149,11 @@ app.include_router(
|
|
| 142 |
tags=["🔐 Authentication"]
|
| 143 |
)
|
| 144 |
|
| 145 |
-
# Chat router (protected endpoints - requires JWT token)
|
| 146 |
app.include_router(
|
| 147 |
-
|
| 148 |
prefix="/api/v1/chat",
|
| 149 |
-
tags=["💬 Chat"]
|
| 150 |
)
|
| 151 |
|
| 152 |
# ============================================================================
|
|
@@ -159,8 +166,8 @@ async def root():
|
|
| 159 |
Root endpoint - API information and available endpoints
|
| 160 |
"""
|
| 161 |
return {
|
| 162 |
-
"message": "Banking RAG Chatbot API with Authentication",
|
| 163 |
-
"version": "
|
| 164 |
"status": "online",
|
| 165 |
"authentication": "JWT Bearer Token Required for chat endpoints",
|
| 166 |
"llm_provider": "Groq (ChatGroq) with HuggingFace fallback",
|
|
@@ -181,9 +188,13 @@ async def root():
|
|
| 181 |
},
|
| 182 |
"chat": {
|
| 183 |
"send_message": "POST /api/v1/chat/ (requires token)",
|
| 184 |
-
"
|
| 185 |
"list_conversations": "GET /api/v1/chat/conversations (requires token)",
|
| 186 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
},
|
| 188 |
"health": "GET /health"
|
| 189 |
}
|
|
@@ -249,6 +260,7 @@ async def health_check():
|
|
| 249 |
return {
|
| 250 |
"status": "healthy" if is_healthy else "degraded",
|
| 251 |
"api": "online",
|
|
|
|
| 252 |
"mongodb": mongodb_status,
|
| 253 |
"authentication": auth_status,
|
| 254 |
"llm_providers": llm_providers,
|
|
@@ -299,3 +311,306 @@ if __name__ == "__main__":
|
|
| 299 |
port=8000,
|
| 300 |
reload=settings.DEBUG # Auto-reload only in debug mode
|
| 301 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
FastAPI Main Application Entry Point (UPDATED)
|
| 3 |
|
| 4 |
Banking RAG Chatbot API with JWT Authentication
|
| 5 |
|
| 6 |
+
CHANGES:
|
| 7 |
+
- Replaced old chat router with new conversation_routes
|
| 8 |
+
- Added conversation management features
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
from fastapi import FastAPI, Request
|
|
|
|
| 27 |
|
| 28 |
Startup:
|
| 29 |
- Connect to MongoDB Atlas
|
| 30 |
+
- Create indexes for conversations
|
| 31 |
- ML models load lazily on first use
|
| 32 |
|
| 33 |
Shutdown:
|
|
|
|
| 47 |
# Connect to MongoDB
|
| 48 |
await connect_to_mongo()
|
| 49 |
|
| 50 |
+
# Create indexes for conversations (async)
|
| 51 |
+
try:
|
| 52 |
+
from app.db.repositories.conversation_repository import conversation_repository
|
| 53 |
+
await conversation_repository.create_indexes()
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"⚠️ Failed to create conversation indexes: {e}")
|
| 56 |
+
|
| 57 |
print("\n💡 ML Models Info:")
|
| 58 |
print(" Policy Network: Loads on first chat request (lazy loading)")
|
| 59 |
print(" Retriever Model: Loads on first retrieval (lazy loading)")
|
|
|
|
| 70 |
print(f"📖 API Docs: https://eeshanyaj-questrag-backend.hf.space/docs")
|
| 71 |
print(f"🏥 Health Check: https://eeshanyaj-questrag-backend.hf.space/health")
|
| 72 |
print(f"🧠 Backend Link: https://eeshanyaj-questrag-backend.hf.space/")
|
|
|
|
| 73 |
print("=" * 80 + "\n")
|
| 74 |
|
| 75 |
yield # Application runs here
|
|
|
|
| 102 |
- 🧠 RL-based Policy Network (BERT)
|
| 103 |
- 🔍 Custom E5 Retriever
|
| 104 |
- ⚡ Groq LLM with HuggingFace Fallback (Llama 3 models)
|
| 105 |
+
- 📝 Conversation Management (List, Search, Archive, Delete)
|
| 106 |
|
| 107 |
**Capabilities:**
|
| 108 |
- Intelligent document retrieval
|
| 109 |
- Context-aware responses
|
| 110 |
+
- Conversation persistence & history
|
| 111 |
+
- Auto-generated conversation titles
|
| 112 |
+
- Real-time chat with RAG pipeline
|
| 113 |
- User authentication & authorization
|
| 114 |
- Multi-provider LLM with automatic fallback
|
| 115 |
""",
|
| 116 |
+
version="2.0.0",
|
| 117 |
docs_url="/docs",
|
| 118 |
redoc_url="/redoc",
|
| 119 |
lifespan=lifespan
|
|
|
|
| 136 |
)
|
| 137 |
|
| 138 |
# ============================================================================
|
| 139 |
+
# INCLUDE API ROUTERS (UPDATED)
|
| 140 |
# ============================================================================
|
| 141 |
|
| 142 |
+
from app.api.v1 import auth
|
| 143 |
+
from app.api.v1 import conversation_routes # ✅ NEW IMPORT
|
| 144 |
|
| 145 |
# Auth router (public endpoints - register, login)
|
| 146 |
app.include_router(
|
|
|
|
| 149 |
tags=["🔐 Authentication"]
|
| 150 |
)
|
| 151 |
|
| 152 |
+
# Conversation & Chat router (protected endpoints - requires JWT token)
|
| 153 |
app.include_router(
|
| 154 |
+
conversation_routes.router, # ✅ NEW ROUTER
|
| 155 |
prefix="/api/v1/chat",
|
| 156 |
+
tags=["💬 Chat & Conversations"]
|
| 157 |
)
|
| 158 |
|
| 159 |
# ============================================================================
|
|
|
|
| 166 |
Root endpoint - API information and available endpoints
|
| 167 |
"""
|
| 168 |
return {
|
| 169 |
+
"message": "Banking RAG Chatbot API with Authentication & Conversation Management",
|
| 170 |
+
"version": "2.0.0",
|
| 171 |
"status": "online",
|
| 172 |
"authentication": "JWT Bearer Token Required for chat endpoints",
|
| 173 |
"llm_provider": "Groq (ChatGroq) with HuggingFace fallback",
|
|
|
|
| 188 |
},
|
| 189 |
"chat": {
|
| 190 |
"send_message": "POST /api/v1/chat/ (requires token)",
|
| 191 |
+
"create_conversation": "POST /api/v1/chat/conversation (requires token)",
|
| 192 |
"list_conversations": "GET /api/v1/chat/conversations (requires token)",
|
| 193 |
+
"get_conversation": "GET /api/v1/chat/conversation/{id} (requires token)",
|
| 194 |
+
"update_conversation": "PATCH /api/v1/chat/conversation/{id} (requires token)",
|
| 195 |
+
"delete_conversation": "DELETE /api/v1/chat/conversation/{id} (requires token)",
|
| 196 |
+
"search_conversations": "GET /api/v1/chat/conversations/search (requires token)",
|
| 197 |
+
"conversation_stats": "GET /api/v1/chat/conversations/stats (requires token)"
|
| 198 |
},
|
| 199 |
"health": "GET /health"
|
| 200 |
}
|
|
|
|
| 260 |
return {
|
| 261 |
"status": "healthy" if is_healthy else "degraded",
|
| 262 |
"api": "online",
|
| 263 |
+
"version": "2.0.0",
|
| 264 |
"mongodb": mongodb_status,
|
| 265 |
"authentication": auth_status,
|
| 266 |
"llm_providers": llm_providers,
|
|
|
|
| 311 |
port=8000,
|
| 312 |
reload=settings.DEBUG # Auto-reload only in debug mode
|
| 313 |
)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
# """
|
| 317 |
+
# FastAPI Main Application Entry Point
|
| 318 |
+
|
| 319 |
+
# Banking RAG Chatbot API with JWT Authentication
|
| 320 |
+
|
| 321 |
+
# This file:
|
| 322 |
+
# 1. Creates the FastAPI app
|
| 323 |
+
# 2. Configures CORS middleware
|
| 324 |
+
# 3. Connects to MongoDB on startup/shutdown
|
| 325 |
+
# 4. Includes API routers (auth + chat)
|
| 326 |
+
# 5. Provides health check endpoints
|
| 327 |
+
# """
|
| 328 |
+
|
| 329 |
+
# from fastapi import FastAPI, Request
|
| 330 |
+
# from fastapi.middleware.cors import CORSMiddleware
|
| 331 |
+
# from fastapi.responses import JSONResponse
|
| 332 |
+
# from contextlib import asynccontextmanager
|
| 333 |
+
|
| 334 |
+
# from app.config import settings
|
| 335 |
+
# from app.db.mongodb import connect_to_mongo, close_mongo_connection
|
| 336 |
+
|
| 337 |
+
# # ============================================================================
|
| 338 |
+
# # LIFESPAN MANAGER (Startup & Shutdown)
|
| 339 |
+
# # ============================================================================
|
| 340 |
+
|
| 341 |
+
# @asynccontextmanager
|
| 342 |
+
# async def lifespan(app: FastAPI):
|
| 343 |
+
# """
|
| 344 |
+
# Manage application lifespan events.
|
| 345 |
+
|
| 346 |
+
# Startup:
|
| 347 |
+
# - Connect to MongoDB Atlas
|
| 348 |
+
# - ML models load lazily on first use
|
| 349 |
+
|
| 350 |
+
# Shutdown:
|
| 351 |
+
# - Close MongoDB connection
|
| 352 |
+
# - Cleanup resources
|
| 353 |
+
# """
|
| 354 |
+
# # ========================================================================
|
| 355 |
+
# # STARTUP
|
| 356 |
+
# # ========================================================================
|
| 357 |
+
# print("\n" + "=" * 80)
|
| 358 |
+
# print("🚀 STARTING BANKING RAG CHATBOT API")
|
| 359 |
+
# print("=" * 80)
|
| 360 |
+
# print(f"Environment: {settings.ENVIRONMENT}")
|
| 361 |
+
# print(f"Debug Mode: {settings.DEBUG}")
|
| 362 |
+
# print("=" * 80)
|
| 363 |
+
|
| 364 |
+
# # Connect to MongoDB
|
| 365 |
+
# await connect_to_mongo()
|
| 366 |
+
|
| 367 |
+
# print("\n💡 ML Models Info:")
|
| 368 |
+
# print(" Policy Network: Loads on first chat request (lazy loading)")
|
| 369 |
+
# print(" Retriever Model: Loads on first retrieval (lazy loading)")
|
| 370 |
+
# print(" LLM: Groq (ChatGroq) with HuggingFace fallback")
|
| 371 |
+
# print("\n🤖 LLM Configuration:")
|
| 372 |
+
# print(f" Chat Model: {settings.GROQ_CHAT_MODEL} (Llama 3 8B)")
|
| 373 |
+
# print(f" Eval Model: {settings.GROQ_EVAL_MODEL} (Llama 3 70B)")
|
| 374 |
+
# print(f" Groq API Keys: {len(settings.get_groq_api_keys())} configured")
|
| 375 |
+
# print(f" HuggingFace Tokens: {len(settings.get_hf_tokens())} configured")
|
| 376 |
+
# print(f" Fallback: Groq → HuggingFace")
|
| 377 |
+
|
| 378 |
+
# print("\n✅ Backend startup complete!")
|
| 379 |
+
# print("=" * 80)
|
| 380 |
+
# print(f"📖 API Docs: https://eeshanyaj-questrag-backend.hf.space/docs")
|
| 381 |
+
# print(f"🏥 Health Check: https://eeshanyaj-questrag-backend.hf.space/health")
|
| 382 |
+
# print(f"🧠 Backend Link: https://eeshanyaj-questrag-backend.hf.space/")
|
| 383 |
+
# # print(f"🔑 Login: POST http://localhost:8000/api/v1/auth/login")
|
| 384 |
+
# print("=" * 80 + "\n")
|
| 385 |
+
|
| 386 |
+
# yield # Application runs here
|
| 387 |
+
|
| 388 |
+
# # ========================================================================
|
| 389 |
+
# # SHUTDOWN
|
| 390 |
+
# # ========================================================================
|
| 391 |
+
# print("\n" + "=" * 80)
|
| 392 |
+
# print("🛑 SHUTTING DOWN API")
|
| 393 |
+
# print("=" * 80)
|
| 394 |
+
|
| 395 |
+
# # Close MongoDB connection
|
| 396 |
+
# await close_mongo_connection()
|
| 397 |
+
|
| 398 |
+
# print("✅ Shutdown complete")
|
| 399 |
+
# print("=" * 80 + "\n")
|
| 400 |
+
|
| 401 |
+
# # ============================================================================
|
| 402 |
+
# # CREATE FASTAPI APPLICATION
|
| 403 |
+
# # ============================================================================
|
| 404 |
+
|
| 405 |
+
# app = FastAPI(
|
| 406 |
+
# title="Banking RAG Chatbot API",
|
| 407 |
+
# description="""
|
| 408 |
+
# 🤖 AI-powered Banking Assistant with:
|
| 409 |
+
|
| 410 |
+
# **Features:**
|
| 411 |
+
# - 🔐 JWT Authentication (Sign up, Login, Protected routes)
|
| 412 |
+
# - 💬 RAG (Retrieval-Augmented Generation)
|
| 413 |
+
# - 🧠 RL-based Policy Network (BERT)
|
| 414 |
+
# - 🔍 Custom E5 Retriever
|
| 415 |
+
# - ⚡ Groq LLM with HuggingFace Fallback (Llama 3 models)
|
| 416 |
+
|
| 417 |
+
# **Capabilities:**
|
| 418 |
+
# - Intelligent document retrieval
|
| 419 |
+
# - Context-aware responses
|
| 420 |
+
# - Conversation history
|
| 421 |
+
# - Real-time chat
|
| 422 |
+
# - User authentication & authorization
|
| 423 |
+
# - Multi-provider LLM with automatic fallback
|
| 424 |
+
# """,
|
| 425 |
+
# version="1.0.0",
|
| 426 |
+
# docs_url="/docs",
|
| 427 |
+
# redoc_url="/redoc",
|
| 428 |
+
# lifespan=lifespan
|
| 429 |
+
# )
|
| 430 |
+
|
| 431 |
+
# # ============================================================================
|
| 432 |
+
# # CORS MIDDLEWARE
|
| 433 |
+
# # ============================================================================
|
| 434 |
+
|
| 435 |
+
# allowed_origins = settings.get_allowed_origins()
|
| 436 |
+
# print("\n🌐 CORS Configuration:")
|
| 437 |
+
# print(f" Allowed Origins: {allowed_origins}")
|
| 438 |
+
|
| 439 |
+
# app.add_middleware(
|
| 440 |
+
# CORSMiddleware,
|
| 441 |
+
# allow_origins=allowed_origins,
|
| 442 |
+
# allow_credentials=True,
|
| 443 |
+
# allow_methods=["*"],
|
| 444 |
+
# allow_headers=["*"],
|
| 445 |
+
# )
|
| 446 |
+
|
| 447 |
+
# # ============================================================================
|
| 448 |
+
# # INCLUDE API ROUTERS
|
| 449 |
+
# # ============================================================================
|
| 450 |
+
|
| 451 |
+
# from app.api.v1 import chat, auth
|
| 452 |
+
|
| 453 |
+
# # Auth router (public endpoints - register, login)
|
| 454 |
+
# app.include_router(
|
| 455 |
+
# auth.router,
|
| 456 |
+
# prefix="/api/v1/auth",
|
| 457 |
+
# tags=["🔐 Authentication"]
|
| 458 |
+
# )
|
| 459 |
+
|
| 460 |
+
# # Chat router (protected endpoints - requires JWT token)
|
| 461 |
+
# app.include_router(
|
| 462 |
+
# chat.router,
|
| 463 |
+
# prefix="/api/v1/chat",
|
| 464 |
+
# tags=["💬 Chat"]
|
| 465 |
+
# )
|
| 466 |
+
|
| 467 |
+
# # ============================================================================
|
| 468 |
+
# # ROOT ENDPOINTS
|
| 469 |
+
# # ============================================================================
|
| 470 |
+
|
| 471 |
+
# @app.get("/", tags=["📍 Root"])
|
| 472 |
+
# async def root():
|
| 473 |
+
# """
|
| 474 |
+
# Root endpoint - API information and available endpoints
|
| 475 |
+
# """
|
| 476 |
+
# return {
|
| 477 |
+
# "message": "Banking RAG Chatbot API with Authentication",
|
| 478 |
+
# "version": "1.0.0",
|
| 479 |
+
# "status": "online",
|
| 480 |
+
# "authentication": "JWT Bearer Token Required for chat endpoints",
|
| 481 |
+
# "llm_provider": "Groq (ChatGroq) with HuggingFace fallback",
|
| 482 |
+
# "models": {
|
| 483 |
+
# "chat": settings.GROQ_CHAT_MODEL,
|
| 484 |
+
# "evaluation": settings.GROQ_EVAL_MODEL
|
| 485 |
+
# },
|
| 486 |
+
# "documentation": {
|
| 487 |
+
# "swagger_ui": "/docs",
|
| 488 |
+
# "redoc": "/redoc"
|
| 489 |
+
# },
|
| 490 |
+
# "endpoints": {
|
| 491 |
+
# "auth": {
|
| 492 |
+
# "register": "POST /api/v1/auth/register",
|
| 493 |
+
# "login": "POST /api/v1/auth/login",
|
| 494 |
+
# "me": "GET /api/v1/auth/me (requires token)",
|
| 495 |
+
# "logout": "POST /api/v1/auth/logout (requires token)"
|
| 496 |
+
# },
|
| 497 |
+
# "chat": {
|
| 498 |
+
# "send_message": "POST /api/v1/chat/ (requires token)",
|
| 499 |
+
# "get_history": "GET /api/v1/chat/history/{conversation_id} (requires token)",
|
| 500 |
+
# "list_conversations": "GET /api/v1/chat/conversations (requires token)",
|
| 501 |
+
# "delete_conversation": "DELETE /api/v1/chat/conversation/{conversation_id} (requires token)"
|
| 502 |
+
# },
|
| 503 |
+
# "health": "GET /health"
|
| 504 |
+
# }
|
| 505 |
+
# }
|
| 506 |
+
|
| 507 |
+
# @app.get("/health", tags=["🏥 Health"])
|
| 508 |
+
# async def health_check():
|
| 509 |
+
# """
|
| 510 |
+
# Comprehensive health check endpoint
|
| 511 |
+
|
| 512 |
+
# Checks status of:
|
| 513 |
+
# - API service
|
| 514 |
+
# - MongoDB connection
|
| 515 |
+
# - ML models (lazy loaded)
|
| 516 |
+
# - Authentication system
|
| 517 |
+
# - LLM providers (Groq & HuggingFace)
|
| 518 |
+
|
| 519 |
+
# Returns:
|
| 520 |
+
# dict: Health status of all components
|
| 521 |
+
# """
|
| 522 |
+
# from app.db.mongodb import get_database
|
| 523 |
+
|
| 524 |
+
# # Check MongoDB
|
| 525 |
+
# mongodb_status = "connected" if get_database() is not None else "disconnected"
|
| 526 |
+
|
| 527 |
+
# # Check ML models (don't load them, just check readiness)
|
| 528 |
+
# ml_models_status = {
|
| 529 |
+
# "policy_network": "ready (lazy load)",
|
| 530 |
+
# "retriever": "ready (lazy load)",
|
| 531 |
+
# "llm": "ready (API-based)"
|
| 532 |
+
# }
|
| 533 |
+
|
| 534 |
+
# # Check LLM providers
|
| 535 |
+
# llm_providers = {
|
| 536 |
+
# "groq": {
|
| 537 |
+
# "enabled": settings.is_groq_enabled(),
|
| 538 |
+
# "api_keys_configured": len(settings.get_groq_api_keys()),
|
| 539 |
+
# "chat_model": settings.GROQ_CHAT_MODEL,
|
| 540 |
+
# "eval_model": settings.GROQ_EVAL_MODEL
|
| 541 |
+
# },
|
| 542 |
+
# "huggingface": {
|
| 543 |
+
# "enabled": settings.is_hf_enabled(),
|
| 544 |
+
# "tokens_configured": len(settings.get_hf_tokens()),
|
| 545 |
+
# "chat_model": settings.HF_CHAT_MODEL,
|
| 546 |
+
# "eval_model": settings.HF_EVAL_MODEL
|
| 547 |
+
# }
|
| 548 |
+
# }
|
| 549 |
+
|
| 550 |
+
# # Check authentication
|
| 551 |
+
# auth_status = {
|
| 552 |
+
# "jwt_enabled": bool(settings.SECRET_KEY and settings.SECRET_KEY != "your-secret-key-change-in-production"),
|
| 553 |
+
# "algorithm": settings.ALGORITHM,
|
| 554 |
+
# "token_expiry_minutes": settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
| 555 |
+
# }
|
| 556 |
+
|
| 557 |
+
# # Overall health
|
| 558 |
+
# is_healthy = (
|
| 559 |
+
# mongodb_status == "connected" and
|
| 560 |
+
# auth_status["jwt_enabled"] and
|
| 561 |
+
# (llm_providers["groq"]["enabled"] or llm_providers["huggingface"]["enabled"])
|
| 562 |
+
# )
|
| 563 |
+
|
| 564 |
+
# return {
|
| 565 |
+
# "status": "healthy" if is_healthy else "degraded",
|
| 566 |
+
# "api": "online",
|
| 567 |
+
# "mongodb": mongodb_status,
|
| 568 |
+
# "authentication": auth_status,
|
| 569 |
+
# "llm_providers": llm_providers,
|
| 570 |
+
# "ml_models": ml_models_status,
|
| 571 |
+
# "environment": settings.ENVIRONMENT,
|
| 572 |
+
# "debug_mode": settings.DEBUG
|
| 573 |
+
# }
|
| 574 |
+
|
| 575 |
+
# # ============================================================================
|
| 576 |
+
# # GLOBAL EXCEPTION HANDLER
|
| 577 |
+
# # ============================================================================
|
| 578 |
+
|
| 579 |
+
# @app.exception_handler(Exception)
|
| 580 |
+
# async def global_exception_handler(request: Request, exc: Exception):
|
| 581 |
+
# """
|
| 582 |
+
# Global exception handler for unhandled errors
|
| 583 |
+
# """
|
| 584 |
+
# print(f"\n❌ Unhandled Exception:")
|
| 585 |
+
# print(f" Path: {request.url.path}")
|
| 586 |
+
# print(f" Error: {str(exc)}")
|
| 587 |
+
|
| 588 |
+
# if settings.DEBUG:
|
| 589 |
+
# import traceback
|
| 590 |
+
# traceback.print_exc()
|
| 591 |
+
|
| 592 |
+
# return JSONResponse(
|
| 593 |
+
# status_code=500,
|
| 594 |
+
# content={
|
| 595 |
+
# "error": "Internal Server Error",
|
| 596 |
+
# "detail": str(exc) if settings.DEBUG else "An unexpected error occurred",
|
| 597 |
+
# "path": str(request.url.path)
|
| 598 |
+
# }
|
| 599 |
+
# )
|
| 600 |
+
|
| 601 |
+
# # ============================================================================
|
| 602 |
+
# # MAIN ENTRY POINT (for direct execution)
|
| 603 |
+
# # ============================================================================
|
| 604 |
+
|
| 605 |
+
# if __name__ == "__main__":
|
| 606 |
+
# import uvicorn
|
| 607 |
+
|
| 608 |
+
# print("\n🚀 Starting server directly...")
|
| 609 |
+
# print(" Note: For production, use: uvicorn app.main:app --host 0.0.0.0 --port 8000")
|
| 610 |
+
|
| 611 |
+
# uvicorn.run(
|
| 612 |
+
# "app.main:app",
|
| 613 |
+
# host="0.0.0.0",
|
| 614 |
+
# port=8000,
|
| 615 |
+
# reload=settings.DEBUG # Auto-reload only in debug mode
|
| 616 |
+
# )
|
app/models/conversation.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Conversation Models for MongoDB
|
| 3 |
+
|
| 4 |
+
Handles conversation persistence with:
|
| 5 |
+
- Auto-generated titles from first message
|
| 6 |
+
- Message metadata (policy actions, retrieval stats)
|
| 7 |
+
- Archive/unarchive support
|
| 8 |
+
- Search indexing ready
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import List, Optional, Dict, Any
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
from bson import ObjectId
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# ============================================================================
|
| 18 |
+
# CUSTOM TYPES
|
| 19 |
+
# ============================================================================
|
| 20 |
+
|
| 21 |
+
class PyObjectId(ObjectId):
|
| 22 |
+
"""Custom ObjectId type for Pydantic validation"""
|
| 23 |
+
|
| 24 |
+
@classmethod
|
| 25 |
+
def __get_validators__(cls):
|
| 26 |
+
yield cls.validate
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def validate(cls, v):
|
| 30 |
+
if not ObjectId.is_valid(v):
|
| 31 |
+
raise ValueError("Invalid ObjectId")
|
| 32 |
+
return ObjectId(v)
|
| 33 |
+
|
| 34 |
+
@classmethod
|
| 35 |
+
def __modify_schema__(cls, field_schema):
|
| 36 |
+
field_schema.update(type="string")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ============================================================================
|
| 40 |
+
# MESSAGE MODEL
|
| 41 |
+
# ============================================================================
|
| 42 |
+
|
| 43 |
+
class Message(BaseModel):
|
| 44 |
+
"""
|
| 45 |
+
Single message in a conversation.
|
| 46 |
+
|
| 47 |
+
Contains:
|
| 48 |
+
- User/assistant content
|
| 49 |
+
- Metadata from RAG pipeline (policy action, retrieval stats)
|
| 50 |
+
- Timestamp
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
role: str = Field(..., description="Role: 'user' or 'assistant'")
|
| 54 |
+
content: str = Field(..., description="Message content")
|
| 55 |
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
| 56 |
+
|
| 57 |
+
# Metadata from RAG pipeline (only for assistant messages)
|
| 58 |
+
metadata: Optional[Dict[str, Any]] = Field(
|
| 59 |
+
default=None,
|
| 60 |
+
description="RAG metadata: policy_action, confidence, docs_retrieved, etc."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
class Config:
|
| 64 |
+
json_encoders = {
|
| 65 |
+
datetime: lambda v: v.isoformat()
|
| 66 |
+
}
|
| 67 |
+
schema_extra = {
|
| 68 |
+
"example": {
|
| 69 |
+
"role": "user",
|
| 70 |
+
"content": "What is my account balance?",
|
| 71 |
+
"timestamp": "2024-01-15T10:30:00",
|
| 72 |
+
"metadata": None
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ============================================================================
|
| 78 |
+
# CONVERSATION MODEL (MongoDB Document)
|
| 79 |
+
# ============================================================================
|
| 80 |
+
|
| 81 |
+
class Conversation(BaseModel):
|
| 82 |
+
"""
|
| 83 |
+
Full conversation document stored in MongoDB.
|
| 84 |
+
|
| 85 |
+
Features:
|
| 86 |
+
- Auto-generated title from first user message
|
| 87 |
+
- Message history with metadata
|
| 88 |
+
- Archive/active status
|
| 89 |
+
- User association
|
| 90 |
+
- Search-ready structure
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
id: Optional[PyObjectId] = Field(alias="_id", default=None)
|
| 94 |
+
user_id: str = Field(..., description="User ID who owns this conversation")
|
| 95 |
+
title: str = Field(..., description="Conversation title (auto-generated or custom)")
|
| 96 |
+
|
| 97 |
+
messages: List[Message] = Field(
|
| 98 |
+
default_factory=list,
|
| 99 |
+
description="List of messages in chronological order"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Status flags
|
| 103 |
+
is_archived: bool = Field(default=False, description="Is conversation archived?")
|
| 104 |
+
is_deleted: bool = Field(default=False, description="Soft delete flag")
|
| 105 |
+
|
| 106 |
+
# Timestamps
|
| 107 |
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
| 108 |
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
| 109 |
+
last_message_at: Optional[datetime] = Field(default=None)
|
| 110 |
+
|
| 111 |
+
# Metadata
|
| 112 |
+
message_count: int = Field(default=0, description="Total messages (excluding deleted)")
|
| 113 |
+
|
| 114 |
+
class Config:
|
| 115 |
+
allow_population_by_field_name = True
|
| 116 |
+
arbitrary_types_allowed = True
|
| 117 |
+
json_encoders = {
|
| 118 |
+
ObjectId: str,
|
| 119 |
+
datetime: lambda v: v.isoformat()
|
| 120 |
+
}
|
| 121 |
+
schema_extra = {
|
| 122 |
+
"example": {
|
| 123 |
+
"user_id": "user_123",
|
| 124 |
+
"title": "Account Balance Inquiry",
|
| 125 |
+
"messages": [
|
| 126 |
+
{
|
| 127 |
+
"role": "user",
|
| 128 |
+
"content": "What is my account balance?",
|
| 129 |
+
"timestamp": "2024-01-15T10:30:00"
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"role": "assistant",
|
| 133 |
+
"content": "Your current account balance is...",
|
| 134 |
+
"timestamp": "2024-01-15T10:30:05",
|
| 135 |
+
"metadata": {
|
| 136 |
+
"policy_action": "FETCH",
|
| 137 |
+
"confidence": 0.95,
|
| 138 |
+
"documents_retrieved": 3
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
],
|
| 142 |
+
"is_archived": False,
|
| 143 |
+
"created_at": "2024-01-15T10:30:00",
|
| 144 |
+
"updated_at": "2024-01-15T10:30:05",
|
| 145 |
+
"message_count": 2
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ============================================================================
|
| 151 |
+
# REQUEST/RESPONSE MODELS (for API)
|
| 152 |
+
# ============================================================================
|
| 153 |
+
|
| 154 |
+
class CreateConversationRequest(BaseModel):
|
| 155 |
+
"""Request body for creating a new conversation"""
|
| 156 |
+
|
| 157 |
+
title: Optional[str] = Field(
|
| 158 |
+
default=None,
|
| 159 |
+
description="Optional custom title. If not provided, will be auto-generated from first message",
|
| 160 |
+
max_length=100
|
| 161 |
+
)
|
| 162 |
+
first_message: Optional[str] = Field(
|
| 163 |
+
default=None,
|
| 164 |
+
description="Optional first user message to start the conversation",
|
| 165 |
+
max_length=1000
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
class Config:
|
| 169 |
+
schema_extra = {
|
| 170 |
+
"example": {
|
| 171 |
+
"title": "Savings Account Help",
|
| 172 |
+
"first_message": "How do I open a savings account?"
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class AddMessageRequest(BaseModel):
|
| 178 |
+
"""Request body for adding a message to conversation"""
|
| 179 |
+
|
| 180 |
+
message: str = Field(..., description="User message to add")
|
| 181 |
+
|
| 182 |
+
class Config:
|
| 183 |
+
schema_extra = {
|
| 184 |
+
"example": {
|
| 185 |
+
"message": "What are the interest rates?"
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class UpdateConversationRequest(BaseModel):
|
| 191 |
+
"""Request body for updating conversation properties"""
|
| 192 |
+
|
| 193 |
+
title: Optional[str] = Field(default=None, description="New title")
|
| 194 |
+
is_archived: Optional[bool] = Field(default=None, description="Archive status")
|
| 195 |
+
|
| 196 |
+
class Config:
|
| 197 |
+
schema_extra = {
|
| 198 |
+
"example": {
|
| 199 |
+
"title": "Fixed Deposit Rates Discussion"
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class ConversationResponse(BaseModel):
|
| 205 |
+
"""Response model for single conversation"""
|
| 206 |
+
|
| 207 |
+
id: str = Field(..., description="Conversation ID")
|
| 208 |
+
user_id: str
|
| 209 |
+
title: str
|
| 210 |
+
messages: List[Message]
|
| 211 |
+
is_archived: bool
|
| 212 |
+
created_at: datetime
|
| 213 |
+
updated_at: datetime
|
| 214 |
+
last_message_at: Optional[datetime]
|
| 215 |
+
message_count: int
|
| 216 |
+
|
| 217 |
+
class Config:
|
| 218 |
+
json_encoders = {
|
| 219 |
+
datetime: lambda v: v.isoformat()
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class ConversationListResponse(BaseModel):
|
| 224 |
+
"""Response model for list of conversations (without full messages)"""
|
| 225 |
+
|
| 226 |
+
id: str
|
| 227 |
+
user_id: str
|
| 228 |
+
title: str
|
| 229 |
+
preview: str = Field(..., description="Last message preview (first 100 chars)")
|
| 230 |
+
is_archived: bool
|
| 231 |
+
created_at: datetime
|
| 232 |
+
updated_at: datetime
|
| 233 |
+
last_message_at: Optional[datetime]
|
| 234 |
+
message_count: int
|
| 235 |
+
|
| 236 |
+
class Config:
|
| 237 |
+
json_encoders = {
|
| 238 |
+
datetime: lambda v: v.isoformat()
|
| 239 |
+
}
|
| 240 |
+
schema_extra = {
|
| 241 |
+
"example": {
|
| 242 |
+
"id": "507f1f77bcf86cd799439011",
|
| 243 |
+
"user_id": "user_123",
|
| 244 |
+
"title": "Account Balance Inquiry",
|
| 245 |
+
"preview": "What is my current account balance?",
|
| 246 |
+
"is_archived": False,
|
| 247 |
+
"created_at": "2024-01-15T10:30:00",
|
| 248 |
+
"updated_at": "2024-01-15T10:35:00",
|
| 249 |
+
"last_message_at": "2024-01-15T10:35:00",
|
| 250 |
+
"message_count": 6
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class ConversationListResult(BaseModel):
|
| 256 |
+
"""Paginated list of conversations"""
|
| 257 |
+
|
| 258 |
+
conversations: List[ConversationListResponse]
|
| 259 |
+
total: int = Field(..., description="Total conversations matching filter")
|
| 260 |
+
page: int = Field(default=1, description="Current page number")
|
| 261 |
+
page_size: int = Field(default=20, description="Items per page")
|
| 262 |
+
has_more: bool = Field(..., description="Are there more pages?")
|
| 263 |
+
|
| 264 |
+
class Config:
|
| 265 |
+
schema_extra = {
|
| 266 |
+
"example": {
|
| 267 |
+
"conversations": [],
|
| 268 |
+
"total": 42,
|
| 269 |
+
"page": 1,
|
| 270 |
+
"page_size": 20,
|
| 271 |
+
"has_more": True
|
| 272 |
+
}
|
| 273 |
+
}
|
app/services/conversation_service.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Conversation Service - Business Logic Layer (UPDATED)
|
| 3 |
+
|
| 4 |
+
OPTIMIZED:
|
| 5 |
+
- Better error handling
|
| 6 |
+
- Smart title generation with fallbacks
|
| 7 |
+
- Async LLM title generation (optional)
|
| 8 |
+
- User verification in all operations
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
from typing import Optional, Dict, Any
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
from app.db.repositories.conversation_repository import conversation_repository
|
| 16 |
+
from app.models.conversation import (
|
| 17 |
+
Conversation,
|
| 18 |
+
Message,
|
| 19 |
+
ConversationListResult,
|
| 20 |
+
CreateConversationRequest,
|
| 21 |
+
UpdateConversationRequest
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# CONVERSATION SERVICE
|
| 27 |
+
# ============================================================================
|
| 28 |
+
|
| 29 |
+
class ConversationService:
|
| 30 |
+
"""
|
| 31 |
+
Business logic for conversation management.
|
| 32 |
+
|
| 33 |
+
Handles validation, auto-titles, and business rules.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self):
|
| 37 |
+
"""Initialize service"""
|
| 38 |
+
self.repository = conversation_repository
|
| 39 |
+
print("✅ ConversationService initialized")
|
| 40 |
+
|
| 41 |
+
# ========================================================================
|
| 42 |
+
# AUTO-TITLE GENERATION
|
| 43 |
+
# ========================================================================
|
| 44 |
+
|
| 45 |
+
def generate_title_from_message(
|
| 46 |
+
self,
|
| 47 |
+
message: str,
|
| 48 |
+
max_length: int = 50
|
| 49 |
+
) -> str:
|
| 50 |
+
"""
|
| 51 |
+
Generate conversation title from first user message.
|
| 52 |
+
|
| 53 |
+
Optimized with better truncation logic.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
message: First user message
|
| 57 |
+
max_length: Maximum title length
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
str: Generated title
|
| 61 |
+
"""
|
| 62 |
+
message = message.strip()
|
| 63 |
+
|
| 64 |
+
if not message:
|
| 65 |
+
return "New Conversation"
|
| 66 |
+
|
| 67 |
+
# Remove extra whitespace
|
| 68 |
+
message = re.sub(r'\s+', ' ', message)
|
| 69 |
+
|
| 70 |
+
# Try first sentence
|
| 71 |
+
sentences = re.split(r'[.!?]+', message)
|
| 72 |
+
first_sentence = sentences[0].strip()
|
| 73 |
+
|
| 74 |
+
# Use more if first sentence too short
|
| 75 |
+
if len(first_sentence) < 15 and len(sentences) > 1:
|
| 76 |
+
first_sentence = f"{first_sentence}. {sentences[1].strip()}"
|
| 77 |
+
|
| 78 |
+
# Truncate smartly
|
| 79 |
+
if len(first_sentence) > max_length:
|
| 80 |
+
title = first_sentence[:max_length].strip()
|
| 81 |
+
# Break at word boundary
|
| 82 |
+
last_space = title.rfind(' ')
|
| 83 |
+
if last_space > max_length * 0.6:
|
| 84 |
+
title = title[:last_space]
|
| 85 |
+
title += "..."
|
| 86 |
+
else:
|
| 87 |
+
title = first_sentence
|
| 88 |
+
|
| 89 |
+
# Capitalize
|
| 90 |
+
if title:
|
| 91 |
+
title = title[0].upper() + title[1:]
|
| 92 |
+
|
| 93 |
+
# Remove trailing punctuation before ellipsis
|
| 94 |
+
title = re.sub(r'[,;:]\.\.\.$', '...', title)
|
| 95 |
+
|
| 96 |
+
return title if title else "New Conversation"
|
| 97 |
+
|
| 98 |
+
async def generate_smart_title(
|
| 99 |
+
self,
|
| 100 |
+
first_message: str,
|
| 101 |
+
llm_manager = None
|
| 102 |
+
) -> str:
|
| 103 |
+
"""
|
| 104 |
+
Generate smart title using LLM (optional).
|
| 105 |
+
|
| 106 |
+
Falls back gracefully if LLM unavailable.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
first_message: First user message
|
| 110 |
+
llm_manager: Optional LLM manager
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
str: Generated title
|
| 114 |
+
"""
|
| 115 |
+
# Try LLM generation if available
|
| 116 |
+
if llm_manager:
|
| 117 |
+
try:
|
| 118 |
+
prompt = f"""Generate a concise title (max 50 chars) for this banking query:
|
| 119 |
+
|
| 120 |
+
"{first_message}"
|
| 121 |
+
|
| 122 |
+
Requirements:
|
| 123 |
+
- Clear and descriptive
|
| 124 |
+
- Banking/finance focused
|
| 125 |
+
- No quotes or formatting
|
| 126 |
+
- Maximum 50 characters
|
| 127 |
+
|
| 128 |
+
Title:"""
|
| 129 |
+
|
| 130 |
+
# Use simple generation (not full chat)
|
| 131 |
+
title = await llm_manager.generate_simple_response(
|
| 132 |
+
prompt=prompt,
|
| 133 |
+
max_tokens=15,
|
| 134 |
+
temperature=0.3
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Clean and validate
|
| 138 |
+
title = title.strip().strip('"\'`')
|
| 139 |
+
title = re.sub(r'\s+', ' ', title)
|
| 140 |
+
|
| 141 |
+
if 5 < len(title) <= 60:
|
| 142 |
+
return title
|
| 143 |
+
|
| 144 |
+
except Exception as e:
|
| 145 |
+
print(f"⚠️ Smart title generation failed: {e}")
|
| 146 |
+
|
| 147 |
+
# Fallback to simple generation
|
| 148 |
+
return self.generate_title_from_message(first_message)
|
| 149 |
+
|
| 150 |
+
# ========================================================================
|
| 151 |
+
# CREATE
|
| 152 |
+
# ========================================================================
|
| 153 |
+
|
| 154 |
+
async def create_conversation(
|
| 155 |
+
self,
|
| 156 |
+
user_id: str,
|
| 157 |
+
request: CreateConversationRequest = None,
|
| 158 |
+
llm_manager = None
|
| 159 |
+
) -> Conversation:
|
| 160 |
+
"""
|
| 161 |
+
Create a new conversation with optional first message.
|
| 162 |
+
|
| 163 |
+
OPTIMIZED: Better title generation + error handling
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
user_id: User ID
|
| 167 |
+
request: Optional create request
|
| 168 |
+
llm_manager: Optional LLM manager
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
Conversation: Created conversation (full object)
|
| 172 |
+
"""
|
| 173 |
+
if request is None:
|
| 174 |
+
request = CreateConversationRequest()
|
| 175 |
+
|
| 176 |
+
# Determine title
|
| 177 |
+
if request.title:
|
| 178 |
+
title = request.title
|
| 179 |
+
elif request.first_message:
|
| 180 |
+
# Auto-generate from message
|
| 181 |
+
title = await self.generate_smart_title(
|
| 182 |
+
request.first_message,
|
| 183 |
+
llm_manager
|
| 184 |
+
)
|
| 185 |
+
else:
|
| 186 |
+
title = f"New Chat - {datetime.now().strftime('%b %d, %H:%M')}"
|
| 187 |
+
|
| 188 |
+
# Create conversation (returns ID string)
|
| 189 |
+
conversation_id = await self.repository.create_conversation(
|
| 190 |
+
user_id=user_id,
|
| 191 |
+
title=title,
|
| 192 |
+
first_message=request.first_message
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
# Fetch full conversation
|
| 196 |
+
conversation = await self.repository.get_conversation_by_id(
|
| 197 |
+
conversation_id,
|
| 198 |
+
user_id
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
if not conversation:
|
| 202 |
+
raise ValueError("Failed to create conversation")
|
| 203 |
+
|
| 204 |
+
return conversation
|
| 205 |
+
|
| 206 |
+
# ========================================================================
|
| 207 |
+
# READ
|
| 208 |
+
# ========================================================================
|
| 209 |
+
|
| 210 |
+
async def get_conversation(
|
| 211 |
+
self,
|
| 212 |
+
conversation_id: str,
|
| 213 |
+
user_id: str
|
| 214 |
+
) -> Optional[Conversation]:
|
| 215 |
+
"""
|
| 216 |
+
Get conversation with user verification.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
conversation_id: Conversation ID
|
| 220 |
+
user_id: User ID (must match owner)
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
Conversation or None
|
| 224 |
+
"""
|
| 225 |
+
return await self.repository.get_conversation_by_id(
|
| 226 |
+
conversation_id,
|
| 227 |
+
user_id
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
async def list_conversations(
|
| 231 |
+
self,
|
| 232 |
+
user_id: str,
|
| 233 |
+
page: int = 1,
|
| 234 |
+
page_size: int = 20,
|
| 235 |
+
include_archived: bool = False
|
| 236 |
+
) -> ConversationListResult:
|
| 237 |
+
"""
|
| 238 |
+
List conversations for user with pagination.
|
| 239 |
+
|
| 240 |
+
Args:
|
| 241 |
+
user_id: User ID
|
| 242 |
+
page: Page number (1-indexed)
|
| 243 |
+
page_size: Items per page
|
| 244 |
+
include_archived: Include archived?
|
| 245 |
+
|
| 246 |
+
Returns:
|
| 247 |
+
ConversationListResult: Paginated list
|
| 248 |
+
"""
|
| 249 |
+
# Validate pagination
|
| 250 |
+
page = max(1, page)
|
| 251 |
+
page_size = min(max(1, page_size), 100) # Cap at 100
|
| 252 |
+
|
| 253 |
+
return await self.repository.list_conversations(
|
| 254 |
+
user_id=user_id,
|
| 255 |
+
page=page,
|
| 256 |
+
page_size=page_size,
|
| 257 |
+
include_archived=include_archived
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
async def search_conversations(
|
| 261 |
+
self,
|
| 262 |
+
user_id: str,
|
| 263 |
+
query: str,
|
| 264 |
+
page: int = 1,
|
| 265 |
+
page_size: int = 20
|
| 266 |
+
) -> ConversationListResult:
|
| 267 |
+
"""
|
| 268 |
+
Search conversations by title/content.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
user_id: User ID
|
| 272 |
+
query: Search query
|
| 273 |
+
page: Page number
|
| 274 |
+
page_size: Items per page
|
| 275 |
+
|
| 276 |
+
Returns:
|
| 277 |
+
ConversationListResult: Search results
|
| 278 |
+
"""
|
| 279 |
+
# Validate query
|
| 280 |
+
if not query or len(query.strip()) < 2:
|
| 281 |
+
# Return empty results for invalid query
|
| 282 |
+
return ConversationListResult(
|
| 283 |
+
conversations=[],
|
| 284 |
+
total=0,
|
| 285 |
+
page=page,
|
| 286 |
+
page_size=page_size,
|
| 287 |
+
has_more=False
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
return await self.repository.search_conversations(
|
| 291 |
+
user_id=user_id,
|
| 292 |
+
query=query.strip(),
|
| 293 |
+
page=page,
|
| 294 |
+
page_size=page_size
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# ========================================================================
|
| 298 |
+
# UPDATE
|
| 299 |
+
# ========================================================================
|
| 300 |
+
|
| 301 |
+
async def add_message_to_conversation(
|
| 302 |
+
self,
|
| 303 |
+
conversation_id: str,
|
| 304 |
+
user_id: str,
|
| 305 |
+
role: str,
|
| 306 |
+
content: str,
|
| 307 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 308 |
+
) -> Optional[Conversation]:
|
| 309 |
+
"""
|
| 310 |
+
Add message to conversation with validation.
|
| 311 |
+
|
| 312 |
+
Args:
|
| 313 |
+
conversation_id: Conversation ID
|
| 314 |
+
user_id: User ID (must match owner)
|
| 315 |
+
role: 'user' or 'assistant'
|
| 316 |
+
content: Message content
|
| 317 |
+
metadata: Optional metadata
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
Updated Conversation or None
|
| 321 |
+
"""
|
| 322 |
+
# Validate role
|
| 323 |
+
if role not in ['user', 'assistant']:
|
| 324 |
+
raise ValueError(f"Invalid role: {role}")
|
| 325 |
+
|
| 326 |
+
# Validate content
|
| 327 |
+
if not content or not content.strip():
|
| 328 |
+
raise ValueError("Message content cannot be empty")
|
| 329 |
+
|
| 330 |
+
# Verify ownership
|
| 331 |
+
conversation = await self.get_conversation(conversation_id, user_id)
|
| 332 |
+
if not conversation:
|
| 333 |
+
return None
|
| 334 |
+
|
| 335 |
+
# Create message
|
| 336 |
+
message = Message(
|
| 337 |
+
role=role,
|
| 338 |
+
content=content.strip(),
|
| 339 |
+
timestamp=datetime.utcnow(),
|
| 340 |
+
metadata=metadata
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Add to repository
|
| 344 |
+
success = await self.repository.add_message(
|
| 345 |
+
conversation_id,
|
| 346 |
+
message.dict()
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
if success:
|
| 350 |
+
return await self.get_conversation(conversation_id, user_id)
|
| 351 |
+
return None
|
| 352 |
+
|
| 353 |
+
async def update_conversation(
|
| 354 |
+
self,
|
| 355 |
+
conversation_id: str,
|
| 356 |
+
user_id: str,
|
| 357 |
+
request: UpdateConversationRequest
|
| 358 |
+
) -> Optional[Conversation]:
|
| 359 |
+
"""
|
| 360 |
+
Update conversation properties.
|
| 361 |
+
|
| 362 |
+
Args:
|
| 363 |
+
conversation_id: Conversation ID
|
| 364 |
+
user_id: User ID (must match owner)
|
| 365 |
+
request: Update request
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
Updated Conversation or None
|
| 369 |
+
"""
|
| 370 |
+
update_data = {}
|
| 371 |
+
|
| 372 |
+
if request.title is not None:
|
| 373 |
+
# Validate title
|
| 374 |
+
title = request.title.strip()
|
| 375 |
+
if not title:
|
| 376 |
+
raise ValueError("Title cannot be empty")
|
| 377 |
+
if len(title) > 100:
|
| 378 |
+
raise ValueError("Title too long (max 100 chars)")
|
| 379 |
+
update_data["title"] = title
|
| 380 |
+
|
| 381 |
+
if request.is_archived is not None:
|
| 382 |
+
update_data["is_archived"] = request.is_archived
|
| 383 |
+
|
| 384 |
+
if not update_data:
|
| 385 |
+
# Nothing to update
|
| 386 |
+
return await self.get_conversation(conversation_id, user_id)
|
| 387 |
+
|
| 388 |
+
return await self.repository.update_conversation(
|
| 389 |
+
conversation_id,
|
| 390 |
+
user_id,
|
| 391 |
+
update_data
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
async def rename_conversation(
|
| 395 |
+
self,
|
| 396 |
+
conversation_id: str,
|
| 397 |
+
user_id: str,
|
| 398 |
+
new_title: str
|
| 399 |
+
) -> Optional[Conversation]:
|
| 400 |
+
"""
|
| 401 |
+
Rename conversation with validation.
|
| 402 |
+
|
| 403 |
+
Args:
|
| 404 |
+
conversation_id: Conversation ID
|
| 405 |
+
user_id: User ID
|
| 406 |
+
new_title: New title
|
| 407 |
+
|
| 408 |
+
Returns:
|
| 409 |
+
Updated Conversation or None
|
| 410 |
+
"""
|
| 411 |
+
# Validate title
|
| 412 |
+
new_title = new_title.strip()
|
| 413 |
+
if not new_title:
|
| 414 |
+
raise ValueError("Title cannot be empty")
|
| 415 |
+
if len(new_title) > 100:
|
| 416 |
+
raise ValueError("Title too long (max 100 chars)")
|
| 417 |
+
|
| 418 |
+
return await self.repository.rename_conversation(
|
| 419 |
+
conversation_id,
|
| 420 |
+
user_id,
|
| 421 |
+
new_title
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
async def archive_conversation(
|
| 425 |
+
self,
|
| 426 |
+
conversation_id: str,
|
| 427 |
+
user_id: str
|
| 428 |
+
) -> Optional[Conversation]:
|
| 429 |
+
"""Archive a conversation."""
|
| 430 |
+
return await self.repository.archive_conversation(
|
| 431 |
+
conversation_id,
|
| 432 |
+
user_id,
|
| 433 |
+
archived=True
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
async def unarchive_conversation(
|
| 437 |
+
self,
|
| 438 |
+
conversation_id: str,
|
| 439 |
+
user_id: str
|
| 440 |
+
) -> Optional[Conversation]:
|
| 441 |
+
"""Unarchive a conversation."""
|
| 442 |
+
return await self.repository.archive_conversation(
|
| 443 |
+
conversation_id,
|
| 444 |
+
user_id,
|
| 445 |
+
archived=False
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
# ========================================================================
|
| 449 |
+
# DELETE
|
| 450 |
+
# ========================================================================
|
| 451 |
+
|
| 452 |
+
async def delete_conversation(
|
| 453 |
+
self,
|
| 454 |
+
conversation_id: str,
|
| 455 |
+
user_id: str,
|
| 456 |
+
permanent: bool = False
|
| 457 |
+
) -> bool:
|
| 458 |
+
"""
|
| 459 |
+
Delete conversation (with ownership verification).
|
| 460 |
+
|
| 461 |
+
Args:
|
| 462 |
+
conversation_id: Conversation ID
|
| 463 |
+
user_id: User ID (must match owner)
|
| 464 |
+
permanent: Hard delete if True
|
| 465 |
+
|
| 466 |
+
Returns:
|
| 467 |
+
bool: True if deleted
|
| 468 |
+
"""
|
| 469 |
+
# Verify ownership first
|
| 470 |
+
conversation = await self.get_conversation(conversation_id, user_id)
|
| 471 |
+
if not conversation:
|
| 472 |
+
return False
|
| 473 |
+
|
| 474 |
+
return await self.repository.delete_conversation(
|
| 475 |
+
conversation_id,
|
| 476 |
+
soft_delete=not permanent
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
# ========================================================================
|
| 480 |
+
# UTILITY
|
| 481 |
+
# ========================================================================
|
| 482 |
+
|
| 483 |
+
async def get_conversation_stats(
|
| 484 |
+
self,
|
| 485 |
+
user_id: str
|
| 486 |
+
) -> Dict[str, int]:
|
| 487 |
+
"""
|
| 488 |
+
Get conversation statistics.
|
| 489 |
+
|
| 490 |
+
Args:
|
| 491 |
+
user_id: User ID
|
| 492 |
+
|
| 493 |
+
Returns:
|
| 494 |
+
dict: Stats with total, active, archived
|
| 495 |
+
"""
|
| 496 |
+
return await self.repository.get_conversation_count(user_id)
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
# ============================================================================
|
| 500 |
+
# GLOBAL SERVICE INSTANCE
|
| 501 |
+
# ============================================================================
|
| 502 |
+
|
| 503 |
+
conversation_service = ConversationService()
|