feat: complete production hardening implementation, specialty canonicalization, google-genai migration, voice reliability, sqlite concurrency, and test suite consolidation
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
NextBestActionEngine (Rules 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91):
|
||||
- Conversational State Machine Evaluation
|
||||
- Priority Scoring & Action Selection
|
||||
- Cooldown & Intent Deduplication
|
||||
- Status-aware field tracking (reported, confirmed_none, unknown, declined, skipped, not_asked)
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
|
||||
class NextBestActionEngine:
|
||||
"""
|
||||
Evaluates intake state and determines the next-best action and target question.
|
||||
"""
|
||||
|
||||
def calculate_priority_score(
|
||||
self,
|
||||
field: str,
|
||||
current_state: Dict[str, Any],
|
||||
is_psych: bool = False,
|
||||
asked_intents: Optional[List[str]] = None
|
||||
) -> float:
|
||||
"""
|
||||
Calculates question priority score based on Rule 83:
|
||||
priority = safety_weight + clinical_importance + symptom_completeness
|
||||
+ specialty_relevance + information_value
|
||||
- repetition_penalty - already_answered_penalty - unnecessary_question_penalty
|
||||
"""
|
||||
score = 0.0
|
||||
asked_intents = asked_intents or []
|
||||
|
||||
weights = {
|
||||
"emergency": 100.0,
|
||||
"crisis": 100.0,
|
||||
"reason_for_visit": 50.0,
|
||||
"symptoms": 50.0,
|
||||
"duration": 45.0,
|
||||
"severity": 30.0,
|
||||
"medical_history": 35.0,
|
||||
"medications": 30.0,
|
||||
"allergies": 25.0,
|
||||
"previous_episodes": 15.0,
|
||||
"family_history": 10.0,
|
||||
"additional_information": 5.0,
|
||||
}
|
||||
|
||||
score += weights.get(field, 10.0)
|
||||
|
||||
# Repetition penalty (Rule 84 & 85)
|
||||
if field in asked_intents:
|
||||
score -= 60.0
|
||||
|
||||
# Specialty relevance bonus
|
||||
if is_psych and field in ["duration", "medical_history"]:
|
||||
score += 10.0
|
||||
|
||||
return score
|
||||
|
||||
def determine_next_action(
|
||||
self,
|
||||
current_state: Dict[str, Any],
|
||||
is_emergency: bool = False,
|
||||
is_crisis: bool = False,
|
||||
is_psych: bool = False,
|
||||
missing_fields: Optional[List[str]] = None
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Returns (action_type, target_field)
|
||||
Action types (Rule 82):
|
||||
- SAFETY_ACTION
|
||||
- CLARIFY
|
||||
- ASK_QUESTION
|
||||
- COMPLETE
|
||||
- HANDLE_REFUSAL
|
||||
- HANDLE_UNKNOWN
|
||||
"""
|
||||
if is_crisis or is_emergency:
|
||||
return "SAFETY_ACTION", None
|
||||
|
||||
# Check for state conflicts requiring clarification (Rule 67)
|
||||
if current_state and current_state.get("conflicts"):
|
||||
return "CLARIFY", current_state["conflicts"][-1].get("field")
|
||||
|
||||
missing_fields = missing_fields or []
|
||||
if not missing_fields:
|
||||
return "COMPLETE", None
|
||||
|
||||
# Score missing fields using priority score (Rule 83)
|
||||
scored_fields = []
|
||||
for field in missing_fields:
|
||||
score = self.calculate_priority_score(field, current_state, is_psych=is_psych)
|
||||
scored_fields.append((score, field))
|
||||
|
||||
scored_fields.sort(key=lambda x: x[0], reverse=True)
|
||||
best_field = scored_fields[0][1]
|
||||
|
||||
return "ASK_QUESTION", best_field
|
||||
Reference in New Issue
Block a user