Delete recommender
Browse files- recommender/graph_retrieval_vectordb.py +0 -226
- recommender/recommender.py +0 -154
- recommender/retrieval_ranking.py +0 -215
recommender/graph_retrieval_vectordb.py
DELETED
|
@@ -1,226 +0,0 @@
|
|
| 1 |
-
import google.generativeai as genai
|
| 2 |
-
from neo4j import GraphDatabase
|
| 3 |
-
import os
|
| 4 |
-
import numpy as np
|
| 5 |
-
import faiss
|
| 6 |
-
from app import card_descriptions,eligibility_df
|
| 7 |
-
|
| 8 |
-
#neo4j credentials
|
| 9 |
-
NEO4J_URI = os.environ.get("NEO4J_URI")
|
| 10 |
-
NEO4J_USER = os.environ.get("NEO4J_USER")
|
| 11 |
-
NEO4J_PASS = os.environ.get("NEO4J_PASS")
|
| 12 |
-
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASS))
|
| 13 |
-
|
| 14 |
-
#generating cypher query
|
| 15 |
-
def generate_cypher(user_query, query_intent, include_cobranded):
|
| 16 |
-
genai.configure(api_key='AIzaSyAHoi9xbYAThtjXlyF_IKFtruoWYoUCjJQ')
|
| 17 |
-
model3 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 18 |
-
print("inside cypher query gen")
|
| 19 |
-
|
| 20 |
-
context_note = f"""
|
| 21 |
-
Contextual Flags:
|
| 22 |
-
- FD Card intent: {query_intent}
|
| 23 |
-
- Include co-branded cards: {include_cobranded}
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
cypher_prompt = f"""
|
| 27 |
-
You are an expert Neo4j Cypher query generator.
|
| 28 |
-
|
| 29 |
-
Given a user’s question, graph schema, and **contextual flags**, generate the correct Cypher query. The query should return only the cards `c`.
|
| 30 |
-
|
| 31 |
-
ONLY output the Cypher query. Do NOT explain anything.
|
| 32 |
-
|
| 33 |
-
---
|
| 34 |
-
|
| 35 |
-
Graph Schema:
|
| 36 |
-
- Nodes:
|
| 37 |
-
- (Card): Properties = name, bank_name, card_type, premium, co_branded
|
| 38 |
-
- (Feature): Properties = name
|
| 39 |
-
- Relationships:
|
| 40 |
-
- (Card)-[:HAS_FEATURE]->(Feature)
|
| 41 |
-
|
| 42 |
-
Feature Inclusion Rules:
|
| 43 |
-
- Only include relevant features based on user query.
|
| 44 |
-
- Forex markup fee and foreign transaction fee are the same.
|
| 45 |
-
- If FD Card intent is true then include the features if the query contains any and also include “General Cashback” or “General Reward Points”
|
| 46 |
-
- Don’t add “General Cashback” or “General Reward Points” if it is not required.
|
| 47 |
-
- If fuel is mentioned, include both `Fuel Benefits` and `Fuel Surcharge Waiver`.
|
| 48 |
-
- **ALWAYS** match features using: `f.name IN [...]` — even if there is only **one** feature.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
Valid values:
|
| 52 |
-
- card_type: 'FD Card' or 'Regular'
|
| 53 |
-
- premium: true (no concept of false — just include it if applicable)
|
| 54 |
-
- co_branded: true (no concept of false — just include it if applicable)
|
| 55 |
-
|
| 56 |
-
MANDATORY Condition Rules:
|
| 57 |
-
- If FD Card intent is true → include: `c.card_type = 'FD Card'`
|
| 58 |
-
- Else → include: `c.card_type = 'Regular'`
|
| 59 |
-
- If the query is based on beginners or students or people with no or low credit history then use FD Card.
|
| 60 |
-
- If the query uses words like "premium", "elite", "luxury", "exclusive", "infinia", "black", etc. → include: `AND c.premium = true`
|
| 61 |
-
- If the query includes low spending, without high spending or budget → include: `(c.premium IS NULL OR c.premium = false)`
|
| 62 |
-
- If include co-branded is false → include: `AND (c.co_branded IS NULL OR c.co_branded = false)`
|
| 63 |
-
- Use exact values for `bank_name` as in the database: ["SBI", "HDFC", "Axis", "ICICI", "YES", "HSBC", "IDFC", "American Express", "SMB", "Federal Bank", "AU Bank", "IDBI", "Kotak Mahindra Bank","IndusInd","RBL"]
|
| 64 |
-
- Do not add bank after the name of the bank if it is not mentioned in the datase list.
|
| 65 |
-
- These conditions are **MANDATORY**. If they apply, include them in the `WHERE` clause. Do not skip them.
|
| 66 |
-
|
| 67 |
-
---
|
| 68 |
-
|
| 69 |
-
Available features:
|
| 70 |
-
"General Cashback", "Fuel Surcharge Waiver", "Fuel Benefits", "Welcome Bonus",
|
| 71 |
-
"Airport Lounge Access", "General Reward Points", "Domestic Travel Benefits",
|
| 72 |
-
"Movie Benefits", "Flight Discounts", "International Travel Benefits",
|
| 73 |
-
"Hotel Benefits", "Dining Benefits", "Daily Spends (Grocery)", "Railway Benefits",
|
| 74 |
-
"Travel Benefits", "Railway Lounge", "Insurance", "Utility",
|
| 75 |
-
"E-commerce Platform Benefits", "Air Miles", "Spa Access Benefits",
|
| 76 |
-
"Lifestyle & Luxury Perks", "Golf Access & Perks", "Online Shopping Benefits",
|
| 77 |
-
"UPI Transaction Support", "Health Benefits", "EMI Conversion Options",
|
| 78 |
-
"No Forex Markup Fee", "Roadside Assistance", "Rupay Network Support",Super Premium Cards
|
| 79 |
-
|
| 80 |
-
---
|
| 81 |
-
|
| 82 |
-
Few-shot Examples:
|
| 83 |
-
|
| 84 |
-
User Query: Show premium cards with airport lounge access
|
| 85 |
-
Cypher:
|
| 86 |
-
MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
|
| 87 |
-
WHERE f.name IN ["Airport Lounge Access"]
|
| 88 |
-
AND c.card_type = 'Regular'
|
| 89 |
-
AND c.premium = true
|
| 90 |
-
RETURN c
|
| 91 |
-
|
| 92 |
-
User Query: I want FD cards with spa access and golf perks
|
| 93 |
-
Cypher:
|
| 94 |
-
MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
|
| 95 |
-
WHERE f.name IN ["Spa Access Benefits", "Golf Access & Perks"]
|
| 96 |
-
AND c.card_type = 'FD Card'
|
| 97 |
-
RETURN c
|
| 98 |
-
|
| 99 |
-
User Query: Cards that support UPI but are not co-branded
|
| 100 |
-
Cypher:
|
| 101 |
-
MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
|
| 102 |
-
WHERE f.name IN ["UPI Transaction Support"]
|
| 103 |
-
AND c.card_type = 'Regular'
|
| 104 |
-
AND (c.co_branded IS NULL OR c.co_branded = false)
|
| 105 |
-
RETURN c
|
| 106 |
-
|
| 107 |
-
---
|
| 108 |
-
|
| 109 |
-
{context_note}
|
| 110 |
-
|
| 111 |
-
User Query: {user_query}
|
| 112 |
-
Cypher:
|
| 113 |
-
"""
|
| 114 |
-
|
| 115 |
-
response = model3.generate_content(cypher_prompt.strip())
|
| 116 |
-
cypher_code = response.text.strip()
|
| 117 |
-
|
| 118 |
-
if cypher_code.startswith("```cypher"):
|
| 119 |
-
cypher_code = cypher_code[len("```cypher"):].strip()
|
| 120 |
-
elif cypher_code.startswith("```"):
|
| 121 |
-
cypher_code = cypher_code[len("```"):].strip()
|
| 122 |
-
if cypher_code.endswith("```"):
|
| 123 |
-
cypher_code = cypher_code[:-3].strip()
|
| 124 |
-
|
| 125 |
-
return cypher_code
|
| 126 |
-
|
| 127 |
-
#generating embeddings (run only once)
|
| 128 |
-
def chunk_text(text, chunk_size=1):
|
| 129 |
-
sentences = text.split("; ")
|
| 130 |
-
return ["; ".join(sentences[i:i+chunk_size]) for i in range(0, len(sentences), chunk_size)]
|
| 131 |
-
|
| 132 |
-
def get_gemini_embeddings(text_list):
|
| 133 |
-
embeddings = []
|
| 134 |
-
print("Generating embeddings with Gemini...")
|
| 135 |
-
for text in text_list:
|
| 136 |
-
response = genai.embed_content(
|
| 137 |
-
model=model_name,
|
| 138 |
-
content=text,
|
| 139 |
-
task_type="RETRIEVAL_DOCUMENT")
|
| 140 |
-
embeddings.append(np.array(response["embedding"], dtype=np.float32))
|
| 141 |
-
return np.vstack(embeddings)
|
| 142 |
-
|
| 143 |
-
# Chunk all card descriptions
|
| 144 |
-
chunk_texts = []
|
| 145 |
-
chunk_name_mapping = {}
|
| 146 |
-
for card_idx, (card_name, desc) in enumerate(card_descriptions.items()):
|
| 147 |
-
chunks = chunk_text(desc)
|
| 148 |
-
for chunk in chunks:
|
| 149 |
-
chunk_index = len(chunk_texts)
|
| 150 |
-
chunk_texts.append(chunk)
|
| 151 |
-
chunk_name_mapping[chunk_index] = card_name
|
| 152 |
-
|
| 153 |
-
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 154 |
-
model_name = "models/text-embedding-004"
|
| 155 |
-
|
| 156 |
-
#Generating embeddings
|
| 157 |
-
chunk_embeddings = get_gemini_embeddings(chunk_texts)
|
| 158 |
-
faiss.normalize_L2(chunk_embeddings)
|
| 159 |
-
|
| 160 |
-
print(f"Prepared {len(chunk_texts)} total chunks and embeddings.")
|
| 161 |
-
|
| 162 |
-
#eligibility filter
|
| 163 |
-
def eligibility_filter(cards, user_income, user_cibil, user_age,min_joining_fee, max_joining_fee,
|
| 164 |
-
min_annual_fee, max_annual_fee):
|
| 165 |
-
eligible_cards = []
|
| 166 |
-
print("inside filter")
|
| 167 |
-
for card_name in cards:
|
| 168 |
-
# print(eligibility_df.columns)
|
| 169 |
-
|
| 170 |
-
eligibility = eligibility_df[eligibility_df["Name"] == card_name]
|
| 171 |
-
|
| 172 |
-
if not eligibility.empty:
|
| 173 |
-
min_income = eligibility.iloc[0]["Minimum Income (LPA)"]
|
| 174 |
-
min_cibil = eligibility.iloc[0]["Minimum Credit Score"]
|
| 175 |
-
min_age = eligibility.iloc[0]["Minimum Age"]
|
| 176 |
-
max_age = eligibility.iloc[0]["Maximum Age"]
|
| 177 |
-
joining_fee=eligibility.iloc[0]["Joining fee"]
|
| 178 |
-
annual_fee=eligibility.iloc[0]["Annual fee"]
|
| 179 |
-
if (user_income >= min_income and
|
| 180 |
-
user_cibil >= min_cibil and
|
| 181 |
-
min_age <= user_age <= max_age and
|
| 182 |
-
min_joining_fee<=joining_fee<=max_joining_fee and
|
| 183 |
-
min_annual_fee<=annual_fee<=max_annual_fee):
|
| 184 |
-
eligible_cards.append(card_name)
|
| 185 |
-
|
| 186 |
-
return eligible_cards
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
#function for retrieving cards from knowledge graph
|
| 191 |
-
class Neo4jConnectionError(Exception):
|
| 192 |
-
pass
|
| 193 |
-
|
| 194 |
-
def run_cypher_query(user_query, query, use_eligibility, user_income, user_cibil, user_age,
|
| 195 |
-
min_joining_fee, max_joining_fee, min_annual_fee, max_annual_fee):
|
| 196 |
-
|
| 197 |
-
try:
|
| 198 |
-
with driver.session() as session:
|
| 199 |
-
|
| 200 |
-
result = session.run(query)
|
| 201 |
-
matched_cards = [record["c"] for record in result]
|
| 202 |
-
filtered_cards = [card["name"] for card in matched_cards]
|
| 203 |
-
|
| 204 |
-
except Exception as e:
|
| 205 |
-
raise Neo4jConnectionError("Failed to connect to the Neo4j database.") from e
|
| 206 |
-
|
| 207 |
-
if use_eligibility:
|
| 208 |
-
filtered_cards = eligibility_filter(filtered_cards, user_income, user_cibil, user_age,
|
| 209 |
-
min_joining_fee, max_joining_fee,
|
| 210 |
-
min_annual_fee, max_annual_fee)
|
| 211 |
-
|
| 212 |
-
# for card in filtered_cards:
|
| 213 |
-
# print("error")
|
| 214 |
-
# print(card["name"])
|
| 215 |
-
relevant_indexes = [i for i, name in chunk_name_mapping.items() if name in filtered_cards]
|
| 216 |
-
filtered_embeddings = chunk_embeddings[relevant_indexes]
|
| 217 |
-
filtered_texts = [chunk_texts[i] for i in relevant_indexes]
|
| 218 |
-
filtered_mapping = {i: chunk_name_mapping[idx] for i, idx in enumerate(relevant_indexes)}
|
| 219 |
-
|
| 220 |
-
# Build FAISS index
|
| 221 |
-
dim = filtered_embeddings.shape[1]
|
| 222 |
-
faiss_index = faiss.IndexFlatIP(dim)
|
| 223 |
-
faiss_index.add(filtered_embeddings)
|
| 224 |
-
|
| 225 |
-
print(f"FAISS index created with {len(filtered_embeddings)} filtered chunks.")
|
| 226 |
-
return faiss_index, filtered_mapping
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
recommender/recommender.py
DELETED
|
@@ -1,154 +0,0 @@
|
|
| 1 |
-
import ast
|
| 2 |
-
import re
|
| 3 |
-
import pd
|
| 4 |
-
import tempfile
|
| 5 |
-
import os
|
| 6 |
-
from intent_classification.fd_classification import find_intent
|
| 7 |
-
from intent_classification.retrieval_classification import handle_query_classification,find_matching_card,generate_card_response_with_context
|
| 8 |
-
from recommender.retrieval_ranking import generate_multi_queries,convert_to_direct_query_gradio,retrieve_and_rank_cards,generate_credit_card_recommendation_gemini,cross_encoder
|
| 9 |
-
from app import eligibility_lookup,card_features_lookup
|
| 10 |
-
from recommender.graph_retrieval_vectordb import generate_cypher,run_cypher_query,Neo4jConnectionError
|
| 11 |
-
|
| 12 |
-
#function to pass the retrieved cards and generated response to the UI
|
| 13 |
-
def recommend_cards_gradio(user_query, preferences, income, cibil, age,
|
| 14 |
-
min_joining_fee, max_joining_fee,
|
| 15 |
-
min_annual_fee, max_annual_fee,
|
| 16 |
-
use_eligibility=True,include_cobranded=True):
|
| 17 |
-
try:
|
| 18 |
-
# print(user_query)
|
| 19 |
-
if(user_query):
|
| 20 |
-
result = handle_query_classification(user_query)
|
| 21 |
-
|
| 22 |
-
if result["intent"] == "no_retrieval":
|
| 23 |
-
return (
|
| 24 |
-
f"<div style='background-color:#e3f2fd;padding:20px;border-radius:10px;'>"
|
| 25 |
-
f"<pre style='white-space:pre-wrap;font-size:13px;color:#212121;'>{result['response']}</pre></div>",
|
| 26 |
-
[["No retrieval required", "Answered using LLM"]],
|
| 27 |
-
None,
|
| 28 |
-
[],
|
| 29 |
-
{},
|
| 30 |
-
"Answered without retrieval"
|
| 31 |
-
)
|
| 32 |
-
elif result["intent"] == "specific":
|
| 33 |
-
matched_card = find_matching_card(user_query)
|
| 34 |
-
if matched_card:
|
| 35 |
-
gemini_answer = generate_card_response_with_context(user_query, matched_card)
|
| 36 |
-
card_name = matched_card["name"]
|
| 37 |
-
card_desc = matched_card["description"]
|
| 38 |
-
card_lookup = {card_name: card_desc}
|
| 39 |
-
|
| 40 |
-
# Constructing eligibility info if available
|
| 41 |
-
eligibility_info = eligibility_lookup.get(card_name, "No eligibility or fee information available.")
|
| 42 |
-
chat_history_entry = f"{card_name}:\n{card_desc}\n\nEligibility & Fees:\n{eligibility_info}"
|
| 43 |
-
|
| 44 |
-
return (
|
| 45 |
-
f"<div style='background-color:#fffde7;padding:20px;border-radius:10px;'>"
|
| 46 |
-
f"<pre style='white-space:pre-wrap;font-size:13px;color:#212121;'>{gemini_answer}</pre></div>",
|
| 47 |
-
[["Specific card detected", card_name]],
|
| 48 |
-
None,
|
| 49 |
-
[],
|
| 50 |
-
card_lookup,
|
| 51 |
-
user_query
|
| 52 |
-
)
|
| 53 |
-
else:
|
| 54 |
-
return (
|
| 55 |
-
"<b style='color:red;'>Card mentioned not found in database.</b>",
|
| 56 |
-
[["Card not found", "Try another card name."]],
|
| 57 |
-
None,
|
| 58 |
-
[],
|
| 59 |
-
{},
|
| 60 |
-
"Card not found"
|
| 61 |
-
)
|
| 62 |
-
direct_query = convert_to_direct_query_gradio(user_query, preferences)
|
| 63 |
-
queries = generate_multi_queries(direct_query)
|
| 64 |
-
|
| 65 |
-
if cibil < 700 and use_eligibility:
|
| 66 |
-
query_intent = True
|
| 67 |
-
else:
|
| 68 |
-
query_intent = find_intent(user_query)
|
| 69 |
-
print(query_intent)
|
| 70 |
-
cypher_query = generate_cypher(direct_query, query_intent,include_cobranded)
|
| 71 |
-
print("Generated Cypher:\n", cypher_query)
|
| 72 |
-
|
| 73 |
-
try:
|
| 74 |
-
faiss_index, filtered_mapping = run_cypher_query(
|
| 75 |
-
user_query, cypher_query, use_eligibility,
|
| 76 |
-
income, cibil, age,
|
| 77 |
-
min_joining_fee, max_joining_fee,
|
| 78 |
-
min_annual_fee, max_annual_fee
|
| 79 |
-
)
|
| 80 |
-
except Neo4jConnectionError as graph_err:
|
| 81 |
-
return (
|
| 82 |
-
"<b style='color:red;'>Graph database connection failed. Please try again later.</b>",
|
| 83 |
-
[["Graph database error", str(graph_err)]],
|
| 84 |
-
None,
|
| 85 |
-
[],
|
| 86 |
-
{},
|
| 87 |
-
"Graph DB connection error"
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
-
cards = retrieve_and_rank_cards(faiss_index, filtered_mapping, direct_query, queries, top_k=10)
|
| 91 |
-
gemini_summary = generate_credit_card_recommendation_gemini(user_query, direct_query, cards)
|
| 92 |
-
|
| 93 |
-
if not cards:
|
| 94 |
-
return (
|
| 95 |
-
"<b style='color:red;'>No eligible cards found.</b>",
|
| 96 |
-
[["No eligible cards found", "Please try a different query or check your input values."]],
|
| 97 |
-
None,
|
| 98 |
-
[],
|
| 99 |
-
{},
|
| 100 |
-
"No eligible card found"
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
match = re.search(r"f\.name IN (\[.*?\])", cypher_query)
|
| 104 |
-
query_features = set(ast.literal_eval(match.group(1))) if match else set()
|
| 105 |
-
|
| 106 |
-
card_rows = []
|
| 107 |
-
for score, card in sorted(
|
| 108 |
-
zip(cross_encoder.predict([[direct_query, card["description"]] for card in cards]), cards),
|
| 109 |
-
reverse=True,
|
| 110 |
-
key=lambda x: x[0]
|
| 111 |
-
):
|
| 112 |
-
card_name = card["name"]
|
| 113 |
-
card_desc = card["description"]
|
| 114 |
-
matched_features = query_features.intersection(card_features_lookup.get(card_name, set()))
|
| 115 |
-
feature_str = ", ".join(matched_features) if matched_features else "None"
|
| 116 |
-
card_rows.append([card_name, feature_str, card_desc])
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
card_names = [row[0] for row in card_rows]
|
| 120 |
-
card_lookup = {row[0]: row[2] for row in card_rows}
|
| 121 |
-
top_card_html = f"""
|
| 122 |
-
<div style="
|
| 123 |
-
background-color: #fff3e0;
|
| 124 |
-
color: #212121;
|
| 125 |
-
border-radius: 16px;
|
| 126 |
-
padding: 20px;
|
| 127 |
-
border: 2px solid #ffa726;
|
| 128 |
-
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
|
| 129 |
-
margin-bottom: 16px;
|
| 130 |
-
font-family: sans-serif;
|
| 131 |
-
font-size: 8px;
|
| 132 |
-
">
|
| 133 |
-
<pre style="white-space: pre-wrap; font-size: 13px; color: #212121;">{gemini_summary}</pre>
|
| 134 |
-
</div>
|
| 135 |
-
"""
|
| 136 |
-
|
| 137 |
-
df_cards = pd.DataFrame(card_rows, columns=["Card Name", "Matched Features", "Description"])
|
| 138 |
-
filename = "recommended_cards.csv"
|
| 139 |
-
temp_dir = tempfile.gettempdir()
|
| 140 |
-
file_path = os.path.join(temp_dir, filename)
|
| 141 |
-
df_cards.to_csv(file_path, index=False)
|
| 142 |
-
|
| 143 |
-
return top_card_html, card_rows, file_path, card_names, card_lookup, direct_query
|
| 144 |
-
|
| 145 |
-
except Exception as e:
|
| 146 |
-
print("Error:", e)
|
| 147 |
-
return (
|
| 148 |
-
"An unexpected error occurred. Please try again in a few minutes.",
|
| 149 |
-
[["Something went wrong", "Please try again."]],
|
| 150 |
-
None,
|
| 151 |
-
[],
|
| 152 |
-
{},
|
| 153 |
-
"Unexpected error occurred, please try again in a while"
|
| 154 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
recommender/retrieval_ranking.py
DELETED
|
@@ -1,215 +0,0 @@
|
|
| 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 |
-
# Function to generate direct query using Gemini
|
| 9 |
-
def convert_to_direct_query_gradio(user_query,preferences):
|
| 10 |
-
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 11 |
-
model2 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 12 |
-
|
| 13 |
-
preferences_text = ""
|
| 14 |
-
if preferences:
|
| 15 |
-
preferences_text = "User selected preferences: " + ", ".join(preferences) + "."
|
| 16 |
-
# Prompt with examples for indirect-to-direct conversion
|
| 17 |
-
prompt = f"""
|
| 18 |
-
You are an AI assistant that refines user queries to make them optimized for information retrieval while keeping the original intent.
|
| 19 |
-
|
| 20 |
-
Instructions:
|
| 21 |
-
- Identify the main focus from the query.
|
| 22 |
-
- Reformat the query in a structured way for better retrieval.
|
| 23 |
-
- Ensure the most important feature appears first.
|
| 24 |
-
- Use precise keywords that match credit card benefits.
|
| 25 |
-
- Include the preferences also in the final query.
|
| 26 |
-
- Do NOT introduce new benefits not mentioned by the user.
|
| 27 |
-
- 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.
|
| 28 |
-
- If the query includes terms like "beginner", "entry-level", or "low credit score", include essential features such as cashback, reward points, and basic offers.
|
| 29 |
-
- If the query is empty, generate a useful retrieval-focused query based solely on preferences.
|
| 30 |
-
|
| 31 |
-
Examples:
|
| 32 |
-
|
| 33 |
-
Example 1
|
| 34 |
-
- User Query: "I drive a lot for work and want a credit card with good fuel rewards and travel perks."
|
| 35 |
-
- Optimized Query: "Category: Fuel Rewards | Best credit cards for high fuel spending with maximum rewards & fuel surcharge waiver. Travel perks preferred but secondary."
|
| 36 |
-
|
| 37 |
-
Example 2
|
| 38 |
-
- User Query: "I mostly shop online and want a card that gives high cashback on e-commerce purchases. Food delivery perks would be nice."
|
| 39 |
-
- Optimized Query: "Category: Online Shopping | Credit cards with best cashback on e-commerce platforms like Amazon, Flipkart. Food delivery benefits secondary."
|
| 40 |
-
|
| 41 |
-
Example 3
|
| 42 |
-
- User Query: "I eat out a lot and also order food from Swiggy/Zomato. I want the best dining discounts and food delivery cashback."
|
| 43 |
-
- Optimized Query: "Category: Dining & Food Delivery | Top credit cards offering the best dining discounts at restaurants and cashback on Swiggy/Zomato orders."
|
| 44 |
-
|
| 45 |
-
Now, optimize the following preferences and user query:
|
| 46 |
-
|
| 47 |
-
Preferences: "{preferences_text}"
|
| 48 |
-
User Query: "{user_query}"
|
| 49 |
-
"""
|
| 50 |
-
|
| 51 |
-
print("rewriting")
|
| 52 |
-
response = model2.generate_content(prompt)
|
| 53 |
-
|
| 54 |
-
print(response.text)
|
| 55 |
-
return response.text
|
| 56 |
-
|
| 57 |
-
# Function to generate multiple focused subqueries from a user query
|
| 58 |
-
def generate_multi_queries(direct_query, n=3):
|
| 59 |
-
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 60 |
-
model2 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 61 |
-
prompt = f"""
|
| 62 |
-
The following is a detailed credit card search query:
|
| 63 |
-
"{direct_query}"
|
| 64 |
-
Generate {n} distinct subqueries that **collectively cover all the important features** from the original query.
|
| 65 |
-
Each subquery should emphasize a **different combination** of the features (e.g., lounge access, travel insurance, low foreign transaction fees, hotel discounts, etc.).
|
| 66 |
-
Keep the same format: "Category: ... | ...". Make sure all features from the original query are represented across the {n} queries.
|
| 67 |
-
Output only the subqueries, one per line. Do not include any explanations, numbering, or formatting — just plain queries separated by newline characters.
|
| 68 |
-
"""
|
| 69 |
-
response = model2.generate_content(prompt)
|
| 70 |
-
# print(response.text)
|
| 71 |
-
queries = [q.strip() for q in response.text.strip().split('\n') if q.strip()]
|
| 72 |
-
|
| 73 |
-
return queries
|
| 74 |
-
|
| 75 |
-
#retrieval
|
| 76 |
-
#Cross-Encoder Model
|
| 77 |
-
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L6-v2")
|
| 78 |
-
|
| 79 |
-
def rerank_cards_mini(query, cards, top_n=5):
|
| 80 |
-
card_descriptions = [card["description"] for card in cards]
|
| 81 |
-
query_card_pairs = [[query, desc] for desc in card_descriptions]
|
| 82 |
-
scores = cross_encoder.predict(query_card_pairs)
|
| 83 |
-
|
| 84 |
-
#Normalizing
|
| 85 |
-
if len(scores) == 0 or (max(scores) - min(scores)) == 0:
|
| 86 |
-
scores = [1.0] * len(cards)
|
| 87 |
-
else:
|
| 88 |
-
scores = np.array(scores)
|
| 89 |
-
scores = (scores - scores.min()) / (scores.max() - scores.min())
|
| 90 |
-
|
| 91 |
-
# Sort cards by descending score
|
| 92 |
-
ranked_cards = sorted(zip(scores, cards), key=lambda x: x[0], reverse=True)
|
| 93 |
-
top_cards = [card for _, card in ranked_cards[:top_n]]
|
| 94 |
-
|
| 95 |
-
return top_cards
|
| 96 |
-
|
| 97 |
-
def retrieve_relevant_cards(user_query, index, chunk_name_mapping, df, top_k=15):
|
| 98 |
-
genai.configure(api_key=os.environ.get("api_key_2"))
|
| 99 |
-
model_name = "models/text-embedding-004"
|
| 100 |
-
print(f"\nUser Query: {user_query}")
|
| 101 |
-
|
| 102 |
-
# Generate and normalize query embedding
|
| 103 |
-
query_embedding = genai.embed_content(
|
| 104 |
-
model=model_name,
|
| 105 |
-
content=user_query,
|
| 106 |
-
task_type="RETRIEVAL_QUERY"
|
| 107 |
-
)["embedding"]
|
| 108 |
-
# query_embedding=model.encode(user_query)
|
| 109 |
-
query_embedding = np.array(query_embedding, dtype=np.float32)
|
| 110 |
-
faiss.normalize_L2(query_embedding.reshape(1, -1))
|
| 111 |
-
|
| 112 |
-
# Search FAISS index for top-k chunks
|
| 113 |
-
D, I = index.search(np.expand_dims(query_embedding, axis=0), top_k * 5)
|
| 114 |
-
similarity_scores = D[0]
|
| 115 |
-
|
| 116 |
-
# Map chunks back to unique card names with highest similarity per card
|
| 117 |
-
card_similarity = defaultdict(float)
|
| 118 |
-
card_dict = {card["name"]: card for card in df.to_dict(orient="records")}
|
| 119 |
-
unique_cards = {}
|
| 120 |
-
|
| 121 |
-
for i, chunk_idx in enumerate(I[0]):
|
| 122 |
-
if chunk_idx == -1:
|
| 123 |
-
continue
|
| 124 |
-
card_name = chunk_name_mapping[chunk_idx]
|
| 125 |
-
if card_name not in unique_cards or similarity_scores[i] > card_similarity[card_name]:
|
| 126 |
-
card_similarity[card_name] = similarity_scores[i]
|
| 127 |
-
unique_cards[card_name] = {
|
| 128 |
-
"name": card_name,
|
| 129 |
-
"description": card_dict[card_name]["description"],
|
| 130 |
-
"similarity": similarity_scores[i]
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
# Sort by similarity and take top_k
|
| 134 |
-
ordered_cards = sorted(unique_cards.values(), key=lambda x: x["similarity"], reverse=True)[:top_k]
|
| 135 |
-
|
| 136 |
-
print("\nTop Recommended Cards:")
|
| 137 |
-
for card in ordered_cards:
|
| 138 |
-
print(f"- {card['name']} (Similarity: {card['similarity']:.4f})")
|
| 139 |
-
|
| 140 |
-
return ordered_cards
|
| 141 |
-
|
| 142 |
-
def retrieve_and_rank_cards(faiss_index,filtered_mapping,direct_query, queries, top_k=10):
|
| 143 |
-
all_retrieved=[]
|
| 144 |
-
for query in queries:
|
| 145 |
-
results = retrieve_relevant_cards(
|
| 146 |
-
query, faiss_index, filtered_mapping, df, 10
|
| 147 |
-
)
|
| 148 |
-
|
| 149 |
-
reranked_cards = results[:5]
|
| 150 |
-
|
| 151 |
-
all_retrieved.extend(reranked_cards)
|
| 152 |
-
|
| 153 |
-
# Removing duplicates
|
| 154 |
-
seen = set()
|
| 155 |
-
unique_cards = []
|
| 156 |
-
for card in all_retrieved:
|
| 157 |
-
if card["name"] not in seen:
|
| 158 |
-
seen.add(card["name"])
|
| 159 |
-
unique_cards.append(card)
|
| 160 |
-
if not unique_cards:
|
| 161 |
-
return unique_cards
|
| 162 |
-
reranked_cards = rerank_cards_mini(direct_query, unique_cards, top_n=5)
|
| 163 |
-
return reranked_cards
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def generate_credit_card_recommendation_gemini(indirect_query,user_query,retrieved_cards, max_new_tokens=500):
|
| 167 |
-
genai.configure(api_key=os.environ.get("api_key_3"))
|
| 168 |
-
model3 = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 169 |
-
print("using gemini")
|
| 170 |
-
if not retrieved_cards:
|
| 171 |
-
return "Unfortunately, no credit cards match your eligibility criteria."
|
| 172 |
-
for card in retrieved_cards:
|
| 173 |
-
print(f"Name: {card['name']}")
|
| 174 |
-
print(f"Description: {card['description']}\n")
|
| 175 |
-
|
| 176 |
-
# Formatting retrieved cards for input context
|
| 177 |
-
cards_info = "\n\n".join([
|
| 178 |
-
f"{card['name']}\n"
|
| 179 |
-
f"- Description: {card['description']}\n\n"
|
| 180 |
-
for card in retrieved_cards
|
| 181 |
-
])
|
| 182 |
-
|
| 183 |
-
# Gemini Prompt
|
| 184 |
-
gemini_prompt = f"""
|
| 185 |
-
You are a financial analyst specializing in credit cards and rewards optimization.
|
| 186 |
-
Your task is to analyze and select the best credit card based on user priorities.
|
| 187 |
-
|
| 188 |
-
### User's Query:
|
| 189 |
-
{indirect_query+user_query}
|
| 190 |
-
|
| 191 |
-
### Available Credit Cards:
|
| 192 |
-
{cards_info}
|
| 193 |
-
|
| 194 |
-
### Instructions:
|
| 195 |
-
1️ Analyze the user's need from the given query .
|
| 196 |
-
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.
|
| 197 |
-
3️ Explain why it's the best choice (list benefits concisely).
|
| 198 |
-
4 Do not assume and include benefits or features which is nor explicitly mentioned in the card description.
|
| 199 |
-
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.
|
| 200 |
-
6 Do not include extra symbols like * or #, just generate a textual response maybe with numbers for points.
|
| 201 |
-
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.
|
| 202 |
-
8 If user asks for fd based cards and the descriptions do not mention it just pick a best card from the given list.
|
| 203 |
-
|
| 204 |
-
### Expected Output Format:
|
| 205 |
-
Best Card: [Card Name]
|
| 206 |
-
Why It’s the Best:
|
| 207 |
-
1️ [Primary Benefit] : [Explanation]
|
| 208 |
-
2️ [Additional Perks] : [Explanation]
|
| 209 |
-
3️ [Final Justification] : [Why it's the best fit]
|
| 210 |
-
"""
|
| 211 |
-
|
| 212 |
-
# Generate response using Gemini
|
| 213 |
-
response = model3.generate_content(gemini_prompt)
|
| 214 |
-
|
| 215 |
-
return response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|