Compare commits
30 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b0181a711 | |||
| c3135e1549 | |||
| 7178369f47 | |||
| f873b7e710 | |||
| 25f091d02c | |||
| 13d9a76bed | |||
| 9b1a6340e8 | |||
| dd1b79f7e7 | |||
| 41fd8e3d15 | |||
| 0e33063c99 | |||
| 280611e86f | |||
| dbf468b586 | |||
| 7eb8136cd4 | |||
| b77be4c250 | |||
| 37920ef259 | |||
| 0e6cb8bbd0 | |||
| e0de48a66a | |||
| 52c7292d94 | |||
| 8cfa3762b8 | |||
| 3ff38c9e86 | |||
| 75186b1b70 | |||
| ffb4750717 | |||
| d8d38ab132 | |||
| b2eba19fb1 | |||
| 9b4a6a3ad5 | |||
| d61aac5813 | |||
| 538ea04aa5 | |||
| 0d79803fea | |||
| 137dd66e24 | |||
| 808d1f6d26 |
8 changed files with 606 additions and 48 deletions
|
|
@ -0,0 +1,45 @@
|
|||
"""add user_session
|
||||
|
||||
Revision ID: 31359fcda8a7
|
||||
Revises: 57f67bce2bec
|
||||
Create Date: 2025-09-19 12:03:40.032535
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '31359fcda8a7'
|
||||
down_revision: Union[str, Sequence[str], None] = '57f67bce2bec'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user_session',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), nullable=True),
|
||||
sa.Column('session_id', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('user_session', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_user_session_id'), ['id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_user_session_user_id'), ['user_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user_session', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_user_session_user_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_user_session_id'))
|
||||
|
||||
op.drop_table('user_session')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"""add user page
|
||||
|
||||
Revision ID: 4324550d9c83
|
||||
Revises: 31359fcda8a7
|
||||
Create Date: 2025-10-03 16:08:13.898990
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4324550d9c83'
|
||||
down_revision: Union[str, Sequence[str], None] = '31359fcda8a7'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user_session', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('page_id', sa.String(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user_session', schema=None) as batch_op:
|
||||
batch_op.drop_column('page_id')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
218
prompt-v3.md
Normal file
218
prompt-v3.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Personality
|
||||
You are Alia, a friendly and helpful virtual insurance assistant. You never assume the user knows insurance jargon. You always explain things clearly, like you’re talking to a smart friend. Keep responses short, positive, and polite. Your answers are specific to the user’s question and the plans offered. Reassure users that the application process is easy, fast, and secure.
|
||||
|
||||
Provide clear, concise, accurate, and informative answers using current and expert knowledge. Refrain from addressing and answering on prohibited topics, regardless of whether the question appears related to insurance or a specific plan. Do not lose your personality.
|
||||
|
||||
Before processing any question, check if it falls under prohibited themes (health conditions, legal, tax, HR, medical, off-topic). If it does, respond only with the appropriate prohibited theme message, slightly tailored to reflect the user’s specific question (e.g., mention the specific health condition or topic raised). Do not provide any other information.
|
||||
|
||||
---
|
||||
|
||||
# Prohibited Themes
|
||||
|
||||
* **Health Condition Questions**
|
||||
* If the question is about being **over 65**, respond:
|
||||
> “You’re welcome to complete an application! Once you’re 65, your options for major medical plans are usually limited because Medicare becomes the primary program. But you may still be able to see quotes for other products like dental, vision, or supplemental plans. And if we can’t provide a quote, we’ll guide you toward the right resources so you always know your options.”
|
||||
|
||||
* For all other health conditions (e.g., smoking, weight loss medication, warfarin):
|
||||
* Reassure that users can still apply.
|
||||
* Clarify that eligibility is determined during the application.
|
||||
* Emphasize that if a quote isn’t possible, alternatives will be suggested.
|
||||
* Keep responses friendly, warm, and in line with the “better interaction” examples.
|
||||
|
||||
* **Tax-Related Questions**
|
||||
> “Questions about \[tax-related term] are better suited for a legal or HR expert. I’m here to help you understand your plan and coverage—let me know if you'd like help with that!”
|
||||
|
||||
* **Legal or HR-Type Questions**
|
||||
> “Questions about \[legal/HR topic] are better suited for a legal or HR expert. I’m here to help you understand your plan and coverage—let me know if you'd like help with that!”
|
||||
|
||||
* **Medical Advice Questions**
|
||||
> “I'm really glad you're being proactive about your health, especially regarding \[medical topic]! Since I can’t give medical advice, I recommend checking with your doctor to get the best guidance.”
|
||||
|
||||
* **General Not My Expertise**
|
||||
> “That’s a great question about \[topic]! It’s a bit outside what I can help with, but I’m here for anything related to your insurance plans or benefits.”
|
||||
|
||||
* **Totally Off-Topic / Programming / Self-Harm / Politics**
|
||||
> “Hmm, \[topic] is a little outside my wheelhouse! But if you have any questions about your health or insurance plans, I’d love to help.”
|
||||
|
||||
* **Offering Next Steps**
|
||||
> “While I can’t advise on \[topic] directly, I’d be happy to help you find the right contact or share plan details that might help.”
|
||||
|
||||
---
|
||||
|
||||
# Notes
|
||||
|
||||
## Application & Onboarding
|
||||
|
||||
* **Application process:**
|
||||
|
||||
* Emphasize that it is easy, fast, and flexible.
|
||||
* Most users complete it in just a few minutes.
|
||||
* Users can pause and resume anytime; progress is saved automatically.
|
||||
* Reassure users that you’re available to guide them through each step.
|
||||
|
||||
* **Avoid confusion:**
|
||||
|
||||
* Do not continue conversations that collect application data.
|
||||
* Do not ask questions like *“Ready to get started?”*—this may imply the chat is part of the application itself.
|
||||
* Instead, guide users to interact directly with the form and offer support for questions on each page.
|
||||
|
||||
* **Who is OneHealth?**
|
||||
|
||||
* OneHealth is a licensed health insurance provider offering **exclusive, premium private insurance plans** not available anywhere else.
|
||||
* Not ACA Marketplace, faith-based, or MEC.
|
||||
* Plans are available **year-round, in all 50 states**, with coverage from trusted carriers (e.g., **Cigna, Guardian, Equitable**).
|
||||
* Tailored for gig workers, freelancers, and small businesses (<50 employees).
|
||||
|
||||
* **Plan legitimacy:**
|
||||
|
||||
* Confirm that plans are **real, high-quality, ACA-compliant alternatives** sourced from trusted carriers.
|
||||
* Premium plans are not always the cheapest — emphasize quality and stability.
|
||||
|
||||
* **Comparison to Obamacare:**
|
||||
|
||||
* Clarify that plans are private and not part of the federal Marketplace.
|
||||
* They offer **year-round enrollment**, curated provider networks, and potentially lower rates — while still being compliant and comprehensive.
|
||||
|
||||
* **Eligibility:**
|
||||
|
||||
* Anyone can apply, but acceptance is based on underwriting.
|
||||
* Eligibility is determined by the **Personal Health Questionnaire (PHQ)**.
|
||||
* If a user doesn’t qualify, never provide reasons. Say only: *“we could not offer you a plan at this time.”*
|
||||
* Ancillary products (dental, vision, accident, critical illness, hospital indemnity) are open to everyone without a PHQ.
|
||||
|
||||
* **Data privacy:**
|
||||
|
||||
* All user data is protected with **bank-level encryption** and **HIPAA-compliant systems**.
|
||||
* Data is never sold.
|
||||
* It is only shared with selected insurance carriers when necessary for enrollment.
|
||||
|
||||
* **Post-application:**
|
||||
|
||||
* Applications are reviewed for eligibility and pricing.
|
||||
* Some plans may offer **instant approval and coverage**.
|
||||
|
||||
* **Dependents:** Maximum of **5 children plus a spouse**.
|
||||
|
||||
* **Nicotine use:** Does **not** automatically deny coverage. Eligibility is application-based.
|
||||
|
||||
* **Medications:** Encourage accuracy; if info is unknown, reassure progress is still saved. Never say “denied automatically.”
|
||||
|
||||
* **Minor procedures (e.g., mole removal):** Often too minor to list; reassure omission usually means it’s not required.
|
||||
|
||||
---
|
||||
|
||||
## Plan Selection & Recommendations
|
||||
|
||||
* **Provider Fit:**
|
||||
|
||||
* Current health plans use the **Cigna network**.
|
||||
* Options range from high-deductible to low-deductible PPOs.
|
||||
* Networks are curated — may not include every provider. Always be transparent.
|
||||
|
||||
* **Prescription Coverage:**
|
||||
|
||||
* All health plans cover prescriptions.
|
||||
* For routine, low-cost prescriptions: most plans fit.
|
||||
* For specialty or brand-only meds: recommend richer coverage like **Cigna PPO 1000**.
|
||||
* If a drug is not covered, clearly flag this.
|
||||
|
||||
* **Budget & Risk Trade-off:**
|
||||
|
||||
* Alia should always explain trade-offs:
|
||||
|
||||
* “Lower premium but higher deductible = lower monthly costs, but more if you need care.”
|
||||
* “Higher premium but lower deductible = higher monthly costs, but more predictable bills.”
|
||||
|
||||
* **Usage Scenarios:**
|
||||
|
||||
* **Minimal care:** leaner health plan + ancillary (accident).
|
||||
* **Moderate care:** mid-tier with co-pays.
|
||||
* **Heavy care:** richer PPOs like **Cigna PPO 1000 or 1500**.
|
||||
|
||||
* **Family vs. Individual:**
|
||||
|
||||
* Highlight family deductibles, out-of-pocket max, and pediatric benefits when dependents are added.
|
||||
|
||||
* **Add-ons:**
|
||||
|
||||
* **Dental:** recommend if kids or user asks about cleanings/teeth.
|
||||
* **Vision:** recommend for glasses, contacts, or annual exams.
|
||||
* **Accident / Critical Illness / Hospital Indemnity:**
|
||||
|
||||
* Especially valuable for high-deductible health plans.
|
||||
* Always disclose: *“Hospital Indemnity is supplemental insurance and not a substitute for major medical coverage.”*
|
||||
|
||||
* **Recommendation Style:**
|
||||
|
||||
* Name the plan clearly (**Cigna PPO 1000**).
|
||||
* Explain *why* it may fit (doctor access, Rx coverage, predictable costs).
|
||||
* Point out trade-offs (high vs. low deductible).
|
||||
* Use examples and side-by-side comparisons where helpful.
|
||||
* Plans and prices should be in **bold**.
|
||||
|
||||
---
|
||||
|
||||
## Returning & Rejected Users
|
||||
|
||||
* **Returning Users:**
|
||||
|
||||
* Greet warmly: “Welcome back! Ready to pick up where you left off?”
|
||||
* Remind them of last step if available.
|
||||
* If >30 days: PHQ must be redone. Reassure it’s quick.
|
||||
* If repeated abandons: switch from nudging → supporting (e.g., “Is there something I can explain that might help?”).
|
||||
|
||||
* **Rejected / DTQ’d Users:**
|
||||
|
||||
* Stay empathetic, neutral. Never give specific reasons.
|
||||
* Say: *“we could not offer you a plan at this time.”*
|
||||
* Immediately pivot to ancillary products (dental, vision, accident, etc.).
|
||||
* Optionally suggest ACA Marketplace link if appropriate.
|
||||
* Invite return after 90 days.
|
||||
|
||||
---
|
||||
|
||||
## Payment
|
||||
|
||||
* **Methods:** Only **ACH** (no credit cards).
|
||||
* **Bank connection issues:** Suggest retrying credentials or later attempt.
|
||||
* Progress is always saved.
|
||||
|
||||
---
|
||||
|
||||
# Style Guidelines
|
||||
|
||||
* Plan-specific, actionable answers.
|
||||
* Avoid jargon—use plain English.
|
||||
* Use short paragraphs and bullet points.
|
||||
* Never guess or assume.
|
||||
* Stay warm, positive, not robotic.
|
||||
* Personalize for health condition mentions.
|
||||
* If rejected, only show ancillary plans.
|
||||
* Always emphasize **data security, privacy, and flexibility**.
|
||||
|
||||
---
|
||||
|
||||
# Tool Usage Instructions
|
||||
|
||||
* **show\_plans** → when user wants to see one or more plans (include plan IDs).
|
||||
* **compare\_plans** → when user wants a side-by-side comparison (include plan IDs).
|
||||
* **update\_applicants** → when user updates applicant info (id: 0 = self, 1 = spouse, 2+ = children).
|
||||
|
||||
Use `get_plan_list()` to fetch available plans and `get_plan_by_id(plan_id)` for details (only if user is not rejected).
|
||||
|
||||
---
|
||||
|
||||
# Output Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"answer": "string - the natural language reply to the user",
|
||||
"show_plans": [],
|
||||
"compare_plans": [],
|
||||
"update_applicants": []
|
||||
}
|
||||
```
|
||||
|
||||
Important Output Rules:
|
||||
- Always include "answer" (string) — this is the text the user will read.
|
||||
- Include hooks when the user asks for plans or comparing of plans (show me plan ..., compare ... plan to ..., etc.).
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import base64
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from src.services.estimation_service_v2 import EstimationService
|
||||
|
|
@ -13,14 +16,24 @@ async def insurance_chat(request: models.InsuranceChatRequest):
|
|||
"""Handle insurance chat requests"""
|
||||
try:
|
||||
current_page = None
|
||||
page_id = None
|
||||
if request.context and request.context.page:
|
||||
page_id = request.context.page
|
||||
page_id = str(request.context.page).lower()
|
||||
current_page = await get_page_description(page_id)
|
||||
|
||||
application = None
|
||||
if request.context and request.context.application:
|
||||
application = request.context.application
|
||||
elif request.context and request.context.applicationDTO:
|
||||
application = json.loads(base64.b64decode(request.context.applicationDTO).decode())
|
||||
|
||||
result = await chat_service.process_insurance_chat(
|
||||
message=request.message,
|
||||
session_id=request.session_id,
|
||||
uid=str(request.userId),
|
||||
uid=str(request.userId) if request.userId else None,
|
||||
current_page=current_page,
|
||||
application=application,
|
||||
page_id=page_id
|
||||
)
|
||||
|
||||
return models.InsuranceChatResponse(
|
||||
|
|
@ -35,18 +48,66 @@ async def insurance_chat(request: models.InsuranceChatRequest):
|
|||
# raise e
|
||||
raise HTTPException(status_code=500, detail=f"Error processing chat request: {str(e)}")
|
||||
|
||||
@router.post("/initialize", response_model=models.InitializeChatResponse)
|
||||
async def init_chat(request: models.InitializeChatRequest):
|
||||
application = None
|
||||
if request.context and request.context.application:
|
||||
application = request.context.application
|
||||
elif request.context and request.context.applicationDTO:
|
||||
application = json.loads(base64.b64decode(request.context.applicationDTO).decode())
|
||||
|
||||
name = None
|
||||
if request.context and request.context.name:
|
||||
name = request.context.name.first_name
|
||||
first_visit = True
|
||||
if request.context:
|
||||
first_visit = request.context.isFirstVisit
|
||||
|
||||
page_id = None
|
||||
if request.context and request.context.page:
|
||||
page_id = str(request.context.page).lower()
|
||||
|
||||
result = await chat_service.initialize_chat(str(request.userId), application, name, first_visit, page_id)
|
||||
return models.InitializeChatResponse(
|
||||
session_id=result["session_id"],
|
||||
answer=result["answer"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/estimation", response_model=models.EstimationResponse)
|
||||
async def estimate(request: models.EstimationRequest):
|
||||
"""Handle insurance estimation requests"""
|
||||
try:
|
||||
if not request.applicants or not request.plans:
|
||||
if not request.applicants:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Missing required applicants or plans"
|
||||
)
|
||||
|
||||
print("estimation request: ", request)
|
||||
|
||||
has_primary = False
|
||||
has_spouse = False
|
||||
has_dependents = False
|
||||
for applicant in request.applicants:
|
||||
if applicant.applicant == 1:
|
||||
has_primary = True
|
||||
elif applicant.applicant == 2:
|
||||
has_spouse = True
|
||||
elif applicant.applicant == 3:
|
||||
has_dependents = True
|
||||
|
||||
if has_primary and not has_spouse and not has_dependents:
|
||||
coverage = 1
|
||||
elif has_primary and has_spouse and not has_dependents:
|
||||
coverage = 2
|
||||
elif has_primary and not has_spouse and has_dependents:
|
||||
coverage = 3
|
||||
else:
|
||||
coverage = 4
|
||||
|
||||
estimation_service = EstimationService()
|
||||
estimation_response = await estimation_service.estimate_insurance(request.applicants, request.phq, request.plans)
|
||||
estimation_response = await estimation_service.estimate_insurance(request.applicants, request.phq, coverage)
|
||||
|
||||
return estimation_response
|
||||
|
||||
|
|
|
|||
|
|
@ -34,5 +34,14 @@ class Webpage(Base):
|
|||
description = Column(String)
|
||||
|
||||
|
||||
class UserSession(Base):
|
||||
__tablename__ = "user_session"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
user_id = Column(String, index=True)
|
||||
session_id = Column(String)
|
||||
page_id = Column(String, default=None, nullable=True)
|
||||
|
||||
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Any
|
||||
from datetime import date
|
||||
|
||||
class Applicant(BaseModel):
|
||||
|
|
@ -17,7 +17,7 @@ class Applicant(BaseModel):
|
|||
|
||||
class Plan(BaseModel):
|
||||
id: int
|
||||
priceId: int
|
||||
priceId: int | None = None
|
||||
|
||||
class Medication(BaseModel):
|
||||
applicant: int
|
||||
|
|
@ -25,6 +25,7 @@ class Medication(BaseModel):
|
|||
rxcui: str
|
||||
dosage: str
|
||||
frequency: str
|
||||
frequencyDescription: str = ''
|
||||
description: str
|
||||
|
||||
class IssueDetail(BaseModel):
|
||||
|
|
@ -43,12 +44,12 @@ class PHQ(BaseModel):
|
|||
treatment: bool
|
||||
invalid: bool
|
||||
pregnancy: bool
|
||||
effectiveDate: date
|
||||
disclaimer: bool
|
||||
signature: str
|
||||
medications: List[Medication]
|
||||
issues: List[Issue]
|
||||
conditions: List[Condition]
|
||||
effectiveDate: Any = None
|
||||
|
||||
class Address(BaseModel):
|
||||
address1: Optional[str] = Field("", description="Address line 1")
|
||||
|
|
@ -60,7 +61,6 @@ class Address(BaseModel):
|
|||
class EstimationRequest(BaseModel):
|
||||
userId: str | int | None = Field(None, description="Unique identifier")
|
||||
applicants: List[Applicant]
|
||||
plans: List[Plan]
|
||||
phq: PHQ
|
||||
income: float
|
||||
address: Address
|
||||
|
|
@ -68,7 +68,8 @@ class EstimationRequest(BaseModel):
|
|||
class EstimationDetails(BaseModel):
|
||||
dtq: bool
|
||||
reason: str
|
||||
price_id: int
|
||||
# price_id: int = -1
|
||||
tier: str
|
||||
|
||||
class EstimationResult(BaseModel):
|
||||
name: str
|
||||
|
|
@ -82,10 +83,18 @@ class EstimationResult(BaseModel):
|
|||
class EstimationResponse(BaseModel):
|
||||
status: str
|
||||
details: EstimationDetails
|
||||
results: List[EstimationResult]
|
||||
# results: List[EstimationResult]
|
||||
|
||||
class UserNameContext(BaseModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
|
||||
class InsuranceChatContext(BaseModel):
|
||||
page: str
|
||||
application: dict | None = None
|
||||
applicationDTO: str | None = None
|
||||
name: UserNameContext | None = None
|
||||
isFirstVisit: bool = True
|
||||
|
||||
class InsuranceChatRequest(BaseModel):
|
||||
userId: str | int | None = None
|
||||
|
|
@ -93,6 +102,14 @@ class InsuranceChatRequest(BaseModel):
|
|||
session_id: str | None = Field(None, description="Chat session ID")
|
||||
context: InsuranceChatContext | None = None
|
||||
|
||||
class InitializeChatRequest(BaseModel):
|
||||
userId: str | int
|
||||
context: InsuranceChatContext | None = None
|
||||
|
||||
class InitializeChatResponse(BaseModel):
|
||||
session_id: str
|
||||
answer: str
|
||||
|
||||
class Source(BaseModel):
|
||||
plan_name: str
|
||||
chunk_number: int
|
||||
|
|
@ -108,9 +125,12 @@ class PlansParam(BaseModel):
|
|||
class ApplicantParam(BaseModel):
|
||||
applicants: list[Applicant]
|
||||
|
||||
class PageParam(BaseModel):
|
||||
page: str
|
||||
|
||||
class ChatHook(BaseModel):
|
||||
tool: str
|
||||
params: PlansParam | ApplicantParam
|
||||
params: PlansParam | ApplicantParam | PageParam
|
||||
|
||||
class AIChatResponse(BaseModel):
|
||||
answer: str
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from src.models import ApplicantParam, ChatHook, PlansParam, InsuranceChatContext
|
||||
from src.models import ApplicantParam, ChatHook, PlansParam, PageParam
|
||||
from .session_service import session_service
|
||||
from ..api.v1.models import Source, HistoryItem
|
||||
from ..config import settings
|
||||
from ..database import Session, UserSession
|
||||
|
||||
# vars: name
|
||||
INIT_MESSAGES = [
|
||||
"Hi. I’m Alia, your personal benefits assistant! If you have a question at any point in the enrollment process, just ask, and I’ll try to get you an answer.",
|
||||
"Hi, {name}! Welcome back! As usual, I’m here if you have any questions!",
|
||||
"Hi, {name}! Welcome back! Do you want to pick up where you left off and complete your PHQ?",
|
||||
]
|
||||
|
||||
|
||||
class ChatService:
|
||||
|
|
@ -78,6 +87,15 @@ class ChatService:
|
|||
"message": f"I'm sorry, I'm experiencing technical difficulties. Please try again later. Error: {str(e)}"
|
||||
}
|
||||
|
||||
async def add_message_to_history(self, session_id: str, message: list):
|
||||
async with await self.get_client() as client:
|
||||
response = await client.post(
|
||||
"/messages/",
|
||||
params={"chat_session_id": session_id,},
|
||||
json={"content": message}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
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:
|
||||
|
|
@ -125,6 +143,17 @@ class ChatService:
|
|||
print(f"Error getting chat history: {e}")
|
||||
return []
|
||||
|
||||
async def get_last_chat_message_date(self, session_id: str):
|
||||
async with await self.get_client() as client:
|
||||
try:
|
||||
response = await client.get("/chat/", params={"chat_session_id": session_id, "limit": 1})
|
||||
resp_json = response.json()
|
||||
msg_date = resp_json[0]["created_at"]
|
||||
return datetime.fromisoformat(msg_date)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -135,19 +164,126 @@ class ChatService:
|
|||
# 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, uid: Optional[int] = None, current_page: Optional[str] = None) -> Dict[str, Any]:
|
||||
|
||||
async def get_user_session(self, uid: str) -> str | None:
|
||||
with Session() as session:
|
||||
user_session = session.query(UserSession).filter(UserSession.user_id == uid).first()
|
||||
if not user_session:
|
||||
return None
|
||||
return user_session.session_id
|
||||
|
||||
async def create_user_session(self, uid: str, session_id: str):
|
||||
with Session() as session:
|
||||
user_session = UserSession(
|
||||
user_id=uid,
|
||||
session_id=session_id,
|
||||
)
|
||||
session.add(user_session)
|
||||
session.commit()
|
||||
|
||||
async def update_user_page(self, uid: str, page_id: str | None):
|
||||
with Session() as session:
|
||||
user_session = session.query(UserSession).filter(UserSession.user_id == uid).first()
|
||||
old_page_id = user_session.page_id
|
||||
user_session.page_id = page_id
|
||||
session.add(user_session)
|
||||
session.commit()
|
||||
return old_page_id
|
||||
|
||||
async def get_user_page(self, uid: str):
|
||||
with Session() as session:
|
||||
user_session = session.query(UserSession).filter(UserSession.user_id == uid).first()
|
||||
old_page_id = user_session.page_id
|
||||
return old_page_id
|
||||
|
||||
def get_welcome_message(self, application, name, first_visit):
|
||||
try:
|
||||
if not name:
|
||||
name = application["applicants"][0]["firstName"]
|
||||
except:
|
||||
return INIT_MESSAGES[0]
|
||||
|
||||
if first_visit:
|
||||
return INIT_MESSAGES[0]
|
||||
|
||||
try:
|
||||
applicant = application["applicants"][0]
|
||||
except:
|
||||
return INIT_MESSAGES[2].format(name=name)
|
||||
|
||||
if not applicant.get("gender") or not applicant.get("dob") or applicant.get("dob") == "-01-" or not applicant.get("weight") or applicant.get("heightFt") is None or applicant.get("heightIn") is None:
|
||||
return INIT_MESSAGES[2].format(name=name)
|
||||
|
||||
return INIT_MESSAGES[1].format(name=name)
|
||||
|
||||
|
||||
async def initialize_chat(self, uid: str, application, name, first_visit, page_id):
|
||||
session_id = await self.get_user_session(uid)
|
||||
if not session_id or not await session_service.validate_session(session_id):
|
||||
session_id = await session_service.create_session(agent_id=settings.TALESTORM_AGENT_ID)
|
||||
try:
|
||||
await self.create_user_session(uid, session_id)
|
||||
except:
|
||||
pass
|
||||
# await self.update_user_page(uid, page_id)
|
||||
|
||||
welcome_msg = self.get_welcome_message(application, name, first_visit)
|
||||
|
||||
msg_history_item = [
|
||||
{'parts': [{'content': 'Hi', 'part_kind': 'user-prompt'}],'kind': 'request'},
|
||||
{'parts': [{'content': welcome_msg, 'part_kind': 'text'}], 'kind': 'response'}
|
||||
]
|
||||
|
||||
await self.add_message_to_history(session_id, msg_history_item)
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"answer": welcome_msg
|
||||
}
|
||||
|
||||
|
||||
async def process_insurance_chat(self, message: str, session_id: Optional[str] = None, uid: Optional[str] = None, current_page: Optional[str] = None, application: Optional[dict] = None, page_id = None) -> Dict[str, Any]:
|
||||
"""Process an insurance chat request"""
|
||||
try:
|
||||
if not session_id:
|
||||
if not session_id or not await session_service.validate_session(session_id):
|
||||
if uid:
|
||||
session_id = await self.get_user_session(uid)
|
||||
if not session_id or not await session_service.validate_session(session_id):
|
||||
session_id = await session_service.create_session(agent_id=settings.TALESTORM_AGENT_ID)
|
||||
try:
|
||||
await self.create_user_session(uid, session_id)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
session_id = await session_service.create_session(agent_id=settings.TALESTORM_AGENT_ID)
|
||||
|
||||
elif not await session_service.validate_session(session_id):
|
||||
session_id = await session_service.create_session(agent_id=settings.TALESTORM_AGENT_ID)
|
||||
if page_id.lower() != 'welcome':
|
||||
old_page_id = await self.update_user_page(uid, page_id)
|
||||
else:
|
||||
old_page_id = await self.get_user_page(uid)
|
||||
|
||||
instructions = ""
|
||||
if uid:
|
||||
user_state = await self.get_user_state(uid)
|
||||
instructions += f"\n\n# User Information\nApplication state (None means that application was not sent or pending):\n{user_state}"
|
||||
if application:
|
||||
instructions += f"\n\n# User Application Info\n"
|
||||
if application.get("plans", []):
|
||||
plans_info = "User plans:\n"
|
||||
for p in application["plans"]:
|
||||
plans_info += f"{str(p)}\n"
|
||||
instructions += plans_info
|
||||
if application.get("phq", {}).get("effectiveDate", None):
|
||||
effective_date = application.get("phq", {}).get("effectiveDate", None)
|
||||
instructions += f"Plan effective date: {effective_date}"
|
||||
applicants = application.get("applicants", [])
|
||||
if applicants:
|
||||
applicants_info = "\nApplicants:"
|
||||
for n, applicant in enumerate(applicants, 1):
|
||||
user_name = applicant["firstName"]
|
||||
applicants_info += f"\n{n}. {user_name}"
|
||||
instructions += applicants_info
|
||||
|
||||
if current_page:
|
||||
instructions += f"\n\n# User now is currently on page: {current_page}"
|
||||
|
||||
|
|
@ -159,6 +295,7 @@ class ChatService:
|
|||
compare_plans = ai_response.get("compare_plans")
|
||||
show_plans = ai_response.get("show_plans")
|
||||
update_applicants = ai_response.get("update_applicants")
|
||||
show_page = ai_response.get("show_page")
|
||||
hooks = []
|
||||
if update_applicants:
|
||||
hooks.append(ChatHook(
|
||||
|
|
@ -166,6 +303,8 @@ class ChatService:
|
|||
params=ApplicantParam(applicants=update_applicants)
|
||||
))
|
||||
elif compare_plans:
|
||||
if len(compare_plans) > 3:
|
||||
compare_plans = compare_plans[:3]
|
||||
hooks.append(ChatHook(
|
||||
tool="compare_plans",
|
||||
params=PlansParam(plans=compare_plans)
|
||||
|
|
@ -175,6 +314,12 @@ class ChatService:
|
|||
tool="show_plans",
|
||||
params=PlansParam(plans=show_plans)
|
||||
))
|
||||
elif show_page and old_page_id:
|
||||
hooks.append(ChatHook(
|
||||
tool="show_page",
|
||||
params=PageParam(page=old_page_id)
|
||||
))
|
||||
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ BASE_TIERS = {
|
|||
}
|
||||
|
||||
HEIGHT_WEIGHT_TABLE = {
|
||||
(4, 2): (54, 60, 125, 151),
|
||||
(4, 2): (54, 60, 125, 150),
|
||||
(4, 3): (56, 63, 130, 156),
|
||||
(4, 4): (59, 65, 136, 162),
|
||||
(4, 5): (60, 68, 141, 169),
|
||||
|
|
@ -286,6 +286,13 @@ UNINSURABLE_MEDICATIONS = [
|
|||
"zoledronic acid"
|
||||
]
|
||||
|
||||
UNINSURABLE_ISSUES = [
|
||||
"alcohol", "alzheimer_disease", "chronic_pulmonary_disorder", "copd", "hepatitis", "liver_disorder",
|
||||
"myasthenia_gravis", "organ_transplant", "other_cognitive", "nebulizer", "aids", "als", "amputation",
|
||||
"cancer", "cirrhosis", "emphysema", "multiple_sclerosis", "nervous_disorder", "osteoarthritis",
|
||||
"parkinson_disease", "senile_dementia", "systemic_lupus",
|
||||
]
|
||||
|
||||
class EstimationService:
|
||||
def __init__(self):
|
||||
self.base_url = settings.TALESTORM_API_BASE_URL
|
||||
|
|
@ -333,6 +340,8 @@ class EstimationService:
|
|||
return True, "Applicant is pregnant"
|
||||
|
||||
for issue in phq.issues:
|
||||
if issue.key in UNINSURABLE_ISSUES:
|
||||
return True, f"Uninsurable issue: {issue.key}"
|
||||
if issue.key == "surgery":
|
||||
for detail in issue.details:
|
||||
if detail.key == "not_performed":
|
||||
|
|
@ -342,7 +351,8 @@ class EstimationService:
|
|||
if condition.key in uninsurable_conditions:
|
||||
return True, f"Applicant have uninsurable condition: {condition.key}"
|
||||
|
||||
# issues would be checked by AI
|
||||
|
||||
# issues would be partially checked by AI
|
||||
return False, ""
|
||||
|
||||
def calculate_age(self, born):
|
||||
|
|
@ -375,12 +385,23 @@ class EstimationService:
|
|||
month_times = 90
|
||||
elif medication.frequency == "Four times daily":
|
||||
month_times = 120
|
||||
elif medication.frequency == "Weekly":
|
||||
elif medication.frequency in ["Weekly", "Once weekly"]:
|
||||
month_times = 4
|
||||
elif medication.frequency == "Monthly":
|
||||
elif medication.frequency in ["Monthly", "One-time dose"]:
|
||||
month_times = 1
|
||||
elif medication.frequency == "Every other day":
|
||||
month_times = 15
|
||||
elif medication.frequency == "Three times per week":
|
||||
month_times = 12
|
||||
elif medication.frequency == "Every 2 weeks":
|
||||
month_times = 2
|
||||
elif medication.frequency == "Every X hours":
|
||||
try:
|
||||
freq = int(medication.frequencyDescription)
|
||||
month_times = (24/freq) * 30
|
||||
except:
|
||||
month_times = 1
|
||||
review_reasons.append(f"Unclear frequency of drug {medication.name}: Every X hours; X is not defined.")
|
||||
else:
|
||||
review_reasons.append(f"Unclear frequency of drug {medication.name}: {medication.frequency}")
|
||||
month_times = 1
|
||||
|
|
@ -394,14 +415,14 @@ class EstimationService:
|
|||
def get_tier(self, coverage: int, rx_spend: float) -> tuple[Tier | None, str | None]:
|
||||
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
|
||||
Tier.TIER_4, Tier.TIER_45, Tier.TIER_5, Tier.TIER_55, Tier.TIER_6, 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]
|
||||
1: [122, 127, 133, 139, 145, 151, 158, 164, 172, 182, 196, 222],
|
||||
2: [243, 253, 265, 277, 290, 302, 315, 327, 343, 364, 390, 444],
|
||||
3: [219, 228, 239, 249, 261, 272, 283, 295, 309, 327, 351, 400],
|
||||
4: [365, 379, 397, 415, 435, 452, 472, 491, 514, 545, 585, 666],
|
||||
}
|
||||
|
||||
if coverage not in rates:
|
||||
|
|
@ -437,7 +458,7 @@ class EstimationService:
|
|||
return None
|
||||
|
||||
|
||||
async def estimate_insurance(self, applicants: list[Applicant], phq: PHQ, plans: list[Plan]):
|
||||
async def estimate_insurance(self, applicants: list[Applicant], phq: PHQ, plan_coverage: int):
|
||||
estimation_results = []
|
||||
is_review = False
|
||||
review_reasons = []
|
||||
|
|
@ -457,7 +478,6 @@ class EstimationService:
|
|||
base_tier = tier
|
||||
break
|
||||
|
||||
plan_coverage = self.get_plan_coverage(plans[0])
|
||||
rx_spend = 0
|
||||
for applicant_id, applicant in enumerate(applicants):
|
||||
applicant_review_reasons = []
|
||||
|
|
@ -539,7 +559,7 @@ class EstimationService:
|
|||
)
|
||||
)
|
||||
|
||||
plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage)
|
||||
# plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage)
|
||||
|
||||
if is_dtq:
|
||||
reason = "\n".join(dtq_reasons)
|
||||
|
|
@ -548,9 +568,10 @@ class EstimationService:
|
|||
details=EstimationDetails(
|
||||
dtq=is_dtq,
|
||||
reason=reason,
|
||||
price_id=plan_price_id,
|
||||
# price_id=plan_price_id,
|
||||
tier=f"Tier {base_tier.value}",
|
||||
),
|
||||
results=estimation_results
|
||||
# results=estimation_results
|
||||
)
|
||||
|
||||
if is_review:
|
||||
|
|
@ -560,9 +581,10 @@ class EstimationService:
|
|||
details=EstimationDetails(
|
||||
dtq=is_dtq,
|
||||
reason=reason,
|
||||
price_id=plan_price_id,
|
||||
# price_id=plan_price_id,
|
||||
tier=f"Tier {base_tier.value}",
|
||||
),
|
||||
results=estimation_results
|
||||
# results=estimation_results
|
||||
)
|
||||
|
||||
new_tier, tier_reason = self.get_tier(plan_coverage, rx_spend)
|
||||
|
|
@ -575,15 +597,16 @@ class EstimationService:
|
|||
details=EstimationDetails(
|
||||
dtq=True,
|
||||
reason=reason,
|
||||
price_id=plan_price_id,
|
||||
# price_id=plan_price_id,
|
||||
tier=f"Tier {base_tier.value}",
|
||||
),
|
||||
results=estimation_results
|
||||
# results=estimation_results
|
||||
)
|
||||
|
||||
if new_tier > base_tier:
|
||||
base_tier = new_tier
|
||||
|
||||
plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage)
|
||||
# plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage)
|
||||
|
||||
if base_tier is not None:
|
||||
reason = "\n".join(accept_reasons)
|
||||
|
|
@ -592,9 +615,10 @@ class EstimationService:
|
|||
details=EstimationDetails(
|
||||
dtq=is_dtq,
|
||||
reason=reason,
|
||||
price_id=plan_price_id,
|
||||
# price_id=plan_price_id,
|
||||
tier=f"Tier {base_tier.value}",
|
||||
),
|
||||
results=estimation_results
|
||||
# results=estimation_results
|
||||
)
|
||||
else:
|
||||
reason = "\n".join(dtq_reasons)
|
||||
|
|
@ -603,8 +627,8 @@ class EstimationService:
|
|||
details=EstimationDetails(
|
||||
dtq=is_dtq,
|
||||
reason=reason,
|
||||
price_id=plan_price_id,
|
||||
# price_id=plan_price_id,
|
||||
tier=f"Tier {base_tier.value}",
|
||||
),
|
||||
results=estimation_results
|
||||
# results=estimation_results
|
||||
)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue