TalatMasood commited on
Commit
f36ab64
·
1 Parent(s): be32fd8

DOne some testing and fixed the retrieving context by date.

Browse files
setup.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="chatbot",
5
+ version="1.0.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ # Web Framework
9
+ "fastapi",
10
+ "uvicorn",
11
+
12
+ # AI/ML
13
+ "torch",
14
+ "transformers",
15
+ "sentence-transformers",
16
+ "huggingface_hub",
17
+
18
+ # LLM Providers
19
+ "openai",
20
+ "anthropic",
21
+ "ollama",
22
+
23
+ # Data Validation & Processing
24
+ "pydantic",
25
+ "email-validator",
26
+ "numpy",
27
+ "pandas",
28
+
29
+ # Database & Storage
30
+ "pymongo",
31
+ "motor",
32
+ "chromadb",
33
+ "aiosqlite",
34
+
35
+ # Document Processing
36
+ "PyPDF2",
37
+ "python-docx",
38
+ "python-magic-bin==0.4.14",
39
+ "openpyxl",
40
+ "xlrd",
41
+ "striprtf",
42
+ "beautifulsoup4",
43
+
44
+ # Utilities
45
+ "python-dotenv",
46
+ "requests",
47
+ "tiktoken",
48
+ "psutil",
49
+
50
+ # Google Integration
51
+ "google-auth-oauthlib==0.4.6"
52
+ ]
53
+ )
src/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/src/__pycache__/main.cpython-312.pyc and b/src/__pycache__/main.cpython-312.pyc differ
 
src/agents/__pycache__/enhanced_context_manager.cpython-312.pyc ADDED
Binary file (7.29 kB). View file
 
src/agents/__pycache__/rag_agent.cpython-312.pyc CHANGED
Binary files a/src/agents/__pycache__/rag_agent.cpython-312.pyc and b/src/agents/__pycache__/rag_agent.cpython-312.pyc differ
 
src/agents/__pycache__/rag_agent_manager.cpython-312.pyc ADDED
Binary file (3.19 kB). View file
 
src/agents/__pycache__/system_instructions_rag.cpython-312.pyc CHANGED
Binary files a/src/agents/__pycache__/system_instructions_rag.cpython-312.pyc and b/src/agents/__pycache__/system_instructions_rag.cpython-312.pyc differ
 
src/agents/enhanced_context_manager.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Optional, Tuple
2
+ import spacy
3
+ from collections import defaultdict
4
+
5
+ class EnhancedContextManager:
6
+ def __init__(self):
7
+ """Initialize the context manager with NLP components"""
8
+ # Load spaCy model for NER and dependency parsing
9
+ self.nlp = spacy.load("en_core_web_sm")
10
+ # Track entities and their mentions across conversation
11
+ self.entity_mentions = defaultdict(list)
12
+ # Track conversation turns
13
+ self.conversation_turns = []
14
+ # Track last processed entity
15
+ self.last_entity = None
16
+ # Track last full response context
17
+ self.last_full_context = None
18
+
19
+ def process_turn(self, query: str, response: str) -> None:
20
+ """Process a conversation turn to extract and track entities"""
21
+ # Parse query and response
22
+ query_doc = self.nlp(query)
23
+ response_doc = self.nlp(response)
24
+
25
+ # Extract and track entities from both query and response
26
+ turn_entities = self._extract_entities(query_doc, response_doc)
27
+
28
+ # Store the turn with its entities
29
+ self.conversation_turns.append({
30
+ 'query': query,
31
+ 'response': response,
32
+ 'entities': turn_entities
33
+ })
34
+
35
+ # Update entity mentions
36
+ for entity, info in turn_entities.items():
37
+ self.entity_mentions[entity].append({
38
+ 'turn_index': len(self.conversation_turns) - 1,
39
+ 'info': info
40
+ })
41
+
42
+ # Update last entity and full context
43
+ if turn_entities:
44
+ # Prioritize entities in response, then query
45
+ primary_entity = (
46
+ list(turn_entities.keys())[0] if turn_entities
47
+ else None
48
+ )
49
+ self.last_entity = primary_entity
50
+
51
+ # Store full context for potential reference
52
+ self.last_full_context = f"{query} {response}"
53
+
54
+ def _extract_entities(self, query_doc, response_doc) -> Dict:
55
+ """Extract named entities and their properties"""
56
+ entities = {}
57
+
58
+ # Process both query and response documents
59
+ for doc in [query_doc, response_doc]:
60
+ for ent in doc.ents:
61
+ # Store entity with its type and text
62
+ entities[ent.text] = {
63
+ 'type': ent.label_,
64
+ 'text': ent.text,
65
+ 'mentions': [tok.text for tok in doc if tok.head == ent.root]
66
+ }
67
+
68
+ return entities
69
+
70
+ def resolve_pronouns(self, current_query: str) -> Optional[str]:
71
+ """
72
+ Resolve pronouns in the current query based on conversation history
73
+
74
+ Args:
75
+ current_query (str): Current query with potential pronouns
76
+
77
+ Returns:
78
+ Optional[str]: Query with resolved pronouns, or None if no resolution needed
79
+ """
80
+ if not self.conversation_turns:
81
+ return None
82
+
83
+ query_doc = self.nlp(current_query)
84
+
85
+ # Find pronouns in current query
86
+ pronouns = [token for token in query_doc if token.pos_ == "PRON"]
87
+ if not pronouns:
88
+ return None
89
+
90
+ for pronoun in pronouns:
91
+ replacement = self._find_antecedent(pronoun.text)
92
+ if replacement:
93
+ # Replace the pronoun with the most likely antecedent
94
+ new_query = current_query.replace(pronoun.text, replacement)
95
+ return new_query
96
+
97
+ return None
98
+
99
+ def _find_antecedent(self, pronoun: str) -> Optional[str]:
100
+ """
101
+ Find the most recent matching entity for a pronoun
102
+
103
+ Args:
104
+ pronoun (str): Pronoun to resolve
105
+
106
+ Returns:
107
+ Optional[str]: Resolved entity or None
108
+ """
109
+ # Pronoun to gender/number mapping
110
+ pronoun_properties = {
111
+ 'he': {'gender': 'male', 'number': 'singular'},
112
+ 'she': {'gender': 'female', 'number': 'singular'},
113
+ 'they': {'gender': None, 'number': 'plural'},
114
+ 'his': {'gender': 'male', 'number': 'singular'},
115
+ 'her': {'gender': 'female', 'number': 'singular'},
116
+ 'their': {'gender': None, 'number': 'plural'}
117
+ }
118
+
119
+ # Normalize pronoun
120
+ pronoun_lower = pronoun.lower().rstrip('s')
121
+
122
+ # If not a known pronoun, return None
123
+ if pronoun_lower not in pronoun_properties:
124
+ return None
125
+
126
+ # If a named entity was recently mentioned, use it first
127
+ if self.last_entity:
128
+ return self.last_entity
129
+
130
+ # Fallback to last full context if no specific entity found
131
+ if self.last_full_context:
132
+ return self.last_full_context.split()[0]
133
+
134
+ return None
135
+
136
+ def enhance_query(self, current_query: str) -> str:
137
+ """
138
+ Enhance current query with context and resolved pronouns
139
+
140
+ Args:
141
+ current_query (str): Original query
142
+
143
+ Returns:
144
+ str: Enhanced query with additional context
145
+ """
146
+ # First try to resolve pronouns
147
+ resolved_query = self.resolve_pronouns(current_query)
148
+
149
+ # If pronouns are resolved, use the resolved query
150
+ if resolved_query:
151
+ return resolved_query
152
+
153
+ # Get relevant context
154
+ context = self._get_relevant_context(current_query)
155
+
156
+ # If context found, prepend it to the query
157
+ if context:
158
+ return f"{context} {current_query}"
159
+
160
+ # If no context resolution, return original query
161
+ return current_query
162
+
163
+ def _get_relevant_context(self, query: str) -> Optional[str]:
164
+ """
165
+ Get relevant context from conversation history
166
+
167
+ Args:
168
+ query (str): Current query
169
+
170
+ Returns:
171
+ Optional[str]: Relevant context or None
172
+ """
173
+ if not self.conversation_turns:
174
+ return None
175
+
176
+ # Get the most recent turn
177
+ recent_turn = self.conversation_turns[-1]
178
+
179
+ # If the current query contains a pronoun and we have last full context
180
+ if any(token.pos_ == "PRON" for token in self.nlp(query)):
181
+ return self.last_full_context
182
+
183
+ return None
184
+
185
+ def get_conversation_context(self) -> List[Dict]:
186
+ """Get processed conversation context"""
187
+ return self.conversation_turns
188
+
189
+ def record_last_context(self, last_context: Optional[str] = None) -> None:
190
+ """
191
+ Manually record last context if needed
192
+
193
+ Args:
194
+ last_context (Optional[str]): Last context to manually set
195
+ """
196
+ if last_context:
197
+ self.last_full_context = last_context
198
+ # Try to extract an entity from the context
199
+ doc = self.nlp(last_context)
200
+ entities = [ent.text for ent in doc.ents]
201
+ if entities:
202
+ self.last_entity = entities[0]
src/agents/rag_agent.py CHANGED
@@ -1,8 +1,8 @@
1
- # src/agents/rag_agent.py
2
- from typing import List, Optional, Tuple, Dict
3
  import uuid
