@seed-ship/mcp-ui-solid 3.0.5 → 4.0.1
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/CHANGELOG.md +115 -0
- package/README.md +253 -280
- package/dist/components/ChartJSRenderer.cjs +37 -15
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +37 -15
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DataPreviewSection.cjs +213 -0
- package/dist/components/DataPreviewSection.cjs.map +1 -0
- package/dist/components/DataPreviewSection.d.ts +19 -0
- package/dist/components/DataPreviewSection.d.ts.map +1 -0
- package/dist/components/DataPreviewSection.js +213 -0
- package/dist/components/DataPreviewSection.js.map +1 -0
- package/dist/components/MapRenderer.cjs +168 -26
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts +2 -2
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +169 -27
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +83 -1
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +84 -2
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/components/VerifiedText.cjs +166 -0
- package/dist/components/VerifiedText.cjs.map +1 -0
- package/dist/components/VerifiedText.d.ts +22 -0
- package/dist/components/VerifiedText.d.ts.map +1 -0
- package/dist/components/VerifiedText.js +166 -0
- package/dist/components/VerifiedText.js.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +4 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +4 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +4 -0
- package/dist/components.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useDataValidator.cjs +31 -0
- package/dist/hooks/useDataValidator.cjs.map +1 -0
- package/dist/hooks/useDataValidator.d.ts +42 -0
- package/dist/hooks/useDataValidator.d.ts.map +1 -0
- package/dist/hooks/useDataValidator.js +31 -0
- package/dist/hooks/useDataValidator.js.map +1 -0
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +2 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs +290 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js +291 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs +243 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js +243 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js.map +1 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs +137 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs.map +1 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js +137 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js.map +1 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs +686 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js +687 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js.map +1 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs +1366 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js +1366 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs +54 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js +55 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js.map +1 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs +1256 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js +1256 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js.map +1 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs +47 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js +48 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js.map +1 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs +378 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js +379 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js.map +1 -0
- package/dist/services/data-validator.cjs +85 -0
- package/dist/services/data-validator.cjs.map +1 -0
- package/dist/services/data-validator.d.ts +28 -0
- package/dist/services/data-validator.d.ts.map +1 -0
- package/dist/services/data-validator.js +85 -0
- package/dist/services/data-validator.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/types/chat-bus.d.ts +88 -1
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/dist/types/index.d.ts +135 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +135 -6
- package/dist/types.d.ts +135 -6
- package/package.json +5 -1
- package/src/components/ChartJSRenderer.tsx +35 -13
- package/src/components/DataPreviewSection.tsx +251 -0
- package/src/components/MapRenderer.test.tsx +94 -5
- package/src/components/MapRenderer.tsx +246 -45
- package/src/components/ScratchpadPanel.tsx +19 -3
- package/src/components/VerifiedText.tsx +187 -0
- package/src/components/index.ts +7 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useDataValidator.ts +68 -0
- package/src/index.ts +26 -1
- package/src/services/data-validator.test.ts +151 -0
- package/src/services/data-validator.ts +149 -0
- package/src/services/index.ts +2 -0
- package/src/types/chat-bus.ts +98 -1
- package/src/types/index.ts +145 -6
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDataValidator — reactive SolidJS hook for data validation
|
|
3
|
+
* v3.1.0: Wraps validateAgainstSource in a reactive memo
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createMemo } from 'solid-js'
|
|
9
|
+
import { validateAgainstSource } from '../services/data-validator'
|
|
10
|
+
import type { DataValidation, DataValidationOptions } from '../types/chat-bus'
|
|
11
|
+
|
|
12
|
+
export interface UseDataValidatorOptions extends DataValidationOptions {
|
|
13
|
+
/** Disable validation (returns null) */
|
|
14
|
+
enabled?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseDataValidatorReturn {
|
|
18
|
+
/** Reactive validation result (null if disabled or no text) */
|
|
19
|
+
validation: () => DataValidation | null
|
|
20
|
+
/** Is the text valid (no hallucinations)? */
|
|
21
|
+
valid: () => boolean
|
|
22
|
+
/** Confidence score 0-1 */
|
|
23
|
+
confidence: () => number
|
|
24
|
+
/** Count of hallucinated numbers */
|
|
25
|
+
hallucinatedCount: () => number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reactive hook that validates LLM text against source data rows.
|
|
30
|
+
* Re-validates automatically when text or rows change.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const { validation, valid, confidence } = useDataValidator(
|
|
35
|
+
* () => llmResponse(),
|
|
36
|
+
* () => sourceRows(),
|
|
37
|
+
* { tolerance: 0.02, ignoreColumns: ['code_geo'] }
|
|
38
|
+
* )
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <Show when={!valid()}>
|
|
42
|
+
* <span>⚠️ {validation()!.hallucinated.length} unverified numbers</span>
|
|
43
|
+
* </Show>
|
|
44
|
+
* )
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function useDataValidator(
|
|
48
|
+
text: () => string,
|
|
49
|
+
sourceRows: () => Record<string, unknown>[],
|
|
50
|
+
options: UseDataValidatorOptions = {}
|
|
51
|
+
): UseDataValidatorReturn {
|
|
52
|
+
const { enabled = true, ...validationOptions } = options
|
|
53
|
+
|
|
54
|
+
const validation = createMemo<DataValidation | null>(() => {
|
|
55
|
+
if (!enabled) return null
|
|
56
|
+
const t = text()
|
|
57
|
+
const rows = sourceRows()
|
|
58
|
+
if (!t || !rows || rows.length === 0) return null
|
|
59
|
+
return validateAgainstSource(t, rows, validationOptions)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
validation,
|
|
64
|
+
valid: () => validation()?.valid ?? true,
|
|
65
|
+
confidence: () => validation()?.confidence ?? 1,
|
|
66
|
+
hallucinatedCount: () => validation()?.hallucinated.length ?? 0,
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,10 @@ export { ChatPrompt } from './components/ChatPrompt'
|
|
|
44
44
|
export { ScratchpadPanel } from './components/ScratchpadPanel'
|
|
45
45
|
export { dispatchScratchpad, useScratchpadState } from './stores/scratchpad-store'
|
|
46
46
|
|
|
47
|
+
// Data Verification Components (v3.1.0)
|
|
48
|
+
export { VerifiedText } from './components/VerifiedText'
|
|
49
|
+
export { DataPreviewSection } from './components/DataPreviewSection'
|
|
50
|
+
|
|
47
51
|
// Autocomplete Components
|
|
48
52
|
export { GhostText, GhostTextInput } from './components/GhostText'
|
|
49
53
|
export { AutocompleteDropdown } from './components/AutocompleteDropdown'
|
|
@@ -62,6 +66,8 @@ export type { ExpandableWrapperProps } from './components/ExpandableWrapper'
|
|
|
62
66
|
export type { ComponentToolbarProps, ToolbarAction, ToolbarIcon } from './components/ComponentToolbar'
|
|
63
67
|
export type { ChatPromptProps } from './components/ChatPrompt'
|
|
64
68
|
export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
|
|
69
|
+
export type { VerifiedTextProps } from './components/VerifiedText'
|
|
70
|
+
export type { DataPreviewSectionProps } from './components/DataPreviewSection'
|
|
65
71
|
export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
|
|
66
72
|
export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
|
|
67
73
|
export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
|
|
@@ -81,6 +87,8 @@ export {
|
|
|
81
87
|
useResize,
|
|
82
88
|
// Autocomplete hooks
|
|
83
89
|
useAutocomplete,
|
|
90
|
+
// Data Validator hooks (v3.1.0)
|
|
91
|
+
useDataValidator,
|
|
84
92
|
} from './hooks'
|
|
85
93
|
|
|
86
94
|
export type {
|
|
@@ -107,6 +115,9 @@ export type {
|
|
|
107
115
|
// Autocomplete types
|
|
108
116
|
UseAutocompleteOptions,
|
|
109
117
|
UseAutocompleteReturn,
|
|
118
|
+
// Data Validator types (v3.1.0)
|
|
119
|
+
UseDataValidatorOptions,
|
|
120
|
+
UseDataValidatorReturn,
|
|
110
121
|
} from './hooks'
|
|
111
122
|
|
|
112
123
|
// Context (Phase 5.0)
|
|
@@ -177,10 +188,14 @@ export type {
|
|
|
177
188
|
GalleryImage,
|
|
178
189
|
ImageGalleryParams,
|
|
179
190
|
VideoComponentParams,
|
|
180
|
-
// Code & Maps types (Sprint 6)
|
|
191
|
+
// Code & Maps types (Sprint 6 + v3.1.0)
|
|
181
192
|
CodeComponentParams,
|
|
182
193
|
MapMarker,
|
|
183
194
|
MapComponentParams,
|
|
195
|
+
MapPopupConfig,
|
|
196
|
+
MapGeoJSONStyle,
|
|
197
|
+
MapLayer,
|
|
198
|
+
MapPMTilesConfig,
|
|
184
199
|
// Validation options (v2.0.0)
|
|
185
200
|
IframePolicy,
|
|
186
201
|
ValidationOptions,
|
|
@@ -217,6 +232,7 @@ export {
|
|
|
217
232
|
createCommandHandler,
|
|
218
233
|
createChatBus,
|
|
219
234
|
mergeScratchpadSections,
|
|
235
|
+
validateAgainstSource,
|
|
220
236
|
} from './services'
|
|
221
237
|
|
|
222
238
|
// Chat Bus Types (v2.4.0 — @experimental)
|
|
@@ -246,4 +262,13 @@ export type {
|
|
|
246
262
|
Citation,
|
|
247
263
|
ToolCallEvent,
|
|
248
264
|
ClarificationEvent,
|
|
265
|
+
// Data Validation types (v3.1.0)
|
|
266
|
+
DataValidation,
|
|
267
|
+
LLMNumber,
|
|
268
|
+
HallucinatedNumber,
|
|
269
|
+
DataValidationOptions,
|
|
270
|
+
VerifiedTextContent,
|
|
271
|
+
DataPreviewColumn,
|
|
272
|
+
DataPreviewContent,
|
|
273
|
+
MapSectionContent,
|
|
249
274
|
} from './types/chat-bus'
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for validateAgainstSource — anti-hallucination data validator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { validateAgainstSource } from './data-validator'
|
|
7
|
+
|
|
8
|
+
const SAMPLE_ROWS = [
|
|
9
|
+
{ type: 'Appartement', ventes: 22306, prix_m2: 3337 },
|
|
10
|
+
{ type: 'Maison', ventes: 2492, prix_m2: 4230 },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('validateAgainstSource', () => {
|
|
14
|
+
it('validates text with exact source numbers', () => {
|
|
15
|
+
const text = 'On observe 22 306 ventes appartements et 3 337 EUR/m2 moyen.'
|
|
16
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
17
|
+
|
|
18
|
+
expect(result.valid).toBe(true)
|
|
19
|
+
expect(result.hallucinated).toHaveLength(0)
|
|
20
|
+
expect(result.confidence).toBe(1)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('detects hallucinated numbers', () => {
|
|
24
|
+
const text = 'On observe 18 245 ventes en 2023, prix moyen 2 850 EUR.'
|
|
25
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
26
|
+
|
|
27
|
+
expect(result.valid).toBe(false)
|
|
28
|
+
expect(result.hallucinated.length).toBeGreaterThan(0)
|
|
29
|
+
expect(result.confidence).toBeLessThan(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns hallucinated items with closest source number', () => {
|
|
33
|
+
const text = 'On observe 18 245 ventes.'
|
|
34
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
35
|
+
|
|
36
|
+
expect(result.hallucinated).toHaveLength(1)
|
|
37
|
+
expect(result.hallucinated[0].value).toBe(18245)
|
|
38
|
+
expect(result.hallucinated[0].closest).toBeDefined()
|
|
39
|
+
expect(result.hallucinated[0].distance).toBeDefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('accepts rounding within tolerance', () => {
|
|
43
|
+
// 3337 rounded to 3340 is 0.09% — within 1% tolerance
|
|
44
|
+
const text = 'Le prix moyen est de 3 340 EUR.'
|
|
45
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS, { tolerance: 0.01 })
|
|
46
|
+
|
|
47
|
+
expect(result.valid).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('rejects rounding beyond tolerance', () => {
|
|
51
|
+
// 3337 vs 3500 is ~4.9% — beyond 1% tolerance
|
|
52
|
+
const text = 'Le prix moyen est de 3 500 EUR.'
|
|
53
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS, { tolerance: 0.01 })
|
|
54
|
+
|
|
55
|
+
expect(result.valid).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('ignores years by default', () => {
|
|
59
|
+
const text = 'En 2023, on observe 22 306 ventes.'
|
|
60
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
61
|
+
|
|
62
|
+
// 2023 should be ignored, 22306 should be verified
|
|
63
|
+
expect(result.valid).toBe(true)
|
|
64
|
+
expect(result.llmNumbers).toHaveLength(1) // only 22306
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('ignores postal/INSEE codes by default', () => {
|
|
68
|
+
const text = 'Code commune 34172, on observe 22 306 ventes.'
|
|
69
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
70
|
+
|
|
71
|
+
// 34172 should be ignored (5-digit pattern)
|
|
72
|
+
expect(result.valid).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('ignores specified columns', () => {
|
|
76
|
+
const rows = [
|
|
77
|
+
{ code_geo: '34172', ventes: 22306 },
|
|
78
|
+
]
|
|
79
|
+
const text = 'Le code est 34172 avec 22 306 ventes.'
|
|
80
|
+
const result = validateAgainstSource(text, rows, {
|
|
81
|
+
ignoreColumns: ['code_geo'],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(result.valid).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('handles string-encoded numbers in source', () => {
|
|
88
|
+
const rows = [
|
|
89
|
+
{ prix: '3 337', total: '22306' },
|
|
90
|
+
]
|
|
91
|
+
const text = 'Prix 3 337, total 22 306.'
|
|
92
|
+
const result = validateAgainstSource(text, rows)
|
|
93
|
+
|
|
94
|
+
expect(result.valid).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns confidence = 1 for text with no numbers', () => {
|
|
98
|
+
const text = 'Les données sont stables et cohérentes.'
|
|
99
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
100
|
+
|
|
101
|
+
expect(result.valid).toBe(true)
|
|
102
|
+
expect(result.confidence).toBe(1)
|
|
103
|
+
expect(result.llmNumbers).toHaveLength(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('handles empty source rows', () => {
|
|
107
|
+
const text = 'On observe 22 306 ventes.'
|
|
108
|
+
const result = validateAgainstSource(text, [])
|
|
109
|
+
|
|
110
|
+
// With empty source, any number in text is hallucinated
|
|
111
|
+
expect(result.valid).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('mixes verified and hallucinated', () => {
|
|
115
|
+
const text = 'On observe 22 306 ventes reelles et 15 000 ventes estimees.'
|
|
116
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
117
|
+
|
|
118
|
+
expect(result.valid).toBe(false)
|
|
119
|
+
expect(result.hallucinated).toHaveLength(1)
|
|
120
|
+
expect(result.hallucinated[0].value).toBe(15000)
|
|
121
|
+
// 1 hallucinated out of 2 = 50% confidence
|
|
122
|
+
expect(result.confidence).toBe(0.5)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('provides context around numbers', () => {
|
|
126
|
+
const text = 'Le prix moyen est de 3 337 EUR/m2 a Montpellier.'
|
|
127
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
128
|
+
|
|
129
|
+
expect(result.llmNumbers[0].context).toContain('3 337')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('handles custom ignorePatterns', () => {
|
|
133
|
+
const text = 'Parcelle 1234 avec 22 306 ventes.'
|
|
134
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS, {
|
|
135
|
+
ignorePatterns: [/^\d{4}$/], // ignore 4-digit numbers
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(result.valid).toBe(true)
|
|
139
|
+
expect(result.llmNumbers).toHaveLength(1) // only 22306
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns sourceNumbers as a Set', () => {
|
|
143
|
+
const result = validateAgainstSource('test', SAMPLE_ROWS)
|
|
144
|
+
|
|
145
|
+
expect(result.sourceNumbers).toBeInstanceOf(Set)
|
|
146
|
+
expect(result.sourceNumbers.has(22306)).toBe(true)
|
|
147
|
+
expect(result.sourceNumbers.has(3337)).toBe(true)
|
|
148
|
+
expect(result.sourceNumbers.has(2492)).toBe(true)
|
|
149
|
+
expect(result.sourceNumbers.has(4230)).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Validator — anti-hallucination for LLM-generated text
|
|
3
|
+
* v3.1.0: Pure regex-based number verification against source data
|
|
4
|
+
*
|
|
5
|
+
* Compares numbers in LLM text to numbers in source data rows.
|
|
6
|
+
* Detects ~90% of numerical hallucinations with zero LLM cost, <1ms latency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DataValidation, DataValidationOptions, LLMNumber, HallucinatedNumber } from '../types/chat-bus'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_IGNORE_COLUMNS = new Set(['id', 'code_geo', 'code_parent'])
|
|
12
|
+
const DEFAULT_IGNORE_PATTERNS: RegExp[] = [
|
|
13
|
+
/^20[012]\d$/, // years 2000-2029
|
|
14
|
+
/^\d{5}$/, // postal codes / INSEE codes
|
|
15
|
+
/^\d{1,2}$/, // indices, ranks (1-99)
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract all numeric values from source data rows.
|
|
20
|
+
* Handles both number types and string-encoded numbers (e.g. "22 306", "3,337").
|
|
21
|
+
*/
|
|
22
|
+
function extractSourceNumbers(
|
|
23
|
+
rows: Record<string, unknown>[],
|
|
24
|
+
ignoreColumns: Set<string>
|
|
25
|
+
): Set<number> {
|
|
26
|
+
const numbers = new Set<number>()
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
for (const [col, val] of Object.entries(row)) {
|
|
29
|
+
if (ignoreColumns.has(col)) continue
|
|
30
|
+
if (typeof val === 'number' && isFinite(val)) {
|
|
31
|
+
numbers.add(val)
|
|
32
|
+
} else if (typeof val === 'string') {
|
|
33
|
+
const parsed = Number(val.replace(/\s/g, '').replace(',', '.'))
|
|
34
|
+
if (!isNaN(parsed) && isFinite(parsed)) {
|
|
35
|
+
numbers.add(parsed)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return numbers
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract all numbers from LLM text.
|
|
45
|
+
* Handles French/European formats: "22 306", "3 337", "3,337", "22306".
|
|
46
|
+
*/
|
|
47
|
+
function extractLLMNumbers(
|
|
48
|
+
text: string,
|
|
49
|
+
ignorePatterns: RegExp[]
|
|
50
|
+
): LLMNumber[] {
|
|
51
|
+
const numberRegex = /\d[\d\s,.]*\d|\d+/g
|
|
52
|
+
const results: LLMNumber[] = []
|
|
53
|
+
let match: RegExpExecArray | null
|
|
54
|
+
|
|
55
|
+
while ((match = numberRegex.exec(text)) !== null) {
|
|
56
|
+
const raw = match[0]
|
|
57
|
+
// Normalize: remove spaces/dots (thousand separators), comma → decimal point
|
|
58
|
+
const cleaned = raw.replace(/[\s.]/g, '').replace(',', '.')
|
|
59
|
+
const value = Number(cleaned)
|
|
60
|
+
|
|
61
|
+
if (isNaN(value) || !isFinite(value)) continue
|
|
62
|
+
if (ignorePatterns.some(p => p.test(raw.trim()))) continue
|
|
63
|
+
|
|
64
|
+
results.push({
|
|
65
|
+
value,
|
|
66
|
+
position: match.index,
|
|
67
|
+
context: text.slice(
|
|
68
|
+
Math.max(0, match.index - 10),
|
|
69
|
+
match.index + raw.length + 10
|
|
70
|
+
),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate LLM-generated text against source data rows.
|
|
79
|
+
*
|
|
80
|
+
* Pure function — no IO, no LLM calls, no side effects.
|
|
81
|
+
* Returns which numbers in the text are verified vs hallucinated.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const rows = [{ type: 'Appartement', ventes: 22306, prix_m2: 3337 }]
|
|
86
|
+
* const result = validateAgainstSource(
|
|
87
|
+
* "On observe 22 306 ventes à 3 337 EUR/m². En 2023, 18 245 ventes.",
|
|
88
|
+
* rows
|
|
89
|
+
* )
|
|
90
|
+
* // result.valid === false
|
|
91
|
+
* // result.hallucinated === [{ value: 18245, ... }]
|
|
92
|
+
* // result.confidence === 0.67
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function validateAgainstSource(
|
|
96
|
+
text: string,
|
|
97
|
+
sourceRows: Record<string, unknown>[],
|
|
98
|
+
options: DataValidationOptions = {}
|
|
99
|
+
): DataValidation {
|
|
100
|
+
const tolerance = options.tolerance ?? 0.01
|
|
101
|
+
const ignoreColumns = new Set(options.ignoreColumns || [...DEFAULT_IGNORE_COLUMNS])
|
|
102
|
+
const ignorePatterns = options.ignorePatterns || DEFAULT_IGNORE_PATTERNS
|
|
103
|
+
|
|
104
|
+
// 1. Extract source numbers
|
|
105
|
+
const sourceNumbers = extractSourceNumbers(sourceRows, ignoreColumns)
|
|
106
|
+
|
|
107
|
+
// 2. Extract LLM numbers
|
|
108
|
+
const llmNumbers = extractLLMNumbers(text, ignorePatterns)
|
|
109
|
+
|
|
110
|
+
// 3. Check each LLM number against source
|
|
111
|
+
const hallucinated: HallucinatedNumber[] = []
|
|
112
|
+
|
|
113
|
+
for (const num of llmNumbers) {
|
|
114
|
+
// Exact match
|
|
115
|
+
if (sourceNumbers.has(num.value)) continue
|
|
116
|
+
|
|
117
|
+
// Tolerance match (rounding)
|
|
118
|
+
let closest: number | undefined
|
|
119
|
+
let minDistance = Infinity
|
|
120
|
+
|
|
121
|
+
for (const src of sourceNumbers) {
|
|
122
|
+
const dist = Math.abs(num.value - src) / Math.max(Math.abs(src), 1)
|
|
123
|
+
if (dist < minDistance) {
|
|
124
|
+
minDistance = dist
|
|
125
|
+
closest = src
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (minDistance <= tolerance) continue // acceptable rounding
|
|
130
|
+
|
|
131
|
+
hallucinated.push({
|
|
132
|
+
...num,
|
|
133
|
+
closest,
|
|
134
|
+
distance: minDistance,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const confidence = llmNumbers.length > 0
|
|
139
|
+
? 1 - (hallucinated.length / llmNumbers.length)
|
|
140
|
+
: 1
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
valid: hallucinated.length === 0,
|
|
144
|
+
llmNumbers,
|
|
145
|
+
sourceNumbers,
|
|
146
|
+
hallucinated,
|
|
147
|
+
confidence,
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/services/index.ts
CHANGED
package/src/types/chat-bus.ts
CHANGED
|
@@ -366,7 +366,7 @@ export interface ScratchpadState {
|
|
|
366
366
|
export interface ScratchpadSection {
|
|
367
367
|
id: string
|
|
368
368
|
title: string
|
|
369
|
-
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff'
|
|
369
|
+
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff' | 'verified_text' | 'data_preview' | 'map' | 'chart'
|
|
370
370
|
content: unknown
|
|
371
371
|
/** Can the human edit this section? */
|
|
372
372
|
editable: boolean
|
|
@@ -438,3 +438,100 @@ export interface ClarificationEvent {
|
|
|
438
438
|
}>
|
|
439
439
|
original_message?: string
|
|
440
440
|
}
|
|
441
|
+
|
|
442
|
+
// ─── Data Validation (v3.1.0 — anti-hallucination) ──────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Result of validating LLM text against source data.
|
|
446
|
+
* Pure regex-based — zero LLM cost, <1ms latency.
|
|
447
|
+
*/
|
|
448
|
+
export interface DataValidation {
|
|
449
|
+
/** Is the text free of hallucinated numbers? */
|
|
450
|
+
valid: boolean
|
|
451
|
+
/** Numbers found in the LLM text */
|
|
452
|
+
llmNumbers: LLMNumber[]
|
|
453
|
+
/** Numbers present in the source data */
|
|
454
|
+
sourceNumbers: Set<number>
|
|
455
|
+
/** Numbers from the LLM NOT found in the source */
|
|
456
|
+
hallucinated: HallucinatedNumber[]
|
|
457
|
+
/** Confidence score 0-1 (1 = all numbers verified) */
|
|
458
|
+
confidence: number
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface LLMNumber {
|
|
462
|
+
value: number
|
|
463
|
+
/** Character index in the text */
|
|
464
|
+
position: number
|
|
465
|
+
/** ~20 chars surrounding context */
|
|
466
|
+
context: string
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface HallucinatedNumber extends LLMNumber {
|
|
470
|
+
/** Closest number in source data */
|
|
471
|
+
closest?: number
|
|
472
|
+
/** Distance as ratio (0.18 = 18% off) */
|
|
473
|
+
distance?: number
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Options for validateAgainstSource */
|
|
477
|
+
export interface DataValidationOptions {
|
|
478
|
+
/** Tolerance for rounding (default: 0.01 = 1%) */
|
|
479
|
+
tolerance?: number
|
|
480
|
+
/** Columns to ignore (e.g. 'id', 'code_geo') */
|
|
481
|
+
ignoreColumns?: string[]
|
|
482
|
+
/** Number patterns to ignore (e.g. years, postal codes) */
|
|
483
|
+
ignorePatterns?: RegExp[]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Content for verified_text scratchpad section */
|
|
487
|
+
export interface VerifiedTextContent {
|
|
488
|
+
/** Original LLM text */
|
|
489
|
+
text: string
|
|
490
|
+
/** Validation result from validateAgainstSource */
|
|
491
|
+
validation: DataValidation
|
|
492
|
+
/** Display mode */
|
|
493
|
+
mode?: 'highlight' | 'strip' | 'annotate'
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Column definition for data_preview section */
|
|
497
|
+
export interface DataPreviewColumn {
|
|
498
|
+
key: string
|
|
499
|
+
label: string
|
|
500
|
+
type?: 'number' | 'string' | 'date'
|
|
501
|
+
format?: string
|
|
502
|
+
align?: 'left' | 'right' | 'center'
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Content for data_preview scratchpad section */
|
|
506
|
+
export interface DataPreviewContent {
|
|
507
|
+
columns: DataPreviewColumn[]
|
|
508
|
+
rows: Record<string, unknown>[]
|
|
509
|
+
/** Total rows (if paginated — e.g. 22306 total, 30 displayed) */
|
|
510
|
+
totalRows?: number
|
|
511
|
+
/** Data source attribution */
|
|
512
|
+
source?: string
|
|
513
|
+
/** Data freshness label */
|
|
514
|
+
freshness?: string
|
|
515
|
+
/** Enable export buttons (CSV/JSON) */
|
|
516
|
+
exportable?: boolean
|
|
517
|
+
/** Rows per page (default: 25) */
|
|
518
|
+
pageSize?: number
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Content for map scratchpad section (v3.1.0) */
|
|
522
|
+
export interface MapSectionContent {
|
|
523
|
+
/** GeoJSON FeatureCollection */
|
|
524
|
+
geojson: unknown
|
|
525
|
+
/** Map center [lat, lng] */
|
|
526
|
+
center?: [number, number]
|
|
527
|
+
/** Zoom level */
|
|
528
|
+
zoom?: number
|
|
529
|
+
/** GeoJSON style (including choropleth) */
|
|
530
|
+
style?: import('./index').MapGeoJSONStyle
|
|
531
|
+
/** Popup config for feature click */
|
|
532
|
+
popup?: import('./index').MapPopupConfig
|
|
533
|
+
/** Named layers */
|
|
534
|
+
layers?: import('./index').MapLayer[]
|
|
535
|
+
/** Map height (CSS, default: '300px') */
|
|
536
|
+
height?: string
|
|
537
|
+
}
|