@contractspec/example.policy-safe-knowledge-assistant 0.0.0-canary-20260113170453
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/.turbo/turbo-build$colon$bundle.log +55 -0
- package/.turbo/turbo-build.log +56 -0
- package/CHANGELOG.md +585 -0
- package/LICENSE +21 -0
- package/README.md +22 -0
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.js +1 -0
- package/dist/docs/policy-safe-knowledge-assistant.docblock.d.ts +1 -0
- package/dist/docs/policy-safe-knowledge-assistant.docblock.js +35 -0
- package/dist/docs/policy-safe-knowledge-assistant.docblock.js.map +1 -0
- package/dist/example.d.ts +7 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +54 -0
- package/dist/example.js.map +1 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/policy-safe-knowledge-assistant.handlers.d.ts +127 -0
- package/dist/handlers/policy-safe-knowledge-assistant.handlers.d.ts.map +1 -0
- package/dist/handlers/policy-safe-knowledge-assistant.handlers.js +264 -0
- package/dist/handlers/policy-safe-knowledge-assistant.handlers.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/orchestrator/buildAnswer.d.ts +53 -0
- package/dist/orchestrator/buildAnswer.d.ts.map +1 -0
- package/dist/orchestrator/buildAnswer.js +77 -0
- package/dist/orchestrator/buildAnswer.js.map +1 -0
- package/dist/policy-safe-knowledge-assistant.feature.d.ts +7 -0
- package/dist/policy-safe-knowledge-assistant.feature.d.ts.map +1 -0
- package/dist/policy-safe-knowledge-assistant.feature.js +150 -0
- package/dist/policy-safe-knowledge-assistant.feature.js.map +1 -0
- package/dist/seed/fixtures.d.ts +35 -0
- package/dist/seed/fixtures.d.ts.map +1 -0
- package/dist/seed/fixtures.js +34 -0
- package/dist/seed/fixtures.js.map +1 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seeders/index.d.ts +10 -0
- package/dist/seeders/index.d.ts.map +1 -0
- package/dist/seeders/index.js +16 -0
- package/dist/seeders/index.js.map +1 -0
- package/dist/ui/PolicySafeKnowledgeAssistantDashboard.d.ts +7 -0
- package/dist/ui/PolicySafeKnowledgeAssistantDashboard.d.ts.map +1 -0
- package/dist/ui/PolicySafeKnowledgeAssistantDashboard.js +231 -0
- package/dist/ui/PolicySafeKnowledgeAssistantDashboard.js.map +1 -0
- package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.d.ts +55 -0
- package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.d.ts.map +1 -0
- package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.js +193 -0
- package/dist/ui/hooks/usePolicySafeKnowledgeAssistant.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +3 -0
- package/package.json +81 -0
- package/src/docs/index.ts +1 -0
- package/src/docs/policy-safe-knowledge-assistant.docblock.ts +28 -0
- package/src/example.ts +36 -0
- package/src/handlers/index.ts +1 -0
- package/src/handlers/policy-safe-knowledge-assistant.handlers.ts +476 -0
- package/src/index.ts +11 -0
- package/src/integration.test.ts +108 -0
- package/src/orchestrator/buildAnswer.ts +122 -0
- package/src/policy-safe-knowledge-assistant.feature.ts +58 -0
- package/src/seed/fixtures.ts +31 -0
- package/src/seed/index.ts +1 -0
- package/src/seeders/index.ts +20 -0
- package/src/ui/PolicySafeKnowledgeAssistantDashboard.tsx +206 -0
- package/src/ui/hooks/usePolicySafeKnowledgeAssistant.ts +229 -0
- package/src/ui/index.ts +1 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +17 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
enforceAllowedScope,
|
|
3
|
+
enforceCitations,
|
|
4
|
+
validateEnvelope,
|
|
5
|
+
} from '@contractspec/example.locale-jurisdiction-gate/policy/guard';
|
|
6
|
+
|
|
7
|
+
type AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';
|
|
8
|
+
|
|
9
|
+
export interface AssistantAnswerIR {
|
|
10
|
+
locale: string;
|
|
11
|
+
jurisdiction: string;
|
|
12
|
+
allowedScope: AllowedScope;
|
|
13
|
+
sections: { heading: string; body: string }[];
|
|
14
|
+
citations: {
|
|
15
|
+
kbSnapshotId: string;
|
|
16
|
+
sourceType: string;
|
|
17
|
+
sourceId: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
excerpt?: string;
|
|
20
|
+
}[];
|
|
21
|
+
disclaimers?: string[];
|
|
22
|
+
riskFlags?: string[];
|
|
23
|
+
refused?: boolean;
|
|
24
|
+
refusalReason?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BuildAnswerInput {
|
|
28
|
+
envelope: {
|
|
29
|
+
traceId: string;
|
|
30
|
+
locale: string;
|
|
31
|
+
kbSnapshotId: string;
|
|
32
|
+
allowedScope: AllowedScope;
|
|
33
|
+
regulatoryContext: { jurisdiction: string };
|
|
34
|
+
};
|
|
35
|
+
question: string;
|
|
36
|
+
kbSearch: (input: {
|
|
37
|
+
snapshotId: string;
|
|
38
|
+
jurisdiction: string;
|
|
39
|
+
query: string;
|
|
40
|
+
}) => Promise<{ items: { ruleVersionId: string; excerpt?: string }[] }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a policy-safe assistant answer derived from KB search results.
|
|
45
|
+
*
|
|
46
|
+
* Deterministic: no LLM calls; if search yields no results, it refuses.
|
|
47
|
+
*/
|
|
48
|
+
export async function buildPolicySafeAnswer(
|
|
49
|
+
input: BuildAnswerInput
|
|
50
|
+
): Promise<AssistantAnswerIR> {
|
|
51
|
+
const env = validateEnvelope(input.envelope);
|
|
52
|
+
if (!env.ok) {
|
|
53
|
+
return {
|
|
54
|
+
locale: input.envelope.locale ?? 'en-GB',
|
|
55
|
+
jurisdiction: input.envelope.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
56
|
+
allowedScope: input.envelope.allowedScope ?? 'education_only',
|
|
57
|
+
sections: [{ heading: 'Request blocked', body: env.error.message }],
|
|
58
|
+
citations: [],
|
|
59
|
+
disclaimers: ['This system refuses to answer without a valid envelope.'],
|
|
60
|
+
riskFlags: [env.error.code],
|
|
61
|
+
refused: true,
|
|
62
|
+
refusalReason: env.error.code,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const results = await input.kbSearch({
|
|
67
|
+
snapshotId: env.value.kbSnapshotId,
|
|
68
|
+
jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
69
|
+
query: input.question,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const citations = results.items.map((item) => ({
|
|
73
|
+
kbSnapshotId: env.value.kbSnapshotId,
|
|
74
|
+
sourceType: 'ruleVersion',
|
|
75
|
+
sourceId: item.ruleVersionId,
|
|
76
|
+
title: 'Curated rule version',
|
|
77
|
+
excerpt: item.excerpt,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const draft: AssistantAnswerIR = {
|
|
81
|
+
locale: env.value.locale,
|
|
82
|
+
jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
83
|
+
allowedScope: env.value.allowedScope,
|
|
84
|
+
sections: [
|
|
85
|
+
{
|
|
86
|
+
heading: 'Answer (KB-derived)',
|
|
87
|
+
body:
|
|
88
|
+
results.items.length > 0
|
|
89
|
+
? `This answer is derived from ${results.items.length} curated rule version(s) in the referenced snapshot.`
|
|
90
|
+
: 'No curated knowledge found in the referenced snapshot.',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
citations,
|
|
94
|
+
disclaimers: ['Educational demo only.'],
|
|
95
|
+
riskFlags: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const scope = enforceAllowedScope(env.value.allowedScope, draft);
|
|
99
|
+
if (!scope.ok) {
|
|
100
|
+
return {
|
|
101
|
+
...draft,
|
|
102
|
+
sections: [{ heading: 'Escalation required', body: scope.error.message }],
|
|
103
|
+
refused: true,
|
|
104
|
+
refusalReason: scope.error.code,
|
|
105
|
+
riskFlags: [...(draft.riskFlags ?? []), scope.error.code],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cited = enforceCitations(draft);
|
|
110
|
+
if (!cited.ok) {
|
|
111
|
+
return {
|
|
112
|
+
...draft,
|
|
113
|
+
sections: [{ heading: 'Request blocked', body: cited.error.message }],
|
|
114
|
+
citations: [],
|
|
115
|
+
refused: true,
|
|
116
|
+
refusalReason: cited.error.code,
|
|
117
|
+
riskFlags: [...(draft.riskFlags ?? []), cited.error.code],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return draft;
|
|
122
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineFeature } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
export const PolicySafeKnowledgeAssistantFeature = defineFeature({
|
|
4
|
+
meta: {
|
|
5
|
+
key: 'policy-safe-knowledge-assistant',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
title: 'Policy-safe Knowledge Assistant',
|
|
8
|
+
description:
|
|
9
|
+
'All-in-one example composing locale/jurisdiction gate + versioned KB + HITL pipeline + learning hub.',
|
|
10
|
+
domain: 'knowledge',
|
|
11
|
+
owners: ['@examples'],
|
|
12
|
+
tags: ['assistant', 'knowledge', 'policy', 'hitl', 'learning'],
|
|
13
|
+
stability: 'experimental',
|
|
14
|
+
},
|
|
15
|
+
operations: [
|
|
16
|
+
// Gate
|
|
17
|
+
{ key: 'assistant.answer', version: '1.0.0' },
|
|
18
|
+
{ key: 'assistant.explainConcept', version: '1.0.0' },
|
|
19
|
+
// KB
|
|
20
|
+
{ key: 'kb.ingestSource', version: '1.0.0' },
|
|
21
|
+
{ key: 'kb.upsertRuleVersion', version: '1.0.0' },
|
|
22
|
+
{ key: 'kb.approveRuleVersion', version: '1.0.0' },
|
|
23
|
+
{ key: 'kb.publishSnapshot', version: '1.0.0' },
|
|
24
|
+
{ key: 'kb.search', version: '1.0.0' },
|
|
25
|
+
// Pipeline
|
|
26
|
+
{ key: 'kbPipeline.runWatch', version: '1.0.0' },
|
|
27
|
+
{ key: 'kbPipeline.createReviewTask', version: '1.0.0' },
|
|
28
|
+
{ key: 'kbPipeline.submitDecision', version: '1.0.0' },
|
|
29
|
+
{ key: 'kbPipeline.publishIfReady', version: '1.0.0' },
|
|
30
|
+
],
|
|
31
|
+
events: [
|
|
32
|
+
{ key: 'assistant.answer.requested', version: '1.0.0' },
|
|
33
|
+
{ key: 'assistant.answer.blocked', version: '1.0.0' },
|
|
34
|
+
{ key: 'assistant.answer.delivered', version: '1.0.0' },
|
|
35
|
+
{ key: 'kb.source.ingested', version: '1.0.0' },
|
|
36
|
+
{ key: 'kb.ruleVersion.created', version: '1.0.0' },
|
|
37
|
+
{ key: 'kb.ruleVersion.approved', version: '1.0.0' },
|
|
38
|
+
{ key: 'kb.snapshot.published', version: '1.0.0' },
|
|
39
|
+
{ key: 'kb.change.detected', version: '1.0.0' },
|
|
40
|
+
{ key: 'kb.review.requested', version: '1.0.0' },
|
|
41
|
+
{ key: 'kb.review.decided', version: '1.0.0' },
|
|
42
|
+
],
|
|
43
|
+
presentations: [],
|
|
44
|
+
opToPresentation: [],
|
|
45
|
+
presentationsTargets: [],
|
|
46
|
+
capabilities: {
|
|
47
|
+
requires: [
|
|
48
|
+
{ key: 'identity', version: '1.0.0' },
|
|
49
|
+
{ key: 'audit-trail', version: '1.0.0' },
|
|
50
|
+
{ key: 'notifications', version: '1.0.0' },
|
|
51
|
+
{ key: 'jobs', version: '1.0.0' },
|
|
52
|
+
{ key: 'feature-flags', version: '1.0.0' },
|
|
53
|
+
{ key: 'files', version: '1.0.0' },
|
|
54
|
+
{ key: 'metering', version: '1.0.0' },
|
|
55
|
+
{ key: 'learning-journey', version: '1.0.0' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DEMO_FIXTURES = {
|
|
2
|
+
jurisdictions: ['EU', 'FR'] as const,
|
|
3
|
+
locales: ['en-GB', 'fr-FR'] as const,
|
|
4
|
+
demoOrgId: 'org_demo',
|
|
5
|
+
demoUserId: 'user_demo',
|
|
6
|
+
sources: {
|
|
7
|
+
EU_SOURCE_1: {
|
|
8
|
+
jurisdiction: 'EU',
|
|
9
|
+
authority: 'DemoAuthority',
|
|
10
|
+
title: 'EU Demo Source v1',
|
|
11
|
+
fetchedAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
12
|
+
hash: 'hash_eu_v1',
|
|
13
|
+
fileId: 'file_eu_v1',
|
|
14
|
+
},
|
|
15
|
+
EU_SOURCE_2: {
|
|
16
|
+
jurisdiction: 'EU',
|
|
17
|
+
authority: 'DemoAuthority',
|
|
18
|
+
title: 'EU Demo Source v2',
|
|
19
|
+
fetchedAt: new Date('2026-02-01T00:00:00.000Z'),
|
|
20
|
+
hash: 'hash_eu_v2',
|
|
21
|
+
fileId: 'file_eu_v2',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
rules: {
|
|
25
|
+
EU_RULE_TAX: {
|
|
26
|
+
id: 'rule_eu_tax',
|
|
27
|
+
jurisdiction: 'EU',
|
|
28
|
+
topicKey: 'tax_reporting',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './fixtures';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';
|
|
2
|
+
|
|
3
|
+
export async function seedPolicyKnowledgeAssistant(params: {
|
|
4
|
+
projectId: string;
|
|
5
|
+
db: DatabasePort;
|
|
6
|
+
}) {
|
|
7
|
+
const { projectId, db } = params;
|
|
8
|
+
|
|
9
|
+
const existing = await db.query(
|
|
10
|
+
`SELECT COUNT(*) as count FROM psa_user_context WHERE "projectId" = $1`,
|
|
11
|
+
[projectId]
|
|
12
|
+
);
|
|
13
|
+
if ((existing.rows[0]?.count as number) > 0) return;
|
|
14
|
+
|
|
15
|
+
await db.execute(
|
|
16
|
+
`INSERT INTO psa_user_context ("projectId", locale, jurisdiction, "allowedScope")
|
|
17
|
+
VALUES ($1, $2, $3, $4)`,
|
|
18
|
+
[projectId, 'en-GB', 'EU', 'education_only']
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
ErrorState,
|
|
7
|
+
LoaderBlock,
|
|
8
|
+
StatCard,
|
|
9
|
+
StatCardGroup,
|
|
10
|
+
} from '@contractspec/lib.design-system';
|
|
11
|
+
import { Card } from '@contractspec/lib.ui-kit-web/ui/card';
|
|
12
|
+
import { Input } from '@contractspec/lib.ui-kit-web/ui/input';
|
|
13
|
+
import { Textarea } from '@contractspec/lib.ui-kit-web/ui/textarea';
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@contractspec/lib.ui-kit-web/ui/select';
|
|
21
|
+
|
|
22
|
+
import { usePolicySafeKnowledgeAssistant } from './hooks/usePolicySafeKnowledgeAssistant';
|
|
23
|
+
|
|
24
|
+
type AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';
|
|
25
|
+
|
|
26
|
+
export function PolicySafeKnowledgeAssistantDashboard() {
|
|
27
|
+
const { state, actions } = usePolicySafeKnowledgeAssistant();
|
|
28
|
+
const [question, setQuestion] = useState('reporting obligations');
|
|
29
|
+
const [content, setContent] = useState(
|
|
30
|
+
'EU: Reporting obligations v2 (updated)'
|
|
31
|
+
);
|
|
32
|
+
const [locale, setLocale] = useState('en-GB');
|
|
33
|
+
const [jurisdiction, setJurisdiction] = useState('EU');
|
|
34
|
+
const [allowedScope, setAllowedScope] =
|
|
35
|
+
useState<AllowedScope>('education_only');
|
|
36
|
+
|
|
37
|
+
const snapshotId =
|
|
38
|
+
state.context?.kbSnapshotId ?? state.lastSnapshotId ?? null;
|
|
39
|
+
|
|
40
|
+
const stats = useMemo(() => {
|
|
41
|
+
return [
|
|
42
|
+
{ label: 'Locale', value: state.context?.locale ?? '—' },
|
|
43
|
+
{ label: 'Jurisdiction', value: state.context?.jurisdiction ?? '—' },
|
|
44
|
+
{ label: 'Scope', value: state.context?.allowedScope ?? '—' },
|
|
45
|
+
{ label: 'KB Snapshot', value: snapshotId ?? '—' },
|
|
46
|
+
];
|
|
47
|
+
}, [
|
|
48
|
+
snapshotId,
|
|
49
|
+
state.context?.allowedScope,
|
|
50
|
+
state.context?.jurisdiction,
|
|
51
|
+
state.context?.locale,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const handleSetContext = useCallback(async () => {
|
|
55
|
+
await actions.setContext({ locale, jurisdiction, allowedScope });
|
|
56
|
+
}, [actions, allowedScope, jurisdiction, locale]);
|
|
57
|
+
|
|
58
|
+
const handleAsk = useCallback(async () => {
|
|
59
|
+
await actions.askAssistant(question);
|
|
60
|
+
}, [actions, question]);
|
|
61
|
+
|
|
62
|
+
const handleAdminPublishFlow = useCallback(async () => {
|
|
63
|
+
const ruleId = state.lastRuleId ?? (await actions.createDemoRule());
|
|
64
|
+
const rvId = await actions.upsertRuleVersion({ ruleId, content });
|
|
65
|
+
await actions.approveRuleVersion(rvId);
|
|
66
|
+
await actions.simulateHighRiskChangeAndApprove(rvId);
|
|
67
|
+
await actions.publishSnapshot();
|
|
68
|
+
}, [actions, content, state.lastRuleId]);
|
|
69
|
+
|
|
70
|
+
if (state.loading && !state.context) {
|
|
71
|
+
return <LoaderBlock label="Loading demo..." />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (state.error) {
|
|
75
|
+
return (
|
|
76
|
+
<ErrorState
|
|
77
|
+
title="Failed to load demo"
|
|
78
|
+
description={state.error.message}
|
|
79
|
+
onRetry={actions.refreshContext}
|
|
80
|
+
retryLabel="Retry"
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="space-y-6">
|
|
87
|
+
<StatCardGroup>
|
|
88
|
+
{stats.map((s) => (
|
|
89
|
+
<StatCard key={s.label} label={s.label} value={String(s.value)} />
|
|
90
|
+
))}
|
|
91
|
+
</StatCardGroup>
|
|
92
|
+
|
|
93
|
+
<Card className="p-4">
|
|
94
|
+
<h3 className="text-lg font-semibold">
|
|
95
|
+
1) Onboarding (explicit locale + jurisdiction)
|
|
96
|
+
</h3>
|
|
97
|
+
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
|
98
|
+
<div>
|
|
99
|
+
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
|
|
100
|
+
Locale
|
|
101
|
+
</div>
|
|
102
|
+
<Input value={locale} onChange={(e) => setLocale(e.target.value)} />
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
|
|
106
|
+
Jurisdiction
|
|
107
|
+
</div>
|
|
108
|
+
<Input
|
|
109
|
+
value={jurisdiction}
|
|
110
|
+
onChange={(e) => setJurisdiction(e.target.value)}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<div>
|
|
114
|
+
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
|
|
115
|
+
Allowed scope
|
|
116
|
+
</div>
|
|
117
|
+
<Select
|
|
118
|
+
value={allowedScope}
|
|
119
|
+
onValueChange={(v) => setAllowedScope(v as AllowedScope)}
|
|
120
|
+
>
|
|
121
|
+
<SelectTrigger>
|
|
122
|
+
<SelectValue placeholder="Select scope" />
|
|
123
|
+
</SelectTrigger>
|
|
124
|
+
<SelectContent>
|
|
125
|
+
<SelectItem value="education_only">education_only</SelectItem>
|
|
126
|
+
<SelectItem value="generic_info">generic_info</SelectItem>
|
|
127
|
+
<SelectItem value="escalation_required">
|
|
128
|
+
escalation_required
|
|
129
|
+
</SelectItem>
|
|
130
|
+
</SelectContent>
|
|
131
|
+
</Select>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="mt-4 flex gap-2">
|
|
135
|
+
<Button onPress={handleSetContext}>Save context</Button>
|
|
136
|
+
<Button variant="outline" onPress={actions.refreshContext}>
|
|
137
|
+
Refresh
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
</Card>
|
|
141
|
+
|
|
142
|
+
<Card className="p-4">
|
|
143
|
+
<h3 className="text-lg font-semibold">
|
|
144
|
+
2) Ask the assistant (must cite KB snapshot)
|
|
145
|
+
</h3>
|
|
146
|
+
<div className="mt-3 flex flex-col gap-3">
|
|
147
|
+
<Input
|
|
148
|
+
value={question}
|
|
149
|
+
onChange={(e) => setQuestion(e.target.value)}
|
|
150
|
+
/>
|
|
151
|
+
<div className="flex gap-2">
|
|
152
|
+
<Button onPress={handleAsk}>Ask</Button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{state.lastAnswer ? (
|
|
157
|
+
<div className="mt-4 space-y-3">
|
|
158
|
+
{state.lastAnswer.refused ? (
|
|
159
|
+
<div className="text-sm text-red-600">
|
|
160
|
+
Refused: {state.lastAnswer.refusalReason ?? 'UNKNOWN'}
|
|
161
|
+
</div>
|
|
162
|
+
) : null}
|
|
163
|
+
{state.lastAnswer.sections.map((s, idx) => (
|
|
164
|
+
<div key={`${s.heading}-${idx}`}>
|
|
165
|
+
<div className="text-sm font-semibold">{s.heading}</div>
|
|
166
|
+
<div className="text-muted-foreground text-sm">{s.body}</div>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
<div className="text-sm font-semibold">Citations</div>
|
|
170
|
+
<ul className="text-muted-foreground list-disc pl-5 text-sm">
|
|
171
|
+
{state.lastAnswer.citations.map((c) => (
|
|
172
|
+
<li key={`${c.kbSnapshotId}-${c.sourceId}`}>
|
|
173
|
+
{c.kbSnapshotId} — {c.sourceId}
|
|
174
|
+
</li>
|
|
175
|
+
))}
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
) : null}
|
|
179
|
+
</Card>
|
|
180
|
+
|
|
181
|
+
<Card className="p-4">
|
|
182
|
+
<h3 className="text-lg font-semibold">
|
|
183
|
+
3) Admin: publish a new snapshot (HITL)
|
|
184
|
+
</h3>
|
|
185
|
+
<div className="mt-3 space-y-3">
|
|
186
|
+
<Textarea
|
|
187
|
+
value={content}
|
|
188
|
+
onChange={(e) => setContent(e.target.value)}
|
|
189
|
+
/>
|
|
190
|
+
<Button onPress={handleAdminPublishFlow}>
|
|
191
|
+
Simulate change → review → approve → publish snapshot
|
|
192
|
+
</Button>
|
|
193
|
+
</div>
|
|
194
|
+
</Card>
|
|
195
|
+
|
|
196
|
+
<Card className="p-4">
|
|
197
|
+
<h3 className="text-lg font-semibold">4) Learning hub (patterns)</h3>
|
|
198
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
199
|
+
This template includes drills, ambient coach, and quests as reusable
|
|
200
|
+
Learning Journey tracks. The interactive learning UI is demonstrated
|
|
201
|
+
in dedicated Learning Journey examples.
|
|
202
|
+
</p>
|
|
203
|
+
</Card>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
5
|
+
|
|
6
|
+
type AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';
|
|
7
|
+
type RiskLevel = 'low' | 'medium' | 'high';
|
|
8
|
+
|
|
9
|
+
export interface UsePolicySafeKnowledgeAssistantState {
|
|
10
|
+
context: {
|
|
11
|
+
locale: string;
|
|
12
|
+
jurisdiction: string;
|
|
13
|
+
allowedScope: AllowedScope;
|
|
14
|
+
kbSnapshotId: string | null;
|
|
15
|
+
} | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
lastAnswer: {
|
|
19
|
+
refused?: boolean;
|
|
20
|
+
refusalReason?: string;
|
|
21
|
+
sections: { heading: string; body: string }[];
|
|
22
|
+
citations: {
|
|
23
|
+
kbSnapshotId: string;
|
|
24
|
+
sourceId: string;
|
|
25
|
+
excerpt?: string;
|
|
26
|
+
}[];
|
|
27
|
+
} | null;
|
|
28
|
+
lastRuleId: string | null;
|
|
29
|
+
lastRuleVersionId: string | null;
|
|
30
|
+
lastSnapshotId: string | null;
|
|
31
|
+
lastReviewTaskId: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CitationLike {
|
|
35
|
+
kbSnapshotId: string;
|
|
36
|
+
sourceId: string;
|
|
37
|
+
excerpt?: string;
|
|
38
|
+
}
|
|
39
|
+
interface AnswerLike {
|
|
40
|
+
refused?: boolean;
|
|
41
|
+
refusalReason?: string;
|
|
42
|
+
sections: { heading: string; body: string }[];
|
|
43
|
+
citations: CitationLike[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isCitationLike(value: unknown): value is CitationLike {
|
|
47
|
+
if (!value || typeof value !== 'object') return false;
|
|
48
|
+
const v = value as Record<string, unknown>;
|
|
49
|
+
return typeof v.kbSnapshotId === 'string' && typeof v.sourceId === 'string';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toCitations(value: unknown): CitationLike[] {
|
|
53
|
+
if (!Array.isArray(value)) return [];
|
|
54
|
+
return value.filter(isCitationLike).map((c) => ({
|
|
55
|
+
kbSnapshotId: c.kbSnapshotId,
|
|
56
|
+
sourceId: c.sourceId,
|
|
57
|
+
excerpt: c.excerpt,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
import type { PolicySafeKnowledgeAssistantHandlers } from '../../handlers/policy-safe-knowledge-assistant.handlers';
|
|
62
|
+
|
|
63
|
+
export function usePolicySafeKnowledgeAssistant() {
|
|
64
|
+
const { handlers, projectId } = useTemplateRuntime<{
|
|
65
|
+
policySafeKnowledgeAssistant: PolicySafeKnowledgeAssistantHandlers;
|
|
66
|
+
}>();
|
|
67
|
+
const api = handlers.policySafeKnowledgeAssistant;
|
|
68
|
+
|
|
69
|
+
const [state, setState] = useState<UsePolicySafeKnowledgeAssistantState>({
|
|
70
|
+
context: null,
|
|
71
|
+
loading: true,
|
|
72
|
+
error: null,
|
|
73
|
+
lastAnswer: null,
|
|
74
|
+
lastRuleId: null,
|
|
75
|
+
lastRuleVersionId: null,
|
|
76
|
+
lastSnapshotId: null,
|
|
77
|
+
lastReviewTaskId: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const refreshContext = useCallback(async () => {
|
|
81
|
+
try {
|
|
82
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
83
|
+
const ctx = await api.getUserContext({ projectId });
|
|
84
|
+
setState((s) => ({
|
|
85
|
+
...s,
|
|
86
|
+
context: {
|
|
87
|
+
locale: ctx.locale,
|
|
88
|
+
jurisdiction: ctx.jurisdiction,
|
|
89
|
+
allowedScope: ctx.allowedScope,
|
|
90
|
+
kbSnapshotId: ctx.kbSnapshotId,
|
|
91
|
+
},
|
|
92
|
+
loading: false,
|
|
93
|
+
}));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
setState((s) => ({
|
|
96
|
+
...s,
|
|
97
|
+
loading: false,
|
|
98
|
+
error: e instanceof Error ? e : new Error('Unknown error'),
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
}, [api, projectId]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
refreshContext();
|
|
105
|
+
}, [refreshContext]);
|
|
106
|
+
|
|
107
|
+
const setContext = useCallback(
|
|
108
|
+
async (input: {
|
|
109
|
+
locale: string;
|
|
110
|
+
jurisdiction: string;
|
|
111
|
+
allowedScope: AllowedScope;
|
|
112
|
+
}) => {
|
|
113
|
+
const ctx = await api.setUserContext({ projectId, ...input });
|
|
114
|
+
setState((s) => ({
|
|
115
|
+
...s,
|
|
116
|
+
context: {
|
|
117
|
+
locale: ctx.locale,
|
|
118
|
+
jurisdiction: ctx.jurisdiction,
|
|
119
|
+
allowedScope: ctx.allowedScope,
|
|
120
|
+
kbSnapshotId: ctx.kbSnapshotId,
|
|
121
|
+
},
|
|
122
|
+
}));
|
|
123
|
+
},
|
|
124
|
+
[api, projectId]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const askAssistant = useCallback(
|
|
128
|
+
async (question: string) => {
|
|
129
|
+
const answerUnknown: unknown = await api.answer({ projectId, question });
|
|
130
|
+
const answer = answerUnknown as AnswerLike;
|
|
131
|
+
setState((s) => ({
|
|
132
|
+
...s,
|
|
133
|
+
lastAnswer: {
|
|
134
|
+
refused: answer.refused,
|
|
135
|
+
refusalReason: answer.refusalReason,
|
|
136
|
+
sections: answer.sections,
|
|
137
|
+
citations: toCitations(
|
|
138
|
+
(answerUnknown as { citations?: unknown }).citations
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
},
|
|
143
|
+
[api, projectId]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const createDemoRule = useCallback(async () => {
|
|
147
|
+
const rule = await api.createRule({
|
|
148
|
+
projectId,
|
|
149
|
+
jurisdiction: state.context?.jurisdiction ?? 'EU',
|
|
150
|
+
topicKey: 'tax_reporting',
|
|
151
|
+
});
|
|
152
|
+
setState((s) => ({ ...s, lastRuleId: rule.id }));
|
|
153
|
+
return rule.id as string;
|
|
154
|
+
}, [api, projectId, state.context?.jurisdiction]);
|
|
155
|
+
|
|
156
|
+
const upsertRuleVersion = useCallback(
|
|
157
|
+
async (input: { ruleId: string; content: string }) => {
|
|
158
|
+
const rv = await api.upsertRuleVersion({
|
|
159
|
+
projectId,
|
|
160
|
+
ruleId: input.ruleId,
|
|
161
|
+
content: input.content,
|
|
162
|
+
sourceRefs: [{ sourceDocumentId: 'src_demo', excerpt: 'demo excerpt' }],
|
|
163
|
+
});
|
|
164
|
+
setState((s) => ({ ...s, lastRuleVersionId: rv.id }));
|
|
165
|
+
return rv.id as string;
|
|
166
|
+
},
|
|
167
|
+
[api, projectId]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const approveRuleVersion = useCallback(
|
|
171
|
+
async (ruleVersionId: string) => {
|
|
172
|
+
await api.approveRuleVersion({ ruleVersionId, approver: 'demo_expert' });
|
|
173
|
+
},
|
|
174
|
+
[api]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const publishSnapshot = useCallback(async () => {
|
|
178
|
+
const snap = await api.publishSnapshot({
|
|
179
|
+
projectId,
|
|
180
|
+
jurisdiction: state.context?.jurisdiction ?? 'EU',
|
|
181
|
+
asOfDate: new Date('2026-02-01T00:00:00.000Z'),
|
|
182
|
+
});
|
|
183
|
+
setState((s) => ({ ...s, lastSnapshotId: snap.id }));
|
|
184
|
+
await refreshContext();
|
|
185
|
+
return snap.id as string;
|
|
186
|
+
}, [api, projectId, refreshContext, state.context?.jurisdiction]);
|
|
187
|
+
|
|
188
|
+
const simulateHighRiskChangeAndApprove = useCallback(
|
|
189
|
+
async (ruleVersionId: string) => {
|
|
190
|
+
const cand = await api.createChangeCandidate({
|
|
191
|
+
projectId,
|
|
192
|
+
jurisdiction: state.context?.jurisdiction ?? 'EU',
|
|
193
|
+
diffSummary: 'Simulated change (demo)',
|
|
194
|
+
riskLevel: 'high' satisfies RiskLevel,
|
|
195
|
+
proposedRuleVersionIds: [ruleVersionId],
|
|
196
|
+
});
|
|
197
|
+
const review = await api.createReviewTask({ changeCandidateId: cand.id });
|
|
198
|
+
setState((s) => ({ ...s, lastReviewTaskId: review.id }));
|
|
199
|
+
await api.submitDecision({
|
|
200
|
+
reviewTaskId: review.id,
|
|
201
|
+
decision: 'approve',
|
|
202
|
+
decidedByRole: 'expert',
|
|
203
|
+
decidedBy: 'demo_expert',
|
|
204
|
+
});
|
|
205
|
+
await api.publishIfReady({
|
|
206
|
+
jurisdiction: state.context?.jurisdiction ?? 'EU',
|
|
207
|
+
});
|
|
208
|
+
return review.id as string;
|
|
209
|
+
},
|
|
210
|
+
[api, projectId, state.context?.jurisdiction]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const derived = useMemo(() => ({ projectId }), [projectId]);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
state,
|
|
217
|
+
derived,
|
|
218
|
+
actions: {
|
|
219
|
+
refreshContext,
|
|
220
|
+
setContext,
|
|
221
|
+
askAssistant,
|
|
222
|
+
createDemoRule,
|
|
223
|
+
upsertRuleVersion,
|
|
224
|
+
approveRuleVersion,
|
|
225
|
+
publishSnapshot,
|
|
226
|
+
simulateHighRiskChangeAndApprove,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PolicySafeKnowledgeAssistantDashboard';
|
package/tsconfig.json
ADDED