Sulaiman8 commited on
Commit
6b9a0cd
·
verified ·
1 Parent(s): 1a206a3

Update recommender/graph_retrieval.py

Browse files
Files changed (1) hide show
  1. recommender/graph_retrieval.py +283 -283
recommender/graph_retrieval.py CHANGED
@@ -1,284 +1,284 @@
1
- from data import get_model,debug_print,get_driver,eligibility_df
2
- from nodes.intent import CreditCardState
3
- from neo4j.exceptions import ServiceUnavailable, TransientError
4
- from langchain.tools import BaseTool
5
- from typing import List
6
- import time
7
- import re
8
-
9
-
10
- #Cypher query generation and graph retrieval
11
-
12
- class Neo4jConnectionError(Exception):
13
- pass
14
-
15
- class Neo4jRetrievalTool(BaseTool):
16
- name: str = "neo4j_card_retriever"
17
- description: str = "Runs Cypher and builds FAISS on filtered cards."
18
-
19
- def generate_cypher(self, user_query: str, query_intent: bool, include_cobranded: bool) -> str:
20
- debug_print("TOOL", f"generate_cypher called with query: '{user_query}'")
21
- debug_print("TOOL", f"Parameters: query_intent={query_intent}, include_cobranded={include_cobranded}")
22
-
23
- model = get_model("gemini-2.0-flash", use_chat=False)
24
-
25
- context_flags = f"""
26
- Contextual Flags:
27
- - FD Card intent: {query_intent}
28
- - Include co-branded cards: {include_cobranded}
29
- """
30
- prompt = """
31
- You are an expert Neo4j Cypher query generator.
32
-
33
- Given a user’s question, graph schema, and **contextual flags**, generate the correct Cypher query. The query should return only the card names`c.name as name`.
34
-
35
- ONLY output the Cypher query. Do NOT explain anything.
36
-
37
- ---
38
-
39
- Graph Schema:
40
- - Nodes:
41
- - (Card): Properties = name, bank_name, card_type, premium, co_branded
42
- - (Feature): Properties = name
43
- - (Partner): Properties = name
44
- - Relationships:
45
- - (Card)-[:HAS_FEATURE]->(Feature)
46
- - (Card)-[:PARTNER_WITH]->(Partner)
47
-
48
- Feature Inclusion Rules:
49
- - Include only relevant features mentioned or implied in the refined query. If multiple applicable features from the available list match the query, include all of them together in the f.name IN [...] clause.
50
- - For vague or broad terms such as “vacation”, “travel”, “frequent flyer”, or “general spending”, **analyze the available feature list below carefully** and include **all relevant travel or spending-related features** that accurately represent the intent. Do not limit to only 1-2 features include relevant features covering all aspects
51
- - Forex markup fee and foreign transaction fee are the same.
52
- - If FD Card intent is true then include the features if the query contains any and also include “General Cashback” or “General Reward Points”
53
- - Don’t add “General Cashback” or “General Reward Points” if it is not required.
54
- - If fuel is mentioned, include both `Fuel Benefits` and `Fuel Surcharge Waiver`.
55
- - **ALWAYS** match features using: `f.name IN [...]` — even if there is only **one** feature.
56
-
57
- Partner Inclusion Rules:
58
- - If the user query mentions any available partner brand names, include:
59
- MATCH (c)-[:PARTNER_WITH]->(p:Partner)
60
- AND p.name IN [<matched partner names>]
61
- - Always use `p.name IN [...]` — even if there is only one partner.
62
-
63
- Valid values:
64
- - card_type: 'FD Card' or 'Regular'
65
- - premium: true (no concept of false — just include it if applicable)
66
- - co_branded: true (no concept of false — just include it if applicable)
67
-
68
- MANDATORY Condition Rules:
69
- - If FD Card intent is true → include: `c.card_type = 'FD Card'`
70
- - Else → include: `c.card_type = 'Regular'`
71
- - If the query is based on beginners or students or people with no or low credit history then use FD Card.
72
- - If the query uses words like "premium", "elite", "luxury", "exclusive", "infinia", "black", etc. → include: `AND c.premium = true`
73
- - If the query includes low spending, without high spending or budget → include: `(c.premium IS NULL OR c.premium = false)`
74
- - If include co-branded is false → include: `AND (c.co_branded IS NULL OR c.co_branded = false)`
75
- - 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"]
76
-
77
- ---
78
-
79
- Available features:
80
- 'Fuel Surcharge Waiver','Insurance','Shopping Benefits','Airport Lounge Access','Co-Branded',
81
- 'Daily Spends (Grocery)','Dining Benefits','Domestic Travel Benefits','Entertainment',
82
- 'General Reward Points','Movie Benefits','Rupay Network Support','Student',
83
- 'UPI Transaction Support','Welcome Bonus','International Travel Benefits','premium',
84
- 'Flight Discounts','Hotel Benefits','Travel Benefits','Railway Benefits','Railway Lounge',
85
- 'Utility','Beginners (Entry Level)','E-commerce Platform Benefits','Air Miles',
86
- 'Jewellery Spends','Concierge Services','Food Delivery Benefits','Lifestyle & Luxury Perks',
87
- 'Spa Access Benefits','Golf Access & Perks','Super Premium','Frequent Flyer Benefits',
88
- 'Health Benefits','Rent Payment Benefits','Education','Lifetime Free','Roadside Assistance',
89
- 'EMI Conversion Options','No Forex Markup Fee','Secured FD Based','Cashback','Fuel Benefits','Business'
90
-
91
- Available partners:
92
- "Marriott Bonvoy", "Accor", "Taj", "ITC", "The Postcard", "Indigo", "United Airlines", "Emirates", "Etihad", "Club Vistara", "Air India", "Turkish airlines"
93
-
94
- ---
95
- Few-shot Examples:
96
-
97
- User Query: Show premium cards with airport lounge access
98
- Cypher:
99
- MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
100
- WHERE f.name IN ["Airport Lounge Access"]
101
- AND c.card_type = 'Regular'
102
- AND c.premium = true
103
- RETURN DISTINCT c.name AS name
104
-
105
- User Query: I want FD cards with spa access and golf perks
106
- Cypher:
107
- MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
108
- WHERE f.name IN ["Spa Access Benefits", "Golf Access & Perks"]
109
- AND c.card_type = 'FD Card'
110
- RETURN DISTINCT c.name AS name
111
-
112
- User Query: Cards that support UPI but are not co-branded
113
- Cypher:
114
- MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
115
- WHERE f.name IN ["UPI Transaction Support"]
116
- AND c.card_type = 'Regular'
117
- AND (c.co_branded IS NULL OR c.co_branded = false)
118
- RETURN DISTINCT c.name AS name
119
-
120
- User Query: Cards partnered with Indigo and Vistara that offer flight benefits
121
- Cypher:
122
- MATCH (c:Card)-[:PARTNER_WITH]->(p:Partner)
123
- MATCH (c)-[:HAS_FEATURE]->(f:Feature)
124
- WHERE f.name IN ["Flight Discounts"]
125
- AND p.name IN ["Indigo", "Vistara"]
126
- AND c.card_type = 'Regular'
127
- RETURN DISTINCT c.name AS name
128
-
129
- ---
130
-
131
- {context_flags}
132
-
133
- User Query: {user_query}
134
- Cypher:
135
- """
136
-
137
- cypher_prompt = prompt.format(context_flags=context_flags, user_query=user_query)
138
- debug_print("TOOL", f"Calling Gemini to generate Cypher query, prompt length: {len(cypher_prompt)}")
139
-
140
- cypher_code = model.generate_content(cypher_prompt).text.strip()
141
- cleaned_cypher = cypher_code.strip("`").replace("cypher", "").strip()
142
-
143
- debug_print("TOOL", f"Generated Cypher query: {cleaned_cypher}")
144
- return cleaned_cypher
145
-
146
- def _run(
147
- self,
148
- query_text: str,
149
- query_intent: bool = False,
150
- excluded_cards: List[str]=[],
151
- include_cobranded: bool = True,
152
- use_eligibility: bool = False,
153
- age: int = None,
154
- income: float = None,
155
- cibil: int = None,
156
- min_joining_fee: float = None,
157
- max_joining_fee: float = None,
158
- min_annual_fee: float = None,
159
- max_annual_fee: float = None
160
- ):
161
- debug_print("TOOL", f"neo4j_card_retriever _run called with query: '{query_text}'")
162
- debug_print("TOOL", f"Eligibility filter: {use_eligibility}")
163
-
164
- cypher = self.generate_cypher(query_text, query_intent, include_cobranded)
165
-
166
- debug_print("TOOL", f"Executing Cypher query against Neo4j")
167
- attempt = 0
168
- while attempt < 3:
169
- try:
170
- with get_driver().session() as session:
171
- matched = [rec["name"] for rec in session.run(cypher)]
172
-
173
- break # success: exit retry loop
174
-
175
- except (ServiceUnavailable, TransientError, Neo4jConnectionError, OSError,TimeoutError) as e:
176
- attempt += 1
177
- print(f"Neo4j connection error (attempt {attempt}/{3}): {e}")
178
- if attempt >= 3:
179
- raise Neo4jConnectionError("Failed to connect to the Neo4j database after retries.") from e
180
- time.sleep(2 * attempt)
181
-
182
-
183
- debug_print("TOOL", f"Neo4j returned {len(matched)} cards")
184
- print(excluded_cards)
185
- print("Before filtering out")
186
- print(matched)
187
-
188
- excluded_cards_clean = [normalize_card_name(c) for c in excluded_cards]
189
-
190
- matched = [card for card in matched if normalize_card_name(card) not in excluded_cards_clean]
191
-
192
- if use_eligibility:
193
- debug_print("TOOL", f"Applying eligibility filter with: age={age}, income={income}, cibil={cibil}")
194
- matched = eligibility_filter(
195
- matched,
196
- income,
197
- cibil,
198
- age,
199
- min_joining_fee,
200
- max_joining_fee,
201
- min_annual_fee,
202
- max_annual_fee
203
- )
204
- debug_print("TOOL", f"After eligibility filter: {len(matched)} cards remain")
205
-
206
- return matched
207
-
208
- def neo4j_retrieval_node(state: dict):
209
- debug_print("NODE", f"Entering neo4j_retrieval_node with query: '{state['query']}'")
210
- debug_print("NODE", f"Query intent: {state['query_intent']}, Include cobranded: {state['include_cobranded']}")
211
-
212
- try:
213
- tool = Neo4jRetrievalTool()
214
- cards = tool._run(
215
- query_text=state["query"],
216
- query_intent=state["query_intent"],
217
- include_cobranded=state["include_cobranded"],
218
- use_eligibility=state["use_eligibility"],
219
- excluded_cards=state.get("excluded_cards", []),
220
- age=state["age"],
221
- income=state["income"],
222
- cibil=state["cibil"],
223
- min_joining_fee=state["min_joining_fee"],
224
- max_joining_fee=state["max_joining_fee"],
225
- min_annual_fee=state["min_annual_fee"],
226
- max_annual_fee=state["max_annual_fee"]
227
- )
228
- state["cards"] = cards
229
- state["neo4j_error"] = False
230
- return state
231
-
232
- except Neo4jConnectionError as e:
233
- debug_print("ERROR", f"Neo4j connection failed: {e}")
234
- debug_print("NODE", "Setting neo4j_error to True due to exception")
235
- state["neo4j_error"] = True
236
- state["cards"] = []
237
- debug_print("NODE", f"Returning from neo4j_retrieval_node with state: {state}")
238
- return state
239
-
240
- except Exception as e:
241
- debug_print("ERROR", f"Unexpected exception in neo4j_retrieval_node: {type(e)} - {e}")
242
- state["neo4j_error"] = True
243
- state["cards"] = []
244
- return state
245
-
246
-
247
- def neo4j_error_handler_node(state: CreditCardState):
248
- message = "Sorry,the graph database is temporarily unavailable. Please try again in a few minutes."
249
- print("inside neo4j handler")
250
- state["top_card"] = ""
251
- state["top_card_description"] = [message]
252
- state["card_rows"] = []
253
- state["card_names"] = []
254
- state["card_lookup"] = {}
255
-
256
- return state
257
-
258
- # --- Eligibility Filter ---
259
- def eligibility_filter(cards, user_income, user_cibil, user_age,min_joining_fee, max_joining_fee,
260
- min_annual_fee, max_annual_fee):
261
- eligible_cards = []
262
- for card_name in cards:
263
- eligibility = eligibility_df[eligibility_df["Name"] == card_name]
264
- if not eligibility.empty:
265
- min_income = eligibility.iloc[0]["Minimum Income (LPA)"]
266
- min_cibil = eligibility.iloc[0]["Minimum Credit Score"]
267
- min_age = eligibility.iloc[0]["Minimum Age"]
268
- max_age = eligibility.iloc[0]["Maximum Age"]
269
- joining_fee=eligibility.iloc[0]["Joining fee"]
270
- annual_fee=eligibility.iloc[0]["Annual fee"]
271
- if (user_income >= min_income and
272
- user_cibil >= min_cibil and
273
- min_age <= user_age <= max_age and
274
- min_joining_fee<=joining_fee<=max_joining_fee and
275
- min_annual_fee<=annual_fee<=max_annual_fee):
276
- eligible_cards.append(card_name)
277
- return eligible_cards
278
-
279
- def normalize_card_name(name):
280
- name = name.lower()
281
- name = name.replace("+", "plus")
282
- name = re.sub(r"[^a-z0-9 ]", "", name)
283
- name = re.sub(r"\s+", " ", name).strip()
284
  return name
 
