DeltaVenom commited on
Commit
cff1a2a
·
0 Parent(s):

add project

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. .gitignore +10 -0
  3. agents/graph.py +171 -0
  4. agents/nodes.py +884 -0
  5. agents/states.py +51 -0
  6. api/main.py +52 -0
  7. app.py +266 -0
  8. configs/doc_structure.json +82 -0
  9. configs/plan_metadata.json +103 -0
  10. docs/Aviva/Savings Plan/Aviva Nivesh Bima Brochure.PDF +3 -0
  11. docs/Aviva/Savings Plan/Aviva Nivesh Bima CIS.docx +3 -0
  12. docs/Aviva/Savings Plan/Aviva PoS Dhan Suraksha Brochure.pdf +3 -0
  13. docs/Aviva/Savings Plan/Aviva PoS Dhan Suraksha CIS.docx +3 -0
  14. docs/Aviva/Term Plan/Aviva Protection Plus Brochure.pdf +3 -0
  15. docs/Aviva/Term Plan/Aviva Protection Plus CIS.doc +0 -0
  16. docs/Aviva/ULIP Plan/Aviva Fortune Plus Brochure.pdf +3 -0
  17. docs/Aviva/ULIP Plan/Aviva Fortune Plus CIS.doc +0 -0
  18. docs/Bharti Axa/Retirement Plan/Bharti Axa Swabhimaan Retirement Plan Brochure.pdf +3 -0
  19. docs/Bharti Axa/Saving Plan/Bharti Axa Guaranteed Bachat Plan Brochure.pdf +3 -0
  20. docs/Bharti Axa/Saving Plan/Bharti Axa Guaranteed Wealth Pro Brochure.pdf +3 -0
  21. docs/Bharti Axa/Saving Plan/Bharti Axa Secure Insta Income Plan Brochure.pdf +3 -0
  22. docs/Bharti Axa/Saving Plan/Bharti Axa Unnati Plan Brochure.pdf +3 -0
  23. docs/Bharti Axa/Ulip Plan/Bhart Axa Growth Shield Plus Brochure.pdf +3 -0
  24. docs/Bharti Axa/Ulip Plan/Bharti Axa Life Dream Shield Plus Brochure.pdf +3 -0
  25. docs/Bharti Axa/Ulip Plan/Bharti Axa Unnati Plan Brochure.pdf +3 -0
  26. docs/Canara HSBC/Retirement Plan/Canara HSBC EZ Pension Brochure.pdf +3 -0
  27. docs/Canara HSBC/Retirement Plan/Canara HSBC Pension 4 Life Brochure.pdf +3 -0
  28. docs/Canara HSBC/Retirement Plan/Canara HSBC iSelect Guaranteed Future Plus Brochure.pdf +3 -0
  29. docs/Canara HSBC/Term Plan/Canara HSBC Promise 2 Protect Term Plan Brochure.pdf +3 -0
  30. docs/Canara HSBC/Term Plan/Canara HSBC Young Term Plan Brochure.pdf +3 -0
  31. docs/Canara HSBC/Term Plan/Canara HSBC iSelect Smart 360 Term Plan Brochure.pdf +3 -0
  32. docs/Canara HSBC/ULIP Plan/Canara HSBC Promise 4 Growth Plus Brochure.pdf +3 -0
  33. docs/Canara HSBC/ULIP Plan/Canara HSBC Secure Invest Brochure.pdf +3 -0
  34. docs/Canara HSBC/ULIP Plan/Canara HSBC Wealth Edge Brochure.pdf +3 -0
  35. docs/Edelweiss Life/Group Solutions/EdelweissLife Group Employee Benefit Plus Brochure.pdf +3 -0
  36. docs/Edelweiss Life/Group Solutions/EdelweissLife Group Life Protection Brochure.pdf +3 -0
  37. docs/Edelweiss Life/Group Solutions/EdelweissLife Group Life Protection CIS.pdf +3 -0
  38. docs/Edelweiss Life/Group Solutions/EdelweissLife Group Total Secure Brochure.pdf +3 -0
  39. docs/Edelweiss Life/Group Solutions/EdelweissLife Pradhan Mantri Jeevan Jyoti Bima Yojana Brochure.pdf +3 -0
  40. docs/Edelweiss Life/Group Solutions/EdelweissLife Pradhan Mantri Jeevan Jyoti Bima Yojana CIS.pdf +3 -0
  41. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Bharat Savings STAR Brochure.pdf +3 -0
  42. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Bharat Savings STAR CIS.pdf +3 -0
  43. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Dream Plan Brochure.pdf +3 -0
  44. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Dream Plan CIS.pdf +3 -0
  45. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Savings Plan Brochure.pdf +3 -0
  46. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Savings Plan CIS .pdf +3 -0
  47. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Flexi STAR Brochure.pdf +3 -0
  48. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Flexi STAR CIS.pdf +3 -0
  49. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Income STAR Brochure.pdf +3 -0
  50. docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Income STAR CIS.pdf +3 -0
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.pdf filter=lfs diff=lfs merge=lfs -text
2
+ *.docx filter=lfs diff=lfs merge=lfs -text
3
+ *.faiss filter=lfs diff=lfs merge=lfs -text
4
+ *.pkl filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ .streamlit/
6
+ temp_docs/
7
+ temp_faiss_index/
8
+ debug_*.txt
9
+ test_*.txt
10
+ *.log
agents/graph.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LangGraph Workflow for Insurance RAG System.
3
+ Implements deterministic, compliance-focused retrieval with specialized nodes.
4
+ """
5
+
6
+ from langgraph.graph import StateGraph, END
7
+ from agents.states import AgentState
8
+ from agents.nodes import nodes
9
+
10
+
11
+ def build_rag_workflow() -> StateGraph:
12
+ """
13
+ Builds the LangGraph workflow with the following flow:
14
+
15
+ query_rewriter → query_classifier → entity_extractor → retrieval_router
16
+
17
+ [conditional routing by intent]
18
+
19
+ ┌─────────────────────────────────────────────────────┐
20
+ │ list_plans: listing_agent → guardrail │
21
+ │ plan_details: retriever → aggregator → retrieval → guardrail │
22
+ │ compare_plans: retriever → aggregator → comparison → guardrail │
23
+ │ recommendation: retriever → aggregator → advisory → guardrail │
24
+ │ general_query: retriever → aggregator → faq → guardrail │
25
+ └─────────────────────────────────────────────────────┘
26
+ """
27
+
28
+ workflow = StateGraph(AgentState)
29
+
30
+ # =========================================================================
31
+ # Add all nodes
32
+ # =========================================================================
33
+
34
+ # Pre-processing nodes
35
+ workflow.add_node("query_rewriter", nodes.query_rewriter_node)
36
+ workflow.add_node("query_classifier", nodes.query_classifier_node)
37
+ workflow.add_node("entity_extractor", nodes.entity_extractor_node)
38
+ workflow.add_node("retrieval_router", nodes.retrieval_router_node)
39
+
40
+ # Retrieval nodes
41
+ workflow.add_node("retriever", nodes.retriever_node)
42
+ workflow.add_node("plan_aggregator", nodes.plan_aggregator_node)
43
+
44
+ # Agent nodes
45
+ workflow.add_node("listing_agent", nodes.listing_agent)
46
+ workflow.add_node("retrieval_agent", nodes.retrieval_agent)
47
+ workflow.add_node("comparison_agent", nodes.comparison_agent)
48
+ workflow.add_node("advisory_agent", nodes.advisory_agent)
49
+ workflow.add_node("faq_agent", nodes.faq_agent)
50
+
51
+ # Post-processing
52
+ workflow.add_node("guardrail", nodes.guardrail_node)
53
+
54
+ # =========================================================================
55
+ # Define edges
56
+ # =========================================================================
57
+
58
+ # Entry point
59
+ workflow.set_entry_point("query_rewriter")
60
+
61
+ # Linear pre-processing chain
62
+ workflow.add_edge("query_rewriter", "query_classifier")
63
+ workflow.add_edge("query_classifier", "entity_extractor")
64
+ workflow.add_edge("entity_extractor", "retrieval_router")
65
+
66
+ # Conditional routing based on intent
67
+ def route_by_intent(state: AgentState) -> str:
68
+ """Route to appropriate handler based on classified intent."""
69
+ intent = state.get("intent", "plan_details")
70
+
71
+ if intent == "list_plans":
72
+ # Listing doesn't need retrieval, goes direct to listing agent
73
+ return "listing_agent"
74
+ else:
75
+ # All other intents go through retrieval first
76
+ return "retriever"
77
+
78
+ workflow.add_conditional_edges(
79
+ "retrieval_router",
80
+ route_by_intent,
81
+ {
82
+ "listing_agent": "listing_agent",
83
+ "retriever": "retriever"
84
+ }
85
+ )
86
+
87
+ # Listing agent goes directly to guardrail
88
+ workflow.add_edge("listing_agent", "guardrail")
89
+
90
+ # Retriever always goes to aggregator
91
+ workflow.add_edge("retriever", "plan_aggregator")
92
+
93
+ # Aggregator routes to appropriate agent based on intent
94
+ def route_to_agent(state: AgentState) -> str:
95
+ """Route from aggregator to the appropriate agent."""
96
+ intent = state.get("intent", "plan_details")
97
+
98
+ route_map = {
99
+ "plan_details": "retrieval_agent",
100
+ "compare_plans": "comparison_agent",
101
+ "recommendation": "advisory_agent",
102
+ "general_query": "faq_agent"
103
+ }
104
+
105
+ return route_map.get(intent, "retrieval_agent")
106
+
107
+ workflow.add_conditional_edges(
108
+ "plan_aggregator",
109
+ route_to_agent,
110
+ {
111
+ "retrieval_agent": "retrieval_agent",
112
+ "comparison_agent": "comparison_agent",
113
+ "advisory_agent": "advisory_agent",
114
+ "faq_agent": "faq_agent"
115
+ }
116
+ )
117
+
118
+ # All agents end at guardrail
119
+ workflow.add_edge("retrieval_agent", "guardrail")
120
+ workflow.add_edge("comparison_agent", "guardrail")
121
+ workflow.add_edge("advisory_agent", "guardrail")
122
+ workflow.add_edge("faq_agent", "guardrail")
123
+
124
+ # Guardrail ends the workflow
125
+ workflow.add_edge("guardrail", END)
126
+
127
+ return workflow
128
+
129
+
130
+ # Build and compile the workflow
131
+ workflow = build_rag_workflow()
132
+ app = workflow.compile()
133
+
134
+
135
+ if __name__ == "__main__":
136
+ # Test the graph
137
+ print("Graph compiled successfully!")
138
+
139
+ # Test cases
140
+ test_queries = [
141
+ "List all term plans from Tata AIA",
142
+ "Explain the TATA AIA Smart Value Income plan",
143
+ "Compare Tata AIA vs Edelweiss term plans",
144
+ "Suggest a plan for 30-year-old non-smoker with 1Cr cover"
145
+ ]
146
+
147
+ for query in test_queries:
148
+ print(f"\n{'='*60}")
149
+ print(f"Query: {query}")
150
+ print('='*60)
151
+
152
+ initial_state = {
153
+ "input": query,
154
+ "chat_history": [],
155
+ "intent": "",
156
+ "extracted_entities": {},
157
+ "metadata_filters": {},
158
+ "retrieval_strategy": "",
159
+ "context": [],
160
+ "retrieved_chunks": {},
161
+ "reasoning_output": "",
162
+ "answer": "",
163
+ "next_step": ""
164
+ }
165
+
166
+ try:
167
+ result = app.invoke(initial_state)
168
+ print(f"Intent: {result.get('intent')}")
169
+ print(f"Answer: {result.get('answer', '')[:500]}...")
170
+ except Exception as e:
171
+ print(f"Error: {e}")
agents/nodes.py ADDED
@@ -0,0 +1,884 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import Dict, List, Any, Optional
3
+ from collections import defaultdict
4
+ from agents.states import AgentState, ExtractedEntities
5
+ from rag.retriever import RAGRetriever
6
+ from langchain_core.messages import HumanMessage, SystemMessage
7
+ from models.llm import LLMFactory
8
+
9
+
10
+ # Compliance disclaimer to append to all answers
11
+ COMPLIANCE_DISCLAIMER = (
12
+ "\n\n---\n"
13
+ )
14
+
15
+ # Prompting rules for all agents
16
+ COMPLIANCE_RULES = """
17
+ CRITICAL RULES:
18
+ - ❌ NO invented plan names - only use plans from the provided context
19
+ - ❌ NO assumptions beyond documents - if info is missing, say so explicitly
20
+ - ❌ NO meta-commentary. DO NOT mention "the provided context", "the documents", "the text", or "internal state".
21
+ - ✅ CIS overrides brochure for: exclusions, charges, conditions
22
+ - ✅ Use structured output (markdown tables) for comparisons
23
+ - ✅ Simple, clear language for end users
24
+ - ✅ Provide "OUTPUT ONLY" - start answering the user's question directly.
25
+ """
26
+
27
+
28
+ class AgentNodes:
29
+ """
30
+ Enhanced LangGraph nodes implementing the full RAG specification.
31
+ """
32
+
33
+ def __init__(self):
34
+ self.retriever = None
35
+
36
+ def _get_retriever(self) -> Optional[RAGRetriever]:
37
+ """Lazy initialization of retriever."""
38
+ if not self.retriever:
39
+ try:
40
+ self.retriever = RAGRetriever()
41
+ except Exception:
42
+ return None
43
+ return self.retriever
44
+
45
+ def reload_retriever(self):
46
+ """Triggers a reload of the retriever's index."""
47
+ retriever = self._get_retriever()
48
+ if retriever:
49
+ retriever.reload()
50
+
51
+ # =========================================================================
52
+ # NODE 1: Query Rewriter
53
+ # =========================================================================
54
+ def query_rewriter_node(self, state: AgentState) -> Dict[str, Any]:
55
+ """
56
+ Rewrites query to be self-contained based on chat history.
57
+ Resolves pronouns and references.
58
+ """
59
+ llm = LLMFactory.get_llm("small")
60
+ query = state["input"]
61
+ history = state.get("chat_history", [])
62
+
63
+ if not history:
64
+ return {"input": query}
65
+
66
+ system_prompt = (
67
+ "You are a query rewriter for an insurance RAG system. "
68
+ "Your task is to rewrite the latest question to be self-contained.\n\n"
69
+ "RULES:\n"
70
+ "1. ALWAYS resolve pronouns (it, they, these) or vague terms (the plan, previous one) using the previous context.\n"
71
+ "2. If the user asks a follow-up about 'it' or 'the plan', replace it with the specific plan name mentioned last.\n"
72
+ "3. If the user asks 'is it good for me' or similar, rewrite it to '[Plan Name] recommendation for [user details if any]'.\n"
73
+ "4. If the query is already very specific and names a plan, keep it mostly as-is but ensure insurer names are present.\n"
74
+ "5. Do NOT cross-pollinate unrelated queries. If the user switches topics completely, ignore the history.\n"
75
+ "6. NEVER return a conversational response, suggestion, or question. If you cannot resolve a reference, return the original 'Latest' query as is.\n"
76
+ "7. Return ONLY the rewritten query text."
77
+ )
78
+
79
+ history_str = "\n".join([f"- {h}" for h in history[-5:]]) # Last 5 turns
80
+ prompt = f"History:\n{history_str}\n\nLatest: {query}"
81
+
82
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=prompt)])
83
+ rewritten = getattr(response, 'content', str(response)).strip()
84
+
85
+ return {"input": rewritten}
86
+
87
+ # =========================================================================
88
+ # NODE 2: Query Classifier
89
+ # =========================================================================
90
+ def query_classifier_node(self, state: AgentState) -> Dict[str, Any]:
91
+ """
92
+ Classifies user intent into:
93
+ - list_plans: User wants to see available plans
94
+ - plan_details: User asks about a specific plan
95
+ - compare_plans: User wants to compare multiple plans
96
+ - recommendation: User seeks personalized advice
97
+ - general_query: General insurance questions
98
+ """
99
+ llm = LLMFactory.get_llm("small")
100
+ query = state["input"].lower()
101
+
102
+ # Fast keyword-based classification first
103
+ if any(kw in query for kw in ["list", "which plans", "what plans", "all plans", "available plans", "show me plans"]):
104
+ return {"intent": "list_plans"}
105
+
106
+ if any(kw in query for kw in ["compare", "vs", "versus", "difference between", "which is better"]):
107
+ return {"intent": "compare_plans"}
108
+
109
+ if any(kw in query for kw in ["suggest", "recommend", "best for", "should i", "suitable for"]):
110
+ return {"intent": "recommendation"}
111
+
112
+ # LLM-based classification for ambiguous cases
113
+ system_prompt = (
114
+ "Classify the user's insurance query into ONE of:\n"
115
+ "- 'plan_details': Asking about features, benefits, eligibility of a SPECIFIC plan\n"
116
+ "- 'list_plans': Wants to know WHICH plans are available\n"
117
+ "- 'compare_plans': Wants to COMPARE 2+ plans side-by-side\n"
118
+ "- 'recommendation': Seeks personalized advice based on their profile\n"
119
+ "- 'general_query': General insurance terminology or concepts\n\n"
120
+ "Return ONLY the category name."
121
+ )
122
+
123
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=query)])
124
+ intent = getattr(response, 'content', str(response)).lower().strip()
125
+
126
+ valid_intents = ['list_plans', 'plan_details', 'compare_plans', 'recommendation', 'general_query']
127
+ if intent not in valid_intents:
128
+ intent = "plan_details" # Default fallback
129
+
130
+ return {"intent": intent}
131
+
132
+ # =========================================================================
133
+ # NODE 3: Entity Extractor
134
+ # =========================================================================
135
+ def entity_extractor_node(self, state: AgentState) -> Dict[str, Any]:
136
+ """
137
+ Extracts structured entities from the query:
138
+ - provider (insurer names)
139
+ - insurance_type (term, ulip, savings, etc.)
140
+ - plan_names (specific plan names mentioned)
141
+ - user_profile (age, income, smoker, dependents, goal)
142
+ """
143
+ query = state["input"].lower()
144
+
145
+ # Extract providers
146
+ provider_map = {
147
+ "edelweiss": "Edelweiss Life",
148
+ "tata": "TATA AIA",
149
+ "tata aia": "TATA AIA",
150
+ "generali": "Generali Central",
151
+ "central": "Generali Central",
152
+ "pramerica": "PRAMERICA"
153
+ }
154
+ providers = []
155
+ for keyword, name in provider_map.items():
156
+ if keyword in query and name not in providers:
157
+ providers.append(name)
158
+
159
+ # Extract insurance types
160
+ type_map = {
161
+ "term": ["Term Insurance", "Term Plan"],
162
+ "ulip": ["Unit Linked Insurance Plan", "ULIP Plan"],
163
+ "wealth": ["Unit Linked Insurance Plan"],
164
+ "savings": ["Savings Plan", "Guaranteed Return"],
165
+ "retirement": ["Retirement and Pension"],
166
+ "pension": ["Retirement and Pension"],
167
+ "health": ["Health Insurance"],
168
+ "group": ["Group Plan"]
169
+ }
170
+ insurance_types = []
171
+ for keyword, types in type_map.items():
172
+ if keyword in query:
173
+ for t in types:
174
+ if t not in insurance_types:
175
+ insurance_types.append(t)
176
+
177
+ # Extract specific plan names using LLM
178
+ plan_names = self._extract_plan_names_from_query(state["input"])
179
+
180
+ # Extract user profile for recommendation intent
181
+ user_profile = {}
182
+ if state.get("intent") == "recommendation":
183
+ user_profile = self._extract_user_profile(state["input"])
184
+
185
+ entities: ExtractedEntities = {
186
+ "provider": list(set(providers)) if providers else [],
187
+ "insurance_type": list(set(insurance_types)) if insurance_types else [],
188
+ "plan_names": list(set(plan_names)) if plan_names else [],
189
+ "user_profile": user_profile or {}
190
+ }
191
+
192
+ # Build metadata filters from entities
193
+ filters = {}
194
+ if providers:
195
+ filters["insurer"] = providers
196
+ if insurance_types:
197
+ filters["insurance_type"] = insurance_types
198
+
199
+ return {
200
+ "extracted_entities": entities,
201
+ "metadata_filters": filters
202
+ }
203
+
204
+ def _extract_plan_names_from_query(self, query: str) -> List[str]:
205
+ """Use LLM to extract specific plan names mentioned in query."""
206
+ llm = LLMFactory.get_llm("small")
207
+
208
+ system_prompt = (
209
+ "Extract EXACT insurance plan names from the query.\n"
210
+ "If the user is asking to compare, extract BOTH plan names.\n"
211
+ "RULES:\n"
212
+ "- Return one plan name per line\n"
213
+ "- Include insurer prefix if mentioned (e.g., 'TATA AIA Smart Value Income', 'Edelweiss Saral Jeevan Bima')\n"
214
+ "- Return EMPTY if no specific plan names found\n"
215
+ "- Do NOT invent plan names"
216
+ )
217
+
218
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=query)])
219
+ result = getattr(response, 'content', str(response)).strip()
220
+
221
+ # Validation: If LLM returns a sentence instead of names, skip it
222
+ if "mentioned in" in result.lower() or "referring to" in result.lower() or len(result) > 200:
223
+ return []
224
+
225
+ if not result or result.lower() in ['none', 'empty', 'n/a']:
226
+ return []
227
+
228
+ # Parse response
229
+ plan_names = []
230
+ for line in result.split('\n'):
231
+ line = re.sub(r'^[\d\.\-\*\u2022]\s*', '', line).strip().strip('"\'')
232
+ if len(line) > 5:
233
+ plan_names.append(line)
234
+
235
+ return plan_names
236
+
237
+ def _extract_user_profile(self, query: str) -> Dict[str, Any]:
238
+ """Extract user profile information for recommendations."""
239
+ llm = LLMFactory.get_llm("small")
240
+
241
+ system_prompt = (
242
+ "Extract user profile from the insurance query.\n"
243
+ "Return in format:\n"
244
+ "age: <number or null>\n"
245
+ "smoker: <yes/no or null>\n"
246
+ "cover_amount: <amount or null>\n"
247
+ "goal: <protection/savings/retirement/wealth or null>\n"
248
+ "dependents: <number or null>"
249
+ )
250
+
251
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=query)])
252
+ result = getattr(response, 'content', str(response))
253
+
254
+ profile = {}
255
+ for line in result.split('\n'):
256
+ if ':' in line:
257
+ key, value = line.split(':', 1)
258
+ key = key.strip().lower()
259
+ value = value.strip().lower()
260
+ if value not in ['null', 'none', 'n/a', '']:
261
+ if key == 'age':
262
+ try:
263
+ profile['age'] = int(re.search(r'\d+', value).group())
264
+ except:
265
+ pass
266
+ elif key == 'smoker':
267
+ profile['smoker'] = 'yes' in value
268
+ elif key == 'cover_amount':
269
+ profile['cover_amount'] = value
270
+ elif key == 'goal':
271
+ profile['goal'] = value
272
+ elif key == 'dependents':
273
+ try:
274
+ profile['dependents'] = int(re.search(r'\d+', value).group())
275
+ except:
276
+ pass
277
+
278
+ return profile
279
+
280
+ # =========================================================================
281
+ # NODE 4: Retrieval Router
282
+ # =========================================================================
283
+ def retrieval_router_node(self, state: AgentState) -> Dict[str, Any]:
284
+ """
285
+ Determines retrieval strategy based on intent.
286
+ """
287
+ intent = state.get("intent", "plan_details")
288
+
289
+ strategy_map = {
290
+ "list_plans": "metadata_only",
291
+ "plan_details": "plan_level",
292
+ "compare_plans": "cross_plan",
293
+ "recommendation": "section_specific",
294
+ "general_query": "plan_level"
295
+ }
296
+
297
+ return {"retrieval_strategy": strategy_map.get(intent, "plan_level")}
298
+
299
+ # =========================================================================
300
+ # NODE 5: Retriever
301
+ # =========================================================================
302
+ def retriever_node(self, state: AgentState) -> Dict[str, Any]:
303
+ """
304
+ Retrieves documents with:
305
+ - Metadata filtering
306
+ - CIS boosting for exclusions/charges/conditions
307
+ - Deduplication by similarity
308
+ """
309
+ retriever = self._get_retriever()
310
+ if not retriever:
311
+ return {"context": [], "retrieved_chunks": {}}
312
+
313
+ query = state["input"]
314
+ filters = state.get("metadata_filters", {})
315
+ entities = state.get("extracted_entities", {})
316
+ strategy = state.get("retrieval_strategy", "plan_level")
317
+
318
+ # If specific plan names were extracted, use them for precise retrieval
319
+ plan_names = entities.get("plan_names") or []
320
+ matched_plans = []
321
+ if plan_names:
322
+ # Resolve to actual plan names in index
323
+ all_plans = self._list_plans_from_index()
324
+ for name in plan_names:
325
+ match = self._find_closest_plan_name(name, all_plans)
326
+ if match:
327
+ matched_plans.append(match)
328
+
329
+ # Update filters for non-comparison queries (for comparison, _retrieve_for_comparison handles it)
330
+ if matched_plans and strategy != "cross_plan":
331
+ filters = filters.copy()
332
+ filters["product_name"] = matched_plans[0] if len(matched_plans) == 1 else matched_plans
333
+
334
+ boost_cis = any(kw in query.lower() for kw in
335
+ ["exclusion", "excluded", "not covered", "charges", "fee", "condition", "waiting"])
336
+
337
+ # Retrieve documents
338
+ if strategy == "cross_plan":
339
+ # For comparisons, retrieve for each plan separately
340
+ # Pass matched_plans if we have them, otherwise it will try to find them from filters
341
+ chunks_by_plan = self._retrieve_for_comparison(query, filters, entities, matched_plans=matched_plans)
342
+ else:
343
+ docs = retriever.search(query, filters=filters, k=8)
344
+ chunks_by_plan = self._group_by_plan_id(docs)
345
+
346
+ # Boost CIS documents if needed
347
+ if boost_cis:
348
+ chunks_by_plan = self._boost_cis_chunks(chunks_by_plan)
349
+
350
+ # Format context strings
351
+ limit_per_plan = 5 if strategy == "cross_plan" else 3
352
+ context = self._format_context(chunks_by_plan, limit=limit_per_plan)
353
+
354
+ return {
355
+ "context": context,
356
+ "retrieved_chunks": chunks_by_plan
357
+ }
358
+
359
+ def _format_context(self, chunks_by_plan: Dict[str, List[Dict]], limit: int = 3) -> List[str]:
360
+ """Helper to format chunks into LLM-readable context strings."""
361
+ context = []
362
+ for plan_id, chunks in chunks_by_plan.items():
363
+ for chunk in chunks[:limit]:
364
+ content = chunk.get("content", "")
365
+ plan_name = chunk.get("product_name", "Unknown")
366
+ doc_type = chunk.get("document_type", "brochure")
367
+ section = chunk.get("section", "General")
368
+ context.append(f"[{plan_name} - {doc_type.upper()} - {section}] {content}")
369
+ return context
370
+
371
+ def _retrieve_for_comparison(self, query: str, filters: Dict, entities: Dict, matched_plans: List[str] = None) -> Dict[str, List]:
372
+ """Retrieve chunks for each plan separately in comparison mode."""
373
+ retriever = self._get_retriever()
374
+ if not retriever:
375
+ return {}
376
+
377
+ if not matched_plans:
378
+ plan_names = entities.get("plan_names") or []
379
+ all_index_plans = self._list_plans_from_index()
380
+ matched_plans = []
381
+ for name in plan_names:
382
+ match = self._find_closest_plan_name(name, all_index_plans)
383
+ if match:
384
+ matched_plans.append(match)
385
+
386
+ if not matched_plans:
387
+ # Plan A: Deterministic "List & Match" Discovery
388
+ # For each provider, list all their plans and see if any match the query
389
+ providers = entities.get("provider") or []
390
+
391
+ if not providers:
392
+ search_providers = [None]
393
+ else:
394
+ search_providers = providers
395
+
396
+ discovered_names = []
397
+ all_plans_in_index = self._list_plans_from_index()
398
+
399
+ for prov in search_providers:
400
+ prov_filter = {"insurer": prov} if prov else {}
401
+ prov_plans = self._list_plans_from_index(filters=prov_filter)
402
+ self._log_debug(f"Provider: {prov}, Plans found: {len(prov_plans)}")
403
+
404
+ # Try to find which plan from this insurer is mentioned in the query
405
+ match = self._find_closest_plan_name(query, prov_plans)
406
+ self._log_debug(f"Match for {prov}: {match} (In list: {match in prov_plans})")
407
+
408
+ if match and match in prov_plans and match not in discovered_names:
409
+ discovered_names.append(match)
410
+
411
+ matched_plans = discovered_names
412
+
413
+ if not matched_plans:
414
+ # Plan B: Fall back to broad similarity-based discovery as a last resort
415
+ discovery_docs = retriever.search(query, k=20)
416
+ for d in discovery_docs:
417
+ p_name = d.metadata.get("product_name")
418
+ if p_name and p_name not in matched_plans:
419
+ matched_plans.append(p_name)
420
+ matched_plans = matched_plans[:3]
421
+
422
+ if not matched_plans:
423
+ # Plan B: Fall back to listing plans matching filters (metadata-only)
424
+ matched_plans = self._list_plans_from_index(filters)[:5]
425
+
426
+ chunks_by_plan = defaultdict(list)
427
+ for matched in matched_plans:
428
+
429
+ # Use a focused query for each plan instead of the broad comparison query
430
+ # This helps the retriever find relevant feature chunks for the specific plan
431
+ focused_query = f"features, benefits, eligibility and exclusions of {matched}"
432
+
433
+ # Use a fresh, strictly focused filter for each plan
434
+ # IMPORTANT: Search by insurer and manually filter by product_name
435
+ # This is more robust than passing a combined filter to the vector store
436
+ matched_insurer = None
437
+ if hasattr(self, "_cached_plans") and self._cached_plans:
438
+ for p_meta in self._cached_plans:
439
+ if p_meta["product_name"] == matched:
440
+ matched_insurer = p_meta.get("insurer")
441
+ break
442
+
443
+ search_filters = {"insurer": matched_insurer} if matched_insurer else {}
444
+
445
+ # Search only by insurer and then manually filter by product_name
446
+ # This is more robust than passing a combined filter to the vector store
447
+ docs = retriever.search(focused_query, filters=search_filters, k=50)
448
+
449
+ plan_chunks = []
450
+ for doc in docs:
451
+ doc_product = doc.metadata.get("product_name", "")
452
+ # Use fuzzy match for manual filter consistency
453
+ if self._find_closest_plan_name(doc_product, [matched]) == matched:
454
+ plan_chunks.append(doc)
455
+
456
+ for doc in plan_chunks[:10]:
457
+ plan_id = doc.metadata.get("plan_id", matched)
458
+ chunks_by_plan[plan_id].append({
459
+ "content": doc.page_content,
460
+ "product_name": doc.metadata.get("product_name"),
461
+ "document_type": doc.metadata.get("document_type", "brochure"),
462
+ "section": doc.metadata.get("section", "General")
463
+ })
464
+
465
+ return dict(chunks_by_plan)
466
+
467
+ def _group_by_plan_id(self, docs: List) -> Dict[str, List]:
468
+ """Group retrieved documents by plan_id."""
469
+ grouped = defaultdict(list)
470
+ for doc in docs:
471
+ plan_id = doc.metadata.get("plan_id", doc.metadata.get("product_name", "unknown"))
472
+ grouped[plan_id].append({
473
+ "content": doc.page_content,
474
+ "product_name": doc.metadata.get("product_name"),
475
+ "document_type": doc.metadata.get("document_type", "brochure"),
476
+ "section": doc.metadata.get("section", "General")
477
+ })
478
+ return dict(grouped)
479
+
480
+ def _boost_cis_chunks(self, chunks_by_plan: Dict[str, List]) -> Dict[str, List]:
481
+ """Boost CIS documents to appear first for each plan."""
482
+ boosted = {}
483
+ for plan_id, chunks in chunks_by_plan.items():
484
+ cis_chunks = [c for c in chunks if c.get("document_type") == "cis"]
485
+ brochure_chunks = [c for c in chunks if c.get("document_type") != "cis"]
486
+ boosted[plan_id] = cis_chunks + brochure_chunks
487
+ return boosted
488
+
489
+ # =========================================================================
490
+ # NODE 6: Plan Aggregator
491
+ # =========================================================================
492
+ def plan_aggregator_node(self, state: AgentState) -> Dict[str, Any]:
493
+ """
494
+ Aggregates chunks by plan_id, merging brochure and CIS context.
495
+ CIS overrides brochure for exclusions, charges, conditions.
496
+ """
497
+ chunks_by_plan = state.get("retrieved_chunks", {})
498
+
499
+ # Already grouped, just ensure proper ordering
500
+ aggregated = {}
501
+ for plan_id, chunks in chunks_by_plan.items():
502
+ # Separate by document type
503
+ cis_chunks = [c for c in chunks if c.get("document_type") == "cis"]
504
+ brochure_chunks = [c for c in chunks if c.get("document_type") != "cis"]
505
+
506
+ # For exclusions/charges sections, prefer CIS
507
+ override_sections = ["Exclusions", "Charges", "Waiting Period", "Conditions"]
508
+
509
+ final_chunks = []
510
+ covered_sections = set()
511
+
512
+ # Add CIS chunks first for override sections
513
+ for chunk in cis_chunks:
514
+ section = chunk.get("section", "General")
515
+ if section in override_sections:
516
+ final_chunks.append(chunk)
517
+ covered_sections.add(section)
518
+
519
+ # Add brochure chunks, skipping overridden sections
520
+ for chunk in brochure_chunks:
521
+ section = chunk.get("section", "General")
522
+ if section not in covered_sections:
523
+ final_chunks.append(chunk)
524
+
525
+ # Add remaining CIS chunks
526
+ for chunk in cis_chunks:
527
+ if chunk not in final_chunks:
528
+ final_chunks.append(chunk)
529
+
530
+ aggregated[plan_id] = final_chunks
531
+
532
+ # Refresh context strings based on aggregated chunks
533
+ intent = state.get("intent", "plan_details")
534
+ limit = 5 if intent == "compare_plans" else 3
535
+ context = self._format_context(aggregated, limit=limit)
536
+
537
+ return {
538
+ "retrieved_chunks": aggregated,
539
+ "context": context
540
+ }
541
+
542
+ # =========================================================================
543
+ # NODE 7: Listing Agent
544
+ # =========================================================================
545
+ def listing_agent(self, state: AgentState) -> Dict[str, Any]:
546
+ """
547
+ Lists available plans based on filters.
548
+ Uses direct index access for accuracy.
549
+ """
550
+ llm = LLMFactory.get_llm("small")
551
+ query = state["input"]
552
+ filters = state.get("metadata_filters", {})
553
+
554
+ plans = self._list_plans_from_index(filters)
555
+ plans = sorted(list(set(plans)))
556
+
557
+ if not plans:
558
+ filter_desc = ", ".join([str(v) for v in filters.values()]) if filters else "your criteria"
559
+ answer = f"I couldn't find any plans matching {filter_desc}. Please try a different search."
560
+ return {"context": [], "answer": answer}
561
+
562
+ plans_str = "\n".join([f"- {p}" for p in plans])
563
+
564
+ # Describe the filters
565
+ filter_parts = []
566
+ if filters.get("insurer"):
567
+ insurer_list = filters["insurer"] if isinstance(filters["insurer"], list) else [filters["insurer"]]
568
+ filter_parts.append(f"from {', '.join(insurer_list)}")
569
+ if filters.get("insurance_type"):
570
+ type_list = filters["insurance_type"] if isinstance(filters["insurance_type"], list) else [filters["insurance_type"]]
571
+ filter_parts.append(f"in {', '.join(type_list)} category")
572
+
573
+ filter_desc = " ".join(filter_parts) if filter_parts else ""
574
+
575
+ system_prompt = (
576
+ "Present the following insurance plans in a clear, friendly manner.\n"
577
+ "RULES:\n"
578
+ "- ONLY include plans from the list below\n"
579
+ "- Group by insurer if multiple insurers present\n"
580
+ "- Use bullet points for clarity\n"
581
+ "- Do NOT mention technical details about data retrieval"
582
+ )
583
+
584
+ prompt = f"User asked: {query}\n\nAvailable plans {filter_desc}:\n{plans_str}"
585
+
586
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=prompt)])
587
+ answer = getattr(response, 'content', str(response))
588
+
589
+ return {"context": [f"Plans: {plans}"], "answer": answer}
590
+
591
+ # =========================================================================
592
+ # NODE 8: Plan Details Agent (Retrieval Agent)
593
+ # =========================================================================
594
+ def retrieval_agent(self, state: AgentState) -> Dict[str, Any]:
595
+ """
596
+ Provides detailed information about a specific plan.
597
+ Grounds all responses in retrieved documents.
598
+ """
599
+ llm = LLMFactory.get_llm("medium")
600
+ query = state["input"]
601
+ context = state.get("context", [])
602
+
603
+ if not context:
604
+ # Fallback retrieval
605
+ retriever = self._get_retriever()
606
+ if retriever:
607
+ docs = retriever.search(query, k=5)
608
+ context = [f"[{d.metadata.get('product_name')}] {d.page_content}" for d in docs]
609
+
610
+ context_str = "\n\n".join(context)
611
+
612
+ system_prompt = f"""You are an Insurance Policy Specialist providing accurate information.
613
+
614
+ {COMPLIANCE_RULES}
615
+
616
+ Answer the user's question using ONLY the Policy Context provided to you.
617
+ If information is not in the context, say "I don't have that specific information in our documents."
618
+ DO NOT mention that you are looking at documents or context. Just provide the answer.
619
+ Be warm and helpful while maintaining accuracy."""
620
+
621
+ prompt = f"Policy Context:\n{context_str}\n\nUser Question: {query}"
622
+
623
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=prompt)])
624
+ answer = getattr(response, 'content', str(response))
625
+
626
+ return {"answer": answer}
627
+
628
+ # =========================================================================
629
+ # NODE 9: Comparison Agent
630
+ # =========================================================================
631
+ def comparison_agent(self, state: AgentState) -> Dict[str, Any]:
632
+ """
633
+ Generates structured side-by-side comparisons.
634
+ Normalizes attributes across plans.
635
+ """
636
+ llm = LLMFactory.get_llm("medium")
637
+ query = state["input"]
638
+ context = state.get("context", [])
639
+ chunks_by_plan = state.get("retrieved_chunks", {})
640
+
641
+ # Get plan names being compared
642
+ plan_names = list(chunks_by_plan.keys()) if chunks_by_plan else []
643
+
644
+ if not context and not plan_names:
645
+ return {"answer": "I couldn't find the plans you want to compare. Please specify the plan names."}
646
+
647
+ context_str = "\n\n".join(context)
648
+ plans_info = f"\n\nPlans to compare: {', '.join(plan_names)}" if plan_names else ""
649
+
650
+ system_prompt = f"""You are an Insurance Comparison Expert.
651
+
652
+ {COMPLIANCE_RULES}
653
+
654
+ COMPARISON FORMAT:
655
+ - Return comparison as a Markdown TABLE
656
+ - Columns: Features | Plan 1 | Plan 2 | ...
657
+ - Rows: Plan Type, Eligibility, Sum Assured, Premium Terms, Key Benefits, Exclusions
658
+ - If a detail is missing, put "Not specified"
659
+ - Include ALL plans mentioned in the context
660
+ - Be objective and factual"""
661
+
662
+ prompt = f"Policy Context:\n{context_str}{plans_info}\n\nUser Question: {query}"
663
+
664
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=prompt)])
665
+ answer = getattr(response, 'content', str(response))
666
+
667
+ return {"answer": answer, "reasoning_output": f"Compared {len(plan_names)} plans"}
668
+
669
+ # =========================================================================
670
+ # NODE 10: Recommendation Agent (Advisory)
671
+ # =========================================================================
672
+ def advisory_agent(self, state: AgentState) -> Dict[str, Any]:
673
+ """
674
+ Provides personalized recommendations based on user profile.
675
+ Grounds all advice in retrieved documents.
676
+ """
677
+ llm = LLMFactory.get_llm("large")
678
+ query = state["input"]
679
+ context = state.get("context", [])
680
+ entities = state.get("extracted_entities", {})
681
+ user_profile = entities.get("user_profile", {})
682
+
683
+ context_str = "\n\n".join(context) if context else "No specific plans found matching your criteria."
684
+
685
+ profile_info = ""
686
+ if user_profile:
687
+ profile_parts = []
688
+ if user_profile.get("age"):
689
+ profile_parts.append(f"Age: {user_profile['age']}")
690
+ if user_profile.get("smoker") is not None:
691
+ profile_parts.append(f"Smoker: {'Yes' if user_profile['smoker'] else 'No'}")
692
+ if user_profile.get("cover_amount"):
693
+ profile_parts.append(f"Cover needed: {user_profile['cover_amount']}")
694
+ if user_profile.get("goal"):
695
+ profile_parts.append(f"Goal: {user_profile['goal']}")
696
+ if profile_parts:
697
+ profile_info = f"\n\nUser Profile: {', '.join(profile_parts)}"
698
+
699
+ system_prompt = f"""You are an Expert Insurance Advisor.
700
+
701
+ {COMPLIANCE_RULES}
702
+
703
+ RECOMMENDATION RULES:
704
+ - Base recommendations ONLY on plans in the context
705
+ - Consider user's age, smoking status, cover requirement if provided
706
+ - Explain WHY a plan suits them based on document features
707
+ - List 2-3 suitable options if available
708
+ - Be clear about eligibility criteria
709
+ - DO NOT reference the "context" or "documents" in your answer. Provide the advice directly."""
710
+
711
+ prompt = f"Policy Context:\n{context_str}{profile_info}\n\nUser Question: {query}"
712
+
713
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=prompt)])
714
+ answer = getattr(response, 'content', str(response))
715
+
716
+ return {"answer": answer}
717
+
718
+ # =========================================================================
719
+ # NODE 11: General Query Agent (FAQ)
720
+ # =========================================================================
721
+ def faq_agent(self, state: AgentState) -> Dict[str, Any]:
722
+ """
723
+ Handles general insurance questions.
724
+ Still attempts to ground in documents when possible.
725
+ """
726
+ llm = LLMFactory.get_llm("small")
727
+ query = state["input"]
728
+ context = state.get("context", [])
729
+
730
+ context_str = "\n\n".join(context) if context else ""
731
+
732
+ system_prompt = f"""You are an Insurance Helpdesk Assistant.
733
+
734
+ {COMPLIANCE_RULES}
735
+
736
+ For general insurance terminology questions:
737
+ - Provide accurate, helpful explanations
738
+ - If context is available, use it to give specific examples
739
+ - Keep explanations simple and jargon-free"""
740
+
741
+ prompt = f"Context (if relevant):\n{context_str}\n\nUser Question: {query}" if context_str else f"User Question: {query}"
742
+
743
+ response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=prompt)])
744
+ answer = getattr(response, 'content', str(response))
745
+
746
+ return {"answer": answer}
747
+
748
+ # =========================================================================
749
+ # NODE 12: Guardrail
750
+ # =========================================================================
751
+ def guardrail_node(self, state: AgentState) -> Dict[str, Any]:
752
+ """
753
+ Final validation and compliance disclaimer.
754
+ - Validates answer is grounded
755
+ - Adds compliance disclaimer
756
+ - Blocks hallucinated content
757
+ """
758
+ answer = state.get("answer", "")
759
+
760
+ if not answer:
761
+ answer = "I apologize, but I couldn't generate a response. Please try rephrasing your question."
762
+
763
+ # Add compliance disclaimer
764
+ if COMPLIANCE_DISCLAIMER not in answer:
765
+ answer = answer + COMPLIANCE_DISCLAIMER
766
+
767
+ return {"answer": answer}
768
+
769
+ # =========================================================================
770
+ # HELPER METHODS
771
+ # =========================================================================
772
+ def _list_plans_from_index(self, filters: Dict = None) -> List[str]:
773
+ """Returns unique product names matching filters. Optimized with caching."""
774
+ retriever = self._get_retriever()
775
+ if not retriever:
776
+ return []
777
+
778
+ try:
779
+ # Use a simple cache attribute on the instance if it doesn't exist
780
+ if not hasattr(self, "_cached_plans") or self._cached_plans is None:
781
+ store = retriever.vector_store
782
+ plans_metadata = []
783
+ for doc in store.docstore._dict.values():
784
+ p_name = doc.metadata.get('product_name')
785
+ insurer = doc.metadata.get('insurer')
786
+ i_type = doc.metadata.get('insurance_type')
787
+ if p_name:
788
+ plans_metadata.append({
789
+ "product_name": p_name,
790
+ "insurer": insurer,
791
+ "insurance_type": i_type
792
+ })
793
+ self._cached_plans = plans_metadata
794
+
795
+ # Filter from cache
796
+ plans = set()
797
+ for meta in self._cached_plans:
798
+ if filters:
799
+ match = True
800
+ for k, v in filters.items():
801
+ doc_val = str(meta.get(k, "")).lower().strip()
802
+ if not doc_val:
803
+ match = False
804
+ break
805
+
806
+ # Standardize filter values to list of lowercase strings
807
+ filter_values = v if isinstance(v, list) else [v]
808
+ filter_values = [str(fv).lower().strip() for fv in filter_values]
809
+
810
+ # Robust match: any filter item matches or is matched by doc_val
811
+ val_match = False
812
+ for fv in filter_values:
813
+ if k == "product_name":
814
+ if fv in doc_val or doc_val in fv:
815
+ val_match = True
816
+ break
817
+ elif k == "insurer": # Strictly match insurer names
818
+ if fv == doc_val:
819
+ val_match = True
820
+ break
821
+ else: # For other keys like insurance_type, allow exact match
822
+ if fv == doc_val:
823
+ val_match = True
824
+ break
825
+
826
+ if not val_match:
827
+ match = False
828
+ break
829
+
830
+ if not match:
831
+ continue
832
+ plans.add(meta["product_name"])
833
+
834
+ return sorted(list(plans))
835
+ except Exception:
836
+ return []
837
+
838
+ def _find_closest_plan_name(self, query_plan: str, all_plans: List[str]) -> Optional[str]:
839
+ """Finds closest matching plan name using fuzzy matching."""
840
+ if not all_plans:
841
+ return query_plan
842
+
843
+ def normalize(s):
844
+ return s.lower().replace(" ", "").replace("-", "").replace("_", "").replace("edelweisslife", "edelweiss")
845
+
846
+ query_norm = normalize(query_plan)
847
+
848
+ # 1. Exact match (case insensitive)
849
+ for plan in all_plans:
850
+ if plan.lower() == query_plan.lower():
851
+ return plan
852
+
853
+ # 2. Normalized containment match (High Confidence)
854
+ # Check if the plan name is mentioned in the query
855
+ for plan in all_plans:
856
+ plan_norm = normalize(plan)
857
+ if plan_norm in query_norm or query_norm in plan_norm:
858
+ return plan
859
+
860
+ # 3. Word overlap (Lower Confidence fallback)
861
+ query_words = set(query_plan.lower().split())
862
+ stop_words = {"tata", "aia", "edelweiss", "life", "generali", "central", "plan", "insurance", "the", "a", "of", "with", "compare"}
863
+ query_significant = query_words - stop_words
864
+
865
+ best_match = None
866
+ max_overlap = 0
867
+
868
+ for plan in all_plans:
869
+ plan_words = set(plan.lower().split())
870
+ plan_significant = plan_words - stop_words
871
+
872
+ # Count significant word overlap
873
+ overlap = len(query_significant.intersection(plan_significant))
874
+
875
+ if overlap > max_overlap:
876
+ max_overlap = overlap
877
+ best_match = plan
878
+
879
+ # Return best match if we found significant overlap (at least 2 words)
880
+ return best_match if max_overlap >= 2 else query_plan
881
+
882
+
883
+ # Singleton instance
884
+ nodes = AgentNodes()
agents/states.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Annotated, List, Dict, Any, Optional
2
+ import operator
3
+
4
+
5
+ class UserProfile(TypedDict, total=False):
6
+ """User profile for recommendation intent."""
7
+ age: Optional[int]
8
+ income: Optional[str]
9
+ smoker: Optional[bool]
10
+ dependents: Optional[int]
11
+ goal: Optional[str] # "protection", "savings", "retirement", "wealth"
12
+ cover_amount: Optional[str] # e.g., "1 Cr", "50 Lakh"
13
+
14
+
15
+ class ExtractedEntities(TypedDict, total=False):
16
+ """Entities extracted from user query."""
17
+ provider: Optional[List[str]] # ["TATA AIA", "Edelweiss Life"]
18
+ insurance_type: Optional[List[str]] # ["Term Insurance", "ULIP"]
19
+ plan_names: Optional[List[str]] # Specific plan names mentioned
20
+ user_profile: Optional[UserProfile]
21
+
22
+
23
+ class AgentState(TypedDict):
24
+ """
25
+ Enhanced state for the LangGraph RAG workflow.
26
+ Supports deterministic, compliance-focused retrieval.
27
+ """
28
+ # Input
29
+ input: str
30
+ chat_history: List[str]
31
+
32
+ # Query Classification
33
+ intent: str # 'list_plans', 'plan_details', 'compare_plans', 'recommendation', 'general_query'
34
+
35
+ # Entity Extraction
36
+ extracted_entities: ExtractedEntities
37
+
38
+ # Retrieval Configuration
39
+ metadata_filters: Dict[str, Any] # Filters for vector store
40
+ retrieval_strategy: str # 'metadata_only', 'plan_level', 'section_specific', 'cross_plan'
41
+
42
+ # Retrieved Content
43
+ context: List[str] # accumulated context strings
44
+ retrieved_chunks: Dict[str, List[Dict]] # Grouped by plan_id: {plan_id: [chunks]}
45
+
46
+ # Reasoning & Output
47
+ reasoning_output: str # Structured comparison/recommendation data
48
+ answer: str # Final answer to user
49
+
50
+ # Internal Routing
51
+ next_step: str # For conditional edges
api/main.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Body
2
+ from pydantic import BaseModel
3
+ from typing import List, Dict, Optional, Any
4
+ from agents.graph import app as agent_app
5
+ # from ingestion.pipeline import IngestionPipeline # Optional: Trigger via API
6
+
7
+ app = FastAPI(title="Insurance Advisory AI Agent", version="1.0.0")
8
+
9
+ class ChatRequest(BaseModel):
10
+ message: str
11
+ chat_history: Optional[List[str]] = []
12
+
13
+ class ChatResponse(BaseModel):
14
+ answer: str
15
+ intent: str
16
+ context_used: Optional[List[str]] = None
17
+
18
+ @app.get("/")
19
+ def health_check():
20
+ return {"status": "active", "system": "Insurance Advisory Agent"}
21
+
22
+ @app.post("/chat", response_model=ChatResponse)
23
+ async def chat_endpoint(request: ChatRequest):
24
+ """
25
+ Main chat endpoint. Routes query through the Multi-Agent Graph.
26
+ """
27
+ try:
28
+ # Initial state
29
+ initial_state = {
30
+ "input": request.message,
31
+ "chat_history": request.chat_history or [],
32
+ "intent": "",
33
+ "context": [],
34
+ "answer": "",
35
+ "metadata_filters": {}
36
+ }
37
+
38
+ # Invoke Graph
39
+ result = agent_app.invoke(initial_state)
40
+
41
+ return ChatResponse(
42
+ answer=result.get("answer", "No response generated."),
43
+ intent=result.get("intent", "unknown"),
44
+ context_used=result.get("context", [])
45
+ )
46
+
47
+ except Exception as e:
48
+ raise HTTPException(status_code=500, detail=str(e))
49
+
50
+ if __name__ == "__main__":
51
+ import uvicorn
52
+ uvicorn.run(app, host="0.0.0.0", port=8000)
app.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import threading
4
+ import speech_recognition as sr
5
+ from flask import Flask, render_template, request, jsonify
6
+ from agents.graph import app as agent_app
7
+ from dotenv import load_dotenv
8
+ from ingestion.pipeline import IngestionPipeline
9
+ from rag.vector_store import VectorStoreManager
10
+
11
+ load_dotenv()
12
+
13
+ app = Flask(__name__)
14
+
15
+ # Global state for ingestion tracking
16
+ ingestion_status = {
17
+ "status": "Idle",
18
+ "progress": 0,
19
+ "last_error": None
20
+ }
21
+
22
+ def ingest_worker(file_path, delete_source=None):
23
+ """Worker thread for background ingestion using enhanced pipeline."""
24
+ global ingestion_status
25
+ try:
26
+ ingestion_status["status"] = "Starting..."
27
+ ingestion_status["progress"] = 0
28
+
29
+ base_docs_dir = "docs"
30
+ pipeline = IngestionPipeline(base_docs_dir)
31
+ vector_manager = VectorStoreManager()
32
+
33
+ if delete_source:
34
+ ingestion_status["status"] = "Removing old version..."
35
+ vector_manager.delete_documents_by_source(delete_source)
36
+ ingestion_status["progress"] = 10
37
+
38
+ ingestion_status["status"] = "Processing document..."
39
+ ingestion_status["progress"] = 30
40
+
41
+ # Use the new unified process_single_file method
42
+ # Handles metadata extraction, section detection, and proper chunking
43
+ chunks = pipeline.process_single_file(file_path)
44
+
45
+ if chunks:
46
+ ingestion_status["status"] = "Updating Vector Store..."
47
+ ingestion_status["progress"] = 70
48
+ vector_manager.update_vector_store(chunks)
49
+
50
+ # Reload the retriever in the agent nodes to see new documents
51
+ from agents.nodes import nodes
52
+ nodes.reload_retriever()
53
+
54
+ ingestion_status["status"] = "Completed Successfully!"
55
+ ingestion_status["progress"] = 100
56
+ else:
57
+ ingestion_status["status"] = "Failed: No content extracted."
58
+ ingestion_status["progress"] = 0
59
+
60
+ except Exception as e:
61
+ ingestion_status["status"] = "Failed"
62
+ ingestion_status["last_error"] = str(e)
63
+ ingestion_status["progress"] = 0
64
+
65
+ @app.route("/")
66
+ def index():
67
+ return render_template("index.html")
68
+
69
+ @app.route("/api/chat", methods=["POST"])
70
+ def chat():
71
+ data = request.json
72
+ prompt = data.get("prompt")
73
+ history = data.get("history", [])
74
+
75
+ if not prompt:
76
+ return jsonify({"error": "Prompt is required"}), 400
77
+
78
+ try:
79
+ initial_state = {
80
+ "input": prompt,
81
+ "chat_history": history,
82
+ "intent": "",
83
+ "context": [],
84
+ "answer": "",
85
+ "metadata_filters": {}
86
+ }
87
+
88
+ result = agent_app.invoke(initial_state)
89
+ return jsonify({
90
+ "answer": result.get("answer", ""),
91
+ "context": result.get("context", [])
92
+ })
93
+ except Exception as e:
94
+ return jsonify({"error": str(e), "status": "error"}), 500
95
+
96
+ @app.route("/api/audio-chat", methods=["POST"])
97
+ def audio_chat():
98
+ if 'audio' not in request.files:
99
+ return jsonify({"error": "No audio file part"}), 400
100
+
101
+ file = request.files['audio']
102
+ history = json.loads(request.form.get("history", "[]"))
103
+
104
+ if file.filename == '':
105
+ return jsonify({"error": "No selected file"}), 400
106
+
107
+ temp_path = "temp_voice_query.wav"
108
+ file.save(temp_path)
109
+
110
+ r = sr.Recognizer()
111
+ try:
112
+ with sr.AudioFile(temp_path) as source:
113
+ audio_data = r.record(source)
114
+
115
+ raw_text = r.recognize_google(audio_data)
116
+
117
+ # Summarize/Refine the transcribed audio text
118
+ from models.llm import LLMFactory
119
+ from langchain_core.messages import SystemMessage, HumanMessage
120
+
121
+ refiner_llm = LLMFactory.get_llm("small")
122
+ refine_system = (
123
+ "You are an assistant that cleans up and summarizes noisy speech-to-text transcriptions. "
124
+ "Your goal is to extract the actual insurance-related question or request from the text.\n\n"
125
+ "RULES:\n"
126
+ "1. Remove filler words (um, ah, like, you know).\n"
127
+ "2. Fix grammatical errors caused by transcription.\n"
128
+ "3. If multiple things are mentioned, focus on the core request.\n"
129
+ "4. Return ONLY the cleaned, professional question text."
130
+ )
131
+
132
+ refine_response = refiner_llm.invoke([
133
+ SystemMessage(content=refine_system),
134
+ HumanMessage(content=f"Transcription: {raw_text}")
135
+ ])
136
+ summarized_text = getattr(refine_response, 'content', str(refine_response)).strip()
137
+
138
+ # Process with existing Agent using the summarized text
139
+ initial_state = {
140
+ "input": summarized_text,
141
+ "chat_history": history,
142
+ "intent": "",
143
+ "context": [],
144
+ "answer": "",
145
+ "metadata_filters": {}
146
+ }
147
+
148
+ result = agent_app.invoke(initial_state)
149
+
150
+ if os.path.exists(temp_path):
151
+ os.remove(temp_path)
152
+
153
+ return jsonify({
154
+ "transcription": raw_text,
155
+ "summarized_question": summarized_text,
156
+ "answer": result.get("answer", ""),
157
+ "context": result.get("context", [])
158
+ })
159
+
160
+ except sr.UnknownValueError:
161
+ if os.path.exists(temp_path): os.remove(temp_path)
162
+ return jsonify({"error": "Could not understand audio"}), 400
163
+ except sr.RequestError as e:
164
+ if os.path.exists(temp_path): os.remove(temp_path)
165
+ return jsonify({"error": f"Speech service error: {e}"}), 500
166
+ except Exception as e:
167
+ if os.path.exists(temp_path): os.remove(temp_path)
168
+ return jsonify({"error": str(e)}), 500
169
+
170
+ def update_doc_structure(provider_name, category_name):
171
+ """Helper to persist new providers/categories to the config file."""
172
+ try:
173
+ config_path = os.path.join("configs", "doc_structure.json")
174
+ if not os.path.exists(config_path):
175
+ return
176
+
177
+ with open(config_path, "r") as f:
178
+ config = json.load(f)
179
+
180
+ # Find or create provider
181
+ provider = next((p for p in config["providers"] if p["name"] == provider_name), None)
182
+ if not provider:
183
+ # Insert at the beginning (before 'Other')
184
+ provider = {"name": provider_name, "categories": []}
185
+ config["providers"].insert(0, provider)
186
+
187
+ # Add category if new
188
+ if category_name not in provider["categories"]:
189
+ provider["categories"].append(category_name)
190
+ # Sort categories for cleanliness (except if it was General)
191
+ if len(provider["categories"]) > 1:
192
+ provider["categories"].sort()
193
+
194
+ with open(config_path, "w") as f:
195
+ json.dump(config, f, indent=4)
196
+ except Exception as e:
197
+ pass
198
+
199
+ @app.route("/api/upload", methods=["POST"])
200
+ def upload():
201
+ if 'file' not in request.files:
202
+ return jsonify({"error": "No file part"}), 400
203
+
204
+ file = request.files['file']
205
+ provider = request.form.get("provider")
206
+ category = request.form.get("category")
207
+ mode = request.form.get("mode", "New Upload") # "New Upload" or "Modify Existing"
208
+
209
+ if file.filename == '' or not provider or not category:
210
+ return jsonify({"error": "Missing metadata or file"}), 400
211
+
212
+ # Persist new structure to JSON
213
+ update_doc_structure(provider, category)
214
+
215
+ base_dir = "docs"
216
+ target_dir = os.path.join(base_dir, provider, category)
217
+ os.makedirs(target_dir, exist_ok=True)
218
+
219
+ file_path = os.path.join(target_dir, file.filename)
220
+ file.save(file_path)
221
+
222
+ delete_source = None
223
+ if mode == "Modify Existing":
224
+ file_to_modify = request.form.get("file_to_modify")
225
+ if file_to_modify:
226
+ delete_source = os.path.join(base_dir, provider, category, file_to_modify)
227
+ if os.path.abspath(delete_source) != os.path.abspath(file_path):
228
+ if os.path.exists(delete_source):
229
+ os.remove(delete_source)
230
+
231
+ # Start background ingestion
232
+ thread = threading.Thread(target=ingest_worker, args=(file_path, delete_source))
233
+ thread.start()
234
+
235
+ return jsonify({"message": "File uploaded, ingestion started.", "path": file_path})
236
+
237
+ @app.route("/api/status", methods=["GET"])
238
+ def get_status():
239
+ return jsonify(ingestion_status)
240
+
241
+ @app.route("/api/config", methods=["GET"])
242
+ def get_config():
243
+ config_path = os.path.join("configs", "doc_structure.json")
244
+ if os.path.exists(config_path):
245
+ with open(config_path, "r") as f:
246
+ return jsonify(json.load(f))
247
+ return jsonify({"providers": []})
248
+
249
+ @app.route("/api/files", methods=["GET"])
250
+ def list_files():
251
+ provider = request.args.get("provider")
252
+ category = request.args.get("category")
253
+
254
+ if not provider or not category:
255
+ return jsonify({"files": []})
256
+
257
+ base_dir = "docs"
258
+ target_dir = os.path.join(base_dir, provider, category)
259
+ if os.path.exists(target_dir):
260
+ files = [f for f in os.listdir(target_dir) if f.lower().endswith(('.pdf', '.docx'))]
261
+ return jsonify({"files": files})
262
+ return jsonify({"files": []})
263
+
264
+ if __name__ == "__main__":
265
+ port = int(os.environ.get("PORT", 8000))
266
+ app.run(host="0.0.0.0", port=port)
configs/doc_structure.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "providers": [
3
+ {
4
+ "name": "Canara HSBC",
5
+ "categories": [
6
+ "Term Plan",
7
+ "Retirement Plan",
8
+ "ULIP Plan"
9
+ ]
10
+ },
11
+ {
12
+ "name": "Aviva",
13
+ "categories": [
14
+ "Term Plan",
15
+ "Savings Plan",
16
+ "ULIP Plan"
17
+ ]
18
+ },
19
+ {
20
+ "name": "Bharti Axa",
21
+ "categories": [
22
+ "Ulip Plan",
23
+ "Retirement Plan",
24
+ "Saving Plan"
25
+ ]
26
+ },
27
+ {
28
+ "name": "PRAMERICA",
29
+ "categories": [
30
+ "ULIP Plan",
31
+ "Health Plan",
32
+ "Retirement Plan",
33
+ "Savings Plan",
34
+ "Term Plan"
35
+ ]
36
+ },
37
+ {
38
+ "name": "TATA AIA",
39
+ "categories": [
40
+ "Combo",
41
+ "Health Plan",
42
+ "Retirement and Pension Plan",
43
+ "Savings Plan",
44
+ "ULIP Plans",
45
+ "Term Plans"
46
+ ]
47
+ },
48
+ {
49
+ "name": "IndusInd Nippon",
50
+ "categories": [
51
+ "Combo Plan",
52
+ "Pension Plan",
53
+ "Savings Plan",
54
+ "ULIP Plan",
55
+ "Term Plan"
56
+ ]
57
+ },
58
+ {
59
+ "name": "Edelweiss Life",
60
+ "categories": [
61
+ "Term Insurance Plans",
62
+ "Group Solutions",
63
+ "Guaranteed Income Plans",
64
+ "Micro Plans",
65
+ "Wealth Creation Plans"
66
+ ]
67
+ },
68
+ {
69
+ "name": "Generali Central",
70
+ "categories": [
71
+ "Term Plan",
72
+ "Savings Plan",
73
+ "ULIP Plan",
74
+ "Pension Plan"
75
+ ]
76
+ },
77
+ {
78
+ "name": "Other",
79
+ "categories": []
80
+ }
81
+ ]
82
+ }
configs/plan_metadata.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "EdelweissLife_Wealth_Rise_Plus": {
3
+ "category": [
4
+ "Savings Plan",
5
+ "Unit Linked Insurance Plan"
6
+ ]
7
+ },
8
+ "EdelweissLife_Saral_Jeevan_Bima": {
9
+ "category": [
10
+ "Term Insurance"
11
+ ]
12
+ },
13
+ "EdelweissLife_Zindagi_Protect_Plus": {
14
+ "category": [
15
+ "Term Insurance"
16
+ ]
17
+ },
18
+ "EdelweissLife_Group_Employee_Benefit_Plus": {
19
+ "category": [
20
+ "Uncategorized"
21
+ ]
22
+ },
23
+ "EdelweissLife_Pradhan_Mantri_Jeevan_Jyoti_Bima_Yojana": {
24
+ "category": [
25
+ "Uncategorized"
26
+ ]
27
+ },
28
+ "EdelweissLife_Group_Life_Protection": {
29
+ "category": [
30
+ "Uncategorized"
31
+ ]
32
+ },
33
+ "EdelweissLife_Group_Total_Secure": {
34
+ "category": [
35
+ "Uncategorized"
36
+ ]
37
+ },
38
+ "EdelweissLife_Flexi_Dream_Plan": {
39
+ "category": [
40
+ "Uncategorized"
41
+ ]
42
+ },
43
+ "EdelweissLife_Bharat_Savings_STAR": {
44
+ "category": [
45
+ "Uncategorized"
46
+ ]
47
+ },
48
+ "EdelweissLife_Flexi_Savings_Plan": {
49
+ "category": [
50
+ "Uncategorized"
51
+ ]
52
+ },
53
+ "EdelweissLife_Guaranteed_Flexi_STAR": {
54
+ "category": [
55
+ "Uncategorized"
56
+ ]
57
+ },
58
+ "EdelweissLife_Guaranteed_Income_STAR": {
59
+ "category": [
60
+ "Uncategorized"
61
+ ]
62
+ },
63
+ "EdelweissLife_Guaranteed_Savings_STAR": {
64
+ "category": [
65
+ "Uncategorized"
66
+ ]
67
+ },
68
+ "EdelweissLife_Premier_Guaranteed_STAR": {
69
+ "category": [
70
+ "Uncategorized"
71
+ ]
72
+ },
73
+ "EdelweissLife_Premier_Guaranteed_STAR_PRO": {
74
+ "category": [
75
+ "Uncategorized"
76
+ ]
77
+ },
78
+ "EdelweissLife_Jan_Suraksha": {
79
+ "category": [
80
+ "Uncategorized"
81
+ ]
82
+ },
83
+ "EdelweissLife_Raksha_Kavach_Bima_Yojana": {
84
+ "category": [
85
+ "Uncategorized"
86
+ ]
87
+ },
88
+ "EdelweissLife_Wealth_Plus": {
89
+ "category": [
90
+ "Uncategorized"
91
+ ]
92
+ },
93
+ "EdelweissLife_Wealth_Premier": {
94
+ "category": [
95
+ "Uncategorized"
96
+ ]
97
+ },
98
+ "EdelweissLife_Insurance_Wealth_Ultima": {
99
+ "category": [
100
+ "Uncategorized"
101
+ ]
102
+ }
103
+ }
docs/Aviva/Savings Plan/Aviva Nivesh Bima Brochure.PDF ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9da29e17e6d1f071e9b5eaed05d0f58650de6fce84316c55c630b9faf2eb31d4
3
+ size 886262
docs/Aviva/Savings Plan/Aviva Nivesh Bima CIS.docx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97d68bd2d7f6060cd9a2eccbeb47c98c2dab1e4de24813e5ccd52d0da0d82073
3
+ size 25227
docs/Aviva/Savings Plan/Aviva PoS Dhan Suraksha Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1e69f836f684f144bb5b8c3f624b2fe0774a1e3437d6c4234d4f6253dde79c39
3
+ size 801522
docs/Aviva/Savings Plan/Aviva PoS Dhan Suraksha CIS.docx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15d7238105b4218340066971f8c64e719ebf486a38d2db005822795d5de1b892
3
+ size 29354
docs/Aviva/Term Plan/Aviva Protection Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:58e317ee65d45347aff75de325df45b9758e82e1cc20f84f1944d03f1a0624bd
3
+ size 287843
docs/Aviva/Term Plan/Aviva Protection Plus CIS.doc ADDED
Binary file (77.8 kB). View file
 
