update the codebase poc ver1

This commit is contained in:
DatTT127
2026-07-07 15:54:17 +07:00
parent fed5f277f4
commit 1622dc8fc5
452 changed files with 83999 additions and 66328 deletions

View File

@@ -0,0 +1,115 @@
# Dependencies, Orchestration, and Integration — Sprint 1_2
## Data Engineering Alignment
### Storage Strategy
- **Structured metadata**: PostgreSQL (aligned with backend modules)
- **Artifacts** (DICOM, images, masks, overlays, models): S3-compatible bucket (MinIO)
- **Naming convention**: UUIDs only — no PHI in filenames, keys, or URLs
- **Access**: Presigned URLs for temporary access
### Canonical JSON Schemas
All serialized domain objects must validate against canonical schemas defined in `data/schemas/`. Key schemas:
- `session.schema.json`
- `frame.schema.json`
- `prediction.schema.json`
- `measurement.schema.json`
- `audit.schema.json`
### Model Output Normalization
All model adapters must normalize outputs to canonical labels.
**Segmentation classes:**
- `background`
- `effusion`
- `fat`
- `fat-pat`
- `femur`
- `synovium`
- `tendon`
**Angle classes:**
- `med-lat`
- `post-trans`
- `sup-trans-flex`
- `sup-up-long`
**Severity grades:**
- 0: Rất nhẹ
- 1: Nhẹ
- 2: Trung bình
- 3: Nặng
---
## Orchestrators and Use Cases
Orchestrators coordinate the workflow by sequencing agents and enforcing state machines.
### Key Use Cases
1. **Upload and Ingest**
- Input: multipart DICOM or image upload
- Steps: `DICOMIngestAgent` / `ImageUploadIngestAgent``FrameStorageAdapter`
- Output: `DiagnosticSession`, `ScanFrame`, `ImageAsset`
2. **Run Analysis Pipeline**
- Input: `DiagnosticSession`
- Steps: `VisionPipelineAgent``InferenceRunner``MeasurementAgent``SeverityScorerAgent`
- Output: `AnalysisJob` with completed results
3. **Review and Finalize**
- Input: Clinician review data
- Steps: `LedgerWriterAgent``ReviewDecision`
- Output: Updated session state
---
## Integration with Backend Architecture
This OOP design maps to the backend specification modules:
| OOP Layer | Backend Module |
|-----------|---------------|
| Orchestrators & APIs | `api/` routers (session_api, analysis_api, etc.) |
| Agents/Services | `implementation/` services |
| Adapters | `implementation/` adapters |
| Domain Objects | ORM models (PostgreSQL) + S3 references |
| Orchestration | `implementation/analysis_jobs/service.py` (async jobs) |
---
## Validation and Testing
### Structural Validation
- All candidate objects must map to either a PostgreSQL table or an S3 artifact reference.
- No object may contain PHI fields that bypass scrubbing.
### Behavioral Validation
- Adapter interfaces must support both mock and real implementations.
- Agents must be stateless and idempotent where possible.
### End-to-End Flow
```
image/DICOM upload → secure local ingest → frame extraction → preprocessing → model inference → structured metrics/mask → API result → browser mask preview
```
---
## ObjectObject and ObjectService Relationship Summary
- `ClinicianUser` owns `DiagnosticSession` and authors `ReviewDecision`
- `PatientCase` groups many `DiagnosticSession` records
- `DiagnosticSession` contains `ScanFrame`s, spawns `AnalysisJob`s, and tracks `Calibration` and `ReviewDecision` records
- `AnalysisJob` consists of `PipelineStep`s and produces prediction, mask, measurement, and grade objects
- `ScanFrame` becomes a `PreprocessedImage` via `FramePreprocessor`
- `ImageAsset` stores the raw binary artifact for a `ScanFrame`
- `ArtifactReference` can point to any `ScanFrame` or mask/overlay S3 object
- `LedgerWriterAgent` writes `AuditLedgerEntry` for all state changes
## AgentAdapter Dependencies
- `DICOMIngestAgent` and `ImageUploadIngestAgent``FrameStorageAdapter`
- `ArtifactStoreAgent``FrameStorageAdapter` and `ArtifactStorageAdapter`
- `InferenceRunner``InferenceAdapter` (PyTorch, Triton, or Mock)
- `ModelRegistryAgent``ArtifactStorageAdapter`

View File