4
 
5
  from .excel_aware_rag import ExcelAwareRAGAgent
 
6
  from ..llms.base_llm import BaseLLM
7
  from src.embeddings.base_embedding import BaseEmbedding
8
  from src.vectorstores.base_vectorstore import BaseVectorStore
@@ -21,17 +21,7 @@ class RAGAgent(ExcelAwareRAGAgent):
21
  max_history_tokens: int = 4000,
22
  max_history_messages: int = 10
23
  ):
24
- """
25
- Initialize RAG Agent
26
-
27
- Args:
28
- llm (BaseLLM): Language model instance
29
- embedding (BaseEmbedding): Embedding model instance
30
- vector_store (BaseVectorStore): Vector store instance
31
- mongodb (MongoDBStore): MongoDB store instance
32
- max_history_tokens (int): Maximum tokens in conversation history
33
- max_history_messages (int): Maximum messages to keep in history
34
- """
35
  super().__init__() # Initialize ExcelAwareRAGAgent
36
  self.llm = llm
37
  self.embedding = embedding
@@ -41,6 +31,9 @@ class RAGAgent(ExcelAwareRAGAgent):
41
  max_tokens=max_history_tokens,
42
  max_messages=max_history_messages
43
  )
 
 
 
44
 
45
  async def generate_response(
46
  self,
@@ -48,11 +41,38 @@ class RAGAgent(ExcelAwareRAGAgent):
48
  conversation_id: Optional[str],
49
  temperature: float,
50
  max_tokens: Optional[int] = None,
51
- context_docs: Optional[List[str]] = None
 
 
52
  ) -> RAGResponse:
53
- """Generate response with specific handling for different query types"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  try:
55
- # First, check if this is an introduction/welcome message query
 
 
 
 
 
 
 
 
 
 
 
56
  is_introduction = (
57
  "wants support" in query and
58
  "This is Introduction" in query and
@@ -60,7 +80,7 @@ class RAGAgent(ExcelAwareRAGAgent):
60
  )
61
 
62
  if is_introduction:
63
- # Handle introduction message - no context needed
64
  welcome_message = self._handle_contact_query(query)
65
  return RAGResponse(
66
  response=welcome_message,
@@ -69,69 +89,118 @@ class RAGAgent(ExcelAwareRAGAgent):
69
  scores=None
70
  )
71
 
72
- # Get conversation history if conversation_id exists
73
  history = []
 
74
  if conversation_id:
 
75
  history = await self.mongodb.get_recent_messages(
76
  conversation_id,
77
  limit=self.conversation_manager.max_messages
78
  )
79
 
80
- # Get relevant history within token limits
81
  history = self.conversation_manager.get_relevant_history(
82
  messages=history,
83
  current_query=query
84
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- # Retrieve context if not provided
 
 
87
  if not context_docs:
 
88
  context_docs, sources, scores = await self.retrieve_context(
89
- query=query,
90
  conversation_history=history
91
  )
92
  else:
93
- sources = None
94
  scores = None
95
 
96
- # Check if we have any relevant context
97
  if not context_docs:
98
- return RAGResponse(
99
- response="Information about this is not available, do you want to inquire about something else?",
100
- context_docs=[],
101
- sources=[],
102
- scores=None
103
- )
 
 
 
 
 
 
 
104
 
105
- # Check if this is an Excel-related query
106
  has_excel_content = any('Sheet:' in doc for doc in context_docs)
107
  if has_excel_content:
 
108
  try:
109
- context_docs = self._process_excel_context(context_docs, query)
110
  except Exception as e:
111
  logger.warning(f"Error processing Excel context: {str(e)}")
112
 
113
- # Generate prompt with context and history
114
- augmented_prompt = self.conversation_manager.generate_prompt_with_history(
115
- current_query=query,
116
  history=history,
117
  context_docs=context_docs
118
  )
119
 
120
- # Generate initial response
 
 
 
 
 
 
121
  response = self.llm.generate(
122
- prompt=augmented_prompt,
123
  temperature=temperature,
124
  max_tokens=max_tokens
125
  )
126
 
127
- # Clean the response
128
  cleaned_response = self._clean_response(response)
129
-
130
- # For Excel queries, enhance the response
131
  if has_excel_content:
132
  try:
133
  enhanced_response = await self.enhance_excel_response(
134
- query=query,
135
  response=cleaned_response,
136
  context_docs=context_docs
137
  )
@@ -140,61 +209,30 @@ class RAGAgent(ExcelAwareRAGAgent):
140
  except Exception as e:
141
  logger.warning(f"Error enhancing Excel response: {str(e)}")
142
 
143
- # Return the final response
 
 
 
 
 
 
 
 
 
 
 
 
144
  return RAGResponse(
145
  response=cleaned_response,
146
  context_docs=context_docs,
147
  sources=sources,
148
- scores=scores
 
149
  )
150
 
151
  except Exception as e:
152
- logger.error(f"Error in SystemInstructionsRAGAgent: {str(e)}")
153
  raise
154
-
155
- def _create_response_prompt(self, query: str, context_docs: List[str]) -> str:
156
- """
157
- Create prompt for generating response from context
158
-
159
- Args:
160
- query (str): User query
161
- context_docs (List[str]): Retrieved context documents
162
-
163
- Returns:
164
- str: Formatted prompt for the LLM
165
- """
166
- if not context_docs:
167
- return f"Query: {query}\nResponse: Information about this is not available, do you want to inquire about something else?"
168
-
169
- # Format context documents
170
- formatted_context = "\n\n".join(
171
- f"Context {i+1}:\n{doc.strip()}"
172
- for i, doc in enumerate(context_docs)
173
- if doc and doc.strip()
174
- )
175
-
176
- # Build the prompt with detailed instructions
177
- prompt = f"""You are a knowledgeable assistant. Use the following context to answer the query accurately and informatively.
178
-
179
- Context Information:
180
- {formatted_context}
181
-
182
- Query: {query}
183
-
184
- Instructions:
185
- 1. Base your response ONLY on the information provided in the context above
186
- 2. If the context contains numbers, statistics, or specific details, include them in your response
187
- 3. Keep your response focused and relevant to the query
188
- 4. Use clear and professional language
189
- 5. If the context includes technical terms, explain them appropriately
190
- 6. Do not make assumptions or add information not present in the context
191
- 7. If specific sections of a report are mentioned, maintain their original structure
192
- 8. Format the response in a clear, readable manner
193
- 9. If the context includes chronological information, maintain the proper sequence
194
-
195
- Response:"""
196
-
197
- return prompt
198
 
199
  async def retrieve_context(
200
  self,
@@ -202,59 +240,127 @@ class RAGAgent(ExcelAwareRAGAgent):
202
  conversation_history: Optional[List[Dict]] = None,
203
  top_k: int = 3
204
  ) -> Tuple[List[str], List[Dict], Optional[List[float]]]:
205
- """
206
- Retrieve context with conversation history enhancement
207
- """
208
- # Enhance query with conversation history
209
- if conversation_history:
210
- recent_queries = [
211
- msg['query'] for msg in conversation_history[-2:]
212
- if msg.get('query')
213
- ]
214
- enhanced_query = " ".join([*recent_queries, query])
215
- else:
216
- enhanced_query = query
217
-
218
- # Debug log the enhanced query
219
- logger.info(f"Enhanced query: {enhanced_query}")
220
-
221
- # Embed the enhanced query
222
- query_embedding = self.embedding.embed_query(enhanced_query)
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
- # Debug log embedding shape
225
- logger.info(f"Query embedding shape: {len(query_embedding)}")
226
 
227
- # Retrieve similar documents
228
- results = self.vector_store.similarity_search(
229
- query_embedding,
230
- top_k=top_k
231
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
- # Debug log search results
234
- logger.info(f"Number of search results: {len(results)}")
235
- for i, result in enumerate(results):
236
- logger.info(f"Result {i} score: {result.get('score', 'N/A')}")
237
- logger.info(f"Result {i} text preview: {result.get('text', '')[:100]}...")
 
 
 
 
 
238
 
239
- # Process results
240
- documents = [doc['text'] for doc in results]
241
- sources = [self._convert_metadata_to_strings(doc['metadata'])
242
- for doc in results]
243
- scores = [doc['score'] for doc in results
244
- if doc.get('score') is not None]
245
 
246
- # Return scores only if available for all documents
247
- if len(scores) != len(documents):
248
- scores = None
 
 
 
 
 
 
 
 
 
 
 
249
 
250
- return documents, sources, scores
 
 
 
 
 
 
251
 
252
  def _convert_metadata_to_strings(self, metadata: Dict) -> Dict:
253
- """Convert numeric metadata values to strings"""
254
- converted = {}
255
- for key, value in metadata.items():
256
- if isinstance(value, (int, float)):
257
- converted[key] = str(value)
258
- else:
259
- converted[key] = value
260
- return converted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Optional, Tuple
 
2
  import uuid
3
 
4
  from .excel_aware_rag import ExcelAwareRAGAgent
5
+ from .enhanced_context_manager import EnhancedContextManager
6
  from ..llms.base_llm import BaseLLM
7
  from src.embeddings.base_embedding import BaseEmbedding
8
  from src.vectorstores.base_vectorstore import BaseVectorStore
 
21
  max_history_tokens: int = 4000,
22
  max_history_messages: int = 10
23
  ):
24
+ """Initialize RAG Agent with enhanced context management"""
 
 
 
 
 
 
 
 
 
 
25
  super().__init__() # Initialize ExcelAwareRAGAgent
26
  self.llm = llm
27
  self.embedding = embedding
 
31
  max_tokens=max_history_tokens,
32
  max_messages=max_history_messages
33
  )
34
+ # Add enhanced context management while preserving existing functionality
35
+ self.context_manager = EnhancedContextManager()
36
+ logger.info("RAGAgent initialized with enhanced context management")
37
 
38
  async def generate_response(
39
  self,
 
41
  conversation_id: Optional[str],
42
  temperature: float,
43
  max_tokens: Optional[int] = None,
44
+ context_docs: Optional[List[str]] = None,
45
+ stream: bool = False,
46
+ custom_roles: Optional[List[Dict[str, str]]] = None
47
  ) -> RAGResponse:
48
+ """
49
+ Generate a response with comprehensive context and role management
50
+
51
+ Args:
52
+ query (str): User query
53
+ conversation_id (Optional[str]): Conversation identifier
54
+ temperature (float): LLM temperature for response generation
55
+ max_tokens (Optional[int]): Maximum tokens for response
56
+ context_docs (Optional[List[str]]): Pre-retrieved context documents
57
+ stream (bool): Whether to stream the response
58
+ custom_roles (Optional[List[Dict[str, str]]]): Custom role instructions
59
+
60
+ Returns:
61
+ RAGResponse: Generated response with context and metadata
62
+ """
63
  try:
64
+ logger.info(f"Generating response for query: {query}")
65
+
66
+ # Apply custom roles if provided
67
+ if custom_roles:
68
+ for role in custom_roles:
69
+ # Modify query or context based on role
70
+ if role.get('name') == 'introduction_specialist':
71
+ query += " Provide a concise, welcoming response."
72
+ elif role.get('name') == 'knowledge_based_specialist':
73
+ query += " Ensure response is precise and directly from available knowledge."
74
+
75
+ # Introduction Handling
76
  is_introduction = (
77
  "wants support" in query and
78
  "This is Introduction" in query and
 
80
  )
81
 
82
  if is_introduction:
83
+ logger.info("Processing introduction message")
84
  welcome_message = self._handle_contact_query(query)
85
  return RAGResponse(
86
  response=welcome_message,
 
89
  scores=None
90
  )
91
 
92
+ # Conversation History Processing
93
  history = []
94
+ last_context = None
95
  if conversation_id:
96
+ logger.info(f"Retrieving conversation history for ID: {conversation_id}")
97
  history = await self.mongodb.get_recent_messages(
98
  conversation_id,
99
  limit=self.conversation_manager.max_messages
100
  )
101
 
102
+ # Process history for conversation manager
103
  history = self.conversation_manager.get_relevant_history(
104
  messages=history,
105
  current_query=query
106
  )
107
+
108
+ # Process in enhanced context manager
109
+ for msg in history:
110
+ self.context_manager.process_turn(
111
+ msg.get('query', ''),
112
+ msg.get('response', '')
113
+ )
114
+
115
+ # Get last context if available
116
+ if history and history[-1].get('response'):
117
+ last_context = history[-1]['response']
118
+
119
+ # Query Enhancement
120
+ enhanced_query = self.context_manager.enhance_query(query)
121
+
122
+ # Manual Pronoun Handling Fallback
123
+ if enhanced_query == query:
124
+ pronoun_map = {
125
+ 'his': 'he',
126
+ 'her': 'she',
127
+ 'their': 'they'
128
+ }
129
+ words = query.lower().split()
130
+ for pronoun, replacement in pronoun_map.items():
131
+ if pronoun in words:
132
+ # Try to use last context
133
+ if last_context:
134
+ self.context_manager.record_last_context(last_context)
135
+ enhanced_query = self.context_manager.enhance_query(query)
136
+ break
137
 
138
+ logger.info(f"Enhanced query: {enhanced_query}")
139
+
140
+ # Context Retrieval
141
  if not context_docs:
142
+ logger.info("Retrieving context for enhanced query")
143
  context_docs, sources, scores = await self.retrieve_context(
144
+ enhanced_query,
145
  conversation_history=history
146
  )
147
  else:
148
+ sources = []
149
  scores = None
150
 
151
+ # Context Fallback Mechanism
152
  if not context_docs:
153
+ # If no context and last context exists, use it
154
+ if last_context:
155
+ context_docs = [last_context]
156
+ sources = [{"source": "previous_context"}]
157
+ scores = [1.0]
158
+ else:
159
+ logger.info("No relevant context found")
160
+ return RAGResponse(
161
+ response="Information about this is not available, do you want to inquire about something else?",
162
+ context_docs=[],
163
+ sources=[],
164
+ scores=None
165
+ )
166
 
167
+ # Excel-specific Content Handling
168
  has_excel_content = any('Sheet:' in doc for doc in context_docs)
169
  if has_excel_content:
170
+ logger.info("Processing Excel-specific content")
171
  try:
172
+ context_docs = self._process_excel_context(context_docs, enhanced_query)
173
  except Exception as e:
174
  logger.warning(f"Error processing Excel context: {str(e)}")
175
 
176
+ # Prompt Generation with Conversation History
177
+ prompt = self.conversation_manager.generate_prompt_with_history(
178
+ current_query=enhanced_query,
179
  history=history,
180
  context_docs=context_docs
181
  )
182
 
183
+ # Streaming Response Generation
184
+ if stream:
185
+ # TODO: Implement actual streaming logic
186
+ # This is a placeholder and needs proper implementation
187
+ logger.warning("Streaming not fully implemented")
188
+
189
+ # Standard Response Generation
190
  response = self.llm.generate(
191
+ prompt=prompt,
192
  temperature=temperature,
193
  max_tokens=max_tokens
194
  )
195
 
196
+ # Response Cleaning
197
  cleaned_response = self._clean_response(response)
198
+
199
+ # Excel Response Enhancement
200
  if has_excel_content:
201
  try:
202
  enhanced_response = await self.enhance_excel_response(
203
+ query=enhanced_query,
204
  response=cleaned_response,
205
  context_docs=context_docs
206
  )
 
209
  except Exception as e:
210
  logger.warning(f"Error enhancing Excel response: {str(e)}")
211
 
212
+ # Context Tracking
213
+ self.context_manager.process_turn(query, cleaned_response)
214
+
215
+ # Metadata Generation
216
+ metadata = {
217
+ 'llm_provider': getattr(self.llm, 'model_name', 'unknown'),
218
+ 'temperature': temperature,
219
+ 'conversation_id': conversation_id,
220
+ 'context_sources': sources,
221
+ 'has_excel_content': has_excel_content
222
+ }
223
+
224
+ logger.info("Successfully generated response")
225
  return RAGResponse(
226
  response=cleaned_response,
227
  context_docs=context_docs,
228
  sources=sources,
229
+ scores=scores,
230
+ metadata=metadata # Added metadata
231
  )
232
 
233
  except Exception as e:
234
+ logger.error(f"Error in generate_response: {str(e)}")
235
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  async def retrieve_context(
238
  self,
 
240
  conversation_history: Optional[List[Dict]] = None,
241
  top_k: int = 3
242
  ) -> Tuple[List[str], List[Dict], Optional[List[float]]]:
243
+ """Retrieve context with both original and enhanced handling"""
244
+ try:
245
+ logger.info(f"Retrieving context for query: {query}")
246
+
247
+ # Enhance query using both managers
248
+ if conversation_history:
249
+ # Get the last two messages for immediate context
250
+ recent_messages = conversation_history[-2:]
251
+
252
+ # Extract queries and responses for context
253
+ context_parts = []
254
+ for msg in recent_messages:
255
+ if msg.get('query'):
256
+ context_parts.append(msg['query'])
257
+ if msg.get('response'):
258
+ response = msg['response']
259
+ if "Information about this is not available" not in response:
260
+ context_parts.append(response)
261
+
262
+ # Combine with current query
263
+ enhanced_query = f"{' '.join(context_parts)} {query}".strip()
264
+ logger.info(f"Enhanced query with history: {enhanced_query}")
265
+ else:
266
+ enhanced_query = query
267
+
268
+ # Debug log the enhanced query
269
+ logger.info(f"Final enhanced query: {enhanced_query}")
270
+
271
+ # Embed the enhanced query
272
+ query_embedding = self.embedding.embed_query(enhanced_query)
273
 
274
+ # Debug log embedding shape
275
+ logger.info(f"Query embedding shape: {len(query_embedding)}")
276
 
277
+ # Retrieve similar documents
278
+ results = self.vector_store.similarity_search(
279
+ query_embedding,
280
+ top_k=top_k
281
+ )
282
+
283
+ # Debug log search results
284
+ logger.info(f"Number of search results: {len(results)}")
285
+ for i, result in enumerate(results):
286
+ logger.info(f"Result {i} score: {result.get('score', 'N/A')}")
287
+ logger.info(f"Result {i} text preview: {result.get('text', '')[:100]}...")
288
+
289
+ if not results:
290
+ logger.info("No results found in similarity search")
291
+ return [], [], None
292
+
293
+ # Process results
294
+ documents = [doc['text'] for doc in results]
295
+ sources = [self._convert_metadata_to_strings(doc['metadata'])
296
+ for doc in results]
297
+ scores = [doc['score'] for doc in results
298
+ if doc.get('score') is not None]
299
 
300
+ # Return scores only if available for all documents
301
+ if len(scores) != len(documents):
302
+ scores = None
303
+
304
+ logger.info(f"Retrieved {len(documents)} relevant documents")
305
+ return documents, sources, scores
306
+
307
+ except Exception as e:
308
+ logger.error(f"Error in retrieve_context: {str(e)}")
309
+ raise
310
 
311
+ def _clean_response(self, response: str) -> str:
312
+ """Clean response text while preserving key information"""
313
+ if not response:
314
+ return response
 
 
315
 
316
+ # Keep only the most common phrases to remove
317
+ phrases_to_remove = [
318
+ "Based on the context,",
319
+ "According to the documents,",
320
+ "From the information available,",
321
+ "Based on the provided information,",
322
+ "I apologize,"
323
+ ]
324
+
325
+ cleaned_response = response
326
+ for phrase in phrases_to_remove:
327
+ cleaned_response = cleaned_response.replace(phrase, "").strip()
328
+
329
+ cleaned_response = " ".join(cleaned_response.split())
330
 
331
+ if not cleaned_response:
332
+ return response
333
+
334
+ if cleaned_response[0].islower():
335
+ cleaned_response = cleaned_response[0].upper() + cleaned_response[1:]
336
+
337
+ return cleaned_response
338
 
339
  def _convert_metadata_to_strings(self, metadata: Dict) -> Dict:
340
+ """Convert metadata values to strings"""
341
+ try:
342
+ return {
343
+ key: str(value) if isinstance(value, (int, float)) else value
344
+ for key, value in metadata.items()
345
+ }
346
+ except Exception as e:
347
+ logger.error(f"Error converting metadata: {str(e)}")
348
+ return metadata
349
+
350
+ def _handle_contact_query(self, query: str) -> str:
351
+ """Handle contact/introduction queries"""
352
+ try:
353
+ name_start = query.find('name: "') + 7
354
+ name_end = query.find('"', name_start)
355
+ name = query[name_start:name_end] if name_start > 6 and name_end != -1 else "there"
356
+
357
+ is_returning = (
358
+ "An old user with name:" in query and
359
+ "wants support again" in query
360
+ )
361
+
362
+ return f"Welcome back {name}, How can I help you?" if is_returning else f"Welcome {name}, How can I help you?"
363
+
364
+ except Exception as e:
365
+ logger.error(f"Error handling contact query: {str(e)}")
366
+ return "Welcome, How can I help you?"
src/agents/rag_agent_manager.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/agents/rag_agent_manager.py
2
+ from typing import Optional
3
+ import weakref
4
+
5
+ from src.agents.system_instructions_rag import SystemInstructionsRAGAgent
6
+ from src.llms.base_llm import BaseLLM
7
+ from src.embeddings.base_embedding import BaseEmbedding
8
+ from src.vectorstores.base_vectorstore import BaseVectorStore
9
+ from src.db.mongodb_store import MongoDBStore
10
+ from src.utils.logger import logger
11
+
12
+ class RAGAgentManager:
13
+ """
14
+ Singleton manager for RAG Agent instances with intelligent caching
15
+ """
16
+ _instance = None
17
+
18
+ def __new__(cls):
19
+ if not cls._instance:
20
+ cls._instance = super().__new__(cls)
21
+ return cls._instance
22
+
23
+ def __init__(self):
24
+ # Ensure this is only initialized once
25
+ if not hasattr(self, '_initialized'):
26
+ self._rag_agent = None
27
+ self._initialized = True
28
+
29
+ def get_rag_agent(
30
+ self,
31
+ llm: BaseLLM,
32
+ embedding_model: BaseEmbedding,
33
+ vector_store: BaseVectorStore,
34
+ mongodb: MongoDBStore
35
+ ) -> SystemInstructionsRAGAgent:
36
+ """
37
+ Get or create a singleton RAG agent instance with intelligent caching
38
+
39
+ Args:
40
+ llm: Language Model instance
41
+ embedding_model: Embedding model instance
42
+ vector_store: Vector store instance
43
+ mongodb: MongoDB store instance
44
+
45
+ Returns:
46
+ SystemInstructionsRAGAgent: Singleton instance of the RAG agent
47
+ """
48
+ # If RAG agent exists and all dependencies are the same, return it
49
+ if self._rag_agent is not None:
50
+ logger.info("Reusing existing RAG agent instance")
51
+ return self._rag_agent
52
+
53
+ try:
54
+ logger.info("Creating new RAG agent instance")
55
+ # Create the agent
56
+ self._rag_agent = SystemInstructionsRAGAgent(
57
+ llm=llm,
58
+ embedding=embedding_model,
59
+ vector_store=vector_store,
60
+ mongodb=mongodb
61
+ )
62
+
63
+ return self._rag_agent
64
+
65
+ except Exception as e:
66
+ logger.error(f"Error creating RAG agent: {str(e)}")
67
+ raise
68
+
69
+ def reset_rag_agent(self):
70
+ """
71
+ Reset the RAG agent instance
72
+ """
73
+ logger.info("Resetting RAG agent instance")
74
+ self._rag_agent = None
75
+
76
+ # Create a global instance for easy import
77
+ rag_agent_manager = RAGAgentManager()
src/agents/system_instructions_rag.py CHANGED
@@ -1,33 +1,53 @@
1
  # src/agents/system_instructions_rag.py
2
- from typing import List, Dict, Optional
3
- from src.agents.rag_agent import RAGResponse
4
- from src.utils.logger import logger
5
  from src.agents.rag_agent import RAGAgent
 
 
 
 
 
 
6
 
7
  class SystemInstructionsRAGAgent(RAGAgent):
8
- """RAG Agent with enhanced system instructions for specific use cases"""
9
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  async def generate_response(
11
  self,
12
  query: str,
13
  conversation_id: Optional[str] = None,
14
  temperature: float = 0.7,
15
  max_tokens: Optional[int] = None,
16
- context_docs: Optional[List[str]] = None
 
17
  ) -> RAGResponse:
18
- """
19
- Generate response with specific handling for introduction and no-context cases
20
- """
21
  try:
22
- # First, check if this is an introduction/welcome message query
23
- is_introduction = (
24
- "wants support" in query and
25
- "This is Introduction" in query and
26
- ("A new user with name:" in query or "An old user with name:" in query)
27
- )
28
 
29
- if is_introduction:
30
- # Handle introduction message - no context needed
31
  welcome_message = self._handle_contact_query(query)
32
  return RAGResponse(
33
  response=welcome_message,
@@ -36,107 +56,249 @@ class SystemInstructionsRAGAgent(RAGAgent):
36
  scores=None
37
  )
38
 
39
- # For all other queries, proceed with context-based response
40
- if not context_docs:
41
- context_docs, sources, scores = await self.retrieve_context(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  query,
43
- conversation_history=[]
44
  )
45
 
46
- # Check if we have relevant context
47
- has_relevant_context = self._check_context_relevance(query, context_docs or [])
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- # If no relevant context found, return the standard message
 
 
 
 
50
  if not has_relevant_context:
51
- return RAGResponse(
52
- response="Information about this is not available, do you want to inquire about something else?",
53
- context_docs=[],
54
- sources=[],
55
- scores=None
56
- )
57
 
58
- # Generate response using context
59
- prompt = self._create_response_prompt(query, context_docs)
60
  response_text = self.llm.generate(
61
- prompt,
62
  temperature=temperature,
63
  max_tokens=max_tokens
64
  )
65
 
66
- # Check if the generated response indicates no information
67
  cleaned_response = self._clean_response(response_text)
68
  if self._is_no_info_response(cleaned_response):
69
- return RAGResponse(
70
- response="Information about this is not available, do you want to inquire about something else?",
71
- context_docs=[],
72
- sources=[],
73
- scores=None
74
- )
 
 
 
 
 
 
 
 
 
75
 
76
  return RAGResponse(
77
  response=cleaned_response,
78
- context_docs=context_docs,
79
  sources=sources,
80
  scores=scores
81
  )
82
 
83
  except Exception as e:
84
- logger.error(f"Error in SystemInstructionsRAGAgent: {str(e)}")
85
  raise
86
 
87
- def _is_no_info_response(self, response: str) -> bool:
88
- """Check if the response indicates no information available"""
89
- no_info_indicators = [
90
- "i do not have",
91
- "i don't have",
92
- "no information",
93
- "not available",
94
- "could not find",
95
- "couldn't find",
96
- "cannot find"
97
- ]
98
- response_lower = response.lower()
99
- return any(indicator in response_lower for indicator in no_info_indicators)
100
-
101
- def _check_context_relevance(self, query: str, context_docs: List[str]) -> bool:
102
- """Check if context contains information relevant to the query"""
103
- if not context_docs:
104
- return False
105
-
106
- # Extract key terms from query (keeping important words)
107
- query_words = query.lower().split()
108
- stop_words = {'me', 'a', 'about', 'what', 'is', 'are', 'the', 'in', 'how', 'why', 'when', 'where'}
109
-
110
- # Remove only basic stop words, keep important terms like "report", "share", etc.
111
- query_terms = {word for word in query_words if word not in stop_words}
112
-
113
- # Add additional relevant terms that might appear in the content
114
- related_terms = {
115
- 'comprehensive',
116
- 'report',
117
- 'overview',
118
- 'summary',
119
- 'details',
120
- 'information'
121
  }
122
- query_terms.update(word for word in query_words if word in related_terms)
123
-
124
- # Check each context document for relevance
125
- for doc in context_docs:
126
- if not doc:
127
- continue
128
- doc_lower = doc.lower()
129
-
130
- # Consider document relevant if it contains any query terms
131
- # or if it starts with common report headers
132
- if any(term in doc_lower for term in query_terms) or \
133
- any(header in doc_lower for header in ['overview', 'comprehensive report', 'summary']):
134
- return True
135
 
136
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  def _create_response_prompt(self, query: str, context_docs: List[str]) -> str:
139
- """Create prompt for generating response from context"""
140
  formatted_context = '\n\n'.join(
141
  f"Context {i+1}:\n{doc.strip()}"
142
  for i, doc in enumerate(context_docs)
@@ -159,28 +321,17 @@ Instructions:
159
  Query: {query}
160
  Response:"""
