initial commit

This commit is contained in:
ipu 2025-07-25 14:01:16 +03:00
commit aaba8753ef
36 changed files with 3682 additions and 0 deletions

1
src/services/__init__.py Normal file
View file

@ -0,0 +1 @@
# Services package

View file

@ -0,0 +1,137 @@
import httpx
from typing import Dict, Any, List, Optional
from ..config import settings
from .session_service import session_service
from ..api.v1.models import Source, HistoryItem
class ChatService:
"""Service for handling chat functionality with talestorm-ai"""
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(
headers=headers
)
async def send_message(self, session_id: str, message: str) -> Dict[str, Any]:
"""Send a message to talestorm-ai and get response"""
async with await self.get_client() as client:
try:
response = await client.post(self.base_url + "/chat/", json={
"chat_session_id": session_id,
"user_message": message
})
if response.status_code == 200:
return response.json()
else:
print(response.url)
print(response.status_code)
print(response.json())
raise Exception(f"Error sending message to talestorm-ai: {response.status_code} {response.json()}")
except Exception as e:
return {
"chat_session_id": session_id,
"message": f"I'm sorry, I'm experiencing technical difficulties. Please try again later. Error: {str(e)}"
}
async def get_chat_history(self, session_id: str) -> List[HistoryItem]:
"""Get chat history for a session and format it properly"""
async with await self.get_client() as client:
try:
response = await client.get(self.base_url + "/chat/", params={"chat_session_id": session_id})
if response.status_code == 200:
messages = response.json()
history = []
for message in messages:
content = message.get("content", [])
for item in content:
if not isinstance(item, dict):
continue
kind = item.get("kind", "")
# Handle request messages (user input)
if kind == "request":
parts = item.get("parts", [])
for part in parts:
if part.get("part_kind") == "user-prompt":
history.append(HistoryItem(
role="user",
message=part.get("content", "")
))
# Handle response messages (assistant output)
elif kind == "response":
parts = item.get("parts", [])
for part in parts:
if part.get("part_kind") == "text":
history.append(HistoryItem(
role="assistant",
message=part.get("content", "")
))
return history
return []
except Exception as e:
print(f"Error getting chat history: {e}")
return []
def _extract_sources_from_response(self, response_text: str) -> List[Source]:
"""Extract sources from RAG search results if available"""
# This is a placeholder - in a real implementation, you would:
# 1. Check if the response contains RAG search results
# 2. Parse the results to extract plan names, chunk numbers, and content
# 3. Return properly formatted Source objects
# For now, return empty list - this would be populated when RAG is implemented
return []
async def process_insurance_chat(self, message: str, session_id: Optional[str] = None) -> Dict[str, Any]:
"""Process an insurance chat request"""
try:
# Create session if not provided
if not session_id:
session_id = await session_service.create_session()
# Validate session if provided
elif not await session_service.validate_session(session_id):
# Create new session if invalid
session_id = await session_service.create_session()
# Send message to talestorm-ai
chat_response = await self.send_message(session_id, message)
# Get chat history
history = await self.get_chat_history(session_id)
# Extract sources from the response (placeholder for RAG implementation)
sources = self._extract_sources_from_response(chat_response.get("message", ""))
return {
"session_id": session_id,
"answer": chat_response.get("message", "No response received"),
"sources": sources,
"history": history
}
except Exception as e:
return {
"session_id": session_id or "fallback-session",
"answer": f"I'm sorry, I'm experiencing technical difficulties. Please try again later. Error: {str(e)}",
"sources": [],
"history": []
}
# Global chat service instance
chat_service = ChatService()

View file