@@ -0,0 +1,393 @@
# Object Specifications — Sprint 1_2
## Overview
Domain objects represent **persistable clinical and analysis facts**. They are pure data structures with minimal behavior, focused on encapsulating business rules and state. They are persisted via PostgreSQL (structured metadata) and S3-compatible storage (artifacts).
## OOP Boundary
```
Domain objects = persistable clinical/analysis facts.
Agents/services = runtime workers that transform facts.
Orchestrators = coordinate use cases and enforce workflow state.
Adapters = hide PyTorch, filesystem, image, and API details.
```
## Layer Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ API Layer (FastAPI) │
├─────────────────────────────────────────────────────────────┤
│ Orchestrators (Use Cases) │
├─────────────────────────────────────────────────────────────┤
│ Agents & Services (Workers) │
├─────────────────────────────────────────────────────────────┤
│ Domain Objects │
├─────────────────────────────────────────────────────────────┤
│ Adapters │
└─────────────────────────────────────────────────────────────┘
```
---
## Domain Objects
### ClinicianUser
Represents an authenticated medical professional.
**Fields:**
- `user_id`: UUID / primary key
- `username`: str
- `hashed_password`: str
- `name`: str
- `role`: str (e.g., "radiologist", "support")
- `credentials`: dict | None
- `specialization`: str
- `created_at`: datetime
- `last_login`: datetime | None
**Relationships:**
- owns many `DiagnosticSession`s
- author of many `ReviewDecision`s
**Responsibilities:**
- Authentication (via `AuthModule`)
- Session ownership
- Review and sign decisions
---
### 2. PatientCase
Represents a patient's overall medical case.
**Fields:**
- `case_id`: UUID
- `patient_identifier`: str (hashed / pseudonymized)
- `demographic_info`: dict
- `medical_history_summary`: dict
- `created_by`: `ClinicianUser`
- `created_at`: datetime
**Relationships:**
- has many `DiagnosticSession`s
**Responsibilities:**
- Case registration and tracking
- Session grouping
---
### 3. DiagnosticSession
Represents a single ultrasound examination session.
**Fields:**
- `session_id`: UUID
- `case_id`: ForeignKey → PatientCase
- `clinician_id`: ForeignKey → ClinicianUser
- `status`: str (e.g., "created", "uploaded", "in_progress", "completed", "reviewed")
- `created_at`: datetime
- `updated_at`: datetime
**Relationships:**
- belongs to `PatientCase`
- belongs to `ClinicianUser`
- has many `ScanFrame`s
- has many `AnalysisJob`s
- has many `ReviewDecision`s
**Responsibilities:**
- Session lifecycle management
- Frame and job grouping
- Review state enforcement
---
### 4. ScanFrame
Represents a single ultrasound image frame extracted from DICOM or standard image upload.
**Fields:**
- `frame_id`: UUID
- `session_id`: ForeignKey → DiagnosticSession
- `storage_reference`: str (S3 key)
- `original_format`: str (e.g., "dicom", "png", "jpeg")
- `frame_number`: int | None
- `metadata`: dict (DICOM tags, image dimensions, etc.)
- `checksum`: str (SHA-256)
- `created_at`: datetime
**Relationships:**
- belongs to `DiagnosticSession`
- has one `ImageAsset` (the raw artifact)
- has one `PreprocessedImage`
**Responsibilities:**
- Frame metadata capture
- PHI-safe storage reference management
---
### 5. ImageAsset
Represents the raw storage artifact for a frame.
**Fields:**
- `asset_id`: UUID
- `frame_id`: ForeignKey → ScanFrame
- `storage_key`: str (S3 / MinIO key, UUID-based, no PHI)
- `content_type`: str
- `size_bytes`: int
- `checksum`: str (SHA-256)
- `uploaded_at`: datetime
**Responsibilities:**
- Binary artifact storage reference
- Integrity verification
---
### 6. Calibration
Device-specific calibration parameters for a session.
**Fields:**
- `calibration_id`: UUID
- `session_id`: ForeignKey → DiagnosticSession
- `pixel_to_mm_ratio`: float
- `parameters`: dict
- `recorded_at`: datetime
**Responsibilities:**
- Measurement calibration
- ROI metric scaling
---
### 7. AnalysisJob
Request for AI/ML analysis on session frame(s).
**Fields:**
- `job_id`: UUID
- `session_id`: ForeignKey → DiagnosticSession
- `parameters`: dict (e.g., selected models, flags)
- `model_versions`: dict (task → model_id + version)
- `status`: str (e.g., "pending", "running", "completed", "failed")
- `result`: dict | None
- `created_at`: datetime
- `updated_at`: datetime
**Relationships:**
- belongs to `DiagnosticSession`
- has many `PipelineStep`s
- produces angle, inflammation, segmentation, measurement, and grade results
**Responsibilities:**
- Async job orchestration
- Result aggregation
---
### 8. PipelineStep
Single step in the analysis pipeline.
**Fields:**
- `step_id`: UUID
- `job_id`: ForeignKey → AnalysisJob
- `task_type`: str (e.g., "angle_classification", "inflammation_detection", "segmentation_sup", "segmentation_post", "measurement", "severity_scoring")
- `status`: str
- `output`: dict | None
- `duration_ms`: int | None
- `started_at`: datetime | None
- `completed_at`: datetime | None
**Responsibilities:**
- Step-level progress tracking
- Error isolation
---
### 9. ModelRegistryEntry
Metadata record for a registered ML model.
**Fields:**
- `model_id`: str
- `name`: str
- `task_type`: str
- `version`: str
- `description`: str
- `framework`: str (e.g., "pytorch", "onnx")
- `labels`: list[str]
- `registered_at`: datetime
- `is_active`: bool
**Responsibilities:**
- Model discovery and selection
- Version tracking
---
### 10. ModelArtifact
Actual stored ML model artifact.
**Fields:**
- `artifact_id`: UUID
- `model_id`: ForeignKey → ModelRegistryEntry
- `storage_key`: str (S3 key, UUID-based)
- `format`: str (e.g., ".pth", ".onnx")
- `size_bytes`: int
- `checksum`: str
- `uploaded_at`: datetime
**Responsibilities:**
- Secure storage of model weights
- Integrity verification
---
### 11. PreprocessedImage
Frame after preprocessing transformations.
**Fields:**
- `preprocessed_id`: UUID
- `frame_id`: ForeignKey → ScanFrame
- `preprocessing_steps`: list[str]
- `storage_reference`: str (S3 key, or inline base64 for small artifacts)
- `width`: int
- `height`: int
- `created_at`: datetime
**Responsibilities:**
- Intermediate processing artifact management
---
### 12. AnglePrediction
Output of the angle classification model.
**Fields:**
- `prediction_id`: UUID
- `job_id`: ForeignKey → AnalysisJob
- `step_id`: ForeignKey → PipelineStep
- `angle_class`: str (e.g., "med-lat", "post-trans", "sup-trans-flex", "sup-up-long")
- `confidence`: float
- `metadata`: dict
**Responsibilities:**
- Classification result encapsulation
---
### 13. InflammationPrediction
Output of the inflammation detection model.
**Fields:**
- `prediction_id`: UUID
- `job_id`: ForeignKey → AnalysisJob
- `step_id`: ForeignKey → PipelineStep
- `detected`: bool
- `confidence`: float
**Responsibilities:**
- Binary detection result encapsulation
---
### 14. SegmentationMask
Output of the segmentation model.
**Fields:**
- `mask_id`: UUID
- `job_id`: ForeignKey → AnalysisJob
- `step_id`: ForeignKey → PipelineStep
- `storage_reference`: str (S3 key)
- `overlay_reference`: str (S3 key)
- `color_legend`: dict (class → color)
- `metadata`: dict
**Responsibilities:**
- Segmentation result storage and retrieval
---
### 15. Measurement
Quantitative measurement derived from a segmentation mask.
**Fields:**
- `measurement_id`: UUID
- `job_id`: ForeignKey → AnalysisJob
- `step_id`: ForeignKey → PipelineStep
- `thickness_mm`: float | None
- `pixel_to_mm_ratio`: float
- `roi_specification`: dict (e.g., bounding box, region)
- `created_at`: datetime
**Responsibilities:**
- Measurement calculation and storage
---
### 16. SynovitisGrade
Final severity grade (03) for synovitis.
**Fields:**
- `grade_id`: UUID
- `job_id`: ForeignKey → AnalysisJob
- `step_id`: ForeignKey → PipelineStep
- `level`: int (03)
- `label`: str
- `combined_score`: float | None
- `confidence`: float | None
**Responsibilities:**
- Severity scoring encapsulation
---
### 17. ReviewDecision
Clinician's approval, correction, or rejection of AI results.
**Fields:**
- `decision_id`: UUID
- `session_id`: ForeignKey → DiagnosticSession
- `job_id`: ForeignKey → AnalysisJob
- `reviewer_id`: ForeignKey → ClinicianUser
- `decision_type`: str ("approve", "correct", "reject")
- `justification`: str | None
- `created_at`: datetime
**Responsibilities:**
- HITL decision capture
- Review audit trail
---
### 18. ArtifactReference
Polymorphic reference to any stored artifact.
**Fields:**
- `reference_id`: UUID
- `artifact_type`: str
- `associated_entity_id`: UUID
- `storage_key`: str (S3 key)
- `content_type`: str
- `created_at`: datetime
**Responsibilities:**
- Unified artifact reference management
---
### 19. AuditLedgerEntry
Immutable audit trail entry for any significant event.
**Fields:**
- `entry_id`: UUID
- `entity_type`: str
- `entity_id`: UUID
- `action`: str
- `user_id`: UUID | None
- `checksum`: str (SHA-256 of the event payload)
- `metadata`: dict
- `timestamp`: datetime
**Responsibilities:**
- Immutable audit trail
- Compliance (Decree 13 / Circular 46)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
# Services, Agents, and Adapters — Sprint 1_2
Agents and services are the **runtime workers** that transform domain objects. Each agent has a single, focused responsibility and collaborates via well-defined interfaces.
## Ingestion Agents
### DICOMIngestAgent
- **Responsibility:** Parse and validate DICOM files, extract metadata and renderable frames.
- **Input:** `UploadFile` (DICOM bytes)
- **Output:** `ScanFrame`, `ImageAsset`
- **Collaborators:** `FrameStorageAdapter` (S3)
### ImageUploadIngestAgent
- **Responsibility:** Handle standard image uploads (JPEG, PNG, etc.).
- **Input:** `UploadFile` (image bytes)
- **Output:** `ScanFrame`, `ImageAsset`
- **Collaborators:** `FrameStorageAdapter`
## Preprocessing and Validation Agents
### FramePreprocessor
- **Responsibility:** Apply preprocessing transformations (CLAHE, resizing, normalization).
- **Input:** `ScanFrame`
- **Output:** `PreprocessedImage`
- **Collaborators:** Image libraries via adapter
### AngleValidatorAgent
- **Responsibility:** Validate angle classification results against clinical rules.
- **Input:** `AnglePrediction`
- **Output:** `AnglePrediction` (possibly adjusted confidence)
- **Collaborators:** Clinical rule engine
### ROICropperAgent
- **Responsibility:** Extract regions of interest for specialized models.
- **Input:** `PreprocessedImage`
- **Output:** Cropped image segments
- **Collaborators:** Frame storage, preprocessing
## Core Analysis Agents
### VisionPipelineAgent
- **Responsibility:** Orchestrate the end-to-end vision inference pipeline for a session.
- **Input:** `DiagnosticSession`, list of `ScanFrame`s
- **Output:** `AnalysisJob` with completed `PipelineStep`s and results
- **Collaborators:** `InferenceRunner`, `MeasurementAgent`, `SeverityScorerAgent`, `ModelRegistryAgent`
### InferenceRunner
- **Responsibility:** Execute ML model inference via adapters (PyTorch, Triton, or Mock).
- **Input:** `ModelReference` (id + version), `ProcessedImage` data
- **Output:** Raw prediction payloads
- **Collaborators:** `PyTorchAdapter`, `TritonAdapter`, `MockAdapter`
### MeasurementAgent
- **Responsibility:** Calculate quantitative measurements from `SegmentationMask` using `Calibration`.
- **Input:** `SegmentationMask`, `Calibration`
- **Output:** `Measurement`
- **Collaborators:** Calibration service, segmentation model geometry
### SeverityScorerAgent
- **Responsibility:** Compute synovitis grade (03) from effusion and synovium measurements and inflammation prediction.
- **Input:** `Measurement`, `InflammationPrediction`
- **Output:** `SynovitisGrade`
- **Collaborators:** Clinical scoring rules
## Management Agents
### ModelRegistryAgent
- **Responsibility:** Manage model registration, versioning, and availability checks.
- **Input:** `ModelRegistryEntry` data, `ModelArtifact` binaries
- **Output:** `ModelRegistryEntry`, `ModelArtifact`
- **Collaborators:** `ArtifactStoreAgent`, database persistence
### ArtifactStoreAgent
- **Responsibility:** Store and retrieve large artifacts via S3-compatible storage.
- **Input:** Binary data, storage key
- **Output:** Storage confirmation, presigned URLs or S3 references
- **Collaborators:** `FrameStorageAdapter`, S3 / MinIO
### LedgerWriterAgent
- **Responsibility:** Write immutable `AuditLedgerEntry` records for state changes.
- **Input:** Audit event payloads
- **Output:** `AuditLedgerEntry`
- **Collaborators:** PostgreSQL persistence
---
## Adapter Interfaces
Adapters encapsulate external system details and provide a uniform internal interface.
### Storage Adapters
```python
class FrameStorageAdapter(ABC):
@abstractmethod
def store_frame(self, frame_id: UUID, data: bytes, content_type: str) -> str:
"""Returns S3 storage key"""
pass
@abstractmethod
def generate_presigned_url(self, storage_key: str, expires_in: int) -> str:
pass
@abstractmethod
def delete_frame(self, storage_key: str) -> None:
pass
```
```python
class ArtifactStorageAdapter(ABC):
@abstractmethod
def store_artifact(self, artifact_id: UUID, data: bytes, content_type: str) -> str:
pass
@abstractmethod
def retrieve_artifact(self, storage_key: str) -> bytes:
pass
```
### ML Inference Adapters
```python
class InferenceAdapter(ABC):
@abstractmethod
def load_model(self, model_reference: str) -> None:
pass
@abstractmethod
def infer(self, input_data: ProcessedImage) -> dict:
"""Returns standardized prediction dict"""
pass
@abstractmethod
def unload_model(self, model_reference: str) -> None:
pass
```

View File

@@ -0,0 +1,501 @@
# Visualization — Sprint 1_2 Class and Architecture Diagrams
## Layer Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ API Layer (FastAPI) │
├─────────────────────────────────────────────────────────────┤
│ Orchestrators (Use Cases) │
├─────────────────────────────────────────────────────────────┤
│ Agents & Services (Workers) │
├─────────────────────────────────────────────────────────────┤
│ Domain Objects │
├─────────────────────────────────────────────────────────────┤
│ Adapters │
└─────────────────────────────────────────────────────────────┘
```
## Full Class Diagram
```plantuml
@startuml Sprint 1_2 OOP Class Diagram
skinparam classAttributeAlignment left
skinparam classFontSize 11
skinparam backgroundColor #FEFEFF
skinparam handwritten false
package "Domain Objects" {
class ClinicianUser {
-user_id: UUID
-username: str
-hashed_password: str
-name: str
-role: str
-credentials: dict | None
-specialization: str
-created_at: datetime
-last_login: datetime | None
--
+authenticate(password: str): bool
+owns_sessions(): List[DiagnosticSession]
+creates_review(decision: ReviewDecision): ReviewDecision
}
class PatientCase {
-case_id: UUID
-patient_identifier: str
-demographic_info: dict
-medical_history_summary: dict
-created_at: datetime
--
+add_session(session: DiagnosticSession): void
+list_sessions(): List[DiagnosticSession]
}
class DiagnosticSession {
-session_id: UUID
-case_id: UUID
-clinician_id: UUID
-status: str
-created_at: datetime
-updated_at: datetime
--
+add_frame(frame: ScanFrame): void
+add_job(job: AnalysisJob): void
+add_review(decision: ReviewDecision): void
+can_upload(): bool
+can_analyze(): bool
+can_review(): bool
}
class ScanFrame {
-frame_id: UUID
-session_id: UUID
-storage_reference: str
-original_format: str
-frame_number: int | None
-metadata: dict
-checksum: str
-created_at: datetime
--
+get_image_data(): bytes
+get_metadata(): dict
+has_preprocessed(): bool
+calculate_checksum(): str
}
class ImageAsset {
-asset_id: UUID
-frame_id: UUID
-storage_key: str
-content_type: str
-size_bytes: int
-checksum: str
-uploaded_at: datetime
--
+get_storage_key(): str
+verify_checksum(): bool
}
class Calibration {
-calibration_id: UUID
-session_id: UUID
-pixel_to_mm_ratio: float
-parameters: dict
-recorded_at: datetime
--
+scale_pixels_to_mm(pixels: float): float
+get_parameters(): dict
}
class AnalysisJob {
-job_id: UUID
-session_id: UUID
-parameters: dict
-model_versions: dict
-status: str
-result: dict | None
-created_at: datetime
-updated_at: datetime
--
+add_step(step: PipelineStep): void
+set_status(status: str): void
+set_result(result: dict): void
+is_running(): bool
+is_completed(): bool
+is_failed(): bool
+get_steps(): List[PipelineStep]
}
class PipelineStep {
-step_id: UUID
-job_id: UUID
-task_type: str
-status: str
-output: dict | None
-duration_ms: int | None
-started_at: datetime | None
-completed_at: datetime | None
--
+start(model: ModelReference): void
+complete(output: dict): void
+fail(error: str): void
+get_duration(): int | None
}
class ModelRegistryEntry {
-model_id: str
-name: str
-task_type: str
-version: str
-description: str
-framework: str
-labels: list[str]
-registered_at: datetime
-is_active: bool
--
+get_labels(): list[str]
+is_compatible_with(task: str): bool
+activate(): void
+deactivate(): void
}
class ModelArtifact {
-artifact_id: UUID
-model_id: str
-storage_key: str
-format: str
-size_bytes: int
-checksum: str
-uploaded_at: datetime
--
+get_model_file(): bytes
+verify_checksum(): bool
+get_format(): str
}
class PreprocessedImage {
-preprocessed_id: UUID
-frame_id: UUID
-preprocessing_steps: list[str]
-storage_reference: str
-width: int
-height: int
-created_at: datetime
--
+get_image_data(): bytes
+get_dimensions(): tuple[int, int]
+applied_steps(): list[str]
}
class AnglePrediction {
-prediction_id: UUID
-job_id: UUID
-step_id: UUID
-angle_class: str
-confidence: float
-metadata: dict
--
+get_class(): str
+get_confidence(): float
+is_confident(threshold: float): bool
}
class InflammationPrediction {
-prediction_id: UUID
-job_id: UUID
-step_id: UUID
-detected: bool
-confidence: float
--
+is_detected(): bool
+get_confidence(): float
}
class SegmentationMask {
-mask_id: UUID
-job_id: UUID
-step_id: UUID
-storage_reference: str
-overlay_reference: str
-color_legend: dict
-metadata: dict
--
+get_mask_data(): bytes
+get_overlay_reference(): str
+get_color_legend(): dict
+get_classes(): list[str]
}
class Measurement {
-measurement_id: UUID
-job_id: UUID
-step_id: UUID
-thickness_mm: float | None
-pixel_to_mm_ratio: float
-roi_specification: dict
-created_at: datetime
--
+get_thickness_mm(): float | None
+get_pixel_to_mm_ratio(): float
+calculate_area(pixels: int): float
+get_roi_specification(): dict
}
class SynovitisGrade {
-grade_id: UUID
-job_id: UUID
-step_id: UUID
-level: int
-label: str
-combined_score: float | None
-confidence: float | None
--
+get_level(): int
+get_label(): str
+get_combined_score(): float | None
+get_confidence(): float | None
+is_severe(): bool
}
class ReviewDecision {
-decision_id: UUID
-session_id: UUID
-job_id: UUID
-reviewer_id: UUID
-decision_type: str
-justification: str | None
-created_at: datetime
--
+is_approved(): bool
+is_corrected(): bool
+is_rejected(): bool
+get_justification(): str | None
}
class ArtifactReference {
-reference_id: UUID
-artifact_type: str
-associated_entity_id: UUID
-storage_key: str
-content_type: str
-created_at: datetime
--
+get_storage_key(): str
+get_content_type(): str
+get_entity_id(): UUID
}
class AuditLedgerEntry {
-entry_id: UUID
-entity_type: str
-entity_id: UUID
-action: str
-user_id: UUID | None
-checksum: str
-metadata: dict
-timestamp: datetime
--
+get_action(): str
+get_entity(): str
+verify_checksum(payload: dict): bool
+to_immutable(): AuditLedgerEntry
}
}
package "Agents / Services" {
class DICOMIngestAgent {
+ingest(source: UploadFile): ScanFrame
+validate_dicom(data: bytes): bool
+extract_metadata(data: bytes): dict
+extract_frames(data: bytes): List[bytes]
}
class ImageUploadIngestAgent {
+ingest(source: UploadFile): ScanFrame
+validate_image(data: bytes): bool
+extract_metadata(data: bytes): dict
}
class FramePreprocessor {
+preprocess(frame: ScanFrame): PreprocessedImage
+apply_clahe(image: bytes): bytes
+normalize(image: bytes): bytes
+resize(image: bytes, size: tuple[int, int]): bytes
}
class AngleValidatorAgent {
+validate(prediction: AnglePrediction): AnglePrediction
+adjust_confidence(prediction: AnglePrediction, adjustment: float): AnglePrediction
+check_clinical_rules(angle_class: str): bool
}
class ROICropperAgent {
+crop_for_inflammation(image: PreprocessedImage): PreprocessedImage
+crop_for_segmentation(image: PreprocessedImage, angle: str): PreprocessedImage
+extract_bounding_box(image: bytes): dict
}
class VisionPipelineAgent {
+run_pipeline(session: DiagnosticSession, frames: List[ScanFrame]): AnalysisJob
+coordinate_models(frames: List[ScanFrame], models: dict): dict
+should_apply_inflammation(angle: str): bool
+should_apply_segmentation(angle: str): bool
}
class InferenceRunner {
+infer(model: ModelReference, image: bytes): dict
+load_model(model_id: str, version: str): void
+unload_model(model_id: str): void
+get_model_status(model_id: str): str
}
class MeasurementAgent {
+measure(mask: SegmentationMask, calibration: Calibration): Measurement
+calculate_thickness(mask: bytes, ratio: float): float
+calculate_roi(mask: bytes): dict
+validate_measurement(measurement: Measurement): bool
}
class SeverityScorerAgent {
+score(measurement: Measurement, inflammation: InflammationPrediction): SynovitisGrade
+calculate_combined_score(thickness: float, detected: bool): float
+get_grade_label(score: float): str
+validate_grade(grade: SynovitisGrade): bool
}
class ModelRegistryAgent {
+register_model(entry: ModelRegistryEntry, artifact: ModelArtifact): ModelRegistryEntry
+get_model(task: str, version: str = "latest"): ModelReference
+list_models(): List[ModelRegistryEntry]
+activate_model(model_id: str): void
+deactivate_model(model_id: str): void
+verify_artifact(model_id: str, checksum: str): bool
}
class ArtifactStoreAgent {
+store_artifact(artifact_id: UUID, data: bytes, content_type: str): str
+retrieve_artifact(storage_key: str): bytes
+delete_artifact(storage_key: str): void
+generate_presigned_url(storage_key: str, expires_in: int = 3600): str
+verify_integrity(storage_key: str, checksum: str): bool
}
class LedgerWriterAgent {
+write(event_type: str, entity_type: str, entity_id: UUID, payload: dict, user_id: UUID | None = None): AuditLedgerEntry
+verify_integrity(entry: AuditLedgerEntry): bool
+query_by_entity(entity_type: str, entity_id: UUID): List[AuditLedgerEntry]
}
}
package "Adapters" {
class FrameStorageAdapter {
{abstract} +store_frame(frame_id: UUID, data: bytes, content_type: str) -> str
{abstract} +generate_presigned_url(storage_key: str, expires_in: int) -> str
{abstract} +delete_frame(storage_key: str) -> None
}
class ArtifactStorageAdapter {
{abstract} +store_artifact(artifact_id: UUID, data: bytes, content_type: str) -> str
{abstract} +retrieve_artifact(storage_key: str) -> bytes
}
class InferenceAdapter {
{abstract} +load_model(model_reference: str) -> None
{abstract} +infer(input_data: bytes) -> dict
{abstract} +unload_model(model_reference: str) -> None
}
class PyTorchAdapter {
+load_model(model_reference: str) -> None
+infer(input_data: bytes) -> dict
+unload_model(model_reference: str) -> None
}
class TritonAdapter {
+load_model(model_reference: str) -> None
+infer(input_data: bytes) -> dict
+unload_model(model_reference: str) -> None
}
class MockAdapter {
+load_model(model_reference: str) -> None
+infer(input_data: bytes) -> dict
+unload_model(model_reference: str) -> None
}
}
' Relationships: Domain Objects
PatientCase "1" --> "*" DiagnosticSession : has
DiagnosticSession "1" --> "*" ScanFrame : contains
DiagnosticSession "1" --> "*" AnalysisJob : initiated
DiagnosticSession "1" --> "*" ReviewDecision : reviewed_by
DiagnosticSession "1" --> "*" Calibration : has
DiagnosticSession "*" --> "1" ClinicianUser : conducted_by
ScanFrame "1" --> "1" ImageAsset : stored_as
ScanFrame "1" --> "1" PreprocessedImage : becomes
AnalysisJob "1" --> "*" PipelineStep : consists_of
AnalysisJob "1" --> "*" AnglePrediction : produces
AnalysisJob "1" --> "*" InflammationPrediction : produces
AnalysisJob "1" --> "*" SegmentationMask : produces
AnalysisJob "1" --> "*" Measurement : produces
AnalysisJob "1" --> "*" SynovitisGrade : produces
ModelRegistryEntry "1" --> "*" ModelArtifact : has
PipelineStep "*" --> "1" ModelRegistryEntry : uses
ArtifactReference "1" --> "1" ScanFrame : references
' Relationships: Agents depend on Adapters
DICOMIngestAgent --> FrameStorageAdapter : uses
ImageUploadIngestAgent --> FrameStorageAdapter : uses
ArtifactStoreAgent --> FrameStorageAdapter : uses
ArtifactStoreAgent --> ArtifactStorageAdapter : uses
InferenceRunner --> InferenceAdapter : uses
ModelRegistryAgent --> ArtifactStorageAdapter : uses
' Relationships: Agents operate on Domain Objects
DICOMIngestAgent --> ScanFrame : creates
DICOMIngestAgent --> ImageAsset : creates
ImageUploadIngestAgent --> ScanFrame : creates
ImageUploadIngestAgent --> ImageAsset : creates
FramePreprocessor --> ScanFrame : reads
FramePreprocessor --> PreprocessedImage : creates
AngleValidatorAgent --> AnglePrediction : validates
ROICropperAgent --> PreprocessedImage : modifies
VisionPipelineAgent --> AnalysisJob : orchestrates
InferenceRunner --> AnalysisJob : populates
MeasurementAgent --> SegmentationMask : reads
MeasurementAgent --> Calibration : uses
MeasurementAgent --> Measurement : creates
SeverityScorerAgent --> InflammationPrediction : reads
SeverityScorerAgent --> Measurement : reads
SeverityScorerAgent --> SynovitisGrade : creates
ModelRegistryAgent --> ModelRegistryEntry : manages
ModelRegistryAgent --> ModelArtifact : manages
LedgerWriterAgent --> AuditLedgerEntry : creates
' Relationships: Adapters
PyTorchAdapter ..|> InferenceAdapter
TritonAdapter ..|> InferenceAdapter
MockAdapter ..|> InferenceAdapter
@enduml
```
The diagram above shows:
- **19 Domain Objects** with their attributes, methods, and relationships
- **12 Agents/Services** with their interface methods and collaborators
- **3 Adapter hierarchies** (Storage and Inference) showing abstraction relationships
- **Dependency arrows** showing what objects depend on what adapters or other objects
Key relationships:
- `PatientCase` 1→* `DiagnosticSession` (case has many sessions)
- `DiagnosticSession` 1→* `ScanFrame` (session has many frames)
- `AnalysisJob` 1→* `PipelineStep` (job has many steps)
- All prediction/measurement objects belong to exactly one `AnalysisJob`
- Agents depend on adapters (e.g., `DICOMIngestAgent` uses `FrameStorageAdapter`)
Legend:
- `-->` : Association (uses or references)
- `..|>` : Realization (implements interface)
- Package groupings: Domain Objects, Agents/Services, Adapters

