File size: 10,843 Bytes
80a3a2e
 
 
 
 
 
 
 
 
 
 
 
c1c360d
2f805c6
80a3a2e
 
c1c360d
 
4d8af9a
80a3a2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d8af9a
80a3a2e
4d8af9a
 
 
 
80a3a2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d8af9a
80a3a2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d8af9a
80a3a2e
2f805c6
 
 
 
 
 
14c011a
 
424e506
 
2f805c6
424e506
80a3a2e
 
424e506
56467de
 
 
 
 
424e506
80a3a2e
 
56467de
80a3a2e
2f805c6
80a3a2e
 
 
6cfc434
 
 
 
2f805c6
6cfc434
2f805c6
80a3a2e
b8d9ea0
4d8af9a
b8d9ea0
 
2f805c6
b8d9ea0
6cfc434
 
 
2f805c6
b8d9ea0
 
 
 
 
80a3a2e
b8d9ea0
80a3a2e
cb22046
4d8af9a
80a3a2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d8af9a
80a3a2e
 
 
 
 
 
 
 
 
 
4d8af9a
c1c360d
 
 
 
80a3a2e
 
 
 
 
 
 
 
 
dda3626
 
c1c360d
 
80a3a2e
 
 
c1c360d
 
80a3a2e
 
 
c1c360d
 
 
 
80a3a2e
 
c1c360d
cb22046
c1c360d
 
80a3a2e
 
4d8af9a
80a3a2e
 
7391769
80a3a2e
 
 
 
 
 
 
4d8af9a
 
80a3a2e
 
 
b8d9ea0
 
 
 
 
80a3a2e
 
 
 
 
 
 
 
b8d9ea0
80a3a2e
 
 
 
2f805c6
6cfc434
dda3626
 
2f805c6
6cfc434
80a3a2e
6cfc434
 
 
 
80a3a2e
 
 
 
 
 
 
 
 
 
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
import os
import torch
import pandas as pd
from transformers import AutoTokenizer, AutoModel, pipeline
from sentence_transformers import SentenceTransformer, CrossEncoder
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, CSVLoader
from langchain_community.vectorstores import FAISS
from langchain.prompts import PromptTemplate
from pathlib import Path
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.document_loaders import DirectoryLoader
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor

class RagWithScore:
    def __init__(self, model_name="sentence-transformers/all-MiniLM-L6-v2", 
                cross_encoder_name="cross-encoder/ms-marco-TinyBERT-L-2-v2",
                llm_name="TinyLlama/TinyLlama-1.1B-Chat-v1.0",
                documents_dir="financial_docs"):
        """
        Initialize the Financial RAG system
        
        Args:
            model_name: The embedding model name
            cross_encoder_name: The cross-encoder model for reranking
            llm_name: Small language model for generation
            documents_dir: Directory containing financial statements
        """
        # Initialize embedding model
        self.embedding_model = HuggingFaceEmbeddings(model_name=model_name)
        
        # Initialize cross-encoder for reranking
        self.cross_encoder = CrossEncoder(cross_encoder_name)
        
        # Initialize small language model
        self.tokenizer = AutoTokenizer.from_pretrained(llm_name)
        self.llm = pipeline(
        "text-generation",
        model=llm_name,
        tokenizer=self.tokenizer,
        torch_dtype=torch.bfloat16,
        device_map="auto",
        max_new_tokens=512,
        do_sample=False,  # Set to False for deterministic outputs
        temperature=0.2,   # Reduce randomness
        top_p=1.0          # No nucleus sampling
)
        
        # Store paths
        self.documents_dir = documents_dir
        self.vector_store = None
        
        # Input guardrail rules - sensitive terms/patterns
        self.guardrail_patterns = [
            "insider trading",
            "stock manipulation",
            "fraud detection",
            "embezzlement",
            "money laundering",
            "tax evasion",
            "illegal activities"
        ]
        
        # Confidence score thresholds
        self.confidence_thresholds = {
            "high": 0.75,
            "medium": 0.5,
            "low": 0.3
        }

    import os

## Loadung document and creating vector index at the start of the  application
    def load_and_process_documents(self):
        """Load, split and process financial documents"""

        print("Processing documents to create FAISS index...")
        loader = DirectoryLoader('./financial_docs', glob="**/*.pdf")
        documents = loader.load()
        
        # Split documents into chunks
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000, chunk_overlap=200
        )
        chunks = text_splitter.split_documents(documents)
        print(len(chunks))

        # Create and save FAISS vector store
        self.vector_store = FAISS.from_documents(chunks, embedding=self.embedding_model)
        self.vector_store.save_local("faiss_index")
    
        return self.vector_store


## generating response  with the query and context by the help of the prompt and calling the slm with the prompt
    def generate_answer(self, query, context):
        """Generate answer and calculate confidence score concurrently."""
        # Format context into a single string
        context_str = "\n\n".join([doc.page_content for doc in context])
        
        # Define the prompt
        prompt = f"""
        You are a financial analyst assistant that helps answer questions about company financial statements.
        Use the provided financial information to give accurate and helpful answers.

        Context:
        {context_str}

        Question: {query}

        Instructions:
        1. Be concise and avoid repetition.
        2. If the question is too broad or unclear, ask for clarification or provide a general overview.
        3. Only if the question requires numerical calculations, show the step-by-step logic behind the calculation before providing the final answer.
        4. If the context is insufficient, say "Not enough information to answer this question."
        5. Do not provide sources unless explicitly asked.

        Answer:
        """
  
        
        # Generate answer using the language model
        response = self.llm(prompt)[0]['generated_text']
        answer = response[len(prompt):].strip()
        
        # # Calculate confidence score concurrently
        # with ThreadPoolExecutor() as executor:
        #     future_confidence = executor.submit(self.calculate_confidence_score, query, context, answer)
        #     confidence_score = future_confidence.result()
        
        return answer



