File size: 8,697 Bytes
be76ce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""
tools.py — the entire sandbox
------------------------------
Plain, pure, deterministic Python functions. No I/O, no network, no
filesystem, no eval/exec. This is the *complete* attack surface the LLM
can touch — nothing outside TOOL_REGISTRY is reachable, no matter what
language (English or Malayalam) the conversation is happening in.

Every function takes typed args and returns a JSON-serializable dict.
Docstrings double as the tool schema shown to the model.
"""

from __future__ import annotations

WEIGHT_STATUS_RANGES = [
    (18.5, "Underweight"),
    (25.0, "Normal weight"),
    (30.0, "Overweight"),
    (float("inf"), "Obesity"),
]


def calculate_bmi(weight_kg: float, height_cm: float) -> dict:
    """Calculates Body Mass Index (BMI) and its general weight-status category.

    Args:
        weight_kg: Body weight in kilograms.
        height_cm: Height in centimeters.
    """
    if weight_kg <= 0 or height_cm <= 0:
        return {"error": "weight_kg and height_cm must both be positive numbers."}
    height_m = height_cm / 100.0
    bmi = weight_kg / (height_m ** 2)
    category = next(label for threshold, label in WEIGHT_STATUS_RANGES if bmi < threshold)
    return {
        "bmi": round(bmi, 1),
        "category": category,
        "note": "BMI is a screening tool only, not a diagnosis. It does not "
                "account for muscle mass, bone density, or body composition.",
    }


_UNIT_CONVERSIONS = {
    ("kg", "lb"): lambda v: v * 2.20462,
    ("lb", "kg"): lambda v: v / 2.20462,
    ("cm", "in"): lambda v: v / 2.54,
    ("in", "cm"): lambda v: v * 2.54,
    ("c", "f"): lambda v: v * 9 / 5 + 32,
    ("f", "c"): lambda v: (v - 32) * 5 / 9,
    ("mg", "mcg"): lambda v: v * 1000,
    ("mcg", "mg"): lambda v: v / 1000,
    ("g", "mg"): lambda v: v * 1000,
    ("mg", "g"): lambda v: v / 1000,
}


def convert_unit(value: float, from_unit: str, to_unit: str) -> dict:
    """Converts a numeric health measurement between common units.

    Supported unit codes: kg, lb (weight); cm, in (length); c, f
    (temperature); mg, mcg, g (medication mass — conversion only, does
    NOT validate or recommend any dose).

    Args:
        value: The numeric value to convert.
        from_unit: Source unit code, e.g. "kg".
        to_unit: Target unit code, e.g. "lb".
    """
    key = (from_unit.strip().lower(), to_unit.strip().lower())
    if key[0] == key[1]:
        return {"result": value, "unit": to_unit}
    if key not in _UNIT_CONVERSIONS:
        return {"error": f"Unsupported conversion: {from_unit} -> {to_unit}"}
    result = _UNIT_CONVERSIONS[key](value)
    return {"result": round(result, 3), "unit": to_unit}


_VITAL_RANGES = {
    "heart_rate": (60, 100, "bpm"),
    "systolic_bp": (90, 120, "mmHg"),
    "diastolic_bp": (60, 80, "mmHg"),
    "body_temperature_c": (36.1, 37.2, "°C"),
    "respiratory_rate": (12, 20, "breaths/min"),
    "spo2": (95, 100, "%"),
}


def check_vital_sign_range(vital: str, value: float) -> dict:
    """Compares a vital-sign reading against typical resting reference
    ranges for a healthy adult. General reference only — not diagnostic.

    Args:
        vital: One of "heart_rate", "systolic_bp", "diastolic_bp",
            "body_temperature_c", "respiratory_rate", "spo2".
        value: The measured value.
    """
    key = vital.strip().lower()
    if key not in _VITAL_RANGES:
        return {"error": f"Unknown vital '{vital}'. Supported: {list(_VITAL_RANGES)}"}
    low, high, unit = _VITAL_RANGES[key]
    if value < low:
        status = "below typical adult resting range"
    elif value > high:
        status = "above typical adult resting range"
    else:
        status = "within typical adult resting range"
    return {
        "vital": key,
        "value": value,
        "unit": unit,
        "typical_range": f"{low}-{high} {unit}",
        "status": status,
        "note": "General reference range for a resting healthy adult, not a "
                "diagnosis. Context (exercise, anxiety, medication) matters.",
    }


_EMERGENCY_KEYWORDS = [
    "chest pain", "crushing chest", "can't breathe", "cannot breathe",
    "difficulty breathing", "shortness of breath at rest", "blue lips",
    "severe bleeding", "won't stop bleeding", "stroke", "face drooping",
    "slurred speech", "sudden numbness", "sudden confusion",
    "anaphylaxis", "throat closing", "severe allergic reaction",
    "suicidal", "want to end my life", "overdose", "unresponsive",
    "not breathing", "seizure", "severe burn", "coughing up blood",
]

_URGENT_KEYWORDS = [
    "high fever", "persistent vomiting", "dehydration", "severe pain",
    "broken bone", "deep cut", "spreading redness", "worsening rash",
    "confusion", "fainted", "dizziness that won't go away",
]


def symptom_urgency_triage(symptoms: list) -> dict:
    """Rule-based keyword triage flagging recognized red-flag emergency or
    urgent-care indicators. A static keyword screen, NOT a diagnostic tool.

    Args:
        symptoms: A list of symptom phrases as described by the user, in
            English (translate the user's words into English first if
            they described symptoms in Malayalam), e.g. ["chest pain"].
    """
    joined = " ".join(str(s).lower() for s in symptoms)
    hits_emergency = [kw for kw in _EMERGENCY_KEYWORDS if kw in joined]
    if hits_emergency:
        return {
            "urgency": "emergency",
            "matched_flags": hits_emergency,
            "recommendation": "These symptoms include recognized emergency "
                               "warning signs. Call emergency services (108 "
                               "in India, 911 in the US, or your local "
                               "emergency number) or go to the nearest "
                               "emergency room right now.",
        }
    hits_urgent = [kw for kw in _URGENT_KEYWORDS if kw in joined]
    if hits_urgent:
        return {
            "urgency": "urgent",
            "matched_flags": hits_urgent,
            "recommendation": "These symptoms are worth same-day medical "
                               "attention — contact an urgent care clinic, "
                               "telehealth line, or your doctor today.",
        }
    return {
        "urgency": "routine_or_self_care",
        "matched_flags": [],
        "recommendation": "No recognized emergency or urgent red-flags were "
                           "matched. Routine symptoms can still be worth a "
                           "regular doctor's visit if they persist, worsen, "
                           "or concern you.",
    }


_INTERACTION_TABLE = {
    frozenset({"ibuprofen", "warfarin"}): "Increased bleeding risk — NSAIDs "
        "like ibuprofen can amplify warfarin's blood-thinning effect.",
    frozenset({"aspirin", "warfarin"}): "Increased bleeding risk when combined.",
    frozenset({"acetaminophen", "alcohol"}): "Heavy alcohol use with "
        "acetaminophen (paracetamol) increases risk of liver damage.",
    frozenset({"ibuprofen", "aspirin"}): "Taking both regularly increases "
        "GI irritation/bleeding risk and ibuprofen can blunt aspirin's "
        "heart-protective effect if timed incorrectly.",
    frozenset({"ssri", "maoi"}): "Potentially dangerous serotonin syndrome "
        "risk — should generally not be combined without close medical "
        "supervision.",
    frozenset({"ibuprofen", "lisinopril"}): "NSAIDs can reduce the "
        "effectiveness of ACE inhibitors like lisinopril and stress the kidneys.",
}


def drug_interaction_lookup(drug_a: str, drug_b: str) -> dict:
    """Looks up general, educational interaction-awareness notes between two
    medication names from a small static reference table. NOT exhaustive —
    never confirms an absence of interaction.

    Args:
        drug_a: Name of the first medication (e.g. "ibuprofen").
        drug_b: Name of the second medication (e.g. "warfarin").
    """
    key = frozenset({drug_a.strip().lower(), drug_b.strip().lower()})
    note = _INTERACTION_TABLE.get(key)
    if note:
        return {"pair": [drug_a, drug_b], "known_interaction": True, "note": note}
    return {
        "pair": [drug_a, drug_b],
        "known_interaction": False,
        "note": "No entry in this small reference table for this pair. This "
                "does NOT mean the combination is safe — always check with a "
                "pharmacist or doctor, or a full drug interaction database.",
    }


TOOLS = [
    calculate_bmi,
    convert_unit,
    check_vital_sign_range,
    symptom_urgency_triage,
    drug_interaction_lookup,
]
TOOL_REGISTRY = {fn.__name__: fn for fn in TOOLS}