update dtq conditions
This commit is contained in:
parent
80916f6c3e
commit
0a41d9ba82
11 changed files with 915 additions and 303 deletions
13
compose.yaml
13
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:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ dependencies = [
|
|||
"beautifulsoup4>=4.12.0",
|
||||
"lxml>=4.9.0",
|
||||
"requests>=2.31.0",
|
||||
"redis>=6.2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
@ -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)
|
||||
|
||||
# Call the TALESTORM API for estimation
|
||||
estimation_result = await estimation_service.estimate_insurance(request_data)
|
||||
return estimation_response
|
||||
|
||||
# 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
131
src/cache/redis_cache.py
vendored
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
return DrugPriceResponse(
|
||||
drug_name=drug_name,
|
||||
url=url,
|
||||
prices=prices_data,
|
||||
status="success"
|
||||
)
|
||||
|
||||
|
||||
def _extract_prices(self, soup: BeautifulSoup, drug_name: str) -> Dict[str, Any]:
|
||||
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__":
|
||||
|
|
|
|||
262
src/examples/estimation_v2_example.py
Normal file
262
src/examples/estimation_v2_example.py
Normal 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
127
src/models.py
Normal 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
|
||||
309
src/services/estimation_service_v2.py
Normal file
309
src/services/estimation_service_v2.py
Normal 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
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
uv.lock
generated
11
uv.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue