From 0a41d9ba825f011724937fab0b7d01bddd486798 Mon Sep 17 00:00:00 2001 From: ipu Date: Tue, 5 Aug 2025 00:00:58 +0300 Subject: [PATCH] update dtq conditions --- compose.yaml | 13 ++ pyproject.toml | 1 + src/api/v1/models.py | 130 +---------- src/api/v1/router.py | 141 +----------- src/cache/redis_cache.py | 131 +++++++++++ src/config.py | 7 +- src/drug_price_parser.py | 86 +++---- src/examples/estimation_v2_example.py | 262 ++++++++++++++++++++++ src/models.py | 127 +++++++++++ src/services/estimation_service_v2.py | 309 ++++++++++++++++++++++++++ uv.lock | 11 + 11 files changed, 915 insertions(+), 303 deletions(-) create mode 100644 src/cache/redis_cache.py create mode 100644 src/examples/estimation_v2_example.py create mode 100644 src/models.py create mode 100644 src/services/estimation_service_v2.py diff --git a/compose.yaml b/compose.yaml index 74a8f19..3c581a8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,3 +8,16 @@ services: - "${PUBLIC_API_PORT:-7311}:8000" volumes: - ./src:/app/src + depends_on: + - redis + + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + command: redis-server --appendonly yes + restart: unless-stopped + +volumes: + redis_data: diff --git a/pyproject.toml b/pyproject.toml index e0c8992..42639b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "beautifulsoup4>=4.12.0", "lxml>=4.9.0", "requests>=2.31.0", + "redis>=6.2.0", ] [project.optional-dependencies] diff --git a/src/api/v1/models.py b/src/api/v1/models.py index f7a7cc4..e710db7 100644 --- a/src/api/v1/models.py +++ b/src/api/v1/models.py @@ -1,128 +1,2 @@ -from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Any -from datetime import date - -class InsuranceChatRequest(BaseModel): - message: str = Field(..., description="User message") - session_id: Optional[str] = Field(None, description="Chat session ID") - -class Source(BaseModel): - plan_name: str - chunk_number: int - content_chunk: str - -class HistoryItem(BaseModel): - role: str - message: str - -class InsuranceChatResponse(BaseModel): - session_id: str - answer: str - sources: List[Source] = [] - history: List[HistoryItem] = [] - -# New estimation models matching the specified format -class Applicant(BaseModel): - applicant: int - firstName: str - lastName: str - midName: Optional[str] = Field("", description="Middle name") - phone: Optional[str] = Field("", description="Phone number") - gender: str - dob: date - nicotine: bool - weight: float - heightFt: int - heightIn: int - -class Plan(BaseModel): - id: int - coverage: int - tier: Optional[str] = Field(None, description="Tier assignment") - -class Medication(BaseModel): - applicant: int - name: str - rxcui: str - dosage: str - frequency: str - description: str - -class IssueDetail(BaseModel): - key: str - description: str - -class Issue(BaseModel): - key: str - details: List[IssueDetail] - -class Condition(BaseModel): - key: str - description: str - -class PHQ(BaseModel): - treatment: bool - invalid: bool - pregnancy: bool - effectiveDate: date - disclaimer: bool - signature: str - medications: List[Medication] - issues: List[Issue] - conditions: List[Condition] - -class Address(BaseModel): - address1: Optional[str] = Field("", description="Address line 1") - address2: Optional[str] = Field("", description="Address line 2") - city: Optional[str] = Field("", description="City") - state: Optional[str] = Field("", description="State") - zipcode: Optional[str] = Field("", description="Zip code") - -class EstimationRequest(BaseModel): - uid: Optional[str] = Field(None, description="Unique identifier") - applicants: List[Applicant] - plans: List[Plan] - phq: PHQ - income: float - address: Address - -class EstimationDetails(BaseModel): - dtq: bool - reason: str - tier: int - total_price: float - -class EstimationResult(BaseModel): - name: str - applicant_type: str - age: int - bmi: float - tier: int - rx_spend: float - message: str - -class EstimationResponse(BaseModel): - status: str - details: EstimationDetails - results: List[EstimationResult] - -class SessionCreateResponse(BaseModel): - session_id: str - -class SessionResponse(BaseModel): - id: str - organization_id: Optional[str] = None - agent_id: Optional[str] = None - created_at: Optional[str] = None - -class SessionsListResponse(BaseModel): - sessions: List[SessionResponse] - -class AgentsListResponse(BaseModel): - agents: List[Dict[str, Any]] - -class HealthResponse(BaseModel): - status: str - service: str - version: str - dependencies: Dict[str, str] \ No newline at end of file +# Import all models from the centralized models file +from src.models import * \ No newline at end of file diff --git a/src/api/v1/router.py b/src/api/v1/router.py index 6ebf0d7..5309db1 100644 --- a/src/api/v1/router.py +++ b/src/api/v1/router.py @@ -1,11 +1,9 @@ from fastapi import APIRouter, HTTPException -from typing import Dict, Any -import httpx + +from src.services.estimation_service_v2 import EstimationService from . import models from ...services.chat_service import chat_service -from ...services.estimation_service import estimation_service -from ...config import settings router = APIRouter() @@ -26,148 +24,25 @@ async def insurance_chat(request: models.InsuranceChatRequest): ) except Exception as e: - raise e raise HTTPException(status_code=500, detail=f"Error processing chat request: {str(e)}") @router.post("/estimation", response_model=models.EstimationResponse) async def estimate(request: models.EstimationRequest): """Handle insurance estimation requests""" try: - # Validate required fields if not request.applicants or not request.plans: raise HTTPException( status_code=400, detail="Missing required applicants or plans" ) - # Convert request to the format expected by the estimation service - request_data = { - "applicants": [ - { - "applicant": applicant.applicant, - "firstName": applicant.firstName, - "lastName": applicant.lastName, - "midName": applicant.midName, - "phone": applicant.phone, - "gender": applicant.gender, - "dob": applicant.dob.strftime("%d/%m/%Y"), - "nicotine": applicant.nicotine, - "weight": applicant.weight, - "heightFt": applicant.heightFt, - "heightIn": applicant.heightIn - } for applicant in request.applicants - ], - "plans": [ - { - "id": plan.id, - "coverage": plan.coverage, - "tier": plan.tier - } for plan in request.plans - ], - "phq": { - "treatment": request.phq.treatment, - "invalid": request.phq.invalid, - "pregnancy": request.phq.pregnancy, - "effectiveDate": request.phq.effectiveDate.strftime("%d/%m/%Y"), - "disclaimer": request.phq.disclaimer, - "signature": request.phq.signature, - "medications": [ - { - "applicant": med.applicant, - "name": med.name, - "rxcui": med.rxcui, - "dosage": med.dosage, - "frequency": med.frequency, - "description": med.description - } for med in request.phq.medications - ], - "issues": [ - { - "key": issue.key, - "details": [ - { - "key": detail.key, - "description": detail.description - } for detail in issue.details - ] - } for issue in request.phq.issues - ], - "conditions": [ - { - "key": condition.key, - "description": condition.description - } for condition in request.phq.conditions - ] - }, - "income": request.income, - "address": { - "address1": request.address.address1, - "address2": request.address.address2, - "city": request.address.city, - "state": request.address.state, - "zipcode": request.address.zipcode - } - } + estimation_service = EstimationService() + estimation_response = await estimation_service.estimate_insurance(request.applicants, request.phq, request.plans) + + return estimation_response - # Call the TALESTORM API for estimation - estimation_result = await estimation_service.estimate_insurance(request_data) - - # Handle the response from TALESTORM API - if estimation_result.get("status") == "error": - raise HTTPException( - status_code=500, - detail=estimation_result.get("details", {}).get("reason", "Estimation service error") - ) - - # Extract details and results from the TALESTORM response - details = estimation_result.get("details", {}) - results = estimation_result.get("results", []) - - # Convert results to the expected format - estimation_results = [] - for result in results: - estimation_results.append(models.EstimationResult( - name=result.get("name", "Unknown"), - applicant_type=result.get("applicant_type", "Unknown"), - age=result.get("age", 0), - bmi=result.get("bmi", 0.0), - tier=result.get("tier", 4), - rx_spend=result.get("rx_spend", 0.0), - message=result.get("message", "") - )) - - # Include validation information in the reason field if there are issues - reason = details.get("reason", "") - if "validation" in estimation_result: - validation_data = estimation_result["validation"] - if validation_data.get("issues"): - validation_issues = "; ".join(validation_data["issues"]) - if reason: - reason = f"{reason}; Validation issues: {validation_issues}" - else: - reason = f"Validation issues: {validation_issues}" - - # Add warnings to reason if any - if validation_data.get("warnings"): - validation_warnings = "; ".join(validation_data["warnings"]) - if reason: - reason = f"{reason}; Warnings: {validation_warnings}" - else: - reason = f"Warnings: {validation_warnings}" - - return models.EstimationResponse( - status=estimation_result.get("status", "accepted"), - details=models.EstimationDetails( - dtq=details.get("dtq", False), - reason=reason, - tier=details.get("tier", 4), - total_price=details.get("total_price", 0.0) - ), - results=estimation_results - ) - - except HTTPException: - raise + except HTTPException as e: + raise e except Exception as e: raise HTTPException( status_code=500, diff --git a/src/cache/redis_cache.py b/src/cache/redis_cache.py new file mode 100644 index 0000000..32cb26e --- /dev/null +++ b/src/cache/redis_cache.py @@ -0,0 +1,131 @@ +import json +import httpx +import redis +import functools +from src.config import settings +from src.drug_price_parser import DrugPriceParser, DrugPriceResponse + +# Initialize Redis client as a module-level singleton +_redis_client = None + +def get_redis_client(): + global _redis_client + if _redis_client is None: + _redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=settings.REDIS_DB, + decode_responses=True, + socket_connect_timeout=5, + socket_timeout=5, + retry_on_timeout=True + ) + return _redis_client + +def redis_cache(cache_key: str): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + redis_client = get_redis_client() + + try: + cached_data = redis_client.get(cache_key) + if cached_data: + return json.loads(cached_data) + except Exception: + # If Redis is unavailable, just execute the function + pass + + result = func(*args, **kwargs) + + try: + result_json = json.dumps(result) + redis_client.setex(cache_key, settings.REDIS_CACHE_TTL, result_json) + except Exception: + pass + + return result + return wrapper + return decorator + +def fetch_virgil_api(endpoint: str): + url = settings.VIRGIL_API_BASE_URL + endpoint + with httpx.Client() as client: + response = client.get(url) + response.raise_for_status() + data = response.json() + return data + +@redis_cache(cache_key="plans:all") +def fetch_plans(): + return fetch_virgil_api("/insurance/plans/full") + +@redis_cache(cache_key="conditions:all") +def fetch_conditions(): + return fetch_virgil_api("/health/conditions") + +@redis_cache(cache_key="issues:all") +def fetch_issues(): + return fetch_virgil_api("/health/issues") + +@redis_cache(cache_key="issue_details:all") +def fetch_issue_details(): + return fetch_virgil_api("/health/issue-details") + +def get_plan_by_id(plan_id: int): + plans = fetch_plans() + for plan in plans: + if plan["id"] == plan_id: + return plan + return None + +def fetch_drug(drug_name: str) -> DrugPriceResponse: + cache_key = f"drugs:{drug_name}" + redis_client = get_redis_client() + + try: + cached_data = redis_client.get(cache_key) + if cached_data: + return DrugPriceResponse.model_validate_json(cached_data) + except Exception: + pass + + parser = DrugPriceParser() + result = parser.get_drug_prices(drug_name) + + try: + result_json = result.model_dump_json() + redis_client.setex(cache_key, settings.REDIS_CACHE_TTL_DRUGS, result_json) + except Exception: + pass + + return result + + +def search_drug(drug_name: str) -> str: + cache_key = f"drug_search:{drug_name}" + redis_client = get_redis_client() + + try: + cached_data = redis_client.get(cache_key) + if cached_data: + return cached_data + except Exception: + pass + + client = httpx.Client( + base_url="https://www.drugs.com/api/autocomplete", + # headers=headers, + timeout=httpx.Timeout(60.0, connect=10.0) + ) + response = client.get(f"/?type=price-guide&s={drug_name}") + response_json = response.json() + + result = response_json["categories"][0]["results"][0]["url"].replace("/price-guide/", "") + + try: + redis_client.setex(cache_key, settings.REDIS_CACHE_TTL_DRUGS, result) + except Exception: + pass + + return result diff --git a/src/config.py b/src/config.py index ff97c05..c92c71d 100644 --- a/src/config.py +++ b/src/config.py @@ -12,9 +12,14 @@ class Settings(BaseSettings): TALESTORM_API_BASE_URL: str = "" TALESTORM_API_KEY: str TALESTORM_AGENT_ID: str + TALESTORM_DTQ_AGENT_ID: str TALESTORM_ESTIMATION_AGENT_ID: str VIRGIL_API_BASE_URL: str - + REDIS_HOST: str = "redis" + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_CACHE_TTL: int = 3600 * 24 + REDIS_CACHE_TTL_DRUGS: int = 3600 * 24 * 30 # Global settings instance diff --git a/src/drug_price_parser.py b/src/drug_price_parser.py index 80f3711..7589e9d 100644 --- a/src/drug_price_parser.py +++ b/src/drug_price_parser.py @@ -1,18 +1,25 @@ -""" -Drug Price Parser for Drugs.com - -This module provides functionality to scrape drug pricing information -from Drugs.com and return it in JSON format. -""" - -import json -import re -import time -from typing import Dict, List, Optional, Any -from urllib.parse import quote_plus +from typing import List import requests from bs4 import BeautifulSoup +from pydantic import BaseModel + + +class Dosage(BaseModel): + name: str + price: float + + +class Formulation(BaseModel): + name: str + dosages: List[Dosage] + + +class DrugPriceResponse(BaseModel): + drug_name: str + url: str + prices: List[Formulation] + status: str class DrugPriceParser: @@ -39,7 +46,7 @@ class DrugPriceParser: 'TE': 'trailers' }) - def get_drug_prices(self, drug_name: str) -> Dict[str, Any]: + def get_drug_prices(self, drug_name: str) -> DrugPriceResponse: """ Get pricing information for a specific drug. @@ -47,7 +54,7 @@ class DrugPriceParser: drug_name: Name of the drug (e.g., 'alprazolam') Returns: - Dictionary containing pricing information in JSON format + DrugPriceResponse containing pricing information """ url = f"{self.BASE_URL}/{drug_name.lower()}#prices" user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0' @@ -59,49 +66,46 @@ class DrugPriceParser: prices_data = self._extract_prices(soup, drug_name) - return { - "drug_name": drug_name, - "url": url, - "prices": prices_data, - "status": "success" - } - - - def _extract_prices(self, soup: BeautifulSoup, drug_name: str) -> Dict[str, Any]: + return DrugPriceResponse( + drug_name=drug_name, + url=url, + prices=prices_data, + status="success" + ) + + def _extract_prices(self, soup: BeautifulSoup, drug_name: str) -> List[Formulation]: """Extract pricing information from the parsed HTML""" - prices_data = { - "formulations": [], - } + formulations = [] div_content = soup.find('div', {'id': 'content'}) - formulations = div_content.find_all('h3') - for formulation in formulations: + formulation_elements = div_content.find_all('h3') + for formulation in formulation_elements: if formulation.get('class'): break formulation_name = formulation.get_text() - formulation_data = { - "name": formulation_name.rstrip(), - "dosages": [] - } + dosages = [] dosages_table = formulation.find_next('div') - dosages = dosages_table.find_all('details') - for dosage in dosages: + dosage_elements = dosages_table.find_all('details') + for dosage in dosage_elements: summary = dosage.find('summary') spans = summary.find_all('span') dosage_name = spans[0].find('b').get_text() dosage_price = spans[1].find_next('b').get_text() - formulation_data["dosages"].append({ - "name": dosage_name.rstrip(), - "price": float(dosage_price.rstrip().replace('$', '').replace(',', '')) - }) - prices_data["formulations"].append(formulation_data) - return prices_data + dosages.append(Dosage( + name=dosage_name.rstrip(), + price=float(dosage_price.rstrip().replace('$', '').replace(',', '')) + )) + formulations.append(Formulation( + name=formulation_name.rstrip(), + dosages=dosages + )) + return formulations def parse_drug_prices(drug_name: str) -> str: parser = DrugPriceParser() result = parser.get_drug_prices(drug_name) - return json.dumps(result, indent=2) + return result.model_dump_json(indent=2) if __name__ == "__main__": diff --git a/src/examples/estimation_v2_example.py b/src/examples/estimation_v2_example.py new file mode 100644 index 0000000..d49964d --- /dev/null +++ b/src/examples/estimation_v2_example.py @@ -0,0 +1,262 @@ +import asyncio +import json +from datetime import date +from typing import Dict, Any + +# Import models from the centralized models file +from src.models import ( + Applicant, Plan, PHQ, Medication, Issue, IssueDetail, + Condition, Address, EstimationRequest, EstimationResponse +) +from src.services.estimation_service_v2 import EstimationService + +async def example_estimation_request(): + """Example of how to use the v2 estimation service""" + + # Create sample applicants + applicants = [ + Applicant( + applicant=0, + firstName="John", + lastName="Doe", + midName="", + phone="555-123-4567", + gender="male", + dob=date(1985, 3, 15), + nicotine=False, + weight=180.0, + heightFt=6, + heightIn=1 + ), + Applicant( + applicant=1, + firstName="Jane", + lastName="Doe", + midName="", + phone="555-123-4568", + gender="female", + dob=date(1988, 7, 22), + nicotine=False, + weight=140.0, + heightFt=5, + heightIn=6 + ) + ] + + # Create sample plans + plans = [ + Plan( + id=1, + coverage=1, # Individual coverage + tier=None + ) + ] + + # Create sample medications + medications = [ + Medication( + applicant=0, + name="Lisinopril", + rxcui="29046", + dosage="10mg", + frequency="daily", + description="ACE inhibitor for blood pressure" + ), + Medication( + applicant=1, + name="Metformin", + rxcui="6809", + dosage="500mg", + frequency="twice daily", + description="Diabetes medication" + ) + ] + + # Create sample issues + issues = [ + Issue( + key="hypertension", + details=[ + IssueDetail( + key="diagnosed", + description="Hypertension diagnosed in 2020" + ), + IssueDetail( + key="controlled", + description="Well controlled with medication" + ) + ] + ), + Issue( + key="diabetes", + details=[ + IssueDetail( + key="type2", + description="Type 2 diabetes diagnosed in 2021" + ), + IssueDetail( + key="controlled", + description="Controlled with Metformin" + ) + ] + ) + ] + + # Create sample conditions + conditions = [ + Condition( + key="hypertension", + description="Essential hypertension" + ), + Condition( + key="diabetes", + description="Type 2 diabetes mellitus" + ) + ] + + # Create PHQ + phq = PHQ( + treatment=False, + invalid=False, + pregnancy=False, + effectiveDate=date.today(), + disclaimer=True, + signature="John Doe", + medications=medications, + issues=issues, + conditions=conditions + ) + + # Create address + address = Address( + address1="123 Main Street", + address2="Apt 4B", + city="Springfield", + state="IL", + zipcode="62701" + ) + + # Create estimation request + request = EstimationRequest( + uid="test_estimation_001", + applicants=applicants, + plans=plans, + phq=phq, + income=75000.0, + address=address + ) + + print("=== Insurance Estimation Service V2 Example ===\n") + print("Request Data:") + print(json.dumps(request.model_dump(), indent=2, default=str)) + print("\n" + "="*50 + "\n") + + try: + # Create estimation service + estimation_service = EstimationService() + + # Call the estimation service + print("Calling estimation service...") + result = await estimation_service.estimate_insurance( + applicants=applicants, + phq=phq, + plans=plans + ) + + print("Estimation Result:") + print(json.dumps(result.model_dump(), indent=2, default=str)) + + # Display a summary + if result.status == "accepted": + details = result.details + print(f"\n=== SUMMARY ===") + print(f"Status: {result.status}") + print(f"DTQ: {details.dtq}") + print(f"Reason: {details.reason}") + print(f"Tier: {details.tier}") + print(f"Total Price: ${details.total_price:,.2f}") + + if result.results: + print(f"\nApplicant Results:") + for applicant_result in result.results: + print(f"- {applicant_result.name}") + print(f" Age: {applicant_result.age}") + print(f" BMI: {applicant_result.bmi:.1f}") + print(f" Tier: {applicant_result.tier}") + print(f" RX Spend: ${applicant_result.rx_spend:,.2f}") + print(f" Message: {applicant_result.message}") + else: + print(f"\nEstimation failed: {result.status}") + if result.details: + print(f"Reason: {result.details.reason}") + + except Exception as e: + print(f"Error calling estimation service: {str(e)}") + import traceback + traceback.print_exc() + +async def example_minimal_request(): + """Example with minimal required data""" + + # Create minimal applicant + applicant = Applicant( + applicant=0, + firstName="Alice", + lastName="Smith", + midName="", + phone="", + gender="female", + dob=date(1990, 1, 1), + nicotine=False, + weight=150.0, + heightFt=5, + heightIn=7 + ) + + # Create minimal plan + plan = Plan( + id=1, + coverage=1, + tier=None + ) + + # Create minimal PHQ + phq = PHQ( + treatment=False, + invalid=False, + pregnancy=False, + effectiveDate=date.today(), + disclaimer=True, + signature="Alice Smith", + medications=[], + issues=[], + conditions=[] + ) + + print("\n=== Minimal Request Example ===\n") + print("Minimal Request Data:") + print(f"Applicant: {applicant.firstName} {applicant.lastName}") + print(f"Age: {applicant.dob}") + print(f"Plan Coverage: {plan.coverage}") + print("\n" + "="*50 + "\n") + + try: + estimation_service = EstimationService() + result = await estimation_service.estimate_insurance( + applicants=[applicant], + phq=phq, + plans=[plan] + ) + + print("Minimal Request Result:") + print(json.dumps(result.model_dump(), indent=2, default=str)) + + except Exception as e: + print(f"Error with minimal request: {str(e)}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + # Run the examples + asyncio.run(example_estimation_request()) + asyncio.run(example_minimal_request()) \ No newline at end of file diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..2e880cc --- /dev/null +++ b/src/models.py @@ -0,0 +1,127 @@ +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import date + +class Applicant(BaseModel): + applicant: int + firstName: str + lastName: str + midName: Optional[str] = Field("", description="Middle name") + phone: Optional[str] = Field("", description="Phone number") + gender: str + dob: date + nicotine: bool + weight: float + heightFt: int + heightIn: int + +class Plan(BaseModel): + id: int + coverage: int + tier: Optional[str] = Field(None, description="Tier assignment") + +class Medication(BaseModel): + applicant: int + name: str + rxcui: str + dosage: str + frequency: str + description: str + +class IssueDetail(BaseModel): + key: str + description: str + +class Issue(BaseModel): + key: str + details: List[IssueDetail] + +class Condition(BaseModel): + key: str + description: str + +class PHQ(BaseModel): + treatment: bool + invalid: bool + pregnancy: bool + effectiveDate: date + disclaimer: bool + signature: str + medications: List[Medication] + issues: List[Issue] + conditions: List[Condition] + +class Address(BaseModel): + address1: Optional[str] = Field("", description="Address line 1") + address2: Optional[str] = Field("", description="Address line 2") + city: Optional[str] = Field("", description="City") + state: Optional[str] = Field("", description="State") + zipcode: Optional[str] = Field("", description="Zip code") + +class EstimationRequest(BaseModel): + uid: Optional[str] = Field(None, description="Unique identifier") + applicants: List[Applicant] + plans: List[Plan] + phq: PHQ + income: float + address: Address + +class EstimationDetails(BaseModel): + dtq: bool + reason: str + tier: int + total_price: float + +class EstimationResult(BaseModel): + name: str + applicant_type: str + age: int + bmi: float + tier: int + rx_spend: float + message: str + +class EstimationResponse(BaseModel): + status: str + details: EstimationDetails + results: List[EstimationResult] + +class InsuranceChatRequest(BaseModel): + message: str = Field(..., description="User message") + session_id: Optional[str] = Field(None, description="Chat session ID") + +class Source(BaseModel): + plan_name: str + chunk_number: int + content_chunk: str + +class HistoryItem(BaseModel): + role: str + message: str + +class InsuranceChatResponse(BaseModel): + session_id: str + answer: str + sources: List[Source] = [] + history: List[HistoryItem] = [] + +class SessionCreateResponse(BaseModel): + session_id: str + +class SessionResponse(BaseModel): + id: str + organization_id: Optional[str] = None + agent_id: Optional[str] = None + created_at: Optional[str] = None + +class SessionsListResponse(BaseModel): + sessions: List[SessionResponse] + +class AgentsListResponse(BaseModel): + agents: List[dict] + +class HealthResponse(BaseModel): + status: str + service: str + version: str + dependencies: dict \ No newline at end of file diff --git a/src/services/estimation_service_v2.py b/src/services/estimation_service_v2.py new file mode 100644 index 0000000..0bc5278 --- /dev/null +++ b/src/services/estimation_service_v2.py @@ -0,0 +1,309 @@ +from datetime import date +from enum import Enum +from typing import Optional +from src.models import PHQ, Applicant, Plan, EstimationResponse, EstimationDetails, EstimationResult +from src.cache.redis_cache import fetch_drug, get_plan_by_id, search_drug +from src.config import settings +import httpx + +from src.services.session_service import session_service + +class Tier(Enum): + TIER_1 = 1 + TIER_15 = 1.5 + TIER_2 = 2 + TIER_25 = 2.5 + TIER_3 = 3 + TIER_35 = 3.5 + TIER_4 = 4 + TIER_45 = 4.5 + TIER_5 = 5 + TIER_55 = 5.5 + TIER_6 = 6 + TIER_65 = 6.5 + TIER_7 = 7 + + def __lt__(self, other): + if isinstance(other, Tier): + return self.value < other.value + return NotImplemented + + def __le__(self, other): + if isinstance(other, Tier): + return self.value <= other.value + return NotImplemented + + def __gt__(self, other): + if isinstance(other, Tier): + return self.value > other.value + return NotImplemented + + def __ge__(self, other): + if isinstance(other, Tier): + return self.value >= other.value + return NotImplemented + + +BASE_TIERS = { + 30: Tier.TIER_1, + 40: Tier.TIER_2, + 50: Tier.TIER_3, + 60: Tier.TIER_35, + 200: Tier.TIER_4, +} + +HEIGHT_WEIGHT_TABLE = { + (4, 2): (60, 124), + (4, 3): (63, 129), + (4, 4): (65, 135), + (4, 5): (68, 140), + (4, 6): (71, 145), + (4, 7): (73, 151), + (4, 8): (76, 156), + (4, 9): (79, 162), + (4, 10): (81, 167), + (4, 11): (84, 173), + (5, 0): (87, 179), + (5, 1): (90, 185), + (5, 2): (93, 191), + (5, 3): (96, 197), + (5, 4): (99, 204), + (5, 5): (102, 210), + (5, 6): (105, 217), + (5, 7): (109, 223), + (5, 8): (112, 230), + (5, 9): (115, 237), + (5, 10): (118, 244), + (5, 11): (122, 251), + (6, 0): (125, 258), + (6, 1): (129, 265), + (6, 2): (132, 273), + (6, 3): (136, 280), + (6, 4): (140, 288), + (6, 5): (143, 295), + (6, 6): (147, 303), + (6, 7): (151, 311), + (6, 8): (155, 319), + (6, 9): (159, 327), + (6, 10): (163, 335), + (6, 11): (167, 343), + (7, 0): (171, 351), + (7, 1): (175, 360), + (7, 2): (179, 368), + (7, 3): (183, 377), + (7, 4): (187, 386), +} + + +class EstimationService: + def __init__(self): + self.base_url = settings.TALESTORM_API_BASE_URL + self.api_key = settings.TALESTORM_API_KEY + + async def get_client(self) -> httpx.AsyncClient: + """Get HTTP client for talestorm-ai API""" + headers = {} + if self.api_key: + headers["X-API-Key"] = self.api_key + + return httpx.AsyncClient( + base_url=self.base_url, + headers=headers, + timeout=httpx.Timeout(60.0, connect=10.0) # 30s total timeout, 10s connect timeout + ) + + async def check_dtq_ai(self, phq: PHQ) -> tuple[bool, str]: + try: + client = await self.get_client() + session_id = await session_service.create_session(agent_id=settings.TALESTORM_DTQ_AGENT_ID) + response = await client.post( + "/chat/", + json={"chat_session_id": session_id, "user_message": f"Check if this applicant eligible for insurance:\n {phq.model_dump_json()}"}, + ) + response_json = response.json() + print(f"AI DTQ Response: {response_json}") + + if "insurable" in response_json: + dtq = not response_json["message"]["insurable"] + reason = response_json["message"]["reason"] + else: + dtq = False + reason = "" + return dtq, reason + except Exception as e: + print(f"Error in AI DTQ check: {e}") + # Fallback to not DTQ on error + return False, "" + + def check_dtq(self, phq: PHQ) -> tuple[bool, str]: + if phq.pregnancy: + return True, "Pregnancy" + + for issue in phq.issues: + if issue.key == "surgery": + for detail in issue.details: + if detail.key == "not_performed": + return True, "Surgery that was not performed" + # TODO: Add other DTQ conditions + return False, None + + def calculate_age(self, born): + today = date.today() + return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) + + def calculate_rx_spend(self, phq: PHQ, applicant_id: int) -> float: + rx_spend = 0 + for medication in phq.medications: + if medication.applicant != applicant_id: + continue + try: + drug_name = medication.name + drug_url = search_drug(drug_name) + drug_price = fetch_drug(drug_url) + rx_spend += drug_price.prices[0].dosages[0].price + except Exception: + pass + return rx_spend + + def get_tier(self, coverage: int, rx_spend: float) -> Optional[Tier]: + tiers = [ + Tier.TIER_1, Tier.TIER_15, Tier.TIER_2, Tier.TIER_25, Tier.TIER_3, Tier.TIER_35, + Tier.TIER_4, Tier.TIER_45, Tier.TIER_5, Tier.TIER_55, Tier.TIER_6, Tier.TIER_65, Tier.TIER_7 + ] + + rates = { + 1: [122, 127, 133, 139, 145, 151, 158, 164, 172, 182, 196, 209, 222], + 2: [243, 253, 265, 277, 290, 302, 315, 327, 343, 364, 390, 417, 444], + 3: [219, 228, 239, 249, 261, 272, 283, 295, 309, 327, 351, 376, 400], + 4: [365, 379, 397, 415, 435, 452, 472, 491, 514, 545, 585, 626, 666] + } + + if coverage not in rates: + raise ValueError(f"Unsupported coverage type: {coverage}") + + for i, threshold in enumerate(rates[coverage]): + if rx_spend <= threshold: + return tiers[i] + + return None + + def get_plan_price(self, plan: Plan, tier: Tier, coverage: int): + plan_data = get_plan_by_id(plan.id) + prices = plan_data["prices"] + tier_str = f"Tier {tier.value}" + for price in prices: + if price["tier"] == tier_str and price["coverage"] == coverage: + return price["price"] + return 0 + + def calculate_bmi(self, weight, ft, inch): + try: + inches = ft * 12 + inch + return (weight / (inches ** 2)) * 703 + except: + return None + + + async def estimate_insurance(self, applicants: list[Applicant], phq: PHQ, plans: list[Plan]): + estimation_results = [] + is_dtq, reason = self.check_dtq(phq) + if not is_dtq: + is_dtq, reason = await self.check_dtq_ai(phq) + + max_age = max(self.calculate_age(applicant.dob) for applicant in applicants) + for base_age, tier in BASE_TIERS.items(): + if max_age <= base_age: + base_tier = tier + break + + rx_spend = 0 + for applicant_id, applicant in enumerate(applicants): + applicant_age = self.calculate_age(applicant.dob) + for base_age, tier in BASE_TIERS.items(): + if applicant_age <= base_age: + applicant_tier = tier + break + + weight_min, weight_max = HEIGHT_WEIGHT_TABLE[(applicant.heightFt, applicant.heightIn)] + if applicant.weight < weight_min: + is_dtq = True + reason = "Declined due to low BMI of one or more applicants" + elif applicant.weight > weight_max: + is_dtq = True + reason = "Declined due to high BMI of one or more applicants" + + rx_spend_applicant = self.calculate_rx_spend(phq, applicant_id) + rx_spend += rx_spend_applicant + + applicant_new_tier = self.get_tier(plans[0].coverage, rx_spend_applicant) + if applicant_new_tier is None: + is_dtq = True + reason = "Declined due to high Rx spend" + break + + if applicant_new_tier > applicant_tier: + applicant_tier = applicant_new_tier + reason = f"Rx spend increased tier to {applicant_new_tier}." + + estimation_results.append( + EstimationResult( + name=applicant.firstName, + applicant_type="", + age=applicant_age, + bmi=self.calculate_bmi(applicant.weight, applicant.heightFt, applicant.heightIn), + tier=applicant_tier.value, + rx_spend=rx_spend_applicant, + message=reason if reason else f"Tier {applicant_tier} assigned with Rx spend within allowed limits." + ) + ) + + plan_price = self.get_plan_price(plans[0], base_tier, plans[0].coverage) + + if is_dtq: + return EstimationResponse( + status="rejected", + details=EstimationDetails( + dtq=is_dtq, + reason=reason, + tier=base_tier.value, + total_price=plan_price + ), + results=estimation_results + ) + + new_tier = self.get_tier(plans[0].coverage, rx_spend) + + if new_tier is None: + return EstimationResponse( + status="rejected", + details=EstimationDetails( + dtq=True, + reason="Declined due to high Rx spend", + tier=base_tier.value, + total_price=plan_price + ), + results=estimation_results + ) + + if new_tier > base_tier: + base_tier = new_tier + + plan_price = self.get_plan_price(plans[0], base_tier, plans[0].coverage) + + status = "accepted" if base_tier is not None else "rejected" + + return EstimationResponse( + status=status, + details=EstimationDetails( + dtq=is_dtq, + reason=reason, + tier=base_tier.value, + total_price=plan_price + ), + results=estimation_results + ) + + + + + \ No newline at end of file diff --git a/uv.lock b/uv.lock index 3aab8f6..25fb789 100644 --- a/uv.lock +++ b/uv.lock @@ -296,6 +296,7 @@ dependencies = [ { name = "pandas" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "redis" }, { name = "requests" }, ] @@ -322,6 +323,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "redis", specifier = ">=6.2.0" }, { name = "requests", specifier = ">=2.31.0" }, ] provides-extras = ["dev"] @@ -698,6 +700,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "redis" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129, upload-time = "2025-05-28T05:01:18.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659, upload-time = "2025-05-28T05:01:16.955Z" }, +] + [[package]] name = "requests" version = "2.32.4"