docs/Aviva/ULIP Plan/Aviva Fortune Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1da0950c99b7d786ebc2d1359f1aa52334164b93eb955cfab68001a17b802342
3
+ size 650527
docs/Aviva/ULIP Plan/Aviva Fortune Plus CIS.doc ADDED
Binary file (89.6 kB). View file
 
docs/Bharti Axa/Retirement Plan/Bharti Axa Swabhimaan Retirement Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c1a138c1205a2807b7df04342f08a8a6b986ed2d75b807974b421fe20f64767
3
+ size 1687191
docs/Bharti Axa/Saving Plan/Bharti Axa Guaranteed Bachat Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c39fb2a0ff4ea5d2ad70ef1484dc2483a68e064afa9b618efe7904a0298c2933
3
+ size 3071848
docs/Bharti Axa/Saving Plan/Bharti Axa Guaranteed Wealth Pro Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7d1037a5f6d4bcb59d6f432ddabaf295104f1b81528cb51f8344a49c5030595
3
+ size 679317
docs/Bharti Axa/Saving Plan/Bharti Axa Secure Insta Income Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:62d53318a13b8507913c3da4d146d8c48cc8426d9b0e16782aa73ee20c56d614
3
+ size 2013163
docs/Bharti Axa/Saving Plan/Bharti Axa Unnati Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02583b7c27c7514f2616f92f6e90c710a7e6a405cd7695e641fb3ffe73711a07
3
+ size 644320
docs/Bharti Axa/Ulip Plan/Bhart Axa Growth Shield Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f25d8efd733c20956e2f6fb5cc124d48fcbba315fcdb587670fb46318a05a936
3
+ size 668695
docs/Bharti Axa/Ulip Plan/Bharti Axa Life Dream Shield Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e654c94b96e3be9db9916ffc7cf21557206680aeb92a7b46171386f685603d9
3
+ size 1155895
docs/Bharti Axa/Ulip Plan/Bharti Axa Unnati Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02583b7c27c7514f2616f92f6e90c710a7e6a405cd7695e641fb3ffe73711a07
3
+ size 644320
docs/Canara HSBC/Retirement Plan/Canara HSBC EZ Pension Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:394a3f50d6c8cb17c73294e4640071c2e4dafd2c37036fbdf1e7105a882bd54c
3
+ size 2471470
docs/Canara HSBC/Retirement Plan/Canara HSBC Pension 4 Life Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3bc2d20809919e8fc62e23f74a84c35474c4a20eee5c23631eea2e24e691336
3
+ size 597661
docs/Canara HSBC/Retirement Plan/Canara HSBC iSelect Guaranteed Future Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:61c808b23e6ea9e25e0c99b37034374f533de439dd248ba2446ea7e00289d161
3
+ size 1515545
docs/Canara HSBC/Term Plan/Canara HSBC Promise 2 Protect Term Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06532d990882d27b61e2835f5d8412808fb0a3927558712dbba0fb9bf71dcb32
3
+ size 839879
docs/Canara HSBC/Term Plan/Canara HSBC Young Term Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3043dff7ba993fac87dd48c705afa1ae28787e58fe9b3d255936a87a4cadbf72
3
+ size 821708
docs/Canara HSBC/Term Plan/Canara HSBC iSelect Smart 360 Term Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc12bb1301a51dbdc963c55d58d776ed771b395c9a9226c963af3cd68c02277b
3
+ size 871970
docs/Canara HSBC/ULIP Plan/Canara HSBC Promise 4 Growth Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7a8951e15e50a2deb9e2d84a8b3ea1d385a3fb27e60c113666c1af1a1bd8c52
3
+ size 1068461
docs/Canara HSBC/ULIP Plan/Canara HSBC Secure Invest Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:970f0240da50c6511aa8d9c33a551ac58d018954231877d5774685ecebb02aaf
3
+ size 915741
docs/Canara HSBC/ULIP Plan/Canara HSBC Wealth Edge Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19a66c2727b907e24cd520e47ce5d9a4854c5d0c9f026a919cd995c76a824be1
3
+ size 4263484
docs/Edelweiss Life/Group Solutions/EdelweissLife Group Employee Benefit Plus Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cb77163dfc4623112bece207f55d320d1b1349b63719723f89bbfb9b2edea900
3
+ size 358074
docs/Edelweiss Life/Group Solutions/EdelweissLife Group Life Protection Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0b58cfa8e526cab19e73f530a70333286ba58a71f60563fbf142a97704eece55
3
+ size 289919
docs/Edelweiss Life/Group Solutions/EdelweissLife Group Life Protection CIS.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:961d85be4cc51bcf42f37b9a6b0fe251b0950fb397a0bd3d91a4cd62ce1a7007
3
+ size 193804
docs/Edelweiss Life/Group Solutions/EdelweissLife Group Total Secure Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8bde2b7b8a2dacfef43b89ec30ebb0ff015d90e46a692096518d60d0fd905782
3
+ size 420074
docs/Edelweiss Life/Group Solutions/EdelweissLife Pradhan Mantri Jeevan Jyoti Bima Yojana Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cca1758cd0d4738fdb8e1bf4d026f0107842abf6211ab335dcdb55741faa08f4
3
+ size 334737
docs/Edelweiss Life/Group Solutions/EdelweissLife Pradhan Mantri Jeevan Jyoti Bima Yojana CIS.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f048aed0c55c69399fb9713182e1aec0e39d066e5a0e500cd5278bc97ff68273
3
+ size 177614
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Bharat Savings STAR Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:731ad161b10fa18967176a972ef4c899dac7a4743c4fbe52c94a997ea13a4006
3
+ size 493775
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Bharat Savings STAR CIS.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c409818e932309a79f3489c7e36c4533a76ccde1b4df832f705ebce5752f19b0
3
+ size 230194
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Dream Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:729d4eec3800236ff14ac7e367d4f9b0a3c1fd27da22c3844a2d319c07a21cb8
3
+ size 579303
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Dream Plan CIS.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7f32e3ee795d218481e04b12cee2baaf3207095dd734bc2e33ede2867a66629
3
+ size 345849
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Savings Plan Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0740ae4ce24965183c66b3c6cd8b5325187cd38739c649710c5af6e21ea8f1c3
3
+ size 513027
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Savings Plan CIS .pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d2d629ea0160f5619a74e344faf51bd03cb1d2ce22fd1f25d1faee6a6984c3a
3
+ size 263599
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Flexi STAR Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ae3d1dc3ccdb5f56044f0f81f492827847c04c53d8289652e2ea23b0ede9c6c2
3
+ size 420020
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Flexi STAR CIS.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:76cd977f1887b63dd413e7797961ad01b5d3c4714925bbcfc1f65a65221fca7b
3
+ size 268535
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Income STAR Brochure.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:896453b156d6823e4beb68197ba51bdada11b240afeca3587279637e93805b03
3
+ size 615785
docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Income STAR CIS.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fbbe5519a6bb6f79638eebbe3bf137b4729bf3eda4d0c1a35fdc8ff006c549b1
3
+ size 248372