File size: 10,781 Bytes
415595f
 
9700f95
640b1c8
b953016
640b1c8
e87abff
 
9700f95
 
0739c8b
9700f95
acdfaa9
640b1c8
415595f
b953016
640b1c8
9700f95
 
 
 
 
 
 
640b1c8
415595f
 
 
 
 
 
 
 
 
 
 
b953016
9700f95
 
 
 
 
 
 
640b1c8
9700f95
acdfaa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0739c8b
9700f95
0739c8b
be32fd8
 
0739c8b
415595f
640b1c8
acdfaa9
9700f95
acdfaa9
be32fd8
415595f
 
be32fd8
9700f95
be32fd8
 
 
 
 
 
 
 
 
 
415595f
be32fd8
 
 
 
 
 
 
 
 
 
415595f
 
9700f95
 
415595f
be32fd8
9700f95
 
415595f
9700f95
 
acdfaa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415595f
be32fd8
415595f
 
 
 
 
 
be32fd8
415595f
 
 
be32fd8
9700f95
 
 
acdfaa9
9700f95
415595f
9700f95
 
 
 
415595f
be32fd8
415595f
 
9700f95
be32fd8
9700f95
 
415595f
9700f95
 
 
acdfaa9
9700f95
 
415595f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9700f95
 
 
acdfaa9
9700f95
415595f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acdfaa9
415595f
f36ab64
415595f
 
 
 
 
 
f36ab64
415595f
 
 
 
 
 
f36ab64
415595f
 
 
be32fd8
415595f
9700f95
 
415595f
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# src/agents/rag_agent.py
from typing import List, Optional, Tuple, Dict
import uuid

from .excel_aware_rag import ExcelAwareRAGAgent
from ..llms.base_llm import BaseLLM
from src.embeddings.base_embedding import BaseEmbedding
from src.vectorstores.base_vectorstore import BaseVectorStore
from src.utils.conversation_manager import ConversationManager
from src.db.mongodb_store import MongoDBStore
from src.models.rag import RAGResponse
from src.utils.logger import logger
from config.config import settings