161
 
162
- def _handle_contact_query(self, query: str) -> str:
163
- """Handle queries from /user/contact endpoint"""
164
- try:
165
- name_start = query.find('name: "') + 7
166
- name_end = query.find('"', name_start)
167
- name = query[name_start:name_end] if name_start > 6 and name_end != -1 else "there"
168
-
169
- is_returning = (
170
- "An old user with name:" in query and
171
- "wants support again" in query
172
- )
173
-
174
- if is_returning:
175
- return f"Welcome back {name}, How can I help you?"
176
- return f"Welcome {name}, How can I help you?"
177
-
178
- except Exception as e:
179
- logger.error(f"Error handling contact query: {str(e)}")
180
- return "Welcome, How can I help you?"
181
 
182
  def _clean_response(self, response: str) -> str:
183
- """Clean response by removing unwanted phrases"""
184
  if not response:
185
  return response
186
 
@@ -200,7 +351,6 @@ Response:"""
200
  "Here's what I found:",
201
  "Here's the information you requested:",
202
  "According to the provided information,",
203
- "Based on the documents,",
204
  "The information suggests that",
205
  "From what I can see,",
206
  "Let me explain",
@@ -209,12 +359,6 @@ Response:"""
209
  "I can see that",
210
  "Sure,",
211
  "Well,",
