fuli-context 0.7.3 → 0.7.4-beta.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/README.md +170 -18
- package/README.zh-CN.md +146 -12
- package/compose.graphiti.yml +2 -0
- package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
- package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
- package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
- package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
- package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
- package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
- package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
- package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
- package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
- package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
- package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
- package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
- package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
- package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
- package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
- package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
- package/dist/web/assets/index-F5e2sQ7p.js +2 -0
- package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
- package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
- package/dist/web/assets/paths-CDREouhi.js +1 -0
- package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
- package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
- package/dist/web/index.html +7 -6
- package/graph-provider/fuli_graph/app.py +238 -2
- package/graph-provider/fuli_graph/auth.py +18 -0
- package/graph-provider/fuli_graph/collaboration_context.py +253 -17
- package/graph-provider/fuli_graph/common_knowledge.py +6 -2
- package/graph-provider/fuli_graph/config.py +5 -0
- package/graph-provider/fuli_graph/graph_models.py +6 -0
- package/graph-provider/fuli_graph/graph_projection.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +27 -3
- package/graph-provider/fuli_graph/knowledge_management.py +19 -1
- package/graph-provider/fuli_graph/knowledge_search.py +11 -0
- package/graph-provider/fuli_graph/models.py +15 -2
- package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
- package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
- package/graph-provider/fuli_graph/project_action_models.py +22 -0
- package/graph-provider/fuli_graph/project_knowledge.py +121 -2
- package/graph-provider/fuli_graph/provider_values.py +24 -0
- package/graph-provider/fuli_graph/runtime.py +78 -0
- package/graph-provider/fuli_graph/search_projection.py +14 -1
- package/graph-provider/fuli_graph/store_knowledge.py +116 -2
- package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
- package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
- package/graph-provider/fuli_graph/workflow_observations.py +84 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +4 -4
- package/skills/capturing-session-knowledge/SKILL.md +32 -5
- package/skills/flreview/SKILL.md +4 -3
- package/src/agent-tools/graph-definitions.js +136 -20
- package/src/agent-tools/graph-handlers.js +10 -4
- package/src/cli/command-registry.js +1 -1
- package/src/cli/local-runtime-command.js +69 -2
- package/src/graphiti/agent-knowledge-workflows.js +497 -16
- package/src/graphiti/collaboration-preference-projection.js +162 -0
- package/src/graphiti/federated-application.js +155 -161
- package/src/graphiti/federated-search-support.js +102 -0
- package/src/graphiti/knowledge-provider-mapping.js +15 -0
- package/src/graphiti/provider-client.js +162 -10
- package/src/graphiti/related-project-suggestions.js +109 -0
- package/src/graphiti/runtime-config.js +14 -0
- package/src/graphiti/source-marker.js +2 -2
- package/src/graphiti/user-taste-skill.js +241 -0
- package/src/graphiti/workflow-candidate-mapping.js +87 -0
- package/src/graphiti/workflow-observation.js +82 -0
- package/src/graphiti/writing-taste-profile-workflow.js +25 -0
- package/src/graphiti/writing-taste-profile.js +490 -0
- package/src/http/graph-api-router.js +15 -2
- package/src/mcp/context-resources.js +158 -0
- package/src/mcp/create-mcp-server.js +39 -5
- package/src/mcp/instructions.js +5 -7
- package/src/mcp/session-id.js +60 -0
- package/src/mcp/task-context-registry.js +15 -3
- package/src/mcp/tool-annotations.js +8 -1
- package/src/mcp/tool-result.js +28 -4
- package/src/server.js +0 -1
- package/src/setup/claude-code-config.js +17 -6
- package/src/setup/graph-runtime.js +20 -2
- package/src/system/runtime-settings.js +121 -2
- package/src/system/system-service.js +5 -1
- package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
- package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
- package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
- package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
- package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
- package/dist/web/assets/index-BzhNfzU_.js +0 -2
- package/dist/web/assets/paths-DwHeiGMT.js +0 -1
- package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
- package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { canonicalProviderUrl } from './runtime-config.js';
|
|
2
|
+
import {
|
|
3
|
+
relatedProjectSuggestionsForSearchResults
|
|
4
|
+
} from './related-project-suggestions.js';
|
|
5
|
+
|
|
6
|
+
export function retrievalGuidanceForScope(personalProjectScope) {
|
|
7
|
+
if (personalProjectScope === 'all_local_confirmed') {
|
|
8
|
+
return {
|
|
9
|
+
currentPersonalProjectScope: 'all_local_confirmed',
|
|
10
|
+
markerToUseIfNoSupportingEvidence: 'noMatchSourceMarker',
|
|
11
|
+
requiredNextActionIfNoSupportingEvidence:
|
|
12
|
+
'search_current_workspace_files_or_ask_for_safe_root',
|
|
13
|
+
workspaceFileSearch: {
|
|
14
|
+
available: true,
|
|
15
|
+
consentSource: 'bounded_expansion_confirmation',
|
|
16
|
+
rootBoundary: 'current_working_directory',
|
|
17
|
+
requiresSafeProjectOrWorkspaceRoot: true,
|
|
18
|
+
forbiddenBroadRoots: ['user_home', 'filesystem_root'],
|
|
19
|
+
readOnly: true,
|
|
20
|
+
includesPublicProjects: false
|
|
21
|
+
},
|
|
22
|
+
instruction: 'Use read-only local file search in the current repository or explicit ' +
|
|
23
|
+
'workspace root, preserving exact names first. If the working directory is the user ' +
|
|
24
|
+
'home, filesystem root, or otherwise too broad, ask for a safe root. Never search ' +
|
|
25
|
+
'outside that root or inspect credential stores; if no evidence supports the answer, ' +
|
|
26
|
+
'ask for a source clue.'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
currentPersonalProjectScope: 'bounded',
|
|
31
|
+
markerToUseIfNoSupportingEvidence: 'noMatchSourceMarker',
|
|
32
|
+
requiredNextActionIfNoSupportingEvidence:
|
|
33
|
+
'ask_user_to_confirm_all_local_and_workspace_search',
|
|
34
|
+
expansion: {
|
|
35
|
+
available: true,
|
|
36
|
+
requiresExplicitUserConfirmation: true,
|
|
37
|
+
input: { personalProjectScope: 'all_local_confirmed' },
|
|
38
|
+
readOnly: true,
|
|
39
|
+
oneQueryOnly: true,
|
|
40
|
+
includesPublicProjects: false,
|
|
41
|
+
includesCurrentWorkspaceFiles: true
|
|
42
|
+
},
|
|
43
|
+
instruction: 'Ask whether to widen this one read-only lookup to all registered local ' +
|
|
44
|
+
'personal projects and, if still unresolved, current repository or workspace files. ' +
|
|
45
|
+
'Exclude public projects and paths outside the current workspace; then stop and wait.'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function loadSearchRelatedProjectSuggestions(application, result) {
|
|
50
|
+
const hasExactProjectResult = [
|
|
51
|
+
...(result?.facts ?? []),
|
|
52
|
+
...(result?.entities ?? [])
|
|
53
|
+
].some((item) => (
|
|
54
|
+
typeof item?.defined_project_id === 'string' &&
|
|
55
|
+
item.defined_project_id &&
|
|
56
|
+
!item.inherited_from_project_id &&
|
|
57
|
+
Number(item.scope_distance ?? 0) === 0
|
|
58
|
+
));
|
|
59
|
+
if (!hasExactProjectResult) {
|
|
60
|
+
return { status: 'not_applicable', suggestions: [] };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const graph = await application.personal.graph(
|
|
64
|
+
application.config.personal.spaceId,
|
|
65
|
+
2000
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
status: graph?.truncated ? 'partial' : 'available',
|
|
69
|
+
suggestions: relatedProjectSuggestionsForSearchResults(graph, result)
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return { status: 'unavailable', suggestions: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function groupSubscriptions(subscriptions) {
|
|
77
|
+
const grouped = new Map();
|
|
78
|
+
for (const subscription of subscriptions) {
|
|
79
|
+
const url = canonicalProviderUrl(subscription.provider_url);
|
|
80
|
+
if (!grouped.has(url)) grouped.set(url, []);
|
|
81
|
+
grouped.get(url).push(subscription);
|
|
82
|
+
}
|
|
83
|
+
return grouped;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function rankedSearchItems(searchResults, key, limit) {
|
|
87
|
+
return searchResults
|
|
88
|
+
.flatMap((result) => result[key])
|
|
89
|
+
.map((item, index) => ({ item, index }))
|
|
90
|
+
.sort((left, right) =>
|
|
91
|
+
searchItemScore(right.item) - searchItemScore(left.item) ||
|
|
92
|
+
left.index - right.index
|
|
93
|
+
)
|
|
94
|
+
.slice(0, limit)
|
|
95
|
+
.map(({ item }) => item);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function searchItemScore(item) {
|
|
99
|
+
return typeof item?.score === 'number' && Number.isFinite(item.score)
|
|
100
|
+
? item.score
|
|
101
|
+
: 0;
|
|
102
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { ApplicationError } from '../app/application-error.js';
|
|
1
2
|
import { onlineSourceUri } from './source-uri.js';
|
|
2
3
|
|
|
3
4
|
export function providerEpisode(input) {
|
|
5
|
+
assertReasoningSummaries(input.entities, 'entities');
|
|
6
|
+
assertReasoningSummaries(input.relationships, 'relationships');
|
|
4
7
|
return {
|
|
5
8
|
idempotency_key: input.idempotencyKey,
|
|
6
9
|
session_id: input.sessionId,
|
|
@@ -55,6 +58,18 @@ export function providerEpisode(input) {
|
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
function assertReasoningSummaries(items, collectionName) {
|
|
62
|
+
items.forEach((item, index) => {
|
|
63
|
+
const originQuadrant = item.originQuadrant ?? 'known_known';
|
|
64
|
+
if (originQuadrant === 'known_known' || item.reasoningSummary?.trim()) return;
|
|
65
|
+
throw new ApplicationError(
|
|
66
|
+
'validation',
|
|
67
|
+
`${collectionName}[${index}].reasoningSummary is required when ` +
|
|
68
|
+
`originQuadrant is ${originQuadrant}`
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
export function assertPublicKnowledgeEligible(episode) {
|
|
59
74
|
const blocked = [...episode.entities, ...episode.relationships].find((item) =>
|
|
60
75
|
item.profile_aspect ||
|
|
@@ -2,26 +2,48 @@ import { ApplicationError } from '../app/application-error.js';
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
4
4
|
const MAX_REQUEST_TIMEOUT_MS = 2_147_483_647;
|
|
5
|
+
const MAX_PROVIDER_VALIDATION_ERRORS = 5;
|
|
5
6
|
|
|
6
7
|
export class ProviderRequestError extends ApplicationError {
|
|
7
|
-
constructor(message, {
|
|
8
|
+
constructor(message, {
|
|
9
|
+
status = 0,
|
|
10
|
+
code = 'provider_error',
|
|
11
|
+
details = null,
|
|
12
|
+
diagnostic = null,
|
|
13
|
+
validationErrors = []
|
|
14
|
+
} = {}) {
|
|
8
15
|
super(code, message);
|
|
9
16
|
this.name = 'ProviderRequestError';
|
|
10
17
|
this.status = status;
|
|
11
18
|
this.code = code;
|
|
12
19
|
this.details = details;
|
|
20
|
+
this.diagnostic = diagnostic;
|
|
21
|
+
this.validationErrors = Array.isArray(validationErrors)
|
|
22
|
+
? validationErrors.slice(0, MAX_PROVIDER_VALIDATION_ERRORS)
|
|
23
|
+
: [];
|
|
13
24
|
}
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
export class GraphitiProviderClient {
|
|
28
|
+
#workflowObservationToken;
|
|
29
|
+
|
|
17
30
|
constructor({
|
|
18
31
|
baseUrl,
|
|
19
32
|
accessToken,
|
|
33
|
+
workflowObservationToken = null,
|
|
20
34
|
fetchImpl = globalThis.fetch,
|
|
21
35
|
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS
|
|
22
36
|
}) {
|
|
23
37
|
if (!baseUrl) throw new TypeError('Graphiti provider baseUrl is required');
|
|
24
38
|
if (!accessToken) throw new TypeError('Graphiti provider accessToken is required');
|
|
39
|
+
if (workflowObservationToken !== null && (
|
|
40
|
+
typeof workflowObservationToken !== 'string' ||
|
|
41
|
+
workflowObservationToken.trim().length < 32
|
|
42
|
+
)) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
'MCP host workflow observation credential must contain at least 32 characters'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
25
47
|
if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required');
|
|
26
48
|
if (!Number.isSafeInteger(requestTimeoutMs) ||
|
|
27
49
|
requestTimeoutMs < 1 ||
|
|
@@ -30,6 +52,7 @@ export class GraphitiProviderClient {
|
|
|
30
52
|
}
|
|
31
53
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
32
54
|
this.accessToken = accessToken;
|
|
55
|
+
this.#workflowObservationToken = workflowObservationToken?.trim() ?? null;
|
|
33
56
|
this.fetch = fetchImpl;
|
|
34
57
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
35
58
|
}
|
|
@@ -118,6 +141,14 @@ export class GraphitiProviderClient {
|
|
|
118
141
|
commit(input) {
|
|
119
142
|
return this.#request('/v1/knowledge/commits', { method: 'POST', body: input });
|
|
120
143
|
}
|
|
144
|
+
recordWorkflowObservation(input) {
|
|
145
|
+
if (!this.#workflowObservationToken) {
|
|
146
|
+
throw new TypeError('MCP host workflow observation credential is not configured');
|
|
147
|
+
}
|
|
148
|
+
return this.#request('/v1/workflow-observations', {
|
|
149
|
+
method: 'POST', body: input, workflowObservation: true
|
|
150
|
+
});
|
|
151
|
+
}
|
|
121
152
|
startKnowledgeReview(input) {
|
|
122
153
|
return this.#request('/v1/knowledge/reviews/start', {
|
|
123
154
|
method: 'POST', body: input
|
|
@@ -138,6 +169,16 @@ export class GraphitiProviderClient {
|
|
|
138
169
|
method: 'POST', body: input
|
|
139
170
|
});
|
|
140
171
|
}
|
|
172
|
+
searchWorkflowCandidates(input) {
|
|
173
|
+
return this.#request('/v1/workflow-candidates/search', {
|
|
174
|
+
method: 'POST', body: input
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
recommendWorkflowCandidates(input) {
|
|
178
|
+
return this.#request('/v1/workflow-candidates/recommendations', {
|
|
179
|
+
method: 'POST', body: input
|
|
180
|
+
});
|
|
181
|
+
}
|
|
141
182
|
reviseKnowledgeItem(itemId, input) {
|
|
142
183
|
return this.#request(`/v1/knowledge/items/${encodeURIComponent(itemId)}`, {
|
|
143
184
|
method: 'PATCH', body: input
|
|
@@ -196,6 +237,33 @@ export class GraphitiProviderClient {
|
|
|
196
237
|
method: 'POST', body: input
|
|
197
238
|
});
|
|
198
239
|
}
|
|
240
|
+
personalGlobalPreferenceDecisionStatus(input) {
|
|
241
|
+
return this.#request(
|
|
242
|
+
'/v1/personal-global-preference-candidates/decision-status',
|
|
243
|
+
{ method: 'POST', body: input }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
personalGlobalPreferenceScopeOptions(candidateId, input) {
|
|
247
|
+
return this.#request(
|
|
248
|
+
'/v1/personal-global-preference-candidates/' +
|
|
249
|
+
`${encodeURIComponent(candidateId)}/scope-options`,
|
|
250
|
+
{ method: 'POST', body: input }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
inspectPersonalGlobalPreferenceDecision(candidateId, input) {
|
|
254
|
+
return this.#request(
|
|
255
|
+
'/v1/personal-global-preference-candidates/' +
|
|
256
|
+
`${encodeURIComponent(candidateId)}/decision-inspection`,
|
|
257
|
+
{ method: 'POST', body: input }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
applyPersonalGlobalPreferenceDecision(candidateId, input) {
|
|
261
|
+
return this.#request(
|
|
262
|
+
'/v1/personal-global-preference-candidates/' +
|
|
263
|
+
`${encodeURIComponent(candidateId)}/decision`,
|
|
264
|
+
{ method: 'POST', body: input }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
199
267
|
deferPreferenceConflict(input) {
|
|
200
268
|
return this.#request('/v1/preference-conflicts/defer', {
|
|
201
269
|
method: 'POST', body: input
|
|
@@ -270,10 +338,14 @@ export class GraphitiProviderClient {
|
|
|
270
338
|
async #request(path, {
|
|
271
339
|
method = 'GET',
|
|
272
340
|
body,
|
|
273
|
-
authenticated = true
|
|
341
|
+
authenticated = true,
|
|
342
|
+
workflowObservation = false
|
|
274
343
|
} = {}) {
|
|
275
344
|
const headers = { accept: 'application/json' };
|
|
276
345
|
if (authenticated) headers.authorization = `Bearer ${this.accessToken}`;
|
|
346
|
+
if (workflowObservation) {
|
|
347
|
+
headers['x-fuli-workflow-observation-token'] = this.#workflowObservationToken;
|
|
348
|
+
}
|
|
277
349
|
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
278
350
|
const controller = new AbortController();
|
|
279
351
|
let timedOut = false;
|
|
@@ -293,23 +365,36 @@ export class GraphitiProviderClient {
|
|
|
293
365
|
});
|
|
294
366
|
payload = await parseResponse(response);
|
|
295
367
|
} catch (error) {
|
|
368
|
+
const diagnostic = {
|
|
369
|
+
category: timedOut ? 'provider_timeout' : 'provider_unavailable',
|
|
370
|
+
status: timedOut ? 504 : 0,
|
|
371
|
+
detail: timedOut
|
|
372
|
+
? 'Graphiti provider request timed out.'
|
|
373
|
+
: 'Graphiti provider is unavailable.'
|
|
374
|
+
};
|
|
296
375
|
throw new ProviderRequestError(
|
|
297
|
-
|
|
298
|
-
? 'Graphiti provider request timed out'
|
|
299
|
-
: 'Graphiti provider is unavailable',
|
|
376
|
+
diagnostic.detail,
|
|
300
377
|
{
|
|
301
|
-
status:
|
|
302
|
-
code:
|
|
303
|
-
details: timedOut ? null :
|
|
378
|
+
status: diagnostic.status,
|
|
379
|
+
code: diagnostic.category,
|
|
380
|
+
details: timedOut ? null : diagnostic,
|
|
381
|
+
diagnostic
|
|
304
382
|
}
|
|
305
383
|
);
|
|
306
384
|
} finally {
|
|
307
385
|
clearTimeout(timeout);
|
|
308
386
|
}
|
|
309
387
|
if (!response.ok) {
|
|
388
|
+
const diagnostic = providerErrorDiagnostic(response.status, payload);
|
|
310
389
|
throw new ProviderRequestError(
|
|
311
|
-
|
|
312
|
-
{
|
|
390
|
+
diagnostic.detail,
|
|
391
|
+
{
|
|
392
|
+
status: response.status,
|
|
393
|
+
code: diagnostic.category,
|
|
394
|
+
details: diagnostic,
|
|
395
|
+
diagnostic,
|
|
396
|
+
validationErrors: diagnostic.validationErrors
|
|
397
|
+
}
|
|
313
398
|
);
|
|
314
399
|
}
|
|
315
400
|
return payload;
|
|
@@ -322,3 +407,70 @@ async function parseResponse(response) {
|
|
|
322
407
|
const text = await response.text();
|
|
323
408
|
return text ? { detail: text } : null;
|
|
324
409
|
}
|
|
410
|
+
|
|
411
|
+
function providerErrorDiagnostic(status, payload) {
|
|
412
|
+
const category = status >= 500 && status <= 599
|
|
413
|
+
? 'provider_http_5xx'
|
|
414
|
+
: 'provider_error';
|
|
415
|
+
if (category === 'provider_http_5xx') {
|
|
416
|
+
return {
|
|
417
|
+
category,
|
|
418
|
+
status,
|
|
419
|
+
detail: `Graphiti provider returned HTTP ${status}.`
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const validationErrors = providerValidationErrors(payload);
|
|
423
|
+
return {
|
|
424
|
+
category,
|
|
425
|
+
status,
|
|
426
|
+
detail: sanitizeProviderDetail(providerErrorMessage(payload, validationErrors)),
|
|
427
|
+
validationErrors
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function providerErrorMessage(payload, validationErrors = providerValidationErrors(payload)) {
|
|
432
|
+
if (typeof payload?.detail === 'string') return payload.detail;
|
|
433
|
+
if (!validationErrors.length) return 'Graphiti provider request failed';
|
|
434
|
+
const messages = validationErrors.map(({ field, message }) => (
|
|
435
|
+
field ? `${field} — ${message}` : message
|
|
436
|
+
));
|
|
437
|
+
return `Graphiti provider rejected the request — ${messages.join('; ')}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function sanitizeProviderDetail(value) {
|
|
441
|
+
return String(value ?? 'Graphiti provider request failed')
|
|
442
|
+
.replace(/Bearer\s+\S+/gi, 'Bearer <redacted>')
|
|
443
|
+
.replace(/(?:token|secret|password|api[_-]?key)\s*[:=]\s*\S+/gi, '$1=<redacted>')
|
|
444
|
+
.replace(/(?:\/Users\/|\/home\/|\/private\/|\/tmp\/)[^\s"']+/g, '<path>')
|
|
445
|
+
.replace(/https?:\/\/[^\s"']+/gi, '<url>')
|
|
446
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
447
|
+
.replace(/\s+/g, ' ')
|
|
448
|
+
.trim()
|
|
449
|
+
.replaceAll(':', ' —')
|
|
450
|
+
.slice(0, 320) || 'Graphiti provider request failed';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Issue source: WZ.
|
|
454
|
+
// Provider validation failures must expose the field and actionable reason
|
|
455
|
+
// without echoing FastAPI's submitted input payload.
|
|
456
|
+
function providerValidationErrors(payload) {
|
|
457
|
+
if (!Array.isArray(payload?.detail)) return [];
|
|
458
|
+
return payload.detail
|
|
459
|
+
.filter((item) => item && typeof item.msg === 'string' && item.msg.trim())
|
|
460
|
+
.slice(0, MAX_PROVIDER_VALIDATION_ERRORS)
|
|
461
|
+
.map((item) => ({
|
|
462
|
+
field: validationLocation(item.loc),
|
|
463
|
+
message: sanitizeProviderDetail(item.msg.trim())
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function validationLocation(loc) {
|
|
468
|
+
if (!Array.isArray(loc)) return '';
|
|
469
|
+
return loc
|
|
470
|
+
.filter((segment) => segment !== 'body')
|
|
471
|
+
.reduce((path, segment) => {
|
|
472
|
+
if (typeof segment === 'number') return `${path}[${segment}]`;
|
|
473
|
+
if (typeof segment !== 'string' || !segment) return path;
|
|
474
|
+
return path ? `${path}.${segment}` : segment;
|
|
475
|
+
}, '');
|
|
476
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const SUGGESTED_RELATION_TYPES = new Set(['RELATED_TO']);
|
|
2
|
+
|
|
3
|
+
export function relatedProjectSuggestions(graph, currentProjectId) {
|
|
4
|
+
const nodes = new Map(
|
|
5
|
+
(graph?.nodes ?? [])
|
|
6
|
+
.filter((node) => projectId(node))
|
|
7
|
+
.map((node) => [node.id, node])
|
|
8
|
+
);
|
|
9
|
+
const currentNodes = new Set(
|
|
10
|
+
[...nodes.values()]
|
|
11
|
+
.filter((node) => projectId(node) === currentProjectId)
|
|
12
|
+
.map(({ id }) => id)
|
|
13
|
+
);
|
|
14
|
+
const suggestions = [];
|
|
15
|
+
|
|
16
|
+
for (const edge of graph?.edges ?? []) {
|
|
17
|
+
if (!SUGGESTED_RELATION_TYPES.has(edge.type)) continue;
|
|
18
|
+
if (relationStatus(edge) !== 'active') continue;
|
|
19
|
+
if (relationAuthority(edge) !== 'human_review') continue;
|
|
20
|
+
const currentIsSource = currentNodes.has(edge.source);
|
|
21
|
+
const currentIsTarget = currentNodes.has(edge.target);
|
|
22
|
+
if (currentIsSource === currentIsTarget) continue;
|
|
23
|
+
const relatedNode = nodes.get(currentIsSource ? edge.target : edge.source);
|
|
24
|
+
const relatedProjectId = projectId(relatedNode);
|
|
25
|
+
if (!relatedProjectId || relatedProjectId === currentProjectId) continue;
|
|
26
|
+
suggestions.push({
|
|
27
|
+
personal_project_id: relatedProjectId,
|
|
28
|
+
project_name: relatedNode.name ?? relatedProjectId,
|
|
29
|
+
relation_id: edge.id,
|
|
30
|
+
relation_type: edge.type,
|
|
31
|
+
relation_status: 'active',
|
|
32
|
+
direction: currentIsSource ? 'outgoing' : 'incoming',
|
|
33
|
+
reason: edge.fact ?? `The current project has a ${edge.type} relation to ${relatedProjectId}.`,
|
|
34
|
+
requires_human_confirmation: true,
|
|
35
|
+
expansion_mode: 'one_time_read_only',
|
|
36
|
+
confirmed_search: {
|
|
37
|
+
tool: 'search_knowledge_graph',
|
|
38
|
+
personal_project_scope: 'bounded',
|
|
39
|
+
context_personal_project_ids: [relatedProjectId]
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return suggestions.sort((left, right) => (
|
|
45
|
+
left.personal_project_id.localeCompare(right.personal_project_id)
|
|
46
|
+
|| left.relation_id.localeCompare(right.relation_id)
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function relatedProjectSuggestionsForSearchResults(graph, result) {
|
|
51
|
+
const resultIdsByProject = new Map();
|
|
52
|
+
for (const item of [...(result?.facts ?? []), ...(result?.entities ?? [])]) {
|
|
53
|
+
const projectId = exactProjectId(item);
|
|
54
|
+
if (!projectId) continue;
|
|
55
|
+
const resultIds = resultIdsByProject.get(projectId) ?? [];
|
|
56
|
+
if (typeof item.id === 'string' && item.id && !resultIds.includes(item.id)) {
|
|
57
|
+
resultIds.push(item.id);
|
|
58
|
+
}
|
|
59
|
+
resultIdsByProject.set(projectId, resultIds);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const suggestions = [];
|
|
63
|
+
for (const [projectId, resultIds] of resultIdsByProject) {
|
|
64
|
+
for (const suggestion of relatedProjectSuggestions(graph, projectId)) {
|
|
65
|
+
suggestions.push({
|
|
66
|
+
...suggestion,
|
|
67
|
+
triggered_by_project_id: projectId,
|
|
68
|
+
triggered_by_result_ids: resultIds
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return suggestions.sort((left, right) => (
|
|
73
|
+
left.triggered_by_project_id.localeCompare(right.triggered_by_project_id)
|
|
74
|
+
|| left.personal_project_id.localeCompare(right.personal_project_id)
|
|
75
|
+
|| left.relation_id.localeCompare(right.relation_id)
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function relatedProjectGuidance(suggestions) {
|
|
80
|
+
if (suggestions.length === 0) return null;
|
|
81
|
+
const projectIds = suggestions
|
|
82
|
+
.map(({ personal_project_id: projectId }) => projectId)
|
|
83
|
+
.join(', ');
|
|
84
|
+
return 'Ask whether this task may add a one-time read-only search of exactly: ' +
|
|
85
|
+
`${projectIds}. Do not expand before explicit human confirmation.`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function projectId(node) {
|
|
89
|
+
return node?.attributes?.projectId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function relationStatus(edge) {
|
|
93
|
+
return edge?.attributes?.status ?? edge?.status ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function relationAuthority(edge) {
|
|
97
|
+
return edge?.attributes?.confirmationAuthority
|
|
98
|
+
?? edge?.attributes?.confirmation_authority
|
|
99
|
+
?? edge?.confirmation_authority
|
|
100
|
+
?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function exactProjectId(item) {
|
|
104
|
+
if (item?.inherited_from_project_id) return null;
|
|
105
|
+
if (Number(item?.scope_distance ?? 0) !== 0) return null;
|
|
106
|
+
return typeof item?.defined_project_id === 'string' && item.defined_project_id
|
|
107
|
+
? item.defined_project_id
|
|
108
|
+
: null;
|
|
109
|
+
}
|
|
@@ -30,6 +30,11 @@ export function readGraphRuntimeConfig(path) {
|
|
|
30
30
|
function validateConfig(config) {
|
|
31
31
|
if (config?.version !== 1) fail('unsupported graph runtime config version');
|
|
32
32
|
provider(config.personal, 'personal');
|
|
33
|
+
optionalSecret(
|
|
34
|
+
config.personal,
|
|
35
|
+
'workflowObservationToken',
|
|
36
|
+
'personal.workflowObservationToken'
|
|
37
|
+
);
|
|
33
38
|
if (!config.personal.spaceId) fail('personal.spaceId is required');
|
|
34
39
|
if (!Array.isArray(config.workspaces)) fail('workspaces must be an array');
|
|
35
40
|
for (const [index, workspace] of config.workspaces.entries()) {
|
|
@@ -45,6 +50,15 @@ function provider(value, label) {
|
|
|
45
50
|
value.providerUrl = canonicalProviderUrl(value.providerUrl);
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
function optionalSecret(value, property, label) {
|
|
54
|
+
if (value[property] === undefined) return;
|
|
55
|
+
const normalized = nonEmpty(value[property]);
|
|
56
|
+
if (!normalized || normalized.length < 32) {
|
|
57
|
+
fail(`${label} must contain at least 32 characters`);
|
|
58
|
+
}
|
|
59
|
+
value[property] = normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
export function canonicalProviderUrl(value) {
|
|
49
63
|
const normalized = String(value).replace(/\/$/, '');
|
|
50
64
|
let url;
|
|
@@ -156,9 +156,9 @@ function sourceMarkerMarkdown({ consoleUrl, citations }) {
|
|
|
156
156
|
|
|
157
157
|
function knowledgeUrl(consoleUrl, { scope, spaceId, itemKind, itemId }) {
|
|
158
158
|
const origin = safeLoopbackOrigin(consoleUrl) ?? DEFAULT_CONSOLE_URL;
|
|
159
|
-
const segments = ['knowledge', scope, spaceId, itemKind, itemId]
|
|
159
|
+
const segments = ['knowledge', scope, spaceId, 'directory', itemKind, itemId]
|
|
160
160
|
.map((segment) => encodeURIComponent(segment));
|
|
161
|
-
return `${origin}
|
|
161
|
+
return `${origin}/${segments.join('/')}`;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function safeLoopbackOrigin(value) {
|