View File

@@ -0,0 +1,89 @@
from .auth_schemas import Token, TokenPayload, LoginRequest, UserProfile, UserUpdateRequest, RefreshRequest
from .patient_schemas import Patient, PatientCreate, PatientListResponse, DemographicInfo
from .session_schemas import (
Session, SessionCreate, SessionDetail, SessionPatchReview,
FrameMetadata, PersistResult, ExportResult, ScrubResult,
)
from .analysis_schemas import (
AnalysisJobSubmit, AnalysisJobSyncSubmit, JobStatus, PipelineStep,
StepEvent, JobResult, ModelRegistryEntry, ModelCatalog,
ModelRegistrationResult,
)
from .telemetry_schemas import CorrectionSubmit, CorrectionRecord, AnomalyReport, AnomalyRecord
from .report_schemas import ReportCreate, ReportSignRequest, ReportSyncEMRRequest, SyncResult
from .safety_schemas import (
GradCAMRequest, HeatmapResult, RationaleRequest, RationaleResult,
CircuitBreakerRequest, ChatStreamRequest, ChatEvent, ChatResponse,
DriftCheckResult, RAGEvidenceRequest, EvidenceList, ActivationMeta,
AnnotationArtifact, GroundTruthLabel, EscalationRequest, EscalationTicket,
MorphologyAnnotation, GuardrailCheckRequest, GuardrailResult,
)
from .notification_schemas import NotificationItem, NotificationPreferences
from .settings_schemas import UserSettings, SettingsUpdate
from .ingestion_schemas import IngestionRecord, RecordDetail
from .common_schemas import HealthStatus, ErrorResponse
__all__ = [
"Token",
"TokenPayload",
"LoginRequest",
"UserProfile",
"UserUpdateRequest",
"RefreshRequest",
"Patient",
"PatientCreate",
"PatientListResponse",
"DemographicInfo",
"Session",
"SessionCreate",
"SessionDetail",
"SessionPatchReview",
"FrameMetadata",
"PersistResult",
"ExportResult",
"ScrubResult",
"AnalysisJobSubmit",
"AnalysisJobSyncSubmit",
"JobStatus",
"PipelineStep",
"StepEvent",
"JobResult",
"ModelRegistryEntry",
"ModelCatalog",
"ModelRegistrationResult",
"CorrectionSubmit",
"CorrectionRecord",
"AnomalyReport",
"AnomalyRecord",
"ReportCreate",
"ReportSignRequest",
"ReportSyncEMRRequest",
"SyncResult",
"GradCAMRequest",
"HeatmapResult",
"RationaleRequest",
"RationaleResult",
"CircuitBreakerRequest",
"ChatStreamRequest",
"ChatEvent",
"ChatResponse",
"DriftCheckResult",
"RAGEvidenceRequest",
"EvidenceList",
"ActivationMeta",
"AnnotationArtifact",
"GroundTruthLabel",
"EscalationRequest",
"EscalationTicket",
"MorphologyAnnotation",
"GuardrailCheckRequest",
"GuardrailResult",
"NotificationItem",
"NotificationPreferences",
"UserSettings",
"SettingsUpdate",
"IngestionRecord",
"RecordDetail",
"HealthStatus",
"ErrorResponse",
]

