Spaces:
Running
Running
Commit
·
f36ab64
1
Parent(s):
be32fd8
DOne some testing and fixed the retrieving context by date.
Browse files- setup.py +53 -0
- src/__pycache__/main.cpython-312.pyc +0 -0
- src/agents/__pycache__/enhanced_context_manager.cpython-312.pyc +0 -0
- src/agents/__pycache__/rag_agent.cpython-312.pyc +0 -0
- src/agents/__pycache__/rag_agent_manager.cpython-312.pyc +0 -0
- src/agents/__pycache__/system_instructions_rag.cpython-312.pyc +0 -0
- src/agents/enhanced_context_manager.py +202 -0
- src/agents/rag_agent.py +241 -135
- src/agents/rag_agent_manager.py +77 -0
- src/agents/system_instructions_rag.py +329 -119
- src/implementations/__pycache__/document_service.cpython-312.pyc +0 -0
- src/implementations/document_service.py +20 -8
- src/main.py +5 -31
- src/utils/__pycache__/drive_document_processor.cpython-312.pyc +0 -0
- src/utils/__pycache__/enhanced_excel_processor.cpython-312.pyc +0 -0
- src/utils/__pycache__/google_drive_service.cpython-312.pyc +0 -0
- src/utils/__pycache__/llm_utils.cpython-312.pyc +0 -0
- src/vectorstores/__pycache__/chroma_vectorstore.cpython-312.pyc +0 -0
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 |
-
|
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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
try:
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
#
|
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 |
-
#
|
81 |
history = self.conversation_manager.get_relevant_history(
|
82 |
messages=history,
|
83 |
current_query=query
|
84 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
|
86 |
-
|
|
|
|
|
87 |
if not context_docs:
|
|
|
88 |
context_docs, sources, scores = await self.retrieve_context(
|
89 |
-
|
90 |
conversation_history=history
|
91 |
)
|
92 |
else:
|
93 |
-
sources =
|
94 |
scores = None
|
95 |
|
96 |
-
#
|
97 |
if not context_docs:
|
98 |
-
|
99 |
-
|
100 |
-
context_docs=[]
|
101 |
-
sources=[]
|
102 |
-
scores=
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
|
105 |
-
#
|
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,
|
110 |
except Exception as e:
|
111 |
logger.warning(f"Error processing Excel context: {str(e)}")
|
112 |
|
113 |
-
#
|
114 |
-
|
115 |
-
current_query=
|
116 |
history=history,
|
117 |
context_docs=context_docs
|
118 |
)
|
119 |
|
120 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
response = self.llm.generate(
|
122 |
-
prompt=
|
123 |
temperature=temperature,
|
124 |
max_tokens=max_tokens
|
125 |
)
|
126 |
|
127 |
-
#
|
128 |
cleaned_response = self._clean_response(response)
|
129 |
-
|
130 |
-
#
|
131 |
if has_excel_content:
|
132 |
try:
|
133 |
enhanced_response = await self.enhance_excel_response(
|
134 |
-
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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
223 |
|
224 |
-
|
225 |
-
|
226 |
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
logger.info(f"
|
|
|
|
|
|
|
|
|
|
|
238 |
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
scores = [doc['score'] for doc in results
|
244 |
-
if doc.get('score') is not None]
|
245 |
|
246 |
-
#
|
247 |
-
|
248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
249 |
|
250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
251 |
|
252 |
def _convert_metadata_to_strings(self, metadata: Dict) -> Dict:
|
253 |
-
"""Convert
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
4 |
-
from src.utils.logger import logger
|
5 |
from src.agents.rag_agent import RAGAgent
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
class SystemInstructionsRAGAgent(RAGAgent):
|
8 |
-
|
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 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
("A new user with name:" in query or "An old user with name:" in query)
|
27 |
-
)
|
28 |
|
29 |
-
|
30 |
-
|
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 |
-
#
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
query,
|
43 |
-
conversation_history=
|
44 |
)
|
45 |
|
46 |
-
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
-
#
|
|
|
|
|
|
|
|
|
50 |
if not has_relevant_context:
|
51 |
-
return
|
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
|
59 |
-
prompt = self._create_response_prompt(query,
|
60 |
response_text = self.llm.generate(
|
61 |
-
prompt,
|
62 |
temperature=temperature,
|
63 |
max_tokens=max_tokens
|
64 |
)
|
65 |
|
66 |
-
#
|
67 |
cleaned_response = self._clean_response(response_text)
|
68 |
if self._is_no_info_response(cleaned_response):
|
69 |
-
return
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
return RAGResponse(
|
77 |
response=cleaned_response,
|
78 |
-
context_docs=
|
79 |
sources=sources,
|
80 |
scores=scores
|
81 |
)
|
82 |
|
83 |
except Exception as e:
|
84 |
-
logger.error(f"Error in
|
85 |
raise
|
86 |
|
87 |
-
def
|
88 |
-
"""
|
89 |
-
|
90 |
-
|
91 |
-
|
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 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
|
138 |
def _create_response_prompt(self, query: str, context_docs: List[str]) -> str:
|
139 |
-
"""Create prompt for
|
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
|
163 |
-
"""
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
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
|
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 |
-
#
|
121 |
-
|
|
|
|
|
|
|
|
|
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
|
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 |
-
#
|
349 |
-
|
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 |
-
|
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
|
|