Update recommender/retrieval_ranking.py
Browse files- recommender/retrieval_ranking.py +215 -214
recommender/retrieval_ranking.py
CHANGED
|
@@ -1,215 +1,216 @@
|
|
| 1 |
-
import numpy as np
|
| 2 |
-
import faiss
|
| 3 |
-
from collections import defaultdict
|
| 4 |
-
import os
|
| 5 |
-
import google.generativeai as genai
|
| 6 |
-
from sentence_transformers import CrossEncoder
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
-
|
| 23 |
-
-
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
-
-
|
| 27 |
-
-
|
| 28 |
-
- If the
|
| 29 |
-
- If the query
|
| 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 |
-
scores =
|
| 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 |
-
print(f"
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
f"
|
| 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 |
-
response
|
| 214 |
-
|
|
|
|
| 215 |
return response.text
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import faiss
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
import os
|
| 5 |
+
import google.generativeai as genai
|
| 6 |
+
from sentence_transformers import CrossEncoder
|
| 7 |
+
from data import df
|
| 8 |
+
|
| 9 |
+
# Function to generate direct query using Gemini
|
| 10 |
+
def convert_to_direct_query_gradio(user_query,preferences):
|
| 11 |
+
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 12 |
+
model2 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 13 |
+
|
| 14 |
+
preferences_text = ""
|
| 15 |
+
if preferences:
|
| 16 |
+
preferences_text = "User selected preferences: " + ", ".join(preferences) + "."
|
| 17 |
+
# Prompt with examples for indirect-to-direct conversion
|
| 18 |
+
prompt = f"""
|
| 19 |
+
You are an AI assistant that refines user queries to make them optimized for information retrieval while keeping the original intent.
|
| 20 |
+
|
| 21 |
+
Instructions:
|
| 22 |
+
- Identify the main focus from the query.
|
| 23 |
+
- Reformat the query in a structured way for better retrieval.
|
| 24 |
+
- Ensure the most important feature appears first.
|
| 25 |
+
- Use precise keywords that match credit card benefits.
|
| 26 |
+
- Include the preferences also in the final query.
|
| 27 |
+
- Do NOT introduce new benefits not mentioned by the user.
|
| 28 |
+
- If the user uses vague terms like "vacation", interpret it as travel-related benefits including: airport lounge access, international/domestic travel, hotel benefits, forex waiver.
|
| 29 |
+
- If the query includes terms like "beginner", "entry-level", or "low credit score", include essential features such as cashback, reward points, and basic offers.
|
| 30 |
+
- If the query is empty, generate a useful retrieval-focused query based solely on preferences.
|
| 31 |
+
|
| 32 |
+
Examples:
|
| 33 |
+
|
| 34 |
+
Example 1
|
| 35 |
+
- User Query: "I drive a lot for work and want a credit card with good fuel rewards and travel perks."
|
| 36 |
+
- Optimized Query: "Category: Fuel Rewards | Best credit cards for high fuel spending with maximum rewards & fuel surcharge waiver. Travel perks preferred but secondary."
|
| 37 |
+
|
| 38 |
+
Example 2
|
| 39 |
+
- User Query: "I mostly shop online and want a card that gives high cashback on e-commerce purchases. Food delivery perks would be nice."
|
| 40 |
+
- Optimized Query: "Category: Online Shopping | Credit cards with best cashback on e-commerce platforms like Amazon, Flipkart. Food delivery benefits secondary."
|
| 41 |
+
|
| 42 |
+
Example 3
|
| 43 |
+
- User Query: "I eat out a lot and also order food from Swiggy/Zomato. I want the best dining discounts and food delivery cashback."
|
| 44 |
+
- Optimized Query: "Category: Dining & Food Delivery | Top credit cards offering the best dining discounts at restaurants and cashback on Swiggy/Zomato orders."
|
| 45 |
+
|
| 46 |
+
Now, optimize the following preferences and user query:
|
| 47 |
+
|
| 48 |
+
Preferences: "{preferences_text}"
|
| 49 |
+
User Query: "{user_query}"
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
print("rewriting")
|
| 53 |
+
response = model2.generate_content(prompt)
|
| 54 |
+
|
| 55 |
+
print(response.text)
|
| 56 |
+
return response.text
|
| 57 |
+
|
| 58 |
+
# Function to generate multiple focused subqueries from a user query
|
| 59 |
+
def generate_multi_queries(direct_query, n=3):
|
| 60 |
+
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 61 |
+
model2 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 62 |
+
prompt = f"""
|
| 63 |
+
The following is a detailed credit card search query:
|
| 64 |
+
"{direct_query}"
|
| 65 |
+
Generate {n} distinct subqueries that **collectively cover all the important features** from the original query.
|
| 66 |
+
Each subquery should emphasize a **different combination** of the features (e.g., lounge access, travel insurance, low foreign transaction fees, hotel discounts, etc.).
|
| 67 |
+
Keep the same format: "Category: ... | ...". Make sure all features from the original query are represented across the {n} queries.
|
| 68 |
+
Output only the subqueries, one per line. Do not include any explanations, numbering, or formatting — just plain queries separated by newline characters.
|
| 69 |
+
"""
|
| 70 |
+
response = model2.generate_content(prompt)
|
| 71 |
+
# print(response.text)
|
| 72 |
+
queries = [q.strip() for q in response.text.strip().split('\n') if q.strip()]
|
| 73 |
+
|
| 74 |
+
return queries
|
| 75 |
+
|
| 76 |
+
#retrieval
|
| 77 |
+
#Cross-Encoder Model
|
| 78 |
+
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L6-v2")
|
| 79 |
+
|
| 80 |
+
def rerank_cards_mini(query, cards, top_n=5):
|
| 81 |
+
card_descriptions = [card["description"] for card in cards]
|
| 82 |
+
query_card_pairs = [[query, desc] for desc in card_descriptions]
|
| 83 |
+
scores = cross_encoder.predict(query_card_pairs)
|
| 84 |
+
|
| 85 |
+
#Normalizing
|
| 86 |
+
if len(scores) == 0 or (max(scores) - min(scores)) == 0:
|
| 87 |
+
scores = [1.0] * len(cards)
|
| 88 |
+
else:
|
| 89 |
+
scores = np.array(scores)
|
| 90 |
+
scores = (scores - scores.min()) / (scores.max() - scores.min())
|
| 91 |
+
|
| 92 |
+
# Sort cards by descending score
|
| 93 |
+
ranked_cards = sorted(zip(scores, cards), key=lambda x: x[0], reverse=True)
|
| 94 |
+
top_cards = [card for _, card in ranked_cards[:top_n]]
|
| 95 |
+
|
| 96 |
+
return top_cards
|
| 97 |
+
|
| 98 |
+
def retrieve_relevant_cards(user_query, index, chunk_name_mapping, df, top_k=15):
|
| 99 |
+
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 100 |
+
model_name = "models/text-embedding-004"
|
| 101 |
+
print(f"\nUser Query: {user_query}")
|
| 102 |
+
|
| 103 |
+
# Generate and normalize query embedding
|
| 104 |
+
query_embedding = genai.embed_content(
|
| 105 |
+
model=model_name,
|
| 106 |
+
content=user_query,
|
| 107 |
+
task_type="RETRIEVAL_QUERY"
|
| 108 |
+
)["embedding"]
|
| 109 |
+
# query_embedding=model.encode(user_query)
|
| 110 |
+
query_embedding = np.array(query_embedding, dtype=np.float32)
|
| 111 |
+
faiss.normalize_L2(query_embedding.reshape(1, -1))
|
| 112 |
+
|
| 113 |
+
# Search FAISS index for top-k chunks
|
| 114 |
+
D, I = index.search(np.expand_dims(query_embedding, axis=0), top_k * 5)
|
| 115 |
+
similarity_scores = D[0]
|
| 116 |
+
|
| 117 |
+
# Map chunks back to unique card names with highest similarity per card
|
| 118 |
+
card_similarity = defaultdict(float)
|
| 119 |
+
card_dict = {card["name"]: card for card in df.to_dict(orient="records")}
|
| 120 |
+
unique_cards = {}
|
| 121 |
+
|
| 122 |
+
for i, chunk_idx in enumerate(I[0]):
|
| 123 |
+
if chunk_idx == -1:
|
| 124 |
+
continue
|
| 125 |
+
card_name = chunk_name_mapping[chunk_idx]
|
| 126 |
+
if card_name not in unique_cards or similarity_scores[i] > card_similarity[card_name]:
|
| 127 |
+
card_similarity[card_name] = similarity_scores[i]
|
| 128 |
+
unique_cards[card_name] = {
|
| 129 |
+
"name": card_name,
|
| 130 |
+
"description": card_dict[card_name]["description"],
|
| 131 |
+
"similarity": similarity_scores[i]
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# Sort by similarity and take top_k
|
| 135 |
+
ordered_cards = sorted(unique_cards.values(), key=lambda x: x["similarity"], reverse=True)[:top_k]
|
| 136 |
+
|
| 137 |
+
print("\nTop Recommended Cards:")
|
| 138 |
+
for card in ordered_cards:
|
| 139 |
+
print(f"- {card['name']} (Similarity: {card['similarity']:.4f})")
|
| 140 |
+
|
| 141 |
+
return ordered_cards
|
| 142 |
+
|
| 143 |
+
def retrieve_and_rank_cards(faiss_index,filtered_mapping,direct_query, queries, top_k=10):
|
| 144 |
+
all_retrieved=[]
|
| 145 |
+
for query in queries:
|
| 146 |
+
results = retrieve_relevant_cards(
|
| 147 |
+
query, faiss_index, filtered_mapping, df, 10
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
reranked_cards = results[:5]
|
| 151 |
+
|
| 152 |
+
all_retrieved.extend(reranked_cards)
|
| 153 |
+
|
| 154 |
+
# Removing duplicates
|
| 155 |
+
seen = set()
|
| 156 |
+
unique_cards = []
|
| 157 |
+
for card in all_retrieved:
|
| 158 |
+
if card["name"] not in seen:
|
| 159 |
+
seen.add(card["name"])
|
| 160 |
+
unique_cards.append(card)
|
| 161 |
+
if not unique_cards:
|
| 162 |
+
return unique_cards
|
| 163 |
+
reranked_cards = rerank_cards_mini(direct_query, unique_cards, top_n=5)
|
| 164 |
+
return reranked_cards
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def generate_credit_card_recommendation_gemini(indirect_query,user_query,retrieved_cards, max_new_tokens=500):
|
| 168 |
+
genai.configure(api_key=os.environ.get("api_key_3"))
|
| 169 |
+
model3 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 170 |
+
print("using gemini")
|
| 171 |
+
if not retrieved_cards:
|
| 172 |
+
return "Unfortunately, no credit cards match your eligibility criteria."
|
| 173 |
+
for card in retrieved_cards:
|
| 174 |
+
print(f"Name: {card['name']}")
|
| 175 |
+
print(f"Description: {card['description']}\n")
|
| 176 |
+
|
| 177 |
+
# Formatting retrieved cards for input context
|
| 178 |
+
cards_info = "\n\n".join([
|
| 179 |
+
f"{card['name']}\n"
|
| 180 |
+
f"- Description: {card['description']}\n\n"
|
| 181 |
+
for card in retrieved_cards
|
| 182 |
+
])
|
| 183 |
+
|
| 184 |
+
# Gemini Prompt
|
| 185 |
+
gemini_prompt = f"""
|
| 186 |
+
You are a financial analyst specializing in credit cards and rewards optimization.
|
| 187 |
+
Your task is to analyze and select the best credit card based on user priorities.
|
| 188 |
+
|
| 189 |
+
### User's Query:
|
| 190 |
+
{indirect_query+user_query}
|
| 191 |
+
|
| 192 |
+
### Available Credit Cards:
|
| 193 |
+
{cards_info}
|
| 194 |
+
|
| 195 |
+
### Instructions:
|
| 196 |
+
1️ Analyze the user's need from the given query .
|
| 197 |
+
2️ Select the best card based on that primary need and only from the details present in the desctiption.Do not include details which are not explicitly mentioned.
|
| 198 |
+
3️ Explain why it's the best choice (list benefits concisely).
|
| 199 |
+
4 Do not assume and include benefits or features which is nor explicitly mentioned in the card description.
|
| 200 |
+
5 Mention the benefits only if it explicitly mentioned in the card description and if it is not mentioned just skip it and do not mention anything about it.
|
| 201 |
+
6 Do not include extra symbols like * or #, just generate a textual response maybe with numbers for points.
|
| 202 |
+
7️ If the user mentions they need FD-based cards or cards for students or if they have low credit score, assume all the provided cards are FD-based. Just compare them based on benefits and choose the best one.
|
| 203 |
+
8 If user asks for fd based cards and the descriptions do not mention it just pick a best card from the given list.
|
| 204 |
+
|
| 205 |
+
### Expected Output Format:
|
| 206 |
+
Best Card: [Card Name]
|
| 207 |
+
Why It’s the Best:
|
| 208 |
+
1️ [Primary Benefit] : [Explanation]
|
| 209 |
+
2️ [Additional Perks] : [Explanation]
|
| 210 |
+
3️ [Final Justification] : [Why it's the best fit]
|
| 211 |
+
"""
|
| 212 |
+
|
| 213 |
+
# Generate response using Gemini
|
| 214 |
+
response = model3.generate_content(gemini_prompt)
|
| 215 |
+
|
| 216 |
return response.text
|