fuli-context 0.7.3 → 0.7.4-beta.2
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-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
- package/dist/web/assets/index-nP5p5OaY.js +2 -0
- 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 +21 -21
- 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/task-knowledge-recall.js +66 -15
- 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) {
|
|
@@ -32,7 +32,7 @@ const RECALL_INTENTS = Object.freeze([
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
category: 'runbook_method',
|
|
35
|
-
pattern: /(
|
|
35
|
+
pattern: /(?:怎么(?:做|办|处理|操作|走)?|如何(?:做|处理|操作|进行)?|怎样(?:做|处理|操作)?|(?:什么|哪(?:个|种)|有没有|是否有|给我|提供|使用|采用|遵循|按照|沿用|走).{0,16}(?:方法|方式|流程|步骤)|(?:方法|方式|流程|步骤).{0,16}(?:是什么|有哪些|怎么|如何|怎样|吗|呢|?|\?)|\brunbook\b|\bprocedure\b|\bhow\s+to\b|(?:what|which|show|give|use|follow|existing|documented).{0,24}\bworkflow\b)/iu,
|
|
36
36
|
queries: ['方法 方式 流程 步骤 runbook workflow procedure']
|
|
37
37
|
},
|
|
38
38
|
{
|
|
@@ -48,7 +48,9 @@ export const TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE = Object.freeze({
|
|
|
48
48
|
focused_queries:
|
|
49
49
|
'Use one to four focused action, artifact, target-system, or identifier queries; never use the full conversational request as the only query.',
|
|
50
50
|
scope:
|
|
51
|
-
'Search only the exact active personal project, its explicitly inheritable knowledge, and the bounded personal-global profile.'
|
|
51
|
+
'Search only the exact active personal project, its explicitly inheritable knowledge, and the bounded personal-global profile.',
|
|
52
|
+
candidate_selection:
|
|
53
|
+
'Treat automatic matches as candidates. Cite only items that materially support the task; if all are irrelevant, use noMatchSourceMarker.'
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
export function planTaskKnowledgeRecall(taskPrompt) {
|
|
@@ -67,9 +69,9 @@ export function planTaskKnowledgeRecall(taskPrompt) {
|
|
|
67
69
|
if (!matched.length) {
|
|
68
70
|
return { status: 'not_needed', trigger_categories: [], queries: [] };
|
|
69
71
|
}
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
72
|
+
const focus = distinctivePromptTerms(prompt).join(' ').slice(0, 180);
|
|
73
|
+
const queries = unique(matched.flatMap(({ queries: values }) => values)
|
|
74
|
+
.map((query) => focus ? `${focus} ${query}` : query));
|
|
73
75
|
return {
|
|
74
76
|
status: 'planned',
|
|
75
77
|
trigger_categories: matched.map(({ category }) => category),
|
|
@@ -85,7 +87,7 @@ export async function recallTaskKnowledge(application, resolution, taskPrompt) {
|
|
|
85
87
|
status: 'project_unresolved',
|
|
86
88
|
trigger_categories: plan.trigger_categories,
|
|
87
89
|
query_count: plan.queries.length,
|
|
88
|
-
guidance:
|
|
90
|
+
guidance: taskKnowledgeRetrievalGuidance(),
|
|
89
91
|
facts: [],
|
|
90
92
|
entities: []
|
|
91
93
|
};
|
|
@@ -107,12 +109,19 @@ export async function recallTaskKnowledge(application, resolution, taskPrompt) {
|
|
|
107
109
|
.filter(({ status }) => status === 'fulfilled')
|
|
108
110
|
.map(({ value }) => value);
|
|
109
111
|
const failedQueryCount = settlements.length - results.length;
|
|
112
|
+
const focusTerms = distinctivePromptTerms(singleLine(taskPrompt).slice(0, 8192));
|
|
110
113
|
const facts = rankedUnique(
|
|
111
|
-
|
|
114
|
+
filterRecallCandidates(
|
|
115
|
+
results.flatMap(({ facts = [] }) => facts),
|
|
116
|
+
focusTerms
|
|
117
|
+
),
|
|
112
118
|
'relationship'
|
|
113
119
|
).slice(0, MAX_ITEMS_PER_KIND).map(compactFact);
|
|
114
120
|
const entities = rankedUnique(
|
|
115
|
-
|
|
121
|
+
filterRecallCandidates(
|
|
122
|
+
results.flatMap(({ entities = [] }) => entities),
|
|
123
|
+
focusTerms
|
|
124
|
+
),
|
|
116
125
|
'entity'
|
|
117
126
|
).slice(0, MAX_ITEMS_PER_KIND).map(compactEntity);
|
|
118
127
|
const sourceMarker = createFuliSourceMarker({
|
|
@@ -130,7 +139,7 @@ export async function recallTaskKnowledge(application, resolution, taskPrompt) {
|
|
|
130
139
|
failed_query_count: failedQueryCount,
|
|
131
140
|
trigger_categories: plan.trigger_categories,
|
|
132
141
|
query_count: plan.queries.length,
|
|
133
|
-
guidance:
|
|
142
|
+
guidance: taskKnowledgeRetrievalGuidance(),
|
|
134
143
|
...(searched ? {
|
|
135
144
|
sourceMarker,
|
|
136
145
|
noMatchSourceMarker: createFuliSourceMarker({
|
|
@@ -147,12 +156,16 @@ function recallWithoutSearch(status, plan) {
|
|
|
147
156
|
status,
|
|
148
157
|
trigger_categories: plan.trigger_categories,
|
|
149
158
|
query_count: plan.queries.length,
|
|
150
|
-
guidance:
|
|
159
|
+
guidance: taskKnowledgeRetrievalGuidance(),
|
|
151
160
|
facts: [],
|
|
152
161
|
entities: []
|
|
153
162
|
};
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
function taskKnowledgeRetrievalGuidance() {
|
|
166
|
+
return { ...TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE };
|
|
167
|
+
}
|
|
168
|
+
|
|
156
169
|
function rankedUnique(items, itemKind) {
|
|
157
170
|
const selected = new Map();
|
|
158
171
|
for (const item of items) {
|
|
@@ -209,9 +222,13 @@ function compactItem(item, content) {
|
|
|
209
222
|
};
|
|
210
223
|
}
|
|
211
224
|
|
|
212
|
-
function
|
|
225
|
+
function distinctivePromptTerms(prompt) {
|
|
213
226
|
const quoted = [...prompt.matchAll(/[“”"']([^“”"']{2,80})[“”"']/gu)]
|
|
214
227
|
.map((match) => match[1]);
|
|
228
|
+
const productPhrases = prompt.match(
|
|
229
|
+
/\b[A-Z][A-Za-z0-9]*(?:[ \t]+[A-Z][A-Za-z0-9]*){1,5}\b/gu
|
|
230
|
+
) ?? [];
|
|
231
|
+
const acronyms = prompt.match(/\b[A-Z]{2,}[A-Z0-9]*\b/gu) ?? [];
|
|
215
232
|
const versions = prompt.match(
|
|
216
233
|
/\bv?\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?\b/gu
|
|
217
234
|
) ?? [];
|
|
@@ -221,10 +238,44 @@ function distinctivePromptQuery(prompt) {
|
|
|
221
238
|
const urls = [...prompt.matchAll(/https?:\/\/[^\s<>()]+/gu)]
|
|
222
239
|
.map((match) => safeUrlSearchTerm(match[0]))
|
|
223
240
|
.filter(Boolean);
|
|
224
|
-
return unique([
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
return unique([
|
|
242
|
+
...quoted,
|
|
243
|
+
...productPhrases,
|
|
244
|
+
...acronyms,
|
|
245
|
+
...identifiers,
|
|
246
|
+
...urls,
|
|
247
|
+
...versions
|
|
248
|
+
]).slice(0, 4);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function filterRecallCandidates(items, focusTerms) {
|
|
252
|
+
const focusTokens = recallFocusTokens(focusTerms);
|
|
253
|
+
if (!focusTokens.length) return items;
|
|
254
|
+
return items.filter((item) =>
|
|
255
|
+
Boolean(item?.defined_project_id) || recallItemMatchesFocus(item, focusTokens));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function recallItemMatchesFocus(item, focusTokens) {
|
|
259
|
+
const text = singleLine([
|
|
260
|
+
item?.key,
|
|
261
|
+
item?.source_entity,
|
|
262
|
+
item?.target_entity,
|
|
263
|
+
item?.relationship,
|
|
264
|
+
item?.fact,
|
|
265
|
+
item?.name,
|
|
266
|
+
item?.type,
|
|
267
|
+
item?.summary,
|
|
268
|
+
...(Array.isArray(item?.source_uris) ? item.source_uris : [])
|
|
269
|
+
].filter(Boolean).join(' ')).toLocaleLowerCase('en-US');
|
|
270
|
+
return focusTokens.some((token) => text.includes(token));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function recallFocusTokens(terms) {
|
|
274
|
+
return unique(terms.flatMap((term) => [
|
|
275
|
+
...(term.match(/[\p{Script=Han}]{2,}/gu) ?? []),
|
|
276
|
+
...(term.match(/[A-Za-z][A-Za-z0-9_-]{2,}/gu) ?? [])
|
|
277
|
+
]).map((term) => term.toLocaleLowerCase('en-US'))
|
|
278
|
+
.filter((term) => !['http', 'https', 'www', 'version'].includes(term)));
|
|
228
279
|
}
|
|
229
280
|
|
|
230
281
|
function safeUrlSearchTerm(value) {
|