@ -0,0 +1,225 @@
from datetime import datetime
import json
from typing import List, Dict, Any, Optional
# ---------------- Constants ---------------- #
DECLINABLE_CONDITIONS = {
"aids", "organ_transplant", "multiple_sclerosis", "alzheimer_disease",
"heart_attack", "congestive_heart_failure", "bipolar", "seizures"
}
DECLINABLE_ISSUE_KEYS = {"heart_attack", "congestive_heart_failure", "bipolar", "seizures"}
HIGH_RISK_FLAGS = {"50_units", "treat_diabetes"}
PRICING_TABLE = {
1: [122, 243, 219, 365],
1.5: [127, 253, 228, 379],
2: [133, 265, 239, 397],
2.5: [139, 277, 249, 415],
3: [145, 290, 261, 435],
3.5: [151, 302, 272, 452],
4: [158, 315, 283, 472],
4.5: [164, 327, 295, 491],
5: [172, 343, 309, 514],
5.5: [182, 364, 327, 545],
6: [196, 390, 351, 585],
6.5: [209, 417, 376, 626],
7: [222, 444, 400, 666]
}
# Simplified drug price map (in a real implementation, you'd load this from a file)
DRUG_PRICE_MAP = {
"metformin": 15.0,
"insulin": 45.0,
"aspirin": 8.0,
"atorvastatin": 25.0,
"lisinopril": 12.0,
"amlodipine": 18.0,
"omeprazole": 22.0,
"simvastatin": 20.0,
"hydrochlorothiazide": 10.0,
"losartan": 30.0
}
# ---------------- Helper Functions ---------------- #
def calculate_age(dob_str: str, effective_date_str: str) -> Optional[int]:
"""Calculate age from date of birth and effective date"""
try:
dob = datetime.strptime(dob_str, "%d/%m/%Y")
ref = datetime.strptime(effective_date_str, "%d/%m/%Y")
return (ref - dob).days // 365
except (ValueError, TypeError):
return None
def calculate_bmi(weight: float, ft: int, inch: int) -> Optional[float]:
"""Calculate BMI from weight and height"""
try:
inches = ft * 12 + inch
return (weight / (inches ** 2)) * 703
except (ValueError, TypeError, ZeroDivisionError):
return None
def check_declinable(phq: Dict[str, Any]) -> bool:
"""Check if application should be declined based on PHQ data"""
if phq.get("treatment") or phq.get("invalid"):
return True
for cond in phq.get("conditions", []):
if cond.get("key") in DECLINABLE_CONDITIONS:
return True
for issue in phq.get("issues", []):
for detail in issue.get("details", []):
if detail.get("key") in DECLINABLE_ISSUE_KEYS:
return True
return False
def base_tier(age: Optional[int]) -> float:
"""Calculate base tier based on age"""
if age is None:
return 4.0
if age <= 30:
return 1.0
elif age <= 40:
return 2.0
elif age <= 50:
return 3.0
elif age <= 60:
return 3.5
else:
return 4.0
def adjust_tier(tier: float, bmi: Optional[float], meds: List[Dict[str, Any]],
issues: List[Dict[str, Any]], applicant_index: int) -> float:
"""Adjust tier based on health factors"""
if bmi is not None and (bmi < 16 or bmi > 40):
tier += 1.0
personal_meds = [m for m in meds if m.get("applicant") == applicant_index]
if len(personal_meds) >= 3:
tier += 0.5
for issue in issues:
for detail in issue.get("details", []):
if detail.get("key") in HIGH_RISK_FLAGS:
tier += 0.5
return round(min(tier, 7.0) * 2) / 2
def calculate_rx_spend(medications: List[Dict[str, Any]], applicant_index: int) -> float:
"""Calculate total prescription spend for an applicant"""
total = 0.0
for med in medications:
if med.get("applicant") != applicant_index:
continue
drug = med.get("name", "").lower().strip()
if drug in DRUG_PRICE_MAP:
total += DRUG_PRICE_MAP[drug]
return round(total, 2)
def get_rx_price(tier: float, coverage_type: int) -> float:
"""Get prescription price based on tier and coverage type"""
tier = round(tier * 2) / 2
coverage_index = {
1: 0, 2: 1, 3: 2, 4: 3
}.get(coverage_type, 0)
return PRICING_TABLE.get(tier, PRICING_TABLE[5])[coverage_index]
# ---------------- Main Underwriting Logic ---------------- #
def run_underwriting(applicants: List[Dict[str, Any]], phq: Dict[str, Any],
plans: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Run comprehensive underwriting analysis"""
results = []
max_tier = 0
dtq_found = False
ages = []
# Calculate ages for all applicants
for applicant in applicants:
age = calculate_age(applicant.get("dob"), phq.get("effectiveDate"))
if age is not None:
ages.append(age)
oldest_age = max(ages) if ages else None
base = base_tier(oldest_age) if oldest_age else None
coverage = plans[0]["coverage"] if plans else 1
coverage_index = {1: 0, 2: 1, 3: 2, 4: 3}.get(coverage, 0)
for idx, applicant in enumerate(applicants):
try:
age = calculate_age(applicant.get("dob"), phq.get("effectiveDate"))
bmi = calculate_bmi(
applicant.get("weight"),
applicant.get("heightFt"),
applicant.get("heightIn")
)
name = applicant.get("firstName", "Unknown")
applicant_type = {
1: "Primary",
2: "Spouse",
3: "Dependent"
}.get(applicant.get("applicant"), "Unknown")
is_dtq = check_declinable(phq)
rx_spend = calculate_rx_spend(phq.get("medications", []), idx)
if is_dtq:
tier = 5.0
dtq_found = True
message = "Declined due to high-risk conditions (DTQ triggered)."
elif age is None or bmi is None or base is None:
tier = 4.5
message = "Assigned fallback Tier 4.5 due to missing age or BMI."
else:
tier = base
tier = adjust_tier(tier, bmi, phq.get("medications", []),
phq.get("issues", []), idx)
tier_price = PRICING_TABLE.get(tier, PRICING_TABLE[5])[coverage_index]
max_price = PRICING_TABLE[7][coverage_index]
if rx_spend > max_price:
tier = 5.0
message = f"Declined due to high Rx spend (${rx_spend} > ${max_price})"
elif rx_spend > tier_price:
for t in sorted(PRICING_TABLE.keys()):
if PRICING_TABLE[t][coverage_index] >= rx_spend:
tier = t
break
message = f"Rx spend ${rx_spend} increased tier to {tier}."
else:
message = f"Tier {tier} assigned with Rx spend ${rx_spend} within allowed limits."
except Exception as e:
tier = 4.5
rx_spend = 0.0
message = f"Fallback Tier 4.5 due to system error: {str(e)}"
max_tier = max(max_tier, tier)
results.append({
"name": name,
"applicant_type": applicant_type,
"age": age,
"bmi": round(bmi, 2) if bmi else None,
"tier": tier,
"rx_spend": rx_spend,
"message": message
})
total_price = get_rx_price(max_tier, coverage)
return {
"results": results,
"combined": {
"tier": max_tier,
"total_price": total_price,
"dtq": dtq_found,
"message": f"Final assigned tier is {max_tier} based on age, health, Rx cost, or fallback logic."
}
}

View file

@ -0,0 +1,111 @@
import httpx
import uuid
from typing import Optional, Dict, Any, List
from ..config import settings
class SessionService:
"""Service for managing chat sessions with talestorm-ai"""
def __init__(self):
self.base_url = settings.TALESTORM_API_BASE_URL
self.api_key = settings.TALESTORM_API_KEY
self.agent_id = settings.TALESTORM_AGENT_ID
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
)
async def list_agents(self) -> List[Dict[str, Any]]:
"""List available agents from talestorm-ai"""
async with await self.get_client() as client:
try:
response = await client.get("/agents/")
if response.status_code == 200:
return response.json()
else:
return []
except Exception:
return []
async def get_default_agent(self) -> Optional[Dict[str, Any]]:
"""Get the configured agent or the first available agent"""
# First try to get the configured agent
if self.agent_id:
async with await self.get_client() as client:
try:
response = await client.get(f"/agents/{self.agent_id}")
if response.status_code == 200:
return response.json()
except Exception:
pass
# Fallback to first available agent
agents = await self.list_agents()
if agents:
return agents[0]
return None
async def create_session(self, agent_id: Optional[str] = None) -> Optional[str]:
"""Create a new chat session in talestorm-ai"""
async with await self.get_client() as client:
try:
# Use provided agent_id, then configured agent_id, then fallback to default
if not agent_id:
if self.agent_id:
agent_id = self.agent_id
else:
default_agent = await self.get_default_agent()
if not default_agent:
# Create a simple session ID for now
return str(uuid.uuid4())
agent_id = str(default_agent["id"])
# Create session with talestorm-ai
response = await client.post("/sessions/", params={"agent_id": agent_id})
if response.status_code == 200:
session_data = response.json()
return str(session_data["id"])
else:
# Fallback to local session ID
return str(uuid.uuid4())
except Exception:
# Fallback to local session ID
return str(uuid.uuid4())
async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Get session details from talestorm-ai"""
async with await self.get_client() as client:
try:
response = await client.get(f"/sessions/{session_id}")
if response.status_code == 200:
return response.json()
return None
except Exception:
return None
async def list_sessions(self) -> List[Dict[str, Any]]:
"""List all sessions from talestorm-ai"""
async with await self.get_client() as client:
try:
response = await client.get("/sessions/")
if response.status_code == 200:
return response.json()
return []
except Exception:
return []
async def validate_session(self, session_id: str) -> bool:
"""Validate if a session exists and is accessible"""
session = await self.get_session(session_id)
return session is not None
# Global session service instance
session_service = SessionService()