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,816 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
from .knowledge_audit import record_human_change
|
|
7
|
+
from .knowledge_records import assignment_record
|
|
8
|
+
from .personal_project_access import authorize_personal_project
|
|
9
|
+
from .models import (
|
|
10
|
+
ConfirmationBasis,
|
|
11
|
+
KnowledgeAssignmentChange,
|
|
12
|
+
KnowledgeAssignmentRecord,
|
|
13
|
+
PreferenceScopeChange,
|
|
14
|
+
KnowledgeRevisionCreate,
|
|
15
|
+
KnowledgeRevisionRecord,
|
|
16
|
+
)
|
|
17
|
+
from .provider_values import (
|
|
18
|
+
native_datetime as _native_datetime,
|
|
19
|
+
stable_uuid as _stable_uuid,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def revise_knowledge_item(store, actor: dict, item_id: str, request: KnowledgeRevisionCreate):
|
|
24
|
+
store._require_personal()
|
|
25
|
+
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
26
|
+
if space['kind'] != 'personal':
|
|
27
|
+
raise HTTPException(status_code=422, detail='knowledge revision is personal-only')
|
|
28
|
+
if request.personal_project_id:
|
|
29
|
+
await authorize_personal_project(
|
|
30
|
+
store, actor, space, request.personal_project_id
|
|
31
|
+
)
|
|
32
|
+
if not await _item_belongs_to_project(
|
|
33
|
+
store, space, item_id, request.item_kind, request.personal_project_id
|
|
34
|
+
):
|
|
35
|
+
raise HTTPException(status_code=404, detail='knowledge item not found in project')
|
|
36
|
+
|
|
37
|
+
current = await _read_item(store, space, item_id, request.item_kind)
|
|
38
|
+
if current is None:
|
|
39
|
+
raise HTTPException(status_code=404, detail='knowledge item not found')
|
|
40
|
+
previous = _snapshot(current, request.item_kind)
|
|
41
|
+
if (
|
|
42
|
+
request.action == 'update'
|
|
43
|
+
and request.origin_quadrant is not None
|
|
44
|
+
and previous.get('classificationStateExplicit')
|
|
45
|
+
and request.origin_quadrant != previous.get('originQuadrant')
|
|
46
|
+
):
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=422,
|
|
49
|
+
detail=(
|
|
50
|
+
'origin quadrant is immutable after capture; '
|
|
51
|
+
'update current_quadrant instead'
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
next_profile_aspect = (
|
|
55
|
+
previous.get('profileAspect')
|
|
56
|
+
if request.profile_aspect is None
|
|
57
|
+
else None if request.profile_aspect == 'none'
|
|
58
|
+
else request.profile_aspect
|
|
59
|
+
)
|
|
60
|
+
next_inheritance_mode = (
|
|
61
|
+
request.inheritance_mode
|
|
62
|
+
if request.inheritance_mode is not None
|
|
63
|
+
else previous.get('inheritanceMode', 'local_only')
|
|
64
|
+
)
|
|
65
|
+
if next_profile_aspect and next_inheritance_mode != 'local_only':
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=422,
|
|
68
|
+
detail='personal preferences cannot inherit across projects',
|
|
69
|
+
)
|
|
70
|
+
if request.action == 'link_replacement' and not previous.get('invalidAt'):
|
|
71
|
+
raise HTTPException(
|
|
72
|
+
status_code=409,
|
|
73
|
+
detail='only historical knowledge can be linked to a replacement',
|
|
74
|
+
)
|
|
75
|
+
if request.replacement_item_id:
|
|
76
|
+
if (
|
|
77
|
+
request.replacement_item_id == item_id
|
|
78
|
+
and request.replacement_item_kind == request.item_kind
|
|
79
|
+
):
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=422,
|
|
82
|
+
detail='knowledge cannot replace itself',
|
|
83
|
+
)
|
|
84
|
+
replacement = await _read_item(
|
|
85
|
+
store,
|
|
86
|
+
space,
|
|
87
|
+
request.replacement_item_id,
|
|
88
|
+
request.replacement_item_kind,
|
|
89
|
+
)
|
|
90
|
+
if replacement is None:
|
|
91
|
+
raise HTTPException(status_code=404, detail='replacement knowledge not found')
|
|
92
|
+
if request.personal_project_id and not await _item_belongs_to_project(
|
|
93
|
+
store,
|
|
94
|
+
space,
|
|
95
|
+
request.replacement_item_id,
|
|
96
|
+
request.replacement_item_kind,
|
|
97
|
+
request.personal_project_id,
|
|
98
|
+
):
|
|
99
|
+
raise HTTPException(status_code=404, detail='replacement knowledge not found')
|
|
100
|
+
if _snapshot(replacement, request.replacement_item_kind).get('invalidAt'):
|
|
101
|
+
raise HTTPException(
|
|
102
|
+
status_code=409,
|
|
103
|
+
detail='replacement knowledge must be currently effective',
|
|
104
|
+
)
|
|
105
|
+
if request.action == 'confirm':
|
|
106
|
+
if previous.get('invalidAt'):
|
|
107
|
+
raise HTTPException(
|
|
108
|
+
status_code=409,
|
|
109
|
+
detail='historical knowledge cannot be confirmed',
|
|
110
|
+
)
|
|
111
|
+
if not previous.get('classificationStateExplicit'):
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=409,
|
|
114
|
+
detail='knowledge must have an explicit discovery quadrant before confirmation',
|
|
115
|
+
)
|
|
116
|
+
if (
|
|
117
|
+
previous.get('confirmationStateExplicit')
|
|
118
|
+
and previous.get('confirmationStatus') == 'confirmed'
|
|
119
|
+
):
|
|
120
|
+
raise HTTPException(
|
|
121
|
+
status_code=409,
|
|
122
|
+
detail='knowledge is already confirmed',
|
|
123
|
+
)
|
|
124
|
+
created_at = datetime.now(timezone.utc)
|
|
125
|
+
next_value = _next_snapshot(previous, request, created_at)
|
|
126
|
+
embedding = await _updated_embedding(store, previous, next_value, request)
|
|
127
|
+
await _update_item(
|
|
128
|
+
store, space, item_id, request.item_kind, next_value, embedding
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
revision_id = _stable_uuid(
|
|
132
|
+
space['id'], 'knowledge-revision', request.item_kind, item_id,
|
|
133
|
+
created_at.isoformat(), actor['id']
|
|
134
|
+
)
|
|
135
|
+
await store.runtime.driver.execute_query(
|
|
136
|
+
'''
|
|
137
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
138
|
+
CREATE (revision:FuliKnowledgeRevision {
|
|
139
|
+
id: $revision_id,
|
|
140
|
+
space_id: $space_id,
|
|
141
|
+
item_id: $item_id,
|
|
142
|
+
item_kind: $item_kind,
|
|
143
|
+
action: $action,
|
|
144
|
+
reason: $reason,
|
|
145
|
+
previous_json: $previous_json,
|
|
146
|
+
current_json: $current_json,
|
|
147
|
+
created_by: $created_by,
|
|
148
|
+
created_at: $created_at
|
|
149
|
+
})
|
|
150
|
+
MERGE (space)-[:HAS_KNOWLEDGE_REVISION]->(revision)
|
|
151
|
+
''',
|
|
152
|
+
space_id=space['id'],
|
|
153
|
+
revision_id=revision_id,
|
|
154
|
+
item_id=item_id,
|
|
155
|
+
item_kind=request.item_kind,
|
|
156
|
+
action=request.action,
|
|
157
|
+
reason=request.reason,
|
|
158
|
+
previous_json=json.dumps(previous, ensure_ascii=False, sort_keys=True),
|
|
159
|
+
current_json=json.dumps(next_value, ensure_ascii=False, sort_keys=True),
|
|
160
|
+
created_by=actor['id'],
|
|
161
|
+
created_at=created_at,
|
|
162
|
+
)
|
|
163
|
+
if request.operation_actor == 'human':
|
|
164
|
+
await record_human_change(
|
|
165
|
+
store,
|
|
166
|
+
actor,
|
|
167
|
+
space,
|
|
168
|
+
item_id,
|
|
169
|
+
request.item_kind,
|
|
170
|
+
reason=request.reason,
|
|
171
|
+
operation=f'knowledge_{request.action}',
|
|
172
|
+
)
|
|
173
|
+
return KnowledgeRevisionRecord(
|
|
174
|
+
id=revision_id,
|
|
175
|
+
item_id=item_id,
|
|
176
|
+
item_kind=request.item_kind,
|
|
177
|
+
action=request.action,
|
|
178
|
+
reason=request.reason,
|
|
179
|
+
previous=previous,
|
|
180
|
+
current=next_value,
|
|
181
|
+
created_at=created_at,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def reassign_knowledge_item(
|
|
186
|
+
store,
|
|
187
|
+
actor: dict,
|
|
188
|
+
item_id: str,
|
|
189
|
+
request: KnowledgeAssignmentChange,
|
|
190
|
+
):
|
|
191
|
+
store._require_personal()
|
|
192
|
+
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
193
|
+
if space['kind'] != 'personal':
|
|
194
|
+
raise HTTPException(status_code=422, detail='knowledge assignment is personal-only')
|
|
195
|
+
await authorize_personal_project(store, actor, space, request.target_project_id)
|
|
196
|
+
item = await _read_item(store, space, item_id, request.item_kind)
|
|
197
|
+
if item is None:
|
|
198
|
+
raise HTTPException(status_code=404, detail='knowledge item not found')
|
|
199
|
+
if item.get('profile_aspect'):
|
|
200
|
+
raise HTTPException(
|
|
201
|
+
status_code=422,
|
|
202
|
+
detail='personal profile knowledge cannot be assigned to a project',
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
existing = await _read_assignment(store, space['id'], item_id, request.item_kind)
|
|
206
|
+
previous_project_id = (
|
|
207
|
+
existing.project_id if existing
|
|
208
|
+
else await _implicit_project_id(store, space, item_id, request.item_kind)
|
|
209
|
+
)
|
|
210
|
+
changed_at = datetime.now(timezone.utc)
|
|
211
|
+
assignment_id = existing.id if existing else _stable_uuid(
|
|
212
|
+
space['id'], 'knowledge-assignment', request.item_kind, item_id
|
|
213
|
+
)
|
|
214
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
215
|
+
'''
|
|
216
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
217
|
+
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
218
|
+
(target:FuliPersonalProject {project_id: $target_project_id})
|
|
219
|
+
MERGE (assignment:FuliKnowledgeAssignment {
|
|
220
|
+
space_id: $space_id,
|
|
221
|
+
item_kind: $item_kind,
|
|
222
|
+
item_id: $item_id
|
|
223
|
+
})
|
|
224
|
+
ON CREATE SET assignment.id = $assignment_id,
|
|
225
|
+
assignment.created_at = $changed_at
|
|
226
|
+
SET assignment.previous_project_id = $previous_project_id,
|
|
227
|
+
assignment.project_id = $target_project_id,
|
|
228
|
+
assignment.reason = $reason,
|
|
229
|
+
assignment.changed_by = $changed_by,
|
|
230
|
+
assignment.updated_at = $changed_at
|
|
231
|
+
WITH assignment, target
|
|
232
|
+
OPTIONAL MATCH (assignment)-[old:ASSIGNED_TO]->(:FuliPersonalProject)
|
|
233
|
+
DELETE old
|
|
234
|
+
MERGE (assignment)-[:ASSIGNED_TO]->(target)
|
|
235
|
+
MERGE (space)-[:HAS_KNOWLEDGE_ASSIGNMENT]->(assignment)
|
|
236
|
+
RETURN assignment
|
|
237
|
+
''',
|
|
238
|
+
space_id=space['id'],
|
|
239
|
+
target_project_id=request.target_project_id,
|
|
240
|
+
item_kind=request.item_kind,
|
|
241
|
+
item_id=item_id,
|
|
242
|
+
assignment_id=assignment_id,
|
|
243
|
+
previous_project_id=previous_project_id,
|
|
244
|
+
reason=request.reason,
|
|
245
|
+
changed_by=actor['id'],
|
|
246
|
+
changed_at=changed_at,
|
|
247
|
+
)
|
|
248
|
+
if request.operation_actor == 'human':
|
|
249
|
+
await record_human_change(
|
|
250
|
+
store,
|
|
251
|
+
actor,
|
|
252
|
+
space,
|
|
253
|
+
item_id,
|
|
254
|
+
request.item_kind,
|
|
255
|
+
reason=request.reason,
|
|
256
|
+
operation='knowledge_assignment',
|
|
257
|
+
)
|
|
258
|
+
return assignment_record(dict(records[0]['assignment']))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def set_preference_scope(
|
|
262
|
+
store,
|
|
263
|
+
actor: dict,
|
|
264
|
+
item_id: str,
|
|
265
|
+
request: PreferenceScopeChange,
|
|
266
|
+
) -> KnowledgeRevisionRecord:
|
|
267
|
+
store._require_personal()
|
|
268
|
+
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
269
|
+
if space['kind'] != 'personal':
|
|
270
|
+
raise HTTPException(status_code=422, detail='preference scope is personal-only')
|
|
271
|
+
if request.scope == 'project':
|
|
272
|
+
await authorize_personal_project(store, actor, space, request.project_id)
|
|
273
|
+
|
|
274
|
+
current = await _read_item(store, space, item_id, request.item_kind)
|
|
275
|
+
if current is None:
|
|
276
|
+
raise HTTPException(status_code=404, detail='knowledge item not found')
|
|
277
|
+
if not current.get('profile_aspect'):
|
|
278
|
+
raise HTTPException(
|
|
279
|
+
status_code=422,
|
|
280
|
+
detail='only personal profile knowledge has a preference scope',
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
previous = _snapshot(current, request.item_kind)
|
|
284
|
+
next_value = {
|
|
285
|
+
**previous,
|
|
286
|
+
'preferenceScope': request.scope,
|
|
287
|
+
'preferenceProjectId': request.project_id,
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
previous.get('preferenceScope') == request.scope
|
|
291
|
+
and previous.get('preferenceProjectId') == request.project_id
|
|
292
|
+
):
|
|
293
|
+
raise HTTPException(status_code=409, detail='preference scope is unchanged')
|
|
294
|
+
|
|
295
|
+
await _update_preference_scope(
|
|
296
|
+
store,
|
|
297
|
+
space,
|
|
298
|
+
item_id,
|
|
299
|
+
request.item_kind,
|
|
300
|
+
request.scope,
|
|
301
|
+
request.project_id,
|
|
302
|
+
)
|
|
303
|
+
created_at = datetime.now(timezone.utc)
|
|
304
|
+
revision_id = _stable_uuid(
|
|
305
|
+
space['id'], 'knowledge-revision', request.item_kind, item_id,
|
|
306
|
+
created_at.isoformat(), actor['id']
|
|
307
|
+
)
|
|
308
|
+
await store.runtime.driver.execute_query(
|
|
309
|
+
'''
|
|
310
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
311
|
+
CREATE (revision:FuliKnowledgeRevision {
|
|
312
|
+
id: $revision_id,
|
|
313
|
+
space_id: $space_id,
|
|
314
|
+
item_id: $item_id,
|
|
315
|
+
item_kind: $item_kind,
|
|
316
|
+
action: 'scope_change',
|
|
317
|
+
reason: $reason,
|
|
318
|
+
previous_json: $previous_json,
|
|
319
|
+
current_json: $current_json,
|
|
320
|
+
created_by: $created_by,
|
|
321
|
+
created_at: $created_at
|
|
322
|
+
})
|
|
323
|
+
MERGE (space)-[:HAS_KNOWLEDGE_REVISION]->(revision)
|
|
324
|
+
''',
|
|
325
|
+
space_id=space['id'],
|
|
326
|
+
revision_id=revision_id,
|
|
327
|
+
item_id=item_id,
|
|
328
|
+
item_kind=request.item_kind,
|
|
329
|
+
reason=request.reason,
|
|
330
|
+
previous_json=json.dumps(previous, ensure_ascii=False, sort_keys=True),
|
|
331
|
+
current_json=json.dumps(next_value, ensure_ascii=False, sort_keys=True),
|
|
332
|
+
created_by=actor['id'],
|
|
333
|
+
created_at=created_at,
|
|
334
|
+
)
|
|
335
|
+
if request.operation_actor == 'human':
|
|
336
|
+
await record_human_change(
|
|
337
|
+
store,
|
|
338
|
+
actor,
|
|
339
|
+
space,
|
|
340
|
+
item_id,
|
|
341
|
+
request.item_kind,
|
|
342
|
+
reason=request.reason,
|
|
343
|
+
operation='preference_scope',
|
|
344
|
+
)
|
|
345
|
+
return KnowledgeRevisionRecord(
|
|
346
|
+
id=revision_id,
|
|
347
|
+
item_id=item_id,
|
|
348
|
+
item_kind=request.item_kind,
|
|
349
|
+
action='scope_change',
|
|
350
|
+
reason=request.reason,
|
|
351
|
+
previous=previous,
|
|
352
|
+
current=next_value,
|
|
353
|
+
created_at=created_at,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
async def _read_item(store, space: dict, item_id: str, item_kind: str):
|
|
358
|
+
if item_kind == 'entity':
|
|
359
|
+
query = '''
|
|
360
|
+
MATCH (item:Entity {uuid: $item_id, group_id: $group_id})
|
|
361
|
+
RETURN item.name AS name, coalesce(item.fuli_type, 'Entity') AS type,
|
|
362
|
+
coalesce(item.summary, '') AS summary,
|
|
363
|
+
item.fuli_invalid_at AS invalid_at,
|
|
364
|
+
item.fuli_origin_quadrant IS NOT NULL
|
|
365
|
+
AS classification_state_explicit,
|
|
366
|
+
coalesce(item.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
|
|
367
|
+
coalesce(item.fuli_current_quadrant, 'known_known') AS current_quadrant,
|
|
368
|
+
coalesce(item.fuli_epistemic_status, 'confirmed') AS epistemic_status,
|
|
369
|
+
coalesce(item.fuli_confirmation_status, 'pending')
|
|
370
|
+
AS confirmation_status,
|
|
371
|
+
item.fuli_confirmation_status IS NOT NULL
|
|
372
|
+
AND item.fuli_confirmation_basis_json IS NOT NULL
|
|
373
|
+
AS confirmation_state_explicit,
|
|
374
|
+
item.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
375
|
+
item.fuli_reasoning_summary AS reasoning_summary,
|
|
376
|
+
item.fuli_profile_aspect AS profile_aspect,
|
|
377
|
+
CASE WHEN item.fuli_profile_aspect IS NULL THEN NULL
|
|
378
|
+
ELSE coalesce(item.fuli_preference_scope, 'global') END
|
|
379
|
+
AS preference_scope,
|
|
380
|
+
item.fuli_preference_project_id AS preference_project_id,
|
|
381
|
+
coalesce(item.fuli_inheritance_mode, 'local_only')
|
|
382
|
+
AS inheritance_mode,
|
|
383
|
+
coalesce(item.fuli_inherited_project_ids, [])
|
|
384
|
+
AS inherited_project_ids,
|
|
385
|
+
coalesce(item.fuli_utility_score, 0.0) AS utility_score,
|
|
386
|
+
coalesce(item.fuli_confidence_score, 0.5) AS confidence_score,
|
|
387
|
+
coalesce(item.fuli_qualified_use_count, 0)
|
|
388
|
+
AS qualified_use_count,
|
|
389
|
+
coalesce(item.fuli_distinct_task_count, 0)
|
|
390
|
+
AS distinct_task_count,
|
|
391
|
+
item.fuli_last_used_at AS last_used_at,
|
|
392
|
+
coalesce(item.fuli_usage_generation, 1) AS usage_generation,
|
|
393
|
+
item.fuli_replaced_by_item_id AS replaced_by_item_id,
|
|
394
|
+
item.fuli_replaced_by_item_kind AS replaced_by_item_kind
|
|
395
|
+
'''
|
|
396
|
+
else:
|
|
397
|
+
query = '''
|
|
398
|
+
MATCH ()-[item:RELATES_TO {uuid: $item_id, group_id: $group_id}]->()
|
|
399
|
+
RETURN item.name AS type, item.fact AS fact, item.invalid_at AS invalid_at,
|
|
400
|
+
item.fuli_origin_quadrant IS NOT NULL
|
|
401
|
+
AS classification_state_explicit,
|
|
402
|
+
coalesce(item.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
|
|
403
|
+
coalesce(item.fuli_current_quadrant, 'known_known') AS current_quadrant,
|
|
404
|
+
coalesce(item.fuli_epistemic_status, 'confirmed') AS epistemic_status,
|
|
405
|
+
coalesce(item.fuli_confirmation_status, 'pending')
|
|
406
|
+
AS confirmation_status,
|
|
407
|
+
item.fuli_confirmation_status IS NOT NULL
|
|
408
|
+
AND item.fuli_confirmation_basis_json IS NOT NULL
|
|
409
|
+
AS confirmation_state_explicit,
|
|
410
|
+
item.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
411
|
+
item.fuli_reasoning_summary AS reasoning_summary,
|
|
412
|
+
item.fuli_profile_aspect AS profile_aspect,
|
|
413
|
+
CASE WHEN item.fuli_profile_aspect IS NULL THEN NULL
|
|
414
|
+
ELSE coalesce(item.fuli_preference_scope, 'global') END
|
|
415
|
+
AS preference_scope,
|
|
416
|
+
item.fuli_preference_project_id AS preference_project_id,
|
|
417
|
+
coalesce(item.fuli_inheritance_mode, 'local_only')
|
|
418
|
+
AS inheritance_mode,
|
|
419
|
+
coalesce(item.fuli_inherited_project_ids, [])
|
|
420
|
+
AS inherited_project_ids,
|
|
421
|
+
coalesce(item.fuli_utility_score, 0.0) AS utility_score,
|
|
422
|
+
coalesce(item.fuli_confidence_score, 0.5) AS confidence_score,
|
|
423
|
+
coalesce(item.fuli_qualified_use_count, 0)
|
|
424
|
+
AS qualified_use_count,
|
|
425
|
+
coalesce(item.fuli_distinct_task_count, 0)
|
|
426
|
+
AS distinct_task_count,
|
|
427
|
+
item.fuli_last_used_at AS last_used_at,
|
|
428
|
+
coalesce(item.fuli_usage_generation, 1) AS usage_generation,
|
|
429
|
+
item.fuli_replaced_by_item_id AS replaced_by_item_id,
|
|
430
|
+
item.fuli_replaced_by_item_kind AS replaced_by_item_kind
|
|
431
|
+
'''
|
|
432
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
433
|
+
query,
|
|
434
|
+
item_id=item_id,
|
|
435
|
+
group_id=space['group_id'],
|
|
436
|
+
routing_='r',
|
|
437
|
+
)
|
|
438
|
+
return dict(records[0]) if records else None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
async def _item_belongs_to_project(
|
|
442
|
+
store,
|
|
443
|
+
space: dict,
|
|
444
|
+
item_id: str,
|
|
445
|
+
item_kind: str,
|
|
446
|
+
project_id: str,
|
|
447
|
+
) -> bool:
|
|
448
|
+
assignment = await _read_assignment(store, space['id'], item_id, item_kind)
|
|
449
|
+
if assignment:
|
|
450
|
+
return assignment.project_id == project_id
|
|
451
|
+
return await _implicit_project_id(store, space, item_id, item_kind) == project_id
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
async def _implicit_project_id(store, space: dict, item_id: str, item_kind: str):
|
|
455
|
+
if item_kind == 'entity':
|
|
456
|
+
query = '''
|
|
457
|
+
MATCH (episode:Episodic {group_id: $group_id})-[:MENTIONS]->
|
|
458
|
+
(:Entity {uuid: $item_id, group_id: $group_id})
|
|
459
|
+
WHERE episode.fuli_personal_project_id IS NOT NULL
|
|
460
|
+
RETURN collect(DISTINCT episode.fuli_personal_project_id) AS project_ids
|
|
461
|
+
'''
|
|
462
|
+
else:
|
|
463
|
+
query = '''
|
|
464
|
+
MATCH ()-[item:RELATES_TO {uuid: $item_id, group_id: $group_id}]->()
|
|
465
|
+
UNWIND coalesce(item.episodes, []) AS episode_id
|
|
466
|
+
MATCH (episode:Episodic {uuid: episode_id, group_id: $group_id})
|
|
467
|
+
WHERE episode.fuli_personal_project_id IS NOT NULL
|
|
468
|
+
RETURN collect(DISTINCT episode.fuli_personal_project_id) AS project_ids
|
|
469
|
+
'''
|
|
470
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
471
|
+
query,
|
|
472
|
+
item_id=item_id,
|
|
473
|
+
group_id=space['group_id'],
|
|
474
|
+
routing_='r',
|
|
475
|
+
)
|
|
476
|
+
project_ids = records[0]['project_ids'] if records else []
|
|
477
|
+
return project_ids[0] if len(project_ids) == 1 else None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
async def _read_assignment(store, space_id: str, item_id: str, item_kind: str):
|
|
481
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
482
|
+
'''
|
|
483
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
484
|
+
[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
485
|
+
(assignment:FuliKnowledgeAssignment {
|
|
486
|
+
item_id: $item_id,
|
|
487
|
+
item_kind: $item_kind
|
|
488
|
+
})
|
|
489
|
+
RETURN assignment
|
|
490
|
+
''',
|
|
491
|
+
space_id=space_id,
|
|
492
|
+
item_id=item_id,
|
|
493
|
+
item_kind=item_kind,
|
|
494
|
+
routing_='r',
|
|
495
|
+
)
|
|
496
|
+
return assignment_record(dict(records[0]['assignment'])) if records else None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _snapshot(value: dict, item_kind: str) -> dict:
|
|
500
|
+
confirmation_basis = _confirmation_basis(value.get('confirmation_basis_json'))
|
|
501
|
+
epistemic = {
|
|
502
|
+
'classificationStateExplicit': (
|
|
503
|
+
value.get('classification_state_explicit') is True
|
|
504
|
+
),
|
|
505
|
+
'originQuadrant': value.get('origin_quadrant') or 'known_known',
|
|
506
|
+
'currentQuadrant': value.get('current_quadrant') or 'known_known',
|
|
507
|
+
'epistemicStatus': value.get('epistemic_status') or 'confirmed',
|
|
508
|
+
'confirmationStatus': value.get('confirmation_status') or 'pending',
|
|
509
|
+
'confirmationStateExplicit': (
|
|
510
|
+
value.get('confirmation_state_explicit') is True
|
|
511
|
+
),
|
|
512
|
+
'confirmationBasis': (
|
|
513
|
+
confirmation_basis.model_dump(mode='json')
|
|
514
|
+
if confirmation_basis else None
|
|
515
|
+
),
|
|
516
|
+
'reasoningSummary': value.get('reasoning_summary'),
|
|
517
|
+
'profileAspect': value.get('profile_aspect'),
|
|
518
|
+
'preferenceScope': value.get('preference_scope'),
|
|
519
|
+
'preferenceProjectId': value.get('preference_project_id'),
|
|
520
|
+
'inheritanceMode': value.get('inheritance_mode') or 'local_only',
|
|
521
|
+
'inheritedProjectIds': value.get('inherited_project_ids') or [],
|
|
522
|
+
'utilityScore': float(value.get('utility_score') or 0),
|
|
523
|
+
'confidenceScore': float(value.get('confidence_score') or 0.5),
|
|
524
|
+
'qualifiedUseCount': int(value.get('qualified_use_count') or 0),
|
|
525
|
+
'distinctTaskCount': int(value.get('distinct_task_count') or 0),
|
|
526
|
+
'lastUsedAt': _iso(value.get('last_used_at')),
|
|
527
|
+
'usageGeneration': int(value.get('usage_generation') or 1),
|
|
528
|
+
'replacedByItemId': value.get('replaced_by_item_id'),
|
|
529
|
+
'replacedByItemKind': value.get('replaced_by_item_kind'),
|
|
530
|
+
}
|
|
531
|
+
if item_kind == 'entity':
|
|
532
|
+
return {
|
|
533
|
+
'name': value['name'],
|
|
534
|
+
'summary': value.get('summary') or '',
|
|
535
|
+
'invalidAt': _iso(value.get('invalid_at')),
|
|
536
|
+
**epistemic,
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
'type': value['type'],
|
|
540
|
+
'fact': value['fact'],
|
|
541
|
+
'invalidAt': _iso(value.get('invalid_at')),
|
|
542
|
+
**epistemic,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _next_snapshot(previous: dict, request: KnowledgeRevisionCreate, changed_at: datetime):
|
|
547
|
+
value = dict(previous)
|
|
548
|
+
if request.action == 'confirm':
|
|
549
|
+
basis = request.confirmation_basis.model_dump(mode='json')
|
|
550
|
+
basis['confirmed_at'] = changed_at.isoformat()
|
|
551
|
+
value['confirmationStatus'] = 'confirmed'
|
|
552
|
+
value['confirmationStateExplicit'] = True
|
|
553
|
+
value['confirmationBasis'] = basis
|
|
554
|
+
value['epistemicStatus'] = 'confirmed'
|
|
555
|
+
value['confidenceScore'] = 1.0
|
|
556
|
+
elif request.action == 'update':
|
|
557
|
+
content_changed = False
|
|
558
|
+
if request.item_kind == 'entity':
|
|
559
|
+
if request.name is not None:
|
|
560
|
+
content_changed = content_changed or request.name != previous.get('name')
|
|
561
|
+
value['name'] = request.name
|
|
562
|
+
if request.summary is not None:
|
|
563
|
+
content_changed = (
|
|
564
|
+
content_changed or request.summary != previous.get('summary')
|
|
565
|
+
)
|
|
566
|
+
value['summary'] = request.summary
|
|
567
|
+
elif request.fact is not None:
|
|
568
|
+
content_changed = request.fact != previous.get('fact')
|
|
569
|
+
value['fact'] = request.fact
|
|
570
|
+
origin_established = (
|
|
571
|
+
request.origin_quadrant is not None
|
|
572
|
+
and not previous.get('classificationStateExplicit')
|
|
573
|
+
)
|
|
574
|
+
classification_changed = origin_established or (
|
|
575
|
+
request.current_quadrant is not None
|
|
576
|
+
and request.current_quadrant != previous.get('currentQuadrant')
|
|
577
|
+
)
|
|
578
|
+
if request.origin_quadrant is not None:
|
|
579
|
+
if previous.get('classificationStateExplicit'):
|
|
580
|
+
value['originQuadrant'] = previous.get('originQuadrant')
|
|
581
|
+
else:
|
|
582
|
+
value['originQuadrant'] = request.origin_quadrant
|
|
583
|
+
value['classificationStateExplicit'] = True
|
|
584
|
+
if request.current_quadrant is None:
|
|
585
|
+
value['currentQuadrant'] = request.origin_quadrant
|
|
586
|
+
if request.current_quadrant is not None:
|
|
587
|
+
value['currentQuadrant'] = request.current_quadrant
|
|
588
|
+
if request.epistemic_status is not None:
|
|
589
|
+
value['epistemicStatus'] = request.epistemic_status
|
|
590
|
+
if request.confirmation_status is not None:
|
|
591
|
+
value['confirmationStatus'] = request.confirmation_status
|
|
592
|
+
value['confirmationBasis'] = request.confirmation_basis.model_dump(mode='json')
|
|
593
|
+
value['confirmationStateExplicit'] = True
|
|
594
|
+
elif content_changed or classification_changed:
|
|
595
|
+
value['confirmationStatus'] = 'pending'
|
|
596
|
+
value['confirmationBasis'] = _pending_confirmation_basis(
|
|
597
|
+
value.get('confirmationBasis'),
|
|
598
|
+
request.reason,
|
|
599
|
+
)
|
|
600
|
+
value['confirmationStateExplicit'] = True
|
|
601
|
+
trust_reset = (
|
|
602
|
+
request.confirmation_status == 'pending'
|
|
603
|
+
and previous.get('confirmationStatus') != 'pending'
|
|
604
|
+
)
|
|
605
|
+
if content_changed or classification_changed or trust_reset:
|
|
606
|
+
value['usageGeneration'] = int(
|
|
607
|
+
previous.get('usageGeneration') or 1
|
|
608
|
+
) + 1
|
|
609
|
+
value['utilityScore'] = 0.0
|
|
610
|
+
value['qualifiedUseCount'] = 0
|
|
611
|
+
value['distinctTaskCount'] = 0
|
|
612
|
+
value['lastUsedAt'] = None
|
|
613
|
+
if request.reasoning_summary is not None:
|
|
614
|
+
value['reasoningSummary'] = request.reasoning_summary or None
|
|
615
|
+
if request.profile_aspect is not None:
|
|
616
|
+
value['profileAspect'] = (
|
|
617
|
+
None if request.profile_aspect == 'none' else request.profile_aspect
|
|
618
|
+
)
|
|
619
|
+
if value['profileAspect'] is None:
|
|
620
|
+
value['preferenceScope'] = None
|
|
621
|
+
value['preferenceProjectId'] = None
|
|
622
|
+
elif not previous.get('profileAspect'):
|
|
623
|
+
value['preferenceScope'] = 'global'
|
|
624
|
+
value['preferenceProjectId'] = None
|
|
625
|
+
if request.inheritance_mode is not None:
|
|
626
|
+
value['inheritanceMode'] = request.inheritance_mode
|
|
627
|
+
if request.inherited_project_ids is not None:
|
|
628
|
+
value['inheritedProjectIds'] = request.inherited_project_ids
|
|
629
|
+
if value.get('confirmationStatus') == 'confirmed':
|
|
630
|
+
value['epistemicStatus'] = 'confirmed'
|
|
631
|
+
value['confidenceScore'] = 1.0
|
|
632
|
+
elif value.get('confirmationStatus') == 'agent_confirmed':
|
|
633
|
+
value['epistemicStatus'] = 'observed'
|
|
634
|
+
value['confidenceScore'] = min(
|
|
635
|
+
float(value.get('confidenceScore') or 0.75),
|
|
636
|
+
0.85,
|
|
637
|
+
)
|
|
638
|
+
else:
|
|
639
|
+
value['epistemicStatus'] = (
|
|
640
|
+
'exploratory'
|
|
641
|
+
if value.get('originQuadrant') == 'unknown_unknown'
|
|
642
|
+
else 'observed'
|
|
643
|
+
)
|
|
644
|
+
if content_changed or classification_changed or trust_reset:
|
|
645
|
+
value['confidenceScore'] = 0.5
|
|
646
|
+
elif request.action == 'invalidate':
|
|
647
|
+
value['invalidAt'] = changed_at.isoformat()
|
|
648
|
+
value['replacedByItemId'] = request.replacement_item_id
|
|
649
|
+
value['replacedByItemKind'] = request.replacement_item_kind
|
|
650
|
+
elif request.action == 'link_replacement':
|
|
651
|
+
value['replacedByItemId'] = request.replacement_item_id
|
|
652
|
+
value['replacedByItemKind'] = request.replacement_item_kind
|
|
653
|
+
elif request.action == 'restore':
|
|
654
|
+
value['invalidAt'] = None
|
|
655
|
+
value['replacedByItemId'] = None
|
|
656
|
+
value['replacedByItemKind'] = None
|
|
657
|
+
return value
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
async def _updated_embedding(store, previous: dict, current: dict, request):
|
|
661
|
+
if request.action != 'update':
|
|
662
|
+
return None
|
|
663
|
+
key = 'name' if request.item_kind == 'entity' else 'fact'
|
|
664
|
+
if previous.get(key) == current.get(key):
|
|
665
|
+
return None
|
|
666
|
+
return (await store.runtime.embedder.create_batch([current[key]]))[0]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
async def _update_item(store, space, item_id, item_kind, value, embedding):
|
|
670
|
+
invalid_at = _parse_datetime(value.get('invalidAt'))
|
|
671
|
+
if item_kind == 'entity':
|
|
672
|
+
query = '''
|
|
673
|
+
MATCH (item:Entity {uuid: $item_id, group_id: $group_id})
|
|
674
|
+
SET item.name = $name,
|
|
675
|
+
item.summary = $summary,
|
|
676
|
+
item.fuli_invalid_at = $invalid_at,
|
|
677
|
+
item.fuli_origin_quadrant = $origin_quadrant,
|
|
678
|
+
item.fuli_current_quadrant = $current_quadrant,
|
|
679
|
+
item.fuli_epistemic_status = $epistemic_status,
|
|
680
|
+
item.fuli_confirmation_status = $confirmation_status,
|
|
681
|
+
item.fuli_confirmation_basis_json = $confirmation_basis_json,
|
|
682
|
+
item.fuli_reasoning_summary = $reasoning_summary,
|
|
683
|
+
item.fuli_profile_aspect = $profile_aspect,
|
|
684
|
+
item.fuli_preference_scope = $preference_scope,
|
|
685
|
+
item.fuli_preference_project_id = $preference_project_id,
|
|
686
|
+
item.fuli_inheritance_mode = $inheritance_mode,
|
|
687
|
+
item.fuli_inherited_project_ids = $inherited_project_ids,
|
|
688
|
+
item.fuli_utility_score = $utility_score,
|
|
689
|
+
item.fuli_confidence_score = $confidence_score,
|
|
690
|
+
item.fuli_qualified_use_count = $qualified_use_count,
|
|
691
|
+
item.fuli_distinct_task_count = $distinct_task_count,
|
|
692
|
+
item.fuli_last_used_at = $last_used_at,
|
|
693
|
+
item.fuli_usage_generation = $usage_generation,
|
|
694
|
+
item.fuli_replaced_by_item_id = $replaced_by_item_id,
|
|
695
|
+
item.fuli_replaced_by_item_kind = $replaced_by_item_kind,
|
|
696
|
+
item.name_embedding = coalesce($embedding, item.name_embedding)
|
|
697
|
+
'''
|
|
698
|
+
parameters = {'name': value['name'], 'summary': value['summary']}
|
|
699
|
+
else:
|
|
700
|
+
query = '''
|
|
701
|
+
MATCH ()-[item:RELATES_TO {uuid: $item_id, group_id: $group_id}]->()
|
|
702
|
+
SET item.fact = $fact,
|
|
703
|
+
item.invalid_at = $invalid_at,
|
|
704
|
+
item.fuli_origin_quadrant = $origin_quadrant,
|
|
705
|
+
item.fuli_current_quadrant = $current_quadrant,
|
|
706
|
+
item.fuli_epistemic_status = $epistemic_status,
|
|
707
|
+
item.fuli_confirmation_status = $confirmation_status,
|
|
708
|
+
item.fuli_confirmation_basis_json = $confirmation_basis_json,
|
|
709
|
+
item.fuli_reasoning_summary = $reasoning_summary,
|
|
710
|
+
item.fuli_profile_aspect = $profile_aspect,
|
|
711
|
+
item.fuli_preference_scope = $preference_scope,
|
|
712
|
+
item.fuli_preference_project_id = $preference_project_id,
|
|
713
|
+
item.fuli_inheritance_mode = $inheritance_mode,
|
|
714
|
+
item.fuli_inherited_project_ids = $inherited_project_ids,
|
|
715
|
+
item.fuli_utility_score = $utility_score,
|
|
716
|
+
item.fuli_confidence_score = $confidence_score,
|
|
717
|
+
item.fuli_qualified_use_count = $qualified_use_count,
|
|
718
|
+
item.fuli_distinct_task_count = $distinct_task_count,
|
|
719
|
+
item.fuli_last_used_at = $last_used_at,
|
|
720
|
+
item.fuli_usage_generation = $usage_generation,
|
|
721
|
+
item.fuli_replaced_by_item_id = $replaced_by_item_id,
|
|
722
|
+
item.fuli_replaced_by_item_kind = $replaced_by_item_kind,
|
|
723
|
+
item.fact_embedding = coalesce($embedding, item.fact_embedding)
|
|
724
|
+
'''
|
|
725
|
+
parameters = {'fact': value['fact']}
|
|
726
|
+
await store.runtime.driver.execute_query(
|
|
727
|
+
query,
|
|
728
|
+
item_id=item_id,
|
|
729
|
+
group_id=space['group_id'],
|
|
730
|
+
invalid_at=invalid_at,
|
|
731
|
+
origin_quadrant=value['originQuadrant'],
|
|
732
|
+
current_quadrant=value['currentQuadrant'],
|
|
733
|
+
epistemic_status=value['epistemicStatus'],
|
|
734
|
+
confirmation_status=value.get('confirmationStatus', 'pending'),
|
|
735
|
+
confirmation_basis_json=(
|
|
736
|
+
json.dumps(
|
|
737
|
+
value['confirmationBasis'],
|
|
738
|
+
ensure_ascii=False,
|
|
739
|
+
sort_keys=True,
|
|
740
|
+
)
|
|
741
|
+
if value.get('confirmationBasis') else None
|
|
742
|
+
),
|
|
743
|
+
reasoning_summary=value.get('reasoningSummary'),
|
|
744
|
+
profile_aspect=value.get('profileAspect'),
|
|
745
|
+
preference_scope=value.get('preferenceScope'),
|
|
746
|
+
preference_project_id=value.get('preferenceProjectId'),
|
|
747
|
+
inheritance_mode=value.get('inheritanceMode', 'local_only'),
|
|
748
|
+
inherited_project_ids=value.get('inheritedProjectIds') or [],
|
|
749
|
+
utility_score=float(value.get('utilityScore') or 0),
|
|
750
|
+
confidence_score=float(value.get('confidenceScore') or 0.5),
|
|
751
|
+
qualified_use_count=int(value.get('qualifiedUseCount') or 0),
|
|
752
|
+
distinct_task_count=int(value.get('distinctTaskCount') or 0),
|
|
753
|
+
last_used_at=_parse_datetime(value.get('lastUsedAt')),
|
|
754
|
+
usage_generation=int(value.get('usageGeneration') or 1),
|
|
755
|
+
replaced_by_item_id=value.get('replacedByItemId'),
|
|
756
|
+
replaced_by_item_kind=value.get('replacedByItemKind'),
|
|
757
|
+
embedding=embedding,
|
|
758
|
+
**parameters,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _confirmation_basis(value) -> ConfirmationBasis | None:
|
|
763
|
+
if not value:
|
|
764
|
+
return None
|
|
765
|
+
if isinstance(value, str):
|
|
766
|
+
value = json.loads(value)
|
|
767
|
+
return ConfirmationBasis.model_validate(value)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _pending_confirmation_basis(value, reason: str) -> dict:
|
|
771
|
+
basis = dict(value or {})
|
|
772
|
+
basis.setdefault('existence_reason', reason)
|
|
773
|
+
basis.setdefault('quadrant_reason', reason)
|
|
774
|
+
basis.setdefault('proposed_by', {'kind': 'user', 'label': '当前用户'})
|
|
775
|
+
basis['confirmed_by'] = None
|
|
776
|
+
basis['confirmed_at'] = None
|
|
777
|
+
basis.pop('agent_policy_version', None)
|
|
778
|
+
return basis
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
async def _update_preference_scope(
|
|
782
|
+
store,
|
|
783
|
+
space,
|
|
784
|
+
item_id,
|
|
785
|
+
item_kind,
|
|
786
|
+
scope,
|
|
787
|
+
project_id,
|
|
788
|
+
):
|
|
789
|
+
if item_kind == 'entity':
|
|
790
|
+
query = '''
|
|
791
|
+
MATCH (item:Entity {uuid: $item_id, group_id: $group_id})
|
|
792
|
+
SET item.fuli_preference_scope = $scope,
|
|
793
|
+
item.fuli_preference_project_id = $project_id
|
|
794
|
+
'''
|
|
795
|
+
else:
|
|
796
|
+
query = '''
|
|
797
|
+
MATCH ()-[item:RELATES_TO {uuid: $item_id, group_id: $group_id}]->()
|
|
798
|
+
SET item.fuli_preference_scope = $scope,
|
|
799
|
+
item.fuli_preference_project_id = $project_id
|
|
800
|
+
'''
|
|
801
|
+
await store.runtime.driver.execute_query(
|
|
802
|
+
query,
|
|
803
|
+
item_id=item_id,
|
|
804
|
+
group_id=space['group_id'],
|
|
805
|
+
scope=scope,
|
|
806
|
+
project_id=project_id,
|
|
807
|
+
)
|
|
808
|
+
def _iso(value):
|
|
809
|
+
native = _native_datetime(value)
|
|
810
|
+
return native.isoformat() if isinstance(native, datetime) else native
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _parse_datetime(value):
|
|
814
|
+
if not value or isinstance(value, datetime):
|
|
815
|
+
return value
|
|
816
|
+
return datetime.fromisoformat(value.replace('Z', '+00:00'))
|