///|
/// Document families determine the default profile and review posture.
pub(all) enum DocumentKind {
ClinicalNote
DischargeSummary
LaboratoryReport
ImagingReport
ReferralLetter
BillingRecord
Message
StructuredResource
Unknown
} derive(Debug, Eq)
///|
pub(all) enum RouteAction {
StrictRedaction
StandardRedaction
ReviewBeforeRelease
Reject
Retain
} derive(Debug, Eq)
///|
pub(all) struct RouteDecision {
kind : DocumentKind
action : RouteAction
profile : Profile
confidence : Int
reasons : Array[String]
} derive(Debug, Eq)
///|
pub(all) struct DocumentRoute {
document_id : String
decision : RouteDecision
input : String
output : String
findings : Array[Finding]
diagnostics : Array[Diagnostic]
} derive(Debug)
///|
pub(all) struct RouterPolicy {
min_confidence : Int
reject_empty : Bool
strict_for_billing : Bool
review_for_unknown : Bool
config : RedactionConfig
} derive(Debug, Eq)
///|
pub(all) struct DocumentRouter {
policy : RouterPolicy
mut processed : Int
mut rejected : Int
mut reviewed : Int
} derive(Debug)
///|
pub fn document_kind_name(kind : DocumentKind) -> String {
match kind {
ClinicalNote => "clinical_note"
DischargeSummary => "discharge_summary"
LaboratoryReport => "laboratory_report"
ImagingReport => "imaging_report"
ReferralLetter => "referral_letter"
BillingRecord => "billing_record"
Message => "message"
StructuredResource => "structured_resource"
Unknown => "unknown"
}
}
///|
pub fn document_profile_name(profile : Profile) -> String {
match profile {
ChineseClinical => "chinese_clinical"
EnglishClinical => "english_clinical"
HighPrecision => "high_precision"
AllBuiltins => "all_builtins"
}
}
///|
pub fn route_action_name(action : RouteAction) -> String {
match action {
StrictRedaction => "strict_redaction"
StandardRedaction => "standard_redaction"
ReviewBeforeRelease => "review_before_release"
Reject => "reject"
Retain => "retain"
}
}
///|
pub fn RouterPolicy::default() -> RouterPolicy {
{
min_confidence: 55,
reject_empty: true,
strict_for_billing: true,
review_for_unknown: true,
config: RedactionConfig::default(),
}
}
///|
pub fn RouterPolicy::strict() -> RouterPolicy {
{
..RouterPolicy::default(),
min_confidence: 70,
config: { ..RedactionConfig::default(), policy: RedactionPolicy::strict() },
}
}
///|
pub fn DocumentRouter::new(policy : RouterPolicy) -> DocumentRouter {
{ policy, processed: 0, rejected: 0, reviewed: 0 }
}
///|
pub fn DocumentRouter::default() -> DocumentRouter {
DocumentRouter::new(RouterPolicy::default())
}
///|
pub fn DocumentRouter::reset(self : DocumentRouter) -> Unit {
self.processed = 0
self.rejected = 0
self.reviewed = 0
}
///|
pub fn DocumentRouter::processed_count(self : DocumentRouter) -> Int {
self.processed
}
///|
pub fn DocumentRouter::rejected_count(self : DocumentRouter) -> Int {
self.rejected
}
///|
pub fn DocumentRouter::reviewed_count(self : DocumentRouter) -> Int {
self.reviewed
}
///|
fn keyword_score(text : String, keywords : Array[String]) -> Int {
let mut score = 0
for keyword in keywords {
if text.to_lower().contains(keyword.to_lower()) {
score += 20
}
}
if score > 100 {
100
} else {
score
}
}
///|
pub fn classify_document(text : String) -> (DocumentKind, Int, Array[String]) {
let candidates : Array[(DocumentKind, Array[String])] = [
(DischargeSummary, ["出院小结", "出院记录", "discharge summary"]),
(LaboratoryReport, ["检验报告", "化验", "laboratory", "lab result"]),
(ImagingReport, ["影像报告", "放射", "imaging", "radiology"]),
(ReferralLetter, ["转诊", "会诊", "referral", "consultation"]),
(BillingRecord, ["费用", "账单", "billing", "invoice", "insurance"]),
(Message, ["短信", "聊天", "message", "chat", "联系"]),
(StructuredResource, ["resource_type=", "fhir", "hl7", "json"]),
(
ClinicalNote,
["病史", "主诉", "现病史", "clinical note", "assessment"],
),
]
let mut best = DocumentKind::Unknown
let mut best_score = 0
let mut reasons : Array[String] = []
for candidate in candidates {
let score = keyword_score(text, candidate.1)
if score > best_score {
best = candidate.0
best_score = score
reasons = candidate.1
}
}
(best, best_score, reasons)
}
///|
pub fn document_kind_profile(kind : DocumentKind) -> Profile {
match kind {
ClinicalNote | DischargeSummary | ReferralLetter => AllBuiltins
LaboratoryReport | ImagingReport => HighPrecision
BillingRecord => HighPrecision
Message => EnglishClinical
StructuredResource => AllBuiltins
Unknown => AllBuiltins
}
}
///|
pub fn document_kind_action(
kind : DocumentKind,
confidence : Int,
policy : RouterPolicy,
) -> RouteAction {
if confidence < policy.min_confidence && policy.review_for_unknown {
ReviewBeforeRelease
} else {
match kind {
BillingRecord if policy.strict_for_billing => StrictRedaction
Unknown if policy.review_for_unknown => ReviewBeforeRelease
LaboratoryReport | ImagingReport => StrictRedaction
_ => StandardRedaction
}
}
}
///|
pub fn route_text(text : String, policy : RouterPolicy) -> RouteDecision {
let (kind, confidence, reasons) = classify_document(text)
{
kind,
action: document_kind_action(kind, confidence, policy),
profile: document_kind_profile(kind),
confidence,
reasons,
}
}
///|
pub fn route_decision_summary(decision : RouteDecision) -> String {
let reasons = decision.reasons.join(",")
[
"kind=\{document_kind_name(decision.kind)}",
"action=\{route_action_name(decision.action)}",
"profile=\{document_profile_name(decision.profile)}",
"confidence=\{decision.confidence}",
"reasons=\{reasons}",
].join("\n")
}
///|
pub fn route_decision_json(decision : RouteDecision) -> String {
let reasons = decision.reasons.map(json_escape).join(",")
"{" +
"\"kind\":\{json_escape(document_kind_name(decision.kind))}," +
"\"action\":\{json_escape(route_action_name(decision.action))}," +
"\"profile\":\{json_escape(document_profile_name(decision.profile))}," +
"\"confidence\":\{decision.confidence}," +
"\"reasons\":[" +
reasons +
"]}"
}
///|
pub fn DocumentRouter::route(
self : DocumentRouter,
document_id : String,
input : String,
) -> DocumentRoute raise DeidError {
self.processed += 1
if input.trim().is_empty() && self.policy.reject_empty {
self.rejected += 1
let decision = {
kind: Unknown,
action: Reject,
profile: AllBuiltins,
confidence: 100,
reasons: ["empty input"],
}
{ document_id, decision, input, output: "", findings: [], diagnostics: [] }
} else {
let decision = route_text(input, self.policy)
match decision.action {
Reject => {
self.rejected += 1
{
document_id,
decision,
input,
output: "",
findings: [],
diagnostics: [],
}
}
ReviewBeforeRelease => {
self.reviewed += 1
let pipeline = pipeline_for_profile(
input,
decision.profile,
self.policy.config.policy.mode,
)
{
document_id,
decision,
input,
output: pipeline.text(),
findings: pipeline.findings,
diagnostics: pipeline.diagnostics,
}
}
StrictRedaction | StandardRedaction => {
let mode = if decision.action == StrictRedaction {
PreserveLength
} else {
self.policy.config.policy.mode
}
let pipeline = pipeline_for_profile(input, decision.profile, mode)
{
document_id,
decision,
input,
output: pipeline.text(),
findings: pipeline.findings,
diagnostics: pipeline.diagnostics,
}
}
Retain =>
{
document_id,
decision,
input,
output: input,
findings: [],
diagnostics: [],
}
}
}
}
///|
pub fn DocumentRouter::route_batch(
self : DocumentRouter,
items : Array[BatchItem],
) -> Array[DocumentRoute] raise DeidError {
let routes = []
for item in items {
routes.push(self.route(item.id, item.text))
}
routes
}
///|
pub fn DocumentRouter::route_sections(
self : DocumentRouter,
document_id : String,
input : String,
) -> Array[DocumentRoute] raise DeidError {
let routes = []
let sections = sectionize(input)
if sections.is_empty() {
routes.push(self.route(document_id, input))
} else {
for section in sections {
routes.push(
self.route(
document_id + ":" + section_kind_name(section.kind),
section.text,
),
)
}
}
routes
}
///|
pub fn document_route_is_release_ready(route : DocumentRoute) -> Bool {
route.decision.action != Reject &&
!route.diagnostics.any(fn(item) { item.severity == Error }) &&
output_contract_passes(route.input, {
text: route.output,
findings: route.findings,
offsets: [],
audit: {
input_length: route.input.length(),
output_length: route.output.length(),
finding_count: route.findings.length(),
applied_count: route.findings.length(),
counts: Map([]),
findings: route.findings,
offsets: [],
},
})
}
///|
pub fn document_route_summary(route : DocumentRoute) -> String {
[
"document_id=\{route.document_id}",
route_decision_summary(route.decision),
"input_length=\{route.input.length()}",
"output_length=\{route.output.length()}",
"findings=\{route.findings.length()}",
"diagnostics=\{route.diagnostics.length()}",
"ready=\{document_route_is_release_ready(route)}",
].join("\n")
}
///|
pub fn document_route_json(route : DocumentRoute) -> String {
"{" +
"\"document_id\":\{json_escape(route.document_id)}," +
"\"decision\":\{route_decision_json(route.decision)}," +
"\"input_length\":\{route.input.length()}," +
"\"output_length\":\{route.output.length()}," +
"\"finding_count\":\{route.findings.length()}," +
"\"diagnostic_count\":\{route.diagnostics.length()}," +
"\"ready\":\{document_route_is_release_ready(route)}" +
"}"
}
///|
pub fn document_route_kind_counts(
routes : Array[DocumentRoute],
) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for route in routes {
let key = document_kind_name(route.decision.kind)
counts[key] = counts.get_or_default(key, 0) + 1
}
counts
}
///|
pub fn document_route_action_counts(
routes : Array[DocumentRoute],
) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for route in routes {
let key = route_action_name(route.decision.action)
counts[key] = counts.get_or_default(key, 0) + 1
}
counts
}
///|
pub fn document_route_findings(routes : Array[DocumentRoute]) -> Array[Finding] {
let findings = []
for route in routes {
findings.append(route.findings)
}
findings
}
///|
pub fn document_route_rejected(routes : Array[DocumentRoute]) -> Array[String] {
routes
.filter(fn(route) { route.decision.action == Reject })
.map(fn(route) { route.document_id })
}
///|
pub fn document_route_reviewed(routes : Array[DocumentRoute]) -> Array[String] {
routes
.filter(fn(route) { route.decision.action == ReviewBeforeRelease })
.map(fn(route) { route.document_id })
}
///|
pub fn document_route_ready_count(routes : Array[DocumentRoute]) -> Int {
routes.filter(document_route_is_release_ready).length()
}
///|
pub fn document_route_checksum(routes : Array[DocumentRoute]) -> String {
stable_hash(
routes.map(fn(route) { route.document_id + ":" + route.output }).join("\n"),
)
}
///|
pub fn DocumentRouter::summary(self : DocumentRouter) -> String {
[
"processed=\{self.processed}",
"rejected=\{self.rejected}",
"reviewed=\{self.reviewed}",
"policy_min_confidence=\{self.policy.min_confidence}",
].join("\n")
}
///|
pub fn document_kind_keywords(kind : DocumentKind) -> Array[String] {
match kind {
ClinicalNote => ["病史", "主诉", "现病史", "assessment", "note"]
DischargeSummary => ["出院", "discharge", "summary"]
LaboratoryReport => ["检验", "化验", "laboratory", "lab"]
ImagingReport => ["影像", "放射", "imaging", "radiology"]
ReferralLetter => ["转诊", "会诊", "referral"]
BillingRecord => ["费用", "账单", "billing", "invoice"]
Message => ["短信", "聊天", "message", "chat"]
StructuredResource => ["resource_type", "fhir", "hl7"]
Unknown => []
}
}
///|
pub fn document_kind_contains(text : String, kind : DocumentKind) -> Bool {
keyword_score(text, document_kind_keywords(kind)) > 0
}
///|
pub fn document_kind_candidates(text : String) -> Array[DocumentKind] {
[
ClinicalNote,
DischargeSummary,
LaboratoryReport,
ImagingReport,
ReferralLetter,
BillingRecord,
Message,
StructuredResource,
].filter(fn(kind) { document_kind_contains(text, kind) })
}
///|
pub fn document_route_reasons(route : DocumentRoute) -> Array[String] {
route.decision.reasons
}