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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
+
from math import isfinite
|
|
2
3
|
|
|
3
4
|
from fastapi import HTTPException
|
|
4
5
|
|
|
@@ -33,12 +34,23 @@ async def read_collaboration_context(
|
|
|
33
34
|
space,
|
|
34
35
|
personal_project_id,
|
|
35
36
|
)
|
|
37
|
+
project_scopes = await _project_scopes(
|
|
38
|
+
store,
|
|
39
|
+
space,
|
|
40
|
+
personal_project_id,
|
|
41
|
+
)
|
|
42
|
+
inherited_project_ids = [
|
|
43
|
+
project_id
|
|
44
|
+
for project_id, scope in project_scopes.items()
|
|
45
|
+
if scope['scope_distance'] > 0
|
|
46
|
+
]
|
|
36
47
|
|
|
37
48
|
candidate_limit = min(max(limit * 3, 100), store.settings.graph_limit)
|
|
38
49
|
node_records, _, _ = await store.runtime.driver.execute_query(
|
|
39
50
|
_node_query(),
|
|
40
51
|
group_id=space['group_id'],
|
|
41
52
|
project_id=personal_project_id,
|
|
53
|
+
inherited_project_ids=inherited_project_ids,
|
|
42
54
|
limit=candidate_limit + 1,
|
|
43
55
|
routing_='r',
|
|
44
56
|
)
|
|
@@ -46,6 +58,7 @@ async def read_collaboration_context(
|
|
|
46
58
|
_edge_query(),
|
|
47
59
|
group_id=space['group_id'],
|
|
48
60
|
project_id=personal_project_id,
|
|
61
|
+
inherited_project_ids=inherited_project_ids,
|
|
49
62
|
limit=candidate_limit + 1,
|
|
50
63
|
routing_='r',
|
|
51
64
|
)
|
|
@@ -56,12 +69,12 @@ async def read_collaboration_context(
|
|
|
56
69
|
nodes = [
|
|
57
70
|
item
|
|
58
71
|
for record in node_records[:candidate_limit]
|
|
59
|
-
if (item := _node_item(record)) is not None
|
|
72
|
+
if (item := _node_item(record, project_scopes)) is not None
|
|
60
73
|
]
|
|
61
74
|
edge_pairs = [
|
|
62
75
|
(record, item)
|
|
63
76
|
for record in edge_records[:candidate_limit]
|
|
64
|
-
if (item := _edge_item(record)) is not None
|
|
77
|
+
if (item := _edge_item(record, project_scopes)) is not None
|
|
65
78
|
]
|
|
66
79
|
edges = [item for _, item in edge_pairs]
|
|
67
80
|
edge_endpoint_ids = {
|
|
@@ -84,19 +97,35 @@ async def read_collaboration_context(
|
|
|
84
97
|
project_preferences = [
|
|
85
98
|
item for item in items if item.preference_scope == 'project'
|
|
86
99
|
]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
(
|
|
101
|
+
authoritative_global,
|
|
102
|
+
authoritative_project,
|
|
103
|
+
cross_scope_lower_authority_ids,
|
|
104
|
+
) = _highest_authority_by_key(global_preferences, project_preferences)
|
|
105
|
+
(
|
|
106
|
+
project_effective,
|
|
107
|
+
project_conflicts,
|
|
108
|
+
overridden_inherited_ids,
|
|
109
|
+
overridden_lower_authority_ids,
|
|
110
|
+
) = _scope_conflict_free(authoritative_project)
|
|
111
|
+
overridden_lower_authority_ids = sorted({
|
|
112
|
+
*cross_scope_lower_authority_ids,
|
|
113
|
+
*overridden_lower_authority_ids,
|
|
114
|
+
})
|
|
115
|
+
# Any authoritative project-layer claim blocks global fallback for that
|
|
116
|
+
# semantic key. A same-layer project conflict must remain deferred instead
|
|
117
|
+
# of silently reviving a global value as effective.
|
|
118
|
+
project_keys = {item.preference_key for item in authoritative_project}
|
|
119
|
+
overridden_global_ids = sorted([
|
|
91
120
|
item.id
|
|
92
|
-
for item in
|
|
121
|
+
for item in authoritative_global
|
|
93
122
|
if item.preference_key in project_keys
|
|
94
|
-
]
|
|
95
|
-
|
|
96
|
-
item
|
|
97
|
-
for item in global_effective
|
|
123
|
+
])
|
|
124
|
+
global_effective, global_conflicts = _conflict_free([
|
|
125
|
+
item for item in authoritative_global
|
|
98
126
|
if item.preference_key not in project_keys
|
|
99
|
-
]
|
|
127
|
+
])
|
|
128
|
+
effective_preferences = [*global_effective, *project_effective]
|
|
100
129
|
|
|
101
130
|
return CollaborationContextResult(
|
|
102
131
|
personal_space_id=space_id,
|
|
@@ -106,10 +135,67 @@ async def read_collaboration_context(
|
|
|
106
135
|
effective_preferences=effective_preferences,
|
|
107
136
|
conflicts=[*global_conflicts, *project_conflicts],
|
|
108
137
|
overridden_global_ids=overridden_global_ids,
|
|
138
|
+
overridden_inherited_ids=overridden_inherited_ids,
|
|
139
|
+
overridden_lower_authority_ids=overridden_lower_authority_ids,
|
|
109
140
|
truncated=truncated,
|
|
110
141
|
)
|
|
111
142
|
|
|
112
143
|
|
|
144
|
+
async def _project_scopes(store, space, personal_project_id):
|
|
145
|
+
if not personal_project_id:
|
|
146
|
+
return {}
|
|
147
|
+
scopes = {
|
|
148
|
+
personal_project_id: {
|
|
149
|
+
'scope_distance': 0,
|
|
150
|
+
'scope_path': [personal_project_id],
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
154
|
+
'''
|
|
155
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
156
|
+
[:CONTAINS_PROJECT]->
|
|
157
|
+
(active:FuliPersonalProject {project_id: $active_project_id})
|
|
158
|
+
MATCH path=(active)-[:PERSONAL_PROJECT_RELATION*1..2]->
|
|
159
|
+
(source:FuliPersonalProject)
|
|
160
|
+
WHERE all(
|
|
161
|
+
relation IN relationships(path)
|
|
162
|
+
WHERE relation.relation_type IN ['PART_OF', 'USES_KNOWLEDGE_FROM']
|
|
163
|
+
AND relation.status = 'active'
|
|
164
|
+
AND relation.confirmation_authority = 'human_review'
|
|
165
|
+
)
|
|
166
|
+
AND EXISTS {
|
|
167
|
+
MATCH (space)-[:CONTAINS_PROJECT]->(source)
|
|
168
|
+
}
|
|
169
|
+
RETURN source.project_id AS project_id,
|
|
170
|
+
[node IN nodes(path) | node.project_id] AS scope_path,
|
|
171
|
+
length(path) AS scope_distance
|
|
172
|
+
ORDER BY scope_distance ASC, project_id ASC
|
|
173
|
+
LIMIT $inheritance_limit
|
|
174
|
+
''',
|
|
175
|
+
space_id=space['id'],
|
|
176
|
+
active_project_id=personal_project_id,
|
|
177
|
+
inheritance_limit=min(
|
|
178
|
+
int(getattr(getattr(store, 'settings', None), 'graph_limit', 500)),
|
|
179
|
+
64,
|
|
180
|
+
),
|
|
181
|
+
routing_='r',
|
|
182
|
+
)
|
|
183
|
+
for record in records:
|
|
184
|
+
project_id = record['project_id']
|
|
185
|
+
distance = int(record['scope_distance'])
|
|
186
|
+
path = list(record.get('scope_path') or [])
|
|
187
|
+
if len(path) != distance + 1 or len(path) != len(set(path)):
|
|
188
|
+
continue
|
|
189
|
+
existing = scopes.get(project_id)
|
|
190
|
+
if existing is not None and existing['scope_distance'] <= distance:
|
|
191
|
+
continue
|
|
192
|
+
scopes[project_id] = {
|
|
193
|
+
'scope_distance': distance,
|
|
194
|
+
'scope_path': path,
|
|
195
|
+
}
|
|
196
|
+
return scopes
|
|
197
|
+
|
|
198
|
+
|
|
113
199
|
def _node_query() -> str:
|
|
114
200
|
return '''
|
|
115
201
|
MATCH (node:Entity {group_id: $group_id})
|
|
@@ -120,7 +206,19 @@ def _node_query() -> str:
|
|
|
120
206
|
coalesce(node.fuli_preference_scope, 'global') = 'global'
|
|
121
207
|
OR (
|
|
122
208
|
node.fuli_preference_scope = 'project'
|
|
123
|
-
AND
|
|
209
|
+
AND (
|
|
210
|
+
node.fuli_preference_project_id = $project_id
|
|
211
|
+
OR (
|
|
212
|
+
node.fuli_preference_project_id IN $inherited_project_ids
|
|
213
|
+
AND (
|
|
214
|
+
node.fuli_inheritance_mode = 'descendants'
|
|
215
|
+
OR (
|
|
216
|
+
node.fuli_inheritance_mode = 'selected_projects'
|
|
217
|
+
AND $project_id IN coalesce(node.fuli_inherited_project_ids, [])
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
124
222
|
)
|
|
125
223
|
)
|
|
126
224
|
RETURN node.uuid AS id,
|
|
@@ -132,6 +230,9 @@ def _node_query() -> str:
|
|
|
132
230
|
node.fuli_preference_project_id AS preference_project_id,
|
|
133
231
|
node.fuli_attributes_json AS attributes_json,
|
|
134
232
|
node.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
233
|
+
node.fuli_reasoning_summary AS reasoning_summary,
|
|
234
|
+
coalesce(node.fuli_inheritance_mode, 'local_only') AS inheritance_mode,
|
|
235
|
+
coalesce(node.fuli_inherited_project_ids, []) AS inherited_project_ids,
|
|
135
236
|
node.fuli_confirmation_status AS confirmation_status,
|
|
136
237
|
node.created_at AS created_at
|
|
137
238
|
ORDER BY node.created_at DESC
|
|
@@ -150,7 +251,19 @@ def _edge_query() -> str:
|
|
|
150
251
|
coalesce(edge.fuli_preference_scope, 'global') = 'global'
|
|
151
252
|
OR (
|
|
152
253
|
edge.fuli_preference_scope = 'project'
|
|
153
|
-
AND
|
|
254
|
+
AND (
|
|
255
|
+
edge.fuli_preference_project_id = $project_id
|
|
256
|
+
OR (
|
|
257
|
+
edge.fuli_preference_project_id IN $inherited_project_ids
|
|
258
|
+
AND (
|
|
259
|
+
edge.fuli_inheritance_mode = 'descendants'
|
|
260
|
+
OR (
|
|
261
|
+
edge.fuli_inheritance_mode = 'selected_projects'
|
|
262
|
+
AND $project_id IN coalesce(edge.fuli_inherited_project_ids, [])
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
)
|
|
154
267
|
)
|
|
155
268
|
)
|
|
156
269
|
RETURN edge.uuid AS id,
|
|
@@ -165,6 +278,9 @@ def _edge_query() -> str:
|
|
|
165
278
|
edge.fuli_preference_project_id AS preference_project_id,
|
|
166
279
|
edge.fuli_attributes_json AS attributes_json,
|
|
167
280
|
edge.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
281
|
+
edge.fuli_reasoning_summary AS reasoning_summary,
|
|
282
|
+
coalesce(edge.fuli_inheritance_mode, 'local_only') AS inheritance_mode,
|
|
283
|
+
coalesce(edge.fuli_inherited_project_ids, []) AS inherited_project_ids,
|
|
168
284
|
edge.fuli_confirmation_status AS confirmation_status,
|
|
169
285
|
edge.created_at AS created_at
|
|
170
286
|
ORDER BY edge.created_at DESC
|
|
@@ -172,16 +288,17 @@ def _edge_query() -> str:
|
|
|
172
288
|
'''
|
|
173
289
|
|
|
174
290
|
|
|
175
|
-
def _node_item(record) -> CollaborationPreferenceItem | None:
|
|
291
|
+
def _node_item(record, project_scopes) -> CollaborationPreferenceItem | None:
|
|
176
292
|
return _preference_item(
|
|
177
293
|
record,
|
|
178
294
|
item_kind='entity',
|
|
179
295
|
title=record.get('title') or record.get('key') or record.get('id'),
|
|
180
296
|
instruction=record.get('instruction') or record.get('title') or '',
|
|
297
|
+
project_scopes=project_scopes,
|
|
181
298
|
)
|
|
182
299
|
|
|
183
300
|
|
|
184
|
-
def _edge_item(record) -> CollaborationPreferenceItem | None:
|
|
301
|
+
def _edge_item(record, project_scopes) -> CollaborationPreferenceItem | None:
|
|
185
302
|
source = record.get('source_name') or '偏好'
|
|
186
303
|
target = record.get('target_name') or '适用场景'
|
|
187
304
|
return _preference_item(
|
|
@@ -189,6 +306,7 @@ def _edge_item(record) -> CollaborationPreferenceItem | None:
|
|
|
189
306
|
item_kind='relationship',
|
|
190
307
|
title=f'{source} → {target}',
|
|
191
308
|
instruction=record.get('instruction') or '',
|
|
309
|
+
project_scopes=project_scopes,
|
|
192
310
|
)
|
|
193
311
|
|
|
194
312
|
|
|
@@ -198,6 +316,7 @@ def _preference_item(
|
|
|
198
316
|
item_kind: str,
|
|
199
317
|
title: str,
|
|
200
318
|
instruction: str,
|
|
319
|
+
project_scopes: dict[str, dict],
|
|
201
320
|
) -> CollaborationPreferenceItem | None:
|
|
202
321
|
basis_data = json_object(record.get('confirmation_basis_json'))
|
|
203
322
|
if not basis_data:
|
|
@@ -217,6 +336,35 @@ def _preference_item(
|
|
|
217
336
|
or attributes.get('preference_key')
|
|
218
337
|
or key
|
|
219
338
|
)
|
|
339
|
+
project_id = record.get('preference_project_id')
|
|
340
|
+
scope = project_scopes.get(project_id, {
|
|
341
|
+
'scope_distance': 0,
|
|
342
|
+
'scope_path': [],
|
|
343
|
+
})
|
|
344
|
+
scope_distance = int(scope['scope_distance'])
|
|
345
|
+
inheritance_mode = record.get('inheritance_mode') or 'local_only'
|
|
346
|
+
inherited_project_ids = list(record.get('inherited_project_ids') or [])
|
|
347
|
+
active_project_id = scope['scope_path'][0] if scope['scope_path'] else None
|
|
348
|
+
if scope_distance > 0 and (
|
|
349
|
+
inheritance_mode == 'local_only'
|
|
350
|
+
or (
|
|
351
|
+
inheritance_mode == 'selected_projects'
|
|
352
|
+
and active_project_id not in inherited_project_ids
|
|
353
|
+
)
|
|
354
|
+
):
|
|
355
|
+
return None
|
|
356
|
+
weight = attributes.get('weight')
|
|
357
|
+
if (
|
|
358
|
+
not isinstance(weight, (int, float))
|
|
359
|
+
or isinstance(weight, bool)
|
|
360
|
+
or not isfinite(weight)
|
|
361
|
+
or weight < 0
|
|
362
|
+
or weight > 1
|
|
363
|
+
):
|
|
364
|
+
weight = None
|
|
365
|
+
reason = attributes.get('reason')
|
|
366
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
367
|
+
reason = record.get('reasoning_summary') or basis.existence_reason
|
|
220
368
|
return CollaborationPreferenceItem(
|
|
221
369
|
id=str(record['id']),
|
|
222
370
|
item_kind=item_kind,
|
|
@@ -226,11 +374,20 @@ def _preference_item(
|
|
|
226
374
|
instruction=str(instruction),
|
|
227
375
|
profile_aspect=record['profile_aspect'],
|
|
228
376
|
preference_scope=record['preference_scope'],
|
|
229
|
-
preference_project_id=
|
|
377
|
+
preference_project_id=project_id,
|
|
230
378
|
attributes=attributes,
|
|
379
|
+
weight=weight,
|
|
380
|
+
reason=reason,
|
|
381
|
+
confirmation_basis=basis,
|
|
382
|
+
reasoning_summary=record.get('reasoning_summary'),
|
|
383
|
+
inheritance_mode=inheritance_mode,
|
|
384
|
+
inherited_project_ids=inherited_project_ids,
|
|
231
385
|
confirmation_status=confirmation_status,
|
|
232
386
|
confirmed_at=basis.confirmed_at,
|
|
233
387
|
created_at=_native_datetime(record.get('created_at')),
|
|
388
|
+
scope_distance=scope_distance,
|
|
389
|
+
inherited_from_project_id=project_id if scope_distance > 0 else None,
|
|
390
|
+
scope_path=list(scope['scope_path']),
|
|
234
391
|
)
|
|
235
392
|
|
|
236
393
|
|
|
@@ -274,6 +431,84 @@ def _conflict_free(
|
|
|
274
431
|
return effective, conflicts
|
|
275
432
|
|
|
276
433
|
|
|
434
|
+
def _scope_conflict_free(
|
|
435
|
+
items: list[CollaborationPreferenceItem],
|
|
436
|
+
) -> tuple[
|
|
437
|
+
list[CollaborationPreferenceItem],
|
|
438
|
+
list[CollaborationPreferenceConflict],
|
|
439
|
+
list[str],
|
|
440
|
+
list[str],
|
|
441
|
+
]:
|
|
442
|
+
grouped: dict[str, list[CollaborationPreferenceItem]] = defaultdict(list)
|
|
443
|
+
for item in items:
|
|
444
|
+
grouped[item.preference_key].append(item)
|
|
445
|
+
|
|
446
|
+
effective = []
|
|
447
|
+
conflicts = []
|
|
448
|
+
overridden = []
|
|
449
|
+
lower_authority = []
|
|
450
|
+
for candidates in grouped.values():
|
|
451
|
+
strongest_status = (
|
|
452
|
+
'confirmed'
|
|
453
|
+
if any(item.confirmation_status == 'confirmed' for item in candidates)
|
|
454
|
+
else 'agent_confirmed'
|
|
455
|
+
)
|
|
456
|
+
authoritative = [
|
|
457
|
+
item for item in candidates
|
|
458
|
+
if item.confirmation_status == strongest_status
|
|
459
|
+
]
|
|
460
|
+
lower_authority.extend(
|
|
461
|
+
item.id for item in candidates
|
|
462
|
+
if item.confirmation_status != strongest_status
|
|
463
|
+
)
|
|
464
|
+
nearest_distance = min(item.scope_distance for item in authoritative)
|
|
465
|
+
nearest = [
|
|
466
|
+
item for item in authoritative
|
|
467
|
+
if item.scope_distance == nearest_distance
|
|
468
|
+
]
|
|
469
|
+
overridden.extend(
|
|
470
|
+
item.id for item in authoritative
|
|
471
|
+
if item.scope_distance > nearest_distance
|
|
472
|
+
)
|
|
473
|
+
group_effective, group_conflicts = _conflict_free(nearest)
|
|
474
|
+
effective.extend(group_effective)
|
|
475
|
+
conflicts.extend(group_conflicts)
|
|
476
|
+
effective.sort(key=_sort_key)
|
|
477
|
+
conflicts.sort(key=lambda conflict: conflict.preference_key)
|
|
478
|
+
return effective, conflicts, sorted(overridden), sorted(lower_authority)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _highest_authority_by_key(
|
|
482
|
+
global_items: list[CollaborationPreferenceItem],
|
|
483
|
+
project_items: list[CollaborationPreferenceItem],
|
|
484
|
+
) -> tuple[
|
|
485
|
+
list[CollaborationPreferenceItem],
|
|
486
|
+
list[CollaborationPreferenceItem],
|
|
487
|
+
list[str],
|
|
488
|
+
]:
|
|
489
|
+
grouped: dict[str, list[CollaborationPreferenceItem]] = defaultdict(list)
|
|
490
|
+
for item in [*global_items, *project_items]:
|
|
491
|
+
grouped[item.preference_key].append(item)
|
|
492
|
+
selected_ids = set()
|
|
493
|
+
lower_authority_ids = []
|
|
494
|
+
for candidates in grouped.values():
|
|
495
|
+
strongest_status = (
|
|
496
|
+
'confirmed'
|
|
497
|
+
if any(item.confirmation_status == 'confirmed' for item in candidates)
|
|
498
|
+
else 'agent_confirmed'
|
|
499
|
+
)
|
|
500
|
+
for item in candidates:
|
|
501
|
+
if item.confirmation_status == strongest_status:
|
|
502
|
+
selected_ids.add(item.id)
|
|
503
|
+
else:
|
|
504
|
+
lower_authority_ids.append(item.id)
|
|
505
|
+
return (
|
|
506
|
+
[item for item in global_items if item.id in selected_ids],
|
|
507
|
+
[item for item in project_items if item.id in selected_ids],
|
|
508
|
+
sorted(lower_authority_ids),
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
277
512
|
def _sort_key(item: CollaborationPreferenceItem) -> tuple:
|
|
278
513
|
created = -item.created_at.timestamp() if item.created_at else 0
|
|
279
514
|
return (
|
|
@@ -281,6 +516,7 @@ def _sort_key(item: CollaborationPreferenceItem) -> tuple:
|
|
|
281
516
|
0 if item.confirmation_status == 'confirmed' else 1,
|
|
282
517
|
item.profile_aspect,
|
|
283
518
|
item.preference_key,
|
|
519
|
+
-(item.weight or 0),
|
|
284
520
|
created,
|
|
285
521
|
item.id,
|
|
286
522
|
)
|
|
@@ -224,8 +224,10 @@ async def _direct_child_project_ids(store, space_id, parent_project_id):
|
|
|
224
224
|
'''
|
|
225
225
|
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
226
226
|
[:CONTAINS_PROJECT]->(child:FuliPersonalProject)-
|
|
227
|
-
[:PERSONAL_PROJECT_RELATION {relation_type: 'PART_OF'}]->
|
|
227
|
+
[relation:PERSONAL_PROJECT_RELATION {relation_type: 'PART_OF'}]->
|
|
228
228
|
(parent:FuliPersonalProject {project_id: $parent_project_id})
|
|
229
|
+
WHERE relation.status = 'active'
|
|
230
|
+
AND relation.confirmation_authority = 'human_review'
|
|
229
231
|
RETURN child.project_id AS child_project_id
|
|
230
232
|
ORDER BY child.project_id
|
|
231
233
|
''',
|
|
@@ -315,8 +317,10 @@ def _promotion_query(item_kind):
|
|
|
315
317
|
UNWIND $source_project_ids AS source_project_id
|
|
316
318
|
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
317
319
|
(child:FuliPersonalProject {{project_id: source_project_id}})-
|
|
318
|
-
[:PERSONAL_PROJECT_RELATION {{relation_type: 'PART_OF'}}]->
|
|
320
|
+
[relation:PERSONAL_PROJECT_RELATION {{relation_type: 'PART_OF'}}]->
|
|
319
321
|
(parent)
|
|
322
|
+
WHERE relation.status = 'active'
|
|
323
|
+
AND relation.confirmation_authority = 'human_review'
|
|
320
324
|
WITH space, parent, collect(DISTINCT child.project_id) AS direct_children
|
|
321
325
|
WHERE size(direct_children) = size($source_project_ids)
|
|
322
326
|
{canonical_match}
|
|
@@ -12,6 +12,11 @@ class Settings(BaseSettings):
|
|
|
12
12
|
provider_id: str = 'local-personal'
|
|
13
13
|
provider_name: str = 'Fuli Personal Provider'
|
|
14
14
|
bootstrap_token: str = Field(min_length=24)
|
|
15
|
+
human_review_token: str | None = Field(default=None, min_length=32)
|
|
16
|
+
workflow_observation_token: str | None = Field(
|
|
17
|
+
default=None,
|
|
18
|
+
min_length=32,
|
|
19
|
+
)
|
|
15
20
|
neo4j_uri: str = 'bolt://neo4j:7687'
|
|
16
21
|
neo4j_user: str = 'neo4j'
|
|
17
22
|
neo4j_password: str = Field(min_length=8)
|
|
@@ -32,6 +32,9 @@ class GraphNode(StrictModel):
|
|
|
32
32
|
type: str
|
|
33
33
|
group_id: str
|
|
34
34
|
summary: str
|
|
35
|
+
key: str | None = None
|
|
36
|
+
preference_key: str | None = None
|
|
37
|
+
preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
|
|
35
38
|
origin_quadrant: EpistemicQuadrant = 'known_known'
|
|
36
39
|
current_quadrant: EpistemicQuadrant = 'known_known'
|
|
37
40
|
epistemic_status: EpistemicStatus = 'confirmed'
|
|
@@ -82,6 +85,9 @@ class GraphEdge(StrictModel):
|
|
|
82
85
|
target_name: str | None = None
|
|
83
86
|
type: str
|
|
84
87
|
fact: str
|
|
88
|
+
key: str | None = None
|
|
89
|
+
preference_key: str | None = None
|
|
90
|
+
preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
|
|
85
91
|
origin_quadrant: EpistemicQuadrant = 'known_known'
|
|
86
92
|
current_quadrant: EpistemicQuadrant = 'known_known'
|
|
87
93
|
epistemic_status: EpistemicStatus = 'confirmed'
|
|
@@ -2,6 +2,7 @@ from collections.abc import Iterable
|
|
|
2
2
|
|
|
3
3
|
from .graph_models import GraphEdge, GraphNode
|
|
4
4
|
from .models import ProjectProfile
|
|
5
|
+
from .provider_values import native_datetime
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def management_projection(
|
|
@@ -58,6 +59,7 @@ def management_projection(
|
|
|
58
59
|
target=target_id,
|
|
59
60
|
type=relation['relation_type'],
|
|
60
61
|
fact=_relation_fact(relation),
|
|
62
|
+
attributes=_relation_attributes(relation),
|
|
61
63
|
valid_at=None,
|
|
62
64
|
invalid_at=None,
|
|
63
65
|
))
|
|
@@ -118,6 +120,7 @@ def personal_project_projection(
|
|
|
118
120
|
target=related_id if current_is_source else root_id,
|
|
119
121
|
type=relation['relation_type'],
|
|
120
122
|
fact=_relation_fact(relation),
|
|
123
|
+
attributes=_relation_attributes(relation),
|
|
121
124
|
valid_at=None,
|
|
122
125
|
invalid_at=None,
|
|
123
126
|
))
|
|
@@ -183,6 +186,17 @@ def personal_project_projection(
|
|
|
183
186
|
return nodes, edges
|
|
184
187
|
|
|
185
188
|
|
|
189
|
+
def _relation_attributes(relation: dict) -> dict:
|
|
190
|
+
return {
|
|
191
|
+
'status': relation.get('status') or 'pending',
|
|
192
|
+
'confirmationAuthority': relation.get('confirmation_authority'),
|
|
193
|
+
'decisionRevision': int(relation.get('decision_revision') or 0),
|
|
194
|
+
'reviewReason': relation.get('review_reason'),
|
|
195
|
+
'reviewedBy': relation.get('reviewed_by'),
|
|
196
|
+
'reviewedAt': native_datetime(relation.get('reviewed_at')),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
186
200
|
def coalesce_personal_project_identity(
|
|
187
201
|
project: dict,
|
|
188
202
|
management_nodes: list[GraphNode],
|
|
@@ -12,7 +12,11 @@ from .models import (
|
|
|
12
12
|
from .knowledge_audit import read_knowledge_audits
|
|
13
13
|
from .knowledge_records import assignment_record, revision_record
|
|
14
14
|
from .personal_project_access import authorize_personal_project
|
|
15
|
-
from .provider_values import
|
|
15
|
+
from .provider_values import (
|
|
16
|
+
json_object,
|
|
17
|
+
native_datetime as _native_datetime,
|
|
18
|
+
preference_qualifiers,
|
|
19
|
+
)
|
|
16
20
|
from .project_knowledge import (
|
|
17
21
|
read_knowledge_conflicts,
|
|
18
22
|
read_personal_project_relations,
|
|
@@ -239,6 +243,7 @@ def _node_query(project_scoped: bool, paginated: bool = False) -> str:
|
|
|
239
243
|
node.fuli_last_feedback_at AS last_feedback_at,
|
|
240
244
|
coalesce(node.fuli_usage_generation, 1) AS usage_generation,
|
|
241
245
|
coalesce(node.fuli_attributes_json, '{}') AS attributes_json,
|
|
246
|
+
coalesce(node.fuli_key, node.uuid) AS key,
|
|
242
247
|
[episode IN episodes WHERE episode IS NOT NULL | episode.uuid] AS episodes,
|
|
243
248
|
node.created_at AS created_at,
|
|
244
249
|
node.fuli_invalid_at AS invalid_at,
|
|
@@ -358,6 +363,7 @@ def _edge_query(project_scoped: bool, paginated: bool = False) -> str:
|
|
|
358
363
|
edge.created_at AS created_at,
|
|
359
364
|
edge.fuli_confidence AS confidence,
|
|
360
365
|
coalesce(edge.fuli_attributes_json, '{}') AS attributes_json,
|
|
366
|
+
coalesce(edge.fuli_key, edge.uuid) AS key,
|
|
361
367
|
coalesce(edge.episodes, []) AS episodes
|
|
362
368
|
ORDER BY created_at DESC, id DESC
|
|
363
369
|
''' + pagination
|
|
@@ -467,12 +473,21 @@ def _graph_node(
|
|
|
467
473
|
status, basis, confidence, confirmation_explicit = _confirmation_projection(
|
|
468
474
|
record
|
|
469
475
|
)
|
|
476
|
+
attributes = json_object(record.get('attributes_json'))
|
|
470
477
|
return GraphNode(
|
|
471
478
|
id=record['id'],
|
|
472
479
|
name=record['name'],
|
|
473
480
|
type=record['type'],
|
|
474
481
|
group_id=record['group_id'],
|
|
475
482
|
summary=record['summary'],
|
|
483
|
+
key=record.get('key') or record['id'],
|
|
484
|
+
preference_key=(
|
|
485
|
+
attributes.get('preferenceKey')
|
|
486
|
+
or attributes.get('preference_key')
|
|
487
|
+
or record.get('key')
|
|
488
|
+
or record['id']
|
|
489
|
+
),
|
|
490
|
+
preference_qualifiers=preference_qualifiers(attributes),
|
|
476
491
|
origin_quadrant=record.get('origin_quadrant') or 'known_known',
|
|
477
492
|
current_quadrant=record.get('current_quadrant') or 'known_known',
|
|
478
493
|
epistemic_status=record.get('epistemic_status') or 'confirmed',
|
|
@@ -510,7 +525,7 @@ def _graph_node(
|
|
|
510
525
|
last_feedback_kind=record.get('last_feedback_kind'),
|
|
511
526
|
last_feedback_at=_native_datetime(record.get('last_feedback_at')),
|
|
512
527
|
usage_generation=int(record.get('usage_generation') or 1),
|
|
513
|
-
attributes=
|
|
528
|
+
attributes=attributes,
|
|
514
529
|
evidence=[evidence[item] for item in episode_ids if item in evidence],
|
|
515
530
|
created_at=_native_datetime(record.get('created_at')),
|
|
516
531
|
invalid_at=_native_datetime(record.get('invalid_at')),
|
|
@@ -537,6 +552,7 @@ def _graph_edge(
|
|
|
537
552
|
status, basis, confidence, confirmation_explicit = _confirmation_projection(
|
|
538
553
|
record
|
|
539
554
|
)
|
|
555
|
+
attributes = json_object(record.get('attributes_json'))
|
|
540
556
|
return GraphEdge(
|
|
541
557
|
id=record['id'],
|
|
542
558
|
source=record['source'],
|
|
@@ -545,6 +561,14 @@ def _graph_edge(
|
|
|
545
561
|
target_name=record.get('target_name'),
|
|
546
562
|
type=record['type'],
|
|
547
563
|
fact=record['fact'],
|
|
564
|
+
key=record.get('key') or record['id'],
|
|
565
|
+
preference_key=(
|
|
566
|
+
attributes.get('preferenceKey')
|
|
567
|
+
or attributes.get('preference_key')
|
|
568
|
+
or record.get('key')
|
|
569
|
+
or record['id']
|
|
570
|
+
),
|
|
571
|
+
preference_qualifiers=preference_qualifiers(attributes),
|
|
548
572
|
origin_quadrant=record.get('origin_quadrant') or 'known_known',
|
|
549
573
|
current_quadrant=record.get('current_quadrant') or 'known_known',
|
|
550
574
|
epistemic_status=record.get('epistemic_status') or 'confirmed',
|
|
@@ -588,7 +612,7 @@ def _graph_edge(
|
|
|
588
612
|
replaced_by_item_kind=record.get('replaced_by_item_kind'),
|
|
589
613
|
created_at=_native_datetime(record.get('created_at')),
|
|
590
614
|
confidence=record.get('confidence'),
|
|
591
|
-
attributes=
|
|
615
|
+
attributes=attributes,
|
|
592
616
|
episodes=episode_ids,
|
|
593
617
|
evidence=[evidence[item] for item in episode_ids if item in evidence],
|
|
594
618
|
revisions=revisions.get(record['id'], []),
|
|
@@ -280,6 +280,21 @@ async def set_preference_scope(
|
|
|
280
280
|
detail='only personal profile knowledge has a preference scope',
|
|
281
281
|
)
|
|
282
282
|
|
|
283
|
+
current_scope = current.get('preference_scope') or 'global'
|
|
284
|
+
if not (
|
|
285
|
+
current_scope == 'global'
|
|
286
|
+
and request.scope == 'project'
|
|
287
|
+
and request.project_id
|
|
288
|
+
):
|
|
289
|
+
raise HTTPException(
|
|
290
|
+
status_code=403,
|
|
291
|
+
detail=(
|
|
292
|
+
'direct preference scope changes may only narrow a global '
|
|
293
|
+
'preference to one project; broader or lateral scope changes '
|
|
294
|
+
'require the independently reviewed convergence workflow'
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
|
|
283
298
|
previous = _snapshot(current, request.item_kind)
|
|
284
299
|
next_value = {
|
|
285
300
|
**previous,
|
|
@@ -332,7 +347,10 @@ async def set_preference_scope(
|
|
|
332
347
|
created_by=actor['id'],
|
|
333
348
|
created_at=created_at,
|
|
334
349
|
)
|
|
335
|
-
if
|
|
350
|
+
if (
|
|
351
|
+
request.operation_actor == 'human'
|
|
352
|
+
and actor.get('_human_review_verified') is True
|
|
353
|
+
):
|
|
336
354
|
await record_human_change(
|
|
337
355
|
store,
|
|
338
356
|
actor,
|
|
@@ -10,6 +10,7 @@ from .provider_values import (
|
|
|
10
10
|
json_object,
|
|
11
11
|
native_datetime as _native_datetime,
|
|
12
12
|
normalized_text as _normalized,
|
|
13
|
+
preference_qualifiers,
|
|
13
14
|
)
|
|
14
15
|
from .search_projection import (
|
|
15
16
|
effective_confirmation_status,
|
|
@@ -255,6 +256,8 @@ async def personal_project_scopes(
|
|
|
255
256
|
WHERE all(
|
|
256
257
|
relation IN relationships(path)
|
|
257
258
|
WHERE relation.relation_type IN ['PART_OF', 'USES_KNOWLEDGE_FROM']
|
|
259
|
+
AND relation.status = 'active'
|
|
260
|
+
AND relation.confirmation_authority = 'human_review'
|
|
258
261
|
)
|
|
259
262
|
AND EXISTS {
|
|
260
263
|
MATCH (space)-[:CONTAINS_PROJECT]->(source)
|
|
@@ -634,6 +637,14 @@ def _dedupe_ranked_entities(ranked, active_project_id):
|
|
|
634
637
|
|
|
635
638
|
def _entity_search_result(record, score: float, space_id: str) -> EntitySearchResult:
|
|
636
639
|
value = dict(record)
|
|
640
|
+
attributes = json_object(value.get('attributes_json'))
|
|
641
|
+
value['preference_key'] = (
|
|
642
|
+
attributes.get('preferenceKey')
|
|
643
|
+
or attributes.get('preference_key')
|
|
644
|
+
or value.get('key')
|
|
645
|
+
or value.get('id')
|
|
646
|
+
)
|
|
647
|
+
value['preference_qualifiers'] = preference_qualifiers(attributes)
|
|
637
648
|
basis = json_object(value.pop('confirmation_basis_json', None))
|
|
638
649
|
source_references = value.pop('source_references', [])
|
|
639
650
|
created_at = _native_datetime(value.pop('created_at', None))
|