fuli-context 0.2.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/LICENSE +201 -0
- package/README.md +178 -0
- package/compose.graphiti.yml +86 -0
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
- package/dist/web/assets/console-tbvTUi6p.js +1 -0
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-B3s5CXhv.js +1 -0
- package/dist/web/assets/index-Devf7glZ.css +1 -0
- package/dist/web/assets/index-DwUDhQKc.js +6 -0
- package/dist/web/assets/model-DVT8sjy9.js +1 -0
- package/dist/web/assets/paths-B790iniO.js +1 -0
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
- package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
- package/dist/web/index.html +22 -0
- package/graph-provider/Dockerfile +16 -0
- package/graph-provider/fuli_graph/__init__.py +5 -0
- package/graph-provider/fuli_graph/app.py +547 -0
- package/graph-provider/fuli_graph/auth.py +17 -0
- package/graph-provider/fuli_graph/collaboration_context.py +290 -0
- package/graph-provider/fuli_graph/config.py +34 -0
- package/graph-provider/fuli_graph/graph_models.py +123 -0
- package/graph-provider/fuli_graph/graph_projection.py +405 -0
- package/graph-provider/fuli_graph/graph_query.py +561 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
- package/graph-provider/fuli_graph/knowledge_management.py +816 -0
- package/graph-provider/fuli_graph/knowledge_records.py +65 -0
- package/graph-provider/fuli_graph/knowledge_search.py +723 -0
- package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
- package/graph-provider/fuli_graph/model_validation.py +58 -0
- package/graph-provider/fuli_graph/models.py +1179 -0
- package/graph-provider/fuli_graph/personal_project_access.py +29 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
- package/graph-provider/fuli_graph/project_action_models.py +142 -0
- package/graph-provider/fuli_graph/project_knowledge.py +707 -0
- package/graph-provider/fuli_graph/project_management.py +251 -0
- package/graph-provider/fuli_graph/provider_values.py +40 -0
- package/graph-provider/fuli_graph/runtime.py +350 -0
- package/graph-provider/fuli_graph/search_projection.py +156 -0
- package/graph-provider/fuli_graph/store.py +24 -0
- package/graph-provider/fuli_graph/store_identity.py +237 -0
- package/graph-provider/fuli_graph/store_knowledge.py +447 -0
- package/graph-provider/fuli_graph/store_projects.py +242 -0
- package/graph-provider/fuli_graph/store_publication.py +370 -0
- package/graph-provider/fuli_graph/store_records.py +134 -0
- package/graph-provider/pyproject.toml +35 -0
- package/npm-shrinkwrap.json +6045 -0
- package/package.json +99 -0
- package/skills/capturing-session-knowledge/SKILL.md +226 -0
- package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
- package/skills/grilling-project/SKILL.md +129 -0
- package/skills/grilling-project/agents/openai.yaml +4 -0
- package/skills/grilling-project/references/project-profile.md +105 -0
- package/src/agent/candidate-read-service.js +53 -0
- package/src/agent/context-pack-service.js +83 -0
- package/src/agent/public-read-policy.js +39 -0
- package/src/agent/public-read-service.js +69 -0
- package/src/agent/read-projection.js +107 -0
- package/src/agent/search-service.js +61 -0
- package/src/agent/space-boundary.js +33 -0
- package/src/agent-tools/graph-definitions.js +479 -0
- package/src/agent-tools/graph-handlers.js +96 -0
- package/src/agent-tools/schema.js +37 -0
- package/src/agent-tools.js +16 -0
- package/src/app/application-error.js +13 -0
- package/src/app/bootstrap.js +8 -0
- package/src/app/create-application.js +143 -0
- package/src/app/state-service.js +38 -0
- package/src/candidates.js +78 -0
- package/src/classifier.js +63 -0
- package/src/cli/command-arguments.js +14 -0
- package/src/cli/command-registry.js +78 -0
- package/src/cli/commands/candidates.js +17 -0
- package/src/cli/commands/ingestion.js +32 -0
- package/src/cli/commands/reads.js +50 -0
- package/src/cli/commands/spaces.js +20 -0
- package/src/cli/invocation.js +47 -0
- package/src/cli/local-runtime-command.js +94 -0
- package/src/cli/local-runtime-options.js +64 -0
- package/src/cli/migrate-command.js +71 -0
- package/src/cli/migration-destination.js +117 -0
- package/src/cli/setup-agent-selection.js +195 -0
- package/src/cli/setup-command.js +85 -0
- package/src/cli/uninstall-command.js +67 -0
- package/src/cli.js +90 -0
- package/src/context-pack.js +206 -0
- package/src/defaults.js +1 -0
- package/src/extractor.js +77 -0
- package/src/fact-history.js +39 -0
- package/src/facts/fact-service.js +89 -0
- package/src/graphiti/agent-access-policy.js +88 -0
- package/src/graphiti/capture-policy.js +84 -0
- package/src/graphiti/federated-application.js +1292 -0
- package/src/graphiti/personal-context.js +29 -0
- package/src/graphiti/project-path-context.js +159 -0
- package/src/graphiti/provider-client.js +288 -0
- package/src/graphiti/runtime-config.js +85 -0
- package/src/graphiti/setup.js +43 -0
- package/src/graphiti/source-marker.js +142 -0
- package/src/http/api-router.js +133 -0
- package/src/http/bounded-json-array.js +19 -0
- package/src/http/error-mapping.js +36 -0
- package/src/http/graph-api-router.js +257 -0
- package/src/http/lens-route.js +50 -0
- package/src/http/request-policy.js +41 -0
- package/src/http/response.js +29 -0
- package/src/http/static-handler.js +55 -0
- package/src/ingestion.js +161 -0
- package/src/lens/lens-correction-service.js +81 -0
- package/src/lens/lens-fact-writer.js +85 -0
- package/src/lens/lens-input.js +62 -0
- package/src/lens/lens-query-projection.js +87 -0
- package/src/lens/lens-query-ranking.js +62 -0
- package/src/lens/lens-query.js +143 -0
- package/src/lens/lens-resource-projection.js +93 -0
- package/src/lens/lens-resource-selection.js +45 -0
- package/src/lens/lens-resource-service.js +103 -0
- package/src/lens/lens-search-query.js +122 -0
- package/src/lens/lens-service.js +183 -0
- package/src/local-runtime/lifecycle.js +254 -0
- package/src/mcp/create-mcp-server.js +85 -0
- package/src/mcp/instructions.js +10 -0
- package/src/mcp/project-action-preview-tokens.js +72 -0
- package/src/mcp/runtime.js +71 -0
- package/src/mcp/tool-annotations.js +63 -0
- package/src/mcp/tool-result.js +167 -0
- package/src/mcp/tool-schema.js +60 -0
- package/src/mcp-server.js +76 -0
- package/src/models.js +44 -0
- package/src/observer.js +83 -0
- package/src/package-metadata.js +6 -0
- package/src/project-rules.js +33 -0
- package/src/publication/canonical-json.js +39 -0
- package/src/publication/outbox-service.js +46 -0
- package/src/publication/publication-envelope-schema.js +75 -0
- package/src/publication/publication-envelope.js +37 -0
- package/src/publication/publication-policy.js +82 -0
- package/src/publication/publication-service.js +48 -0
- package/src/router.js +58 -0
- package/src/runtime-options.js +139 -0
- package/src/security/episode-visibility.js +14 -0
- package/src/security/fact-visibility.js +15 -0
- package/src/security/sensitive-content.js +18 -0
- package/src/security/source-metadata-policy.js +8 -0
- package/src/server/application-lifecycle.js +34 -0
- package/src/server/listen.js +24 -0
- package/src/server.js +135 -0
- package/src/setup/agent-installation-status.js +97 -0
- package/src/setup/agents.js +123 -0
- package/src/setup/codex-bootstrap.js +118 -0
- package/src/setup/codex-config.js +122 -0
- package/src/setup/config-backup.js +22 -0
- package/src/setup/container-runtime.js +388 -0
- package/src/setup/cursor-config.js +61 -0
- package/src/setup/graph-runtime.js +360 -0
- package/src/setup/node-runtime.js +24 -0
- package/src/setup/options.js +91 -0
- package/src/setup/paths.js +47 -0
- package/src/setup/session-skill.js +91 -0
- package/src/setup/setup.js +85 -0
- package/src/setup/uninstall.js +90 -0
- package/src/storage/episode-evidence-preview.js +100 -0
- package/src/storage/fact-search-order.js +18 -0
- package/src/storage/file/episode-evidence.js +37 -0
- package/src/storage/file/episode-index.js +59 -0
- package/src/storage/file/fact-search.js +60 -0
- package/src/storage/file-store.js +437 -0
- package/src/storage/import-json.js +70 -0
- package/src/storage/json-file.js +41 -0
- package/src/storage/legacy-record-validation.js +152 -0
- package/src/storage/legacy-snapshot-validation.js +32 -0
- package/src/storage/migrate.js +128 -0
- package/src/storage/migrations/001-initial.sql +83 -0
- package/src/storage/migrations/002-storage-integrity.sql +113 -0
- package/src/storage/record-validation.js +46 -0
- package/src/storage/snapshot-defaults.js +54 -0
- package/src/storage/sqlite/candidates-repository.js +84 -0
- package/src/storage/sqlite/episode-evidence-repository.js +154 -0
- package/src/storage/sqlite/episode-privacy-sql.js +33 -0
- package/src/storage/sqlite/episodes-repository.js +52 -0
- package/src/storage/sqlite/fact-search-repository.js +162 -0
- package/src/storage/sqlite/facts-repository.js +146 -0
- package/src/storage/sqlite/imports-repository.js +39 -0
- package/src/storage/sqlite/mapper.js +84 -0
- package/src/storage/sqlite/open-database.js +15 -0
- package/src/storage/sqlite/outbox-repository.js +103 -0
- package/src/storage/sqlite/snapshot-repository.js +69 -0
- package/src/storage/sqlite/spaces-repository.js +52 -0
- package/src/storage/sqlite/subscriptions-repository.js +54 -0
- package/src/storage/sqlite-store.js +159 -0
- package/src/storage/stable-json.js +24 -0
- package/src/storage/store-port.js +20 -0
- package/src/store.js +2 -0
- package/src/workspace-protocol/array-validation.js +19 -0
- package/src/workspace-protocol/base64url-contract.js +27 -0
- package/src/workspace-protocol/constants.js +37 -0
- package/src/workspace-protocol/error-contract.js +24 -0
- package/src/workspace-protocol/fact-contract.js +110 -0
- package/src/workspace-protocol/governance-contract.js +47 -0
- package/src/workspace-protocol/index.js +53 -0
- package/src/workspace-protocol/json-budget.js +19 -0
- package/src/workspace-protocol/manifest-contract.js +186 -0
- package/src/workspace-protocol/publication-contract.js +212 -0
- package/src/workspace-protocol/query-contract.js +149 -0
- package/src/workspace-protocol/scalar-contract.js +192 -0
- package/src/workspace-protocol/sync-contract.js +184 -0
- package/src/workspace-protocol/text-contract.js +39 -0
- package/src/workspace-protocol/validation.js +66 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
5
|
+
from .knowledge_audit import record_human_change
|
|
6
|
+
from .knowledge_records import conflict_record, reference_record
|
|
7
|
+
from .personal_project_access import authorize_personal_project
|
|
8
|
+
from .knowledge_management import (
|
|
9
|
+
_implicit_project_id,
|
|
10
|
+
_read_assignment,
|
|
11
|
+
_read_item,
|
|
12
|
+
reassign_knowledge_item,
|
|
13
|
+
)
|
|
14
|
+
from .models import (
|
|
15
|
+
KnowledgeAssignmentChange,
|
|
16
|
+
PersonalProjectUpsert,
|
|
17
|
+
ProjectProfile,
|
|
18
|
+
ProjectSource,
|
|
19
|
+
)
|
|
20
|
+
from .project_action_models import (
|
|
21
|
+
KnowledgeProjectActionRequest,
|
|
22
|
+
KnowledgeProjectActionResult,
|
|
23
|
+
KnowledgeProjectMatch,
|
|
24
|
+
KnowledgeProjectPreviewRecord,
|
|
25
|
+
KnowledgeProjectPreviewRequest,
|
|
26
|
+
)
|
|
27
|
+
from .provider_values import (
|
|
28
|
+
normalized_text as _normalized,
|
|
29
|
+
stable_uuid as _stable_uuid,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def preview_knowledge_project_action(
|
|
34
|
+
store,
|
|
35
|
+
actor: dict,
|
|
36
|
+
item_id: str,
|
|
37
|
+
request: KnowledgeProjectPreviewRequest,
|
|
38
|
+
) -> KnowledgeProjectPreviewRecord:
|
|
39
|
+
space, item = await _authorize_item(store, actor, item_id, request)
|
|
40
|
+
if request.mode == 'create':
|
|
41
|
+
source_project_id = await _source_project_id(
|
|
42
|
+
store, space, item_id, request.item_kind
|
|
43
|
+
)
|
|
44
|
+
target_project_id = request.new_project_id
|
|
45
|
+
await _ensure_new_project_id(store, space, target_project_id)
|
|
46
|
+
match = KnowledgeProjectMatch(
|
|
47
|
+
kind='none',
|
|
48
|
+
reason='新项目标识可用,尚未执行创建。',
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
target_project_id = request.target_project_id
|
|
52
|
+
await authorize_personal_project(store, actor, space, target_project_id)
|
|
53
|
+
source_project_id = await _source_project_id(
|
|
54
|
+
store, space, item_id, request.item_kind
|
|
55
|
+
)
|
|
56
|
+
match = await _match_target_project(
|
|
57
|
+
store,
|
|
58
|
+
space,
|
|
59
|
+
item_id,
|
|
60
|
+
request.item_kind,
|
|
61
|
+
item,
|
|
62
|
+
target_project_id,
|
|
63
|
+
source_project_id,
|
|
64
|
+
)
|
|
65
|
+
return KnowledgeProjectPreviewRecord(
|
|
66
|
+
item_id=item_id,
|
|
67
|
+
item_name=item['name'],
|
|
68
|
+
item_summary=item.get('summary') or '',
|
|
69
|
+
source_project_id=source_project_id,
|
|
70
|
+
target_project_id=target_project_id,
|
|
71
|
+
match=match,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def apply_knowledge_project_action(
|
|
76
|
+
store,
|
|
77
|
+
actor: dict,
|
|
78
|
+
item_id: str,
|
|
79
|
+
request: KnowledgeProjectActionRequest,
|
|
80
|
+
) -> KnowledgeProjectActionResult:
|
|
81
|
+
space, item = await _authorize_item(store, actor, item_id, request)
|
|
82
|
+
source_project_id = await _source_project_id(
|
|
83
|
+
store, space, item_id, request.item_kind
|
|
84
|
+
)
|
|
85
|
+
project_created = request.mode == 'create'
|
|
86
|
+
if project_created:
|
|
87
|
+
target_project_id = request.new_project_id
|
|
88
|
+
await _ensure_new_project_id(store, space, target_project_id)
|
|
89
|
+
await store.upsert_personal_project(
|
|
90
|
+
actor,
|
|
91
|
+
PersonalProjectUpsert(
|
|
92
|
+
personal_space_id=space['id'],
|
|
93
|
+
project_id=target_project_id,
|
|
94
|
+
profile=_new_project_profile(item_id, item, request),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
match = KnowledgeProjectMatch(
|
|
98
|
+
kind='none',
|
|
99
|
+
reason='新项目中还没有同名知识,将以当前节点作为项目起点。',
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
target_project_id = request.target_project_id
|
|
103
|
+
await authorize_personal_project(store, actor, space, target_project_id)
|
|
104
|
+
match = await _match_target_project(
|
|
105
|
+
store,
|
|
106
|
+
space,
|
|
107
|
+
item_id,
|
|
108
|
+
request.item_kind,
|
|
109
|
+
item,
|
|
110
|
+
target_project_id,
|
|
111
|
+
source_project_id,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if match.kind == 'already_linked':
|
|
115
|
+
relation_present = await _maybe_create_project_relation(
|
|
116
|
+
store,
|
|
117
|
+
space,
|
|
118
|
+
source_project_id,
|
|
119
|
+
target_project_id,
|
|
120
|
+
request,
|
|
121
|
+
actor['id'],
|
|
122
|
+
)
|
|
123
|
+
result = _action_result(
|
|
124
|
+
'already_linked', source_project_id, target_project_id,
|
|
125
|
+
project_created, relation_present, match,
|
|
126
|
+
)
|
|
127
|
+
await _record_project_action_if_human(
|
|
128
|
+
store, actor, space, item_id, request
|
|
129
|
+
)
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
if source_project_id is None and match.kind == 'none':
|
|
133
|
+
await reassign_knowledge_item(
|
|
134
|
+
store,
|
|
135
|
+
actor,
|
|
136
|
+
item_id,
|
|
137
|
+
KnowledgeAssignmentChange(
|
|
138
|
+
personal_space_id=space['id'],
|
|
139
|
+
item_kind=request.item_kind,
|
|
140
|
+
target_project_id=target_project_id,
|
|
141
|
+
reason=request.reason,
|
|
142
|
+
operation_actor=request.operation_actor,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
status = 'created' if project_created else 'linked'
|
|
146
|
+
return _action_result(
|
|
147
|
+
status, None, target_project_id, project_created, False, match,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
reference_status = _reference_status(match.kind, request.conflict_resolution)
|
|
151
|
+
reference = await _upsert_reference(
|
|
152
|
+
store,
|
|
153
|
+
space,
|
|
154
|
+
actor,
|
|
155
|
+
item_id,
|
|
156
|
+
request.item_kind,
|
|
157
|
+
target_project_id,
|
|
158
|
+
source_project_id,
|
|
159
|
+
reference_status,
|
|
160
|
+
match.item_id,
|
|
161
|
+
request.reason,
|
|
162
|
+
)
|
|
163
|
+
conflict = None
|
|
164
|
+
if match.kind == 'conflict':
|
|
165
|
+
conflict = await _upsert_conflict(
|
|
166
|
+
store,
|
|
167
|
+
space,
|
|
168
|
+
actor,
|
|
169
|
+
item_id,
|
|
170
|
+
match.item_id,
|
|
171
|
+
source_project_id,
|
|
172
|
+
target_project_id,
|
|
173
|
+
request.conflict_resolution,
|
|
174
|
+
request.reason,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
relation_present = await _maybe_create_project_relation(
|
|
178
|
+
store,
|
|
179
|
+
space,
|
|
180
|
+
source_project_id,
|
|
181
|
+
target_project_id,
|
|
182
|
+
request,
|
|
183
|
+
actor['id'],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
status = _action_status(
|
|
187
|
+
project_created, match.kind, request.conflict_resolution
|
|
188
|
+
)
|
|
189
|
+
result = _action_result(
|
|
190
|
+
status,
|
|
191
|
+
source_project_id,
|
|
192
|
+
target_project_id,
|
|
193
|
+
project_created,
|
|
194
|
+
relation_present,
|
|
195
|
+
match,
|
|
196
|
+
reference,
|
|
197
|
+
conflict,
|
|
198
|
+
)
|
|
199
|
+
await _record_project_action_if_human(
|
|
200
|
+
store, actor, space, item_id, request
|
|
201
|
+
)
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def _record_project_action_if_human(
|
|
206
|
+
store,
|
|
207
|
+
actor,
|
|
208
|
+
space,
|
|
209
|
+
item_id,
|
|
210
|
+
request,
|
|
211
|
+
):
|
|
212
|
+
if request.operation_actor != 'human':
|
|
213
|
+
return
|
|
214
|
+
await record_human_change(
|
|
215
|
+
store,
|
|
216
|
+
actor,
|
|
217
|
+
space,
|
|
218
|
+
item_id,
|
|
219
|
+
request.item_kind,
|
|
220
|
+
reason=request.reason,
|
|
221
|
+
operation='project_action',
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def read_project_references(store, space_id: str, item_ids: list[str]):
|
|
226
|
+
if not item_ids:
|
|
227
|
+
return {}
|
|
228
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
229
|
+
'''
|
|
230
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
231
|
+
[:HAS_KNOWLEDGE_REFERENCE]->
|
|
232
|
+
(reference:FuliKnowledgeProjectReference)
|
|
233
|
+
WHERE reference.item_id IN $item_ids
|
|
234
|
+
RETURN reference ORDER BY reference.updated_at DESC
|
|
235
|
+
''',
|
|
236
|
+
space_id=space_id,
|
|
237
|
+
item_ids=item_ids,
|
|
238
|
+
routing_='r',
|
|
239
|
+
)
|
|
240
|
+
result = {}
|
|
241
|
+
for record in records:
|
|
242
|
+
value = reference_record(dict(record['reference']))
|
|
243
|
+
result.setdefault(value.item_id, []).append(value)
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def read_knowledge_conflicts(store, space_id: str, item_ids: list[str]):
|
|
248
|
+
if not item_ids:
|
|
249
|
+
return {}
|
|
250
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
251
|
+
'''
|
|
252
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
253
|
+
[:HAS_KNOWLEDGE_CONFLICT]->(conflict:FuliKnowledgeConflict)
|
|
254
|
+
WHERE conflict.item_id IN $item_ids OR conflict.target_item_id IN $item_ids
|
|
255
|
+
RETURN conflict ORDER BY conflict.updated_at DESC
|
|
256
|
+
''',
|
|
257
|
+
space_id=space_id,
|
|
258
|
+
item_ids=item_ids,
|
|
259
|
+
routing_='r',
|
|
260
|
+
)
|
|
261
|
+
result = {}
|
|
262
|
+
for record in records:
|
|
263
|
+
value = conflict_record(dict(record['conflict']))
|
|
264
|
+
result.setdefault(value.item_id, []).append(value)
|
|
265
|
+
if value.target_item_id != value.item_id:
|
|
266
|
+
result.setdefault(value.target_item_id, []).append(value)
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def read_personal_project_relations(store, space_id: str) -> list[dict]:
|
|
271
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
272
|
+
'''
|
|
273
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
274
|
+
[:CONTAINS_PROJECT]->(source:FuliPersonalProject)-
|
|
275
|
+
[relation:PERSONAL_PROJECT_RELATION]->(target:FuliPersonalProject)
|
|
276
|
+
RETURN source.project_id AS source_project_id,
|
|
277
|
+
source.name AS source_name,
|
|
278
|
+
target.project_id AS target_project_id,
|
|
279
|
+
target.name AS target_name,
|
|
280
|
+
relation
|
|
281
|
+
ORDER BY relation.updated_at DESC
|
|
282
|
+
''',
|
|
283
|
+
space_id=space_id,
|
|
284
|
+
routing_='r',
|
|
285
|
+
)
|
|
286
|
+
return [
|
|
287
|
+
{
|
|
288
|
+
'source_project_id': record['source_project_id'],
|
|
289
|
+
'source_name': record['source_name'],
|
|
290
|
+
'target_project_id': record['target_project_id'],
|
|
291
|
+
'target_name': record['target_name'],
|
|
292
|
+
**dict(record['relation']),
|
|
293
|
+
}
|
|
294
|
+
for record in records
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def _authorize_item(store, actor, item_id, request):
|
|
299
|
+
store._require_personal()
|
|
300
|
+
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
301
|
+
if space['kind'] != 'personal':
|
|
302
|
+
raise HTTPException(status_code=422, detail='knowledge project action is personal-only')
|
|
303
|
+
item = await _read_item(store, space, item_id, request.item_kind)
|
|
304
|
+
if item is None:
|
|
305
|
+
raise HTTPException(status_code=404, detail='knowledge item not found')
|
|
306
|
+
if item.get('profile_aspect'):
|
|
307
|
+
raise HTTPException(
|
|
308
|
+
status_code=422,
|
|
309
|
+
detail='personal profile knowledge cannot be added to a project',
|
|
310
|
+
)
|
|
311
|
+
if item.get('invalid_at'):
|
|
312
|
+
raise HTTPException(
|
|
313
|
+
status_code=422,
|
|
314
|
+
detail='historical knowledge must be restored before adding it to a project',
|
|
315
|
+
)
|
|
316
|
+
return space, item
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
async def _source_project_id(store, space, item_id, item_kind):
|
|
320
|
+
assignment = await _read_assignment(store, space['id'], item_id, item_kind)
|
|
321
|
+
if assignment:
|
|
322
|
+
return assignment.project_id
|
|
323
|
+
return await _implicit_project_id(store, space, item_id, item_kind)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def _ensure_new_project_id(store, space, project_id):
|
|
327
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
328
|
+
'''
|
|
329
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
330
|
+
[:CONTAINS_PROJECT]->
|
|
331
|
+
(project:FuliPersonalProject {project_id: $project_id})
|
|
332
|
+
RETURN project.project_id AS project_id
|
|
333
|
+
''',
|
|
334
|
+
space_id=space['id'],
|
|
335
|
+
project_id=project_id,
|
|
336
|
+
routing_='r',
|
|
337
|
+
)
|
|
338
|
+
if records:
|
|
339
|
+
raise HTTPException(
|
|
340
|
+
status_code=409,
|
|
341
|
+
detail='personal project id already exists; choose the existing-project option',
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _new_project_profile(item_id, item, request):
|
|
346
|
+
purpose = request.new_project_purpose or item.get('summary') or None
|
|
347
|
+
return ProjectProfile(
|
|
348
|
+
name=request.new_project_name,
|
|
349
|
+
purpose=purpose,
|
|
350
|
+
scope=f'以图谱节点“{item["name"]}”作为项目知识起点。',
|
|
351
|
+
lifecycle='active',
|
|
352
|
+
sources=[ProjectSource(
|
|
353
|
+
key=f'graph-node-{item_id[:24]}',
|
|
354
|
+
kind='other',
|
|
355
|
+
title=f'图谱节点:{item["name"]}',
|
|
356
|
+
summary=item.get('summary') or '由本机个人图谱节点创建。',
|
|
357
|
+
sensitivity='private',
|
|
358
|
+
)],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def _match_target_project(
|
|
363
|
+
store,
|
|
364
|
+
space,
|
|
365
|
+
item_id,
|
|
366
|
+
item_kind,
|
|
367
|
+
item,
|
|
368
|
+
target_project_id,
|
|
369
|
+
source_project_id,
|
|
370
|
+
):
|
|
371
|
+
if source_project_id == target_project_id:
|
|
372
|
+
return KnowledgeProjectMatch(
|
|
373
|
+
kind='already_linked',
|
|
374
|
+
reason='这条知识的主要归属已经是目标项目。',
|
|
375
|
+
item_id=item_id,
|
|
376
|
+
item_name=item['name'],
|
|
377
|
+
item_summary=item.get('summary') or '',
|
|
378
|
+
)
|
|
379
|
+
existing = await _read_reference(
|
|
380
|
+
store, space['id'], item_id, item_kind, target_project_id
|
|
381
|
+
)
|
|
382
|
+
if existing and existing.status in {'active', 'pending_conflict', 'duplicate'}:
|
|
383
|
+
kind = {
|
|
384
|
+
'active': 'already_linked',
|
|
385
|
+
'pending_conflict': 'conflict',
|
|
386
|
+
'duplicate': 'exact_duplicate',
|
|
387
|
+
}[existing.status]
|
|
388
|
+
target = await _read_entity_summary(
|
|
389
|
+
store, space, existing.matched_item_id
|
|
390
|
+
) if existing.matched_item_id else None
|
|
391
|
+
return KnowledgeProjectMatch(
|
|
392
|
+
kind=kind,
|
|
393
|
+
reason={
|
|
394
|
+
'active': '目标项目已经在引用这条知识。',
|
|
395
|
+
'pending_conflict': '这条知识与目标项目内容的冲突仍在待处理。',
|
|
396
|
+
'duplicate': '目标项目已经存在完全相同的内容。',
|
|
397
|
+
}[existing.status],
|
|
398
|
+
item_id=existing.matched_item_id or item_id,
|
|
399
|
+
item_name=(target or item).get('name'),
|
|
400
|
+
item_summary=(target or item).get('summary') or '',
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
404
|
+
'''
|
|
405
|
+
MATCH (candidate:Entity {group_id: $group_id})
|
|
406
|
+
WHERE candidate.uuid <> $item_id
|
|
407
|
+
AND toLower(trim(candidate.name)) = $normalized_name
|
|
408
|
+
AND coalesce(candidate.fuli_type, 'Entity') = $item_type
|
|
409
|
+
AND candidate.fuli_invalid_at IS NULL
|
|
410
|
+
OPTIONAL MATCH (episode:Episodic {group_id: $group_id})-[:MENTIONS]->(candidate)
|
|
411
|
+
WITH candidate, collect(DISTINCT episode) AS episodes
|
|
412
|
+
OPTIONAL MATCH (assignment:FuliKnowledgeAssignment {
|
|
413
|
+
space_id: $space_id,
|
|
414
|
+
item_kind: 'entity',
|
|
415
|
+
item_id: candidate.uuid
|
|
416
|
+
})
|
|
417
|
+
OPTIONAL MATCH (:FuliSpace {id: $space_id})-[:HAS_KNOWLEDGE_REFERENCE]->
|
|
418
|
+
(reference:FuliKnowledgeProjectReference {item_id: candidate.uuid})
|
|
419
|
+
WITH candidate, episodes, assignment, collect(reference) AS references
|
|
420
|
+
WHERE assignment.project_id = $target_project_id
|
|
421
|
+
OR (assignment.project_id IS NULL AND any(
|
|
422
|
+
episode IN episodes
|
|
423
|
+
WHERE episode.fuli_personal_project_id = $target_project_id
|
|
424
|
+
))
|
|
425
|
+
OR any(reference IN references WHERE
|
|
426
|
+
reference.project_id = $target_project_id
|
|
427
|
+
AND reference.status = 'active')
|
|
428
|
+
RETURN candidate.uuid AS id, candidate.name AS name,
|
|
429
|
+
coalesce(candidate.summary, '') AS summary,
|
|
430
|
+
coalesce(candidate.fuli_current_quadrant, 'known_known') AS current_quadrant,
|
|
431
|
+
coalesce(candidate.fuli_epistemic_status, 'confirmed') AS epistemic_status,
|
|
432
|
+
coalesce(candidate.fuli_confirmation_status, 'pending')
|
|
433
|
+
AS confirmation_status
|
|
434
|
+
ORDER BY candidate.created_at DESC LIMIT 8
|
|
435
|
+
''',
|
|
436
|
+
group_id=space['group_id'],
|
|
437
|
+
space_id=space['id'],
|
|
438
|
+
item_id=item_id,
|
|
439
|
+
normalized_name=_normalized(item['name']),
|
|
440
|
+
item_type=item.get('type') or 'Entity',
|
|
441
|
+
target_project_id=target_project_id,
|
|
442
|
+
routing_='r',
|
|
443
|
+
)
|
|
444
|
+
exact = next((record for record in records if _same_content(item, record)), None)
|
|
445
|
+
if exact:
|
|
446
|
+
return KnowledgeProjectMatch(
|
|
447
|
+
kind='exact_duplicate',
|
|
448
|
+
reason='目标项目已经存在名称、类型和说明完全相同的内容,将复用现有节点。',
|
|
449
|
+
item_id=exact['id'],
|
|
450
|
+
item_name=exact['name'],
|
|
451
|
+
item_summary=exact['summary'],
|
|
452
|
+
)
|
|
453
|
+
conflict = next((record for record in records if _confirmed_current(item, record)), None)
|
|
454
|
+
if conflict:
|
|
455
|
+
return KnowledgeProjectMatch(
|
|
456
|
+
kind='conflict',
|
|
457
|
+
reason='目标项目存在同名的已确认知识,但两者说明不同。',
|
|
458
|
+
item_id=conflict['id'],
|
|
459
|
+
item_name=conflict['name'],
|
|
460
|
+
item_summary=conflict['summary'],
|
|
461
|
+
)
|
|
462
|
+
return KnowledgeProjectMatch(
|
|
463
|
+
kind='none',
|
|
464
|
+
reason='目标项目中没有检测到完全重复或已确认的同名冲突。',
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
async def _read_reference(store, space_id, item_id, item_kind, project_id):
|
|
469
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
470
|
+
'''
|
|
471
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
472
|
+
[:HAS_KNOWLEDGE_REFERENCE]->
|
|
473
|
+
(reference:FuliKnowledgeProjectReference {
|
|
474
|
+
item_id: $item_id,
|
|
475
|
+
item_kind: $item_kind,
|
|
476
|
+
project_id: $project_id
|
|
477
|
+
})
|
|
478
|
+
RETURN reference
|
|
479
|
+
''',
|
|
480
|
+
space_id=space_id,
|
|
481
|
+
item_id=item_id,
|
|
482
|
+
item_kind=item_kind,
|
|
483
|
+
project_id=project_id,
|
|
484
|
+
routing_='r',
|
|
485
|
+
)
|
|
486
|
+
return reference_record(dict(records[0]['reference'])) if records else None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
async def _read_entity_summary(store, space, item_id):
|
|
490
|
+
if not item_id:
|
|
491
|
+
return None
|
|
492
|
+
return await _read_item(store, space, item_id, 'entity')
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
async def _upsert_reference(
|
|
496
|
+
store,
|
|
497
|
+
space,
|
|
498
|
+
actor,
|
|
499
|
+
item_id,
|
|
500
|
+
item_kind,
|
|
501
|
+
project_id,
|
|
502
|
+
source_project_id,
|
|
503
|
+
status,
|
|
504
|
+
matched_item_id,
|
|
505
|
+
reason,
|
|
506
|
+
):
|
|
507
|
+
reference_id = _stable_uuid(
|
|
508
|
+
space['id'], 'knowledge-reference', item_kind, item_id, project_id
|
|
509
|
+
)
|
|
510
|
+
changed_at = datetime.now(timezone.utc)
|
|
511
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
512
|
+
'''
|
|
513
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
514
|
+
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
515
|
+
(target:FuliPersonalProject {project_id: $project_id})
|
|
516
|
+
MERGE (reference:FuliKnowledgeProjectReference {
|
|
517
|
+
space_id: $space_id,
|
|
518
|
+
item_kind: $item_kind,
|
|
519
|
+
item_id: $item_id,
|
|
520
|
+
project_id: $project_id
|
|
521
|
+
})
|
|
522
|
+
ON CREATE SET reference.id = $reference_id,
|
|
523
|
+
reference.created_at = $changed_at
|
|
524
|
+
SET reference.source_project_id = $source_project_id,
|
|
525
|
+
reference.status = $status,
|
|
526
|
+
reference.matched_item_id = $matched_item_id,
|
|
527
|
+
reference.reason = $reason,
|
|
528
|
+
reference.changed_by = $changed_by,
|
|
529
|
+
reference.updated_at = $changed_at
|
|
530
|
+
MERGE (space)-[:HAS_KNOWLEDGE_REFERENCE]->(reference)
|
|
531
|
+
MERGE (reference)-[:REFERENCES_IN]->(target)
|
|
532
|
+
RETURN reference
|
|
533
|
+
''',
|
|
534
|
+
space_id=space['id'],
|
|
535
|
+
item_kind=item_kind,
|
|
536
|
+
item_id=item_id,
|
|
537
|
+
project_id=project_id,
|
|
538
|
+
reference_id=reference_id,
|
|
539
|
+
source_project_id=source_project_id,
|
|
540
|
+
status=status,
|
|
541
|
+
matched_item_id=matched_item_id,
|
|
542
|
+
reason=reason,
|
|
543
|
+
changed_by=actor['id'],
|
|
544
|
+
changed_at=changed_at,
|
|
545
|
+
)
|
|
546
|
+
return reference_record(dict(records[0]['reference']))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def _upsert_conflict(
|
|
550
|
+
store,
|
|
551
|
+
space,
|
|
552
|
+
actor,
|
|
553
|
+
item_id,
|
|
554
|
+
target_item_id,
|
|
555
|
+
source_project_id,
|
|
556
|
+
target_project_id,
|
|
557
|
+
resolution,
|
|
558
|
+
reason,
|
|
559
|
+
):
|
|
560
|
+
conflict_id = _stable_uuid(
|
|
561
|
+
space['id'], 'knowledge-conflict', item_id, target_project_id, target_item_id
|
|
562
|
+
)
|
|
563
|
+
changed_at = datetime.now(timezone.utc)
|
|
564
|
+
status = 'pending' if resolution == 'defer' else 'resolved'
|
|
565
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
566
|
+
'''
|
|
567
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
568
|
+
MERGE (conflict:FuliKnowledgeConflict {id: $conflict_id})
|
|
569
|
+
ON CREATE SET conflict.created_at = $changed_at
|
|
570
|
+
SET conflict.space_id = $space_id,
|
|
571
|
+
conflict.item_id = $item_id,
|
|
572
|
+
conflict.target_item_id = $target_item_id,
|
|
573
|
+
conflict.source_project_id = $source_project_id,
|
|
574
|
+
conflict.target_project_id = $target_project_id,
|
|
575
|
+
conflict.status = $status,
|
|
576
|
+
conflict.resolution = $resolution,
|
|
577
|
+
conflict.reason = $reason,
|
|
578
|
+
conflict.decided_by = $decided_by,
|
|
579
|
+
conflict.updated_at = $changed_at
|
|
580
|
+
MERGE (space)-[:HAS_KNOWLEDGE_CONFLICT]->(conflict)
|
|
581
|
+
RETURN conflict
|
|
582
|
+
''',
|
|
583
|
+
space_id=space['id'],
|
|
584
|
+
conflict_id=conflict_id,
|
|
585
|
+
item_id=item_id,
|
|
586
|
+
target_item_id=target_item_id,
|
|
587
|
+
source_project_id=source_project_id,
|
|
588
|
+
target_project_id=target_project_id,
|
|
589
|
+
status=status,
|
|
590
|
+
resolution=resolution,
|
|
591
|
+
reason=reason,
|
|
592
|
+
decided_by=actor['id'] if status == 'resolved' else None,
|
|
593
|
+
changed_at=changed_at,
|
|
594
|
+
)
|
|
595
|
+
return conflict_record(dict(records[0]['conflict']))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
async def _maybe_create_project_relation(
|
|
599
|
+
store,
|
|
600
|
+
space,
|
|
601
|
+
source_project_id,
|
|
602
|
+
target_project_id,
|
|
603
|
+
request,
|
|
604
|
+
actor_id,
|
|
605
|
+
):
|
|
606
|
+
if (
|
|
607
|
+
not request.keep_source_relation
|
|
608
|
+
or not source_project_id
|
|
609
|
+
or source_project_id == target_project_id
|
|
610
|
+
):
|
|
611
|
+
return False
|
|
612
|
+
source, target = _relation_endpoints(
|
|
613
|
+
source_project_id, target_project_id, request
|
|
614
|
+
)
|
|
615
|
+
if request.relation_type == 'RELATED_TO' and source > target:
|
|
616
|
+
source, target = target, source
|
|
617
|
+
relation_id = _stable_uuid(
|
|
618
|
+
space['id'], 'personal-project-relation', source, target, request.relation_type
|
|
619
|
+
)
|
|
620
|
+
changed_at = datetime.now(timezone.utc)
|
|
621
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
622
|
+
'''
|
|
623
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
624
|
+
[:CONTAINS_PROJECT]->
|
|
625
|
+
(source:FuliPersonalProject {project_id: $source_project_id})
|
|
626
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
627
|
+
[:CONTAINS_PROJECT]->
|
|
628
|
+
(target:FuliPersonalProject {project_id: $target_project_id})
|
|
629
|
+
MERGE (source)-[relation:PERSONAL_PROJECT_RELATION {
|
|
630
|
+
id: $relation_id,
|
|
631
|
+
relation_type: $relation_type
|
|
632
|
+
}]->(target)
|
|
633
|
+
ON CREATE SET relation.created_at = $changed_at,
|
|
634
|
+
relation.created_by = $created_by
|
|
635
|
+
SET relation.updated_at = $changed_at
|
|
636
|
+
RETURN relation.id AS id
|
|
637
|
+
''',
|
|
638
|
+
space_id=space['id'],
|
|
639
|
+
source_project_id=source,
|
|
640
|
+
target_project_id=target,
|
|
641
|
+
relation_id=relation_id,
|
|
642
|
+
relation_type=request.relation_type,
|
|
643
|
+
created_by=actor_id,
|
|
644
|
+
changed_at=changed_at,
|
|
645
|
+
)
|
|
646
|
+
return bool(records)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _relation_endpoints(source_project_id, target_project_id, request):
|
|
650
|
+
"""Keep an extracted project as the child side of directional relations."""
|
|
651
|
+
if request.mode == 'create':
|
|
652
|
+
return target_project_id, source_project_id
|
|
653
|
+
return source_project_id, target_project_id
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _reference_status(match_kind, resolution):
|
|
657
|
+
if match_kind == 'exact_duplicate':
|
|
658
|
+
return 'duplicate'
|
|
659
|
+
if match_kind != 'conflict':
|
|
660
|
+
return 'active'
|
|
661
|
+
return {
|
|
662
|
+
'defer': 'pending_conflict',
|
|
663
|
+
'keep_target': 'rejected',
|
|
664
|
+
'use_source': 'active',
|
|
665
|
+
'coexist': 'active',
|
|
666
|
+
}[resolution]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _action_status(project_created, match_kind, resolution):
|
|
670
|
+
if match_kind == 'exact_duplicate':
|
|
671
|
+
return 'duplicate_reused'
|
|
672
|
+
if match_kind == 'conflict':
|
|
673
|
+
return 'conflict_pending' if resolution == 'defer' else 'conflict_resolved'
|
|
674
|
+
return 'created' if project_created else 'linked'
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _action_result(
|
|
678
|
+
status,
|
|
679
|
+
source_project_id,
|
|
680
|
+
target_project_id,
|
|
681
|
+
project_created,
|
|
682
|
+
relation_present,
|
|
683
|
+
match,
|
|
684
|
+
reference=None,
|
|
685
|
+
conflict=None,
|
|
686
|
+
):
|
|
687
|
+
return KnowledgeProjectActionResult(
|
|
688
|
+
status=status,
|
|
689
|
+
source_project_id=source_project_id,
|
|
690
|
+
target_project_id=target_project_id,
|
|
691
|
+
project_created=project_created,
|
|
692
|
+
project_relation_created=relation_present,
|
|
693
|
+
match=match,
|
|
694
|
+
reference=reference,
|
|
695
|
+
conflict=conflict,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _same_content(source, target):
|
|
700
|
+
return _normalized(source.get('summary')) == _normalized(target.get('summary'))
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _confirmed_current(source, target):
|
|
704
|
+
return all(
|
|
705
|
+
item.get('confirmation_status', 'pending') == 'confirmed'
|
|
706
|
+
for item in (source, target)
|
|
707
|
+
)
|