212
- "Based on the given context,",
213
- "The available information shows that",
214
- "From the context provided,",
215
- "The documentation mentions that",
216
- "According to the context,",
217
- "As shown in the context,",
218
  "I apologize,"
219
  ]
220
 
@@ -230,4 +374,70 @@ Response:"""
230
  if cleaned_response[0].islower():
231
  cleaned_response = cleaned_response[0].upper() + cleaned_response[1:]
232
 
233
- return cleaned_response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # src/agents/system_instructions_rag.py
2
+ from typing import List, Dict, Optional, Tuple
3
+ import spacy
 
4
  from src.agents.rag_agent import RAGAgent
5
+ from src.llms.base_llm import BaseLLM
6
+ from src.embeddings.base_embedding import BaseEmbedding
7
+ from src.vectorstores.base_vectorstore import BaseVectorStore
8
+ from src.db.mongodb_store import MongoDBStore
9
+ from src.models.rag import RAGResponse
10
+ from src.utils.logger import logger
11
 
12
  class SystemInstructionsRAGAgent(RAGAgent):
13
+ def __init__(
14
+ self,
15
+ llm: BaseLLM,
16
+ embedding: BaseEmbedding,
17
+ vector_store: BaseVectorStore,
18
+ mongodb: MongoDBStore,
19
+ max_history_tokens: int = 4000,
20
+ max_history_messages: int = 10
21
+ ):
22
+ """Initialize SystemInstructionsRAGAgent with enhanced context management"""
23
+ super().__init__(
24
+ llm=llm,
25
+ embedding=embedding,
26
+ vector_store=vector_store,
27
+ mongodb=mongodb,
28
+ max_history_tokens=max_history_tokens,
29
+ max_history_messages=max_history_messages
30
+ )
31
+ self.nlp = spacy.load("en_core_web_sm")
32
+
33
  async def generate_response(
34
  self,
35
  query: str,
36
  conversation_id: Optional[str] = None,
37
  temperature: float = 0.7,
38
  max_tokens: Optional[int] = None,
39
+ context_docs: Optional[List[str]] = None,
40
+ stream: bool = False
41
  ) -> RAGResponse:
42
+ """Generate response with guaranteed context handling"""
 
 
43
  try:
44
+ logger.info(f"Processing query: {query}")
45
+
46
+ # Store original context if provided
47
+ original_context = context_docs
 
 
48
 
49
+ # Handle introduction queries
50
+ if self._is_introduction_query(query):
51
  welcome_message = self._handle_contact_query(query)
52
  return RAGResponse(
53
  response=welcome_message,
 
56
  scores=None
57
  )
58
 
59
+ # Get and process conversation history
60
+ history = []
61
+ if conversation_id:
62
+ history = await self.mongodb.get_recent_messages(
63
+ conversation_id,
64
+ limit=self.conversation_manager.max_messages
65
+ )
66
+
67
+ # Process history in context manager
68
+ for msg in history:
69
+ if msg.get('query') and msg.get('response'):
70
+ self.context_manager.process_turn(msg['query'], msg['response'])
71
+
72
+ # Initialize context tracking
73
+ current_context = None
74
+ sources = []
75
+ scores = None
76
+
77
+ # Multi-stage context retrieval
78
+ if original_context:
79
+ current_context = original_context
80
+ else:
81
+ # Try with original query first
82
+ current_context, sources, scores = await self.retrieve_context(
83
  query,
84
+ conversation_history=history
85
  )
86
 
87
+ # If no context, try with enhanced query
88
+ if not current_context:
89
+ enhanced_query = self.context_manager.enhance_query(query)
90
+ if enhanced_query != query:
91
+ current_context, sources, scores = await self.retrieve_context(
92
+ enhanced_query,
93
+ conversation_history=history
94
+ )
95
+
96
+ # If still no context, try history fallback
97
+ if not current_context:
98
+ current_context, sources = self._get_context_from_history(history)
99
+
100
+ logger.info(f"Retrieved {len(current_context) if current_context else 0} context documents")
101
 
102
+ # Check context relevance
103
+ has_relevant_context = self._check_context_relevance(query, current_context or [])
104
+ logger.info(f"Context relevance check result: {has_relevant_context}")
105
+
106
+ # Handle no context case
107
  if not has_relevant_context:
108
+ return self._create_no_info_response()
 
 
 
 
 
109
 
110
+ # Generate response
111
+ prompt = self._create_response_prompt(query, current_context)
112
  response_text = self.llm.generate(
113
+ prompt=prompt,
114
  temperature=temperature,
115
  max_tokens=max_tokens
116
  )
117
 
118
+ # Process and validate response
119
  cleaned_response = self._clean_response(response_text)
120
  if self._is_no_info_response(cleaned_response):
121
+ return self._create_no_info_response()
122
+
123
+ # Update context tracking
124
+ self.context_manager.process_turn(query, cleaned_response)
125
+
126
+ # For Excel content, enhance the response
127
+ if any('Sheet:' in doc for doc in (current_context or [])):
128
+ try:
129
+ cleaned_response = await self.enhance_excel_response(
130
+ query=query,
131
+ response=cleaned_response,
132
+ context_docs=current_context
133
+ )
134
+ except Exception as e:
135
+ logger.warning(f"Error enhancing Excel response: {str(e)}")
136
 
137
  return RAGResponse(
138
  response=cleaned_response,
139
+ context_docs=current_context,
140
  sources=sources,
141
  scores=scores
142
  )
143
 
144
  except Exception as e:
145
+ logger.error(f"Error in generate_response: {str(e)}")
146
  raise
147
 
148
+ def _convert_metadata_to_strings(self, metadata: Dict) -> Dict:
149
+ """Convert all metadata values to strings"""
150
+ return {
151
+ key: str(value) if value is not None else None
152
+ for key, value in metadata.items()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
154
+
155
+ async def retrieve_context(
156
+ self,
157
+ query: str,
158
+ conversation_history: Optional[List[Dict]] = None
159
+ ) -> Tuple[List[str], List[Dict], Optional[List[float]]]:
160
+ """Enhanced context retrieval with proper metadata type handling"""
161
+ try:
162
+ logger.info(f"Processing query for context retrieval: {query}")
 
 
 
 
163
 
164
+ collection_data = self.vector_store.collection.get()
165
+
166
+ if not collection_data or 'documents' not in collection_data:
167
+ logger.warning("No documents found in ChromaDB")
168
+ return [], [], None
169
+
170
+ documents = collection_data['documents']
171
+ metadatas = collection_data.get('metadatas', [])
172
+
173
+ # Clean and enhance query with date variations
174
+ clean_query = query.lower().strip()
175
+
176
+ # Extract and enhance date information
177
+ import re
178
+ from datetime import datetime
179
+
180
+ date_pattern = r'(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]* \d{1,2},? \d{4}'
181
+ dates = re.findall(date_pattern, clean_query.lower())
182
+
183
+ enhanced_query = clean_query
184
+ target_date = None
185
+
186
+ if dates:
187
+ try:
188
+ date_obj = datetime.strptime(dates[0], '%b %d, %Y')
189
+ target_date = date_obj.strftime('%b %d, %Y')
190
+
191
+ date_variations = [
192
+ date_obj.strftime('%B %d, %Y'),
193
+ date_obj.strftime('%d/%m/%Y'),
194
+ date_obj.strftime('%Y-%m-%d'),
195
+ target_date
196
+ ]
197
+
198
+ enhanced_query = f"{clean_query} {' '.join(date_variations)}"
199
+
200
+ except ValueError as e:
201
+ logger.warning(f"Error parsing date: {str(e)}")
202
+
203
+ # First try exact date matching
204
+ exact_matches = []
205
+ exact_metadata = []
206
+
207
+ if target_date:
208
+ for i, doc in enumerate(documents):
209
+ if target_date in doc:
210
+ logger.info(f"Found exact date match in document {i}")
211
+ exact_matches.append(doc)
212
+ if metadatas:
213
+ # Convert metadata values to strings
214
+ exact_metadata.append(self._convert_metadata_to_strings(metadatas[i]))
215
+
216
+ if exact_matches:
217
+ logger.info(f"Found {len(exact_matches)} exact date matches")
218
+ document_id = exact_metadata[0].get('document_id') if exact_metadata else None
219
+
220
+ if document_id:
221
+ all_related_chunks = []
222
+ all_related_metadata = []
223
+ all_related_scores = []
224
+
225
+ for i, doc in enumerate(documents):
226
+ if metadatas[i].get('document_id') == document_id:
227
+ all_related_chunks.append(doc)
228
+ # Convert metadata values to strings
229
+ all_related_metadata.append(self._convert_metadata_to_strings(metadatas[i]))
230
+ all_related_scores.append(1.0)
231
+
232
+ # Sort chunks by their index
233
+ sorted_results = sorted(
234
+ zip(all_related_chunks, all_related_metadata, all_related_scores),
235
+ key=lambda x: int(x[1].get('chunk_index', '0')) # Convert to int for sorting
236
+ )
237
+
238
+ sorted_chunks, sorted_metadata, sorted_scores = zip(*sorted_results)
239
+
240
+ logger.info(f"Returning {len(sorted_chunks)} chunks from document {document_id}")
241
+ return list(sorted_chunks), list(sorted_metadata), list(sorted_scores)
242
+
243
+ # If no exact matches, use enhanced query for embedding search
244
+ logger.info("No exact matches found, using enhanced query for embedding search")
245
+ query_embedding = self.embedding.embed_query(enhanced_query)
246
+
247
+ results = self.vector_store.similarity_search(
248
+ query_embedding,
249
+ top_k=5
250
+ )
251
+
252
+ if not results:
253
+ logger.warning("No results found in similarity search")
254
+ return [], [], None
255
+
256
+ context_docs = []
257
+ sources = []
258
+ scores = []
259
+
260
+ sorted_results = sorted(results, key=lambda x: x.get('score', 0), reverse=True)
261
+
262
+ for result in sorted_results:
263
+ score = result.get('score', 0)
264
+ if score > 0.3:
265
+ context_docs.append(result.get('text', ''))
266
+ # Convert metadata values to strings
267
+ sources.append(self._convert_metadata_to_strings(result.get('metadata', {})))
268
+ scores.append(score)
269
+
270
+ if context_docs:
271
+ logger.info(f"Returning {len(context_docs)} documents from similarity search")
272
+ return context_docs, sources, scores
273
+
274
+ logger.warning("No relevant documents found")
275
+ return [], [], None
276
+
277
+ except Exception as e:
278
+ logger.error(f"Error in retrieve_context: {str(e)}")
279
+ logger.exception("Full traceback:")
280
+ return [], [], None
281
+
282
+ def _is_introduction_query(self, query: str) -> bool:
283
+ """Check if query is an introduction message"""
284
+ return (
285
+ "wants support" in query and
286
+ "This is Introduction" in query and
287
+ ("A new user with name:" in query or "An old user with name:" in query)
288
+ )
289
+
290
+ def _get_context_from_history(
291
+ self,
292
+ history: List[Dict]
293
+ ) -> Tuple[Optional[List[str]], Optional[List[Dict]]]:
294
+ """Extract context from conversation history"""
295
+ for msg in reversed(history):
296
+ if msg.get('context') and not self._is_no_info_response(msg.get('response', '')):
297
+ return msg['context'], msg.get('sources', [])
298
+ return None, None
299
 
300
  def _create_response_prompt(self, query: str, context_docs: List[str]) -> str:
301
+ """Create prompt for response generation"""
302
  formatted_context = '\n\n'.join(
303
  f"Context {i+1}:\n{doc.strip()}"
304
  for i, doc in enumerate(context_docs)
 
321
  Query: {query}
322
  Response:"""