View File

@@ -0,0 +1,78 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Any
class AnalysisJobSubmit(BaseModel):
session_id: str
params: dict[str, Any] | None = None
model_versions: dict[str, str] | None = None
class AnalysisJobSyncSubmit(BaseModel):
session_id: str
params: dict[str, Any] | None = None
model_versions: dict[str, str] | None = None
class PipelineStep(BaseModel):
step_id: str
job_id: str
task_type: str
status: str
output: dict | None = None
duration_ms: int | None = None
started_at: datetime | None = None
completed_at: datetime | None = None
class JobStatus(BaseModel):
job_id: str
session_id: str
status: str
result: dict | None = None
steps: list[PipelineStep] | None = None
created_at: datetime
updated_at: datetime
class StepEvent(BaseModel):
step_id: str
job_id: str
event_type: str
task_type: str
status: str
data: dict | None = None
timestamp: datetime
class JobResult(BaseModel):
job_id: str
session_id: str
status: str
result: dict | None = None
duration_ms: int | None = None
class ModelRegistryEntry(BaseModel):
model_id: str
name: str
task_type: str
version: str
description: str
framework: str
labels: list[str]
registered_at: datetime
is_active: bool
class ModelCatalog(BaseModel):
models: list[ModelRegistryEntry]
total: int
class ModelRegistrationResult(BaseModel):
model_id: str
status: str
s3_key: str
registered_at: datetime

