add drug parser; add iha estimation rules
This commit is contained in:
parent
bf1d988d36
commit
80916f6c3e
10 changed files with 1271 additions and 15 deletions
|
|
@ -136,11 +136,30 @@ async def estimate(request: models.EstimationRequest):
|
|||
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=details.get("reason", ""),
|
||||
reason=reason,
|
||||
tier=details.get("tier", 4),
|
||||
total_price=details.get("total_price", 0.0)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class Settings(BaseSettings):
|
|||
TALESTORM_API_KEY: str
|
||||
TALESTORM_AGENT_ID: str
|
||||
TALESTORM_ESTIMATION_AGENT_ID: str
|
||||
VIRGIL_API_BASE_URL: str
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
116
src/drug_price_parser.py
Normal file
116
src/drug_price_parser.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
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
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class DrugPriceParser:
|
||||
"""Parser for extracting drug pricing information from Drugs.com"""
|
||||
|
||||
BASE_URL = "https://www.drugs.com/price-guide"
|
||||
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'Sec-GPC': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Cookie': 'ddc-pvc=8; ddcsubscribe=disabled',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Priority': 'u=0, i',
|
||||
'TE': 'trailers'
|
||||
})
|
||||
|
||||
def get_drug_prices(self, drug_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get pricing information for a specific drug.
|
||||
|
||||
Args:
|
||||
drug_name: Name of the drug (e.g., 'alprazolam')
|
||||
|
||||
Returns:
|
||||
Dictionary containing pricing information in JSON format
|
||||
"""
|
||||
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'
|
||||
|
||||
self.session.headers.update({'User-Agent': user_agent})
|
||||
|
||||
response = self.session.get(url, timeout=15)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
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]:
|
||||
"""Extract pricing information from the parsed HTML"""
|
||||
|
||||
prices_data = {
|
||||
"formulations": [],
|
||||
}
|
||||
div_content = soup.find('div', {'id': 'content'})
|
||||
formulations = div_content.find_all('h3')
|
||||
for formulation in formulations:
|
||||
if formulation.get('class'):
|
||||
break
|
||||
formulation_name = formulation.get_text()
|
||||
formulation_data = {
|
||||
"name": formulation_name.rstrip(),
|
||||
"dosages": []
|
||||
}
|
||||
dosages_table = formulation.find_next('div')
|
||||
dosages = dosages_table.find_all('details')
|
||||
for dosage in dosages:
|
||||
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
|
||||
|
||||
|
||||
def parse_drug_prices(drug_name: str) -> str:
|
||||
parser = DrugPriceParser()
|
||||
result = parser.get_drug_prices(drug_name)
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
drug_name = sys.argv[1]
|
||||
result = parse_drug_prices(drug_name)
|
||||
print(result)
|
||||
else:
|
||||
print("Usage: python drug_price_parser.py <drug_name>")
|
||||
print("Example: python drug_price_parser.py alprazolam")
|
||||
208
src/examples/estimation_example.py
Normal file
208
src/examples/estimation_example.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import asyncio
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from services.estimation_service import estimation_service
|
||||
|
||||
async def example_estimation_request():
|
||||
"""Example of how to use the estimation service"""
|
||||
|
||||
# Sample request data
|
||||
request_data = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"dob": "1985-03-15",
|
||||
"gender": "male",
|
||||
"weight": 180,
|
||||
"heightFt": 6,
|
||||
"heightIn": 1,
|
||||
"nicotine": False
|
||||
},
|
||||
{
|
||||
"firstName": "Jane",
|
||||
"lastName": "Doe",
|
||||
"dob": "1988-07-22",
|
||||
"gender": "female",
|
||||
"weight": 140,
|
||||
"heightFt": 5,
|
||||
"heightIn": 6,
|
||||
"nicotine": False
|
||||
},
|
||||
{
|
||||
"firstName": "Emma",
|
||||
"lastName": "Doe",
|
||||
"dob": "2015-11-08",
|
||||
"gender": "female",
|
||||
"weight": 65,
|
||||
"heightFt": 4,
|
||||
"heightIn": 2,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [
|
||||
{
|
||||
"id": "plan_001",
|
||||
"coverage": "family"
|
||||
},
|
||||
{
|
||||
"id": "plan_002",
|
||||
"coverage": "family"
|
||||
}
|
||||
],
|
||||
"phq": {
|
||||
"medications": [
|
||||
{
|
||||
"name": "Lisinopril",
|
||||
"dosage": "10mg",
|
||||
"frequency": "daily"
|
||||
},
|
||||
{
|
||||
"name": "Metformin",
|
||||
"dosage": "500mg",
|
||||
"frequency": "twice daily"
|
||||
},
|
||||
{
|
||||
"name": "Albuterol",
|
||||
"dosage": "90mcg",
|
||||
"frequency": "as needed"
|
||||
}
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"key": "diabetes",
|
||||
"details": [
|
||||
{
|
||||
"description": "Type 2 diabetes diagnosed in 2020"
|
||||
},
|
||||
{
|
||||
"description": "Well controlled with medication"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "hypertension",
|
||||
"details": [
|
||||
{
|
||||
"description": "High blood pressure managed with Lisinopril"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "asthma",
|
||||
"details": [
|
||||
{
|
||||
"description": "Mild asthma, uses inhaler as needed"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"description": "Type 2 diabetes mellitus"
|
||||
},
|
||||
{
|
||||
"description": "Essential hypertension"
|
||||
},
|
||||
{
|
||||
"description": "Mild persistent asthma"
|
||||
}
|
||||
]
|
||||
},
|
||||
"income": 75000,
|
||||
"address": {
|
||||
"address1": "123 Main Street",
|
||||
"address2": "Apt 4B",
|
||||
"city": "Springfield",
|
||||
"state": "IL",
|
||||
"zipcode": "62701"
|
||||
}
|
||||
}
|
||||
|
||||
print("=== Insurance Estimation Service Example ===\n")
|
||||
print("Request Data:")
|
||||
print(json.dumps(request_data, indent=2))
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
try:
|
||||
# Call the estimation service
|
||||
print("Calling estimation service...")
|
||||
result = await estimation_service.estimate_insurance(request_data)
|
||||
|
||||
print("Estimation Result:")
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
# Display a summary
|
||||
if result.get("status") == "accepted":
|
||||
details = result.get("details", {})
|
||||
print(f"\n=== SUMMARY ===")
|
||||
print(f"Status: {result['status']}")
|
||||
print(f"DTQ: {details.get('dtq', False)}")
|
||||
print(f"Reason: {details.get('reason', 'N/A')}")
|
||||
print(f"Tier: {details.get('tier', 'N/A')}")
|
||||
print(f"Total Price: ${details.get('total_price', 0):,.2f}")
|
||||
|
||||
results = result.get("results", [])
|
||||
if results:
|
||||
print(f"\nApplicant Results:")
|
||||
for applicant_result in results:
|
||||
print(f"- {applicant_result.get('name', 'N/A')} ({applicant_result.get('applicant_type', 'N/A')})")
|
||||
print(f" Age: {applicant_result.get('age', 'N/A')}")
|
||||
print(f" BMI: {applicant_result.get('bmi', 'N/A')}")
|
||||
print(f" Tier: {applicant_result.get('tier', 'N/A')}")
|
||||
print(f" RX Spend: ${applicant_result.get('rx_spend', 0):,.2f}")
|
||||
print(f" Message: {applicant_result.get('message', 'N/A')}")
|
||||
else:
|
||||
print(f"\nEstimation failed: {result.get('status', 'unknown')}")
|
||||
if result.get("details"):
|
||||
print(f"Reason: {result['details'].get('reason', 'N/A')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error calling estimation service: {str(e)}")
|
||||
|
||||
async def example_minimal_request():
|
||||
"""Example with minimal required data"""
|
||||
|
||||
minimal_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "Alice",
|
||||
"lastName": "Smith",
|
||||
"dob": "1990-01-01",
|
||||
"gender": "female",
|
||||
"weight": 150,
|
||||
"heightFt": 5,
|
||||
"heightIn": 7,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [
|
||||
{
|
||||
"id": "basic_plan",
|
||||
"coverage": "individual"
|
||||
}
|
||||
],
|
||||
"income": 50000,
|
||||
"address": {
|
||||
"city": "Chicago",
|
||||
"state": "IL"
|
||||
}
|
||||
}
|
||||
|
||||
print("\n=== Minimal Request Example ===\n")
|
||||
print("Minimal Request Data:")
|
||||
print(json.dumps(minimal_request, indent=2))
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
try:
|
||||
result = await estimation_service.estimate_insurance(minimal_request)
|
||||
print("Minimal Request Result:")
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error with minimal request: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the examples
|
||||
asyncio.run(example_estimation_request())
|
||||
asyncio.run(example_minimal_request())
|
||||
304
src/examples/test_validation.py
Normal file
304
src/examples/test_validation.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for IHA underwriting validation functionality
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from services.estimation_service import estimation_service
|
||||
|
||||
async def test_validation():
|
||||
"""Test various validation scenarios"""
|
||||
|
||||
print("Testing IHA Underwriting Validation\n")
|
||||
|
||||
# Test Case 1: Valid application
|
||||
print("=== Test Case 1: Valid Application ===")
|
||||
valid_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"dob": "15/03/1985",
|
||||
"gender": "Male",
|
||||
"weight": 180,
|
||||
"heightFt": 6,
|
||||
"heightIn": 0,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
|
||||
"phq": {
|
||||
"medications": [
|
||||
{
|
||||
"name": "Lisinopril",
|
||||
"dosage": "10mg",
|
||||
"frequency": "daily"
|
||||
}
|
||||
],
|
||||
"conditions": [],
|
||||
"issues": []
|
||||
},
|
||||
"income": 50000,
|
||||
"address": {
|
||||
"address1": "123 Main St",
|
||||
"city": "Anytown",
|
||||
"state": "CA",
|
||||
"zipcode": "12345"
|
||||
}
|
||||
}
|
||||
|
||||
result1 = estimation_service._validate_application(valid_request)
|
||||
print(f"Eligible: {result1['is_eligible']}")
|
||||
print(f"Issues: {result1['issues']}")
|
||||
print(f"Warnings: {result1['warnings']}")
|
||||
print()
|
||||
|
||||
# Test Case 2: Uninsurable medication
|
||||
print("=== Test Case 2: Uninsurable Medication ===")
|
||||
uninsurable_med_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "Jane",
|
||||
"lastName": "Smith",
|
||||
"dob": "20/07/1970",
|
||||
"gender": "Female",
|
||||
"weight": 150,
|
||||
"heightFt": 5,
|
||||
"heightIn": 6,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
|
||||
"phq": {
|
||||
"medications": [
|
||||
{
|
||||
"name": "Warfarin",
|
||||
"dosage": "5mg",
|
||||
"frequency": "daily"
|
||||
}
|
||||
],
|
||||
"conditions": [],
|
||||
"issues": []
|
||||
},
|
||||
"income": 60000,
|
||||
"address": {
|
||||
"address1": "456 Oak Ave",
|
||||
"city": "Somewhere",
|
||||
"state": "NY",
|
||||
"zipcode": "67890"
|
||||
}
|
||||
}
|
||||
|
||||
result2 = estimation_service._validate_application(uninsurable_med_request)
|
||||
print(f"Eligible: {result2['is_eligible']}")
|
||||
print(f"Issues: {result2['issues']}")
|
||||
print(f"Warnings: {result2['warnings']}")
|
||||
print()
|
||||
|
||||
# Test Case 3: Uninsurable condition
|
||||
print("=== Test Case 3: Uninsurable Condition ===")
|
||||
uninsurable_condition_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "Bob",
|
||||
"lastName": "Johnson",
|
||||
"dob": "10/12/1965",
|
||||
"gender": "Male",
|
||||
"weight": 200,
|
||||
"heightFt": 5,
|
||||
"heightIn": 10,
|
||||
"nicotine": True
|
||||
}
|
||||
],
|
||||
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
|
||||
"phq": {
|
||||
"medications": [],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "cancer",
|
||||
"description": "Lung cancer diagnosed 2 years ago"
|
||||
}
|
||||
],
|
||||
"issues": []
|
||||
},
|
||||
"income": 40000,
|
||||
"address": {
|
||||
"address1": "789 Pine St",
|
||||
"city": "Elsewhere",
|
||||
"state": "TX",
|
||||
"zipcode": "54321"
|
||||
}
|
||||
}
|
||||
|
||||
result3 = estimation_service._validate_application(uninsurable_condition_request)
|
||||
print(f"Eligible: {result3['is_eligible']}")
|
||||
print(f"Issues: {result3['issues']}")
|
||||
print(f"Warnings: {result3['warnings']}")
|
||||
print()
|
||||
|
||||
# Test Case 4: Height/weight issues
|
||||
print("=== Test Case 4: Height/Weight Issues ===")
|
||||
weight_issue_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "Alice",
|
||||
"lastName": "Brown",
|
||||
"dob": "05/09/1980",
|
||||
"gender": "Female",
|
||||
"weight": 300, # Very high weight
|
||||
"heightFt": 5,
|
||||
"heightIn": 4,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
|
||||
"phq": {
|
||||
"medications": [],
|
||||
"conditions": [],
|
||||
"issues": []
|
||||
},
|
||||
"income": 70000,
|
||||
"address": {
|
||||
"address1": "321 Elm St",
|
||||
"city": "Nowhere",
|
||||
"state": "FL",
|
||||
"zipcode": "98765"
|
||||
}
|
||||
}
|
||||
|
||||
result4 = estimation_service._validate_application(weight_issue_request)
|
||||
print(f"Eligible: {result4['is_eligible']}")
|
||||
print(f"Issues: {result4['issues']}")
|
||||
print(f"Warnings: {result4['warnings']}")
|
||||
print()
|
||||
|
||||
# Test Case 5: Diabetes risk factors
|
||||
print("=== Test Case 5: Diabetes Risk Factors ===")
|
||||
diabetes_risk_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "Charlie",
|
||||
"lastName": "Wilson",
|
||||
"dob": "15/01/1975",
|
||||
"gender": "Male",
|
||||
"weight": 220,
|
||||
"heightFt": 6,
|
||||
"heightIn": 2,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
|
||||
"phq": {
|
||||
"medications": [
|
||||
{
|
||||
"name": "Insulin",
|
||||
"dosage": "60 units",
|
||||
"frequency": "daily"
|
||||
},
|
||||
{
|
||||
"name": "Metformin",
|
||||
"dosage": "1000mg",
|
||||
"frequency": "twice daily"
|
||||
},
|
||||
{
|
||||
"name": "Glipizide",
|
||||
"dosage": "10mg",
|
||||
"frequency": "daily"
|
||||
},
|
||||
{
|
||||
"name": "Lisinopril",
|
||||
"dosage": "20mg",
|
||||
"frequency": "daily"
|
||||
},
|
||||
{
|
||||
"name": "Amlodipine",
|
||||
"dosage": "10mg",
|
||||
"frequency": "daily"
|
||||
},
|
||||
{
|
||||
"name": "Hydrochlorothiazide",
|
||||
"dosage": "25mg",
|
||||
"frequency": "daily"
|
||||
}
|
||||
],
|
||||
"conditions": [],
|
||||
"issues": []
|
||||
},
|
||||
"income": 55000,
|
||||
"address": {
|
||||
"address1": "654 Maple Dr",
|
||||
"city": "Someplace",
|
||||
"state": "OH",
|
||||
"zipcode": "11111"
|
||||
}
|
||||
}
|
||||
|
||||
result5 = estimation_service._validate_application(diabetes_risk_request)
|
||||
print(f"Eligible: {result5['is_eligible']}")
|
||||
print(f"Issues: {result5['issues']}")
|
||||
print(f"Warnings: {result5['warnings']}")
|
||||
print()
|
||||
|
||||
# Test Case 6: Multiple applicants with mixed eligibility
|
||||
print("=== Test Case 6: Multiple Applicants ===")
|
||||
multiple_applicants_request = {
|
||||
"applicants": [
|
||||
{
|
||||
"firstName": "David",
|
||||
"lastName": "Miller",
|
||||
"dob": "12/06/1988",
|
||||
"gender": "Male",
|
||||
"weight": 175,
|
||||
"heightFt": 5,
|
||||
"heightIn": 11,
|
||||
"nicotine": False
|
||||
},
|
||||
{
|
||||
"firstName": "Sarah",
|
||||
"lastName": "Miller",
|
||||
"dob": "08/11/1990",
|
||||
"gender": "Female",
|
||||
"weight": 140,
|
||||
"heightFt": 5,
|
||||
"heightIn": 6,
|
||||
"nicotine": False
|
||||
}
|
||||
],
|
||||
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
|
||||
"phq": {
|
||||
"medications": [
|
||||
{
|
||||
"name": "Donepezil",
|
||||
"dosage": "10mg",
|
||||
"frequency": "daily"
|
||||
}
|
||||
],
|
||||
"conditions": [],
|
||||
"issues": []
|
||||
},
|
||||
"income": 80000,
|
||||
"address": {
|
||||
"address1": "987 Cedar Ln",
|
||||
"city": "Anywhere",
|
||||
"state": "WA",
|
||||
"zipcode": "22222"
|
||||
}
|
||||
}
|
||||
|
||||
result6 = estimation_service._validate_application(multiple_applicants_request)
|
||||
print(f"Eligible: {result6['is_eligible']}")
|
||||
print(f"Issues: {result6['issues']}")
|
||||
print(f"Warnings: {result6['warnings']}")
|
||||
print("Applicant validations:")
|
||||
for app_val in result6['applicant_validations']:
|
||||
print(f" {app_val['name']}: BMI={app_val['bmi']:.1f}, Category={app_val['premium_category']}, Eligible={app_val['is_eligible']}")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_validation())
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import httpx
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from ..config import settings
|
||||
from .session_service import session_service
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from src.config import settings
|
||||
from src.services.session_service import session_service
|
||||
|
||||
class EstimationService:
|
||||
"""Service for handling insurance estimation via TALESTORM API"""
|
||||
|
|
@ -11,6 +12,331 @@ class EstimationService:
|
|||
self.base_url = settings.TALESTORM_API_BASE_URL
|
||||
self.api_key = settings.TALESTORM_API_KEY
|
||||
self.agent_id = settings.TALESTORM_ESTIMATION_AGENT_ID
|
||||
self.insurance_api_base_url = settings.VIRGIL_API_BASE_URL
|
||||
|
||||
# Initialize uninsurable conditions and medications
|
||||
self._initialize_underwriting_guidelines()
|
||||
|
||||
def _initialize_underwriting_guidelines(self):
|
||||
"""Initialize uninsurable conditions and medications based on IHA guidelines"""
|
||||
|
||||
# Permanently uninsurable conditions
|
||||
self.permanently_uninsurable_conditions = {
|
||||
"organ_transplant", "amputation_disease", "emphysema", "chronic_pulmonary",
|
||||
"oxygen_use", "nebulizer_use", "parkinsons", "multiple_sclerosis", "als",
|
||||
"lupus", "myasthenia_gravis", "alzheimers", "dementia", "cognitive_disorders",
|
||||
"aids", "arc", "hiv"
|
||||
}
|
||||
|
||||
# Conditions uninsurable if diagnosed/treated in past 5 years
|
||||
self.recent_uninsurable_conditions = {
|
||||
"coronary_artery_disease", "heart_failure", "pacemaker", "defibrillator",
|
||||
"enlarged_heart", "valve_surgery", "stroke", "tia", "carotid_disease",
|
||||
"peripheral_vascular_disease", "rheumatoid_arthritis", "crippling_arthritis",
|
||||
"cancer", "kidney_disease", "liver_disease", "hepatitis", "cirrhosis",
|
||||
"alcoholism", "drug_abuse", "mental_nervous_hospitalization",
|
||||
"heart_rhythm_disorders", "osteoporosis_fractures"
|
||||
}
|
||||
|
||||
# Uninsurable medications (partial list)
|
||||
self.uninsurable_medications = {
|
||||
# HIV/AIDS medications
|
||||
"abacavir", "lamivudine", "zidovudine", "tenofovir", "efavirenz", "nevirapine",
|
||||
"atazanavir", "darunavir", "raltegravir", "dolutegravir",
|
||||
|
||||
# Cancer medications
|
||||
"abarelix", "chlorambucil", "tamoxifen", "anastrozole", "letrozole",
|
||||
"exemestane", "fulvestrant", "bicalutamide", "flutamide", "goserelin",
|
||||
"leuprolide", "triptorelin", "bexarotene", "vorinostat", "romidepsin",
|
||||
|
||||
# Severe arthritis medications
|
||||
"adalimumab", "methotrexate", "infliximab", "etanercept", "golimumab",
|
||||
"certolizumab", "ustekinumab", "secukinumab", "ixekizumab", "guselkumab",
|
||||
"tildrakizumab", "risankizumab", "upadacitinib", "tofacitinib", "baricitinib",
|
||||
|
||||
# Dementia medications
|
||||
"donepezil", "memantine", "galantamine", "rivastigmine", "tacrine",
|
||||
|
||||
# Schizophrenia/psychosis medications
|
||||
"aripiprazole", "olanzapine", "risperidone", "quetiapine", "ziprasidone",
|
||||
"clozapine", "paliperidone", "asenapine", "iloperidone", "lurasidone",
|
||||
"cariprazine", "brexpiprazole", "lumateperone",
|
||||
|
||||
# Parkinson's disease medications
|
||||
"carbidopa", "levodopa", "selegiline", "rasagiline", "entacapone",
|
||||
"tolcapone", "pramipexole", "ropinirole", "rotigotine", "apomorphine",
|
||||
"amantadine", "trihexyphenidyl", "benztropine",
|
||||
|
||||
# Multiple sclerosis medications
|
||||
"glatiramer", "interferon_beta", "natalizumab", "fingolimod", "teriflunomide",
|
||||
"dimethyl_fumarate", "alemtuzumab", "ocrelizumab", "cladribine", "siponimod",
|
||||
"ozanimod", "ponesimod",
|
||||
|
||||
# Severe cardiovascular disease medications
|
||||
"amiodarone", "warfarin", "dabigatran", "rivaroxaban", "apixaban",
|
||||
"edoxaban", "clopidogrel", "ticagrelor", "prasugrel", "dipyridamole",
|
||||
"cilostazol", "pentoxifylline"
|
||||
}
|
||||
|
||||
# Height and weight thresholds for premium categories
|
||||
self.height_weight_chart = {
|
||||
"5'0": {"preferred": (90, 102), "standard": (103, 218), "high": (219, 260)},
|
||||
"5'1": {"preferred": (93, 105), "standard": (106, 221), "high": (222, 264)},
|
||||
"5'2": {"preferred": (96, 108), "standard": (109, 224), "high": (225, 268)},
|
||||
"5'3": {"preferred": (99, 111), "standard": (112, 227), "high": (228, 272)},
|
||||
"5'4": {"preferred": (102, 114), "standard": (115, 230), "high": (231, 276)},
|
||||
"5'5": {"preferred": (105, 117), "standard": (118, 233), "high": (234, 280)},
|
||||
"5'6": {"preferred": (108, 120), "standard": (121, 236), "high": (237, 284)},
|
||||
"5'7": {"preferred": (111, 123), "standard": (124, 239), "high": (240, 288)},
|
||||
"5'8": {"preferred": (114, 126), "standard": (127, 242), "high": (243, 292)},
|
||||
"5'9": {"preferred": (117, 129), "standard": (130, 245), "high": (246, 296)},
|
||||
"5'10": {"preferred": (120, 132), "standard": (133, 248), "high": (249, 300)},
|
||||
"5'11": {"preferred": (123, 135), "standard": (136, 251), "high": (252, 304)},
|
||||
"6'0": {"preferred": (126, 138), "standard": (139, 254), "high": (255, 308)},
|
||||
"6'1": {"preferred": (129, 141), "standard": (142, 257), "high": (258, 312)},
|
||||
"6'2": {"preferred": (132, 144), "standard": (145, 260), "high": (261, 316)},
|
||||
"6'3": {"preferred": (135, 147), "standard": (148, 263), "high": (264, 320)},
|
||||
"6'4": {"preferred": (138, 150), "standard": (151, 266), "high": (267, 324)},
|
||||
"6'5": {"preferred": (141, 153), "standard": (154, 269), "high": (270, 328)},
|
||||
"6'6": {"preferred": (144, 156), "standard": (157, 272), "high": (273, 332)}
|
||||
}
|
||||
|
||||
def _check_uninsurable_conditions(self, phq: Dict[str, Any]) -> List[str]:
|
||||
"""Check for uninsurable conditions in PHQ data"""
|
||||
issues = []
|
||||
|
||||
if not phq:
|
||||
return issues
|
||||
|
||||
# Check conditions
|
||||
for condition in phq.get("conditions", []):
|
||||
condition_desc = condition.get("description", "").lower()
|
||||
condition_key = condition.get("key", "").lower()
|
||||
|
||||
# Check for permanently uninsurable conditions
|
||||
for uninsurable in self.permanently_uninsurable_conditions:
|
||||
if uninsurable in condition_desc or uninsurable in condition_key:
|
||||
issues.append(f"Permanently uninsurable condition: {condition.get('description', '')}")
|
||||
|
||||
# Check for recent uninsurable conditions (past 5 years)
|
||||
for recent in self.recent_uninsurable_conditions:
|
||||
if recent in condition_desc or recent in condition_key:
|
||||
issues.append(f"Recent uninsurable condition (past 5 years): {condition.get('description', '')}")
|
||||
|
||||
# Check health issues
|
||||
for issue in phq.get("issues", []):
|
||||
issue_key = issue.get("key", "").lower()
|
||||
issue_desc = " ".join([detail.get("description", "") for detail in issue.get("details", [])]).lower()
|
||||
|
||||
# Check for uninsurable issues
|
||||
for uninsurable in self.permanently_uninsurable_conditions:
|
||||
if uninsurable in issue_desc or uninsurable in issue_key:
|
||||
issues.append(f"Permanently uninsurable health issue: {issue.get('key', '')}")
|
||||
|
||||
for recent in self.recent_uninsurable_conditions:
|
||||
if recent in issue_desc or recent in issue_key:
|
||||
issues.append(f"Recent uninsurable health issue (past 5 years): {issue.get('key', '')}")
|
||||
|
||||
return issues
|
||||
|
||||
def _check_uninsurable_medications(self, phq: Dict[str, Any]) -> List[str]:
|
||||
"""Check for uninsurable medications in PHQ data"""
|
||||
issues = []
|
||||
|
||||
if not phq:
|
||||
return issues
|
||||
|
||||
for medication in phq.get("medications", []):
|
||||
med_name = medication.get("name", "").lower()
|
||||
|
||||
# Check against uninsurable medication list
|
||||
for uninsurable_med in self.uninsurable_medications:
|
||||
if uninsurable_med in med_name:
|
||||
issues.append(f"Uninsurable medication: {medication.get('name', '')}")
|
||||
break
|
||||
|
||||
return issues
|
||||
|
||||
def _check_diabetes_risk_factors(self, phq: Dict[str, Any]) -> List[str]:
|
||||
"""Check for diabetes-related uninsurability factors"""
|
||||
issues = []
|
||||
|
||||
if not phq:
|
||||
return issues
|
||||
|
||||
# Count diabetes and hypertension medications
|
||||
diabetes_meds = []
|
||||
hypertension_meds = []
|
||||
|
||||
for medication in phq.get("medications", []):
|
||||
med_name = medication.get("name", "").lower()
|
||||
|
||||
# Diabetes medications
|
||||
if any(dm_med in med_name for dm_med in ["insulin", "metformin", "glipizide", "glyburide", "glimepiride", "sitagliptin", "saxagliptin", "linagliptin", "alogliptin", "empagliflozin", "dapagliflozin", "canagliflozin", "dulaglutide", "liraglutide", "semaglutide", "exenatide"]):
|
||||
diabetes_meds.append(medication.get("name", ""))
|
||||
|
||||
# Hypertension medications
|
||||
if any(htn_med in med_name for htn_med in ["lisinopril", "enalapril", "ramipril", "quinapril", "benazepril", "fosinopril", "trandolapril", "moexipril", "perindopril", "losartan", "valsartan", "irbesartan", "candesartan", "olmesartan", "telmisartan", "eprosartan", "azilsartan", "amlodipine", "nifedipine", "felodipine", "isradipine", "nicardipine", "nifedipine", "diltiazem", "verapamil", "atenolol", "metoprolol", "propranolol", "carvedilol", "nebivolol", "bisoprolol", "hydrochlorothiazide", "chlorthalidone", "indapamide", "furosemide", "bumetanide", "torsemide", "spironolactone", "eplerenone", "doxazosin", "terazosin", "prazosin", "clonidine", "methyldopa", "hydralazine", "minoxidil"]):
|
||||
hypertension_meds.append(medication.get("name", ""))
|
||||
|
||||
# Check for diabetes risk factors
|
||||
if len(diabetes_meds) >= 3:
|
||||
issues.append(f"High diabetes medication count ({len(diabetes_meds)}): {', '.join(diabetes_meds)}")
|
||||
|
||||
if len(hypertension_meds) >= 3:
|
||||
issues.append(f"High hypertension medication count ({len(hypertension_meds)}): {', '.join(hypertension_meds)}")
|
||||
|
||||
# Check for insulin use > 50 units/day
|
||||
for medication in phq.get("medications", []):
|
||||
if "insulin" in medication.get("name", "").lower():
|
||||
dosage = medication.get("dosage", "")
|
||||
frequency = medication.get("frequency", "")
|
||||
|
||||
# Simple check for high insulin dosage (this would need more sophisticated parsing)
|
||||
if "50" in dosage or "100" in dosage:
|
||||
issues.append(f"High insulin dosage detected: {medication.get('name', '')} {dosage} {frequency}")
|
||||
|
||||
return issues
|
||||
|
||||
def _check_height_weight_eligibility(self, applicant: Dict[str, Any]) -> Tuple[str, str]:
|
||||
"""Check height/weight eligibility and determine premium category"""
|
||||
height_ft = applicant.get("heightFt", 0)
|
||||
height_in = applicant.get("heightIn", 0)
|
||||
weight = applicant.get("weight", 0)
|
||||
|
||||
if not all([height_ft, height_in, weight]):
|
||||
return "unknown", "Missing height or weight information"
|
||||
|
||||
# Format height as "5'6" style
|
||||
height_key = f"{height_ft}'{height_in}"
|
||||
|
||||
if height_key not in self.height_weight_chart:
|
||||
return "unknown", f"Height {height_key} not in eligibility chart"
|
||||
|
||||
thresholds = self.height_weight_chart[height_key]
|
||||
|
||||
if weight < thresholds["preferred"][0]:
|
||||
return "underweight", f"Weight {weight} lbs below minimum for height {height_key}"
|
||||
elif weight <= thresholds["preferred"][1]:
|
||||
return "preferred", "Eligible for preferred premium"
|
||||
elif weight <= thresholds["standard"][1]:
|
||||
return "standard", "Standard premium category"
|
||||
elif weight <= thresholds["high"][1]:
|
||||
return "high", "High premium category due to weight"
|
||||
else:
|
||||
return "uninsurable", f"Weight {weight} lbs exceeds maximum for height {height_key}"
|
||||
|
||||
return "unknown", "Unable to determine eligibility"
|
||||
|
||||
def _calculate_bmi(self, applicant: Dict[str, Any]) -> float:
|
||||
"""Calculate BMI for an applicant"""
|
||||
height_ft = applicant.get("heightFt", 0)
|
||||
height_in = applicant.get("heightIn", 0)
|
||||
weight = applicant.get("weight", 0)
|
||||
|
||||
if not all([height_ft, height_in, weight]):
|
||||
return 0.0
|
||||
|
||||
# Convert to total inches
|
||||
total_inches = (height_ft * 12) + height_in
|
||||
|
||||
# Convert to meters
|
||||
height_m = total_inches * 0.0254
|
||||
|
||||
# Convert weight to kg
|
||||
weight_kg = weight * 0.453592
|
||||
|
||||
# Calculate BMI
|
||||
if height_m > 0:
|
||||
return weight_kg / (height_m * height_m)
|
||||
|
||||
return 0.0
|
||||
|
||||
def _validate_application(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Perform comprehensive IHA underwriting validation"""
|
||||
validation_result = {
|
||||
"is_eligible": True,
|
||||
"issues": [],
|
||||
"warnings": [],
|
||||
"applicant_validations": []
|
||||
}
|
||||
|
||||
applicants = request_data.get("applicants", [])
|
||||
phq = request_data.get("phq", {})
|
||||
|
||||
# Check for uninsurable conditions
|
||||
condition_issues = self._check_uninsurable_conditions(phq)
|
||||
validation_result["issues"].extend(condition_issues)
|
||||
|
||||
# Check for uninsurable medications
|
||||
medication_issues = self._check_uninsurable_medications(phq)
|
||||
validation_result["issues"].extend(medication_issues)
|
||||
|
||||
# Check diabetes risk factors
|
||||
diabetes_issues = self._check_diabetes_risk_factors(phq)
|
||||
validation_result["issues"].extend(diabetes_issues)
|
||||
|
||||
# Validate each applicant
|
||||
for i, applicant in enumerate(applicants):
|
||||
applicant_validation = {
|
||||
"applicant_index": i,
|
||||
"name": f"{applicant.get('firstName', '')} {applicant.get('lastName', '')}",
|
||||
"is_eligible": True,
|
||||
"issues": [],
|
||||
"warnings": [],
|
||||
"bmi": 0.0,
|
||||
"premium_category": "unknown"
|
||||
}
|
||||
|
||||
# Calculate BMI
|
||||
bmi = self._calculate_bmi(applicant)
|
||||
applicant_validation["bmi"] = bmi
|
||||
|
||||
# Check height/weight eligibility
|
||||
premium_category, message = self._check_height_weight_eligibility(applicant)
|
||||
applicant_validation["premium_category"] = premium_category
|
||||
|
||||
if premium_category == "uninsurable":
|
||||
applicant_validation["is_eligible"] = False
|
||||
applicant_validation["issues"].append(f"Height/weight: {message}")
|
||||
elif premium_category == "underweight":
|
||||
applicant_validation["warnings"].append(f"Height/weight: {message}")
|
||||
elif premium_category == "high":
|
||||
applicant_validation["warnings"].append(f"Height/weight: {message}")
|
||||
|
||||
# Check for nicotine use
|
||||
if applicant.get("nicotine"):
|
||||
applicant_validation["warnings"].append("Nicotine use detected - may affect premium")
|
||||
|
||||
# Check age
|
||||
try:
|
||||
dob = applicant.get("dob", "")
|
||||
if dob:
|
||||
# Parse date of birth (assuming format like "dd/mm/yyyy")
|
||||
if "/" in dob:
|
||||
day, month, year = dob.split("/")
|
||||
birth_date = datetime(int(year), int(month), int(day))
|
||||
age = (datetime.now() - birth_date).days // 365
|
||||
|
||||
if age > 80:
|
||||
applicant_validation["warnings"].append(f"Advanced age ({age} years) may affect eligibility")
|
||||
elif age < 18:
|
||||
applicant_validation["is_eligible"] = False
|
||||
applicant_validation["issues"].append("Applicant must be 18 or older")
|
||||
except:
|
||||
applicant_validation["warnings"].append("Unable to determine age from date of birth")
|
||||
|
||||
validation_result["applicant_validations"].append(applicant_validation)
|
||||
|
||||
# If any applicant is ineligible, mark overall as ineligible
|
||||
if not applicant_validation["is_eligible"]:
|
||||
validation_result["is_eligible"] = False
|
||||
|
||||
# If there are any critical issues, mark as ineligible
|
||||
if validation_result["issues"]:
|
||||
validation_result["is_eligible"] = False
|
||||
|
||||
return validation_result
|
||||
|
||||
async def get_client(self) -> httpx.AsyncClient:
|
||||
"""Get HTTP client for TALESTORM API"""
|
||||
|
|
@ -26,8 +352,28 @@ class EstimationService:
|
|||
headers=headers
|
||||
)
|
||||
|
||||
def _format_estimation_request(self, request_data: Dict[str, Any]) -> str:
|
||||
"""Format the estimation request as a natural language message for the AI"""
|
||||
async def get_plan_details(self, plan_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch plan details from the insurance API"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.insurance_api_base_url}/insurance/plans/{plan_id}",
|
||||
headers={"accept": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to fetch plan {plan_id}: {response.status_code} {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error fetching plan {plan_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
async def _format_estimation_request_with_plan_details(self, request_data: Dict[str, Any]) -> str:
|
||||
"""Format the estimation request with fetched plan details"""
|
||||
applicants = request_data.get("applicants", [])
|
||||
plans = request_data.get("plans", [])
|
||||
phq = request_data.get("phq", {})
|
||||
|
|
@ -48,10 +394,30 @@ class EstimationService:
|
|||
message_parts.append(f" Height: {applicant.get('heightFt', '')}'{applicant.get('heightIn', '')}\"")
|
||||
message_parts.append(f" Nicotine use: {'Yes' if applicant.get('nicotine') else 'No'}")
|
||||
|
||||
# Add plan information
|
||||
# Fetch and add detailed plan information for all plans
|
||||
if plans:
|
||||
plan = plans[0]
|
||||
message_parts.append(f"\nPlan: Coverage type {plan.get('coverage', '')}")
|
||||
message_parts.append("\nPlan Details:")
|
||||
for i, plan in enumerate(plans):
|
||||
plan_id = plan.get('id')
|
||||
if plan_id:
|
||||
plan_details = await self.get_plan_details(plan_id)
|
||||
if plan_details:
|
||||
message_parts.append(f"\nPlan {i+1}:")
|
||||
message_parts.append(f"- Plan Name: {plan_details.get('name', 'N/A')}")
|
||||
message_parts.append(f"- Coverage Type: {plan_details.get('coverage_type', 'N/A')}")
|
||||
message_parts.append(f"- Deductible: ${plan_details.get('deductible', 0):,.2f}")
|
||||
message_parts.append(f"- Premium: ${plan_details.get('premium', 0):,.2f}")
|
||||
message_parts.append(f"- Coinsurance: {plan_details.get('coinsurance', 0)}%")
|
||||
message_parts.append(f"- Copay: ${plan_details.get('copay', 0):,.2f}")
|
||||
if plan_details.get('benefits'):
|
||||
message_parts.append("- Benefits:")
|
||||
for benefit in plan_details['benefits']:
|
||||
message_parts.append(f" * {benefit.get('name', '')}: {benefit.get('description', '')}")
|
||||
else:
|
||||
# Fallback to basic plan info if API call fails
|
||||
message_parts.append(f"\nPlan {i+1}: Coverage type {plan.get('coverage', '')}")
|
||||
else:
|
||||
message_parts.append(f"\nPlan {i+1}: Coverage type {plan.get('coverage', '')}")
|
||||
|
||||
# Add PHQ information
|
||||
if phq:
|
||||
|
|
@ -123,11 +489,28 @@ class EstimationService:
|
|||
async def estimate_insurance(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Send estimation request to TALESTORM API and parse response"""
|
||||
try:
|
||||
# Perform IHA underwriting validation first
|
||||
validation_result = self._validate_application(request_data)
|
||||
|
||||
# If application is not eligible, return rejection immediately
|
||||
if not validation_result["is_eligible"]:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"details": {
|
||||
"dtq": False,
|
||||
"reason": f"Application not eligible: {'; '.join(validation_result['issues'])}",
|
||||
"tier": 4,
|
||||
"total_price": 0.0
|
||||
},
|
||||
"results": [],
|
||||
"validation": validation_result
|
||||
}
|
||||
|
||||
# Create or get existing session with estimation agent
|
||||
session_id = await session_service.create_session(agent_id=self.agent_id)
|
||||
|
||||
# Format the request as a natural language message
|
||||
estimation_message = self._format_estimation_request(request_data)
|
||||
# Format the request as a natural language message with plan details
|
||||
estimation_message = await self._format_estimation_request_with_plan_details(request_data)
|
||||
|
||||
# Send request to TALESTORM API
|
||||
async with await self.get_client() as client:
|
||||
|
|
@ -194,6 +577,15 @@ class EstimationService:
|
|||
if field not in result:
|
||||
raise ValueError(f"Missing required result field: {field}")
|
||||
|
||||
# Add validation information to the response
|
||||
parsed_response["validation"] = validation_result
|
||||
|
||||
# If there are warnings, add them to the response
|
||||
if validation_result["warnings"]:
|
||||
if "warnings" not in parsed_response:
|
||||
parsed_response["warnings"] = []
|
||||
parsed_response["warnings"].extend(validation_result["warnings"])
|
||||
|
||||
return parsed_response
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
|
|
@ -208,7 +600,8 @@ class EstimationService:
|
|||
"tier": 4,
|
||||
"total_price": 0.0
|
||||
},
|
||||
"results": []
|
||||
"results": [],
|
||||
"validation": validation_result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -221,7 +614,8 @@ class EstimationService:
|
|||
"tier": 4,
|
||||
"total_price": 0.0
|
||||
},
|
||||
"results": []
|
||||
"results": [],
|
||||
"validation": validation_result if 'validation_result' in locals() else None
|
||||
}
|
||||
|
||||
# Global estimation service instance
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import httpx
|
||||
import uuid
|
||||
from typing import Optional, Dict, Any, List
|
||||
from ..config import settings
|
||||
from src.config import settings
|
||||
|
||||
class SessionService:
|
||||
"""Service for managing chat sessions with talestorm-ai"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue