@trustgraph/react-state 1.5.3 → 1.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/dist/index.cjs +922 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +810 -23
- package/dist/index.esm.js.map +1 -1
- package/dist/model/message.d.ts +1 -0
- package/dist/model/message.d.ts.map +1 -1
- package/dist/model/settings-types.d.ts +1 -0
- package/dist/model/settings-types.d.ts.map +1 -1
- package/dist/state/chat-session.d.ts.map +1 -1
- package/dist/state/conversation.d.ts +2 -2
- package/dist/state/conversation.d.ts.map +1 -1
- package/dist/state/document-metadata.d.ts +37 -0
- package/dist/state/document-metadata.d.ts.map +1 -0
- package/dist/state/explainability-store.d.ts +21 -0
- package/dist/state/explainability-store.d.ts.map +1 -0
- package/dist/state/explainability.d.ts +43 -0
- package/dist/state/explainability.d.ts.map +1 -0
- package/dist/state/inference.d.ts +19 -1
- package/dist/state/inference.d.ts.map +1 -1
- package/dist/state/provenance.d.ts +28 -0
- package/dist/state/provenance.d.ts.map +1 -0
- package/dist/utils/explainability.d.ts +120 -0
- package/dist/utils/explainability.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.esm.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useSocket, useConnectionState } from '@trustgraph/react-provider';
|
|
2
2
|
export { ConnectionStateContext, SocketContext, SocketProvider } from '@trustgraph/react-provider';
|
|
3
|
+
import { TG_EDGE, TG_REASONING, TG_EDGE_COUNT, TG_SELECTED_EDGE, TG_QUERY, PROV_STARTED_AT_TIME, TG_CONTENT, RDFS_LABEL as RDFS_LABEL$2, PROV_WAS_DERIVED_FROM, TG_REIFIES } from '@trustgraph/client';
|
|
4
|
+
export { PROV, PROV_ACTIVITY, PROV_ENTITY, PROV_STARTED_AT_TIME, PROV_WAS_DERIVED_FROM, PROV_WAS_GENERATED_BY, RDF, RDFS, RDF_TYPE, SCHEMA, SCHEMA_AUTHOR, SCHEMA_DESCRIPTION, SCHEMA_KEYWORDS, SCHEMA_NAME, SKOS, SKOS_DEFINITION, TG, TG_CONTENT, TG_DOCUMENT, TG_EDGE, TG_EDGE_COUNT, TG_QUERY, TG_REASONING, TG_REIFIES, TG_SELECTED_EDGE } from '@trustgraph/client';
|
|
3
5
|
import { jsx } from 'react/jsx-runtime';
|
|
4
|
-
import { createContext, useContext, useEffect, useState,
|
|
6
|
+
import { createContext, useContext, useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
5
7
|
import { create } from 'zustand';
|
|
6
8
|
import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
|
|
7
9
|
import { v4 } from 'uuid';
|
|
@@ -155,23 +157,25 @@ const useConversation = create()((set) => ({
|
|
|
155
157
|
setMessages: (v) => set(() => ({
|
|
156
158
|
messages: v,
|
|
157
159
|
})),
|
|
158
|
-
addMessage: (role, text, type) => set((state) => ({
|
|
160
|
+
addMessage: (role, text, type, explainSessionId) => set((state) => ({
|
|
159
161
|
messages: [
|
|
160
162
|
...state.messages,
|
|
161
163
|
{
|
|
162
164
|
role: role,
|
|
163
165
|
text: text,
|
|
164
166
|
type: type || "normal",
|
|
167
|
+
explainSessionId,
|
|
165
168
|
},
|
|
166
169
|
],
|
|
167
170
|
})),
|
|
168
|
-
updateLastMessage: (text) => set((state) => {
|
|
171
|
+
updateLastMessage: (text, explainSessionId) => set((state) => {
|
|
169
172
|
if (state.messages.length === 0)
|
|
170
173
|
return state;
|
|
171
174
|
const messages = [...state.messages];
|
|
172
175
|
messages[messages.length - 1] = {
|
|
173
176
|
...messages[messages.length - 1],
|
|
174
177
|
text: text,
|
|
178
|
+
...(explainSessionId !== undefined && { explainSessionId }),
|
|
175
179
|
};
|
|
176
180
|
return { messages };
|
|
177
181
|
}),
|
|
@@ -311,6 +315,7 @@ const DEFAULT_SETTINGS = {
|
|
|
311
315
|
flowBlueprintEditor: false, // Off by default - experimental feature
|
|
312
316
|
structuredQuery: false, // Off by default
|
|
313
317
|
llmModels: false, // Off by default
|
|
318
|
+
explainability: true, // On by default
|
|
314
319
|
},
|
|
315
320
|
};
|
|
316
321
|
const SETTINGS_STORAGE_KEY = "trustgraph-settings";
|
|
@@ -967,7 +972,7 @@ const useTriples = ({ flow, s, p, o, limit, collection }) => {
|
|
|
967
972
|
};
|
|
968
973
|
|
|
969
974
|
// Helper to get the string value from a Term (IRI or Literal)
|
|
970
|
-
const getTermValue$
|
|
975
|
+
const getTermValue$3 = (term) => {
|
|
971
976
|
if (term.t === "i")
|
|
972
977
|
return term.i;
|
|
973
978
|
if (term.t === "l")
|
|
@@ -1086,7 +1091,7 @@ const queryLabel = (socket, uri, add, remove, collection) => {
|
|
|
1086
1091
|
// If got a result, return the label, otherwise the URI
|
|
1087
1092
|
// can be its own label
|
|
1088
1093
|
if (triples.length > 0)
|
|
1089
|
-
return getTermValue$
|
|
1094
|
+
return getTermValue$3(triples[0].o);
|
|
1090
1095
|
else
|
|
1091
1096
|
return uri;
|
|
1092
1097
|
})
|
|
@@ -1103,7 +1108,7 @@ const queryLabel = (socket, uri, add, remove, collection) => {
|
|
|
1103
1108
|
// Returns a promise
|
|
1104
1109
|
const labelS = (socket, triples, add, remove, collection) => {
|
|
1105
1110
|
return Promise.all(triples.map((t) => {
|
|
1106
|
-
return queryLabel(socket, getTermValue$
|
|
1111
|
+
return queryLabel(socket, getTermValue$3(t.s), add, remove, collection).then((label) => {
|
|
1107
1112
|
return {
|
|
1108
1113
|
...t,
|
|
1109
1114
|
s: {
|
|
@@ -1118,7 +1123,7 @@ const labelS = (socket, triples, add, remove, collection) => {
|
|
|
1118
1123
|
// Returns a promise
|
|
1119
1124
|
const labelP = (socket, triples, add, remove, collection) => {
|
|
1120
1125
|
return Promise.all(triples.map((t) => {
|
|
1121
|
-
return queryLabel(socket, getTermValue$
|
|
1126
|
+
return queryLabel(socket, getTermValue$3(t.p), add, remove, collection).then((label) => {
|
|
1122
1127
|
return {
|
|
1123
1128
|
...t,
|
|
1124
1129
|
p: {
|
|
@@ -1151,7 +1156,7 @@ const labelO = (socket, triples, add, remove, collection) => {
|
|
|
1151
1156
|
...t,
|
|
1152
1157
|
o: {
|
|
1153
1158
|
...t.o,
|
|
1154
|
-
label: getTermValue$
|
|
1159
|
+
label: getTermValue$3(t.o),
|
|
1155
1160
|
},
|
|
1156
1161
|
});
|
|
1157
1162
|
});
|
|
@@ -1187,7 +1192,7 @@ const getTriples = (socket, uri, add, remove, limit, collection) => {
|
|
|
1187
1192
|
// Functionality here helps construct subgraphs for react-force-graph
|
|
1188
1193
|
// visualisation
|
|
1189
1194
|
// Helper to get the string value from a Term (IRI or Literal)
|
|
1190
|
-
const getTermValue$
|
|
1195
|
+
const getTermValue$2 = (term) => {
|
|
1191
1196
|
if (term.t === "i")
|
|
1192
1197
|
return term.i;
|
|
1193
1198
|
if (term.t === "l")
|
|
@@ -1215,11 +1220,11 @@ const updateSubgraphTriples = (sg, triples) => {
|
|
|
1215
1220
|
continue;
|
|
1216
1221
|
}
|
|
1217
1222
|
// Source has a URI, that can be its unique ID
|
|
1218
|
-
const sourceId = getTermValue$
|
|
1223
|
+
const sourceId = getTermValue$2(t.s);
|
|
1219
1224
|
// Target is always an entity now (we filtered out literals above)
|
|
1220
|
-
const targetId = getTermValue$
|
|
1225
|
+
const targetId = getTermValue$2(t.o);
|
|
1221
1226
|
// Links have an ID so that this edge is unique
|
|
1222
|
-
const linkId = getTermValue$
|
|
1227
|
+
const linkId = getTermValue$2(t.s) + "@@" + getTermValue$2(t.p) + "@@" + getTermValue$2(t.o);
|
|
1223
1228
|
if (!nodeIds.has(sourceId)) {
|
|
1224
1229
|
const sLabeled = t.s;
|
|
1225
1230
|
const n = {
|
|
@@ -1730,10 +1735,14 @@ const useInference = ({ flow } = {}) => {
|
|
|
1730
1735
|
// Use explicit param if provided, otherwise fall back to session state
|
|
1731
1736
|
const effectiveFlow = flow ?? sessionFlowId;
|
|
1732
1737
|
/**
|
|
1733
|
-
* Graph RAG inference with entity discovery
|
|
1738
|
+
* Graph RAG inference with entity discovery and optional explainability
|
|
1739
|
+
* Explainability events are only tracked if callbacks.onExplain is provided
|
|
1734
1740
|
*/
|
|
1735
1741
|
const graphRagMutation = useMutation({
|
|
1736
1742
|
mutationFn: async ({ input, options, collection, callbacks, }) => {
|
|
1743
|
+
// Only collect explain events if caller provided onExplain callback
|
|
1744
|
+
const wantsExplainability = !!callbacks?.onExplain;
|
|
1745
|
+
const explainEvents = wantsExplainability ? [] : undefined;
|
|
1737
1746
|
// If callbacks provided, use streaming API
|
|
1738
1747
|
const response = callbacks
|
|
1739
1748
|
? await new Promise((resolve, reject) => {
|
|
@@ -1749,9 +1758,16 @@ const useInference = ({ flow } = {}) => {
|
|
|
1749
1758
|
callbacks?.onError?.(error);
|
|
1750
1759
|
reject(new Error(error));
|
|
1751
1760
|
};
|
|
1761
|
+
// Only wire up onExplain if caller wants explainability
|
|
1762
|
+
const onExplain = wantsExplainability
|
|
1763
|
+
? (event) => {
|
|
1764
|
+
explainEvents.push(event);
|
|
1765
|
+
callbacks.onExplain(event);
|
|
1766
|
+
}
|
|
1767
|
+
: undefined;
|
|
1752
1768
|
socket
|
|
1753
1769
|
.flow(effectiveFlow)
|
|
1754
|
-
.graphRagStreaming(input, onChunk, onError, options, collection);
|
|
1770
|
+
.graphRagStreaming(input, onChunk, onError, options, collection, onExplain);
|
|
1755
1771
|
})
|
|
1756
1772
|
: await socket.flow(effectiveFlow).graphRag(input, options || {}, collection);
|
|
1757
1773
|
// Get embeddings for entity discovery
|
|
@@ -1761,7 +1777,7 @@ const useInference = ({ flow } = {}) => {
|
|
|
1761
1777
|
const entities = await socket
|
|
1762
1778
|
.flow(effectiveFlow)
|
|
1763
1779
|
.graphEmbeddingsQuery(embeddings, options?.entityLimit || 10, collection);
|
|
1764
|
-
return { response, entities };
|
|
1780
|
+
return { response, entities, explainEvents };
|
|
1765
1781
|
},
|
|
1766
1782
|
});
|
|
1767
1783
|
/**
|
|
@@ -1792,10 +1808,45 @@ const useInference = ({ flow } = {}) => {
|
|
|
1792
1808
|
},
|
|
1793
1809
|
});
|
|
1794
1810
|
/**
|
|
1795
|
-
*
|
|
1811
|
+
* Document RAG inference with optional explainability
|
|
1812
|
+
*/
|
|
1813
|
+
const documentRagMutation = useMutation({
|
|
1814
|
+
mutationFn: async ({ input, collection, docLimit, callbacks, }) => {
|
|
1815
|
+
const wantsExplainability = !!callbacks?.onExplain;
|
|
1816
|
+
const explainEvents = wantsExplainability ? [] : undefined;
|
|
1817
|
+
const response = await new Promise((resolve, reject) => {
|
|
1818
|
+
let accumulated = "";
|
|
1819
|
+
const onChunk = (chunk, complete) => {
|
|
1820
|
+
accumulated += chunk;
|
|
1821
|
+
callbacks?.onChunk?.(chunk, complete);
|
|
1822
|
+
if (complete) {
|
|
1823
|
+
resolve(accumulated);
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
const onError = (error) => {
|
|
1827
|
+
callbacks?.onError?.(error);
|
|
1828
|
+
reject(new Error(error));
|
|
1829
|
+
};
|
|
1830
|
+
const onExplain = wantsExplainability
|
|
1831
|
+
? (event) => {
|
|
1832
|
+
explainEvents.push(event);
|
|
1833
|
+
callbacks.onExplain(event);
|
|
1834
|
+
}
|
|
1835
|
+
: undefined;
|
|
1836
|
+
socket
|
|
1837
|
+
.flow(effectiveFlow)
|
|
1838
|
+
.documentRagStreaming(input, onChunk, onError, docLimit, collection, onExplain);
|
|
1839
|
+
});
|
|
1840
|
+
return { response, explainEvents };
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1843
|
+
/**
|
|
1844
|
+
* Agent inference with streaming callbacks and optional explainability
|
|
1796
1845
|
*/
|
|
1797
1846
|
const agentMutation = useMutation({
|
|
1798
1847
|
mutationFn: async ({ input, callbacks, }) => {
|
|
1848
|
+
const wantsExplainability = !!callbacks?.onExplain;
|
|
1849
|
+
const explainEvents = wantsExplainability ? [] : undefined;
|
|
1799
1850
|
return new Promise((resolve, reject) => {
|
|
1800
1851
|
let fullAnswer = "";
|
|
1801
1852
|
const onThink = (thought, complete) => {
|
|
@@ -1815,22 +1866,644 @@ const useInference = ({ flow } = {}) => {
|
|
|
1815
1866
|
callbacks?.onError?.(error);
|
|
1816
1867
|
reject(new Error(error));
|
|
1817
1868
|
};
|
|
1869
|
+
const onExplain = wantsExplainability
|
|
1870
|
+
? (event) => {
|
|
1871
|
+
explainEvents.push(event);
|
|
1872
|
+
callbacks.onExplain(event);
|
|
1873
|
+
}
|
|
1874
|
+
: undefined;
|
|
1818
1875
|
socket
|
|
1819
1876
|
.flow(effectiveFlow)
|
|
1820
|
-
.agent(input, onThink, onObserve, onAnswer, onError);
|
|
1877
|
+
.agent(input, onThink, onObserve, onAnswer, onError, onExplain);
|
|
1821
1878
|
});
|
|
1822
1879
|
},
|
|
1823
1880
|
});
|
|
1824
1881
|
return {
|
|
1825
1882
|
graphRag: graphRagMutation.mutateAsync,
|
|
1883
|
+
documentRag: documentRagMutation.mutateAsync,
|
|
1826
1884
|
textCompletion: textCompletionMutation.mutateAsync,
|
|
1827
1885
|
agent: agentMutation.mutateAsync,
|
|
1828
1886
|
isLoading: graphRagMutation.isPending ||
|
|
1887
|
+
documentRagMutation.isPending ||
|
|
1829
1888
|
textCompletionMutation.isPending ||
|
|
1830
1889
|
agentMutation.isPending,
|
|
1831
1890
|
};
|
|
1832
1891
|
};
|
|
1833
1892
|
|
|
1893
|
+
/**
|
|
1894
|
+
* Zustand store for managing explainability sessions
|
|
1895
|
+
* Sessions are keyed by ID and linked to messages via explainSessionId
|
|
1896
|
+
*/
|
|
1897
|
+
const useExplainabilityStore = create()((set, get) => ({
|
|
1898
|
+
sessions: {},
|
|
1899
|
+
addSession: (id, session) => set((state) => ({
|
|
1900
|
+
sessions: {
|
|
1901
|
+
...state.sessions,
|
|
1902
|
+
[id]: session,
|
|
1903
|
+
},
|
|
1904
|
+
})),
|
|
1905
|
+
updateSession: (id, partial) => set((state) => ({
|
|
1906
|
+
sessions: {
|
|
1907
|
+
...state.sessions,
|
|
1908
|
+
[id]: {
|
|
1909
|
+
...state.sessions[id],
|
|
1910
|
+
...partial,
|
|
1911
|
+
},
|
|
1912
|
+
},
|
|
1913
|
+
})),
|
|
1914
|
+
getSession: (id) => get().sessions[id],
|
|
1915
|
+
removeSession: (id) => set((state) => {
|
|
1916
|
+
const { [id]: _, ...rest } = state.sessions;
|
|
1917
|
+
return { sessions: rest };
|
|
1918
|
+
}),
|
|
1919
|
+
clearSessions: () => set({ sessions: {} }),
|
|
1920
|
+
}));
|
|
1921
|
+
|
|
1922
|
+
/**
|
|
1923
|
+
* Explainability utilities for parsing and structuring explain events
|
|
1924
|
+
*/
|
|
1925
|
+
/**
|
|
1926
|
+
* Extract event type from explainId URI
|
|
1927
|
+
* e.g., "urn:trustgraph:question:abc123" → "question"
|
|
1928
|
+
*/
|
|
1929
|
+
function getEventType(explainId) {
|
|
1930
|
+
if (explainId.includes("question"))
|
|
1931
|
+
return "question";
|
|
1932
|
+
if (explainId.includes("exploration"))
|
|
1933
|
+
return "exploration";
|
|
1934
|
+
if (explainId.includes("focus"))
|
|
1935
|
+
return "focus";
|
|
1936
|
+
if (explainId.includes("synthesis"))
|
|
1937
|
+
return "synthesis";
|
|
1938
|
+
return "unknown";
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Get term value from a Term object
|
|
1942
|
+
*/
|
|
1943
|
+
function getTermValue$1(term) {
|
|
1944
|
+
if (!term)
|
|
1945
|
+
return "";
|
|
1946
|
+
if (term.t === "i")
|
|
1947
|
+
return term.i || "";
|
|
1948
|
+
if (term.t === "l")
|
|
1949
|
+
return term.v || "";
|
|
1950
|
+
if (term.t === "t" && term.tr) {
|
|
1951
|
+
// Quoted triple - return a serialized form
|
|
1952
|
+
const s = getTermValue$1(term.tr.s);
|
|
1953
|
+
const p = getTermValue$1(term.tr.p);
|
|
1954
|
+
const o = getTermValue$1(term.tr.o);
|
|
1955
|
+
return `<<${s} ${p} ${o}>>`;
|
|
1956
|
+
}
|
|
1957
|
+
return "";
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Extract quoted triple from a Term
|
|
1961
|
+
*/
|
|
1962
|
+
function extractQuotedTriple(term) {
|
|
1963
|
+
if (term.t === "t" && term.tr) {
|
|
1964
|
+
return {
|
|
1965
|
+
s: getTermValue$1(term.tr.s),
|
|
1966
|
+
p: getTermValue$1(term.tr.p),
|
|
1967
|
+
o: getTermValue$1(term.tr.o),
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
return null;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Parse triples for a question event
|
|
1974
|
+
*/
|
|
1975
|
+
function parseQuestionTriples(explainId, explainGraph, triples) {
|
|
1976
|
+
const event = {
|
|
1977
|
+
type: "question",
|
|
1978
|
+
explainId,
|
|
1979
|
+
explainGraph,
|
|
1980
|
+
};
|
|
1981
|
+
for (const triple of triples) {
|
|
1982
|
+
const p = getTermValue$1(triple.p);
|
|
1983
|
+
const o = getTermValue$1(triple.o);
|
|
1984
|
+
if (p === TG_QUERY) {
|
|
1985
|
+
event.query = o;
|
|
1986
|
+
}
|
|
1987
|
+
else if (p === PROV_STARTED_AT_TIME) {
|
|
1988
|
+
event.timestamp = o;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return event;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Parse triples for an exploration event
|
|
1995
|
+
*/
|
|
1996
|
+
function parseExplorationTriples(explainId, explainGraph, triples) {
|
|
1997
|
+
const event = {
|
|
1998
|
+
type: "exploration",
|
|
1999
|
+
explainId,
|
|
2000
|
+
explainGraph,
|
|
2001
|
+
};
|
|
2002
|
+
for (const triple of triples) {
|
|
2003
|
+
const p = getTermValue$1(triple.p);
|
|
2004
|
+
const o = getTermValue$1(triple.o);
|
|
2005
|
+
if (p === TG_EDGE_COUNT) {
|
|
2006
|
+
event.edgeCount = parseInt(o, 10);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return event;
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Parse triples for a focus event
|
|
2013
|
+
*/
|
|
2014
|
+
function parseFocusTriples(explainId, explainGraph, triples) {
|
|
2015
|
+
const event = {
|
|
2016
|
+
type: "focus",
|
|
2017
|
+
explainId,
|
|
2018
|
+
explainGraph,
|
|
2019
|
+
edgeSelectionUris: [],
|
|
2020
|
+
};
|
|
2021
|
+
for (const triple of triples) {
|
|
2022
|
+
const p = getTermValue$1(triple.p);
|
|
2023
|
+
const o = getTermValue$1(triple.o);
|
|
2024
|
+
if (p === TG_SELECTED_EDGE && typeof o === "string") {
|
|
2025
|
+
event.edgeSelectionUris.push(o);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return event;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Parse triples for a synthesis event
|
|
2032
|
+
*/
|
|
2033
|
+
function parseSynthesisTriples(explainId, explainGraph, triples) {
|
|
2034
|
+
const event = {
|
|
2035
|
+
type: "synthesis",
|
|
2036
|
+
explainId,
|
|
2037
|
+
explainGraph,
|
|
2038
|
+
};
|
|
2039
|
+
for (const triple of triples) {
|
|
2040
|
+
const p = getTermValue$1(triple.p);
|
|
2041
|
+
const o = getTermValue$1(triple.o);
|
|
2042
|
+
if (p === TG_CONTENT) {
|
|
2043
|
+
event.contentLength = o.length;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return event;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Parse triples for an edge selection entity
|
|
2050
|
+
*/
|
|
2051
|
+
function parseEdgeSelectionTriples(triples) {
|
|
2052
|
+
let edge = null;
|
|
2053
|
+
let reasoning = null;
|
|
2054
|
+
for (const triple of triples) {
|
|
2055
|
+
const p = getTermValue$1(triple.p);
|
|
2056
|
+
if (p === TG_EDGE) {
|
|
2057
|
+
edge = extractQuotedTriple(triple.o);
|
|
2058
|
+
}
|
|
2059
|
+
else if (p === TG_REASONING) {
|
|
2060
|
+
reasoning = getTermValue$1(triple.o);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return { edge, reasoning };
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Parse triples based on event type
|
|
2067
|
+
*/
|
|
2068
|
+
function parseExplainTriples(explainId, explainGraph, triples) {
|
|
2069
|
+
const eventType = getEventType(explainId);
|
|
2070
|
+
switch (eventType) {
|
|
2071
|
+
case "question":
|
|
2072
|
+
return parseQuestionTriples(explainId, explainGraph, triples);
|
|
2073
|
+
case "exploration":
|
|
2074
|
+
return parseExplorationTriples(explainId, explainGraph, triples);
|
|
2075
|
+
case "focus":
|
|
2076
|
+
return parseFocusTriples(explainId, explainGraph, triples);
|
|
2077
|
+
case "synthesis":
|
|
2078
|
+
return parseSynthesisTriples(explainId, explainGraph, triples);
|
|
2079
|
+
default:
|
|
2080
|
+
return null;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* Hook for tracing provenance chains in the knowledge graph
|
|
2086
|
+
* Follows prov:wasDerivedFrom relationships from any entity to its source documents
|
|
2087
|
+
*/
|
|
2088
|
+
/**
|
|
2089
|
+
* Hook for tracing provenance chains
|
|
2090
|
+
*/
|
|
2091
|
+
const useProvenance = (options = {}) => {
|
|
2092
|
+
const { flow, collection = "default", maxDepth = 10 } = options;
|
|
2093
|
+
const socket = useSocket();
|
|
2094
|
+
const connectionState = useConnectionState();
|
|
2095
|
+
const sessionFlowId = useSessionStore((state) => state.flowId);
|
|
2096
|
+
const effectiveFlow = flow ?? sessionFlowId;
|
|
2097
|
+
const [isTracing, setIsTracing] = useState(false);
|
|
2098
|
+
// Label cache to avoid repeated queries
|
|
2099
|
+
const labelCacheRef = useRef(new Map());
|
|
2100
|
+
/**
|
|
2101
|
+
* Check if connected
|
|
2102
|
+
*/
|
|
2103
|
+
const isConnected = useCallback(() => {
|
|
2104
|
+
return (connectionState?.status === "authenticated" ||
|
|
2105
|
+
connectionState?.status === "unauthenticated");
|
|
2106
|
+
}, [connectionState]);
|
|
2107
|
+
/**
|
|
2108
|
+
* Query for rdfs:label of a URI
|
|
2109
|
+
*/
|
|
2110
|
+
const resolveLabel = useCallback(async (uri) => {
|
|
2111
|
+
// Check cache
|
|
2112
|
+
if (labelCacheRef.current.has(uri)) {
|
|
2113
|
+
return labelCacheRef.current.get(uri);
|
|
2114
|
+
}
|
|
2115
|
+
// Not an IRI - return as-is
|
|
2116
|
+
if (!uri.startsWith("http") && !uri.startsWith("urn:")) {
|
|
2117
|
+
return uri;
|
|
2118
|
+
}
|
|
2119
|
+
if (!isConnected()) {
|
|
2120
|
+
return uri;
|
|
2121
|
+
}
|
|
2122
|
+
try {
|
|
2123
|
+
const triples = await socket
|
|
2124
|
+
.flow(effectiveFlow)
|
|
2125
|
+
.triplesQuery({ t: "i", i: uri }, { t: "i", i: RDFS_LABEL$2 }, undefined, 1, collection);
|
|
2126
|
+
const label = triples.length > 0 ? getTermValue$1(triples[0].o) : uri;
|
|
2127
|
+
// Cache the result
|
|
2128
|
+
labelCacheRef.current.set(uri, label);
|
|
2129
|
+
return label;
|
|
2130
|
+
}
|
|
2131
|
+
catch {
|
|
2132
|
+
return uri;
|
|
2133
|
+
}
|
|
2134
|
+
}, [socket, effectiveFlow, collection, isConnected]);
|
|
2135
|
+
/**
|
|
2136
|
+
* Query for prov:wasDerivedFrom parent of a URI
|
|
2137
|
+
*/
|
|
2138
|
+
const queryDerivedFrom = useCallback(async (uri) => {
|
|
2139
|
+
if (!isConnected())
|
|
2140
|
+
return null;
|
|
2141
|
+
try {
|
|
2142
|
+
const triples = await socket
|
|
2143
|
+
.flow(effectiveFlow)
|
|
2144
|
+
.triplesQuery({ t: "i", i: uri }, { t: "i", i: PROV_WAS_DERIVED_FROM }, undefined, 1, collection);
|
|
2145
|
+
if (triples.length > 0) {
|
|
2146
|
+
return getTermValue$1(triples[0].o);
|
|
2147
|
+
}
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
catch {
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
}, [socket, effectiveFlow, collection, isConnected]);
|
|
2154
|
+
/**
|
|
2155
|
+
* Trace the full provenance chain from a URI to root
|
|
2156
|
+
*/
|
|
2157
|
+
const traceChain = useCallback(async (uri) => {
|
|
2158
|
+
setIsTracing(true);
|
|
2159
|
+
try {
|
|
2160
|
+
const chain = [];
|
|
2161
|
+
let current = uri;
|
|
2162
|
+
for (let depth = 0; depth < maxDepth && current; depth++) {
|
|
2163
|
+
const label = await resolveLabel(current);
|
|
2164
|
+
chain.push({ uri: current, label });
|
|
2165
|
+
const parent = await queryDerivedFrom(current);
|
|
2166
|
+
if (!parent || parent === current) {
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
current = parent;
|
|
2170
|
+
}
|
|
2171
|
+
// The last item in the chain is the root document
|
|
2172
|
+
const rootItem = chain.length > 0 ? chain[chain.length - 1] : undefined;
|
|
2173
|
+
return {
|
|
2174
|
+
chain,
|
|
2175
|
+
documentUri: rootItem?.uri,
|
|
2176
|
+
documentLabel: rootItem?.label,
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
finally {
|
|
2180
|
+
setIsTracing(false);
|
|
2181
|
+
}
|
|
2182
|
+
}, [maxDepth, resolveLabel, queryDerivedFrom]);
|
|
2183
|
+
/**
|
|
2184
|
+
* Query for statements that reify an edge via tg:reifies
|
|
2185
|
+
*/
|
|
2186
|
+
const queryReifyingStatements = useCallback(async (s, p, o) => {
|
|
2187
|
+
if (!isConnected())
|
|
2188
|
+
return [];
|
|
2189
|
+
try {
|
|
2190
|
+
// Build the quoted triple term
|
|
2191
|
+
const quotedTriple = {
|
|
2192
|
+
t: "t",
|
|
2193
|
+
tr: {
|
|
2194
|
+
s: { t: "i", i: s },
|
|
2195
|
+
p: { t: "i", i: p },
|
|
2196
|
+
o: o.startsWith("http") || o.startsWith("urn:")
|
|
2197
|
+
? { t: "i", i: o }
|
|
2198
|
+
: { t: "l", v: o },
|
|
2199
|
+
},
|
|
2200
|
+
};
|
|
2201
|
+
const triples = await socket
|
|
2202
|
+
.flow(effectiveFlow)
|
|
2203
|
+
.triplesQuery(undefined, { t: "i", i: TG_REIFIES }, quotedTriple, 10, collection);
|
|
2204
|
+
return triples.map((t) => getTermValue$1(t.s));
|
|
2205
|
+
}
|
|
2206
|
+
catch {
|
|
2207
|
+
return [];
|
|
2208
|
+
}
|
|
2209
|
+
}, [socket, effectiveFlow, collection, isConnected]);
|
|
2210
|
+
/**
|
|
2211
|
+
* Trace provenance for an edge - finds reifying statements and traces each
|
|
2212
|
+
*/
|
|
2213
|
+
const traceEdgeProvenance = useCallback(async (s, p, o) => {
|
|
2214
|
+
setIsTracing(true);
|
|
2215
|
+
try {
|
|
2216
|
+
// Find statements that reify this edge
|
|
2217
|
+
const stmtUris = await queryReifyingStatements(s, p, o);
|
|
2218
|
+
// For each reifying statement, trace its provenance chain
|
|
2219
|
+
const chains = [];
|
|
2220
|
+
for (const stmtUri of stmtUris) {
|
|
2221
|
+
// Get the wasDerivedFrom source for this statement
|
|
2222
|
+
const sourceUri = await queryDerivedFrom(stmtUri);
|
|
2223
|
+
if (sourceUri) {
|
|
2224
|
+
const chain = await traceChain(sourceUri);
|
|
2225
|
+
chains.push(chain);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return chains;
|
|
2229
|
+
}
|
|
2230
|
+
finally {
|
|
2231
|
+
setIsTracing(false);
|
|
2232
|
+
}
|
|
2233
|
+
}, [queryReifyingStatements, queryDerivedFrom, traceChain]);
|
|
2234
|
+
/**
|
|
2235
|
+
* Clear the label cache
|
|
2236
|
+
*/
|
|
2237
|
+
const clearCache = useCallback(() => {
|
|
2238
|
+
labelCacheRef.current.clear();
|
|
2239
|
+
}, []);
|
|
2240
|
+
return {
|
|
2241
|
+
traceChain,
|
|
2242
|
+
traceEdgeProvenance,
|
|
2243
|
+
resolveLabel,
|
|
2244
|
+
clearCache,
|
|
2245
|
+
isTracing,
|
|
2246
|
+
};
|
|
2247
|
+
};
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* Hook for managing explainability state during GraphRAG queries
|
|
2251
|
+
* Unpacks explain events into structured data with provenance chains
|
|
2252
|
+
*
|
|
2253
|
+
* Processing strategy:
|
|
2254
|
+
* - Events are processed immediately as they arrive
|
|
2255
|
+
* - Main event nodes (question, exploration, focus, synthesis) use
|
|
2256
|
+
* stability-based retry: fetch until count > 0 AND stable
|
|
2257
|
+
* - Edge sub-objects use simple retry-until-non-empty (fast)
|
|
2258
|
+
* - All edge fetching + label resolution + provenance runs in parallel
|
|
2259
|
+
*/
|
|
2260
|
+
/**
|
|
2261
|
+
* Hook for managing explainability during inference
|
|
2262
|
+
*/
|
|
2263
|
+
const useExplainability = (options = {}) => {
|
|
2264
|
+
const { flow, collection = "default", traceProvenance = true, } = options;
|
|
2265
|
+
const socket = useSocket();
|
|
2266
|
+
const connectionState = useConnectionState();
|
|
2267
|
+
const sessionFlowId = useSessionStore((state) => state.flowId);
|
|
2268
|
+
const effectiveFlow = flow ?? sessionFlowId;
|
|
2269
|
+
const { traceEdgeProvenance, resolveLabel } = useProvenance({
|
|
2270
|
+
flow: effectiveFlow,
|
|
2271
|
+
collection,
|
|
2272
|
+
});
|
|
2273
|
+
const [events, setEvents] = useState([]);
|
|
2274
|
+
const [session, setSession] = useState({});
|
|
2275
|
+
const [isUnpacking, setIsUnpacking] = useState(false);
|
|
2276
|
+
const [error, setError] = useState(null);
|
|
2277
|
+
// Mirror session in a ref so it's always immediately readable (no render delay)
|
|
2278
|
+
const sessionRef = useRef({});
|
|
2279
|
+
// Track pending unpack operations
|
|
2280
|
+
const unpackQueueRef = useRef([]);
|
|
2281
|
+
const isProcessingRef = useRef(false);
|
|
2282
|
+
/**
|
|
2283
|
+
* Check if connected
|
|
2284
|
+
*/
|
|
2285
|
+
const isConnected = useCallback(() => {
|
|
2286
|
+
return (connectionState?.status === "authenticated" ||
|
|
2287
|
+
connectionState?.status === "unauthenticated");
|
|
2288
|
+
}, [connectionState]);
|
|
2289
|
+
/**
|
|
2290
|
+
* Single triple query (no retry)
|
|
2291
|
+
*/
|
|
2292
|
+
const fetchTriples = useCallback(async (explainId, explainGraph) => {
|
|
2293
|
+
return socket
|
|
2294
|
+
.flow(effectiveFlow)
|
|
2295
|
+
.triplesQuery({ t: "i", i: explainId }, undefined, undefined, 100, collection, explainGraph);
|
|
2296
|
+
}, [socket, effectiveFlow, collection]);
|
|
2297
|
+
/**
|
|
2298
|
+
* Stability-based retry for main event nodes.
|
|
2299
|
+
* Retries until: count > 0 AND count matches previous fetch.
|
|
2300
|
+
* Used for question, exploration, focus, synthesis nodes where the
|
|
2301
|
+
* backend may write multiple triples incrementally.
|
|
2302
|
+
*/
|
|
2303
|
+
const queryWithStabilityRetry = useCallback(async (explainId, explainGraph, timeoutMs = 5000) => {
|
|
2304
|
+
if (!isConnected())
|
|
2305
|
+
return [];
|
|
2306
|
+
const retryDelay = 500;
|
|
2307
|
+
const maxAttempts = Math.ceil(timeoutMs / retryDelay) + 1;
|
|
2308
|
+
let prevCount = -1;
|
|
2309
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2310
|
+
try {
|
|
2311
|
+
const triples = await fetchTriples(explainId, explainGraph);
|
|
2312
|
+
const count = triples.length;
|
|
2313
|
+
if (count > 0 && count === prevCount) {
|
|
2314
|
+
return triples;
|
|
2315
|
+
}
|
|
2316
|
+
prevCount = count;
|
|
2317
|
+
if (attempt < maxAttempts - 1) {
|
|
2318
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
catch (err) {
|
|
2322
|
+
console.error("[explain] triple query failed:", explainId, err);
|
|
2323
|
+
return [];
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
// Return last fetch if we got anything
|
|
2327
|
+
if (prevCount > 0) {
|
|
2328
|
+
try {
|
|
2329
|
+
return await fetchTriples(explainId, explainGraph);
|
|
2330
|
+
}
|
|
2331
|
+
catch {
|
|
2332
|
+
return [];
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return [];
|
|
2336
|
+
}, [fetchTriples, isConnected]);
|
|
2337
|
+
/**
|
|
2338
|
+
* Simple retry-until-non-empty for sub-objects (edge selections).
|
|
2339
|
+
* These are small atomic writes — either fully there or not yet.
|
|
2340
|
+
*/
|
|
2341
|
+
const queryWithSimpleRetry = useCallback(async (explainId, explainGraph, timeoutMs = 5000) => {
|
|
2342
|
+
if (!isConnected())
|
|
2343
|
+
return [];
|
|
2344
|
+
const retryDelay = 300;
|
|
2345
|
+
const maxAttempts = Math.ceil(timeoutMs / retryDelay) + 1;
|
|
2346
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2347
|
+
try {
|
|
2348
|
+
const triples = await fetchTriples(explainId, explainGraph);
|
|
2349
|
+
if (triples.length > 0)
|
|
2350
|
+
return triples;
|
|
2351
|
+
if (attempt < maxAttempts - 1) {
|
|
2352
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
catch (err) {
|
|
2356
|
+
console.error("[explain] triple query failed:", explainId, err);
|
|
2357
|
+
return [];
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
return [];
|
|
2361
|
+
}, [fetchTriples, isConnected]);
|
|
2362
|
+
/**
|
|
2363
|
+
* Resolve a single edge: fetch triples, labels, and provenance in parallel
|
|
2364
|
+
*/
|
|
2365
|
+
const resolveEdge = useCallback(async (edgeSelUri, explainGraph) => {
|
|
2366
|
+
const triples = await queryWithSimpleRetry(edgeSelUri, explainGraph);
|
|
2367
|
+
const { edge, reasoning } = parseEdgeSelectionTriples(triples);
|
|
2368
|
+
if (!edge)
|
|
2369
|
+
return null;
|
|
2370
|
+
const selectedEdge = {
|
|
2371
|
+
edge,
|
|
2372
|
+
reasoning: reasoning || undefined,
|
|
2373
|
+
};
|
|
2374
|
+
// Kick off labels and provenance in parallel
|
|
2375
|
+
const [labels, provenanceChains] = await Promise.all([
|
|
2376
|
+
// Labels — all 3 in parallel
|
|
2377
|
+
Promise.all([
|
|
2378
|
+
resolveLabel(edge.s),
|
|
2379
|
+
resolveLabel(edge.p),
|
|
2380
|
+
resolveLabel(edge.o),
|
|
2381
|
+
]),
|
|
2382
|
+
// Provenance
|
|
2383
|
+
traceProvenance
|
|
2384
|
+
? traceEdgeProvenance(edge.s, edge.p, edge.o)
|
|
2385
|
+
: Promise.resolve([]),
|
|
2386
|
+
]);
|
|
2387
|
+
selectedEdge.labels = { s: labels[0], p: labels[1], o: labels[2] };
|
|
2388
|
+
if (provenanceChains.length > 0) {
|
|
2389
|
+
selectedEdge.sources = provenanceChains.map((c) => c.chain).flat();
|
|
2390
|
+
}
|
|
2391
|
+
return selectedEdge;
|
|
2392
|
+
}, [queryWithSimpleRetry, resolveLabel, traceProvenance, traceEdgeProvenance]);
|
|
2393
|
+
/**
|
|
2394
|
+
* Unpack a focus event — resolve ALL edges in parallel
|
|
2395
|
+
*/
|
|
2396
|
+
const unpackFocusEvent = useCallback(async (focusEvent) => {
|
|
2397
|
+
// Fire off all edge resolutions concurrently
|
|
2398
|
+
const edgePromises = focusEvent.edgeSelectionUris.map((uri) => resolveEdge(uri, focusEvent.explainGraph));
|
|
2399
|
+
const results = await Promise.all(edgePromises);
|
|
2400
|
+
const selectedEdges = results.filter((e) => e !== null);
|
|
2401
|
+
return {
|
|
2402
|
+
...focusEvent,
|
|
2403
|
+
selectedEdges,
|
|
2404
|
+
};
|
|
2405
|
+
}, [resolveEdge]);
|
|
2406
|
+
/** Helper to update both state and ref together */
|
|
2407
|
+
const updateSession = useCallback((updater) => {
|
|
2408
|
+
sessionRef.current = updater(sessionRef.current);
|
|
2409
|
+
setSession(updater);
|
|
2410
|
+
}, []);
|
|
2411
|
+
/**
|
|
2412
|
+
* Process a single explain event
|
|
2413
|
+
*/
|
|
2414
|
+
const processEvent = useCallback(async (event) => {
|
|
2415
|
+
// Query triples for main event node (stability retry)
|
|
2416
|
+
const triples = await queryWithStabilityRetry(event.explainId, event.explainGraph);
|
|
2417
|
+
// Parse into structured data
|
|
2418
|
+
const parsed = parseExplainTriples(event.explainId, event.explainGraph, triples);
|
|
2419
|
+
if (!parsed)
|
|
2420
|
+
return;
|
|
2421
|
+
// Update session based on event type
|
|
2422
|
+
updateSession((prev) => {
|
|
2423
|
+
const next = { ...prev };
|
|
2424
|
+
switch (parsed.type) {
|
|
2425
|
+
case "question":
|
|
2426
|
+
next.question = parsed;
|
|
2427
|
+
break;
|
|
2428
|
+
case "exploration":
|
|
2429
|
+
next.exploration = parsed;
|
|
2430
|
+
break;
|
|
2431
|
+
case "focus":
|
|
2432
|
+
// Will be updated again after unpacking
|
|
2433
|
+
next.focus = parsed;
|
|
2434
|
+
break;
|
|
2435
|
+
case "synthesis":
|
|
2436
|
+
next.synthesis = parsed;
|
|
2437
|
+
break;
|
|
2438
|
+
}
|
|
2439
|
+
return next;
|
|
2440
|
+
});
|
|
2441
|
+
// For focus events, unpack edges in parallel
|
|
2442
|
+
if (parsed.type === "focus") {
|
|
2443
|
+
const unpackedFocus = await unpackFocusEvent(parsed);
|
|
2444
|
+
updateSession((prev) => ({
|
|
2445
|
+
...prev,
|
|
2446
|
+
focus: unpackedFocus,
|
|
2447
|
+
}));
|
|
2448
|
+
}
|
|
2449
|
+
}, [queryWithStabilityRetry, unpackFocusEvent, updateSession]);
|
|
2450
|
+
/**
|
|
2451
|
+
* Process the unpack queue
|
|
2452
|
+
*/
|
|
2453
|
+
const processQueue = useCallback(async () => {
|
|
2454
|
+
if (isProcessingRef.current)
|
|
2455
|
+
return;
|
|
2456
|
+
if (unpackQueueRef.current.length === 0)
|
|
2457
|
+
return;
|
|
2458
|
+
isProcessingRef.current = true;
|
|
2459
|
+
setIsUnpacking(true);
|
|
2460
|
+
try {
|
|
2461
|
+
while (unpackQueueRef.current.length > 0) {
|
|
2462
|
+
const event = unpackQueueRef.current.shift();
|
|
2463
|
+
await processEvent(event);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
catch (err) {
|
|
2467
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2468
|
+
setError(msg);
|
|
2469
|
+
}
|
|
2470
|
+
isProcessingRef.current = false;
|
|
2471
|
+
setIsUnpacking(false);
|
|
2472
|
+
// Check if more events were queued during processing (e.g., after a reset)
|
|
2473
|
+
if (unpackQueueRef.current.length > 0) {
|
|
2474
|
+
processQueue();
|
|
2475
|
+
}
|
|
2476
|
+
}, [processEvent]);
|
|
2477
|
+
/**
|
|
2478
|
+
* Add an explain event — immediately queues and starts processing
|
|
2479
|
+
*/
|
|
2480
|
+
const addEvent = useCallback((event) => {
|
|
2481
|
+
setEvents((prev) => [...prev, event]);
|
|
2482
|
+
unpackQueueRef.current.push(event);
|
|
2483
|
+
processQueue();
|
|
2484
|
+
}, [processQueue]);
|
|
2485
|
+
/**
|
|
2486
|
+
* Reset the session
|
|
2487
|
+
*/
|
|
2488
|
+
const reset = useCallback(() => {
|
|
2489
|
+
setEvents([]);
|
|
2490
|
+
setSession({});
|
|
2491
|
+
sessionRef.current = {};
|
|
2492
|
+
setError(null);
|
|
2493
|
+
unpackQueueRef.current = [];
|
|
2494
|
+
}, []);
|
|
2495
|
+
return {
|
|
2496
|
+
addEvent,
|
|
2497
|
+
session,
|
|
2498
|
+
sessionRef,
|
|
2499
|
+
events,
|
|
2500
|
+
isUnpacking,
|
|
2501
|
+
isProcessingRef,
|
|
2502
|
+
error,
|
|
2503
|
+
reset,
|
|
2504
|
+
};
|
|
2505
|
+
};
|
|
2506
|
+
|
|
1834
2507
|
/**
|
|
1835
2508
|
* High-level hook for managing chat sessions
|
|
1836
2509
|
* Combines conversation state with inference services
|
|
@@ -1854,6 +2527,19 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1854
2527
|
const effectiveFlow = flow ?? sessionFlowId;
|
|
1855
2528
|
// Settings for GraphRAG configuration
|
|
1856
2529
|
const { settings } = useSettings();
|
|
2530
|
+
// Explainability store for persisting sessions
|
|
2531
|
+
const addExplainSession = useExplainabilityStore((state) => state.addSession);
|
|
2532
|
+
// Explainability hook for processing events (processes each event immediately)
|
|
2533
|
+
const explainability = useExplainability({
|
|
2534
|
+
flow: effectiveFlow,
|
|
2535
|
+
collection: settings.collection,
|
|
2536
|
+
});
|
|
2537
|
+
const explainabilityRef = useRef(explainability);
|
|
2538
|
+
explainabilityRef.current = explainability;
|
|
2539
|
+
// Generate unique session IDs
|
|
2540
|
+
const generateSessionId = useCallback(() => {
|
|
2541
|
+
return `explain-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2542
|
+
}, []);
|
|
1857
2543
|
// Inference services
|
|
1858
2544
|
const inference = useInference({ flow });
|
|
1859
2545
|
/**
|
|
@@ -1865,6 +2551,13 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1865
2551
|
addActivity(ragActivity);
|
|
1866
2552
|
let accumulated = "";
|
|
1867
2553
|
let messageAdded = false;
|
|
2554
|
+
// Check if explainability is enabled
|
|
2555
|
+
const explainabilityEnabled = settings.featureSwitches.explainability;
|
|
2556
|
+
const sessionId = explainabilityEnabled ? generateSessionId() : undefined;
|
|
2557
|
+
// Reset explainability state for new query
|
|
2558
|
+
if (explainabilityEnabled) {
|
|
2559
|
+
explainabilityRef.current.reset();
|
|
2560
|
+
}
|
|
1868
2561
|
try {
|
|
1869
2562
|
// Execute Graph RAG with streaming and entity discovery
|
|
1870
2563
|
const result = await inference.graphRag({
|
|
@@ -1880,8 +2573,8 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1880
2573
|
onChunk: (chunk, complete) => {
|
|
1881
2574
|
accumulated += chunk;
|
|
1882
2575
|
if (!messageAdded) {
|
|
1883
|
-
// Add empty message on first chunk
|
|
1884
|
-
addMessage("ai", accumulated);
|
|
2576
|
+
// Add empty message on first chunk (with session ID if enabled)
|
|
2577
|
+
addMessage("ai", accumulated, undefined, sessionId);
|
|
1885
2578
|
messageAdded = true;
|
|
1886
2579
|
}
|
|
1887
2580
|
else {
|
|
@@ -1889,9 +2582,32 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1889
2582
|
updateLastMessage(accumulated);
|
|
1890
2583
|
}
|
|
1891
2584
|
},
|
|
2585
|
+
// Wire up explainability callback if feature is enabled
|
|
2586
|
+
...(explainabilityEnabled && {
|
|
2587
|
+
onExplain: (event) => {
|
|
2588
|
+
console.log("[explain] event received:", event.explainId);
|
|
2589
|
+
explainabilityRef.current.addEvent(event);
|
|
2590
|
+
},
|
|
2591
|
+
}),
|
|
1892
2592
|
},
|
|
1893
2593
|
});
|
|
1894
2594
|
removeActivity(ragActivity);
|
|
2595
|
+
// Store explainability session progressively — events are already being
|
|
2596
|
+
// processed as they arrive. Wait for processing to finish, then snapshot.
|
|
2597
|
+
if (explainabilityEnabled && sessionId) {
|
|
2598
|
+
const waitAndStore = async () => {
|
|
2599
|
+
// Poll until processing completes (events are processed as they arrive)
|
|
2600
|
+
const maxWait = 30000;
|
|
2601
|
+
let elapsed = 0;
|
|
2602
|
+
while (explainabilityRef.current.isProcessingRef.current && elapsed < maxWait) {
|
|
2603
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2604
|
+
elapsed += 500;
|
|
2605
|
+
}
|
|
2606
|
+
const sess = explainabilityRef.current.sessionRef.current;
|
|
2607
|
+
addExplainSession(sessionId, sess);
|
|
2608
|
+
};
|
|
2609
|
+
waitAndStore();
|
|
2610
|
+
}
|
|
1895
2611
|
// Start embeddings activity
|
|
1896
2612
|
addActivity(embActivity);
|
|
1897
2613
|
// Get labels for each entity
|
|
@@ -1899,7 +2615,7 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1899
2615
|
.filter((match) => match.entity !== null)
|
|
1900
2616
|
.map(async (match) => {
|
|
1901
2617
|
const entity = match.entity;
|
|
1902
|
-
const labelActivity = "Label " + getTermValue$
|
|
2618
|
+
const labelActivity = "Label " + getTermValue$3(entity);
|
|
1903
2619
|
addActivity(labelActivity);
|
|
1904
2620
|
try {
|
|
1905
2621
|
const triples = await socket
|
|
@@ -1918,8 +2634,8 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1918
2634
|
const entityList = labelResponses
|
|
1919
2635
|
.filter((resp) => resp && resp.length > 0)
|
|
1920
2636
|
.map((resp) => ({
|
|
1921
|
-
label: getTermValue$
|
|
1922
|
-
uri: getTermValue$
|
|
2637
|
+
label: getTermValue$3(resp[0].o),
|
|
2638
|
+
uri: getTermValue$3(resp[0].s),
|
|
1923
2639
|
}));
|
|
1924
2640
|
setEntities(entityList);
|
|
1925
2641
|
removeActivity(embActivity);
|
|
@@ -5187,5 +5903,76 @@ const useChunkedDownload = (options = {}) => {
|
|
|
5187
5903
|
};
|
|
5188
5904
|
};
|
|
5189
5905
|
|
|
5190
|
-
|
|
5906
|
+
/**
|
|
5907
|
+
* Hook for fetching document metadata from the librarian service
|
|
5908
|
+
*/
|
|
5909
|
+
/**
|
|
5910
|
+
* Hook for fetching a single document's metadata
|
|
5911
|
+
*/
|
|
5912
|
+
const useDocumentMetadata = (options = {}) => {
|
|
5913
|
+
const { documentId, enabled } = options;
|
|
5914
|
+
const socket = useSocket();
|
|
5915
|
+
const connectionState = useConnectionState();
|
|
5916
|
+
const isSocketReady = connectionState?.status === "authenticated" ||
|
|
5917
|
+
connectionState?.status === "unauthenticated";
|
|
5918
|
+
const query = useQuery({
|
|
5919
|
+
queryKey: ["document-metadata", documentId],
|
|
5920
|
+
enabled: isSocketReady && !!documentId && (enabled !== false),
|
|
5921
|
+
queryFn: async () => {
|
|
5922
|
+
if (!documentId)
|
|
5923
|
+
return null;
|
|
5924
|
+
return socket.librarian().getDocumentMetadata(documentId);
|
|
5925
|
+
},
|
|
5926
|
+
});
|
|
5927
|
+
return {
|
|
5928
|
+
metadata: query.data ?? null,
|
|
5929
|
+
isLoading: query.isLoading,
|
|
5930
|
+
isError: query.isError,
|
|
5931
|
+
error: query.error,
|
|
5932
|
+
refetch: query.refetch,
|
|
5933
|
+
};
|
|
5934
|
+
};
|
|
5935
|
+
/**
|
|
5936
|
+
* Hook for fetching multiple documents' metadata
|
|
5937
|
+
*/
|
|
5938
|
+
const useDocumentsMetadata = (documentIds = []) => {
|
|
5939
|
+
const socket = useSocket();
|
|
5940
|
+
const connectionState = useConnectionState();
|
|
5941
|
+
const queryClient = useQueryClient();
|
|
5942
|
+
const isSocketReady = connectionState?.status === "authenticated" ||
|
|
5943
|
+
connectionState?.status === "unauthenticated";
|
|
5944
|
+
const query = useQuery({
|
|
5945
|
+
queryKey: ["documents-metadata", documentIds],
|
|
5946
|
+
enabled: isSocketReady && documentIds.length > 0,
|
|
5947
|
+
queryFn: async () => {
|
|
5948
|
+
const results = await Promise.all(documentIds.map(async (id) => {
|
|
5949
|
+
// Check cache first
|
|
5950
|
+
const cached = queryClient.getQueryData([
|
|
5951
|
+
"document-metadata",
|
|
5952
|
+
id,
|
|
5953
|
+
]);
|
|
5954
|
+
if (cached !== undefined) {
|
|
5955
|
+
return { id, metadata: cached };
|
|
5956
|
+
}
|
|
5957
|
+
// Fetch and cache
|
|
5958
|
+
const metadata = await socket.librarian().getDocumentMetadata(id);
|
|
5959
|
+
queryClient.setQueryData(["document-metadata", id], metadata);
|
|
5960
|
+
return { id, metadata };
|
|
5961
|
+
}));
|
|
5962
|
+
return results.reduce((acc, { id, metadata }) => {
|
|
5963
|
+
acc[id] = metadata;
|
|
5964
|
+
return acc;
|
|
5965
|
+
}, {});
|
|
5966
|
+
},
|
|
5967
|
+
});
|
|
5968
|
+
return {
|
|
5969
|
+
metadataMap: query.data ?? {},
|
|
5970
|
+
isLoading: query.isLoading,
|
|
5971
|
+
isError: query.isError,
|
|
5972
|
+
error: query.error,
|
|
5973
|
+
refetch: query.refetch,
|
|
5974
|
+
};
|
|
5975
|
+
};
|
|
5976
|
+
|
|
5977
|
+
export { DEFAULT_SETTINGS, NotificationProvider, RDFS_LABEL, SETTINGS_STORAGE_KEY, createDocId, extractQuotedTriple, fileToBase64, generateFlowBlueprintId, getEventType, getTermValue$1 as getExplainTermValue, getTermValue$3 as getTermValue, getTriples, parseEdgeSelectionTriples, parseExplainTriples, parseExplorationTriples, parseFocusTriples, parseQuestionTriples, parseSynthesisTriples, prepareMetadata, textToBase64, useActivity, useAgentTools, useChat, useChatSession, useChunkedDownload, useChunkedUpload, useCollections, useConversation, useDocumentEmbeddingsQuery, useDocumentMetadata, useDocumentsMetadata, useEmbeddings, useEntityDetail, useExplainability, useExplainabilityStore, useFlowBlueprints, useFlowParameters, useFlows, useGraphEmbeddings, useGraphSubgraph, useInference, useKnowledgeCores, useLLMModels, useLibrary, useLoadStateStore, useMcpTools, useNlpQuery, useNodeDetails, useNotification, useOntologies, useParameterValidation, useProcessing, useProgressStateStore, usePrompts, useProvenance, useRowEmbeddingsQuery, useRowsQuery, useSchemas, useSearchStateStore, useSessionStore, useSettings, useStructuredQuery, useTokenCosts, useTriples, useVectorSearch, useWorkbenchStateStore, vectorSearch };
|
|
5191
5978
|
//# sourceMappingURL=index.esm.js.map
|