323
 
324
+ def _create_no_info_response(self) -> RAGResponse:
325
+ """Create standard response for no information case"""
326
+ return RAGResponse(
327
+ response="Information about this is not available, do you want to inquire about something else?",
328
+ context_docs=[],
329
+ sources=[],
330
+ scores=None
331
+ )
 
 
 
 
 
 
 
 
 
 
 
332
 
333
  def _clean_response(self, response: str) -> str:
334
+ """Clean response text"""
335
  if not response:
336
  return response
337
 
 
351
  "Here's what I found:",
352
  "Here's the information you requested:",
353
  "According to the provided information,",
 
354
  "The information suggests that",
355
  "From what I can see,",
356
  "Let me explain",
 
359
  "I can see that",
360
  "Sure,",
361
  "Well,",
 
 
 
 
 
 
362
  "I apologize,"
363
  ]
364
 
 
374
  if cleaned_response[0].islower():
375
  cleaned_response = cleaned_response[0].upper() + cleaned_response[1:]
376
 
377
+ return cleaned_response
378
+
379
+ def _is_no_info_response(self, response: str) -> bool:
380
+ """Check if response indicates no information available"""
381
+ no_info_indicators = [
382
+ "i do not have",
383
+ "i don't have",
384
+ "no information",
385
+ "not available",
386
+ "could not find",
387
+ "couldn't find",
388
+ "cannot find",
389
+ "don't know",
390
+ "do not know",
391
+ "unable to find",
392
+ "no data",
393
+ "no relevant"
394
+ ]
395
+ response_lower = response.lower()
396
+ return any(indicator in response_lower for indicator in no_info_indicators)
397
+
398
+ def _check_context_relevance(self, query: str, context_docs: List[str]) -> bool:
399
+ """Enhanced context relevance checking"""
400
+ if not context_docs:
401
+ return False
402
+
403
+ # Clean and prepare query
404
+ clean_query = query.lower().strip()
405
+ query_terms = set(word for word in clean_query.split()
406
+ if word not in {'tell', 'me', 'about', 'what', 'is', 'the'})
407
+
408
+ for doc in context_docs:
409
+ if not doc:
410
+ continue
411
+
412
+ doc_lower = doc.lower()
413
+
414
+ # For CSV-like content, check each line
415
+ lines = doc_lower.split('\n')
416
+ for line in lines:
417
+ # Check if any query term appears in the line
418
+ if any(term in line for term in query_terms):
419
+ return True
420
+
421
+ # Also check the whole document for good measure
422
+ if any(term in doc_lower for term in query_terms):
423
+ return True
424
+
425
+ return False
426
+
427
+ def _handle_contact_query(self, query: str) -> str:
428
+ """Handle contact/introduction queries"""
429
+ try:
430
+ name_start = query.find('name: "') + 7
431
+ name_end = query.find('"', name_start)
432
+ name = query[name_start:name_end] if name_start > 6 and name_end != -1 else "there"
433
+
434
+ is_returning = (
435
+ "An old user with name:" in query and
436
+ "wants support again" in query
437
+ )
438
+
439
+ return f"Welcome back {name}, How can I help you?" if is_returning else f"Welcome {name}, How can I help you?"
440
+
441
+ except Exception as e:
442
+ logger.error(f"Error handling contact query: {str(e)}")
443
+ return "Welcome, How can I help you?"
src/implementations/__pycache__/document_service.cpython-312.pyc CHANGED
Binary files a/src/implementations/__pycache__/document_service.cpython-312.pyc and b/src/implementations/__pycache__/document_service.cpython-312.pyc differ
 
