fuli-context 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +336 -0
- package/README.md +220 -174
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +1 -0
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
- package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
- package/dist/web/assets/console-B_Ena2DS.js +5 -0
- package/dist/web/assets/identity-CjBwYSMX.js +1 -0
- package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
- package/dist/web/assets/index-DAzeFJSc.js +2 -0
- package/dist/web/assets/model-C-5RVIJ2.js +1 -0
- package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
- package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
- package/dist/web/index.html +5 -6
- package/graph-provider/fuli_graph/app.py +48 -0
- package/graph-provider/fuli_graph/common_knowledge.py +443 -0
- package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
- package/graph-provider/fuli_graph/graph_models.py +9 -0
- package/graph-provider/fuli_graph/graph_query.py +25 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
- package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
- package/graph-provider/fuli_graph/knowledge_management.py +37 -0
- package/graph-provider/fuli_graph/knowledge_search.py +24 -2
- package/graph-provider/fuli_graph/models.py +69 -0
- package/graph-provider/fuli_graph/search_projection.py +59 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +97 -2
- package/package.json +3 -1
- package/skills/capturing-session-knowledge/SKILL.md +83 -13
- package/src/agent-tools/graph-definitions.js +181 -13
- package/src/agent-tools/graph-handlers.js +19 -0
- package/src/cli/command-registry.js +3 -58
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/local-runtime-options.js +6 -2
- package/src/cli.js +2 -37
- package/src/graphiti/agent-knowledge-workflows.js +543 -0
- package/src/graphiti/federated-application.js +85 -104
- package/src/graphiti/knowledge-provider-mapping.js +91 -0
- package/src/graphiti/provider-client.js +15 -0
- package/src/graphiti/source-uri.js +29 -0
- package/src/http/request-policy.js +37 -2
- package/src/local-runtime/lifecycle.js +11 -1
- package/src/mcp/common-knowledge-preview-tokens.js +67 -0
- package/src/mcp/create-mcp-server.js +43 -7
- package/src/mcp/instructions.js +6 -7
- package/src/mcp/lifecycle-audit.js +27 -0
- package/src/mcp/task-context-registry.js +93 -0
- package/src/mcp/tool-annotations.js +16 -1
- package/src/mcp/tool-result.js +24 -0
- package/src/runtime-options.js +7 -8
- package/src/server/lan-access.js +52 -0
- package/src/server.js +67 -8
- package/src/setup/agent-installation-status.js +14 -2
- package/src/setup/agents.js +12 -1
- package/src/setup/claude-code-config.js +194 -0
- package/src/setup/config-backup.js +20 -5
- package/src/setup/graph-runtime.js +78 -9
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
- package/dist/web/assets/console-tbvTUi6p.js +0 -1
- package/dist/web/assets/identity-B3s5CXhv.js +0 -1
- package/dist/web/assets/index-DwUDhQKc.js +0 -6
- package/dist/web/assets/model-DVT8sjy9.js +0 -1
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
- package/src/cli/commands/candidates.js +0 -17
- package/src/cli/commands/ingestion.js +0 -32
- package/src/cli/commands/reads.js +0 -50
- package/src/cli/commands/spaces.js +0 -20
- package/src/cli/invocation.js +0 -47
- package/src/cli/migrate-command.js +0 -71
- package/src/cli/migration-destination.js +0 -117
- package/src/storage/import-json.js +0 -70
- package/src/storage/legacy-record-validation.js +0 -152
- package/src/storage/legacy-snapshot-validation.js +0 -32
package/dist/web/index.html
CHANGED
|
@@ -9,12 +9,11 @@
|
|
|
9
9
|
/>
|
|
10
10
|
<link rel="icon" type="image/png" href="/assets/fuli-logo-DGuyqchQ.png" />
|
|
11
11
|
<title>复利 · Context Graph</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/console-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
16
|
-
<link rel="
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-Devf7glZ.css">
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-DAzeFJSc.js"></script>
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/console-B_Ena2DS.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/paths-BkULqmGI.js">
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/vue-router-DnD3hUm7.js">
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CpLDx3of.css">
|
|
18
17
|
</head>
|
|
19
18
|
<body>
|
|
20
19
|
<div id="app"></div>
|
|
@@ -7,6 +7,15 @@ from .auth import matches_bootstrap_token
|
|
|
7
7
|
from .config import Settings, get_settings
|
|
8
8
|
from .graph_models import GraphResult
|
|
9
9
|
from .knowledge_usage_models import KnowledgeUsageCreate, KnowledgeUsageResult
|
|
10
|
+
from .knowledge_feedback_models import (
|
|
11
|
+
KnowledgeFeedbackCreate,
|
|
12
|
+
KnowledgeFeedbackResult,
|
|
13
|
+
)
|
|
14
|
+
from .common_knowledge_models import (
|
|
15
|
+
CommonKnowledgePromotionPreview,
|
|
16
|
+
CommonKnowledgePromotionRequest,
|
|
17
|
+
CommonKnowledgePromotionResult,
|
|
18
|
+
)
|
|
10
19
|
from .project_action_models import (
|
|
11
20
|
KnowledgeProjectActionRequest,
|
|
12
21
|
KnowledgeProjectActionResult,
|
|
@@ -63,11 +72,16 @@ from .models import (
|
|
|
63
72
|
)
|
|
64
73
|
from .knowledge_audit import (
|
|
65
74
|
record_agent_views,
|
|
75
|
+
record_knowledge_feedback,
|
|
66
76
|
record_knowledge_usage,
|
|
67
77
|
review_human_change,
|
|
68
78
|
search_human_changes,
|
|
69
79
|
)
|
|
70
80
|
from .knowledge_batch_confirmation import confirm_knowledge_batch
|
|
81
|
+
from .common_knowledge import (
|
|
82
|
+
apply_common_knowledge_promotion,
|
|
83
|
+
preview_common_knowledge_promotion,
|
|
84
|
+
)
|
|
71
85
|
from .knowledge_management import (
|
|
72
86
|
reassign_knowledge_item,
|
|
73
87
|
revise_knowledge_item,
|
|
@@ -339,6 +353,16 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
339
353
|
) -> KnowledgeUsageResult:
|
|
340
354
|
return await record_knowledge_usage(store, actor, request)
|
|
341
355
|
|
|
356
|
+
@application.post(
|
|
357
|
+
'/v1/knowledge/feedback',
|
|
358
|
+
response_model=KnowledgeFeedbackResult,
|
|
359
|
+
)
|
|
360
|
+
async def record_negative_knowledge_evidence(
|
|
361
|
+
request: KnowledgeFeedbackCreate,
|
|
362
|
+
actor: Actor,
|
|
363
|
+
) -> KnowledgeFeedbackResult:
|
|
364
|
+
return await record_knowledge_feedback(store, actor, request)
|
|
365
|
+
|
|
342
366
|
@application.post(
|
|
343
367
|
'/v1/knowledge/items/{item_id}/agent-review',
|
|
344
368
|
response_model=KnowledgeAuditRecord,
|
|
@@ -392,6 +416,30 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
392
416
|
) -> KnowledgeRevisionRecord:
|
|
393
417
|
return await set_preference_scope(store, actor, item_id, request)
|
|
394
418
|
|
|
419
|
+
@application.post(
|
|
420
|
+
'/v1/knowledge/common-promotions/preview',
|
|
421
|
+
response_model=CommonKnowledgePromotionPreview,
|
|
422
|
+
)
|
|
423
|
+
async def preview_common_promotion(
|
|
424
|
+
request: CommonKnowledgePromotionRequest,
|
|
425
|
+
actor: Actor,
|
|
426
|
+
) -> CommonKnowledgePromotionPreview:
|
|
427
|
+
return await preview_common_knowledge_promotion(
|
|
428
|
+
store, actor, request
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
@application.post(
|
|
432
|
+
'/v1/knowledge/common-promotions',
|
|
433
|
+
response_model=CommonKnowledgePromotionResult,
|
|
434
|
+
)
|
|
435
|
+
async def apply_common_promotion(
|
|
436
|
+
request: CommonKnowledgePromotionRequest,
|
|
437
|
+
actor: Actor,
|
|
438
|
+
) -> CommonKnowledgePromotionResult:
|
|
439
|
+
return await apply_common_knowledge_promotion(
|
|
440
|
+
store, actor, request
|
|
441
|
+
)
|
|
442
|
+
|
|
395
443
|
@application.post(
|
|
396
444
|
'/v1/preference-conflicts/defer',
|
|
397
445
|
response_model=PreferenceConflictRecord,
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from fastapi import HTTPException
|
|
6
|
+
|
|
7
|
+
from .common_knowledge_models import (
|
|
8
|
+
CommonKnowledgePromotionPreview,
|
|
9
|
+
CommonKnowledgePromotionRequest,
|
|
10
|
+
CommonKnowledgePromotionResult,
|
|
11
|
+
)
|
|
12
|
+
from .knowledge_management import (
|
|
13
|
+
_read_assignment,
|
|
14
|
+
_read_item,
|
|
15
|
+
_snapshot,
|
|
16
|
+
_implicit_project_id,
|
|
17
|
+
)
|
|
18
|
+
from .personal_project_access import authorize_personal_project
|
|
19
|
+
from .provider_values import stable_uuid
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class _PromotionItem:
|
|
24
|
+
item_id: str
|
|
25
|
+
project_id: str
|
|
26
|
+
previous: dict
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class _PromotionPlan:
|
|
31
|
+
space: dict
|
|
32
|
+
canonical: _PromotionItem
|
|
33
|
+
duplicates: list[_PromotionItem]
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def source_project_ids(self) -> list[str]:
|
|
37
|
+
return sorted([
|
|
38
|
+
self.canonical.project_id,
|
|
39
|
+
*(item.project_id for item in self.duplicates),
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def preview_common_knowledge_promotion(
|
|
44
|
+
store,
|
|
45
|
+
actor: dict,
|
|
46
|
+
request: CommonKnowledgePromotionRequest,
|
|
47
|
+
) -> CommonKnowledgePromotionPreview:
|
|
48
|
+
plan = await _promotion_plan(store, actor, request)
|
|
49
|
+
return CommonKnowledgePromotionPreview(
|
|
50
|
+
status='ready',
|
|
51
|
+
personal_space_id=request.personal_space_id,
|
|
52
|
+
parent_project_id=request.parent_project_id,
|
|
53
|
+
item_kind=request.item_kind,
|
|
54
|
+
canonical_item_id=request.canonical_item_id,
|
|
55
|
+
duplicate_item_ids=sorted(request.duplicate_item_ids),
|
|
56
|
+
source_project_ids=plan.source_project_ids,
|
|
57
|
+
reason=request.reason,
|
|
58
|
+
human_confirmation_reason=request.human_confirmation_reason,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def apply_common_knowledge_promotion(
|
|
63
|
+
store,
|
|
64
|
+
actor: dict,
|
|
65
|
+
request: CommonKnowledgePromotionRequest,
|
|
66
|
+
*,
|
|
67
|
+
changed_at: datetime | None = None,
|
|
68
|
+
) -> CommonKnowledgePromotionResult:
|
|
69
|
+
plan = await _promotion_plan(store, actor, request)
|
|
70
|
+
promoted_at = changed_at or datetime.now(timezone.utc)
|
|
71
|
+
promotion_id = stable_uuid(
|
|
72
|
+
request.personal_space_id,
|
|
73
|
+
'common-knowledge-promotion',
|
|
74
|
+
request.parent_project_id,
|
|
75
|
+
request.item_kind,
|
|
76
|
+
request.canonical_item_id,
|
|
77
|
+
promoted_at.isoformat(),
|
|
78
|
+
)
|
|
79
|
+
assignment_id = stable_uuid(
|
|
80
|
+
request.personal_space_id,
|
|
81
|
+
'knowledge-assignment',
|
|
82
|
+
request.item_kind,
|
|
83
|
+
request.canonical_item_id,
|
|
84
|
+
)
|
|
85
|
+
canonical_current = {
|
|
86
|
+
**plan.canonical.previous,
|
|
87
|
+
'inheritanceMode': 'descendants',
|
|
88
|
+
'inheritedProjectIds': [],
|
|
89
|
+
}
|
|
90
|
+
revision_specs = [{
|
|
91
|
+
'revision_id': stable_uuid(
|
|
92
|
+
promotion_id, 'revision', request.canonical_item_id
|
|
93
|
+
),
|
|
94
|
+
'item_id': request.canonical_item_id,
|
|
95
|
+
'action': 'promote_common',
|
|
96
|
+
'previous_json': _json(plan.canonical.previous),
|
|
97
|
+
'current_json': _json(canonical_current),
|
|
98
|
+
}]
|
|
99
|
+
duplicate_specs = []
|
|
100
|
+
for duplicate in plan.duplicates:
|
|
101
|
+
current = {
|
|
102
|
+
**duplicate.previous,
|
|
103
|
+
'invalidAt': promoted_at.isoformat(),
|
|
104
|
+
'replacedByItemId': request.canonical_item_id,
|
|
105
|
+
'replacedByItemKind': request.item_kind,
|
|
106
|
+
}
|
|
107
|
+
revision_specs.append({
|
|
108
|
+
'revision_id': stable_uuid(
|
|
109
|
+
promotion_id, 'revision', duplicate.item_id
|
|
110
|
+
),
|
|
111
|
+
'item_id': duplicate.item_id,
|
|
112
|
+
'action': 'replace_common_duplicate',
|
|
113
|
+
'previous_json': _json(duplicate.previous),
|
|
114
|
+
'current_json': _json(current),
|
|
115
|
+
})
|
|
116
|
+
duplicate_specs.append({
|
|
117
|
+
'item_id': duplicate.item_id,
|
|
118
|
+
'project_id': duplicate.project_id,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
query = _promotion_query(request.item_kind)
|
|
122
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
123
|
+
query,
|
|
124
|
+
space_id=request.personal_space_id,
|
|
125
|
+
group_id=plan.space['group_id'],
|
|
126
|
+
parent_project_id=request.parent_project_id,
|
|
127
|
+
source_project_ids=plan.source_project_ids,
|
|
128
|
+
canonical_item_id=request.canonical_item_id,
|
|
129
|
+
canonical_source_project_id=plan.canonical.project_id,
|
|
130
|
+
duplicate_specs=duplicate_specs,
|
|
131
|
+
item_kind=request.item_kind,
|
|
132
|
+
assignment_id=assignment_id,
|
|
133
|
+
promotion_id=promotion_id,
|
|
134
|
+
revision_specs=revision_specs,
|
|
135
|
+
reason=request.reason,
|
|
136
|
+
human_confirmation_reason=request.human_confirmation_reason,
|
|
137
|
+
changed_by=actor['id'],
|
|
138
|
+
operation_actor=request.operation_actor,
|
|
139
|
+
changed_at=promoted_at,
|
|
140
|
+
)
|
|
141
|
+
if not records:
|
|
142
|
+
raise HTTPException(
|
|
143
|
+
status_code=409,
|
|
144
|
+
detail='common knowledge changed after preview; review it again',
|
|
145
|
+
)
|
|
146
|
+
record = records[0]
|
|
147
|
+
return CommonKnowledgePromotionResult(
|
|
148
|
+
status='promoted',
|
|
149
|
+
promotion_id=record.get('promotion_id') or promotion_id,
|
|
150
|
+
personal_space_id=request.personal_space_id,
|
|
151
|
+
parent_project_id=request.parent_project_id,
|
|
152
|
+
item_kind=request.item_kind,
|
|
153
|
+
canonical_item_id=request.canonical_item_id,
|
|
154
|
+
invalidated_item_ids=sorted(request.duplicate_item_ids),
|
|
155
|
+
source_project_ids=plan.source_project_ids,
|
|
156
|
+
revision_ids=record.get('revision_ids') or [
|
|
157
|
+
item['revision_id'] for item in revision_specs
|
|
158
|
+
],
|
|
159
|
+
reason=request.reason,
|
|
160
|
+
human_confirmation_reason=request.human_confirmation_reason,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def _promotion_plan(store, actor, request) -> _PromotionPlan:
|
|
165
|
+
store._require_personal()
|
|
166
|
+
space = await store.authorize(
|
|
167
|
+
actor, request.personal_space_id, 'maintainer'
|
|
168
|
+
)
|
|
169
|
+
if space['kind'] != 'personal':
|
|
170
|
+
raise HTTPException(
|
|
171
|
+
status_code=422,
|
|
172
|
+
detail='common knowledge promotion is personal-only',
|
|
173
|
+
)
|
|
174
|
+
await authorize_personal_project(
|
|
175
|
+
store, actor, space, request.parent_project_id
|
|
176
|
+
)
|
|
177
|
+
direct_child_ids = await _direct_child_project_ids(
|
|
178
|
+
store, space['id'], request.parent_project_id
|
|
179
|
+
)
|
|
180
|
+
item_ids = [request.canonical_item_id, *request.duplicate_item_ids]
|
|
181
|
+
items = []
|
|
182
|
+
for item_id in item_ids:
|
|
183
|
+
value = await _read_item(store, space, item_id, request.item_kind)
|
|
184
|
+
if value is None:
|
|
185
|
+
raise HTTPException(status_code=404, detail='knowledge item not found')
|
|
186
|
+
if value.get('profile_aspect'):
|
|
187
|
+
raise HTTPException(
|
|
188
|
+
status_code=422,
|
|
189
|
+
detail='personal profile knowledge cannot become common project knowledge',
|
|
190
|
+
)
|
|
191
|
+
if value.get('invalid_at'):
|
|
192
|
+
raise HTTPException(
|
|
193
|
+
status_code=409,
|
|
194
|
+
detail='historical knowledge cannot be promoted as common knowledge',
|
|
195
|
+
)
|
|
196
|
+
project_id = await _item_project_id(
|
|
197
|
+
store, space, item_id, request.item_kind
|
|
198
|
+
)
|
|
199
|
+
if project_id not in direct_child_ids:
|
|
200
|
+
raise HTTPException(
|
|
201
|
+
status_code=422,
|
|
202
|
+
detail='every promoted item must belong to a direct PART_OF child',
|
|
203
|
+
)
|
|
204
|
+
items.append(_PromotionItem(
|
|
205
|
+
item_id=item_id,
|
|
206
|
+
project_id=project_id,
|
|
207
|
+
previous=_snapshot(value, request.item_kind),
|
|
208
|
+
))
|
|
209
|
+
project_ids = [item.project_id for item in items]
|
|
210
|
+
if len(set(project_ids)) != len(project_ids):
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=422,
|
|
213
|
+
detail='common knowledge evidence must come from distinct child projects',
|
|
214
|
+
)
|
|
215
|
+
return _PromotionPlan(
|
|
216
|
+
space=space,
|
|
217
|
+
canonical=items[0],
|
|
218
|
+
duplicates=items[1:],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def _direct_child_project_ids(store, space_id, parent_project_id):
|
|
223
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
224
|
+
'''
|
|
225
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
226
|
+
[:CONTAINS_PROJECT]->(child:FuliPersonalProject)-
|
|
227
|
+
[:PERSONAL_PROJECT_RELATION {relation_type: 'PART_OF'}]->
|
|
228
|
+
(parent:FuliPersonalProject {project_id: $parent_project_id})
|
|
229
|
+
RETURN child.project_id AS child_project_id
|
|
230
|
+
ORDER BY child.project_id
|
|
231
|
+
''',
|
|
232
|
+
space_id=space_id,
|
|
233
|
+
parent_project_id=parent_project_id,
|
|
234
|
+
routing_='r',
|
|
235
|
+
)
|
|
236
|
+
return {record['child_project_id'] for record in records}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _item_project_id(store, space, item_id, item_kind):
|
|
240
|
+
assignment = await _read_assignment(
|
|
241
|
+
store, space['id'], item_id, item_kind
|
|
242
|
+
)
|
|
243
|
+
if assignment:
|
|
244
|
+
return assignment.project_id
|
|
245
|
+
return await _implicit_project_id(store, space, item_id, item_kind)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _promotion_query(item_kind):
|
|
249
|
+
canonical_project_guard = _current_project_guard(
|
|
250
|
+
item_kind,
|
|
251
|
+
item_variable='canonical',
|
|
252
|
+
variable_prefix='canonical',
|
|
253
|
+
item_id_expression='$canonical_item_id',
|
|
254
|
+
project_id_expression='$canonical_source_project_id',
|
|
255
|
+
)
|
|
256
|
+
duplicate_project_guard = _current_project_guard(
|
|
257
|
+
item_kind,
|
|
258
|
+
item_variable='duplicate',
|
|
259
|
+
variable_prefix='duplicate',
|
|
260
|
+
item_id_expression='duplicate_spec.item_id',
|
|
261
|
+
project_id_expression='duplicate_spec.project_id',
|
|
262
|
+
)
|
|
263
|
+
if item_kind == 'entity':
|
|
264
|
+
canonical_match = f'''
|
|
265
|
+
MATCH (canonical:Entity {{
|
|
266
|
+
uuid: $canonical_item_id,
|
|
267
|
+
group_id: $group_id
|
|
268
|
+
}})
|
|
269
|
+
WHERE canonical.fuli_invalid_at IS NULL
|
|
270
|
+
{canonical_project_guard}
|
|
271
|
+
'''
|
|
272
|
+
duplicate_match = f'''
|
|
273
|
+
MATCH (duplicate:Entity {{
|
|
274
|
+
uuid: duplicate_spec.item_id,
|
|
275
|
+
group_id: $group_id
|
|
276
|
+
}})
|
|
277
|
+
WHERE duplicate.fuli_invalid_at IS NULL
|
|
278
|
+
{duplicate_project_guard}
|
|
279
|
+
'''
|
|
280
|
+
duplicate_update = '''
|
|
281
|
+
FOREACH (pair IN duplicate_pairs |
|
|
282
|
+
SET pair.item.fuli_invalid_at = $changed_at,
|
|
283
|
+
pair.item.fuli_replaced_by_item_id = $canonical_item_id,
|
|
284
|
+
pair.item.fuli_replaced_by_item_kind = $item_kind
|
|
285
|
+
)
|
|
286
|
+
'''
|
|
287
|
+
else:
|
|
288
|
+
canonical_match = f'''
|
|
289
|
+
MATCH ()-[canonical:RELATES_TO {{
|
|
290
|
+
uuid: $canonical_item_id,
|
|
291
|
+
group_id: $group_id
|
|
292
|
+
}}]->()
|
|
293
|
+
WHERE canonical.invalid_at IS NULL
|
|
294
|
+
{canonical_project_guard}
|
|
295
|
+
'''
|
|
296
|
+
duplicate_match = f'''
|
|
297
|
+
MATCH ()-[duplicate:RELATES_TO {{
|
|
298
|
+
uuid: duplicate_spec.item_id,
|
|
299
|
+
group_id: $group_id
|
|
300
|
+
}}]->()
|
|
301
|
+
WHERE duplicate.invalid_at IS NULL
|
|
302
|
+
{duplicate_project_guard}
|
|
303
|
+
'''
|
|
304
|
+
duplicate_update = '''
|
|
305
|
+
FOREACH (pair IN duplicate_pairs |
|
|
306
|
+
SET pair.item.invalid_at = $changed_at,
|
|
307
|
+
pair.item.fuli_replaced_by_item_id = $canonical_item_id,
|
|
308
|
+
pair.item.fuli_replaced_by_item_kind = $item_kind
|
|
309
|
+
)
|
|
310
|
+
'''
|
|
311
|
+
return f'''
|
|
312
|
+
MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})-
|
|
313
|
+
[:CONTAINS_PROJECT]->
|
|
314
|
+
(parent:FuliPersonalProject {{project_id: $parent_project_id}})
|
|
315
|
+
UNWIND $source_project_ids AS source_project_id
|
|
316
|
+
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
317
|
+
(child:FuliPersonalProject {{project_id: source_project_id}})-
|
|
318
|
+
[:PERSONAL_PROJECT_RELATION {{relation_type: 'PART_OF'}}]->
|
|
319
|
+
(parent)
|
|
320
|
+
WITH space, parent, collect(DISTINCT child.project_id) AS direct_children
|
|
321
|
+
WHERE size(direct_children) = size($source_project_ids)
|
|
322
|
+
{canonical_match}
|
|
323
|
+
WITH space, parent, canonical, direct_children
|
|
324
|
+
UNWIND $duplicate_specs AS duplicate_spec
|
|
325
|
+
{duplicate_match}
|
|
326
|
+
WITH space, parent, canonical, direct_children,
|
|
327
|
+
collect({{item: duplicate, spec: duplicate_spec}}) AS duplicate_pairs
|
|
328
|
+
WHERE size(duplicate_pairs) = size($duplicate_specs)
|
|
329
|
+
SET canonical.fuli_inheritance_mode = 'descendants',
|
|
330
|
+
canonical.fuli_inherited_project_ids = []
|
|
331
|
+
{duplicate_update}
|
|
332
|
+
MERGE (assignment:FuliKnowledgeAssignment {{
|
|
333
|
+
space_id: $space_id,
|
|
334
|
+
item_kind: $item_kind,
|
|
335
|
+
item_id: $canonical_item_id
|
|
336
|
+
}})
|
|
337
|
+
ON CREATE SET assignment.id = $assignment_id,
|
|
338
|
+
assignment.created_at = $changed_at
|
|
339
|
+
SET assignment.previous_project_id = $canonical_source_project_id,
|
|
340
|
+
assignment.project_id = $parent_project_id,
|
|
341
|
+
assignment.reason = $reason,
|
|
342
|
+
assignment.changed_by = $changed_by,
|
|
343
|
+
assignment.updated_at = $changed_at
|
|
344
|
+
WITH space, parent, assignment
|
|
345
|
+
OPTIONAL MATCH (assignment)-[old:ASSIGNED_TO]->
|
|
346
|
+
(:FuliPersonalProject)
|
|
347
|
+
DELETE old
|
|
348
|
+
MERGE (assignment)-[:ASSIGNED_TO]->(parent)
|
|
349
|
+
MERGE (space)-[:HAS_KNOWLEDGE_ASSIGNMENT]->(assignment)
|
|
350
|
+
CREATE (promotion:FuliCommonKnowledgePromotion {{
|
|
351
|
+
id: $promotion_id,
|
|
352
|
+
space_id: $space_id,
|
|
353
|
+
parent_project_id: $parent_project_id,
|
|
354
|
+
item_kind: $item_kind,
|
|
355
|
+
canonical_item_id: $canonical_item_id,
|
|
356
|
+
duplicate_item_ids: [item IN $duplicate_specs | item.item_id],
|
|
357
|
+
source_project_ids: $source_project_ids,
|
|
358
|
+
reason: $reason,
|
|
359
|
+
human_confirmation_reason: $human_confirmation_reason,
|
|
360
|
+
operation_actor: $operation_actor,
|
|
361
|
+
created_by: $changed_by,
|
|
362
|
+
created_at: $changed_at
|
|
363
|
+
}})
|
|
364
|
+
MERGE (space)-[:HAS_COMMON_KNOWLEDGE_PROMOTION]->(promotion)
|
|
365
|
+
WITH promotion, assignment, space
|
|
366
|
+
UNWIND $revision_specs AS revision_spec
|
|
367
|
+
CREATE (revision:FuliKnowledgeRevision {{
|
|
368
|
+
id: revision_spec.revision_id,
|
|
369
|
+
space_id: $space_id,
|
|
370
|
+
item_id: revision_spec.item_id,
|
|
371
|
+
item_kind: $item_kind,
|
|
372
|
+
action: revision_spec.action,
|
|
373
|
+
reason: $reason,
|
|
374
|
+
previous_json: revision_spec.previous_json,
|
|
375
|
+
current_json: revision_spec.current_json,
|
|
376
|
+
created_by: $changed_by,
|
|
377
|
+
created_at: $changed_at
|
|
378
|
+
}})
|
|
379
|
+
MERGE (space)-[:HAS_KNOWLEDGE_REVISION]->(revision)
|
|
380
|
+
RETURN promotion.id AS promotion_id,
|
|
381
|
+
assignment.id AS assignment_id,
|
|
382
|
+
collect(revision.id) AS revision_ids
|
|
383
|
+
'''
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _current_project_guard(
|
|
387
|
+
item_kind,
|
|
388
|
+
*,
|
|
389
|
+
item_variable,
|
|
390
|
+
variable_prefix,
|
|
391
|
+
item_id_expression,
|
|
392
|
+
project_id_expression,
|
|
393
|
+
):
|
|
394
|
+
if item_kind == 'entity':
|
|
395
|
+
implicit_project_match = f'''
|
|
396
|
+
MATCH ({variable_prefix}_episode:Episodic {{group_id: $group_id}})-
|
|
397
|
+
[:MENTIONS]->({item_variable})
|
|
398
|
+
'''
|
|
399
|
+
else:
|
|
400
|
+
implicit_project_match = f'''
|
|
401
|
+
UNWIND coalesce({item_variable}.episodes, [])
|
|
402
|
+
AS {variable_prefix}_episode_id
|
|
403
|
+
MATCH ({variable_prefix}_episode:Episodic {{
|
|
404
|
+
uuid: {variable_prefix}_episode_id,
|
|
405
|
+
group_id: $group_id
|
|
406
|
+
}})
|
|
407
|
+
'''
|
|
408
|
+
return f'''
|
|
409
|
+
AND (
|
|
410
|
+
EXISTS {{
|
|
411
|
+
MATCH (space)-[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
412
|
+
({variable_prefix}_assignment:FuliKnowledgeAssignment {{
|
|
413
|
+
item_kind: $item_kind,
|
|
414
|
+
item_id: {item_id_expression}
|
|
415
|
+
}})
|
|
416
|
+
WHERE {variable_prefix}_assignment.project_id = {project_id_expression}
|
|
417
|
+
}}
|
|
418
|
+
OR (
|
|
419
|
+
NOT EXISTS {{
|
|
420
|
+
MATCH (space)-[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
421
|
+
(:FuliKnowledgeAssignment {{
|
|
422
|
+
item_kind: $item_kind,
|
|
423
|
+
item_id: {item_id_expression}
|
|
424
|
+
}})
|
|
425
|
+
}}
|
|
426
|
+
AND EXISTS {{
|
|
427
|
+
{implicit_project_match}
|
|
428
|
+
WHERE {variable_prefix}_episode.fuli_personal_project_id
|
|
429
|
+
IS NOT NULL
|
|
430
|
+
WITH collect(DISTINCT
|
|
431
|
+
{variable_prefix}_episode.fuli_personal_project_id)
|
|
432
|
+
AS {variable_prefix}_project_ids
|
|
433
|
+
WHERE {variable_prefix}_project_ids =
|
|
434
|
+
[{project_id_expression}]
|
|
435
|
+
RETURN {variable_prefix}_project_ids
|
|
436
|
+
}}
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
'''
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _json(value):
|
|
443
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, model_validator
|
|
4
|
+
|
|
5
|
+
from .models import KnowledgeItemKind, KnowledgeOperationActor, StrictModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommonKnowledgePromotionRequest(StrictModel):
|
|
9
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
10
|
+
parent_project_id: str = Field(min_length=1, max_length=128)
|
|
11
|
+
item_kind: KnowledgeItemKind
|
|
12
|
+
canonical_item_id: str = Field(min_length=1, max_length=256)
|
|
13
|
+
duplicate_item_ids: list[str] = Field(min_length=1, max_length=31)
|
|
14
|
+
reason: str = Field(min_length=1, max_length=2000)
|
|
15
|
+
human_confirmation_reason: str = Field(min_length=1, max_length=2000)
|
|
16
|
+
operation_actor: KnowledgeOperationActor = 'agent'
|
|
17
|
+
|
|
18
|
+
@model_validator(mode='after')
|
|
19
|
+
def validate_item_ids(self):
|
|
20
|
+
if len(set(self.duplicate_item_ids)) != len(self.duplicate_item_ids):
|
|
21
|
+
raise ValueError('duplicate item ids must be unique')
|
|
22
|
+
if self.canonical_item_id in self.duplicate_item_ids:
|
|
23
|
+
raise ValueError('canonical item cannot also be a duplicate')
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommonKnowledgePromotionPreview(StrictModel):
|
|
28
|
+
status: Literal['ready']
|
|
29
|
+
personal_space_id: str
|
|
30
|
+
parent_project_id: str
|
|
31
|
+
item_kind: KnowledgeItemKind
|
|
32
|
+
canonical_item_id: str
|
|
33
|
+
duplicate_item_ids: list[str]
|
|
34
|
+
source_project_ids: list[str]
|
|
35
|
+
inheritance_mode: Literal['descendants'] = 'descendants'
|
|
36
|
+
atomic: bool = True
|
|
37
|
+
requires_human_confirmation: bool = True
|
|
38
|
+
reason: str
|
|
39
|
+
human_confirmation_reason: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CommonKnowledgePromotionResult(StrictModel):
|
|
43
|
+
status: Literal['promoted']
|
|
44
|
+
promotion_id: str
|
|
45
|
+
personal_space_id: str
|
|
46
|
+
parent_project_id: str
|
|
47
|
+
item_kind: KnowledgeItemKind
|
|
48
|
+
canonical_item_id: str
|
|
49
|
+
invalidated_item_ids: list[str]
|
|
50
|
+
source_project_ids: list[str]
|
|
51
|
+
inheritance_mode: Literal['descendants'] = 'descendants'
|
|
52
|
+
revision_ids: list[str]
|
|
53
|
+
reason: str
|
|
54
|
+
human_confirmation_reason: str
|
|
@@ -13,6 +13,7 @@ from .models import (
|
|
|
13
13
|
KnowledgeAssignmentRecord,
|
|
14
14
|
KnowledgeAuditRecord,
|
|
15
15
|
KnowledgeInheritanceMode,
|
|
16
|
+
KnowledgeFeedbackKind,
|
|
16
17
|
KnowledgeItemKind,
|
|
17
18
|
KnowledgeRevisionRecord,
|
|
18
19
|
PersonalProfileAspect,
|
|
@@ -55,6 +56,10 @@ class GraphNode(StrictModel):
|
|
|
55
56
|
qualified_use_count: int = Field(default=0, ge=0)
|
|
56
57
|
distinct_task_count: int = Field(default=0, ge=0)
|
|
57
58
|
last_used_at: datetime | None = None
|
|
59
|
+
negative_evidence_count: int = Field(default=0, ge=0)
|
|
60
|
+
requires_attention: bool = False
|
|
61
|
+
last_feedback_kind: KnowledgeFeedbackKind | None = None
|
|
62
|
+
last_feedback_at: datetime | None = None
|
|
58
63
|
usage_generation: int = Field(default=1, ge=1)
|
|
59
64
|
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
60
65
|
evidence: list[GraphEvidence] = Field(default_factory=list)
|
|
@@ -99,6 +104,10 @@ class GraphEdge(StrictModel):
|
|
|
99
104
|
qualified_use_count: int = Field(default=0, ge=0)
|
|
100
105
|
distinct_task_count: int = Field(default=0, ge=0)
|
|
101
106
|
last_used_at: datetime | None = None
|
|
107
|
+
negative_evidence_count: int = Field(default=0, ge=0)
|
|
108
|
+
requires_attention: bool = False
|
|
109
|
+
last_feedback_kind: KnowledgeFeedbackKind | None = None
|
|
110
|
+
last_feedback_at: datetime | None = None
|
|
102
111
|
usage_generation: int = Field(default=1, ge=1)
|
|
103
112
|
valid_at: datetime | None
|
|
104
113
|
invalid_at: datetime | None
|
|
@@ -220,6 +220,12 @@ def _node_query(project_scoped: bool) -> str:
|
|
|
220
220
|
coalesce(node.fuli_distinct_task_count, 0)
|
|
221
221
|
AS distinct_task_count,
|
|
222
222
|
node.fuli_last_used_at AS last_used_at,
|
|
223
|
+
coalesce(node.fuli_negative_evidence_count, 0)
|
|
224
|
+
AS negative_evidence_count,
|
|
225
|
+
coalesce(node.fuli_requires_attention, false)
|
|
226
|
+
AS requires_attention,
|
|
227
|
+
node.fuli_last_feedback_kind AS last_feedback_kind,
|
|
228
|
+
node.fuli_last_feedback_at AS last_feedback_at,
|
|
223
229
|
coalesce(node.fuli_usage_generation, 1) AS usage_generation,
|
|
224
230
|
coalesce(node.fuli_attributes_json, '{}') AS attributes_json,
|
|
225
231
|
[episode IN episodes WHERE episode IS NOT NULL | episode.uuid] AS episodes,
|
|
@@ -295,6 +301,12 @@ def _edge_query(project_scoped: bool) -> str:
|
|
|
295
301
|
coalesce(edge.fuli_distinct_task_count, 0)
|
|
296
302
|
AS distinct_task_count,
|
|
297
303
|
edge.fuli_last_used_at AS last_used_at,
|
|
304
|
+
coalesce(edge.fuli_negative_evidence_count, 0)
|
|
305
|
+
AS negative_evidence_count,
|
|
306
|
+
coalesce(edge.fuli_requires_attention, false)
|
|
307
|
+
AS requires_attention,
|
|
308
|
+
edge.fuli_last_feedback_kind AS last_feedback_kind,
|
|
309
|
+
edge.fuli_last_feedback_at AS last_feedback_at,
|
|
298
310
|
coalesce(edge.fuli_usage_generation, 1) AS usage_generation,
|
|
299
311
|
edge.valid_at AS valid_at, edge.invalid_at AS invalid_at,
|
|
300
312
|
edge.fuli_replaced_by_item_id AS replaced_by_item_id,
|
|
@@ -331,6 +343,7 @@ async def _read_evidence(store, episode_ids: list[str]) -> dict[str, GraphEviden
|
|
|
331
343
|
RETURN episode.uuid AS id, episode.name AS name,
|
|
332
344
|
episode.source_description AS source_description,
|
|
333
345
|
coalesce(episode.fuli_source_kind, 'unknown') AS source_kind,
|
|
346
|
+
episode.fuli_source_uri AS source_uri,
|
|
334
347
|
coalesce(episode.fuli_summary, '') AS summary,
|
|
335
348
|
episode.fuli_session_id AS session_id,
|
|
336
349
|
episode.fuli_source_application AS source_application,
|
|
@@ -446,6 +459,12 @@ def _graph_node(
|
|
|
446
459
|
qualified_use_count=int(record.get('qualified_use_count') or 0),
|
|
447
460
|
distinct_task_count=int(record.get('distinct_task_count') or 0),
|
|
448
461
|
last_used_at=_native_datetime(record.get('last_used_at')),
|
|
462
|
+
negative_evidence_count=int(
|
|
463
|
+
record.get('negative_evidence_count') or 0
|
|
464
|
+
),
|
|
465
|
+
requires_attention=record.get('requires_attention') is True,
|
|
466
|
+
last_feedback_kind=record.get('last_feedback_kind'),
|
|
467
|
+
last_feedback_at=_native_datetime(record.get('last_feedback_at')),
|
|
449
468
|
usage_generation=int(record.get('usage_generation') or 1),
|
|
450
469
|
attributes=json_object(record.get('attributes_json')),
|
|
451
470
|
evidence=[evidence[item] for item in episode_ids if item in evidence],
|
|
@@ -510,6 +529,12 @@ def _graph_edge(
|
|
|
510
529
|
qualified_use_count=int(record.get('qualified_use_count') or 0),
|
|
511
530
|
distinct_task_count=int(record.get('distinct_task_count') or 0),
|
|
512
531
|
last_used_at=_native_datetime(record.get('last_used_at')),
|
|
532
|
+
negative_evidence_count=int(
|
|
533
|
+
record.get('negative_evidence_count') or 0
|
|
534
|
+
),
|
|
535
|
+
requires_attention=record.get('requires_attention') is True,
|
|
536
|
+
last_feedback_kind=record.get('last_feedback_kind'),
|
|
537
|
+
last_feedback_at=_native_datetime(record.get('last_feedback_at')),
|
|
513
538
|
usage_generation=int(record.get('usage_generation') or 1),
|
|
514
539
|
valid_at=_native_datetime(record.get('valid_at')),
|
|
515
540
|
invalid_at=_native_datetime(record.get('invalid_at')),
|