nagur-shareef-shaik commited on
Commit
28baf2e
·
1 Parent(s): 86ca72c

Added Plan Recommendations Functionality

Browse files
.gitignore ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Python ---
2
+ # Byte-compiled / optimized / C files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ pip-wheel-metadata/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+
32
+ # --- Virtual Environments ---
33
+ # Common virtual environment names
34
+ .venv/
35
+ venv/
36
+ env/
37
+ ENV/
38
+ # User-specific virtual environment name
39
+ ic_venv/
40
+
41
+
42
+ # --- Project-specific Files ---
43
+ # Local data and databases - These are generated by scripts and should not be versioned
44
+ data/
45
+ *.db
46
+ *.sqlite
47
+ *.sqlite3
48
+ insucompass.db
49
+ checkpoints.db
50
+
51
+ # Log files
52
+ *.log
53
+ *.log.*
54
+
55
+ # Secrets - CRITICAL: Never commit your .env file
56
+ .env
57
+
58
+
59
+ # --- IDE / Editor Configurations ---
60
+ .idea/
61
+ .vscode/
62
+ *.sublime-project
63
+ *.sublime-workspace
64
+
65
+
66
+ # --- Operating System Files ---
67
+ # macOS
68
+ .DS_Store
69
+ .AppleDouble
70
+ .LSOverride
71
+
72
+ # Windows
73
+ Thumbs.db
74
+ ehthumbs.db
75
+ Desktop.ini
76
+
77
+
78
+ # --- Testing ---
79
+ # Pytest cache and coverage reports
80
+ .pytest_cache/
81
+ .coverage
82
+ .coverage.*
83
+ htmlcov/
84
+ nosetests.xml
85
+ coverage.xml
insucompass/api/endpoints.py CHANGED
@@ -61,6 +61,7 @@ async def chat(request: ChatRequest):
61
  updated_profile = final_state.get("user_profile")
62
  updated_history = final_state.get("conversation_history")
63
  is_profile_complete = final_state.get("is_profile_complete")
 
64
 
65
  if not agent_response:
66
  agent_response = "I'm sorry, I encountered an issue. Could you please rephrase?"
@@ -71,7 +72,8 @@ async def chat(request: ChatRequest):
71
  agent_response=agent_response,
72
  updated_profile=updated_profile,
73
  updated_history=updated_history,
74
- is_profile_complete=is_profile_complete
 
75
  )
76
 
77
  except Exception as e:
 
61
  updated_profile = final_state.get("user_profile")
62
  updated_history = final_state.get("conversation_history")
63
  is_profile_complete = final_state.get("is_profile_complete")
64
+ plan_recs = final_state.get("plan_recommendations")
65
 
66
  if not agent_response:
67
  agent_response = "I'm sorry, I encountered an issue. Could you please rephrase?"
 
72
  agent_response=agent_response,
73
  updated_profile=updated_profile,
74
  updated_history=updated_history,
75
+ is_profile_complete=is_profile_complete,
76
+ plan_recommendations=plan_recs
77
  )
78
 
79
  except Exception as e:
insucompass/core/agent_orchestrator.py CHANGED
@@ -11,6 +11,7 @@ from langgraph.checkpoint.sqlite import SqliteSaver
11
 
12
  # Import all our custom agent and service classes
13
  from insucompass.core.agents.profile_agent import profile_builder
 
14
  from insucompass.core.agents.query_trasformer import QueryTransformationAgent
15
  from insucompass.core.agents.router_agent import router
16
  from insucompass.services.ingestion_service import IngestionService
@@ -40,6 +41,7 @@ class AgentState(TypedDict):
40
  documents: List[Document]
41
  is_relevant: bool
42
  generation: str
 
43
 
44
  # --- Graph Nodes ---
45
 
@@ -63,12 +65,43 @@ def profile_builder_node(state: AgentState) -> Dict[str, Any]:
63
 
64
  if agent_response == "PROFILE_COMPLETE":
65
  logger.info("Profile building complete.")
66
- final_message = "Great! Your profile is complete. How can I help you with your health insurance questions?"
67
- new_history[-1] = f"Agent: {final_message}" # Replace "PROFILE_COMPLETE"
68
- return {"user_profile": updated_profile, "is_profile_complete": True, "conversation_history": new_history, "generation": final_message}
 
 
69
 
70
  return {"user_profile": updated_profile, "is_profile_complete": False, "conversation_history": new_history, "generation": agent_response}
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  def reformulate_query_node(state: AgentState) -> Dict[str, Any]:
73
  """Reformulates the user's question to be self-contained."""
