Spaces:
Runtime error
Runtime error
Commit ·
cff1a2a
0
Parent(s):
add project
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +4 -0
- .gitignore +10 -0
- agents/graph.py +171 -0
- agents/nodes.py +884 -0
- agents/states.py +51 -0
- api/main.py +52 -0
- app.py +266 -0
- configs/doc_structure.json +82 -0
- configs/plan_metadata.json +103 -0
- docs/Aviva/Savings Plan/Aviva Nivesh Bima Brochure.PDF +3 -0
- docs/Aviva/Savings Plan/Aviva Nivesh Bima CIS.docx +3 -0
- docs/Aviva/Savings Plan/Aviva PoS Dhan Suraksha Brochure.pdf +3 -0
- docs/Aviva/Savings Plan/Aviva PoS Dhan Suraksha CIS.docx +3 -0
- docs/Aviva/Term Plan/Aviva Protection Plus Brochure.pdf +3 -0
- docs/Aviva/Term Plan/Aviva Protection Plus CIS.doc +0 -0
- docs/Aviva/ULIP Plan/Aviva Fortune Plus Brochure.pdf +3 -0
- docs/Aviva/ULIP Plan/Aviva Fortune Plus CIS.doc +0 -0
- docs/Bharti Axa/Retirement Plan/Bharti Axa Swabhimaan Retirement Plan Brochure.pdf +3 -0
- docs/Bharti Axa/Saving Plan/Bharti Axa Guaranteed Bachat Plan Brochure.pdf +3 -0
- docs/Bharti Axa/Saving Plan/Bharti Axa Guaranteed Wealth Pro Brochure.pdf +3 -0
- docs/Bharti Axa/Saving Plan/Bharti Axa Secure Insta Income Plan Brochure.pdf +3 -0
- docs/Bharti Axa/Saving Plan/Bharti Axa Unnati Plan Brochure.pdf +3 -0
- docs/Bharti Axa/Ulip Plan/Bhart Axa Growth Shield Plus Brochure.pdf +3 -0
- docs/Bharti Axa/Ulip Plan/Bharti Axa Life Dream Shield Plus Brochure.pdf +3 -0
- docs/Bharti Axa/Ulip Plan/Bharti Axa Unnati Plan Brochure.pdf +3 -0
- docs/Canara HSBC/Retirement Plan/Canara HSBC EZ Pension Brochure.pdf +3 -0
- docs/Canara HSBC/Retirement Plan/Canara HSBC Pension 4 Life Brochure.pdf +3 -0
- docs/Canara HSBC/Retirement Plan/Canara HSBC iSelect Guaranteed Future Plus Brochure.pdf +3 -0
- docs/Canara HSBC/Term Plan/Canara HSBC Promise 2 Protect Term Plan Brochure.pdf +3 -0
- docs/Canara HSBC/Term Plan/Canara HSBC Young Term Plan Brochure.pdf +3 -0
- docs/Canara HSBC/Term Plan/Canara HSBC iSelect Smart 360 Term Plan Brochure.pdf +3 -0
- docs/Canara HSBC/ULIP Plan/Canara HSBC Promise 4 Growth Plus Brochure.pdf +3 -0
- docs/Canara HSBC/ULIP Plan/Canara HSBC Secure Invest Brochure.pdf +3 -0
- docs/Canara HSBC/ULIP Plan/Canara HSBC Wealth Edge Brochure.pdf +3 -0
- docs/Edelweiss Life/Group Solutions/EdelweissLife Group Employee Benefit Plus Brochure.pdf +3 -0
- docs/Edelweiss Life/Group Solutions/EdelweissLife Group Life Protection Brochure.pdf +3 -0
- docs/Edelweiss Life/Group Solutions/EdelweissLife Group Life Protection CIS.pdf +3 -0
- docs/Edelweiss Life/Group Solutions/EdelweissLife Group Total Secure Brochure.pdf +3 -0
- docs/Edelweiss Life/Group Solutions/EdelweissLife Pradhan Mantri Jeevan Jyoti Bima Yojana Brochure.pdf +3 -0
- docs/Edelweiss Life/Group Solutions/EdelweissLife Pradhan Mantri Jeevan Jyoti Bima Yojana CIS.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Bharat Savings STAR Brochure.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Bharat Savings STAR CIS.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Dream Plan Brochure.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Dream Plan CIS.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Savings Plan Brochure.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Flexi Savings Plan CIS .pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Flexi STAR Brochure.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Flexi STAR CIS.pdf +3 -0
- docs/Edelweiss Life/Guaranteed Income Plans/EdelweissLife Guaranteed Income STAR Brochure.pdf +3 -0
- 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
|