## for  confidence score cosine  similarity is  calculated  between the query  embedding and answer embedding
    def calculate_confidence_score(self, query, retrieved_docs, answer):
        """
        Calculate confidence score using embedding similarity (parallelized).
        """
       # Get embeddings for query and answer
        query_embedding = self.embedding_model.embed_query(query)
        answer_embedding = self.embedding_model.embed_query(answer)
            
        # Calculate cosine similarity
        dot_product = sum(a * b for a, b in zip(query_embedding, answer_embedding))
        magnitude_a = sum(a * a for a in query_embedding) ** 0.5
        magnitude_b = sum(b * b for b in answer_embedding) ** 0.5
        similarity = dot_product / (magnitude_a * magnitude_b) if magnitude_a * magnitude_b > 0 else 0
        
        return similarity
    
   
   ## confidence level is determined from the confidence score
    def get_confidence_level(self, confidence_score):
        """
        Convert numerical confidence score to a level (high, medium, low)
        
        Args:
            confidence_score: Float between 0 and 1
            
        Returns:
            str: Confidence level ("high", "medium", or "low")
        """
        if confidence_score >= self.confidence_thresholds["high"]:
            return "high"
        elif confidence_score >= self.confidence_thresholds["medium"]:
            return "medium"
        elif confidence_score >= self.confidence_thresholds["low"]:
            return "low"
        else:
            return "very low"

## guardrail is  applied to filter harmful user  queries
    def apply_input_guardrail(self, query):
        """Check if query violates input guardrails"""
        query_lower = query.lower()
        
        for pattern in self.guardrail_patterns:
            if pattern in query_lower:
                return True, f"I cannot process queries about {pattern}. Please reformulate your question."
        
        return False, ""

## first the   to 5 chunks are retrieved. then after reranking with cross encoder top 2 are rerieved
    def retrieve_with_reranking(self, query, top_k=5, rerank_top_k=3):

        print("retrieve_with_reranking start")
        print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        """Retrieve relevant chunks and rerank them with cross-encoder"""
        # Initial retrieval using embedding similarity
        docs_and_scores = self.vector_store.similarity_search_with_score(query, k=top_k)

         # Sort retrieved documents by FAISS similarity score (deterministic sorting)
        docs_and_scores.sort(key=lambda x: x[1], reverse=True)
        
        # Prepare pairs for cross-encoder
        pairs = [(query, doc.page_content) for doc, _ in docs_and_scores]

        print(pairs)

        print(len(pairs))
        
        # Get scores from cross-encoder
        scores = self.cross_encoder.predict(pairs)

        print(scores)
        
        # Sort by cross-encoder scores
        reranked_results = sorted(zip(docs_and_scores, scores), key=lambda x: x[1], reverse=True)

        print(reranked_results)

        print(len(reranked_results))
        
        # Return the top reranked results

        print("retrieve_with_reranking end")
        print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

        return [doc for (doc, _), _ in reranked_results[:rerank_top_k]]

## to handle irrerelevant questions, a rule based claasifier is  bein used to classify the questions
    def is_financial_question(self,query):
        financial_keywords = [
            "finance", "financial", "revenue", "profit", "loss", "ebitda", "cash flow",
            "balance sheet", "income statement", "stock", "bond", "investment", "risk",
            "interest rate", "inflation", "debt", "equity", "valuation", "dividend",
            "market", "economy", "GDP", "currency", "exchange rate", "tax", "audit",
            "compliance", "regulation", "SEC", "earnings", "capital", "asset", "liability"
        ]
        query_lower = query.lower()
        return any(keyword in query_lower for keyword in financial_keywords)

    ##the   pipeline of answer and confidence score generation from the query        
    def answer_question(self, query):
        """End-to-end pipeline to answer a question with confidence score"""

         # Apply input guardrail
        blocked, message = self.apply_input_guardrail(query)
        if blocked:
            return {"answer": message, "source_documents": [], "blocked": True, "confidence_score": 0, "confidence_level": "none"}

        if not self.is_financial_question(query):
            return {
                "answer": "This question is outside the scope of financial data. Please ask a question related to finance.",
                "source_documents": [],
                "blocked": True,
                "confidence_score": 0,
                "confidence_level": "none"
            }
       
        
        # Retrieve and rerank relevant contexts
        reranked_docs = self.retrieve_with_reranking(query)
        
        # Generate answer and confidence  score
        answer  = self.generate_answer(query, reranked_docs)

        print(f"answer : {answer}")

    
        
        # Calculate confidence score
        confidence_score = self.calculate_confidence_score(query, reranked_docs, answer)

        print(f"confidence score : {confidence_score}")
        
        confidence_level = self.get_confidence_level(confidence_score)
        
        return {
            "answer": answer,
            "source_documents": reranked_docs,
            "blocked": False,
            "confidence_score": confidence_score,
            "confidence_level": confidence_level
        }