1
+ from data import get_model,debug_print,get_driver,eligibility_df
2
+ from nodes.intent import CreditCardState
3
+ from neo4j.exceptions import ServiceUnavailable, TransientError, SessionExpired
4
+ from langchain.tools import BaseTool
5
+ from typing import List
6
+ import time
7
+ import re
8
+
9
+
10
+ #Cypher query generation and graph retrieval
11
+
12
+ class Neo4jConnectionError(Exception):
13
+ pass
14
+
15
+ class Neo4jRetrievalTool(BaseTool):
16
+ name: str = "neo4j_card_retriever"
17
+ description: str = "Runs Cypher and builds FAISS on filtered cards."
18
+
19
+ def generate_cypher(self, user_query: str, query_intent: bool, include_cobranded: bool) -> str:
20
+ debug_print("TOOL", f"generate_cypher called with query: '{user_query}'")
21
+ debug_print("TOOL", f"Parameters: query_intent={query_intent}, include_cobranded={include_cobranded}")
22
+
23
+ model = get_model("gemini-2.0-flash", use_chat=False)
24
+
25
+ context_flags = f"""
26
+ Contextual Flags:
27
+ - FD Card intent: {query_intent}
28
+ - Include co-branded cards: {include_cobranded}
29
+ """
30
+ prompt = """
31
+ You are an expert Neo4j Cypher query generator.
32
+
33
+ Given a user’s question, graph schema, and **contextual flags**, generate the correct Cypher query. The query should return only the card names`c.name as name`.
34
+
35
+ ONLY output the Cypher query. Do NOT explain anything.
36
+
37
+ ---
38
+
39
+ Graph Schema:
40
+ - Nodes:
41
+ - (Card): Properties = name, bank_name, card_type, premium, co_branded
42
+ - (Feature): Properties = name
43
+ - (Partner): Properties = name
44
+ - Relationships:
45
+ - (Card)-[:HAS_FEATURE]->(Feature)
46
+ - (Card)-[:PARTNER_WITH]->(Partner)
47
+
48
+ Feature Inclusion Rules:
49
+ - Include only relevant features mentioned or implied in the refined query. If multiple applicable features from the available list match the query, include all of them together in the f.name IN [...] clause.
50
+ - For vague or broad terms such as “vacation”, “travel”, “frequent flyer”, or “general spending”, **analyze the available feature list below carefully** and include **all relevant travel or spending-related features** that accurately represent the intent. Do not limit to only 1-2 features include relevant features covering all aspects
51
+ - Forex markup fee and foreign transaction fee are the same.
52
+ - If FD Card intent is true then include the features if the query contains any and also include “General Cashback” or “General Reward Points”
53
+ - Don’t add “General Cashback” or “General Reward Points” if it is not required.
54
+ - If fuel is mentioned, include both `Fuel Benefits` and `Fuel Surcharge Waiver`.
55
+ - **ALWAYS** match features using: `f.name IN [...]` — even if there is only **one** feature.
56
+
57
+ Partner Inclusion Rules:
58
+ - If the user query mentions any available partner brand names, include:
59
+ MATCH (c)-[:PARTNER_WITH]->(p:Partner)
60
+ AND p.name IN [<matched partner names>]
61
+ - Always use `p.name IN [...]` — even if there is only one partner.
62
+
63
+ Valid values:
64
+ - card_type: 'FD Card' or 'Regular'
65
+ - premium: true (no concept of false — just include it if applicable)
66
+ - co_branded: true (no concept of false — just include it if applicable)
67
+
68
+ MANDATORY Condition Rules:
69
+ - If FD Card intent is true → include: `c.card_type = 'FD Card'`
70
+ - Else → include: `c.card_type = 'Regular'`
71
+ - If the query is based on beginners or students or people with no or low credit history then use FD Card.
72
+ - If the query uses words like "premium", "elite", "luxury", "exclusive", "infinia", "black", etc. → include: `AND c.premium = true`
73
+ - If the query includes low spending, without high spending or budget → include: `(c.premium IS NULL OR c.premium = false)`
74
+ - If include co-branded is false → include: `AND (c.co_branded IS NULL OR c.co_branded = false)`
75
+ - 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"]
76
+
77
+ ---
78
+
79
+ Available features:
80
+ 'Fuel Surcharge Waiver','Insurance','Shopping Benefits','Airport Lounge Access','Co-Branded',
81
+ 'Daily Spends (Grocery)','Dining Benefits','Domestic Travel Benefits','Entertainment',
82
+ 'General Reward Points','Movie Benefits','Rupay Network Support','Student',
83
+ 'UPI Transaction Support','Welcome Bonus','International Travel Benefits','premium',
84
+ 'Flight Discounts','Hotel Benefits','Travel Benefits','Railway Benefits','Railway Lounge',
85
+ 'Utility','Beginners (Entry Level)','E-commerce Platform Benefits','Air Miles',
86
+ 'Jewellery Spends','Concierge Services','Food Delivery Benefits','Lifestyle & Luxury Perks',
87
+ 'Spa Access Benefits','Golf Access & Perks','Super Premium','Frequent Flyer Benefits',
88
+ 'Health Benefits','Rent Payment Benefits','Education','Lifetime Free','Roadside Assistance',
89
+ 'EMI Conversion Options','No Forex Markup Fee','Secured FD Based','Cashback','Fuel Benefits','Business'
90
+
91
+ Available partners:
92
+ "Marriott Bonvoy", "Accor", "Taj", "ITC", "The Postcard", "Indigo", "United Airlines", "Emirates", "Etihad", "Club Vistara", "Air India", "Turkish airlines"
93
+
94
+ ---
95
+ Few-shot Examples:
96
+
97
+ User Query: Show premium cards with airport lounge access
98
+ Cypher:
99
+ MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
100
+ WHERE f.name IN ["Airport Lounge Access"]
101
+ AND c.card_type = 'Regular'
102
+ AND c.premium = true
103
+ RETURN DISTINCT c.name AS name
104
+
105
+ User Query: I want FD cards with spa access and golf perks
106
+ Cypher:
107
+ MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
108
+ WHERE f.name IN ["Spa Access Benefits", "Golf Access & Perks"]
109
+ AND c.card_type = 'FD Card'
110
+ RETURN DISTINCT c.name AS name
111
+
112
+ User Query: Cards that support UPI but are not co-branded
113
+ Cypher:
114
+ MATCH (c:Card)-[:HAS_FEATURE]->(f:Feature)
115
+ WHERE f.name IN ["UPI Transaction Support"]
116
+ AND c.card_type = 'Regular'
117
+ AND (c.co_branded IS NULL OR c.co_branded = false)
118
+ RETURN DISTINCT c.name AS name
119
+
120
+ User Query: Cards partnered with Indigo and Vistara that offer flight benefits
121
+ Cypher:
122
+ MATCH (c:Card)-[:PARTNER_WITH]->(p:Partner)
123
+ MATCH (c)-[:HAS_FEATURE]->(f:Feature)
124
+ WHERE f.name IN ["Flight Discounts"]
125
+ AND p.name IN ["Indigo", "Vistara"]
126
+ AND c.card_type = 'Regular'
127
+ RETURN DISTINCT c.name AS name
128
+
129
+ ---
130
+
131
+ {context_flags}
132
+
133
+ User Query: {user_query}
134
+ Cypher:
135
+ """
136
+
137
+ cypher_prompt = prompt.format(context_flags=context_flags, user_query=user_query)
138
+ debug_print("TOOL", f"Calling Gemini to generate Cypher query, prompt length: {len(cypher_prompt)}")
139
+
140
+ cypher_code = model.generate_content(cypher_prompt).text.strip()
141
+ cleaned_cypher = cypher_code.strip("`").replace("cypher", "").strip()
142
+
143
+ debug_print("TOOL", f"Generated Cypher query: {cleaned_cypher}")
144
+ return cleaned_cypher
145
+
146
+ def _run(
147
+ self,
148
+ query_text: str,
149
+ query_intent: bool = False,
150
+ excluded_cards: List[str]=[],
151
+ include_cobranded: bool = True,
152
+ use_eligibility: bool = False,
153
+ age: int = None,
154
+ income: float = None,
155
+ cibil: int = None,
156
+ min_joining_fee: float = None,
157
+ max_joining_fee: float = None,
158
+ min_annual_fee: float = None,
159
+ max_annual_fee: float = None
160
+ ):
161
+ debug_print("TOOL", f"neo4j_card_retriever _run called with query: '{query_text}'")
162
+ debug_print("TOOL", f"Eligibility filter: {use_eligibility}")
163
+
164
+ cypher = self.generate_cypher(query_text, query_intent, include_cobranded)
165
+
166
+ debug_print("TOOL", f"Executing Cypher query against Neo4j")
167
+ attempt = 0
168
+ while attempt < 3:
169
+ try:
170
+ with get_driver().session() as session:
171
+ matched = [rec["name"] for rec in session.run(cypher)]
172
+
173
+ break # success: exit retry loop
174
+
175
+ except (ServiceUnavailable, TransientError, Neo4jConnectionError,SessionExpired, OSError,TimeoutError) as e:
176
+ attempt += 1
177
+ print(f"Neo4j connection error (attempt {attempt}/{3}): {e}")
178
+ if attempt >= 3:
179
+ raise Neo4jConnectionError("Failed to connect to the Neo4j database after retries.") from e
180
+ time.sleep(2 * attempt)
181
+
182
+
183
+ debug_print("TOOL", f"Neo4j returned {len(matched)} cards")
184
+ print(excluded_cards)
185
+ print("Before filtering out")
186
+ print(matched)
187
+
188
+ excluded_cards_clean = [normalize_card_name(c) for c in excluded_cards]
189
+
190
+ matched = [card for card in matched if normalize_card_name(card) not in excluded_cards_clean]
191
+
192
+ if use_eligibility:
193
+ debug_print("TOOL", f"Applying eligibility filter with: age={age}, income={income}, cibil={cibil}")
194
+ matched = eligibility_filter(
195
+ matched,
196
+ income,
197
+ cibil,
198
+ age,
199
+ min_joining_fee,
200
+ max_joining_fee,
201
+ min_annual_fee,
202
+ max_annual_fee
203
+ )
204
+ debug_print("TOOL", f"After eligibility filter: {len(matched)} cards remain")
205
+
206
+ return matched
207
+
208
+ def neo4j_retrieval_node(state: dict):
209
+ debug_print("NODE", f"Entering neo4j_retrieval_node with query: '{state['query']}'")
210
+ debug_print("NODE", f"Query intent: {state['query_intent']}, Include cobranded: {state['include_cobranded']}")
211
+
212
+ try:
213
+ tool = Neo4jRetrievalTool()
214
+ cards = tool._run(
215
+ query_text=state["query"],
216
+ query_intent=state["query_intent"],
217
+ include_cobranded=state["include_cobranded"],
218
+ use_eligibility=state["use_eligibility"],
219
+ excluded_cards=state.get("excluded_cards", []),
220
+ age=state["age"],
221
+ income=state["income"],
222
+ cibil=state["cibil"],
223
+ min_joining_fee=state["min_joining_fee"],
224
+ max_joining_fee=state["max_joining_fee"],
225
+ min_annual_fee=state["min_annual_fee"],
226
+ max_annual_fee=state["max_annual_fee"]
227
+ )
228
+ state["cards"] = cards
229
+ state["neo4j_error"] = False
230
+ return state
231
+
232
+ except Neo4jConnectionError as e:
233
+ debug_print("ERROR", f"Neo4j connection failed: {e}")
234
+ debug_print("NODE", "Setting neo4j_error to True due to exception")
235
+ state["neo4j_error"] = True
236
+ state["cards"] = []
237
+ debug_print("NODE", f"Returning from neo4j_retrieval_node with state: {state}")
238
+ return state
239
+
240
+ except Exception as e:
241
+ debug_print("ERROR", f"Unexpected exception in neo4j_retrieval_node: {type(e)} - {e}")
242
+ state["neo4j_error"] = True
243
+ state["cards"] = []
244
+ return state
245
+
246
+
247
+ def neo4j_error_handler_node(state: CreditCardState):
248
+ message = "Sorry,the graph database is temporarily unavailable. Please try again in a few minutes."
249
+ print("inside neo4j handler")
250
+ state["top_card"] = ""
251
+ state["top_card_description"] = [message]
252
+ state["card_rows"] = []
253
+ state["card_names"] = []
254
+ state["card_lookup"] = {}
255
+
256
+ return state
257
+
258
+ # --- Eligibility Filter ---
259
+ def eligibility_filter(cards, user_income, user_cibil, user_age,min_joining_fee, max_joining_fee,
260
+ min_annual_fee, max_annual_fee):
261
+ eligible_cards = []
262
+ for card_name in cards:
263
+ eligibility = eligibility_df[eligibility_df["Name"] == card_name]
264
+ if not eligibility.empty:
265
+ min_income = eligibility.iloc[0]["Minimum Income (LPA)"]
266
+ min_cibil = eligibility.iloc[0]["Minimum Credit Score"]
267
+ min_age = eligibility.iloc[0]["Minimum Age"]
268
+ max_age = eligibility.iloc[0]["Maximum Age"]
269
+ joining_fee=eligibility.iloc[0]["Joining fee"]
270
+ annual_fee=eligibility.iloc[0]["Annual fee"]
271
+ if (user_income >= min_income and
272
+ user_cibil >= min_cibil and
273
+ min_age <= user_age <= max_age and
274
+ min_joining_fee<=joining_fee<=max_joining_fee and
275
+ min_annual_fee<=annual_fee<=max_annual_fee):
276
+ eligible_cards.append(card_name)
277
+ return eligible_cards
278
+
279
+ def normalize_card_name(name):
280
+ name = name.lower()
281
+ name = name.replace("+", "plus")
282
+ name = re.sub(r"[^a-z0-9 ]", "", name)
283
+ name = re.sub(r"\s+", " ", name).strip()
284
  return name