{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "46c8044f", "metadata": {}, "outputs": [], "source": [ "import logging\n", "import requests\n", "from typing import Optional, Dict, Any, List\n", "\n", "logger = logging.getLogger(__name__)\n", "\n", "class ZipCodeData(object):\n", " \"\"\"A simple data class to hold the results of our geolocation lookup.\"\"\"\n", " def __init__(self, state: str, state_abbr: str, city: str, county: str):\n", " self.state = state\n", " self.state_abbr = state_abbr\n", " self.city = city\n", " self.county = county\n", "\n", " def to_dict(self) -> Dict[str, Any]:\n", " return {\n", " \"state\": self.state,\n", " \"state_abbreviation\": self.state_abbr,\n", " \"city\": self.city,\n", " \"county\": self.county\n", " }\n", "\n", "def get_lat_lon_from_zip(zip_code: str) -> Optional[Dict[str, float]]:\n", " \"\"\"\n", " Step 1: Get latitude and longitude from a ZIP code using a simple API.\n", " We'll use zippopotam.us for this first step.\n", " \"\"\"\n", " url = f\"https://api.zippopotam.us/us/{zip_code}\"\n", " logger.info(f\"Fetching lat/lon for ZIP code: {zip_code} from {url}\")\n", " try:\n", " response = requests.get(url, timeout=10)\n", " response.raise_for_status()\n", " data = response.json()\n", " \n", " if not data.get(\"places\"):\n", " logger.warning(f\"No places found for ZIP code {zip_code}\")\n", " return None\n", " \n", " place = data[\"places\"][0]\n", " return {\n", " \"latitude\": float(place[\"latitude\"]),\n", " \"longitude\": float(place[\"longitude\"]),\n", " \"state\": place[\"state\"],\n", " \"state_abbr\": place[\"state abbreviation\"],\n", " \"city\": place[\"place name\"]\n", " }\n", " except (requests.RequestException, KeyError, ValueError) as e:\n", " logger.error(f\"Failed to get lat/lon for ZIP {zip_code}: {e}\")\n", " return None\n", "\n", "def get_county_from_lat_lon(lat: float, lon: float) -> Optional[str]:\n", " \"\"\"\n", " Step 2: Get county information from latitude and longitude using the\n", " U.S. Census Bureau's Geocoding API.\n", " \"\"\"\n", " url = \"https://geocoding.geo.census.gov/geocoder/geographies/coordinates\"\n", " params = {\n", " 'x': lon,\n", " 'y': lat,\n", " 'benchmark': 'Public_AR_Current',\n", " 'vintage': 'Current_Current',\n", " 'format': 'json'\n", " }\n", " logger.info(f\"Fetching county for coordinates: (lat={lat}, lon={lon}) from Census Bureau API\")\n", " try:\n", " response = requests.get(url, params=params, timeout=15)\n", " response.raise_for_status()\n", " data = response.json()\n", " \n", " geographies = data.get(\"result\", {}).get(\"geographies\", {})\n", " counties = geographies.get(\"Counties\", [])\n", " \n", " if counties:\n", " county_name = counties[0].get(\"NAME\")\n", " logger.info(f\"Found county: {county_name}\")\n", " return county_name\n", " else:\n", " logger.warning(f\"No county found for coordinates (lat={lat}, lon={lon})\")\n", " return None\n", " except (requests.RequestException, KeyError, ValueError) as e:\n", " logger.error(f\"Failed to get county from coordinates: {e}\")\n", " return None\n", "\n", "def get_geo_data_from_zip(zip_code: str) -> Optional[ZipCodeData]:\n", " \"\"\"\n", " Orchestrates the two-step process to get state, city, and county from a ZIP code.\n", " \"\"\"\n", " # Step 1: Get Lat/Lon and basic info\n", " geo_basics = get_lat_lon_from_zip(zip_code)\n", " if not geo_basics:\n", " return None\n", " \n", " # Step 2: Get County from Lat/Lon\n", " county = get_county_from_lat_lon(geo_basics[\"latitude\"], geo_basics[\"longitude\"])\n", " if not county:\n", " # Fallback: sometimes county info is not available, but we can proceed without it\n", " logger.warning(f\"Could not determine county for ZIP {zip_code}, proceeding without it.\")\n", " county = \"Unknown\"\n", "\n", " return ZipCodeData(\n", " state=geo_basics[\"state\"],\n", " state_abbr=geo_basics[\"state_abbr\"],\n", " city=geo_basics[\"city\"],\n", " county=county[:-7]\n", " )\n", "\n", "data = get_geo_data_from_zip(\"23294\")\n", "print(data.county, data.city, data.state, data.state_abbr, sep=\",\")" ] }, { "cell_type": "code", "execution_count": 2, "id": "2e671137", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:15:45,995 - INFO - Initialized LLM Provider: gemini-2.5-flash\n", "2025-07-09 18:15:46,000 - INFO - QueryIntentClassifierAgent initialized successfully.\n", "2025-07-09 18:15:46,001 - INFO - QueryTransformationAgent initialized successfully.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "==================== Turn 1 ====================\n", "Current Profile State:\n", "{\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\",\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": null,\n", " \"medications\": null,\n", " \"special_cases\": null\n", "}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:15:47,304 - INFO - LLM returned next step: 'Alright, let's continue building your health profile. To help us understand your needs better, could you please share a bit about your medical history? For example, have you been diagnosed with any chronic conditions like diabetes, high blood pressure, or asthma, or have you had any major surgeries in the past? No need to go into excessive detail, just the key points that might be relevant for your health coverage.'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: Alright, let's continue building your health profile. To help us understand your needs better, could you please share a bit about your medical history? For example, have you been diagnosed with any chronic conditions like diabetes, high blood pressure, or asthma, or have you had any major surgeries in the past? No need to go into excessive detail, just the key points that might be relevant for your health coverage.\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:15:51,608 - INFO - Successfully updated profile with user's answer.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "==================== Turn 2 ====================\n", "Current Profile State:\n", "{\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\",\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": \"None reported.\",\n", " \"medications\": null,\n", " \"special_cases\": null\n", "}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:15:54,271 - INFO - LLM returned next step: 'Thank you for confirming your medical history. Just one last area to cover: are there any major life events, like a pregnancy, or planned medical procedures we should be aware of? Also, could you let me know if you use tobacco products?'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: Thank you for confirming your medical history. Just one last area to cover: are there any major life events, like a pregnancy, or planned medical procedures we should be aware of? Also, could you let me know if you use tobacco products?\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:15:57,890 - INFO - Successfully updated profile with user's answer.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "==================== Turn 3 ====================\n", "Current Profile State:\n", "{\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\",\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": \"None reported.\",\n", " \"medications\": null,\n", " \"special_cases\": \"None reported.\"\n", "}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:16:00,166 - INFO - LLM returned next step: 'Thank you for confirming your medical history. Just one last area to cover: are there any major life events, planned medical procedures, or tobacco usage we should factor into your plan?'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: Thank you for confirming your medical history. Just one last area to cover: are there any major life events, planned medical procedures, or tobacco usage we should factor into your plan?\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:16:06,687 - INFO - Successfully updated profile with user's answer.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "==================== Turn 4 ====================\n", "Current Profile State:\n", "{\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\",\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": \"None reported.\",\n", " \"medications\": null,\n", " \"special_cases\": \"None reported.\"\n", "}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:16:09,695 - INFO - LLM returned next step: 'PROFILE_COMPLETE'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: PROFILE_COMPLETE\n", "\n", "--- Profile building complete! ---\n", "\n", "==================== FINAL PROFILE ====================\n", "{\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\",\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": \"None reported.\",\n", " \"medications\": null,\n", " \"special_cases\": \"None reported.\"\n", "}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:16:17,528 - INFO - Starting query transformation and retrieval for: 'quit'\n", "2025-07-09 18:16:25,004 - INFO - Successfully classified query. Intent: Concise (Step-Back)\n", "2025-07-09 18:16:25,007 - INFO - Query classified with intent: Concise (Step-Back). Reasoning: The query is a single word, 'quit', which completely lacks context within the domain of health insurance. It could be a command to end the conversation, or it could relate to quitting a plan, a job, or something else entirely. A step-back question is needed to understand the user's intent.\n", "2025-07-09 18:16:34,538 - INFO - Aggregated and merged chunks into 10 final documents.\n", "2025-07-09 18:16:34,540 - INFO - Retrieved 10 documents for query: 'quit'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[Document(metadata={'source_id': 91, 'source_url': 'https://www.healthcare.gov/privacy/', 'source_name': 'privacy', 'source_local_path': 'data/raw/source_91_privacy.html', 'merged_chunks_count': 1, 'original_chunk_numbers': [52]}, page_content=\". You're about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site. Learn more about links to third-party sites . Continue Cancel YouTube This link goes to an external site You are leaving HealthCare.gov. You're about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site. Learn more about links to third-party sites . Continue Cancel LinkedIn This link goes to an external site You are leaving HealthCare.gov. You're about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site. Learn more about links to third-party sites . Continue Cancel Instagram This link goes to an external site You are leaving HealthCare.gov. You're about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site. Learn more about links to third-party sites\"), Document(metadata={'source_id': 121, 'source_url': 'https://www.healthcare.gov/choose-a-plan/your-total-costs/', 'source_name': 'Your total costs for health care: Premium, deductible, and out-of ...', 'source_local_path': 'data/dynamic/https___www.healthcare.gov_choose-a-plan_your-total-costs_.html', 'merged_chunks_count': 1, 'original_chunk_numbers': [2]}, page_content='. Out-of-pocket limit: A weighed scale leaning less towards the customer, Jane, who is paying zero and more towards her plan who is paying 100%. ## Compare estimated total costs for plans [...] ## Compare plans marked with \"easy pricing\" ### What services count for day 1 coverage? ### Â ## Resources ## Connect with us ## You are leaving HealthCare.gov. You\\'re about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site. Learn more about links to third-party sites. ## You are leaving HealthCare.gov. You\\'re about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site.'), Document(metadata={'source_id': 277, 'source_url': 'https://www.healthcare.gov/preventive-care-women/', 'source_name': 'preventive-care-women', 'source_local_path': 'data/raw/source_277_preventive-care-women.html', 'merged_chunks_count': 1, 'original_chunk_numbers': [7]}, page_content=\". You're about to connect to a third-party site. Select CONTINUE to proceed or CANCEL to stay on this site. Learn more about links to third-party sites . Continue Cancel for women yearly Well-woman visits to get recommended services for all women More on prevention Learn more about preventive care from the CDC . See preventive services covered for all adults and children . Learn more about what else Marketplace health insurance plans cover. Back to top\"), Document(metadata={'source_id': 418, 'source_url': 'https://www.cms.gov/files/document/ffe-enrollment-manual-2023-5cr-071323.pdf', 'source_name': 'ffe-enrollment-manual-2023-5cr-071323.pdf', 'source_local_path': 'data/raw/source_418_files_document_ffe-enrollment-manual-2023-5cr-071323.pdf.pdf', 'merged_chunks_count': 1, 'original_chunk_numbers': [755]}, page_content='FFE Enrollment \\n182 \\n \\nSubscription List \\n(As of Publication) Description \\nRescission/Fraud FFE issuers that wish to cancel enrollments due to fraud must first get the \\nCMS rescission team’s concurrence, and information about the rescission \\nprocess may be shared from time to time via this subscription. CMS may \\nalso share information with issuer fraud contacts about changing trends.'), Document(metadata={'source_id': 414, 'source_url': 'https://www.cms.gov/files/document/ffeffshop-enrollment-manual-2022.pdf', 'source_name': 'ffeffshop-enrollment-manual-2022.pdf', 'source_local_path': 'data/raw/source_414_files_document_ffeffshop-enrollment-manual-2022.pdf.pdf', 'merged_chunks_count': 1, 'original_chunk_numbers': [491]}, page_content='effective date and reason for termination, to enrollees for all termination events. \\n En rollee Requested Terminations \\nIn accordance with 45 CFR 155.430(b)(1), enrollees have the right to terminate their coverage or \\nenrollment in a QHP/QDP through an Exchange. Enrollees in a QHP must request a voluntary \\ntermination of their coverage or enrollment through the FFE. Enrollees in a QDP, however, may \\ncontact the QDP issuer directly to request a voluntary termination of their coverage; the QDP issuer \\nthen notifies the FFE of the termination using Enrollment Data Alignment (EDA). According to 45 \\nCFR 155.430(d)(2), an enrollee who voluntarily terminates coverage or enrollment through the \\nExchanges, at the option of the Exchange, will be granted same-day or prospective coverage \\ntermination dates based on the date of their request. QHP issuers are encouraged to remind enrollees to \\nreport voluntary termination requests to the Exchange.'), Document(metadata={'source_id': 465, 'source_url': 'https://www.cms.gov/files/document/cms-9895-f-patient-protection-final.pdf', 'source_name': 'cms-9895-f-patient-protection-final.pdf', 'source_local_path': 'data/raw/source_465_files_document_cms-9895-f-patient-protection-final.pdf.pdf', 'merged_chunks_count': 1, 'original_chunk_numbers': [1971]}, page_content='also benefit by the expansion of entities and enrollment pathways available to assist with \\nenrolling in health insurance coverage. \\nWe sought comment on these estimated impacts and assumptions. \\nAfter consideration of comments and for the reasons outlined in the proposed rule and \\nour responses to comments, we are finalizing the burden estimates with modifications to the'), Document(metadata={'source_id': 464, 'source_url': 'https://www.cms.gov/files/document/cms-9895-p-patient-protection-final.pdf', 'source_name': 'cms-9895-p-patient-protection-final.pdf', 'source_local_path': 'data/raw/source_464_files_document_cms-9895-p-patient-protection-final.pdf.pdf', 'merged_chunks_count': 1, 'original_chunk_numbers': [788]}, page_content='select an EHB-benchmark plan with a scope of benefit requirement that tracks with such changes \\nto employer plans in the States, to the extent they exist. \\nWe continue to believe that this list of plans appropriately represents the scope of benefits \\nprovided under typical employer plans. Based on our research on how the scope of benefits in \\nemployer-sponsored or other job-based coverage has changed since 2014, which includes our \\nreview of the comments submitted in response to the EHB RFI, we believe that the scope of \\nbenefits in employer-sponsored or other job-based coverage has either remained the same or \\nincreased incrementally overall since 2014. To the extent it has increased in certain States or \\ncertain regions, we believe that the scope of benefits in employer-sponsored or other job-based \\ncoverage increasingly tends to provide coverage for telehealth services, gender-affirming care,'), Document(metadata={'source_id': 88, 'source_url': 'https://www.healthcare.gov/reporting-changes/which-changes-to-report/', 'source_name': 'which-changes-to-report', 'source_local_path': 'data/raw/source_88_reporting-changes_which-changes-to-report.html', 'merged_chunks_count': 1, 'original_chunk_numbers': [2]}, page_content='. Expected income change: Find out how to estimate your income. Health coverage change: Someone in your household: Got an offer of job-based insurance, even if they don’t enroll in it Got coverage from a public program like Medicaid, the Children’s Health Insurance Program (CHIP), or Medicare Loses coverage, like job-based coverage or Medicaid If someone in your household got job-based coverage (either through your employer or through a family member’s), you may want to end your Marketplace plan . Household or individual member change: Birth or adoption Place a child for adoption or foster care Become pregnant Marriage or divorce A child on your plan turns 26 Death Gain or lose a dependent some other way Move to a new permanent address in the same state Don’t update your application if you move to a different state . Learn what to do when you move out of state'), Document(metadata={'source_id': 127, 'source_url': 'https://www.healthcare.gov/how-to-cancel-a-marketplace-plan', 'source_name': 'how-to-cancel-a-marketplace-plan', 'source_local_path': 'data/raw/source_127_how-to-cancel-a-marketplace-plan.html', 'merged_chunks_count': 1, 'original_chunk_numbers': [2]}, page_content=\". Refer to glossary for more details. . Notice: Don't end your Marketplace plan until you know for sure when your new coverage starts to avoid a gap in coverage. Once you end Marketplace coverage, you can’t re-enroll until the next Open Enrollment Period (unless you qualify for a Special Enrollment Period A time outside the yearly Open Enrollment Period when you can sign up for health insurance. You qualify for a Special Enrollment Period if you’ve had certain life events, including losing health coverage, moving, getting married, having a baby, or adopting a child, or if your household income is below a certain amount. Refer to glossary for more details. ). What are the risks if I drop all health coverage? Risks if you drop all health coverage Close If you don’t want health coverage, think about these items before you cancel your Marketplace plan: Once you cancel your coverage, you might have to wait for the next Open Enrollment Period to enroll again\"), Document(metadata={'source_id': 584, 'source_url': 'https://www.cms.gov/files/document/2025-benefit-year-discontinuation-notices-safe-harbor.pdf', 'source_name': '2025-benefit-year-discontinuation-notices-safe-harbor.pdf', 'source_local_path': 'data/raw/source_584_files_document_2025-benefit-year-discontinuation-notices-safe-harbor.pdf.pdf', 'merged_chunks_count': 1, 'original_chunk_numbers': [2]}, page_content='is to inform consumers that their current health coverage is being terminated and that they have \\nother health coverage options. \\n \\nDue to the timing of qualified health plan (QHP) certification for each of the 2015 through 2024 \\nbenefit years, issuers were in many instances unable to finalize their plan offerings until closer to \\nthe start of the annual open enrollment period, after the deadline to meet the 90-day \\ndiscontinuation notice requirement. This meant consumers could potentially receive product \\ndiscontinuation notices without being able to take prompt action to shop for new coverage, and \\nissuers would not have been able to suggest replacement coverage options, as explicitly \\nenvisioned by the discontinuation notices. Therefore, in connection with the open enrollment \\nperiod for coverage in each of these benefit years, the Centers for Medicare & Medicaid Services \\n(CMS) announced that it would not take enforcement action against an issuer failing to meet the')]\n", "\n", "Testing with relevant docs for question: '{'zip_code': '30303', 'county': 'Fulton', 'state': 'Georgia', 'age': 34, 'gender': 'Female', 'household_size': 1, 'income': 65000, 'employment_status': 'employed_without_coverage', 'citizenship': 'US Citizen', 'medical_history': 'None reported.', 'medications': None, 'special_cases': 'None reported.'} is my complete profile, answer the question: quit'\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:16:34,920 - WARNING - GRADE: Documents are NOT RELEVANT.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " - Are docs relevant? -> False\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 18:16:39,079 - INFO - Formulated search query: 'Health insurance options after quitting job COBRA ACA special enrollment period'\n", "2025-07-09 18:16:39,081 - INFO - Performing web search with Tavily for query: 'Health insurance options after quitting job COBRA ACA special enrollment period'\n", "2025-07-09 18:16:42,650 - INFO - Saved web content from https://www.healthinsurance.org/special-enrollment-guide/involuntary-loss-of-coverage-is-a-qualifying-event/ to data/dynamic/https___www.healthinsurance.org_special-enrollment-guide_involuntary-loss-of-coverage-is-a-qualifying-event_.html\n", "2025-07-09 18:16:42,653 - INFO - Saved web content from https://www.healthcare.gov/have-job-based-coverage/if-you-lose-job-based-coverage/ to data/dynamic/https___www.healthcare.gov_have-job-based-coverage_if-you-lose-job-based-coverage_.html\n", "2025-07-09 18:16:42,655 - INFO - Saved web content from https://www.healthcare.gov/unemployed/cobra-coverage/ to data/dynamic/https___www.healthcare.gov_unemployed_cobra-coverage_.html\n", "2025-07-09 18:16:42,656 - INFO - Saved web content from https://www.cobrainsurance.com/kb/can-i-get-cobra-if-i-quit/ to data/dynamic/https___www.cobrainsurance.com_kb_can-i-get-cobra-if-i-quit_.html\n", "2025-07-09 18:16:42,657 - INFO - Saved web content from https://www.dol.gov/general/topic/health-plans/cobra to data/dynamic/https___www.dol.gov_general_topic_health-plans_cobra.html\n", "2025-07-09 18:16:42,658 - INFO - Found and saved 5 documents from the web.\n", "2025-07-09 18:16:42,658 - INFO - Starting dynamic ingestion of 5 documents...\n", "2025-07-09 18:16:42,664 - INFO - Registering new dynamic web source: https://www.healthinsurance.org/special-enrollment-guide/involuntary-loss-of-coverage-is-a-qualifying-event/\n", "2025-07-09 18:16:42,679 - INFO - Created 2 chunks for source_id None\n", "2025-07-09 18:16:42,681 - INFO - Created 2 chunks for source_id None\n", "2025-07-09 18:16:42,684 - INFO - Created 2 chunks for source_id None\n", "2025-07-09 18:16:42,685 - INFO - Registering new dynamic web source: https://www.cobrainsurance.com/kb/can-i-get-cobra-if-i-quit/\n", "2025-07-09 18:16:42,687 - INFO - Created 2 chunks for source_id None\n", "2025-07-09 18:16:42,688 - INFO - Registering new dynamic web source: https://www.dol.gov/general/topic/health-plans/cobra\n", "2025-07-09 18:16:42,690 - INFO - Created 2 chunks for source_id None\n", "2025-07-09 18:16:42,690 - INFO - Embedding and storing 10 new chunks in ChromaDB.\n", "2025-07-09 18:16:42,690 - INFO - Adding 10 documents to the vector store...\n", "2025-07-09 18:16:43,012 - INFO - Successfully added 10 documents.\n", "2025-07-09 18:16:43,012 - INFO - Dynamic ingestion completed successfully.\n", "2025-07-09 18:16:43,012 - INFO - Generating final conversational response with AdvisorAgent...\n", "2025-07-09 18:16:47,420 - INFO - Successfully generated final conversational answer.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Of course. Before you go, I just want to make sure—are you certain you don't have any other questions for me today? I'm here to help if anything else comes to mind.\n" ] } ], "source": [ "import json\n", "import logging\n", "from typing import Optional, Dict, Any, List\n", "from langchain.docstore.document import Document\n", "\n", "from insucompass.core.agents.profile_agent import profile_builder\n", "from insucompass.core.agents.query_trasformer import QueryTransformationAgent\n", "from insucompass.core.agents.router_agent import router\n", "from insucompass.services.ingestion_service import IngestionService\n", "from insucompass.core.agents.search_agent import searcher\n", "from insucompass.core.agents.advisor_agent import advisor\n", "\n", "\n", "from insucompass.services import llm_provider\n", "from insucompass.prompts.prompt_loader import load_prompt\n", "from insucompass.services.vector_store import vector_store_service\n", "\n", "llm = llm_provider.get_gemini_llm()\n", "retriever = vector_store_service.get_retriever()\n", "transformer = QueryTransformationAgent(llm, retriever)\n", "ingestor = IngestionService()\n", "\n", "# Configure logging\n", "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n", "logger = logging.getLogger(__name__)\n", "\n", "# 2. Set up the initial state\n", "# This is the profile *after* the user has entered their basic info in the UI.\n", "user_profile = {\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\", # Change to \"Male\" to test the other logic path\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": None,\n", " \"medications\": None,\n", " \"special_cases\": None\n", "}\n", "\n", "# 3. Start the conversation loop\n", "last_question = \"\"\n", "turn_count = 0\n", "while turn_count < 10: # Safety break\n", " turn_count += 1\n", " print(\"\\n\" + \"=\"*20 + f\" Turn {turn_count} \" + \"=\"*20)\n", " print(\"Current Profile State:\")\n", " print(json.dumps(user_profile, indent=2))\n", "\n", " # Get the next question based on the current profile state\n", " next_question = profile_builder.get_next_question(user_profile)\n", " print(f\"\\nInsuCompass Agent: {next_question}\")\n", "\n", " if next_question == \"PROFILE_COMPLETE\":\n", " print(\"\\n--- Profile building complete! ---\")\n", " break\n", " \n", " # Store the question we just asked so we can provide it as context for the update\n", " last_question = next_question\n", " \n", " # Get live input from the person testing the script\n", " user_answer = input(\"Your Answer > \")\n", " if user_answer.lower() == 'quit':\n", " break\n", " \n", " # Use the updater method to get the new profile state\n", " user_profile = profile_builder.update_profile_with_answer(\n", " current_profile=user_profile,\n", " last_question=last_question,\n", " user_answer=user_answer\n", " )\n", "\n", "print(\"\\n\" + \"=\"*20 + \" FINAL PROFILE \" + \"=\"*20)\n", "print(json.dumps(user_profile, indent=2))\n", "\n", "# Test Case 1: Relevant documents\n", "query = input(\"How can I help you?\")\n", "\n", "query_with_profile = f\"{user_profile} is my complete profile, answer the question: {query}\"\n", "\n", "retrieved_docs = transformer.transform_and_retrieve(query)\n", "\n", "print(retrieved_docs)\n", "\n", "print(f\"\\nTesting with relevant docs for question: '{query_with_profile}'\")\n", "is_relevant = router.grade_documents(query_with_profile, retrieved_docs)\n", "print(f\" - Are docs relevant? -> {is_relevant}\")\n", "\n", "if is_relevant:\n", " final_answer = advisor.generate_response(query, user_profile, retrieved_docs)\n", " print(final_answer)\n", "else:\n", " result_docs = searcher.search(query)\n", " if result_docs:\n", " ingestor.ingest_documents(result_docs)\n", " final_answer = advisor.generate_response(query, user_profile, result_docs)\n", " print(final_answer)\n", " else:\n", " print(\"Error...Exit..\")" ] }, { "cell_type": "code", "execution_count": null, "id": "ef3253d1", "metadata": {}, "outputs": [], "source": [ "import logging\n", "import json\n", "import sqlite3\n", "from typing import List, Dict, Any\n", "from typing_extensions import TypedDict\n", "\n", "from langchain_core.documents import Document\n", "from langgraph.graph import StateGraph, END\n", "from langgraph.checkpoint.sqlite import SqliteSaver\n", "\n", "# Import all our custom agent and service classes\n", "from insucompass.core.agents.profile_agent import profile_builder\n", "from insucompass.core.agents.query_trasformer import QueryTransformationAgent\n", "from insucompass.core.agents.router_agent import router\n", "from insucompass.services.ingestion_service import IngestionService\n", "from insucompass.core.agents.search_agent import searcher\n", "from insucompass.core.agents.advisor_agent import advisor\n", "\n", "from insucompass.services import llm_provider\n", "from insucompass.prompts.prompt_loader import load_prompt\n", "from insucompass.services.vector_store import vector_store_service\n", "\n", "llm = llm_provider.get_gemini_llm()\n", "retriever = vector_store_service.get_retriever()\n", "transformer = QueryTransformationAgent(llm, retriever)\n", "ingestor = IngestionService()\n", "\n", "# Configure logging\n", "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n", "logger = logging.getLogger(__name__)\n", "\n", "# --- LangGraph State Definition ---\n", "class AgentState(TypedDict):\n", " \"\"\"\n", " Represents the state of our Q&A graph. This state is passed between nodes.\n", " \"\"\"\n", " user_profile: Dict[str, Any]\n", " question: str\n", " contextual_question: str\n", " documents: List[Document]\n", " conversation_history: List[str]\n", " generation: str\n", " is_relevant: bool\n", "\n", "def reformulate_query_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"\n", " Node 0 (New): Reformulate the user's question to be self-contained.\n", " \"\"\"\n", " logger.info(\"---NODE: REFORMULATE QUERY---\")\n", " question = state[\"question\"]\n", " history = state[\"conversation_history\"]\n", " user_profile = state[\"user_profile\"]\n", "\n", " prompt = load_prompt(\"query_reformulator\")\n", " history_str = \"\\n\".join(history)\n", " \n", " # if not history:\n", " # # If there's no history, the question is already standalone\n", " # return {\"contextual_question\": question}\n", " \n", " full_prompt = (\n", " f\"{prompt}\\n\\n\"\n", " f\"### User Profile:\\n{str(user_profile)}\\n\\n\"\n", " f\"### Conversation History:\\n{history_str}\\n\\n\"\n", " f\"### Follow-up Question:\\n{question}\"\n", " )\n", " \n", " response = llm.invoke(full_prompt)\n", " contextual_question = response.content.strip()\n", " logger.info(f\"Reformulated question: '{contextual_question}'\")\n", " return {\"contextual_question\": contextual_question}\n", "\n", "# --- Agent Nodes for the Graph ---\n", "\n", "def transform_query_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 1: Transform the user's query and retrieve initial documents.\"\"\"\n", " logger.info(\"---NODE: TRANSFORM QUERY & RETRIEVE---\")\n", " question = state[\"contextual_question\"]\n", " documents = transformer.transform_and_retrieve(question)\n", " return {\"documents\": documents}\n", "\n", "def route_documents_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 2: Grade the retrieved documents to decide if a web search is needed.\"\"\"\n", " logger.info(\"---NODE: ROUTE DOCUMENTS---\")\n", " question = state[\"question\"]\n", " documents = state[\"documents\"]\n", " is_relevant = router.grade_documents(question, documents)\n", " return {\"is_relevant\": is_relevant}\n", "\n", "def search_and_ingest_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 3 (Fallback Path): Search the web and ingest new information.\"\"\"\n", " logger.info(\"---NODE: SEARCH & INGEST---\")\n", " question = state[\"question\"]\n", " web_documents = searcher.search(question)\n", " if web_documents:\n", " ingestor.ingest_documents(web_documents)\n", " return {}\n", "\n", "def generate_answer_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 4: Generate the final, conversational answer.\"\"\"\n", " logger.info(\"---NODE: GENERATE ADVISOR RESPONSE---\")\n", " question = state[\"question\"]\n", " user_profile = state[\"user_profile\"]\n", " documents = state[\"documents\"]\n", " generation = advisor.generate_response(question, user_profile, documents)\n", " \n", " history = state.get(\"conversation_history\", [])\n", " history.append(f\"User: {question}\")\n", " history.append(f\"Agent: {generation}\")\n", " \n", " return {\"generation\": generation, \"conversation_history\": history}\n", "\n", "# --- Conditional Edge Logic ---\n", "def should_search_web(state: AgentState) -> str:\n", " \"\"\"The conditional edge that directs the graph's flow.\"\"\"\n", " logger.info(\"---ROUTING: Evaluating document relevance---\")\n", " if state[\"is_relevant\"]:\n", " logger.info(\">>> Route: Documents are relevant. Proceeding to generate answer.\")\n", " return \"generate\"\n", " else:\n", " logger.info(\">>> Route: Documents are NOT relevant. Proceeding to web search.\")\n", " return \"search\"\n", "\n", "# --- Build and Compile the Graph ---\n", "db_connection = sqlite3.connect(\"data/checkpoints.db\", check_same_thread=False)\n", "memory = SqliteSaver(db_connection)\n", "\n", "builder = StateGraph(AgentState)\n", "\n", "builder.add_node(\"reformulate_query\", reformulate_query_node)\n", "builder.add_node(\"transform_query\", transform_query_node)\n", "builder.add_node(\"route_documents\", route_documents_node)\n", "builder.add_node(\"search_and_ingest\", search_and_ingest_node)\n", "builder.add_node(\"generate_answer\", generate_answer_node)\n", "\n", "builder.set_entry_point(\"reformulate_query\")\n", "builder.add_edge(\"reformulate_query\", \"transform_query\")\n", "builder.add_edge(\"transform_query\", \"route_documents\")\n", "builder.add_conditional_edges(\n", " \"route_documents\",\n", " should_search_web,\n", " {\"search\": \"search_and_ingest\", \"generate\": \"generate_answer\"},\n", ")\n", "builder.add_edge(\"search_and_ingest\", \"generate_answer\")\n", "builder.add_edge(\"generate_answer\", END)\n", "\n", "app = builder.compile(checkpointer=memory)\n", "\n", "# --- Interactive Test Harness (Updated to manage state correctly) ---\n", "if __name__ == '__main__':\n", " print(\"--- InsuCompass AI Orchestrator ---\")\n", " \n", " # --- Phase 1: Profile Building (This part remains the same) ---\n", " print(\"Phase 1: Building your health profile...\")\n", " user_profile = {\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\", # Change to \"Male\" to test the other logic path\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": None,\n", " \"medications\": None,\n", " \"special_cases\": None\n", " }\n", " profile_conversation_history = []\n", " for turn in range(10):\n", " question = profile_builder.get_next_question(user_profile)\n", " if question == \"PROFILE_COMPLETE\": break\n", " print(f\"\\nInsuCompass Agent: {question}\")\n", " profile_conversation_history.append(f\"Agent: {question}\")\n", " user_answer = input(\"Your Answer > \")\n", " if user_answer.lower() == 'quit': exit()\n", " profile_conversation_history.append(f\"User: {user_answer}\")\n", " user_profile = profile_builder.update_profile_with_answer(\n", " user_profile, question, user_answer\n", " )\n", " print(\"\\n--- Profile building complete! ---\")\n", " print(\"\\nFinal Profile:\", json.dumps(user_profile, indent=2))\n", " \n", " # --- Phase 2: Q&A Session ---\n", " print(\"\\n\" + \"#\"*20 + \" Q&A Session \" + \"#\"*20)\n", " print(\"Your profile is complete. You can now ask me any questions.\")\n", " \n", " thread_config = {\"configurable\": {\"thread_id\": \"user-123-session-1\"}}\n", " qna_history = []\n", "\n", " while True:\n", " user_question = input(\"\\nYour Question > \")\n", " if user_question.lower() == 'quit': break\n", " \n", " inputs = {\n", " \"question\": user_question,\n", " \"user_profile\": user_profile,\n", " \"conversation_history\": qna_history # Pass the current history\n", " }\n", " \n", " print(\"\\n--- InsuCompass is thinking... ---\")\n", " final_state = {}\n", " for s in app.stream(inputs, config=thread_config):\n", " node_name = list(s.keys())[0]\n", " print(f\"--- Executed Node: {node_name} ---\")\n", " final_state.update(s)\n", "\n", " final_answer = final_state.get(\"generate_answer\", {}).get(\"generation\", \"Sorry, I couldn't generate a response.\")\n", " \n", " print(\"\\n\" + \"=\"*20 + \" FINAL ANSWER \" + \"=\"*20)\n", " print(final_answer)\n", " \n", " qna_history = final_state.get(\"generate_answer\", {}).get(\"conversation_history\", qna_history)" ] }, { "cell_type": "code", "execution_count": null, "id": "d51cd66d", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "id": "6a0db72a", "metadata": {}, "outputs": [], "source": [ "import logging\n", "import json\n", "import sqlite3\n", "from typing import List, Dict, Any\n", "from typing_extensions import TypedDict\n", "\n", "from langchain_core.documents import Document\n", "from langgraph.graph import StateGraph, END\n", "from langgraph.checkpoint.sqlite import SqliteSaver\n", "\n", "# Import all our custom agent and service classes\n", "from insucompass.core.agents.profile_agent import profile_builder\n", "from insucompass.core.agents.query_trasformer import QueryTransformationAgent\n", "from insucompass.core.agents.router_agent import router\n", "from insucompass.services.ingestion_service import IngestionService\n", "from insucompass.core.agents.search_agent import searcher\n", "from insucompass.core.agents.advisor_agent import advisor\n", "\n", "from insucompass.services import llm_provider\n", "from insucompass.prompts.prompt_loader import load_prompt\n", "from insucompass.services.vector_store import vector_store_service\n", "\n", "llm = llm_provider.get_gemini_llm()\n", "retriever = vector_store_service.get_retriever()\n", "transformer = QueryTransformationAgent(llm, retriever)\n", "ingestor = IngestionService()\n", "\n", "# Configure logging\n", "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n", "logger = logging.getLogger(__name__)\n", "\n", "# --- LangGraph State Definition ---\n", "class AgentState(TypedDict):\n", " \"\"\"\n", " Represents the state of our Q&A graph. This state is passed between nodes.\n", " \"\"\"\n", " user_profile: Dict[str, Any]\n", " question: str\n", " contextual_question: str\n", " documents: List[Document]\n", " conversation_history: List[str]\n", " generation: str\n", " is_relevant: bool\n", "\n", "def reformulate_query_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"\n", " Node 0 (New): Reformulate the user's question to be self-contained.\n", " \"\"\"\n", " logger.info(\"---NODE: REFORMULATE QUERY---\")\n", " question = state[\"question\"]\n", " history = state[\"conversation_history\"]\n", " user_profile = state[\"user_profile\"]\n", "\n", " prompt = load_prompt(\"query_reformulator\")\n", " history_str = \"\\n\".join(history)\n", " \n", " # if not history:\n", " # # If there's no history, the question is already standalone\n", " # return {\"contextual_question\": question}\n", " \n", " full_prompt = (\n", " f\"{prompt}\\n\\n\"\n", " f\"### User Profile:\\n{str(user_profile)}\\n\\n\"\n", " f\"### Conversation History:\\n{history_str}\\n\\n\"\n", " f\"### Follow-up Question:\\n{question}\"\n", " )\n", " \n", " response = llm.invoke(full_prompt)\n", " contextual_question = response.content.strip()\n", " logger.info(f\"Reformulated question: '{contextual_question}'\")\n", " return {\"contextual_question\": contextual_question}\n", "\n", "# --- Agent Nodes for the Graph ---\n", "\n", "def transform_query_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 1: Transform the user's query and retrieve initial documents.\"\"\"\n", " logger.info(\"---NODE: TRANSFORM QUERY & RETRIEVE---\")\n", " question = state[\"contextual_question\"]\n", " documents = transformer.transform_and_retrieve(question)\n", " return {\"documents\": documents}\n", "\n", "def route_documents_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 2: Grade the retrieved documents to decide if a web search is needed.\"\"\"\n", " logger.info(\"---NODE: ROUTE DOCUMENTS---\")\n", " question = state[\"question\"]\n", " documents = state[\"documents\"]\n", " is_relevant = router.grade_documents(question, documents)\n", " return {\"is_relevant\": is_relevant}\n", "\n", "def search_and_ingest_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 3 (Fallback Path): Search the web and ingest new information.\"\"\"\n", " logger.info(\"---NODE: SEARCH & INGEST---\")\n", " question = state[\"question\"]\n", " web_documents = searcher.search(question)\n", " if web_documents:\n", " ingestor.ingest_documents(web_documents)\n", " return {}\n", "\n", "def generate_answer_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Node 4: Generate the final, conversational answer.\"\"\"\n", " logger.info(\"---NODE: GENERATE ADVISOR RESPONSE---\")\n", " question = state[\"question\"]\n", " user_profile = state[\"user_profile\"]\n", " documents = state[\"documents\"]\n", " generation = advisor.generate_response(question, user_profile, documents)\n", " \n", " history = state.get(\"conversation_history\", [])\n", " history.append(f\"User: {question}\")\n", " history.append(f\"Agent: {generation}\")\n", " \n", " return {\"generation\": generation, \"conversation_history\": history}\n", "\n", "# --- Conditional Edge Logic ---\n", "def should_search_web(state: AgentState) -> str:\n", " \"\"\"The conditional edge that directs the graph's flow.\"\"\"\n", " logger.info(\"---ROUTING: Evaluating document relevance---\")\n", " if state[\"is_relevant\"]:\n", " logger.info(\">>> Route: Documents are relevant. Proceeding to generate answer.\")\n", " return \"generate\"\n", " else:\n", " logger.info(\">>> Route: Documents are NOT relevant. Proceeding to web search.\")\n", " return \"search\"\n", "\n", "# --- Build and Compile the Graph ---\n", "db_connection = sqlite3.connect(\"data/checkpoints.db\", check_same_thread=False)\n", "memory = SqliteSaver(db_connection)\n", "\n", "builder = StateGraph(AgentState)\n", "\n", "builder.add_node(\"reformulate_query\", reformulate_query_node)\n", "builder.add_node(\"transform_query\", transform_query_node)\n", "builder.add_node(\"route_documents\", route_documents_node)\n", "builder.add_node(\"search_and_ingest\", search_and_ingest_node)\n", "builder.add_node(\"generate_answer\", generate_answer_node)\n", "\n", "builder.set_entry_point(\"reformulate_query\")\n", "builder.add_edge(\"reformulate_query\", \"transform_query\")\n", "builder.add_edge(\"transform_query\", \"route_documents\")\n", "builder.add_conditional_edges(\n", " \"route_documents\",\n", " should_search_web,\n", " {\"search\": \"search_and_ingest\", \"generate\": \"generate_answer\"},\n", ")\n", "builder.add_edge(\"search_and_ingest\", \"generate_answer\")\n", "builder.add_edge(\"generate_answer\", END)\n", "\n", "app = builder.compile(checkpointer=memory)\n", "\n", "# --- Interactive Test Harness (Updated to manage state correctly) ---\n", "if __name__ == '__main__':\n", " print(\"--- InsuCompass AI Orchestrator ---\")\n", " \n", " # --- Phase 1: Profile Building (This part remains the same) ---\n", " print(\"Phase 1: Building your health profile...\")\n", " user_profile = {\n", " \"zip_code\": \"30303\",\n", " \"county\": \"Fulton\",\n", " \"state\": \"Georgia\",\n", " \"age\": 34,\n", " \"gender\": \"Female\", # Change to \"Male\" to test the other logic path\n", " \"household_size\": 1,\n", " \"income\": 65000,\n", " \"employment_status\": \"employed_without_coverage\",\n", " \"citizenship\": \"US Citizen\",\n", " \"medical_history\": None,\n", " \"medications\": None,\n", " \"special_cases\": None\n", " }\n", " profile_conversation_history = []\n", " for turn in range(10):\n", " question = profile_builder.get_next_question(user_profile)\n", " if question == \"PROFILE_COMPLETE\": break\n", " print(f\"\\nInsuCompass Agent: {question}\")\n", " profile_conversation_history.append(f\"Agent: {question}\")\n", " user_answer = input(\"Your Answer > \")\n", " if user_answer.lower() == 'quit': exit()\n", " profile_conversation_history.append(f\"User: {user_answer}\")\n", " user_profile = profile_builder.update_profile_with_answer(\n", " user_profile, question, user_answer\n", " )\n", " print(\"\\n--- Profile building complete! ---\")\n", " print(\"\\nFinal Profile:\", json.dumps(user_profile, indent=2))\n", " \n", " # --- Phase 2: Q&A Session ---\n", " print(\"\\n\" + \"#\"*20 + \" Q&A Session \" + \"#\"*20)\n", " print(\"Your profile is complete. You can now ask me any questions.\")\n", " \n", " thread_config = {\"configurable\": {\"thread_id\": \"user-123-session-1\"}}\n", " qna_history = []\n", "\n", " while True:\n", " user_question = input(\"\\nYour Question > \")\n", " if user_question.lower() == 'quit': break\n", " \n", " inputs = {\n", " \"question\": user_question,\n", " \"user_profile\": user_profile,\n", " \"conversation_history\": qna_history # Pass the current history\n", " }\n", " \n", " print(\"\\n--- InsuCompass is thinking... ---\")\n", " final_state = {}\n", " for s in app.stream(inputs, config=thread_config):\n", " node_name = list(s.keys())[0]\n", " print(f\"--- Executed Node: {node_name} ---\")\n", " final_state.update(s)\n", "\n", " final_answer = final_state.get(\"generate_answer\", {}).get(\"generation\", \"Sorry, I couldn't generate a response.\")\n", " \n", " print(\"\\n\" + \"=\"*20 + \" FINAL ANSWER \" + \"=\"*20)\n", " print(final_answer)\n", " \n", " qna_history = final_state.get(\"generate_answer\", {}).get(\"conversation_history\", qna_history)" ] }, { "cell_type": "code", "execution_count": null, "id": "98a4c436", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "id": "2a374452", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 1, "id": "def827af", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 19:09:10,854 - INFO - Initialized LLM Provider: gemini-2.5-flash\n", "2025-07-09 19:09:10,854 - INFO - Loading prompt 'profile_agent' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/profile_agent.txt\n", "2025-07-09 19:09:10,855 - INFO - Loading prompt 'profile_updater' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/profile_updater.txt\n", "2025-07-09 19:09:10,856 - INFO - ProfileBuilder initialized successfully with all prompts.\n", "2025-07-09 19:09:10,867 - INFO - Loading prompt 'query_intent_classifier' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/query_intent_classifier.txt\n", "2025-07-09 19:09:10,868 - INFO - Loading prompt 'query_transformer' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/query_transformer.txt\n", "/var/folders/cv/flgh8s7960bc7q380pyn0c6m0000gn/T/ipykernel_41603/1771970172.py:15: LangChainDeprecationWarning: As of langchain-core 0.3.0, LangChain uses pydantic v2 internally. The langchain_core.pydantic_v1 module was a compatibility shim for pydantic v1, and should no longer be used. Please update the code to import from Pydantic directly.\n", "\n", "For example, replace imports like: `from langchain_core.pydantic_v1 import BaseModel`\n", "with: `from pydantic import BaseModel`\n", "or the v1 compatibility namespace if you are working in a code base that has not been fully upgraded to pydantic 2 yet. \tfrom pydantic.v1 import BaseModel\n", "\n", " from insucompass.core.agents.router_agent import router\n", "2025-07-09 19:09:10,872 - INFO - Initialized LLM Provider: gemini-2.5-flash-lite-preview-06-17\n", "2025-07-09 19:09:10,873 - INFO - Loading prompt 'document_grader' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/document_grader.txt\n", "2025-07-09 19:09:10,874 - INFO - RouterAgent (Document Grader) initialized successfully.\n", "2025-07-09 19:09:11,217 - INFO - Anonymized telemetry enabled. See https://docs.trychroma.com/telemetry for more information.\n", "2025-07-09 19:09:11,282 - INFO - Loading embedding model: sentence-transformers/all-MiniLM-L6-v2\n", "/Users/nagurshareefshaik/Desktop/InsuCompass-AI/ic_venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", " from .autonotebook import tqdm as notebook_tqdm\n", "2025-07-09 19:09:15,208 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2\n", "2025-07-09 19:09:17,878 - INFO - ChromaDB service initialized. Collection 'insucompass_kb' at data/vector_store\n", "2025-07-09 19:09:18,008 - INFO - Initialized LLM Provider: gemini-2.5-flash\n", "2025-07-09 19:09:18,008 - INFO - Loading prompt 'search_agent' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/search_agent.txt\n", "2025-07-09 19:09:18,009 - INFO - SearchAgent initialized successfully.\n", "2025-07-09 19:09:18,011 - INFO - Initialized LLM Provider: gemini-2.5-flash\n", "2025-07-09 19:09:18,011 - INFO - Loading prompt 'advisor_agent' from: /Users/nagurshareefshaik/Desktop/InsuCompass-AI/insucompass/prompts/advisor_agent.txt\n", "2025-07-09 19:09:18,012 - INFO - Conversational AdvisorAgent initialized successfully.\n", "2025-07-09 19:09:18,013 - INFO - Initialized LLM Provider: gemini-2.5-flash\n", "2025-07-09 19:09:18,014 - INFO - QueryIntentClassifierAgent initialized successfully.\n", "2025-07-09 19:09:18,014 - INFO - QueryTransformationAgent initialized successfully.\n", "2025-07-09 19:09:18,040 - INFO - ---ROUTING: ENTRY POINT---\n", "2025-07-09 19:09:18,041 - INFO - >>> Route: Profile is not complete. Starting Profile Builder.\n", "2025-07-09 19:09:18,041 - INFO - ---NODE: PROFILE BUILDER---\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "--- InsuCompass AI Unified Orchestrator Interactive Test ---\n", "Type 'quit' at any time to exit.\n", "Using conversation thread_id: interactive-test-89478936-0bc5-4b80-a549-840c5268e536\n", "\n", "==================== INVOKING GRAPH ====================\n", "Sending message: 'START_PROFILE_BUILDING'\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 19:09:19,918 - INFO - LLM returned next step: '\"Thank you for sharing those details! Now, let's gently move on to your health history. Could you please tell me about any significant medical conditions, diagnoses, or chronic illnesses you've experienced? There's no need to go into extreme detail, just the main points that might be relevant for your health plan.\"'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: \"Thank you for sharing those details! Now, let's gently move on to your health history. Could you please tell me about any significant medical conditions, diagnoses, or chronic illnesses you've experienced? There's no need to go into extreme detail, just the main points that might be relevant for your health plan.\"\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 19:09:35,799 - INFO - ---ROUTING: ENTRY POINT---\n", "2025-07-09 19:09:35,800 - INFO - >>> Route: Profile is not complete. Starting Profile Builder.\n", "2025-07-09 19:09:35,803 - INFO - ---NODE: PROFILE BUILDER---\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "==================== INVOKING GRAPH ====================\n", "Sending message: 'Nothing like that'\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 19:09:37,173 - INFO - Successfully updated profile with user's answer.\n", "2025-07-09 19:09:39,073 - INFO - LLM returned next step: 'Thank you for confirming your medical history. Just one last area to cover: are there any major life events, planned medical procedures, or tobacco usage we should factor into your plan?'\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: Thank you for confirming your medical history. Just one last area to cover: are there any major life events, planned medical procedures, or tobacco usage we should factor into your plan?\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 19:09:45,074 - INFO - ---ROUTING: ENTRY POINT---\n", "2025-07-09 19:09:45,075 - INFO - >>> Route: Profile is not complete. Starting Profile Builder.\n", "2025-07-09 19:09:45,076 - INFO - ---NODE: PROFILE BUILDER---\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "==================== INVOKING GRAPH ====================\n", "Sending message: 'Nope'\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "2025-07-09 19:09:46,483 - INFO - Successfully updated profile with user's answer.\n", "2025-07-09 19:09:50,978 - INFO - LLM returned next step: 'PROFILE_COMPLETE'\n", "2025-07-09 19:09:50,980 - INFO - Profile building complete.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "InsuCompass Agent: Great! Your profile is complete. How can I help you with your health insurance questions?\n", "Exiting test.\n" ] } ], "source": [ "import logging\n", "import json\n", "import uuid\n", "import sqlite3\n", "from typing import List, Dict, Any\n", "from typing_extensions import TypedDict\n", "\n", "from langchain_core.documents import Document\n", "from langgraph.graph import StateGraph, END\n", "from langgraph.checkpoint.sqlite import SqliteSaver\n", "\n", "# Import all our custom agent and service classes\n", "from insucompass.core.agents.profile_agent import profile_builder\n", "from insucompass.core.agents.query_trasformer import QueryTransformationAgent\n", "from insucompass.core.agents.router_agent import router\n", "from insucompass.services.ingestion_service import IngestionService\n", "from insucompass.core.agents.search_agent import searcher\n", "from insucompass.core.agents.advisor_agent import advisor\n", "\n", "from insucompass.services import llm_provider\n", "from insucompass.prompts.prompt_loader import load_prompt\n", "from insucompass.services.vector_store import vector_store_service\n", "\n", "llm = llm_provider.get_gemini_llm()\n", "retriever = vector_store_service.get_retriever()\n", "transformer = QueryTransformationAgent(llm, retriever)\n", "ingestor = IngestionService()\n", "\n", "# Configure logging\n", "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n", "logger = logging.getLogger(__name__)\n", "\n", "# --- Unified LangGraph State Definition ---\n", "class AgentState(TypedDict):\n", " user_profile: Dict[str, Any]\n", " user_message: str\n", " conversation_history: List[str]\n", " is_profile_complete: bool\n", " # Q&A specific fields\n", " standalone_question: str\n", " documents: List[Document]\n", " is_relevant: bool\n", " generation: str\n", "\n", "# --- Graph Nodes ---\n", "\n", "def profile_builder_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"A single turn of the profile building conversation.\"\"\"\n", " logger.info(\"---NODE: PROFILE BUILDER---\")\n", " profile = state[\"user_profile\"]\n", " message = state[\"user_message\"]\n", " history = state.get(\"conversation_history\", [])\n", "\n", " if message == \"START_PROFILE_BUILDING\":\n", " agent_response = profile_builder.get_next_question(profile, [])\n", " new_history = [f\"Agent: {agent_response}\"]\n", " return {\"conversation_history\": new_history, \"generation\": agent_response, \"user_profile\": profile, \"is_profile_complete\": False}\n", "\n", " last_question = history[-1][len(\"Agent: \"):] if history and history[-1].startswith(\"Agent:\") else \"\"\n", " updated_profile = profile_builder.update_profile_with_answer(profile, last_question, message)\n", " agent_response = profile_builder.get_next_question(updated_profile, history + [f\"User: {message}\"])\n", " \n", " new_history = history + [f\"User: {message}\", f\"Agent: {agent_response}\"]\n", " \n", " if agent_response == \"PROFILE_COMPLETE\":\n", " logger.info(\"Profile building complete.\")\n", " final_message = \"Great! Your profile is complete. How can I help you with your health insurance questions?\"\n", " new_history[-1] = f\"Agent: {final_message}\" # Replace \"PROFILE_COMPLETE\"\n", " return {\"user_profile\": updated_profile, \"is_profile_complete\": True, \"conversation_history\": new_history, \"generation\": final_message}\n", " \n", " return {\"user_profile\": updated_profile, \"is_profile_complete\": False, \"conversation_history\": new_history, \"generation\": agent_response}\n", "\n", "def reformulate_query_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Reformulates the user's question to be self-contained.\"\"\"\n", " logger.info(\"---NODE: REFORMULATE QUERY---\")\n", " question = state[\"user_message\"]\n", " history = state[\"conversation_history\"]\n", " user_profile = state[\"user_profile\"]\n", " \n", " profile_summary = f\"User profile context: State={user_profile.get('state')}, Age={user_profile.get('age')}, History={user_profile.get('medical_history')}\"\n", " prompt = load_prompt(\"query_reformulator\")\n", " history_str = \"\\n\".join(history)\n", " \n", " full_prompt = f\"{prompt}\\n\\n### User Profile Summary\\n{profile_summary}\\n\\n### Conversation History:\\n{history_str}\\n\\n### Follow-up Question:\\n{question}\"\n", " \n", " response = llm.invoke(full_prompt)\n", " standalone_question = response.content.strip()\n", " return {\"standalone_question\": standalone_question}\n", "\n", "def retrieve_and_grade_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Retrieves documents and grades them.\"\"\"\n", " logger.info(\"---NODE: RETRIEVE & GRADE---\")\n", " standalone_question = state[\"standalone_question\"]\n", " documents = transformer.transform_and_retrieve(standalone_question)\n", " is_relevant = router.grade_documents(standalone_question, documents)\n", " return {\"documents\": documents, \"is_relevant\": is_relevant}\n", "\n", "def search_and_ingest_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Searches the web and ingests new info.\"\"\"\n", " logger.info(\"---NODE: SEARCH & INGEST---\")\n", " web_documents = searcher.search(state[\"standalone_question\"])\n", " if web_documents:\n", " ingestor.ingest_documents(web_documents)\n", " return {}\n", "\n", "def generate_answer_node(state: AgentState) -> Dict[str, Any]:\n", " \"\"\"Generates the final answer.\"\"\"\n", " logger.info(\"---NODE: GENERATE ADVISOR RESPONSE---\")\n", " generation = advisor.generate_response(\n", " state[\"standalone_question\"], state[\"user_profile\"], state[\"documents\"]\n", " )\n", " history = state[\"conversation_history\"] + [f\"User: {state['user_message']}\", f\"Agent: {generation}\"]\n", " return {\"generation\": generation, \"conversation_history\": history}\n", "\n", "# --- Conditional Edges ---\n", "def should_search_web(state: AgentState) -> str:\n", " return \"search\" if not state[\"is_relevant\"] else \"generate\"\n", "\n", "# (CORRECTED) This is the function for the entry point conditional edge\n", "def decide_entry_point(state: AgentState) -> str:\n", " \"\"\"Decides the initial path based on profile completion status.\"\"\"\n", " logger.info(\"---ROUTING: ENTRY POINT---\")\n", " if state.get(\"is_profile_complete\"):\n", " logger.info(\">>> Route: Profile is complete. Starting Q&A.\")\n", " return \"qna\"\n", " else:\n", " logger.info(\">>> Route: Profile is not complete. Starting Profile Builder.\")\n", " return \"profile\"\n", "\n", "# --- Build the Graph ---\n", "db_connection = sqlite3.connect(\"data/checkpoints.db\", check_same_thread=False)\n", "memory = SqliteSaver(db_connection)\n", "\n", "builder = StateGraph(AgentState)\n", "\n", "# (CORRECTED) Removed the faulty entry_router_node\n", "builder.add_node(\"profile_builder\", profile_builder_node)\n", "builder.add_node(\"reformulate_query\", reformulate_query_node)\n", "builder.add_node(\"retrieve_and_grade\", retrieve_and_grade_node)\n", "builder.add_node(\"search_and_ingest\", search_and_ingest_node)\n", "builder.add_node(\"generate_answer\", generate_answer_node)\n", "\n", "# (CORRECTED) Set a conditional entry point\n", "builder.set_conditional_entry_point(\n", " decide_entry_point,\n", " {\n", " \"profile\": \"profile_builder\",\n", " \"qna\": \"reformulate_query\"\n", " }\n", ")\n", "\n", "# Define graph edges\n", "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.\n", "builder.add_edge(\"reformulate_query\", \"retrieve_and_grade\")\n", "builder.add_conditional_edges(\"retrieve_and_grade\", should_search_web, {\"search\": \"search_and_ingest\", \"generate\": \"generate_answer\"})\n", "builder.add_edge(\"search_and_ingest\", \"retrieve_and_grade\") # Loop back to re-retrieve\n", "builder.add_edge(\"generate_answer\", END)\n", "\n", "app = builder.compile(checkpointer=memory)\n", "\n", "# --- Interactive Test Harness (CORRECTED) ---\n", "if __name__ == '__main__':\n", " print(\"--- InsuCompass AI Unified Orchestrator Interactive Test ---\")\n", " print(\"Type 'quit' at any time to exit.\")\n", "\n", " test_thread_id = f\"interactive-test-{uuid.uuid4()}\"\n", " thread_config = {\"configurable\": {\"thread_id\": test_thread_id}}\n", " print(f\"Using conversation thread_id: {test_thread_id}\")\n", "\n", " # Initial state for a new user\n", " current_state = {\n", " \"user_profile\": {\n", " \"zip_code\": \"90210\", \"county\": \"Los Angeles\", \"state\": \"California\", \"state_abbreviation\": \"CA\",\n", " \"age\": 45, \"gender\": \"Male\", \"household_size\": 2, \"income\": 120000,\n", " \"employment_status\": \"employed_with_employer_coverage\", \"citizenship\": \"US Citizen\",\n", " \"medical_history\": None, \"medications\": None, \"special_cases\": None\n", " },\n", " \"user_message\": \"START_PROFILE_BUILDING\",\n", " \"is_profile_complete\": False,\n", " \"conversation_history\": [],\n", " }\n", "\n", " while True:\n", " print(\"\\n\" + \"=\"*20 + \" INVOKING GRAPH \" + \"=\"*20)\n", " print(f\"Sending message: '{current_state['user_message']}'\")\n", " \n", " # The graph is invoked with the current state\n", " final_state = app.invoke(current_state, config=thread_config)\n", "\n", " # Update our local state from the graph's final output\n", " current_state = final_state\n", " agent_response = current_state[\"generation\"]\n", " \n", " print(f\"\\nInsuCompass Agent: {agent_response}\")\n", "\n", " # Get the next input from the user\n", " if current_state[\"is_profile_complete\"]:\n", " # If the last response was the completion message, prompt for a question\n", " if \"profile is complete\" in agent_response:\n", " next_message = input(\"Your Question > \")\n", " else: # It was a Q&A response, so prompt for another question\n", " next_message = input(\"Your Follow-up Question > \")\n", " else:\n", " next_message = input(\"Your Answer > \")\n", "\n", " if next_message.lower() == 'quit':\n", " print(\"Exiting test.\")\n", " break\n", " \n", " # Prepare the state for the next turn\n", " current_state[\"user_message\"] = next_message" ] }, { "cell_type": "code", "execution_count": null, "id": "151d65d5", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "ic_venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.9" } }, "nbformat": 4, "nbformat_minor": 5 }