update dtq conditions

This commit is contained in:
ipu 2025-08-05 00:00:58 +03:00
parent 80916f6c3e
commit 0a41d9ba82
11 changed files with 915 additions and 303 deletions

View file

@ -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]
# Import all models from the centralized models file
from src.models import *

View file

@ -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,

131
src/cache/redis_cache.py vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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__":

View file

@ -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())

127
src/models.py Normal file
View file

@ -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

View file

@ -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
)