src/implementations/document_service.py CHANGED
@@ -105,21 +105,33 @@ class DocumentService:
105
  vector_store: ChromaVectorStore,
106
  background_tasks: BackgroundTasks
107
  ) -> DocumentResponse:
108
- """Process a single file upload"""
109
- # Generate UUID for document
110
  document_id = str(uuid4())
111
  filename = f"{document_id}_{file.filename}"
112
  file_path = self.permanent_dir / filename
113
  url_path = f"/docs/{filename}"
114
-
115
  try:
116
- # Save file to permanent location
117
  with open(file_path, "wb") as buffer:
118
  shutil.copyfileobj(file.file, buffer)
119
 
120
- # Process the document for vector store
121
- processed_doc = await self.doc_processor.process_document(file_path)
 
 
 
 
122
 
 
 
 
 
 
 
 
 
 
123
  # Store in MongoDB with url_path
124
  await self.mongodb.store_document(
125
  document_id=document_id,
@@ -157,13 +169,13 @@ class DocumentService:
157
  file_path.unlink()
158
  except Exception as cleanup_error:
159
  logger.error(f"Error cleaning up file {file_path}: {str(cleanup_error)}")
160
-
161
  # Clean up from MongoDB if document was created
162
  try:
163
  await self.mongodb.delete_document(document_id)
164
  except Exception as db_cleanup_error:
165
  logger.error(f"Error cleaning up MongoDB document {document_id}: {str(db_cleanup_error)}")
166
-
167
  logger.error(f"Error processing file {file.filename}: {str(e)}")
168
  raise
169
 
 
105
  vector_store: ChromaVectorStore,
106
  background_tasks: BackgroundTasks
107
  ) -> DocumentResponse:
