Spaces:
Sleeping
Sleeping
| """Tool logic: SOP retrieval, ticket creation, history recall, handoff generation, calculation validation, inventory lookup. | |
| Every function that produces a "fact" stamps it with source_type + confidence so the agent | |
| never presents a hypothesis as a confirmed finding. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from pathlib import Path | |
| from typing import Any | |
| from labops_api import storage | |
| SOPS_DIR = Path(__file__).parent / "sops" | |
| # Maps issue_type strings to SOP IDs (filename stems) | |
| ISSUE_TYPE_TO_SOP: dict[str, str] = { | |
| "temperature_excursion": "freezer_temperature_excursion", | |
| "centrifuge_error": "centrifuge_error_code_42", | |
| "inventory_shortage": "inventory_shortage", | |
| "freezer_temperature_excursion": "freezer_temperature_excursion", | |
| "centrifuge_error_code_42": "centrifuge_error_code_42", | |
| } | |
| # ββ SOP parsing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_sop(path: Path) -> dict[str, Any]: | |
| text = path.read_text(encoding="utf-8") | |
| meta: dict[str, Any] = { | |
| "sop_id": path.stem, | |
| "title": path.stem, | |
| "tags": [], | |
| "applies_to": [], | |
| "required_fields": [], | |
| } | |
| fm = re.match(r"^---\n(.*?)\n---\n(.*)$", text, re.DOTALL) | |
| body = text | |
| if fm: | |
| body = fm.group(2) | |
| for line in fm.group(1).splitlines(): | |
| if ":" not in line: | |
| continue | |
| key, _, val = line.partition(":") | |
| key, val = key.strip(), val.strip() | |
| if val.startswith("[") and val.endswith("]"): | |
| meta[key] = [v.strip() for v in val[1:-1].split(",") if v.strip()] | |
| else: | |
| meta[key] = val | |
| # Pull escalation rules (lines under ## Escalation Rules) | |
| escalation_match = re.search(r"##\s*Escalation Rules\s*\n(.+?)(\n##|\Z)", body, re.DOTALL) | |
| matched_rules = [] | |
| if escalation_match: | |
| for line in escalation_match.group(1).splitlines(): | |
| line = line.strip().lstrip("- ").strip() | |
| if line: | |
| matched_rules.append(line) | |
| # Pull numbered recommended actions | |
| recommended_actions = [ | |
| re.sub(r"^\d+\.\s*", "", ln).strip() | |
| for ln in body.splitlines() | |
| if re.match(r"^\d+\.\s+", ln.strip()) | |
| ] | |
| caution_match = re.search(r"##\s*Caution\s*\n(.+?)(\n##|\Z)", body, re.DOTALL) | |
| caution = caution_match.group(1).strip() if caution_match else "" | |
| return { | |
| **meta, | |
| "matched_rules": matched_rules, | |
| "recommended_actions": recommended_actions[:8], | |
| "caution": caution, | |
| } | |
| def retrieve_sop( | |
| issue_type: str | None = None, | |
| equipment_id: str | None = None, | |
| query: str | None = None, | |
| sample_id: str | None = None, | |
| ) -> dict[str, Any]: | |
| """Return grounded SOP content. Never invents steps β only returns what is in local files.""" | |
| all_sops = {p.stem: _parse_sop(p) for p in sorted(SOPS_DIR.glob("*.md"))} | |
| # 1. Direct issue_type β SOP ID lookup | |
| if issue_type: | |
| sop_id = ISSUE_TYPE_TO_SOP.get(issue_type.lower().replace(" ", "_")) | |
| if sop_id and sop_id in all_sops: | |
| sop = all_sops[sop_id] | |
| return { | |
| "found": True, | |
| "sop_id": sop["sop_id"], | |
| "title": sop.get("title", sop["sop_id"]), | |
| "matched_rules": sop.get("matched_rules", []), | |
| "recommended_actions": sop.get("recommended_actions", []), | |
| "caution": sop.get("caution", "Confirm details before proceeding."), | |
| "source_type": "sop_grounded", | |
| "confidence": "high", | |
| } | |
| # 2. Tag/keyword search using issue_type or query | |
| search_text = (issue_type or query or "").lower() | |
| if not search_text: | |
| return {"found": False, "message": "No issue_type or query provided."} | |
| best, best_score = None, 0 | |
| for sop in all_sops.values(): | |
| terms = sop.get("tags", []) + sop.get("applies_to", []) | |
| score = sum(1 for t in terms if t and t.lower() in search_text) | |
| # Also score if search terms appear in the SOP's tags | |
| score += sum(1 for word in search_text.split() if any(word in t.lower() for t in terms)) | |
| if sample_id and sample_id.upper().startswith("C") and "cardio" in sop["sop_id"]: | |
| score += 1 | |
| if score > best_score: | |
| best, best_score = sop, score | |
| if not best or best_score == 0: | |
| return {"found": False, "message": "No matching local SOP."} | |
| return { | |
| "found": True, | |
| "sop_id": best["sop_id"], | |
| "title": best.get("title", best["sop_id"]), | |
| "matched_rules": best.get("matched_rules", []), | |
| "recommended_actions": best.get("recommended_actions", []), | |
| "caution": best.get("caution", "Confirm details before proceeding."), | |
| "source_type": "sop_grounded", | |
| "confidence": "high", | |
| } | |
| # ββ Ticket creation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def create_ticket( | |
| incident_id: str, | |
| severity: str, | |
| assigned_to: str, | |
| summary: str | None = None, | |
| notes: str | None = None, | |
| ) -> dict[str, Any]: | |
| tickets = storage.load("tickets") | |
| n = len(tickets) + 1 | |
| ticket = { | |
| "ticket_id": f"LAB-TICKET-{n:03d}", | |
| "incident_id": incident_id, | |
| "summary": summary or f"Maintenance required: {incident_id}", | |
| "severity": severity, | |
| "assigned_to": assigned_to, | |
| "notes": notes, | |
| "status": "open", | |
| "created_at": storage.now_iso(), | |
| } | |
| tickets.append(ticket) | |
| storage.save("tickets", tickets) | |
| # Link ticket back to its incident | |
| incidents = storage.load("incidents") | |
| incident = next((i for i in incidents if i["incident_id"] == incident_id), None) | |
| if incident: | |
| incident.setdefault("tickets", []).append(ticket["ticket_id"]) | |
| incident["updated_at"] = storage.now_iso() | |
| storage.save("incidents", incidents) | |
| return ticket | |
| # ββ History recall ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def recall_history(equipment_id: str, issue_type: str | None = None) -> dict[str, Any]: | |
| """Return prior events for this equipment, clearly marked as past history, not current cause.""" | |
| prior_events = storage.load("prior_events") | |
| matches = [] | |
| for event in prior_events: | |
| if event.get("equipment_id") != equipment_id: | |
| continue | |
| if issue_type and event.get("issue_type") != issue_type: | |
| continue | |
| matches.append({ | |
| "summary": event.get("summary", f"{equipment_id} had a {event.get('issue_type')} event"), | |
| "timestamp": event.get("timestamp"), | |
| "recorded_cause": event.get("recorded_cause"), | |
| "resolution": event.get("resolution"), | |
| "duration_hours": event.get("duration_hours"), | |
| "source_type": event.get("source_type", "human_confirmed"), | |
| "confidence": event.get("confidence", "high"), | |
| }) | |
| if not matches: | |
| return { | |
| "found": False, | |
| "related_events": [], | |
| "uncertainty_note": "No prior incidents found for this equipment and issue type.", | |
| "source_type": "human_confirmed", | |
| "confidence": "high", | |
| } | |
| return { | |
| "found": True, | |
| "related_events": matches, | |
| "uncertainty_note": ( | |
| "Prior incidents may be relevant, but current root cause is not confirmed. " | |
| "Do not assume the same cause applies to the current event." | |
| ), | |
| "source_type": "human_confirmed", | |
| "confidence": "high", | |
| } | |
| # ββ Handoff generation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_handoff( | |
| shift: str | None = None, | |
| incident_id: str | None = None, | |
| ) -> dict[str, Any]: | |
| """Generate a shift handoff summary. | |
| With incident_id: focused incident handoff for the agent to narrate. | |
| Without: generic lab-wide shift handoff. | |
| """ | |
| if incident_id: | |
| return _incident_handoff(incident_id) | |
| return _shift_handoff(shift) | |
| def _incident_handoff(incident_id: str) -> dict[str, Any]: | |
| incidents = storage.load("incidents") | |
| incident = next((i for i in incidents if i["incident_id"] == incident_id), None) | |
| if not incident: | |
| return {"error": f"Incident {incident_id} not found.", "incident_id": incident_id} | |
| equipment_list = storage.load("equipment") | |
| eq = next((e for e in equipment_list if e["id"] == incident["equipment_id"]), {}) | |
| tickets = storage.load("tickets") | |
| inc_tickets = [t for t in tickets if t.get("incident_id") == incident_id] | |
| prior_events = storage.load("prior_events") | |
| related_prior = [ | |
| e for e in prior_events | |
| if e.get("equipment_id") == incident["equipment_id"] | |
| and e.get("issue_type") == incident["type"] | |
| ] | |
| sop = retrieve_sop(issue_type=incident["type"], equipment_id=incident["equipment_id"]) | |
| summary_points = [ | |
| f"{eq.get('name', incident['equipment_id'])} {incident['type'].replace('_', ' ')} detected", | |
| f"Current reading: {incident.get('current_value', 'unknown')}", | |
| f"Threshold: {incident.get('threshold', 'unknown')}", | |
| f"Severity: {incident['severity']}", | |
| f"Incident status: {incident['status']}", | |
| ] | |
| if incident.get("observations"): | |
| for obs in incident["observations"]: | |
| summary_points.append(f"Observation: {obs}") | |
| open_actions = sop.get("recommended_actions", []) if sop.get("found") else [ | |
| "Confirm door is closed", | |
| "Check alarm state", | |
| "Move critical samples if escalation threshold is confirmed met", | |
| ] | |
| prior_note = None | |
| if related_prior: | |
| p = related_prior[0] | |
| prior_note = ( | |
| f"{eq.get('name', incident['equipment_id'])} had a {incident['type'].replace('_', ' ')} " | |
| f"on {p.get('timestamp', 'unknown date')}. " | |
| f"Recorded cause: {p.get('recorded_cause', 'unknown')}. " | |
| f"Resolution: {p.get('resolution', 'unknown')}." | |
| ) | |
| return { | |
| "generated_at": storage.now_iso(), | |
| "incident_id": incident_id, | |
| "summary": summary_points, | |
| "current_status": { | |
| "equipment": eq.get("name"), | |
| "current_value": incident.get("current_value"), | |
| "threshold": incident.get("threshold"), | |
| "severity": incident["severity"], | |
| "incident_status": incident["status"], | |
| }, | |
| "observations": incident.get("observations", []), | |
| "sop_used": sop.get("title") if sop.get("found") else None, | |
| "matched_rules": sop.get("matched_rules", []) if sop.get("found") else [], | |
| "open_actions": open_actions, | |
| "tickets_created": inc_tickets, | |
| "related_prior_event": prior_note, | |
| "uncertainty_statement": ( | |
| "Prior incidents may be relevant, but the current root cause is not confirmed. " | |
| "All observations are recorded as-seen. Root cause remains unconfirmed." | |
| ), | |
| } | |
| def _shift_handoff(shift: str | None) -> dict[str, Any]: | |
| samples = storage.load("samples") | |
| events = storage.load("events") | |
| reminders = storage.load("reminders") | |
| messages = storage.load("messages") | |
| incidents = storage.load("incidents") | |
| movements = [e for e in events if e.get("type") == "sample_moved"] | |
| calcs = [e for e in events if e.get("type") == "calculation_validated"] | |
| sops = [e for e in events if e.get("type") == "sop_retrieved"] | |
| lookups = [e for e in events if e.get("type") == "inventory_lookup"] | |
| open_incidents = [i for i in incidents if i.get("status") == "open"] | |
| unresolved = [] | |
| for s in samples: | |
| if s.get("room_temp_started_at") and "freezer" not in s.get("location", "").lower(): | |
| unresolved.append( | |
| f"{s['sample_id']} is out of cold storage on {s['location']} β outcome not confirmed." | |
| ) | |
| for inc in open_incidents: | |
| unresolved.append( | |
| f"Open incident {inc['incident_id']}: {inc['type']} on {inc['equipment_id']} " | |
| f"(severity: {inc['severity']}) β root cause not confirmed." | |
| ) | |
| if not unresolved: | |
| unresolved.append("No open risks detected. Some facts may be user-reported and unconfirmed.") | |
| return { | |
| "generated_at": storage.now_iso(), | |
| "shift": shift or "next", | |
| "open_incidents": open_incidents, | |
| "sample_movements": movements, | |
| "current_sample_status": samples, | |
| "active_reminders": [r for r in reminders if r.get("status") == "open"], | |
| "calculations_validated": calcs, | |
| "sops_retrieved": sops, | |
| "inventory_lookups": lookups, | |
| "messages": messages, | |
| "unresolved_risks": unresolved, | |
| "uncertainty_note": ( | |
| "Some facts are user-reported or camera-inferred and not human-confirmed." | |
| ), | |
| } | |
| # ββ Calculation validation ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _close(a: float, b: float) -> bool: | |
| return abs(a - b) <= max(0.01 * abs(b), 0.001) | |
| def validate_calculation( | |
| calculation_type: str, | |
| target_percent: float | None, | |
| final_volume_ml: float | None, | |
| user_answer_ul: float | None = None, | |
| stock_percent: float | None = None, | |
| user_answer_g: float | None = None, | |
| ) -> dict[str, Any]: | |
| """Validate a reagent calculation. Supports three bases: | |
| - percent_volume_volume (v/v): X% v/v β microliters of neat reagent | |
| - percent_weight_volume (w/v): X% w/v β grams of solid (X g per 100 mL) | |
| - stock_dilution (C1V1=C2V2): dilute a stock_percent stock to target_percent | |
| """ | |
| ct = (calculation_type or "percent_volume_volume").lower() | |
| ct = ct.replace("/", "_").replace("-", "_").replace(" ", "_") | |
| # ββ Stock dilution (C1Β·V1 = C2Β·V2). Triggered by an explicit type OR a stock_percent. ββ | |
| if stock_percent is not None or ct in ("stock_dilution", "dilution", "c1v1", "c1v1c2v2"): | |
| if target_percent is None or final_volume_ml is None or not stock_percent: | |
| return { | |
| "status": "ambiguous", "formula": None, "assumptions": [], | |
| "warning": "For a stock dilution I need the stock %, the target %, and the final volume.", | |
| "source_type": "calculated", "confidence": "low", | |
| } | |
| v1_ml = (target_percent * final_volume_ml) / stock_percent | |
| expected_ul = round(v1_ml * 1000.0, 4) | |
| status = "ambiguous" if user_answer_ul is None else ("correct" if _close(user_answer_ul, expected_ul) else "incorrect") | |
| return { | |
| "status": status, | |
| "expected_ul": expected_ul, | |
| "user_answer_ul": user_answer_ul, | |
| "formula": f"C1Β·V1 = C2Β·V2 β V1 = {target_percent}% Γ {final_volume_ml:g} mL / {stock_percent}% = {v1_ml:g} mL = {expected_ul:g} uL of stock", | |
| "assumptions": [ | |
| f"diluting a {stock_percent}% stock down to {target_percent}%", | |
| f"final volume is {final_volume_ml:g} mL", | |
| "stock % and target % share the same basis (both v/v or both w/v)", | |
| ], | |
| "warning": "Add the stock, then top up (q.s.) to the final volume with diluent.", | |
| "source_type": "calculated", "confidence": "high", | |
| } | |
| # ββ Weight / volume (X% w/v = X g per 100 mL). ββ | |
| if ct in ("percent_weight_volume", "w_v", "wv", "weight_volume", "mass_volume", "weight_per_volume"): | |
| if target_percent is None or final_volume_ml is None: | |
| return { | |
| "status": "ambiguous", "formula": None, "assumptions": [], | |
| "warning": "I need the target percent and the final volume for a w/v calculation.", | |
| "source_type": "calculated", "confidence": "low", | |
| } | |
| expected_g = round((target_percent / 100.0) * final_volume_ml, 6) | |
| expected_mg = round(expected_g * 1000.0, 4) | |
| status = "ambiguous" if user_answer_g is None else ("correct" if _close(user_answer_g, expected_g) else "incorrect") | |
| return { | |
| "status": status, | |
| "expected_g": expected_g, | |
| "expected_mg": expected_mg, | |
| "user_answer_g": user_answer_g, | |
| "formula": f"{target_percent}% w/v = {target_percent} g / 100 mL β {expected_g:g} g ({expected_mg:g} mg) in {final_volume_ml:g} mL", | |
| "assumptions": [ | |
| "percent is w/v (grams of solute per 100 mL)", | |
| f"final volume is {final_volume_ml:g} mL", | |
| ], | |
| "warning": "Dissolve the solid first, then bring to final volume (q.s.) β don't add solid to the full volume.", | |
| "source_type": "calculated", "confidence": "high", | |
| } | |
| # ββ Volume / volume (default). ββ | |
| if ct in ("percent_volume_volume", "v_v", "vv", "volume_volume", "percent_v_v", "volume_per_volume"): | |
| if target_percent is None or final_volume_ml is None: | |
| return { | |
| "status": "ambiguous", "formula": None, "assumptions": [], | |
| "warning": "I need both the target percent and the final volume to check this.", | |
| "source_type": "calculated", "confidence": "low", | |
| } | |
| expected_ml = (target_percent / 100.0) * final_volume_ml | |
| expected_ul = round(expected_ml * 1000.0, 4) | |
| status = "ambiguous" if user_answer_ul is None else ("correct" if _close(user_answer_ul, expected_ul) else "incorrect") | |
| return { | |
| "status": status, | |
| "expected_ul": expected_ul, | |
| "user_answer_ul": user_answer_ul, | |
| "formula": f"{target_percent} / 100 * {final_volume_ml} mL = {expected_ml:g} mL = {expected_ul:g} uL", | |
| "assumptions": ["percent is v/v", f"{final_volume_ml:g} mL is the final volume"], | |
| "warning": "If you're diluting from a stock, give me the stock concentration and I'll use C1V1=C2V2.", | |
| "source_type": "calculated", "confidence": "high", | |
| } | |
| return { | |
| "status": "ambiguous", "formula": None, "assumptions": [], | |
| "warning": f"Calculation type '{calculation_type}' is not supported. I handle v/v, w/v, and stock dilutions.", | |
| "source_type": "calculated", "confidence": "low", | |
| } | |
| # ββ Emergency / escalation messaging ββββββββββββββββββββββββββββββββββββββββββ | |
| def send_emergency_message(recipient_role: str, message: str, confirmed: bool = False) -> dict[str, Any]: | |
| """Draft (confirmed=False) or send (confirmed=True) an escalation message. The record's | |
| truth state reflects the gate: a draft is pending_confirmation, a sent message is human_confirmed. | |
| Shape matches POST /api/tools/send_emergency_message so the agent and the UI produce identical records. | |
| """ | |
| messages = storage.load("messages") | |
| record = { | |
| "id": storage.next_id("msg", messages), | |
| "recipient_role": recipient_role, | |
| "message": message, | |
| "status": "sent" if confirmed else "draft", | |
| "source_type": "human_confirmed" if confirmed else "pending_confirmation", | |
| "timestamp": storage.now_iso(), | |
| } | |
| messages.append(record) | |
| storage.save("messages", messages) | |
| return record | |
| # ββ Inventory lookup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def find_inventory(item_name: str) -> dict[str, Any]: | |
| q = item_name.lower().strip() | |
| for item in storage.load("inventory"): | |
| name = item["item_name"].lower() | |
| if q in name or name in q: | |
| note = ( | |
| "Inventory record location is high-confidence; the count is camera-inferred." | |
| if item.get("camera_inferred_count") is not None | |
| else "From the inventory record." | |
| ) | |
| return {"found": True, **item, "note": note} | |
| return {"found": False, "message": f"No inventory record for '{item_name}'."} | |