View File

@@ -0,0 +1,37 @@
from pydantic import BaseModel, EmailStr, Field
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
class TokenPayload(BaseModel):
sub: str
exp: int
role: str = "clinician"
class LoginRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
password: str = Field(..., min_length=6)
class UserProfile(BaseModel):
user_id: str
username: str
name: str
role: str
credentials: dict | None = None
specialization: str | None = None
class UserUpdateRequest(BaseModel):
name: str | None = None
specialization: str | None = None
credentials: dict | None = None
class RefreshRequest(BaseModel):
refresh_token: str

View File

@@ -0,0 +1,14 @@
from pydantic import BaseModel
from typing import Any
class HealthStatus(BaseModel):
status: str
version: str
dependencies: dict[str, str]
uptime_seconds: float
class ErrorResponse(BaseModel):
detail: str
code: str | None = None

View File

@@ -0,0 +1,22 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Any
class IngestionRecord(BaseModel):
record_id: str
user_id: str
patient_id: str
session_id: str | None = None
filename: str
file_type: str
size_bytes: int
status: str
created_at: datetime
metadata: dict | None = None
class RecordDetail(IngestionRecord):
s3_key: str
checksum: str
frame_count: int | None = None

View File

@@ -0,0 +1,22 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Any
class NotificationPreferences(BaseModel):
user_id: str
email_enabled: bool = True
push_enabled: bool = True
in_app_enabled: bool = True
categories: dict[str, bool] | None = None
class NotificationItem(BaseModel):
notification_id: str
user_id: str
title: str
message: str
category: str
is_read: bool = False
created_at: datetime
metadata: dict | None = None