class RAGAgent(ExcelAwareRAGAgent):
    def __init__(
        self,
        llm: BaseLLM,
        embedding: BaseEmbedding,
        vector_store: BaseVectorStore,
        mongodb: MongoDBStore,
        max_history_tokens: int = 4000,
        max_history_messages: int = 10
    ):
        """
        Initialize RAG Agent

        Args:
            llm (BaseLLM): Language model instance
            embedding (BaseEmbedding): Embedding model instance
            vector_store (BaseVectorStore): Vector store instance
            mongodb (MongoDBStore): MongoDB store instance
            max_history_tokens (int): Maximum tokens in conversation history
            max_history_messages (int): Maximum messages to keep in history
        """
        super().__init__()  # Initialize ExcelAwareRAGAgent
        self.llm = llm
        self.embedding = embedding
        self.vector_store = vector_store
        self.mongodb = mongodb
        self.conversation_manager = ConversationManager(
            max_tokens=max_history_tokens,
            max_messages=max_history_messages
        )

    def _extract_markdown_section(self, docs: List[str], section_header: str) -> str:
        """Extract complete section content from markdown documents"""
        combined_text = '\n'.join(docs)

        section_start = combined_text.find(section_header)
        if section_start == -1:
            return ""

        next_section = combined_text.find(
            "\n\n**", section_start + len(section_header))
        if next_section == -1:
            section_content = combined_text[section_start:]
        else:
            section_content = combined_text[section_start:next_section]

        return self._clean_markdown_content(section_content)

    def _clean_markdown_content(self, content: str) -> str:
        """Clean and format markdown content"""
        lines = content.split('\n')
        seen_lines = set()
        cleaned_lines = []

        for line in lines:
            # Always keep headers and table formatting
            if '| :----' in line or line.startswith('**'):
                if line not in seen_lines:
                    cleaned_lines.append(line)
                    seen_lines.add(line)
                continue

            # Keep table rows and list items
            if line.strip().startswith('|') or line.strip().startswith('-'):
                cleaned_lines.append(line)
                continue

            # Remove duplicates for other content
            stripped = line.strip()
            if stripped and stripped not in seen_lines:
                cleaned_lines.append(line)
                seen_lines.add(stripped)

        return '\n'.join(cleaned_lines)

    async def generate_response(
        self,
        query: str,
        conversation_id: Optional[str],
        temperature: float,
        max_tokens: Optional[int] = None,
        context_docs: Optional[List[str]] = None
    ) -> RAGResponse:
        """Generate response with improved markdown and conversation handling"""
        try:
            # Handle introduction/welcome message queries
            is_introduction = (
                "wants support" in query and
                "This is Introduction" in query and
                ("A new user with name:" in query or "An old user with name:" in query)
            )

            if is_introduction:
                welcome_message = self._handle_contact_query(query)
                return RAGResponse(
                    response=welcome_message,
                    context_docs=[],
                    sources=[],
                    scores=None
                )

            # Get conversation history if conversation_id exists
            history = []
            if conversation_id:
                history = await self.mongodb.get_recent_messages(
                    conversation_id,
                    limit=self.conversation_manager.max_messages
                )
                history = self.conversation_manager.get_relevant_history(
                    messages=history,
                    current_query=query
                )

            # Retrieve context if not provided
            if not context_docs:
                context_docs, sources, scores = await self.retrieve_context(
                    query=query,
                    conversation_history=history
                )
            else:
                sources = None
                scores = None

            # Special handling for markdown section queries
            if "DISCUSSIONS AND ACTION ITEMS" in query.upper():
                section_content = self._extract_markdown_section(
                    context_docs,
                    "**DISCUSSIONS AND ACTION ITEMS**"
                )

                if section_content:
                    return RAGResponse(
                        response=section_content.strip(),
                        context_docs=context_docs,
                        sources=sources,
                        scores=scores
                    )

            # Check if we have any relevant context
            if not context_docs:
                return RAGResponse(
                    response="Information about this is not available, do you want to inquire about something else?",
                    context_docs=[],
                    sources=[],
                    scores=None
                )

            # Generate prompt with context and history
            augmented_prompt = self.conversation_manager.generate_prompt_with_history(
                current_query=query,
                history=history,
                context_docs=context_docs
            )

            # Generate response
            response = self.llm.generate(
                prompt=augmented_prompt,
                temperature=temperature,
                max_tokens=max_tokens
            )

            # Clean the response
            cleaned_response = self._clean_response(response)

            # Return the final response
            return RAGResponse(
                response=cleaned_response,
                context_docs=context_docs,
                sources=sources,
                scores=scores
            )

        except Exception as e:
            logger.error(f"Error in RAGAgent: {str(e)}")
            raise

    def _create_response_prompt(self, query: str, context_docs: List[str]) -> str:
        """
        Create prompt for generating response from context

        Args:
            query (str): User query
            context_docs (List[str]): Retrieved context documents

        Returns:
            str: Formatted prompt for the LLM
        """
        if not context_docs:
            return f"Query: {query}\nResponse: Information about this is not available, do you want to inquire about something else?"

        # Format context documents
        formatted_context = "\n\n".join(
            f"Context {i+1}:\n{doc.strip()}"
            for i, doc in enumerate(context_docs)
            if doc and doc.strip()
        )

        # Build the prompt with detailed instructions
        prompt = f"""You are a knowledgeable assistant. Use the following context to answer the query accurately and informatively.

    Context Information:
    {formatted_context}

    Query: {query}

    Instructions:
    1. Base your response ONLY on the information provided in the context above
    2. If the context contains numbers, statistics, or specific details, include them in your response
    3. Keep your response focused and relevant to the query
    4. Use clear and professional language
    5. If the context includes technical terms, explain them appropriately
    6. Do not make assumptions or add information not present in the context
    7. If specific sections of a report are mentioned, maintain their original structure
    8. Format the response in a clear, readable manner
    9. If the context includes chronological information, maintain the proper sequence

    Response:"""

        return prompt

    async def retrieve_context(
        self,
        query: str,
        conversation_history: Optional[List[Dict]] = None
    ) -> Tuple[List[str], List[Dict], Optional[List[float]]]:
        """
        Retrieve context with conversation history enhancement
        """
        # Enhance query with conversation history
        if conversation_history:
            recent_queries = [
                msg['query'] for msg in conversation_history[-2:]
                if msg.get('query')
            ]
            enhanced_query = " ".join([*recent_queries, query])
        else:
            enhanced_query = query

        # Debug log the enhanced query
        logger.info(f"Enhanced query: {enhanced_query}")

        # Embed the enhanced query
        query_embedding = self.embedding.embed_query(enhanced_query)

        # Debug log embedding shape
        logger.info(f"Query embedding shape: {len(query_embedding)}")

        # Retrieve similar documents
        results = self.vector_store.similarity_search(
            query_embedding,
            top_k=settings.TOP_CHUNKS
        )

        # Debug log search results
        logger.info(f"Number of search results: {len(results)}")
        for i, result in enumerate(results):
            logger.info(f"Result {i} score: {result.get('score', 'N/A')}")
            logger.info(
                f"Result {i} text preview: {result.get('text', '')[:100]}...")

        # Process results
        documents = [doc['text'] for doc in results]
        sources = [self._convert_metadata_to_strings(doc['metadata'])
                   for doc in results]
        scores = [doc['score'] for doc in results
                  if doc.get('score') is not None]

        # Return scores only if available for all documents
        if len(scores) != len(documents):
            scores = None

        return documents, sources, scores

    def _convert_metadata_to_strings(self, metadata: Dict) -> Dict:
        """Convert numeric metadata values to strings"""
        converted = {}
        for key, value in metadata.items():
            if isinstance(value, (int, float)):
                converted[key] = str(value)
            else:
                converted[key] = value
        return converted