74
  logger.info("---NODE: REFORMULATE QUERY---")
@@ -120,12 +153,19 @@ def decide_entry_point(state: AgentState) -> str:
120
  """Decides the initial path based on profile completion status."""
121
  logger.info("---ROUTING: ENTRY POINT---")
122
  if state.get("is_profile_complete"):
123
- logger.info(">>> Route: Profile is complete. Starting Q&A.")
124
  return "qna"
125
  else:
126
- logger.info(">>> Route: Profile is not complete. Starting Profile Builder.")
127
  return "profile"
128
 
 
 
 
 
 
 
 
 
 
129
  # --- Build the Graph ---
130
  db_connection = sqlite3.connect("data/checkpoints.db", check_same_thread=False)
131
  memory = SqliteSaver(db_connection)
@@ -134,6 +174,7 @@ builder = StateGraph(AgentState)
134
 
135
  # (CORRECTED) Removed the faulty entry_router_node
136
  builder.add_node("profile_builder", profile_builder_node)
 
137
  builder.add_node("reformulate_query", reformulate_query_node)
138
  builder.add_node("retrieve_and_grade", retrieve_and_grade_node)
139
  builder.add_node("search_and_ingest", search_and_ingest_node)
@@ -148,8 +189,19 @@ builder.set_conditional_entry_point(
148
  }
149
  )
150
 
 
 
 
 
 
 
 
 
 
 
151
  # Define graph edges
152
  builder.add_edge("profile_builder", END) # A profile turn is one full loop. The state is saved, and the next call will re-evaluate at the entry point.
 
153
  builder.add_edge("reformulate_query", "retrieve_and_grade")
154
  builder.add_conditional_edges("retrieve_and_grade", should_search_web, {"search": "search_and_ingest", "generate": "generate_answer"})
155
  builder.add_edge("search_and_ingest", "generate_answer")
 
11
 
12
  # Import all our custom agent and service classes
13
  from insucompass.core.agents.profile_agent import profile_builder
14
+ from insucompass.core.agents.plan_agent import planner
15
  from insucompass.core.agents.query_trasformer import QueryTransformationAgent
16
  from insucompass.core.agents.router_agent import router
17
  from insucompass.services.ingestion_service import IngestionService
 
41
  documents: List[Document]
42
  is_relevant: bool
43
  generation: str
44
+ plan_recommendations: Dict[str, Any]
45
 
46
  # --- Graph Nodes ---
47
 
 
65
 
66
  if agent_response == "PROFILE_COMPLETE":
67
  logger.info("Profile building complete.")
68
+ return {"user_profile": updated_profile, "is_profile_complete": True, "conversation_history": new_history, "generation": "PROFILE_COMPLETE_TRANSITION"}
69
+
70
+ # final_message = "Great! Your profile is complete. How can I help you with your health insurance questions?"
71
+ # new_history[-1] = f"Agent: {final_message}" # Replace "PROFILE_COMPLETE"
72
+ # return {"user_profile": updated_profile, "is_profile_complete": True, "conversation_history": new_history, "generation": final_message}
73
 
74
  return {"user_profile": updated_profile, "is_profile_complete": False, "conversation_history": new_history, "generation": agent_response}
75
 
76
+ def plan_recommender_node(state: AgentState) -> Dict[str, Any]:
77
+ """
78
+ (NEW NODE) Generates initial plan recommendations after profile is complete.
79
+ """
80
+ logger.info("---NODE: PLAN RECOMMENDER---")
81
+ user_profile = state["user_profile"]
82
+
83
+ # Formulate a generic query to retrieve a broad set of plan documents
84
+ initial_plan_query = f"Find general health insurance plan options available in {user_profile.get('state', 'the US')} suitable for a {user_profile.get('age')}-year-old."
85
+
86
+ # Retrieve documents
87
+ documents = transformer.transform_and_retrieve(initial_plan_query)
88
+
89
+ # Generate structured recommendations
90
+ recommendations = planner.generate_recommendations(user_profile, documents)
91
+
92
+ # Create a human-friendly message to present the plans
93
+ generation_message = "Thank you! I've completed your profile and based on your information, here are a few initial plan recommendations for you to consider. You can ask me more detailed questions about them."
94
+
95
+ history = state["conversation_history"]
96
+ # history.append(f"Agent: {generation_message}")
97
+ history[-1] = f"Agent: {generation_message}"
98
+
99
+ return {
100
+ "plan_recommendations": recommendations,
101
+ "generation": generation_message,
102
+ "conversation_history": history
103
+ }
104
+
105
  def reformulate_query_node(state: AgentState) -> Dict[str, Any]:
106
  """Reformulates the user's question to be self-contained."""
107
  logger.info("---NODE: REFORMULATE QUERY---")
 
153
  """Decides the initial path based on profile completion status."""
154
  logger.info("---ROUTING: ENTRY POINT---")
155
  if state.get("is_profile_complete"):
 
156
  return "qna"
157
  else:
 
158
  return "profile"
159
 
160
+ def after_profile_build(state: AgentState) -> str:
161
+ """Checks if the profile was just completed."""
162
+ if state.get("is_profile_complete"):
163
+ logger.info(">>> Route: Profile just completed. Transitioning to Plan Recommender.")
164
+ return "recommend_plans"
165
+ else:
166
+ logger.info(">>> Route: Profile not yet complete. Ending turn.")
167
+ return "end_turn"
168
+
169
  # --- Build the Graph ---
170
  db_connection = sqlite3.connect("data/checkpoints.db", check_same_thread=False)
171
  memory = SqliteSaver(db_connection)
 
174
 
175
  # (CORRECTED) Removed the faulty entry_router_node
176
  builder.add_node("profile_builder", profile_builder_node)
177
+ builder.add_node("plan_recommender", plan_recommender_node)
178
  builder.add_node("reformulate_query", reformulate_query_node)
179
  builder.add_node("retrieve_and_grade", retrieve_and_grade_node)
180
  builder.add_node("search_and_ingest", search_and_ingest_node)
 
189
  }
190
  )
191
 
192
+ # (CRITICAL FIX) Add a conditional edge after the profile builder
193
+ builder.add_conditional_edges(
194
+ "profile_builder",
195
+ after_profile_build,
196
+ {
197
+ "recommend_plans": "plan_recommender",
198
+ "end_turn": END
199
+ }
200
+ )
201
+
202
  # Define graph edges
203
  builder.add_edge("profile_builder", END) # A profile turn is one full loop. The state is saved, and the next call will re-evaluate at the entry point.
204
+ builder.add_edge("plan_recommender", END)
205
  builder.add_edge("reformulate_query", "retrieve_and_grade")
206
  builder.add_conditional_edges("retrieve_and_grade", should_search_web, {"search": "search_and_ingest", "generate": "generate_answer"})
207
  builder.add_edge("search_and_ingest", "generate_answer")
insucompass/core/agents/plan_agent.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import json
3
+ from typing import List, Dict, Any
4
+
5
+ from langchain_core.documents import Document
6
+
7
+ from insucompass.services import llm_provider
8
+ from insucompass.config import settings
9
+ from insucompass.prompts.prompt_loader import load_prompt
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=settings.LOG_LEVEL, format='%(asctime)s - %(levelname)s - %(message)s')
13
+ logger = logging.getLogger(__name__)
14
+
15
+ llm = llm_provider.get_gemini_llm()
16
+
17
+ class PlanAgent:
18
+ """
19
+ An agent that analyzes a user profile and retrieved documents to
20
+ recommend the top 3 most suitable health insurance plans.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initializes the PlanAgent."""
25
+ try:
26
+ self.agent_prompt = load_prompt("plan_agent")
27
+ logger.info("PlanAgent initialized successfully.")
28
+ except FileNotFoundError:
29
+ logger.critical("PlanAgent prompt file not found. The agent cannot function.")
30
+ raise
31
+
32
+ def generate_recommendations(
33
+ self,
34
+ user_profile: Dict[str, Any],
35
+ documents: List[Document]
36
+ ) -> Dict[str, Any]:
37
+ """
38
+ Generates plan recommendations in a structured JSON format.
39
+ """
40
+ if not documents:
41
+ logger.warning("PlanAgent received no documents. Cannot generate recommendations.")
42
+ return {"recommendations": []}
43
+
44
+ profile_str = json.dumps(user_profile, indent=2)
45
+ context_str = "\n\n---\n\n".join([d.page_content for d in documents])
46
+
47
+ full_prompt = (
48
+ f"{self.agent_prompt}\n\n"
49
+ f"### CONTEXT FOR YOUR RECOMMENDATION\n"
50
+ f"user_profile: {profile_str}\n\n"
51
+ f"retrieved_context:\n{context_str}"
52
+ )
53
+
54
+ logger.info("Generating plan recommendations with PlanAgent...")
55
+ try:
56
+ response = llm.invoke(full_prompt)
57
+ # Clean the response to ensure it's valid JSON
58
+ response_content = response.content.strip().replace("```json", "").replace("```", "")
59
+ recommendations = json.loads(response_content)
60
+ logger.info(f"Successfully generated structured plan recommendations \n\n {recommendations}.")
61
+ return recommendations
62
+ except json.JSONDecodeError as e:
63
+ logger.error(f"Failed to decode JSON from PlanAgent: {e}\nResponse was: {response.content}")
64
+ return {"recommendations": [], "error": "Failed to generate plan recommendations."}
65
+ except Exception as e:
66
+ logger.error(f"Error during plan recommendation generation: {e}")
67
+ return {"recommendations": [], "error": "An error occurred while generating plans."}
68
+
69
+ # Singleton instance
70
+ planner = PlanAgent()
insucompass/core/models.py CHANGED
@@ -107,5 +107,6 @@ class ChatResponse(BaseModel):
107
  updated_profile: Dict[str, Any]