View File

@@ -0,0 +1,27 @@
from pydantic import BaseModel, Field
from datetime import datetime
class DemographicInfo(BaseModel):
age: int | None = None
sex: str | None = None
class PatientCreate(BaseModel):
patient_identifier: str = Field(..., max_length=100)
demographic_info: dict | None = None
medical_history_summary: dict | None = None
class Patient(BaseModel):
case_id: str
patient_identifier: str
demographic_info: dict | None = None
medical_history_summary: dict | None = None
created_by: str
created_at: datetime
class PatientListResponse(BaseModel):
items: list[Patient]
total: int

View File

@@ -0,0 +1,24 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Any
class ReportCreate(BaseModel):
session_id: str
payload: dict[str, Any]
class ReportSignRequest(BaseModel):
report_id: str
signature: dict[str, Any]
class ReportSyncEMRRequest(BaseModel):
report_id: str
class SyncResult(BaseModel):
report_id: str
emr_status: str
emr_reference: str | None = None
synced_at: datetime | None = None

View File

@@ -0,0 +1,120 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Any
class GradCAMRequest(BaseModel):
session_id: str
layer_name: str | None = None
class HeatmapResult(BaseModel):
session_id: str
heatmap_url: str
overlay_url: str | None = None
metadata: dict | None = None
class RationaleRequest(BaseModel):
session_id: str
prompt: str | None = None
class RationaleResult(BaseModel):
session_id: str
text: str
confidence: float | None = None
class CircuitBreakerRequest(BaseModel):
session_id: str
flag: bool
class ChatStreamRequest(BaseModel):
session_id: str
prompt: str
context: dict | None = None
class ChatEvent(BaseModel):
session_id: str
event_type: str
content: str
is_final: bool = False
metadata: dict | None = None
class ChatResponse(BaseModel):
session_id: str
response: str
metadata: dict | None = None
class DriftCheckResult(BaseModel):
session_id: str
drift_detected: bool
drift_score: float
threshold: float
details: dict | None = None
class RAGEvidenceRequest(BaseModel):
session_id: str
query: str | None = None
class EvidenceList(BaseModel):
session_id: str
items: list[dict[str, Any]]
class ActivationMeta(BaseModel):
session_id: str
activations: dict[str, Any]
layer_info: dict | None = None
class AnnotationArtifact(BaseModel):
artifact_id: str
session_id: str
storage_key: str
content_type: str
size_bytes: int
annotation_type: str
class GroundTruthLabel(BaseModel):
session_id: str
label: dict[str, Any]
class EscalationRequest(BaseModel):
session_id: str
reason: str
class EscalationTicket(BaseModel):
ticket_id: str
session_id: str
reason: str
status: str
created_at: datetime
class MorphologyAnnotation(BaseModel):
session_id: str
annotation: dict[str, Any]
class GuardrailCheckRequest(BaseModel):
session_id: str
prompt: str
score: float
class GuardrailResult(BaseModel):
session_id: str
passed: bool
flags: list[str]
recommendations: list[str] | None = None

