@nexalink/carecompass-sdk 0.4.0-beta.15 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +285 -3
- package/dist/cli.js +0 -0
- package/dist/client.d.ts +32 -2
- package/dist/client.js +45 -3
- package/dist/compliance.d.ts +69 -0
- package/dist/compliance.js +121 -0
- package/dist/component.d.ts +24 -2
- package/dist/component.js +63 -4
- package/dist/core.d.ts +19 -2
- package/dist/core.js +140 -8
- package/dist/index.d.ts +11 -5
- package/dist/index.js +5 -2
- package/dist/markdown.d.ts +2 -1
- package/dist/mount.d.ts +4 -1
- package/dist/mount.js +2 -2
- package/dist/rcm.d.ts +95 -0
- package/dist/rcm.js +288 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +1 -0
- package/dist/schema.d.ts +49 -0
- package/dist/schema.js +36 -0
- package/dist/telemetry.d.ts +3 -2
- package/dist/telemetry.js +127 -114
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/dist/web-component.d.ts +13 -0
- package/dist/web-component.js +236 -0
- package/package.json +30 -14
- package/qa-fixtures/DEMO-ENC-1042-rcm-policy.md +27 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const CARECOMPASS_COMPLIANCE_PRESETS = {
|
|
2
|
+
// Standard is intentionally non-mutating for existing integrations. Detection
|
|
3
|
+
// can still drive a safety callback when the host opts into one.
|
|
4
|
+
standard: {
|
|
5
|
+
auditEnabled: false,
|
|
6
|
+
phiAction: "allow",
|
|
7
|
+
replacement: "[REDACTED]",
|
|
8
|
+
},
|
|
9
|
+
// Healthcare protects browser-visible requests and retains metadata-only audit
|
|
10
|
+
// records. It is the recommended starting point for PHI-oriented deployments.
|
|
11
|
+
healthcare: {
|
|
12
|
+
auditEnabled: true,
|
|
13
|
+
phiAction: "redact",
|
|
14
|
+
replacement: "[REDACTED]",
|
|
15
|
+
},
|
|
16
|
+
// Strict is appropriate when the host must stop detectable PHI before it leaves
|
|
17
|
+
// the browser. It may reject legitimate identifier-bearing workflow requests.
|
|
18
|
+
strict: {
|
|
19
|
+
auditEnabled: true,
|
|
20
|
+
phiAction: "block",
|
|
21
|
+
replacement: "[REDACTED]",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const PHI_PATTERNS = [
|
|
25
|
+
{ category: "email", pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
26
|
+
{ category: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
27
|
+
{
|
|
28
|
+
category: "phone",
|
|
29
|
+
pattern: /(?:\+1[ .-]?)?(?:\(?\d{3}\)?[ .-]?)\d{3}[ .-]\d{4}\b/g,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
category: "date_of_birth",
|
|
33
|
+
pattern: /\b(?:dob|date of birth|birth date)\s*[:#-]?\s*(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})\b/gi,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
category: "medical_record_number",
|
|
37
|
+
pattern: /\b(?:mrn|medical record(?: number)?)\s*[:#-]?\s*[A-Z0-9-]{3,}\b/gi,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
category: "member_id",
|
|
41
|
+
pattern: /\b(?:member|patient|policy|subscriber)(?:\s+(?:id|number))?\s*[:#-]?\s*[A-Z0-9-]{3,}\b/gi,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const PHI_CATEGORIES = new Set([
|
|
45
|
+
"date_of_birth",
|
|
46
|
+
"email",
|
|
47
|
+
"medical_record_number",
|
|
48
|
+
"member_id",
|
|
49
|
+
"phone",
|
|
50
|
+
"ssn",
|
|
51
|
+
]);
|
|
52
|
+
const validMatches = (value, matches) => matches
|
|
53
|
+
.filter((match) => PHI_CATEGORIES.has(match.category) &&
|
|
54
|
+
Number.isInteger(match.start) &&
|
|
55
|
+
Number.isInteger(match.end) &&
|
|
56
|
+
match.start >= 0 &&
|
|
57
|
+
match.end > match.start &&
|
|
58
|
+
match.end <= value.length)
|
|
59
|
+
.sort((a, b) => a.start - b.start || b.end - a.end)
|
|
60
|
+
.filter((match, index, sorted) => index === 0 || match.start >= sorted[index - 1].end);
|
|
61
|
+
export const detectCareCompassPhi = (value) => {
|
|
62
|
+
const matches = [];
|
|
63
|
+
for (const { category, pattern } of PHI_PATTERNS) {
|
|
64
|
+
pattern.lastIndex = 0;
|
|
65
|
+
for (const match of value.matchAll(pattern)) {
|
|
66
|
+
const start = match.index;
|
|
67
|
+
if (start === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
matches.push({ category, start, end: start + match[0].length });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return validMatches(value, matches);
|
|
73
|
+
};
|
|
74
|
+
export const redactCareCompassPhi = (value, matches, replacement) => {
|
|
75
|
+
let redacted = value;
|
|
76
|
+
for (const match of [...matches].sort((a, b) => b.start - a.start)) {
|
|
77
|
+
redacted = `${redacted.slice(0, match.start)}${replacement}${redacted.slice(match.end)}`;
|
|
78
|
+
}
|
|
79
|
+
return redacted;
|
|
80
|
+
};
|
|
81
|
+
export const resolveCareCompassCompliancePolicy = (config) => {
|
|
82
|
+
const preset = config?.preset ?? "standard";
|
|
83
|
+
const defaults = CARECOMPASS_COMPLIANCE_PRESETS[preset];
|
|
84
|
+
return {
|
|
85
|
+
preset,
|
|
86
|
+
auditEnabled: config?.audit?.enabled ?? defaults.auditEnabled,
|
|
87
|
+
phiAction: config?.phi?.action ?? defaults.phiAction,
|
|
88
|
+
replacement: config?.phi?.replacement ?? defaults.replacement,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export const phiSummary = (matches) => ({
|
|
92
|
+
categories: [...new Set(matches.map((match) => match.category))].sort(),
|
|
93
|
+
count: matches.length,
|
|
94
|
+
});
|
|
95
|
+
export const sanitizeCareCompassValue = (value, source, policy, hooks) => {
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
const matches = validMatches(value, (hooks?.detect ?? detectCareCompassPhi)(value, { source }));
|
|
98
|
+
if (policy.phiAction !== "redact" || matches.length === 0) {
|
|
99
|
+
return { matches, value };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
matches,
|
|
103
|
+
value: (hooks?.redact ?? redactCareCompassPhi)(value, matches, policy.replacement),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
const entries = value.map((entry) => sanitizeCareCompassValue(entry, source, policy, hooks));
|
|
108
|
+
return {
|
|
109
|
+
matches: entries.flatMap((entry) => entry.matches),
|
|
110
|
+
value: entries.map((entry) => entry.value),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (value && typeof value === "object") {
|
|
114
|
+
const entries = Object.entries(value).map(([key, entry]) => [key, sanitizeCareCompassValue(entry, source, policy, hooks)]);
|
|
115
|
+
return {
|
|
116
|
+
matches: entries.flatMap(([, entry]) => entry.matches),
|
|
117
|
+
value: Object.fromEntries(entries.map(([key, entry]) => [key, entry.value])),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { matches: [], value };
|
|
121
|
+
};
|
package/dist/component.d.ts
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { type CareCompassContext, type CareCompassCoreEvent, type CareCompassMessage } from "./core.js";
|
|
2
|
+
import { type CareCompassContext, type CareCompassCore, type CareCompassCoreEvent, type CareCompassCoreOptions, type CareCompassCoreSnapshot, type CareCompassMessage, type CareCompassSendMessageOptions } from "./core.js";
|
|
3
|
+
import type { CareCompassRuntimeTokenProvider } from "./client.js";
|
|
3
4
|
import type { CareCompassConfig } from "./schema.js";
|
|
5
|
+
import type { CareCompassAuditListener, CareCompassSafetyListener } from "./compliance.js";
|
|
4
6
|
export type CareCompassChatProps = {
|
|
5
7
|
config: CareCompassConfig;
|
|
6
8
|
initialContext?: CareCompassContext;
|
|
9
|
+
onAuditEvent?: CareCompassAuditListener;
|
|
7
10
|
onEvent?: (event: CareCompassCoreEvent) => void;
|
|
11
|
+
onSafetyEvent?: CareCompassSafetyListener;
|
|
8
12
|
pollIntervalMs?: number;
|
|
9
13
|
renderMessage?: (message: CareCompassMessage) => React.ReactNode;
|
|
10
14
|
retryLabel?: string;
|
|
15
|
+
runtimeTokenProvider?: CareCompassRuntimeTokenProvider;
|
|
11
16
|
};
|
|
12
|
-
|
|
17
|
+
/** Options for the headless React hook. Keep object-valued options stable with
|
|
18
|
+
* `useMemo` when the calling component rerenders. */
|
|
19
|
+
export type UseCareCompassOptions = CareCompassCoreOptions;
|
|
20
|
+
export type CareCompassReactController = {
|
|
21
|
+
core: CareCompassCore;
|
|
22
|
+
snapshot: CareCompassCoreSnapshot;
|
|
23
|
+
sendMessage: (prompt: string, options?: CareCompassSendMessageOptions) => Promise<CareCompassCoreSnapshot>;
|
|
24
|
+
streamMessage: CareCompassCore["streamMessage"];
|
|
25
|
+
retryLast: CareCompassCore["retryLast"];
|
|
26
|
+
reset: CareCompassCore["reset"];
|
|
27
|
+
setContext: CareCompassCore["setContext"];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* React adapter for the headless CareCompass core. It owns the core lifecycle
|
|
31
|
+
* and subscribes React to its snapshots, leaving hosts free to render any UI.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useCareCompass(config: CareCompassConfig, options?: UseCareCompassOptions): CareCompassReactController;
|
|
34
|
+
export declare function CareCompassChat({ config, initialContext, onAuditEvent, onEvent, onSafetyEvent, pollIntervalMs, renderMessage, retryLabel, runtimeTokenProvider, }: CareCompassChatProps): React.JSX.Element;
|
|
13
35
|
export declare const CareCompassEmbed: typeof CareCompassChat;
|
package/dist/component.js
CHANGED
|
@@ -26,11 +26,68 @@ class CareCompassErrorBoundary extends React.Component {
|
|
|
26
26
|
return this.props.children;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* React adapter for the headless CareCompass core. It owns the core lifecycle
|
|
31
|
+
* and subscribes React to its snapshots, leaving hosts free to render any UI.
|
|
32
|
+
*/
|
|
33
|
+
export function useCareCompass(config, options = {}) {
|
|
34
|
+
const { compliance, fetcher, initialContext, now, onAuditEvent, onSafetyEvent, pollIntervalMs, runtimeTokenProvider, timeoutMs, } = options;
|
|
30
35
|
const core = React.useMemo(() => createCareCompassCore(config, {
|
|
36
|
+
compliance,
|
|
37
|
+
fetcher,
|
|
31
38
|
initialContext,
|
|
39
|
+
now,
|
|
40
|
+
onAuditEvent,
|
|
41
|
+
onSafetyEvent,
|
|
32
42
|
pollIntervalMs,
|
|
33
|
-
|
|
43
|
+
runtimeTokenProvider,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
}), [
|
|
46
|
+
compliance,
|
|
47
|
+
config,
|
|
48
|
+
fetcher,
|
|
49
|
+
initialContext,
|
|
50
|
+
now,
|
|
51
|
+
onAuditEvent,
|
|
52
|
+
onSafetyEvent,
|
|
53
|
+
pollIntervalMs,
|
|
54
|
+
runtimeTokenProvider,
|
|
55
|
+
timeoutMs,
|
|
56
|
+
]);
|
|
57
|
+
const [snapshot, setSnapshot] = React.useState(() => core.getSnapshot());
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
const unsubscribe = core.subscribe(setSnapshot);
|
|
60
|
+
setSnapshot(core.getSnapshot());
|
|
61
|
+
return () => {
|
|
62
|
+
unsubscribe();
|
|
63
|
+
core.dispose();
|
|
64
|
+
};
|
|
65
|
+
}, [core]);
|
|
66
|
+
return React.useMemo(() => ({
|
|
67
|
+
core,
|
|
68
|
+
snapshot,
|
|
69
|
+
sendMessage: core.sendMessage,
|
|
70
|
+
streamMessage: core.streamMessage,
|
|
71
|
+
retryLast: core.retryLast,
|
|
72
|
+
reset: core.reset,
|
|
73
|
+
setContext: core.setContext,
|
|
74
|
+
}), [core, snapshot]);
|
|
75
|
+
}
|
|
76
|
+
function CareCompassChatContent({ config, initialContext, onAuditEvent, onEvent, onSafetyEvent, telemetry, pollIntervalMs = 1500, renderMessage, retryLabel = "Retry", runtimeTokenProvider, }) {
|
|
77
|
+
const core = React.useMemo(() => createCareCompassCore(config, {
|
|
78
|
+
initialContext,
|
|
79
|
+
onAuditEvent,
|
|
80
|
+
onSafetyEvent,
|
|
81
|
+
pollIntervalMs,
|
|
82
|
+
runtimeTokenProvider,
|
|
83
|
+
}), [
|
|
84
|
+
config,
|
|
85
|
+
initialContext,
|
|
86
|
+
onAuditEvent,
|
|
87
|
+
onSafetyEvent,
|
|
88
|
+
pollIntervalMs,
|
|
89
|
+
runtimeTokenProvider,
|
|
90
|
+
]);
|
|
34
91
|
const [snapshot, setSnapshot] = React.useState(() => core.getSnapshot());
|
|
35
92
|
const [input, setInput] = React.useState("");
|
|
36
93
|
React.useEffect(() => {
|
|
@@ -51,6 +108,7 @@ function CareCompassChatContent({ config, initialContext, onEvent, telemetry, po
|
|
|
51
108
|
return;
|
|
52
109
|
setInput("");
|
|
53
110
|
try {
|
|
111
|
+
core.recordWidgetAction();
|
|
54
112
|
for await (const coreEvent of core.streamMessage(prompt)) {
|
|
55
113
|
onEvent?.(coreEvent);
|
|
56
114
|
}
|
|
@@ -62,6 +120,7 @@ function CareCompassChatContent({ config, initialContext, onEvent, telemetry, po
|
|
|
62
120
|
}
|
|
63
121
|
async function retryMessage() {
|
|
64
122
|
try {
|
|
123
|
+
core.recordWidgetAction();
|
|
65
124
|
await core.retryLast();
|
|
66
125
|
}
|
|
67
126
|
catch {
|
|
@@ -154,10 +213,10 @@ function CareCompassChatContent({ config, initialContext, onEvent, telemetry, po
|
|
|
154
213
|
padding: "4px 8px",
|
|
155
214
|
}, type: "button", children: retryLabel })) : null] })) : null, _jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: config.labels.disclaimer })] })] }));
|
|
156
215
|
}
|
|
157
|
-
export function CareCompassChat({ config, initialContext, onEvent, pollIntervalMs = 1500, renderMessage, retryLabel, }) {
|
|
216
|
+
export function CareCompassChat({ config, initialContext, onAuditEvent, onEvent, onSafetyEvent, pollIntervalMs = 1500, renderMessage, retryLabel, runtimeTokenProvider, }) {
|
|
158
217
|
// Telemetry is scoped to the validated public config; no per-message content
|
|
159
218
|
// is passed through this component-level telemetry object.
|
|
160
219
|
const telemetry = React.useMemo(() => createCareCompassTelemetry(config), [config]);
|
|
161
|
-
return (_jsx(CareCompassErrorBoundary, { config: config, telemetry: telemetry, children: _jsx(CareCompassChatContent, { config: config, initialContext: initialContext, onEvent: onEvent, pollIntervalMs: pollIntervalMs, renderMessage: renderMessage, retryLabel: retryLabel, telemetry: telemetry }) }));
|
|
220
|
+
return (_jsx(CareCompassErrorBoundary, { config: config, telemetry: telemetry, children: _jsx(CareCompassChatContent, { config: config, initialContext: initialContext, onAuditEvent: onAuditEvent, onEvent: onEvent, onSafetyEvent: onSafetyEvent, pollIntervalMs: pollIntervalMs, renderMessage: renderMessage, retryLabel: retryLabel, runtimeTokenProvider: runtimeTokenProvider, telemetry: telemetry }) }));
|
|
162
221
|
}
|
|
163
222
|
export const CareCompassEmbed = CareCompassChat;
|
package/dist/core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type CareCompassStatusResponse } from "./client.js";
|
|
1
|
+
import { type CareCompassRuntimeTokenProvider, type CareCompassStatusResponse, type CareCompassWorkflowId } from "./client.js";
|
|
2
|
+
import { type CareCompassAuditEvent, type CareCompassAuditListener, type CareCompassComplianceRuntimeOptions, type CareCompassSafetyDecision, type CareCompassSafetyListener } from "./compliance.js";
|
|
2
3
|
import type { CareCompassConfig } from "./schema.js";
|
|
3
4
|
type Fetcher = typeof fetch;
|
|
4
5
|
export type CareCompassContext = Record<string, unknown>;
|
|
@@ -8,15 +9,18 @@ export type CareCompassMessage = {
|
|
|
8
9
|
text: string;
|
|
9
10
|
createdAt: number;
|
|
10
11
|
status: "complete" | "failed" | "pending";
|
|
12
|
+
retrieval?: CareCompassStatusResponse["retrieval"];
|
|
11
13
|
};
|
|
12
14
|
export type CareCompassLifecyclePhase = "idle" | "submitting" | "streaming" | "complete" | "error" | "disposed";
|
|
13
|
-
export type CareCompassCoreErrorCode = "request_failed" | "timeout" | "disposed";
|
|
15
|
+
export type CareCompassCoreErrorCode = "request_failed" | "safety_blocked" | "timeout" | "disposed";
|
|
14
16
|
export type CareCompassCoreError = {
|
|
15
17
|
code: CareCompassCoreErrorCode;
|
|
16
18
|
message: string;
|
|
17
19
|
};
|
|
18
20
|
export type CareCompassRetryMetadata = {
|
|
19
21
|
prompt: string;
|
|
22
|
+
requestId?: string;
|
|
23
|
+
workflow?: CareCompassWorkflowId;
|
|
20
24
|
context: CareCompassContext;
|
|
21
25
|
attempts: number;
|
|
22
26
|
};
|
|
@@ -55,18 +59,31 @@ export type CareCompassCoreEvent = {
|
|
|
55
59
|
snapshot: CareCompassCoreSnapshot;
|
|
56
60
|
};
|
|
57
61
|
export type CareCompassCoreOptions = {
|
|
62
|
+
compliance?: CareCompassComplianceRuntimeOptions;
|
|
58
63
|
fetcher?: Fetcher;
|
|
59
64
|
initialContext?: CareCompassContext;
|
|
60
65
|
now?: () => number;
|
|
66
|
+
onAuditEvent?: CareCompassAuditListener;
|
|
67
|
+
onSafetyEvent?: CareCompassSafetyListener;
|
|
61
68
|
pollIntervalMs?: number;
|
|
69
|
+
runtimeTokenProvider?: CareCompassRuntimeTokenProvider;
|
|
62
70
|
timeoutMs?: number;
|
|
63
71
|
};
|
|
64
72
|
export type CareCompassSendMessageOptions = {
|
|
73
|
+
/** Reuse this value only when retrying the same hosted submission. */
|
|
74
|
+
requestId?: string;
|
|
75
|
+
workflow?: CareCompassWorkflowId;
|
|
65
76
|
context?: CareCompassContext;
|
|
66
77
|
};
|
|
67
78
|
export type CareCompassCore = {
|
|
79
|
+
exportAuditTrail: () => CareCompassAuditEvent[];
|
|
68
80
|
getSnapshot: () => CareCompassCoreSnapshot;
|
|
81
|
+
recordSafetyDecision: (decision: Exclude<CareCompassSafetyDecision, "redacted" | "blocked">, source?: "tool_call" | "widget_action") => void;
|
|
82
|
+
recordToolCall: (outcome?: "completed" | "failed") => void;
|
|
83
|
+
recordWidgetAction: (outcome?: "recorded" | "completed") => void;
|
|
69
84
|
subscribe: (listener: (snapshot: CareCompassCoreSnapshot) => void) => () => void;
|
|
85
|
+
subscribeAudit: (listener: CareCompassAuditListener) => () => void;
|
|
86
|
+
subscribeSafety: (listener: CareCompassSafetyListener) => () => void;
|
|
70
87
|
setContext: (context: CareCompassContext | ((current: CareCompassContext) => CareCompassContext)) => void;
|
|
71
88
|
sendMessage: (prompt: string, options?: CareCompassSendMessageOptions) => Promise<CareCompassCoreSnapshot>;
|
|
72
89
|
streamMessage: (prompt: string, options?: CareCompassSendMessageOptions) => AsyncIterable<CareCompassCoreEvent>;
|
package/dist/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCareCompassClient, } from "./client.js";
|
|
2
|
+
import { phiSummary, resolveCareCompassCompliancePolicy, sanitizeCareCompassValue, } from "./compliance.js";
|
|
2
3
|
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
3
4
|
const errorFrom = (error, code = "request_failed") => ({
|
|
4
5
|
code,
|
|
@@ -11,7 +12,16 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
11
12
|
const client = createCareCompassClient(config, {
|
|
12
13
|
fetcher: options.fetcher,
|
|
13
14
|
now,
|
|
15
|
+
runtimeTokenProvider: options.runtimeTokenProvider,
|
|
14
16
|
});
|
|
17
|
+
const compliancePolicy = resolveCareCompassCompliancePolicy(config.compliance);
|
|
18
|
+
const auditEvents = [];
|
|
19
|
+
const auditListeners = new Set();
|
|
20
|
+
const safetyListeners = new Set();
|
|
21
|
+
if (options.onAuditEvent)
|
|
22
|
+
auditListeners.add(options.onAuditEvent);
|
|
23
|
+
if (options.onSafetyEvent)
|
|
24
|
+
safetyListeners.add(options.onSafetyEvent);
|
|
15
25
|
const listeners = new Set();
|
|
16
26
|
let sequence = 0;
|
|
17
27
|
let disposed = false;
|
|
@@ -30,6 +40,44 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
30
40
|
},
|
|
31
41
|
];
|
|
32
42
|
const nextId = (prefix) => `${prefix}-${now()}-${++sequence}`;
|
|
43
|
+
const emitAudit = (type, outcome, source, matches = []) => {
|
|
44
|
+
if (!compliancePolicy.auditEnabled)
|
|
45
|
+
return;
|
|
46
|
+
const auditEvent = {
|
|
47
|
+
id: nextId("audit"),
|
|
48
|
+
occurredAt: now(),
|
|
49
|
+
outcome,
|
|
50
|
+
phi: phiSummary(matches),
|
|
51
|
+
...(source ? { source } : {}),
|
|
52
|
+
type,
|
|
53
|
+
};
|
|
54
|
+
auditEvents.push(auditEvent);
|
|
55
|
+
for (const listener of auditListeners)
|
|
56
|
+
listener(auditEvent);
|
|
57
|
+
};
|
|
58
|
+
const emitSafety = (decision, source, matches = []) => {
|
|
59
|
+
const safetyEvent = {
|
|
60
|
+
decision,
|
|
61
|
+
occurredAt: now(),
|
|
62
|
+
phi: phiSummary(matches),
|
|
63
|
+
source,
|
|
64
|
+
};
|
|
65
|
+
for (const listener of safetyListeners)
|
|
66
|
+
listener(safetyEvent);
|
|
67
|
+
emitAudit("safety_decision", decision === "policy_sensitive" ? "recorded" : decision, source, matches);
|
|
68
|
+
};
|
|
69
|
+
const applyPhiPolicy = (value, source) => {
|
|
70
|
+
const sanitized = sanitizeCareCompassValue(value, source, compliancePolicy, options.compliance?.phi);
|
|
71
|
+
if (sanitized.matches.length === 0)
|
|
72
|
+
return sanitized;
|
|
73
|
+
const decision = compliancePolicy.phiAction === "block"
|
|
74
|
+
? "blocked"
|
|
75
|
+
: compliancePolicy.phiAction === "redact"
|
|
76
|
+
? "redacted"
|
|
77
|
+
: "policy_sensitive";
|
|
78
|
+
emitSafety(decision, source, sanitized.matches);
|
|
79
|
+
return sanitized;
|
|
80
|
+
};
|
|
33
81
|
const getSnapshot = () => ({
|
|
34
82
|
messages: messages.map((message) => ({ ...message })),
|
|
35
83
|
phase,
|
|
@@ -40,6 +88,8 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
40
88
|
retry: retry
|
|
41
89
|
? {
|
|
42
90
|
prompt: retry.prompt,
|
|
91
|
+
requestId: retry.requestId,
|
|
92
|
+
workflow: retry.workflow,
|
|
43
93
|
context: { ...retry.context },
|
|
44
94
|
attempts: retry.attempts,
|
|
45
95
|
}
|
|
@@ -56,6 +106,7 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
56
106
|
error = coreError;
|
|
57
107
|
phase = disposed ? "disposed" : "error";
|
|
58
108
|
activeJobId = null;
|
|
109
|
+
emitAudit("error", "failed");
|
|
59
110
|
notify();
|
|
60
111
|
return event({ type: "error", error: coreError });
|
|
61
112
|
};
|
|
@@ -72,14 +123,33 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
72
123
|
const prompt = promptInput.trim();
|
|
73
124
|
if (!prompt)
|
|
74
125
|
return;
|
|
75
|
-
const
|
|
126
|
+
const protectedPrompt = applyPhiPolicy(prompt, "message");
|
|
127
|
+
const protectedContext = applyPhiPolicy({
|
|
76
128
|
...context,
|
|
77
129
|
...(sendOptions.context ?? {}),
|
|
78
|
-
};
|
|
130
|
+
}, "context");
|
|
131
|
+
if (compliancePolicy.phiAction === "block" &&
|
|
132
|
+
(protectedPrompt.matches.length > 0 ||
|
|
133
|
+
protectedContext.matches.length > 0)) {
|
|
134
|
+
emitAudit("message", "blocked", "message", protectedPrompt.matches);
|
|
135
|
+
const blocked = {
|
|
136
|
+
code: "safety_blocked",
|
|
137
|
+
message: "CareCompass blocked this request because detectable PHI is not allowed in strict compliance mode.",
|
|
138
|
+
};
|
|
139
|
+
retry = null;
|
|
140
|
+
yield fail(blocked);
|
|
141
|
+
throw new Error(blocked.message);
|
|
142
|
+
}
|
|
143
|
+
const requestContext = protectedContext.value;
|
|
144
|
+
const protectedPromptValue = protectedPrompt.value;
|
|
145
|
+
const requestId = sendOptions.requestId ??
|
|
146
|
+
(typeof globalThis.crypto?.randomUUID === "function"
|
|
147
|
+
? globalThis.crypto.randomUUID()
|
|
148
|
+
: nextId("request"));
|
|
79
149
|
const userMessage = {
|
|
80
150
|
id: nextId("user"),
|
|
81
151
|
role: "user",
|
|
82
|
-
text:
|
|
152
|
+
text: protectedPromptValue,
|
|
83
153
|
createdAt: now(),
|
|
84
154
|
status: "complete",
|
|
85
155
|
};
|
|
@@ -89,12 +159,18 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
89
159
|
activeJobId = null;
|
|
90
160
|
retry = null;
|
|
91
161
|
notify();
|
|
162
|
+
emitAudit("message", protectedPrompt.matches.length > 0 &&
|
|
163
|
+
compliancePolicy.phiAction === "redact"
|
|
164
|
+
? "redacted"
|
|
165
|
+
: "submitted", "message", protectedPrompt.matches);
|
|
92
166
|
yield event({ type: "message_submitted", message: userMessage });
|
|
93
167
|
const startedAt = now();
|
|
94
168
|
let failureAlreadyCaptured = false;
|
|
95
169
|
try {
|
|
96
170
|
const { jobId } = await client.ask({
|
|
97
|
-
prompt,
|
|
171
|
+
prompt: protectedPromptValue,
|
|
172
|
+
requestId,
|
|
173
|
+
workflow: sendOptions.workflow,
|
|
98
174
|
userContext: requestContext,
|
|
99
175
|
});
|
|
100
176
|
activeJobId = jobId;
|
|
@@ -104,12 +180,25 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
104
180
|
const status = await client.status(jobId);
|
|
105
181
|
yield event({ type: "status", jobId, status: status.status });
|
|
106
182
|
if (status.status === "complete" && status.result) {
|
|
183
|
+
const protectedResponse = applyPhiPolicy(status.result, "response");
|
|
184
|
+
if (compliancePolicy.phiAction === "block" &&
|
|
185
|
+
protectedResponse.matches.length > 0) {
|
|
186
|
+
const blocked = {
|
|
187
|
+
code: "safety_blocked",
|
|
188
|
+
message: "CareCompass blocked a response because detectable PHI is not allowed in strict compliance mode.",
|
|
189
|
+
};
|
|
190
|
+
retry = null;
|
|
191
|
+
yield fail(blocked);
|
|
192
|
+
failureAlreadyCaptured = true;
|
|
193
|
+
throw new Error(blocked.message);
|
|
194
|
+
}
|
|
107
195
|
const assistantMessage = {
|
|
108
196
|
id: nextId("assistant"),
|
|
109
197
|
role: "assistant",
|
|
110
|
-
text:
|
|
198
|
+
text: protectedResponse.value,
|
|
111
199
|
createdAt: now(),
|
|
112
200
|
status: "complete",
|
|
201
|
+
retrieval: status.retrieval,
|
|
113
202
|
};
|
|
114
203
|
messages = [...messages, assistantMessage];
|
|
115
204
|
phase = "complete";
|
|
@@ -117,6 +206,10 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
117
206
|
activeJobId = null;
|
|
118
207
|
retry = null;
|
|
119
208
|
notify();
|
|
209
|
+
emitAudit("message", protectedResponse.matches.length > 0 &&
|
|
210
|
+
compliancePolicy.phiAction === "redact"
|
|
211
|
+
? "redacted"
|
|
212
|
+
: "completed", "response", protectedResponse.matches);
|
|
120
213
|
yield event({
|
|
121
214
|
type: "message_completed",
|
|
122
215
|
message: assistantMessage,
|
|
@@ -125,7 +218,12 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
125
218
|
}
|
|
126
219
|
if (status.status === "failed") {
|
|
127
220
|
const failed = errorFrom(new Error(status.result ?? config.labels.errorMessage));
|
|
128
|
-
retry = {
|
|
221
|
+
retry = {
|
|
222
|
+
prompt: protectedPromptValue,
|
|
223
|
+
workflow: sendOptions.workflow,
|
|
224
|
+
context: requestContext,
|
|
225
|
+
attempts: 0,
|
|
226
|
+
};
|
|
129
227
|
yield fail(failed);
|
|
130
228
|
failureAlreadyCaptured = true;
|
|
131
229
|
throw new Error(failed.message);
|
|
@@ -135,7 +233,13 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
135
233
|
}
|
|
136
234
|
client.recordInferenceTimeout(jobId, now() - startedAt);
|
|
137
235
|
const timedOut = errorFrom(new Error("CareCompass request timed out."), "timeout");
|
|
138
|
-
retry = {
|
|
236
|
+
retry = {
|
|
237
|
+
prompt: protectedPromptValue,
|
|
238
|
+
requestId,
|
|
239
|
+
workflow: sendOptions.workflow,
|
|
240
|
+
context: requestContext,
|
|
241
|
+
attempts: 0,
|
|
242
|
+
};
|
|
139
243
|
yield fail(timedOut);
|
|
140
244
|
failureAlreadyCaptured = true;
|
|
141
245
|
throw new Error(timedOut.message);
|
|
@@ -143,20 +247,44 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
143
247
|
catch (caught) {
|
|
144
248
|
if (failureAlreadyCaptured)
|
|
145
249
|
throw caught;
|
|
146
|
-
retry = {
|
|
250
|
+
retry = {
|
|
251
|
+
prompt: protectedPromptValue,
|
|
252
|
+
requestId,
|
|
253
|
+
workflow: sendOptions.workflow,
|
|
254
|
+
context: requestContext,
|
|
255
|
+
attempts: 0,
|
|
256
|
+
};
|
|
147
257
|
const requestError = errorFrom(caught);
|
|
148
258
|
yield fail(requestError);
|
|
149
259
|
throw caught instanceof Error ? caught : new Error(requestError.message);
|
|
150
260
|
}
|
|
151
261
|
}
|
|
152
262
|
return {
|
|
263
|
+
exportAuditTrail: () => auditEvents.map((auditEvent) => ({
|
|
264
|
+
...auditEvent,
|
|
265
|
+
phi: {
|
|
266
|
+
categories: [...auditEvent.phi.categories],
|
|
267
|
+
count: auditEvent.phi.count,
|
|
268
|
+
},
|
|
269
|
+
})),
|
|
153
270
|
getSnapshot,
|
|
271
|
+
recordSafetyDecision: (decision, source = "tool_call") => emitSafety(decision, source),
|
|
272
|
+
recordToolCall: (outcome = "completed") => emitAudit("tool_call", outcome, "tool_call"),
|
|
273
|
+
recordWidgetAction: (outcome = "recorded") => emitAudit("widget_action", outcome, "widget_action"),
|
|
154
274
|
subscribe: (listener) => {
|
|
155
275
|
listeners.add(listener);
|
|
156
276
|
return () => {
|
|
157
277
|
listeners.delete(listener);
|
|
158
278
|
};
|
|
159
279
|
},
|
|
280
|
+
subscribeAudit: (listener) => {
|
|
281
|
+
auditListeners.add(listener);
|
|
282
|
+
return () => auditListeners.delete(listener);
|
|
283
|
+
},
|
|
284
|
+
subscribeSafety: (listener) => {
|
|
285
|
+
safetyListeners.add(listener);
|
|
286
|
+
return () => safetyListeners.delete(listener);
|
|
287
|
+
},
|
|
160
288
|
setContext: (nextContext) => {
|
|
161
289
|
ensureAvailable();
|
|
162
290
|
context =
|
|
@@ -206,10 +334,14 @@ export function createCareCompassCore(config, options = {}) {
|
|
|
206
334
|
phase = "disposed";
|
|
207
335
|
activeJobId = null;
|
|
208
336
|
listeners.clear();
|
|
337
|
+
auditListeners.clear();
|
|
338
|
+
safetyListeners.clear();
|
|
209
339
|
},
|
|
210
340
|
};
|
|
211
341
|
async function streamRetry(retryRequest) {
|
|
212
342
|
const iterator = streamMessage(retryRequest.prompt, {
|
|
343
|
+
requestId: retryRequest.requestId,
|
|
344
|
+
workflow: retryRequest.workflow,
|
|
213
345
|
context: retryRequest.context,
|
|
214
346
|
})[Symbol.asyncIterator]();
|
|
215
347
|
while (!(await iterator.next()).done) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
export { createCareCompassClient } from "./client.js";
|
|
2
|
-
export type { CareCompassAskRequest, CareCompassAskResponse, CareCompassStatusResponse, } from "./client.js";
|
|
3
|
-
export { CareCompassChat, CareCompassEmbed } from "./component.js";
|
|
4
|
-
export type { CareCompassChatProps } from "./component.js";
|
|
2
|
+
export type { CareCompassAskRequest, CareCompassAskResponse, CareCompassClientOptions, CareCompassRetrievalMetadata, CareCompassRetrievalReference, CareCompassRuntimeTokenProvider, CareCompassRuntimeTokenRequest, CareCompassStatusResponse, CareCompassWorkflowId, } from "./client.js";
|
|
3
|
+
export { CareCompassChat, CareCompassEmbed, useCareCompass, } from "./component.js";
|
|
4
|
+
export type { CareCompassChatProps, CareCompassReactController, UseCareCompassOptions, } from "./component.js";
|
|
5
5
|
export { createCareCompassCore } from "./core.js";
|
|
6
6
|
export type { CareCompassContext, CareCompassCore, CareCompassCoreError, CareCompassCoreErrorCode, CareCompassCoreEvent, CareCompassCoreOptions, CareCompassCoreSnapshot, CareCompassLifecyclePhase, CareCompassMessage, CareCompassRetryMetadata, CareCompassSendMessageOptions, } from "./core.js";
|
|
7
|
+
export { CARECOMPASS_RCM_SYNTHETIC_DEMO, createRcmCodingWorkflow, RcmCodingAssistant, } from "./rcm.js";
|
|
8
|
+
export type { RcmCodeRecommendation, RcmCodeSystem, RcmCodingAssistanceInput, RcmCodingAssistanceResult, RcmCodingAssistantProps, RcmCodingWorkflow, RcmCodingWorkflowOptions, RcmConfidence, RcmCustomerRules, RcmPolicyReference, RcmPolicyRetriever, RcmReviewDisposition, } from "./rcm.js";
|
|
7
9
|
export { mountCareCompass } from "./mount.js";
|
|
10
|
+
export { CARECOMPASS_ELEMENT_TAG, defineCareCompassElement, } from "./web-component.js";
|
|
11
|
+
export type { CareCompassWebComponentElement } from "./web-component.js";
|
|
8
12
|
export { generateCareCompassIframeSnippet, generateCareCompassSnippet, } from "./snippet.js";
|
|
9
|
-
export { careCompassApiBaseUrlSchema, careCompassConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
|
|
10
|
-
export type { CareCompassConfig, CareCompassConfigInput, CareCompassLabels, CareCompassPromptConfig, CareCompassTheme, CareCompassValidationResult, } from "./schema.js";
|
|
13
|
+
export { careCompassApiBaseUrlSchema, careCompassComplianceConfigSchema, careCompassConfigSchema, careCompassFeatureFlagsSchema, careCompassModelConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
|
|
14
|
+
export type { CareCompassComplianceConfig, CareCompassConfig, CareCompassConfigInput, CareCompassFeatureFlags, CareCompassLabels, CareCompassModelConfig, CareCompassPromptConfig, CareCompassTheme, CareCompassValidationResult, } from "./schema.js";
|
|
15
|
+
export { CARECOMPASS_COMPLIANCE_PRESETS, detectCareCompassPhi, redactCareCompassPhi, resolveCareCompassCompliancePolicy, } from "./compliance.js";
|
|
16
|
+
export type { CareCompassAuditEvent, CareCompassAuditEventType, CareCompassAuditListener, CareCompassAuditOutcome, CareCompassAuditSource, CareCompassCompliancePolicy, CareCompassCompliancePreset, CareCompassComplianceRuntimeOptions, CareCompassPhiAction, CareCompassPhiCategory, CareCompassPhiDetectionContext, CareCompassPhiDetector, CareCompassPhiHooks, CareCompassPhiMatch, CareCompassPhiRedactor, CareCompassPhiSource, CareCompassSafetyDecision, CareCompassSafetyEvent, CareCompassSafetyListener, } from "./compliance.js";
|
|
11
17
|
export { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, careCompassTelemetryDurationBucket, createCareCompassCliTelemetry, createCareCompassTelemetry, } from "./telemetry.js";
|
|
12
18
|
export type { CareCompassTelemetry, CareCompassTelemetryCaptureOptions, CareCompassTelemetryDurationBucket, CareCompassTelemetryErrorCode, CareCompassTelemetryEvent, CareCompassTelemetryFailureCategory, } from "./telemetry.js";
|
|
13
19
|
export { CARECOMPASS_COMPONENT_VERSION, CARECOMPASS_SDK_VERSION, CARECOMPASS_TELEMETRY_CONTRACT_VERSION, } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { createCareCompassClient } from "./client.js";
|
|
2
|
-
export { CareCompassChat, CareCompassEmbed } from "./component.js";
|
|
2
|
+
export { CareCompassChat, CareCompassEmbed, useCareCompass, } from "./component.js";
|
|
3
3
|
export { createCareCompassCore } from "./core.js";
|
|
4
|
+
export { CARECOMPASS_RCM_SYNTHETIC_DEMO, createRcmCodingWorkflow, RcmCodingAssistant, } from "./rcm.js";
|
|
4
5
|
export { mountCareCompass } from "./mount.js";
|
|
6
|
+
export { CARECOMPASS_ELEMENT_TAG, defineCareCompassElement, } from "./web-component.js";
|
|
5
7
|
export { generateCareCompassIframeSnippet, generateCareCompassSnippet, } from "./snippet.js";
|
|
6
|
-
export { careCompassApiBaseUrlSchema, careCompassConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
|
|
8
|
+
export { careCompassApiBaseUrlSchema, careCompassComplianceConfigSchema, careCompassConfigSchema, careCompassFeatureFlagsSchema, careCompassModelConfigSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
|
|
9
|
+
export { CARECOMPASS_COMPLIANCE_PRESETS, detectCareCompassPhi, redactCareCompassPhi, resolveCareCompassCompliancePolicy, } from "./compliance.js";
|
|
7
10
|
export { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, careCompassTelemetryDurationBucket, createCareCompassCliTelemetry, createCareCompassTelemetry, } from "./telemetry.js";
|
|
8
11
|
export { CARECOMPASS_COMPONENT_VERSION, CARECOMPASS_SDK_VERSION, CARECOMPASS_TELEMETRY_CONTRACT_VERSION, } from "./version.js";
|
package/dist/markdown.d.ts
CHANGED
package/dist/mount.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { CareCompassRuntimeTokenProvider } from "./client.js";
|
|
1
2
|
import type { CareCompassConfig } from "./schema.js";
|
|
2
|
-
export declare function mountCareCompass(element: Element | null, config: CareCompassConfig
|
|
3
|
+
export declare function mountCareCompass(element: Element | null, config: CareCompassConfig, options?: {
|
|
4
|
+
runtimeTokenProvider?: CareCompassRuntimeTokenProvider;
|
|
5
|
+
}): {
|
|
3
6
|
unmount: () => void;
|
|
4
7
|
};
|
package/dist/mount.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { CareCompassEmbed } from "./component.js";
|
|
4
4
|
import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassTelemetry, } from "./telemetry.js";
|
|
5
|
-
export function mountCareCompass(element, config) {
|
|
5
|
+
export function mountCareCompass(element, config, options = {}) {
|
|
6
6
|
if (!element) {
|
|
7
7
|
// Missing mount points usually mean a copied snippet and page markup drifted
|
|
8
8
|
// apart, so capture the setup error before throwing to the caller.
|
|
@@ -13,7 +13,7 @@ export function mountCareCompass(element, config) {
|
|
|
13
13
|
throw new Error("CareCompass mount element was not found.");
|
|
14
14
|
}
|
|
15
15
|
const root = createRoot(element);
|
|
16
|
-
root.render(_jsx(CareCompassEmbed, { config: config }));
|
|
16
|
+
root.render(_jsx(CareCompassEmbed, { config: config, runtimeTokenProvider: options.runtimeTokenProvider }));
|
|
17
17
|
return {
|
|
18
18
|
// Return the small React root facade so non-React hosts can clean up embeds.
|
|
19
19
|
unmount: () => root.unmount(),
|