eeshanyaj commited on
Commit
bcbf1db
·
1 Parent(s): 61a1e05

backend changes, added new features

Browse files
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
- # # Initialize repository
421
- # conversation_repo = ConversationRepository()
 
 
 
 
 
 
 
 
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) # ← 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,
@@ -547,13 +130,13 @@ async def chat_health():
547
  # )
548
 
549
  # # Get conversation history
550
- # history = await conversation_repo.get_conversation_history(
551
  # conversation_id=conversation_id,
552
- # max_messages=10 # Last 5 turns (10 messages)
553
  # )
554
 
555
- # # Save user message to database
556
- # await conversation_repo.add_message(
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 to database
573
- # await conversation_repo.add_message(
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 conversation_repo.log_retrieval({
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 # Re-raise HTTP exceptions
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) # ← 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(
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) # ← 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
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) # ← 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,
@@ -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) # ← 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(
@@ -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
- # # Delete conversation
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
- # # - 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
  # # # ============================================================================
@@ -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: str = Field(..., description="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(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(
@@ -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=request.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': request.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(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(
@@ -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(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
@@ -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(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'],
@@ -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
- # # 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
- # # """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Collections used:
26
- # - conversations: Stores complete conversations with messages
27
- # - retrieval_logs: Logs each retrieval operation (for RL training)
28
  # """
29
 
30
  # def __init__(self):
31
- # """Initialize repository with database connection"""
 
 
 
 
32
  # self.db = get_database()
33
- # self.conversations = self.db["conversations"]
34
- # self.retrieval_logs = self.db["retrieval_logs"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Get all conversations for a user.
99
-
100
- # Args:
101
- # user_id: User ID
102
- # limit: Maximum number of conversations to return
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
- def __init__(self):
535
- """
536
- Initialize repository with database connection.
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
- def _check_connection(self):
554
- """
555
- Check if MongoDB is connected.
556
-
557
- Raises:
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
- # CONVERSATION CRUD OPERATIONS
568
- # ========================================================================
569
 
570
- async def create_conversation(
571
- self,
572
- user_id: str,
573
- conversation_id: Optional[str] = None
574
- ) -> str:
575
- """
576
- Create a new conversation.
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
- Raises:
586
- RuntimeError: If MongoDB not connected
587
- """
588
- self._check_connection()
589
 
590
- if conversation_id is None:
591
- conversation_id = str(uuid.uuid4())
 
 
 
592
 
593
- conversation = {
594
- "conversation_id": conversation_id,
595
- "user_id": user_id,
596
- "messages": [], # Will store all messages
597
- "created_at": datetime.now(),
598
- "updated_at": datetime.now(),
599
- "status": "active" # active, archived, deleted
600
- }
601
 
602
- await self.conversations.insert_one(conversation)
603
 
604
- return conversation_id
605
 
606
- async def get_conversation(self, conversation_id: str) -> Optional[Dict]:
607
- """
608
- Get a conversation by ID.
609
-
610
- Args:
611
- conversation_id: Conversation ID
612
-
613
- Returns:
614
- dict or None: Conversation document
615
 
616
- Raises:
617
- RuntimeError: If MongoDB not connected
618
- """
619
- self._check_connection()
620
 
621
- conversation = await self.conversations.find_one(
622
- {"conversation_id": conversation_id}
623
- )
 
 
 
624
 
625
- # Convert MongoDB ObjectId to string for JSON serialization
626
- if conversation and "_id" in conversation:
627
- conversation["_id"] = str(conversation["_id"])
628
 
629
- return conversation
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
- async def add_message(
692
- self,
693
- conversation_id: str,
694
- message: Dict
695
- ) -> bool:
696
- """
697
- Add a message to a conversation.
 
698
 
699
- Args:
700
- conversation_id: Conversation ID
701
- message: Message dict
702
- {
703
- 'role': 'user' or 'assistant',
704
- 'content': str,
705
- 'timestamp': datetime,
706
- 'metadata': dict (optional - policy_action, docs_retrieved, etc.)
707
- }
708
 
709
- Returns:
710
- bool: Success status
 
 
 
 
711
 
712
- Raises:
713
- RuntimeError: If MongoDB not connected
714
- """
715
- self._check_connection()
716
-
717
- # Ensure timestamp exists
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
- return result.modified_count > 0
731
 
732
- async def get_conversation_history(
733
- self,
734
- conversation_id: str,
735
- max_messages: int = None
736
- ) -> List[Dict]:
737
- """
738
- Get conversation history (messages only).
739
 
740
- Args:
741
- conversation_id: Conversation ID
742
- max_messages: Optional limit on number of messages
 
 
 
 
 
 
743
 
744
- Returns:
745
- list: List of messages
 
 
 
 
746
 
747
- Raises:
748
- RuntimeError: If MongoDB not connected
749
- """
750
- self._check_connection()
 
 
 
 
751
 
752
- conversation = await self.get_conversation(conversation_id)
 
 
 
 
 
 
 
 
753
 
754
- if not conversation:
755
- return []
 
756
 
757
- messages = conversation.get("messages", [])
 
 
 
758
 
759
- if max_messages:
760
- messages = messages[-max_messages:]
761
 
762
- return messages
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
- Args:
769
- conversation_id: Conversation ID
770
 
771
- Returns:
772
- bool: Success status
 
 
 
773
 
774
- Raises:
775
- RuntimeError: If MongoDB not connected
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
- return result.modified_count > 0
 
 
 
 
 
 
 
 
 
 
 
 
 
790
 
791
- # ========================================================================
792
- # RETRIEVAL LOGS (for RL training)
793
- # ========================================================================
794
 
795
- async def log_retrieval(
796
- self,
797
- log_data: Dict
798
- ) -> str:
799
- """
800
- Log a retrieval operation (for RL training and analysis).
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
- Raises:
824
- RuntimeError: If MongoDB not connected
825
- """
826
- self._check_connection()
 
 
 
 
 
 
 
 
 
 
 
 
 
827
 
828
- # Add timestamp if not present
829
- if "timestamp" not in log_data:
830
- log_data["timestamp"] = datetime.now()
 
 
 
831
 
832
- # Generate log ID
833
- log_id = str(uuid.uuid4())
834
- log_data["log_id"] = log_id
835
 
836
- # Insert log
837
- await self.retrieval_logs.insert_one(log_data)
838
 
839
- return log_id
840
 
841
- async def get_retrieval_logs(
842
- self,
843
- conversation_id: Optional[str] = None,
844
- user_id: Optional[str] = None,
845
- limit: int = 100,
846
- skip: int = 0
847
- ) -> List[Dict]:
848
- """
849
- Get retrieval logs (for analysis and RL training).
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
- Raises:
861
- RuntimeError: If MongoDB not connected
862
- """
863
- self._check_connection()
 
864
 
865
- # Build query
866
- query = {}
867
- if conversation_id:
868
- query["conversation_id"] = conversation_id
869
- if user_id:
870
- query["user_id"] = user_id
 
 
 
871
 
872
- # Fetch logs
873
- cursor = self.retrieval_logs.find(query).sort("timestamp", -1).skip(skip).limit(limit)
874
- logs = await cursor.to_list(length=limit)
875
 
876
- # Convert ObjectIds to strings
877
- for log in logs:
878
- if "_id" in log:
879
- log["_id"] = str(log["_id"])
880
 
881
- return logs
882
 
883
- async def get_logs_for_rl_training(
884
- self,
885
- min_date: Optional[datetime] = None,
886
- limit: int = 1000
887
- ) -> List[Dict]:
888
- """
889
- Get logs specifically for RL training.
890
- Filters for logs with both policy decision and retrieval results.
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
- Raises:
900
- RuntimeError: If MongoDB not connected
901
- """
902
- self._check_connection()
903
 
904
- # Build query
905
- query = {
906
- "policy_action": {"$exists": True},
907
- "response": {"$exists": True}
908
- }
 
 
 
909
 
910
- if min_date:
911
- query["timestamp"] = {"$gte": min_date}
912
 
913
- # Fetch logs
914
- cursor = self.retrieval_logs.find(query).sort("timestamp", -1).limit(limit)
915
- logs = await cursor.to_list(length=limit)
916
 
917
- # Convert ObjectIds
918
- for log in logs:
919
- if "_id" in log:
920
- log["_id"] = str(log["_id"])
921
 
922
- return logs
923
 
924
- # ========================================================================
925
- # ANALYTICS QUERIES
926
- # ========================================================================
927
 
928
- async def get_conversation_stats(self, user_id: str) -> Dict:
929
- """
930
- Get conversation statistics for a user.
931
-
932
- Args:
933
- user_id: User ID
934
-
935
- Returns:
936
- dict: Statistics
937
 
938
- Raises:
939
- RuntimeError: If MongoDB not connected
940
- """
941
- self._check_connection()
942
 
943
- # Count total conversations
944
- total_conversations = await self.conversations.count_documents({
945
- "user_id": user_id,
946
- "status": "active"
947
- })
 
 
 
948
 
949
- # Count total messages
950
- pipeline = [
951
- {"$match": {"user_id": user_id, "status": "active"}},
952
- {"$project": {"message_count": {"$size": "$messages"}}}
953
- ]
954
 
955
- result = await self.conversations.aggregate(pipeline).to_list(length=None)
956
- total_messages = sum(doc.get("message_count", 0) for doc in result)
957
 
958
- return {
959
- "total_conversations": total_conversations,
960
- "total_messages": total_messages,
961
- "avg_messages_per_conversation": total_messages / total_conversations if total_conversations > 0 else 0
962
- }
963
 
964
- async def get_policy_stats(self, user_id: Optional[str] = None) -> Dict:
965
- """
966
- Get policy decision statistics.
967
-
968
- Args:
969
- user_id: Optional user ID filter
970
-
971
- Returns:
972
- dict: Policy statistics
973
 
974
- Raises:
975
- RuntimeError: If MongoDB not connected
976
- """
977
- self._check_connection()
978
 
979
- # Build query
980
- query = {}
981
- if user_id:
982
- query["user_id"] = user_id
 
 
 
983
 
984
- # Count FETCH vs NO_FETCH
985
- fetch_count = await self.retrieval_logs.count_documents({
986
- **query,
987
- "policy_action": "FETCH"
988
- })
989
 
990
- no_fetch_count = await self.retrieval_logs.count_documents({
991
- **query,
992
- "policy_action": "NO_FETCH"
993
- })
994
 
995
- total = fetch_count + no_fetch_count
996
 
997
- return {
998
- "fetch_count": fetch_count,
999
- "no_fetch_count": no_fetch_count,
1000
- "total": total,
1001
- "fetch_rate": fetch_count / total if total > 0 else 0,
1002
- "no_fetch_rate": no_fetch_count / total if total > 0 else 0
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
- 'role': 'user',
1022
- 'content': 'What is my balance?',
1023
- 'timestamp': datetime.now()
1024
- })
1025
 
1026
- # Add assistant message
1027
- await repo.add_message(conv_id, {
1028
- 'role': 'assistant',
1029
- 'content': 'Your balance is $1000',
1030
- 'timestamp': datetime.now(),
1031
- 'metadata': {
1032
- 'policy_action': 'FETCH',
1033
- 'documents_retrieved': 3
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
- 'conversation_id': conv_id,
1043
- 'user_id': 'user_123',
1044
- 'query': 'What is my balance?',
1045
- 'policy_action': 'FETCH',
1046
- 'documents_retrieved': 3,
1047
- 'response': 'Your balance is $1000'
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
- This file:
7
- 1. Creates the FastAPI app
8
- 2. Configures CORS middleware
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
- - Real-time chat
 
107
  - User authentication & authorization
108
  - Multi-provider LLM with automatic fallback
109
  """,
110
- version="1.0.0",
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 chat, auth
 
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
- chat.router,
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": "1.0.0",
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
- "get_history": "GET /api/v1/chat/history/{conversation_id} (requires token)",
185
  "list_conversations": "GET /api/v1/chat/conversations (requires token)",
186
- "delete_conversation": "DELETE /api/v1/chat/conversation/{conversation_id} (requires token)"
 
 
 
 
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()