from langchain_core.messages import SystemMessage, HumanMessage from typing import List, Dict, Any import time import requests import gradio as gr from data import debug_print,all_card_names,all_card_lookup,eligibility_lookup,df from nodes.intent import get_pretty_state_string from langgraph_pipeline import run_langgraph_pipeline,utility_app #Gradio UI with the fucntion calls to invoke the graphs and pass the user inputs custom_css=""" #compare_output_markdown, #full_compare_output_markdown, #top_card_markdown { min-height: 100px; border: 1px solid #e0e0e0; padding: 10px; overflow-y: auto; } #left_column_box, #right_column_box { padding: 16px !important; /* Adds 16px of space INSIDE the bordered box */ } """ with gr.Blocks(title="Agentic Credit Card Recommender", css=custom_css, theme=gr.themes.Soft()) as demo: gr.Markdown(""" # Credit Card Recommender Discover and receive personalized recommendations for credit cards based on your needs and preferences """) with gr.Tab("Get Recommendations"): with gr.Row(): query_input = gr.Textbox( label="What kind of card are you looking for?", placeholder="Best cards for online shopping with fuel benefits", elem_id="query-textbox", scale=2 ) preferences = gr.Dropdown( choices=["Cashback", "Travel", "Fuel", "Airport Lounge access", "Railways", "Dining", "Online Spends", "Grocery"], multiselect=True, label="Select Preferred Card Categories", scale=1 ) with gr.Accordion("Eligibility & Fee Filters", open=False): with gr.Row(): income = gr.Slider(minimum=1, maximum=60, step=1, label="Annual Income (LPA)") cibil = gr.Slider(minimum=300, maximum=900, step=10, label="CIBIL Score") age = gr.Slider(minimum=18, maximum=75, step=1, label="Age") with gr.Row(): with gr.Group(): gr.Markdown("

Preferred Joining Fee (₹)

") with gr.Row(): min_joining_fee = gr.Number(label="Min", value=0) max_joining_fee = gr.Number(label="Max", value=150000) with gr.Group(): gr.Markdown("

Preferred Annual Fee (₹)