View File

@@ -0,0 +1,59 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Any
class SessionCreate(BaseModel):
case_id: str
patient_id: str
class Session(BaseModel):
session_id: str
case_id: str
clinician_id: str
patient_id: str | None = None
status: str = "created"
calibration: dict | None = None
created_at: datetime
updated_at: datetime
class SessionDetail(Session):
frame_count: int = 0
job_count: int = 0
class SessionPatchReview(BaseModel):
status: str = Field(..., pattern="^(reviewed|completed|flagged)$")
notes: str | None = None
decisions: list[dict[str, Any]] | None = None
class FrameMetadata(BaseModel):
frame_id: str
session_id: str
storage_reference: str
original_format: str
frame_number: int | None = None
metadata: dict | None = None
checksum: str
created_at: datetime
class PersistResult(BaseModel):
session_id: str
status: str
updated_at: datetime
class ExportResult(BaseModel):
session_id: str
export_url: str
expires_at: datetime
class ScrubResult(BaseModel):
session_id: str
scrubbed_fields: list[str]
phi_removed: bool

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
from typing import Any
class SettingsUpdate(BaseModel):
updates: dict[str, Any]
class UserSettings(BaseModel):
user_id: str
settings: dict[str, Any]
updated_at: str | None = None

View File

@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Any, Literal
class CorrectionSubmit(BaseModel):
session_id: str
quadrant: Literal["Q2", "Q3", "Q4"]
event_type: str = Field(..., pattern="^(grade_override|mask_correction|ground_truth_commit|socratic_response|manual_morphology_annotation|drift_impass|artifact_isolation)$")
clinician_correction: dict[str, Any] | None = None
behavioral_telemetry: dict[str, Any] | None = None
raw_artifacts: dict[str, Any] | None = None
trust_level: Literal["high", "medium", "low"] | None = None
class CorrectionRecord(BaseModel):
correction_id: str
session_id: str
quadrant: str
event_type: str
status: str
audit_trail_id: str
queued_for_retraining: bool
submitted_at: datetime
class AnomalyReport(BaseModel):
session_id: str
data: dict[str, Any]
class AnomalyRecord(BaseModel):
anomaly_id: str
session_id: str
anomaly_type: str
severity: str
description: str
metadata: dict | None = None
reported_at: datetime