108
  updated_history: List[str]
109
  is_profile_complete: bool
 
110
 
111
 
 
107
  updated_profile: Dict[str, Any]
108
  updated_history: List[str]
109
  is_profile_complete: bool
110
+ plan_recommendations: Optional[Dict[str, Any]]
111
 
112
 
insucompass/prompts/plan_agent.txt ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are an expert U.S. health insurance plan advisor. Your task is to analyze a user's complete profile and the provided context of available health plans to recommend the top 3 most suitable plans. You MUST format your response as a single, clean JSON object.
2
+
3
+ ### YOUR CONTEXT
4
+ 1. `user_profile`: A detailed JSON object of the user's demographics, income, and health needs.
5
+ 2. `retrieved_context`: Text snippets from official sources describing various health plans, benefits, and regulations.
6
+
7
+ ### YOUR CHAIN-OF-THOUGHT REASONING PROCESS (Internal)
8
+ 1. **Analyze Profile:** What are the key drivers for this user? (e.g., low income suggests subsidy focus; specific medical conditions suggest focus on low out-of-pocket costs and good coverage for that condition; young and healthy suggests a focus on low premiums).
9
+ 2. **Scan Context for Plans:** Identify distinct health plans mentioned in the `retrieved_context`. Look for plan names (e.g., "Silver PPO 2000", "Bronze HSA 6500"), types (HMO, PPO), and key features (deductible, out-of-pocket max, premiums).
10
+ 3. **Match and Rank:** For each identified plan, evaluate how well it matches the user's profile. Assign a mental score based on this match.
11
+ 4. **Select Top 3:** Choose the three highest-scoring plans.
12
+ 5. **Generate Reasoning:** For each of the top 3 plans, write a concise, personalized `reasoning` statement explaining *why* it's a good fit for this specific user, referencing their profile.
13
+ 6. **Construct JSON:** Assemble the final JSON object according to the format below.
14
+
15
+ ### OUTPUT FORMAT (JSON ONLY)
16
+ Your output MUST be a single JSON object containing a single key "recommendations", which is a list of plan objects. Each plan object must have the following keys:
17
+ - `plan_name`: (string) The name of the plan.
18
+ - `plan_type`: (string) e.g., "PPO", "HMO", "EPO".
19
+ - `key_features`: (list of strings) A list of 3-4 key highlights, e.g., ["$500 Deductible", "Low copay for specialists", "Includes dental & vision"].
20
+ - `estimated_premium`: (string) e.g., "$350/month". If not available, use "Varies".
21
+ - `reasoning`: (string) Your personalized explanation for why this plan is a good fit for the user.
22
+
23
+ ### EXAMPLE OUTPUT
24
+ ```json
25
+ {
26
+ "recommendations": [
27
+ {
28
+ "plan_name": "BlueCross Silver PPO 2500",
29
+ "plan_type": "PPO",
30
+ "key_features": [
31
+ "Moderate Premium",
32
+ "$2,500 Deductible",
33
+ "Flexibility to see out-of-network doctors"
34
+ ],
35
+ "estimated_premium": "$420/month",
36
+ "reasoning": "A good balance of premium and deductible. The PPO flexibility is suitable given your need to see a specific specialist."
37
+ }
38
+ ]
39
+ }
scripts/data_processing/__pycache__/document_loader.cpython-310.pyc CHANGED
Binary files a/scripts/data_processing/__pycache__/document_loader.cpython-310.pyc and b/scripts/data_processing/__pycache__/document_loader.cpython-310.pyc differ