") with gr.Row(): min_annual_fee = gr.Number(label="Min", value=0) max_annual_fee = gr.Number(label="Max", value=150000) with gr.Row(): use_eligibility = gr.Checkbox(label="Apply Eligibility Filter", value=False) fd_checkbox = gr.Checkbox(label="Beginner / Student", value=False) cobrand_checkbox = gr.Checkbox(label="Include Co-branded Cards", value=True) run_button = gr.Button("Recommend Cards", variant='primary') top_card_recommendation = gr.Markdown(value="", elem_id="top_card_markdown") with gr.Row(visible=True) as results_container: with gr.Column(): with gr.Group(elem_id="left_column_box"): recommendation_heading = gr.Markdown("### Top-Ranked Cards") card_table_markdown = gr.Markdown() with gr.Column(): with gr.Group(elem_id="right_column_box"): card_links_heading = gr.Markdown("### 🔗 Issuer Links") card_links_html = gr.HTML() with gr.Column(visible=False) as chat_container: with gr.Accordion("💬 Ask Follow-up Questions", open=True): chatbox = gr.Chatbot(type="messages", label="Chat") followup_input = gr.Textbox(label="Enter Your question", placeholder="Compare the lounge access benefits of card X and card Y") followup_submit = gr.Button("Submit", variant="primary") with gr.Tab("Compare Cards"): with gr.Column(visible=False) as compare_recommended_cards_container: gr.Markdown("## Compare Recommended Cards") compare_checkboxes = gr.CheckboxGroup(choices=[], label="Select 2 or more cards to compare", info="Pick from the recommended list to see a comparison.") compare_btn = gr.Button("Compare Selected Cards", variant='primary') compare_output = gr.Markdown(value="", elem_id="compare_output_markdown") gr.Markdown("---") gr.Markdown("## Compare Any Cards") full_compare_dropdown = gr.Dropdown(choices=all_card_names, multiselect=True, label="Select 2 or more cards", info="Compare any cards from the entire database.") full_compare_btn = gr.Button("Compare Selected Cards", variant='primary') full_compare_output = gr.Markdown(value="", elem_id="full_compare_output_markdown") def format_to_markdown(top_card_out, top_card_description_out): debug_print("UI", f"Formatting top card output to markdown") if not top_card_out and top_card_description_out: message_block = "\n".join(f"- {desc}" for desc in top_card_description_out if desc) return f"### Note\n\n{message_block}\n\n" top_card_recommendation = f"### Best card: {top_card_out}\n\n" if top_card_description_out: for desc in top_card_description_out: if isinstance(desc, str): desc = desc.strip() if desc: top_card_recommendation += f"- {desc}\n" return top_card_recommendation def format_rows_to_markdown_table(card_rows): """Converts a list of card data into a markdown table string.""" if not card_rows: return "" markdown_table = "| Card Name | Joining Fee | Annual Fee |\n" markdown_table += "|---|---|---|\n" for row in card_rows: name = row[0] joining_fee = row[1] annual_fee = row[2] markdown_table += f"| {name} | {joining_fee} | {annual_fee} |\n" return markdown_table #main function that invokes the graph async def recommend(query, preferences, fd_intent, include_cobranded, income, cibil, age, min_joining_fee, max_joining_fee, min_annual_fee, max_annual_fee, use_eligibility): debug_print("UI", f"recommend called with query: '{query}'") debug_print("UI", f"Preferences: {preferences}") debug_print("UI", f"FD intent: {fd_intent}, Include cobranded: {include_cobranded}") global chat_history chat_history = [] global messages messages = [] preferences_text="" if preferences: preferences_text = "User selected preferences: " + ", ".join(preferences) + "." query = query.strip() if query else "" if not query: error_message = "Please enter a valid query." debug_print("UI", f"recommend function error: {error_message}") return error_message, gr.update(visible=True), gr.update(value=None), gr.update(visible=False), gr.update(visible=False), gr.update(value=None), gr.update(visible=False), [], {}, "", gr.update(visible=False),{} try: debug_print("UI", f"Calling run_langgraph_pipeline") top_card_out, top_card_description_out, card_rows_out, card_names_out, card_lookup_out,card_links = await run_langgraph_pipeline( query, preferences_text, query_intent=fd_intent, include_cobranded=include_cobranded, use_eligibility=use_eligibility, income=income, cibil=cibil, age=age, min_joining_fee=min_joining_fee, max_joining_fee=max_joining_fee, min_annual_fee=min_annual_fee, max_annual_fee=max_annual_fee ) debug_print("UI", f"Pipeline returned {len(card_rows_out)} card rows") recommendation_visible = bool(top_card_out) or bool(top_card_description_out) df_visible = bool(card_rows_out) chat_container_visible = gr.update(visible=True if card_rows_out else False) top_card_md = format_to_markdown(top_card_out, top_card_description_out) card_table_md = format_rows_to_markdown_table(card_rows_out) debug_print("UI", f"recommend function completed successfully") initial_context = { "query": query, "top_cards": card_names_out[:5], "recommendation_summary": top_card_md } card_links_section = "
" for name, link in zip(card_names_out, card_links): card_links_section += f"
{name}
" card_links_section += "
" return top_card_md, gr.update(visible=recommendation_visible), card_table_md, gr.update(visible=df_visible), gr.update(visible=df_visible), card_names_out, card_lookup_out, query, chat_container_visible, initial_context,gr.update(value=card_links_section, visible=True), gr.update(value="### 🔗 Issuer Links", visible=True) except Exception as e: error_message = "### Service Unavailable\n The AI model server could not be reached or an error occurred. Please try again later." debug_print("ERROR", f"recommend function error: {str(e)}") return ( gr.update(value=error_message, visible=True), gr.update(visible=True), "", gr.update(visible=False), gr.update(visible=False), [], {}, "", gr.update(visible=False), {}, gr.update(value="", visible=False), gr.update(visible=False) ) #for comparison async def compare_cards_via_graph(selected_cards, card_lookup): state = { "trigger_compare": True, "trigger_chat": False, "selected_cards": selected_cards, "card_lookup": card_lookup } result = await utility_app.ainvoke(state) return result.get("comparison_result", "") async def compare_cards_wrapper(selected_cards): return await compare_cards_via_graph(selected_cards, all_card_lookup) #for chat async def chat_with_agent(user_message: str, chat_history: List, messages: List, initial_context: Dict[str, Any]): debug_print("UI", f"Entering chat_with_agent with user_message: '{user_message}'") # Preparing the initial context message if this is the first question if not messages: user_query = initial_context.get("query", "No query provided") top_cards = initial_context.get("top_cards", []) context = "\n".join(top_cards) if top_cards else "No top cards available" recommended_summary = initial_context.get("recommendation_summary", "No recommendations available") recommended_card_info = "" other_card_info = "" for card_name in top_cards: description = all_card_lookup.get(card_name, "Description not found.") eligibility = eligibility_lookup.get(card_name, "No eligibility or fee information available.") card_block = f"""Card: {card_name} Description: {description} Eligibility & Fees: {eligibility} --- """ if card_name in recommended_summary: recommended_card_info += card_block else: other_card_info += card_block # System prompt with context for chat node system_message = f""" **Your Role: Secure Credit Card Expert** You are a helpful and secure AI assistant. Your goal is to provide a comprehensive answer to the user's question using all available information. Your knowledge base includes the following and section if it is provided: {user_query} {context} {recommended_card_info} {other_card_info} **CRITICAL RULES:** 1. **Synthesize All Information:** Base your answer on the `` and any `` provided. If the user asks for a comparison, use details from both. 2. **Make a Recommendation:** If asked "which is best?" or "which is suitable?", analyze all available card info against the `` and provide a direct, reasoned recommendation. 3. **Be Direct:** Do not mention your internal processes. Just provide the final answer to the user. **RESPONSE STYLE AND TONE:** - **Be Direct and Concise:** Get straight to the point. Do not explain your internal thought process. - **For Factual Questions (like "what are the fees?"):** Provide a direct, simple sentence as the answer. - **For Recommendation Questions (like "which is best for me?"):** Start your response with the name of the recommended card, followed by a brief, bulleted list of the key features that make it the best fit for the user's requirements. """ # print(f"system message: {system_message}") messages = [SystemMessage(content=system_message)] else: debug_print("UI", "Subsequent turn: using existing message history.") top_cards = initial_context.get("top_cards", []) user_query = initial_context.get("query", "No query provided") current_turn_messages = messages + [HumanMessage(content=user_message)] # Defining the state for the utility app state = { "messages": current_turn_messages, "trigger_chat": True, "trigger_compare": False, "card_names": top_cards, } # invoking the graph response_state = await utility_app.ainvoke(state) updated_messages_from_agent = response_state.get("messages", []) debug_print("UI", f"Chat agent response state: {get_pretty_state_string(updated_messages_from_agent)}") final_response = updated_messages_from_agent[-1].content chat_history.append({"role": "user", "content": user_message}) chat_history.append({"role": "assistant", "content": final_response}) return chat_history, updated_messages_from_agent, "" card_names_state = gr.State([]) card_lookup_state = gr.State() chat_history = gr.State([]) query = gr.State("") initial_chat_context = gr.State({}) messages= gr.State([]) run_button.click( fn=recommend, inputs=[ query_input, preferences, fd_checkbox, cobrand_checkbox, income, cibil, age, min_joining_fee, max_joining_fee, min_annual_fee, max_annual_fee, use_eligibility ], outputs=[ top_card_recommendation, top_card_recommendation, card_table_markdown, card_table_markdown, recommendation_heading, card_names_state, card_lookup_state, query, chat_container, initial_chat_context,card_links_html, card_links_heading ], concurrency_limit=20 ).then( fn=lambda card_names: (gr.update(choices=card_names if card_names else [], value=[]), gr.update(visible=True if card_names else False)), inputs=card_names_state, outputs=[compare_checkboxes, compare_recommended_cards_container], concurrency_limit=20 ).then( fn=lambda: ([], [], [], gr.update(value=""), gr.update(value="")), outputs=[chat_history, chatbox, messages, compare_output, full_compare_output], # Clear both compare outputs concurrency_limit=20 ) compare_btn.click( fn=compare_cards_via_graph, inputs=[compare_checkboxes, card_lookup_state], outputs=compare_output, show_progress=True, concurrency_limit=20 ) full_compare_btn.click( fn=compare_cards_wrapper, inputs=[full_compare_dropdown], outputs=full_compare_output, show_progress=True, concurrency_limit=20 ) followup_submit.click( fn=chat_with_agent, inputs=[followup_input, chatbox, messages, initial_chat_context], outputs=[chatbox, messages, followup_input], concurrency_limit=20 ) debug_print("APP", f"Credit Card Recommender Agent initialized with {len(df)} cards") debug_print("APP", f"Launching Gradio UI at {time.strftime('%Y-%m-%d %H:%M:%S')}")