CliniqAl / app /contracts.py
someonesphantom's picture
person A updated, Wiki → 10 pages, Evals/RAGAS, Langfuse traces on retrieval, Telegram notification channel, Postgres-backed mini calendar MCP (book / cancel / change)
3448ea9
Raw
History Blame Contribute Delete
8.65 kB
"""Shared Pydantic contracts — the team interface between A, B, and C.
RULES
-----
1. Field names are **frozen** as of Day 0. Additive changes only.
2. If you must change a field, post in the team channel; others have 24h to ack.
3. Every agent returns one of these models — no dicts, no strings.
4. This file is the only one we type-check with `mypy --strict`.
"""
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
# ---------------------------------------------------------------------------
# Common / shared
# ---------------------------------------------------------------------------
class StrictModel(BaseModel):
"""Base class that forbids extra fields and freezes instances."""
model_config = ConfigDict(extra="forbid", frozen=False, populate_by_name=True)
class TaskType(str, Enum):
DOCUMENTATION = "documentation"
APPOINTMENT = "appointment"
ROSTER = "roster"
HANDOVER = "handover"
DISCHARGE = "discharge"
CLERICAL = "clerical"
WIKI_MAINT = "wiki_maintenance"
class Channel(str, Enum):
WHATSAPP = "whatsapp"
WEB = "web"
EMAIL = "email"
PHONE = "phone"
TELEGRAM = "telegram" # additive (Person A) — bot-based notifications
class Priority(str, Enum):
STABLE = "stable"
WATCH = "watch"
URGENT = "urgent"
class Citation(StrictModel):
"""A pointer back to a wiki page, guideline, or retrieval hit. Required on
every clinically-significant output for the audit trail."""
source_type: Literal["wiki", "guideline", "icd10", "drug_db", "lab", "soap"]
source_id: str
title: str | None = None
url: str | None = None
snippet: str | None = None
class Hit(StrictModel):
"""A search hit from the retrieval layer (Chroma + BM25 hybrid)."""
doc_id: str
text: str
score: float
source: Literal["chroma", "bm25", "hybrid"]
metadata: dict[str, str] = Field(default_factory=dict)
# ---------------------------------------------------------------------------
# Wiki refs (Person A owns the wiki; A and B both read)
# ---------------------------------------------------------------------------
class PatientWikiPageRef(StrictModel):
patient_id: str
page_path: str # e.g. "wiki/patients/p-001.md"
version: str # git sha or content hash
last_updated: datetime
class WikiLintIssue(StrictModel):
page_path: str
kind: Literal["missing_field", "contradiction", "stale", "broken_xref"]
detail: str
class WikiLintReport(StrictModel):
generated_at: datetime
issues: list[WikiLintIssue] = Field(default_factory=list)
# ---------------------------------------------------------------------------
# Documentation (Person A returns this)
# ---------------------------------------------------------------------------
class SOAPSection(StrictModel):
subjective: str
objective: str
assessment: str
plan: str
class ICD10Suggestion(StrictModel):
code: str
label: str
confidence: float = Field(ge=0.0, le=1.0)
class SOAPNoteDraft(StrictModel):
draft_id: str
patient_id: str
encounter_ts: datetime
soap: SOAPSection
icd10_suggestions: list[ICD10Suggestion] = Field(default_factory=list)
guideline_suggestions: list[str] = Field(default_factory=list)
sources: list[Citation] = Field(default_factory=list)
referral_needed: bool = False
confidence_overall: float = Field(ge=0.0, le=1.0, default=0.0)
transcript_excerpt: str | None = None
model: str # e.g. "groq/qwen3-32b"
# ---------------------------------------------------------------------------
# Appointments (Person B returns this)
# ---------------------------------------------------------------------------
class BookingStatus(str, Enum):
PROPOSED = "proposed"
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
NO_SHOW = "no_show"
COMPLETED = "completed"
WAITLISTED = "waitlisted"
class ReminderJob(StrictModel):
job_id: str
channel: Channel
fire_at: datetime
enqueued: bool = False
class BookingResult(StrictModel):
appointment_id: str
patient_id: str
doctor_id: str
slot_iso: datetime
duration_minutes: int = 15
status: BookingStatus
confirmation_sent_at: datetime | None = None
reminder_jobs: list[ReminderJob] = Field(default_factory=list)
calendar_event_id: str | None = None
notes: str | None = None
class CommunicationDraft(StrictModel):
"""Outbound message awaiting approval (or already sent if `sent_at` set)."""
draft_id: str
channel: Channel
to: str # phone number, email, etc
subject: str | None = None
body: str
requires_approval: bool = True
approved_by: str | None = None
sent_at: datetime | None = None
related_patient_id: str | None = None
related_appointment_id: str | None = None
# ---------------------------------------------------------------------------
# Discharge (Person B returns this)
# ---------------------------------------------------------------------------
class DischargeSubtaskStatus(str, Enum):
QUEUED = "queued"
IN_PROGRESS = "in_progress"
DONE = "done"
FAILED = "failed"
AWAITING_APPROVAL = "awaiting_approval"
class DischargeSubtask(StrictModel):
name: Literal[
"discharge_summary",
"pharmacy",
"family_notification",
"follow_up_appointment",
"billing_trigger",
]
status: DischargeSubtaskStatus
detail: str | None = None
artifact_id: str | None = None # FK back to SOAP draft, booking, etc.
class DischargeCoordination(StrictModel):
discharge_id: str
patient_id: str
initiated_at: datetime
subtasks: list[DischargeSubtask]
target_complete_by: datetime
completed_at: datetime | None = None
# ---------------------------------------------------------------------------
# Rostering (Person C returns this)
# ---------------------------------------------------------------------------
class ShiftAssignment(StrictModel):
shift_id: str
staff_id: str
role: Literal["doctor", "nurse", "tech", "admin"]
start: datetime
end: datetime
is_replacement: bool = False
class RosterResult(StrictModel):
roster_id: str
window_start: datetime
window_end: datetime
assignments: list[ShiftAssignment]
fairness_score: float = Field(ge=0.0, le=1.0) # higher is fairer
unresolved_conflicts: list[str] = Field(default_factory=list)
generated_in_seconds: float
# ---------------------------------------------------------------------------
# Handover (Person C returns this)
# ---------------------------------------------------------------------------
class PatientBrief(StrictModel):
patient_id: str
bed: str | None = None
priority: Priority
one_liner: str # ≤ 160 chars
actions_pending: list[str] = Field(default_factory=list)
sources: list[Citation] = Field(default_factory=list)
class HandoverBrief(StrictModel):
brief_id: str
ward_id: str
shift_end_ts: datetime
patients: list[PatientBrief] # ordered urgent → stable
estimated_read_minutes: int
# ---------------------------------------------------------------------------
# Orchestrator routing (Person C returns this)
# ---------------------------------------------------------------------------
class RoutingDecision(StrictModel):
task: TaskType
confidence: float = Field(ge=0.0, le=1.0)
rationale: str
escalate: bool = False
# ---------------------------------------------------------------------------
# Error / escalation surface shared across all agents
# ---------------------------------------------------------------------------
class AgentError(StrictModel):
agent: str
code: str # e.g. "llm_timeout", "calendar_conflict"
message: str
recoverable: bool = True
__all__ = [
# enums
"TaskType",
"Channel",
"Priority",
"BookingStatus",
"DischargeSubtaskStatus",
# base
"StrictModel",
"Citation",
"Hit",
# wiki
"PatientWikiPageRef",
"WikiLintIssue",
"WikiLintReport",
# documentation
"SOAPSection",
"ICD10Suggestion",
"SOAPNoteDraft",
# appointments + comms
"ReminderJob",
"BookingResult",
"CommunicationDraft",
# discharge
"DischargeSubtask",
"DischargeCoordination",
# roster
"ShiftAssignment",
"RosterResult",
# handover
"PatientBrief",
"HandoverBrief",
# orchestrator
"RoutingDecision",
# error
"AgentError",
]