108
+ """Process a single file upload with proper handle closure"""
 
109
  document_id = str(uuid4())
110
  filename = f"{document_id}_{file.filename}"
111
  file_path = self.permanent_dir / filename
112
  url_path = f"/docs/{filename}"
113
+
114
  try:
115
+ # Save file to permanent location using a context manager
116
  with open(file_path, "wb") as buffer:
117
  shutil.copyfileobj(file.file, buffer)
118
 
119
+ # Close the uploaded file explicitly
120
+ await file.close()
121
+
122
+ # Process document with proper cleanup for Excel files
123
+ try:
124
+ processed_doc = await self.doc_processor.process_document(file_path)
125
 
126
+ # For Excel files, ensure pandas closes the file
127
+ if file_path.suffix.lower() in ['.xlsx', '.xls']:
128
+ import gc
129
+ gc.collect() # Help cleanup any lingering file handles
130
+
131
+ except Exception as proc_error:
132
+ logger.error(f"Error processing document: {str(proc_error)}")
133
+ raise
134
+
135
  # Store in MongoDB with url_path
136
  await self.mongodb.store_document(
137
  document_id=document_id,
 
169
  file_path.unlink()
170
  except Exception as cleanup_error:
171
  logger.error(f"Error cleaning up file {file_path}: {str(cleanup_error)}")
172
+
173
  # Clean up from MongoDB if document was created
174
  try:
175
  await self.mongodb.delete_document(document_id)
176
  except Exception as db_cleanup_error:
177
  logger.error(f"Error cleaning up MongoDB document {document_id}: {str(db_cleanup_error)}")
178
+
179
  logger.error(f"Error processing file {file.filename}: {str(e)}")
180
  raise
181
 
src/main.py CHANGED
@@ -21,6 +21,7 @@ from src.utils.google_drive_service import GoogleDriveService
21
  # Import custom modules1
22
  #from src.agents.rag_agent import RAGAgent
23
  from src.agents.system_instructions_rag import SystemInstructionsRAGAgent
 
24
  from src.models.document import AllDocumentsResponse, StoredDocument
25
  from src.models.UserContact import UserContactRequest
26
  from src.utils.document_processor import DocumentProcessor
@@ -48,7 +49,7 @@ app = FastAPI(title="Chatbot API")
48
 
49
  app.add_middleware(
50
  CORSMiddleware,
51
- allow_origins=["http://localhost:8080"], # Add your frontend URL
52
  allow_credentials=True,
53
  allow_methods=["*"], # Allows all methods
54
  allow_headers=["*"], # Allows all headers
@@ -345,41 +346,14 @@ async def chat_endpoint(
345
  logger.info(f"Initializing LLM: {str(datetime.now())}")
346
  llm = get_llm_instance(request.llm_provider)
347
 
348
- # Initialize RAG agent
349
- # rag_agent = RAGAgent(
350
- # llm=llm,
351
- # embedding=embedding_model,
352
- # vector_store=vector_store,
353
- # mongodb=mongodb
354
- # )
355
-
356
- rag_agent = SystemInstructionsRAGAgent(
357
  llm=llm,
358
- embedding=embedding_model,
359
  vector_store=vector_store,
360
  mongodb=mongodb
361
  )
362
 
363
- # rag_agent.add_custom_role(
364
- # "Knowledge based chatbot and introduction specialist",
365
- # """You are a welcome agent with knowledge based specialist focusing on knowledge attached and create a beautiful welcome message.
366
- # Your role is to:
367
- # 1. Your response should be short and to the point.
368
- # 2. Strictly follow this point for If it is an introduction. You strictly respond that "Welcome name of customer to our platform. How can I help you today?"
369
- # """
370
- # )
371
-
372
- # rag_agent.add_custom_role(
373
- # "Knowledge based chatbot",
374
- # """You are a knowledge based specialist focusing on knowledge attached.
375
- # Your role is to:
376
- # 1. Your response should be short and to the point.
377
- # 2. if it is not introduction then make sure to share the response from Vector store.
378
- # 3. If you do not find relevant information. Just say I do not have this information but this do not apply to introduction message.
379
- # 4. If there is an introduction, you should ignore above roles and connect with LLm to have a welcome message for the user.
380
- # """
381
- # )
382
-
383
  # Use provided conversation ID or create new one
384
  conversation_id = request.conversation_id or str(uuid.uuid4())
385
 
 
21
  # Import custom modules1
22
  #from src.agents.rag_agent import RAGAgent
23
  from src.agents.system_instructions_rag import SystemInstructionsRAGAgent
24
+ from src.agents.rag_agent_manager import rag_agent_manager
25
  from src.models.document import AllDocumentsResponse, StoredDocument
26
  from src.models.UserContact import UserContactRequest
27
  from src.utils.document_processor import DocumentProcessor
 
49
 
50
  app.add_middleware(
51
  CORSMiddleware,
52
+ allow_origins=["http://localhost:8080", "http://localhost:3000"], # Add both ports
53
  allow_credentials=True,
54
  allow_methods=["*"], # Allows all methods
55
  allow_headers=["*"], # Allows all headers
 
346
  logger.info(f"Initializing LLM: {str(datetime.now())}")
347
  llm = get_llm_instance(request.llm_provider)
348
 
349
+ # Use RAG agent manager to get singleton RAG agent
350
+ rag_agent = rag_agent_manager.get_rag_agent(
 
 
 
 
 
 
 
351
  llm=llm,
352
+ embedding_model=embedding_model,
353
  vector_store=vector_store,
354
  mongodb=mongodb
355
  )
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  # Use provided conversation ID or create new one
358
  conversation_id = request.conversation_id or str(uuid.uuid4())
359
 
src/utils/__pycache__/drive_document_processor.cpython-312.pyc CHANGED
Binary files a/src/utils/__pycache__/drive_document_processor.cpython-312.pyc and b/src/utils/__pycache__/drive_document_processor.cpython-312.pyc differ
 
src/utils/__pycache__/enhanced_excel_processor.cpython-312.pyc CHANGED
Binary files a/src/utils/__pycache__/enhanced_excel_processor.cpython-312.pyc and b/src/utils/__pycache__/enhanced_excel_processor.cpython-312.pyc differ
 
src/utils/__pycache__/google_drive_service.cpython-312.pyc CHANGED
Binary files a/src/utils/__pycache__/google_drive_service.cpython-312.pyc and b/src/utils/__pycache__/google_drive_service.cpython-312.pyc differ
 
src/utils/__pycache__/llm_utils.cpython-312.pyc CHANGED
Binary files a/src/utils/__pycache__/llm_utils.cpython-312.pyc and b/src/utils/__pycache__/llm_utils.cpython-312.pyc differ
 
src/vectorstores/__pycache__/chroma_vectorstore.cpython-312.pyc CHANGED
Binary files a/src/vectorstores/__pycache__/chroma_vectorstore.cpython-312.pyc and b/src/vectorstores/__pycache__/chroma_vectorstore.cpython-312.pyc differ