fuli-context 0.7.3 → 0.7.4-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -18
- package/README.zh-CN.md +146 -12
- package/compose.graphiti.yml +2 -0
- package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
- package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
- package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
- package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
- package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
- package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
- package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
- package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
- package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
- package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
- package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
- package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
- package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
- package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
- package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
- package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
- package/dist/web/assets/index-F5e2sQ7p.js +2 -0
- package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
- package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
- package/dist/web/assets/paths-CDREouhi.js +1 -0
- package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
- package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
- package/dist/web/index.html +7 -6
- package/graph-provider/fuli_graph/app.py +238 -2
- package/graph-provider/fuli_graph/auth.py +18 -0
- package/graph-provider/fuli_graph/collaboration_context.py +253 -17
- package/graph-provider/fuli_graph/common_knowledge.py +6 -2
- package/graph-provider/fuli_graph/config.py +5 -0
- package/graph-provider/fuli_graph/graph_models.py +6 -0
- package/graph-provider/fuli_graph/graph_projection.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +27 -3
- package/graph-provider/fuli_graph/knowledge_management.py +19 -1
- package/graph-provider/fuli_graph/knowledge_search.py +11 -0
- package/graph-provider/fuli_graph/models.py +15 -2
- package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
- package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
- package/graph-provider/fuli_graph/project_action_models.py +22 -0
- package/graph-provider/fuli_graph/project_knowledge.py +121 -2
- package/graph-provider/fuli_graph/provider_values.py +24 -0
- package/graph-provider/fuli_graph/runtime.py +78 -0
- package/graph-provider/fuli_graph/search_projection.py +14 -1
- package/graph-provider/fuli_graph/store_knowledge.py +116 -2
- package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
- package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
- package/graph-provider/fuli_graph/workflow_observations.py +84 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +4 -4
- package/skills/capturing-session-knowledge/SKILL.md +32 -5
- package/skills/flreview/SKILL.md +4 -3
- package/src/agent-tools/graph-definitions.js +136 -20
- package/src/agent-tools/graph-handlers.js +10 -4
- package/src/cli/command-registry.js +1 -1
- package/src/cli/local-runtime-command.js +69 -2
- package/src/graphiti/agent-knowledge-workflows.js +497 -16
- package/src/graphiti/collaboration-preference-projection.js +162 -0
- package/src/graphiti/federated-application.js +155 -161
- package/src/graphiti/federated-search-support.js +102 -0
- package/src/graphiti/knowledge-provider-mapping.js +15 -0
- package/src/graphiti/provider-client.js +162 -10
- package/src/graphiti/related-project-suggestions.js +109 -0
- package/src/graphiti/runtime-config.js +14 -0
- package/src/graphiti/source-marker.js +2 -2
- package/src/graphiti/user-taste-skill.js +241 -0
- package/src/graphiti/workflow-candidate-mapping.js +87 -0
- package/src/graphiti/workflow-observation.js +82 -0
- package/src/graphiti/writing-taste-profile-workflow.js +25 -0
- package/src/graphiti/writing-taste-profile.js +490 -0
- package/src/http/graph-api-router.js +15 -2
- package/src/mcp/context-resources.js +158 -0
- package/src/mcp/create-mcp-server.js +39 -5
- package/src/mcp/instructions.js +5 -7
- package/src/mcp/session-id.js +60 -0
- package/src/mcp/task-context-registry.js +15 -3
- package/src/mcp/tool-annotations.js +8 -1
- package/src/mcp/tool-result.js +28 -4
- package/src/server.js +0 -1
- package/src/setup/claude-code-config.js +17 -6
- package/src/setup/graph-runtime.js +20 -2
- package/src/system/runtime-settings.js +121 -2
- package/src/system/system-service.js +5 -1
- package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
- package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
- package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
- package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
- package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
- package/dist/web/assets/index-BzhNfzU_.js +0 -2
- package/dist/web/assets/paths-DwHeiGMT.js +0 -1
- package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
- package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
|
@@ -29,6 +29,7 @@ KnowledgeProjectMatchKind = Literal[
|
|
|
29
29
|
'exact_duplicate',
|
|
30
30
|
'conflict',
|
|
31
31
|
]
|
|
32
|
+
PersonalProjectRelationStatus = Literal['pending', 'active', 'rejected']
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class KnowledgeProjectPreviewRequest(StrictModel):
|
|
@@ -140,3 +141,24 @@ class KnowledgeProjectActionResult(StrictModel):
|
|
|
140
141
|
match: KnowledgeProjectMatch
|
|
141
142
|
reference: KnowledgeProjectReferenceRecord | None = None
|
|
142
143
|
conflict: KnowledgeConflictRecord | None = None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class PersonalProjectRelationReviewRequest(StrictModel):
|
|
147
|
+
decision: Literal['activate', 'reject']
|
|
148
|
+
decision_revision: int = Field(ge=0)
|
|
149
|
+
reason: str = Field(min_length=1, max_length=2000)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PersonalProjectRelationReviewRecord(StrictModel):
|
|
153
|
+
relation_id: str
|
|
154
|
+
personal_space_id: str
|
|
155
|
+
source_project_id: str
|
|
156
|
+
target_project_id: str
|
|
157
|
+
relation_type: ProjectRelationType
|
|
158
|
+
status: PersonalProjectRelationStatus
|
|
159
|
+
confirmation_authority: Literal['human_review'] | None = None
|
|
160
|
+
decision_revision: int = Field(ge=1)
|
|
161
|
+
reviewed_by: str
|
|
162
|
+
reviewed_at: datetime
|
|
163
|
+
review_reason: str
|
|
164
|
+
review_event_id: str
|
|
@@ -23,8 +23,11 @@ from .project_action_models import (
|
|
|
23
23
|
KnowledgeProjectMatch,
|
|
24
24
|
KnowledgeProjectPreviewRecord,
|
|
25
25
|
KnowledgeProjectPreviewRequest,
|
|
26
|
+
PersonalProjectRelationReviewRecord,
|
|
27
|
+
PersonalProjectRelationReviewRequest,
|
|
26
28
|
)
|
|
27
29
|
from .provider_values import (
|
|
30
|
+
native_datetime as _native_datetime,
|
|
28
31
|
normalized_text as _normalized,
|
|
29
32
|
stable_uuid as _stable_uuid,
|
|
30
33
|
)
|
|
@@ -209,7 +212,10 @@ async def _record_project_action_if_human(
|
|
|
209
212
|
item_id,
|
|
210
213
|
request,
|
|
211
214
|
):
|
|
212
|
-
if
|
|
215
|
+
if (
|
|
216
|
+
request.operation_actor != 'human'
|
|
217
|
+
or actor.get('_human_review_verified') is not True
|
|
218
|
+
):
|
|
213
219
|
return
|
|
214
220
|
await record_human_change(
|
|
215
221
|
store,
|
|
@@ -295,6 +301,70 @@ async def read_personal_project_relations(store, space_id: str) -> list[dict]:
|
|
|
295
301
|
]
|
|
296
302
|
|
|
297
303
|
|
|
304
|
+
async def review_personal_project_relation(
|
|
305
|
+
store,
|
|
306
|
+
actor: dict,
|
|
307
|
+
space_id: str,
|
|
308
|
+
relation_id: str,
|
|
309
|
+
request: PersonalProjectRelationReviewRequest,
|
|
310
|
+
) -> PersonalProjectRelationReviewRecord:
|
|
311
|
+
store._require_personal()
|
|
312
|
+
space = await store.authorize(actor, space_id, 'maintainer')
|
|
313
|
+
if space['kind'] != 'personal':
|
|
314
|
+
raise HTTPException(
|
|
315
|
+
status_code=422,
|
|
316
|
+
detail='personal project relation review is personal-only',
|
|
317
|
+
)
|
|
318
|
+
reviewed_at = datetime.now(timezone.utc)
|
|
319
|
+
next_revision = request.decision_revision + 1
|
|
320
|
+
status = 'active' if request.decision == 'activate' else 'rejected'
|
|
321
|
+
review_event_id = _stable_uuid(
|
|
322
|
+
space_id,
|
|
323
|
+
'personal-project-relation-review',
|
|
324
|
+
relation_id,
|
|
325
|
+
str(next_revision),
|
|
326
|
+
request.decision,
|
|
327
|
+
)
|
|
328
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
329
|
+
_REVIEW_PERSONAL_PROJECT_RELATION_QUERY,
|
|
330
|
+
space_id=space_id,
|
|
331
|
+
relation_id=relation_id,
|
|
332
|
+
expected_revision=request.decision_revision,
|
|
333
|
+
next_revision=next_revision,
|
|
334
|
+
status=status,
|
|
335
|
+
confirmation_authority=(
|
|
336
|
+
'human_review' if status == 'active' else None
|
|
337
|
+
),
|
|
338
|
+
reviewed_by=actor['id'],
|
|
339
|
+
reviewed_at=reviewed_at,
|
|
340
|
+
review_reason=request.reason,
|
|
341
|
+
review_event_id=review_event_id,
|
|
342
|
+
)
|
|
343
|
+
if not records:
|
|
344
|
+
raise HTTPException(
|
|
345
|
+
status_code=409,
|
|
346
|
+
detail=(
|
|
347
|
+
'personal project relation is missing or its review revision '
|
|
348
|
+
'changed; refresh before deciding'
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
value = dict(records[0])
|
|
352
|
+
return PersonalProjectRelationReviewRecord(
|
|
353
|
+
relation_id=value['relation_id'],
|
|
354
|
+
personal_space_id=space_id,
|
|
355
|
+
source_project_id=value['source_project_id'],
|
|
356
|
+
target_project_id=value['target_project_id'],
|
|
357
|
+
relation_type=value['relation_type'],
|
|
358
|
+
status=value['status'],
|
|
359
|
+
confirmation_authority=value.get('confirmation_authority'),
|
|
360
|
+
decision_revision=int(value['decision_revision']),
|
|
361
|
+
reviewed_by=value['reviewed_by'],
|
|
362
|
+
reviewed_at=_native_datetime(value['reviewed_at']),
|
|
363
|
+
review_reason=value['review_reason'],
|
|
364
|
+
review_event_id=value['review_event_id'],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
298
368
|
async def _authorize_item(store, actor, item_id, request):
|
|
299
369
|
store._require_personal()
|
|
300
370
|
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
@@ -631,7 +701,13 @@ async def _maybe_create_project_relation(
|
|
|
631
701
|
relation_type: $relation_type
|
|
632
702
|
}]->(target)
|
|
633
703
|
ON CREATE SET relation.created_at = $changed_at,
|
|
634
|
-
relation.created_by = $created_by
|
|
704
|
+
relation.created_by = $created_by,
|
|
705
|
+
relation.status = 'pending',
|
|
706
|
+
relation.confirmation_authority = null,
|
|
707
|
+
relation.decision_revision = 0,
|
|
708
|
+
relation.reviewed_by = null,
|
|
709
|
+
relation.reviewed_at = null,
|
|
710
|
+
relation.review_reason = null
|
|
635
711
|
SET relation.updated_at = $changed_at
|
|
636
712
|
RETURN relation.id AS id
|
|
637
713
|
''',
|
|
@@ -646,6 +722,49 @@ async def _maybe_create_project_relation(
|
|
|
646
722
|
return bool(records)
|
|
647
723
|
|
|
648
724
|
|
|
725
|
+
_REVIEW_PERSONAL_PROJECT_RELATION_QUERY = '''
|
|
726
|
+
/* fuli:review-personal-project-relation */
|
|
727
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
728
|
+
[:CONTAINS_PROJECT]->
|
|
729
|
+
(source:FuliPersonalProject)-
|
|
730
|
+
[relation:PERSONAL_PROJECT_RELATION {id: $relation_id}]->
|
|
731
|
+
(target:FuliPersonalProject)
|
|
732
|
+
SET relation.cas_lock = coalesce(relation.cas_lock, 0) + 1
|
|
733
|
+
WITH space, source, target, relation
|
|
734
|
+
WHERE coalesce(relation.decision_revision, 0) = $expected_revision
|
|
735
|
+
SET relation.status = $status,
|
|
736
|
+
relation.confirmation_authority = $confirmation_authority,
|
|
737
|
+
relation.decision_revision = $next_revision,
|
|
738
|
+
relation.reviewed_by = $reviewed_by,
|
|
739
|
+
relation.reviewed_at = $reviewed_at,
|
|
740
|
+
relation.review_reason = $review_reason,
|
|
741
|
+
relation.updated_at = $reviewed_at
|
|
742
|
+
CREATE (review:FuliPersonalProjectRelationReview {
|
|
743
|
+
id: $review_event_id,
|
|
744
|
+
space_id: $space_id,
|
|
745
|
+
relation_id: $relation_id,
|
|
746
|
+
decision_revision: $next_revision,
|
|
747
|
+
decision: $status,
|
|
748
|
+
confirmation_authority: 'human_review',
|
|
749
|
+
reason: $review_reason,
|
|
750
|
+
reviewed_by: $reviewed_by,
|
|
751
|
+
reviewed_at: $reviewed_at
|
|
752
|
+
})
|
|
753
|
+
MERGE (space)-[:HAS_PERSONAL_PROJECT_RELATION_REVIEW]->(review)
|
|
754
|
+
RETURN relation.id AS relation_id,
|
|
755
|
+
source.project_id AS source_project_id,
|
|
756
|
+
target.project_id AS target_project_id,
|
|
757
|
+
relation.relation_type AS relation_type,
|
|
758
|
+
relation.status AS status,
|
|
759
|
+
relation.confirmation_authority AS confirmation_authority,
|
|
760
|
+
relation.decision_revision AS decision_revision,
|
|
761
|
+
relation.reviewed_by AS reviewed_by,
|
|
762
|
+
relation.reviewed_at AS reviewed_at,
|
|
763
|
+
relation.review_reason AS review_reason,
|
|
764
|
+
review.id AS review_event_id
|
|
765
|
+
'''
|
|
766
|
+
|
|
767
|
+
|
|
649
768
|
def _relation_endpoints(source_project_id, target_project_id, request):
|
|
650
769
|
"""Keep an extracted project as the child side of directional relations."""
|
|
651
770
|
if request.mode == 'create':
|
|
@@ -36,5 +36,29 @@ def json_object(value) -> dict:
|
|
|
36
36
|
return parsed if isinstance(parsed, dict) else {}
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
_PREFERENCE_QUALIFIER_EXCLUSIONS = frozenset({
|
|
40
|
+
'preferenceKey',
|
|
41
|
+
'preference_key',
|
|
42
|
+
'searchTerms',
|
|
43
|
+
'search_terms',
|
|
44
|
+
'weight',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def preference_qualifiers(value) -> dict:
|
|
49
|
+
"""Project content qualifiers safe to expose and bind to review state.
|
|
50
|
+
|
|
51
|
+
Retrieval aliases, ranking hints, and the separately projected semantic key
|
|
52
|
+
are intentionally omitted. Remaining JSON attributes preserve source-level
|
|
53
|
+
qualifications such as language, audience, conditions, and rationale.
|
|
54
|
+
"""
|
|
55
|
+
attributes = json_object(value)
|
|
56
|
+
return {
|
|
57
|
+
str(key): attributes[key]
|
|
58
|
+
for key in sorted(attributes, key=str)
|
|
59
|
+
if key not in _PREFERENCE_QUALIFIER_EXCLUSIONS
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
def normalized_text(value) -> str:
|
|
40
64
|
return ' '.join(str(value or '').strip().casefold().split())
|
|
@@ -181,6 +181,60 @@ class GraphitiRuntime:
|
|
|
181
181
|
'CREATE CONSTRAINT fuli_knowledge_review_decision_id IF NOT EXISTS '
|
|
182
182
|
'FOR (n:FuliKnowledgeReviewDecision) REQUIRE n.id IS UNIQUE'
|
|
183
183
|
)
|
|
184
|
+
await self.driver.execute_query(
|
|
185
|
+
'CREATE CONSTRAINT fuli_workflow_candidate_id IF NOT EXISTS '
|
|
186
|
+
'FOR (n:FuliWorkflowCandidate) REQUIRE n.id IS UNIQUE'
|
|
187
|
+
)
|
|
188
|
+
await self.driver.execute_query(
|
|
189
|
+
'CREATE CONSTRAINT fuli_workflow_review_event_id IF NOT EXISTS '
|
|
190
|
+
'FOR (n:FuliWorkflowReviewEvent) REQUIRE n.id IS UNIQUE'
|
|
191
|
+
)
|
|
192
|
+
await self.driver.execute_query(
|
|
193
|
+
'CREATE CONSTRAINT fuli_workflow_review_preview_id IF NOT EXISTS '
|
|
194
|
+
'FOR (n:FuliWorkflowReviewPreview) REQUIRE n.id IS UNIQUE'
|
|
195
|
+
)
|
|
196
|
+
await self.driver.execute_query(
|
|
197
|
+
'CREATE CONSTRAINT fuli_workflow_review_preview_token IF NOT EXISTS '
|
|
198
|
+
'FOR (n:FuliWorkflowReviewPreview) REQUIRE n.token_hash IS UNIQUE'
|
|
199
|
+
)
|
|
200
|
+
await self.driver.execute_query(
|
|
201
|
+
'CREATE CONSTRAINT fuli_workflow_rule_id IF NOT EXISTS '
|
|
202
|
+
'FOR (n:FuliWorkflowRule) REQUIRE n.id IS UNIQUE'
|
|
203
|
+
)
|
|
204
|
+
await self.driver.execute_query(
|
|
205
|
+
'CREATE CONSTRAINT fuli_workflow_authorization_id IF NOT EXISTS '
|
|
206
|
+
'FOR (n:FuliWorkflowAuthorization) REQUIRE n.id IS UNIQUE'
|
|
207
|
+
)
|
|
208
|
+
await self.driver.execute_query(
|
|
209
|
+
'CREATE CONSTRAINT fuli_personal_global_preference_decision '
|
|
210
|
+
'IF NOT EXISTS '
|
|
211
|
+
'FOR (n:FuliPersonalGlobalPreferenceDecision) '
|
|
212
|
+
'REQUIRE (n.space_id, n.candidate_id) IS UNIQUE'
|
|
213
|
+
)
|
|
214
|
+
await self.driver.execute_query(
|
|
215
|
+
'CREATE CONSTRAINT fuli_personal_global_preference_event '
|
|
216
|
+
'IF NOT EXISTS '
|
|
217
|
+
'FOR (n:FuliPersonalGlobalPreferenceDecisionEvent) '
|
|
218
|
+
'REQUIRE n.id IS UNIQUE'
|
|
219
|
+
)
|
|
220
|
+
await self.driver.execute_query(
|
|
221
|
+
'CREATE CONSTRAINT fuli_personal_global_preference_preview '
|
|
222
|
+
'IF NOT EXISTS '
|
|
223
|
+
'FOR (n:FuliPersonalGlobalPreferenceDecisionPreview) '
|
|
224
|
+
'REQUIRE n.id IS UNIQUE'
|
|
225
|
+
)
|
|
226
|
+
await self.driver.execute_query(
|
|
227
|
+
'CREATE CONSTRAINT fuli_personal_global_preference_preview_token '
|
|
228
|
+
'IF NOT EXISTS '
|
|
229
|
+
'FOR (n:FuliPersonalGlobalPreferenceDecisionPreview) '
|
|
230
|
+
'REQUIRE n.token_hash IS UNIQUE'
|
|
231
|
+
)
|
|
232
|
+
await self.driver.execute_query(
|
|
233
|
+
'CREATE CONSTRAINT fuli_personal_project_relation_review '
|
|
234
|
+
'IF NOT EXISTS '
|
|
235
|
+
'FOR (n:FuliPersonalProjectRelationReview) '
|
|
236
|
+
'REQUIRE n.id IS UNIQUE'
|
|
237
|
+
)
|
|
184
238
|
for query in (
|
|
185
239
|
'CREATE INDEX fuli_entity_confirmation IF NOT EXISTS '
|
|
186
240
|
'FOR (n:Entity) ON (n.group_id, n.fuli_confirmation_status)',
|
|
@@ -208,10 +262,34 @@ class GraphitiRuntime:
|
|
|
208
262
|
):
|
|
209
263
|
await self.driver.execute_query(query)
|
|
210
264
|
await self._migrate_knowledge_lifecycle_defaults()
|
|
265
|
+
await self._migrate_personal_project_relation_review_defaults()
|
|
211
266
|
await self._migrate_space_visibility_and_owners()
|
|
212
267
|
await self._migrate_project_releases()
|
|
213
268
|
await self._migrate_legacy_group_ids()
|
|
214
269
|
|
|
270
|
+
async def _migrate_personal_project_relation_review_defaults(self) -> None:
|
|
271
|
+
await self.driver.execute_query(
|
|
272
|
+
'''
|
|
273
|
+
MATCH ()-[relation:PERSONAL_PROJECT_RELATION]->()
|
|
274
|
+
WHERE relation.status IS NULL
|
|
275
|
+
OR relation.decision_revision IS NULL
|
|
276
|
+
OR relation.confirmation_authority IS NULL
|
|
277
|
+
SET relation.status = CASE
|
|
278
|
+
WHEN relation.status IN ['active', 'rejected']
|
|
279
|
+
THEN relation.status
|
|
280
|
+
ELSE 'pending'
|
|
281
|
+
END,
|
|
282
|
+
relation.decision_revision =
|
|
283
|
+
coalesce(relation.decision_revision, 0),
|
|
284
|
+
relation.confirmation_authority = CASE
|
|
285
|
+
WHEN relation.status = 'active'
|
|
286
|
+
AND relation.confirmation_authority = 'human_review'
|
|
287
|
+
THEN 'human_review'
|
|
288
|
+
ELSE null
|
|
289
|
+
END
|
|
290
|
+
'''
|
|
291
|
+
)
|
|
292
|
+
|
|
215
293
|
async def _migrate_space_visibility_and_owners(self) -> None:
|
|
216
294
|
await self.driver.execute_query(
|
|
217
295
|
"MATCH (s:FuliSpace) WHERE s.visibility IS NULL "
|
|
@@ -7,7 +7,11 @@ from .models import (
|
|
|
7
7
|
FactResult,
|
|
8
8
|
_validate_confirmation_state,
|
|
9
9
|
)
|
|
10
|
-
from .provider_values import
|
|
10
|
+
from .provider_values import (
|
|
11
|
+
json_object,
|
|
12
|
+
native_datetime as _native_datetime,
|
|
13
|
+
preference_qualifiers,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
def is_default_retrievable(metadata: dict) -> bool:
|
|
@@ -54,6 +58,7 @@ async def read_edge_epistemic_metadata(store, edge_ids: list[str]) -> dict[str,
|
|
|
54
58
|
AS preference_scope,
|
|
55
59
|
edge.fuli_preference_project_id AS preference_project_id,
|
|
56
60
|
edge.fuli_key AS key,
|
|
61
|
+
edge.fuli_attributes_json AS attributes_json,
|
|
57
62
|
coalesce(edge.fuli_inheritance_mode, 'local_only')
|
|
58
63
|
AS inheritance_mode,
|
|
59
64
|
coalesce(edge.fuli_inherited_project_ids, [])
|
|
@@ -111,6 +116,7 @@ def fact_result(
|
|
|
111
116
|
epistemic: dict | None = None,
|
|
112
117
|
) -> FactResult:
|
|
113
118
|
metadata = epistemic or {}
|
|
119
|
+
attributes = json_object(metadata.get('attributes_json'))
|
|
114
120
|
return FactResult(
|
|
115
121
|
id=edge.uuid,
|
|
116
122
|
space_id=space_id,
|
|
@@ -135,6 +141,13 @@ def fact_result(
|
|
|
135
141
|
preference_scope=metadata.get('preference_scope'),
|
|
136
142
|
preference_project_id=metadata.get('preference_project_id'),
|
|
137
143
|
key=metadata.get('key'),
|
|
144
|
+
preference_key=(
|
|
145
|
+
attributes.get('preferenceKey')
|
|
146
|
+
or attributes.get('preference_key')
|
|
147
|
+
or metadata.get('key')
|
|
148
|
+
or edge.uuid
|
|
149
|
+
),
|
|
150
|
+
preference_qualifiers=preference_qualifiers(attributes),
|
|
138
151
|
defined_project_id=metadata.get('defined_project_id'),
|
|
139
152
|
inheritance_mode=metadata.get('inheritance_mode') or 'local_only',
|
|
140
153
|
inherited_project_ids=metadata.get('inherited_project_ids') or [],
|
|
@@ -17,6 +17,7 @@ from .models import (
|
|
|
17
17
|
)
|
|
18
18
|
from .personal_project_access import authorize_personal_project
|
|
19
19
|
from .provider_values import now_utc, stable_uuid
|
|
20
|
+
from .workflow_candidates import materialize_workflow_candidates
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class StoreKnowledge:
|
|
@@ -35,6 +36,29 @@ class StoreKnowledge:
|
|
|
35
36
|
personal_project_id=request.personal_project_id,
|
|
36
37
|
)
|
|
37
38
|
|
|
39
|
+
async def commit_workflow_observation(
|
|
40
|
+
self,
|
|
41
|
+
actor: dict,
|
|
42
|
+
request: KnowledgeCommit,
|
|
43
|
+
) -> CommitResult:
|
|
44
|
+
self._require_personal()
|
|
45
|
+
space = await self.authorize(actor, request.space_id, 'maintainer')
|
|
46
|
+
if space['kind'] != 'personal':
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=422,
|
|
49
|
+
detail='workflow observations are personal-only',
|
|
50
|
+
)
|
|
51
|
+
if request.personal_project_id:
|
|
52
|
+
await authorize_personal_project(
|
|
53
|
+
self, actor, space, request.personal_project_id
|
|
54
|
+
)
|
|
55
|
+
return await self._commit_episode(
|
|
56
|
+
space,
|
|
57
|
+
request.episode,
|
|
58
|
+
personal_project_id=request.personal_project_id,
|
|
59
|
+
workflow_session_authority='mcp_host',
|
|
60
|
+
)
|
|
61
|
+
|
|
38
62
|
async def search(self, actor: dict, request: SearchRequest) -> SearchResult:
|
|
39
63
|
return await search_knowledge(self, actor, request)
|
|
40
64
|
|
|
@@ -71,6 +95,7 @@ class StoreKnowledge:
|
|
|
71
95
|
episode: StructuredEpisode,
|
|
72
96
|
*,
|
|
73
97
|
personal_project_id: str | None = None,
|
|
98
|
+
workflow_session_authority: str | None = None,
|
|
74
99
|
) -> CommitResult:
|
|
75
100
|
group_id = space['group_id']
|
|
76
101
|
lock = self._group_locks.setdefault(group_id, asyncio.Lock())
|
|
@@ -90,6 +115,12 @@ class StoreKnowledge:
|
|
|
90
115
|
)
|
|
91
116
|
for entity in episode.entities
|
|
92
117
|
]
|
|
118
|
+
entity_id_by_key = {
|
|
119
|
+
entity.key: entity_id
|
|
120
|
+
for entity, entity_id in zip(
|
|
121
|
+
episode.entities, entity_ids, strict=True
|
|
122
|
+
)
|
|
123
|
+
}
|
|
93
124
|
relationship_ids = [
|
|
94
125
|
_relationship_id(
|
|
95
126
|
group_id,
|
|
@@ -98,10 +129,40 @@ class StoreKnowledge:
|
|
|
98
129
|
relationship.key,
|
|
99
130
|
relationship.fact,
|
|
100
131
|
(relationship.valid_at or episode.reference_time).isoformat(),
|
|
132
|
+
workflow_session_authority=workflow_session_authority,
|
|
101
133
|
)
|
|
102
134
|
for relationship in episode.relationships
|
|
103
135
|
]
|
|
136
|
+
entity_input_by_key = {
|
|
137
|
+
entity.key: entity for entity in episode.entities
|
|
138
|
+
}
|
|
139
|
+
workflow_pairs = [
|
|
140
|
+
{
|
|
141
|
+
'workflow_key': relationship.key,
|
|
142
|
+
'source_step_id': entity_id_by_key[relationship.source],
|
|
143
|
+
'source_step_key': relationship.source,
|
|
144
|
+
'source_step_name': (
|
|
145
|
+
entity_input_by_key[relationship.source].name
|
|
146
|
+
),
|
|
147
|
+
'target_step_id': entity_id_by_key[relationship.target],
|
|
148
|
+
'target_step_key': relationship.target,
|
|
149
|
+
'target_step_name': (
|
|
150
|
+
entity_input_by_key[relationship.target].name
|
|
151
|
+
),
|
|
152
|
+
'condition_json': _workflow_condition_json(
|
|
153
|
+
relationship.attributes
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
for relationship in episode.relationships
|
|
157
|
+
if relationship.type == 'RECOMMENDS_NEXT'
|
|
158
|
+
]
|
|
104
159
|
if existing:
|
|
160
|
+
await materialize_workflow_candidates(
|
|
161
|
+
self,
|
|
162
|
+
space,
|
|
163
|
+
personal_project_id=personal_project_id,
|
|
164
|
+
pairs=workflow_pairs,
|
|
165
|
+
)
|
|
105
166
|
return CommitResult(
|
|
106
167
|
status='duplicate',
|
|
107
168
|
space_id=space['id'],
|
|
@@ -210,6 +271,15 @@ class StoreKnowledge:
|
|
|
210
271
|
'attributes_json': json.dumps(
|
|
211
272
|
relationship.attributes, ensure_ascii=False, sort_keys=True
|
|
212
273
|
),
|
|
274
|
+
'workflow_condition_json': _workflow_condition_json(
|
|
275
|
+
relationship.attributes
|
|
276
|
+
),
|
|
277
|
+
'workflow_confirmation_authority': (
|
|
278
|
+
_workflow_confirmation_authority(relationship)
|
|
279
|
+
),
|
|
280
|
+
'workflow_session_authority': (
|
|
281
|
+
workflow_session_authority
|
|
282
|
+
),
|
|
213
283
|
}
|
|
214
284
|
)
|
|
215
285
|
|
|
@@ -276,7 +346,9 @@ class StoreKnowledge:
|
|
|
276
346
|
fuli_summary: $summary,
|
|
277
347
|
fuli_sensitivity: $sensitivity,
|
|
278
348
|
fuli_personal_project_id: $personal_project_id,
|
|
279
|
-
fuli_idempotency_key: $idempotency_key
|
|
349
|
+
fuli_idempotency_key: $idempotency_key,
|
|
350
|
+
fuli_workflow_session_authority:
|
|
351
|
+
$workflow_session_authority
|
|
280
352
|
})
|
|
281
353
|
WITH episode
|
|
282
354
|
UNWIND $entities AS row
|
|
@@ -316,7 +388,13 @@ class StoreKnowledge:
|
|
|
316
388
|
edge.fuli_inherited_project_ids = row.inherited_project_ids,
|
|
317
389
|
edge.fuli_preference_scope = row.preference_scope,
|
|
318
390
|
edge.fuli_preference_project_id = row.preference_project_id,
|
|
319
|
-
edge.fuli_attributes_json = row.attributes_json
|
|
391
|
+
edge.fuli_attributes_json = row.attributes_json,
|
|
392
|
+
edge.fuli_workflow_condition_json =
|
|
393
|
+
row.workflow_condition_json,
|
|
394
|
+
edge.fuli_workflow_confirmation_authority =
|
|
395
|
+
row.workflow_confirmation_authority,
|
|
396
|
+
edge.fuli_workflow_session_authority =
|
|
397
|
+
row.workflow_session_authority
|
|
320
398
|
SET edge.episodes =
|
|
321
399
|
CASE WHEN $episode_id IN coalesce(edge.episodes, [])
|
|
322
400
|
THEN edge.episodes
|
|
@@ -400,8 +478,15 @@ class StoreKnowledge:
|
|
|
400
478
|
summary=episode.summary,
|
|
401
479
|
sensitivity=episode.sensitivity,
|
|
402
480
|
personal_project_id=personal_project_id,
|
|
481
|
+
workflow_session_authority=workflow_session_authority,
|
|
403
482
|
idempotency_key=episode.idempotency_key,
|
|
404
483
|
)
|
|
484
|
+
await materialize_workflow_candidates(
|
|
485
|
+
self,
|
|
486
|
+
space,
|
|
487
|
+
personal_project_id=personal_project_id,
|
|
488
|
+
pairs=workflow_pairs,
|
|
489
|
+
)
|
|
405
490
|
return CommitResult(
|
|
406
491
|
status='committed',
|
|
407
492
|
space_id=space['id'],
|
|
@@ -419,6 +504,28 @@ def _initial_confidence(status: str) -> float:
|
|
|
419
504
|
}[status]
|
|
420
505
|
|
|
421
506
|
|
|
507
|
+
def _workflow_condition_json(attributes: dict) -> str:
|
|
508
|
+
condition = attributes.get(
|
|
509
|
+
'workflowCondition',
|
|
510
|
+
attributes.get('workflow_condition', {}),
|
|
511
|
+
)
|
|
512
|
+
if not isinstance(condition, dict):
|
|
513
|
+
condition = {}
|
|
514
|
+
return json.dumps(condition, ensure_ascii=False, sort_keys=True)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _workflow_confirmation_authority(relationship) -> str:
|
|
518
|
+
basis = relationship.confirmation_basis
|
|
519
|
+
if relationship.confirmation_status == 'confirmed' and basis.confirmed_by:
|
|
520
|
+
return basis.confirmed_by.kind
|
|
521
|
+
return {
|
|
522
|
+
'agent': 'agent_proposed',
|
|
523
|
+
'import': 'import_proposed',
|
|
524
|
+
'user': 'user',
|
|
525
|
+
'authoritative_source': 'authoritative_source',
|
|
526
|
+
}[basis.proposed_by.kind]
|
|
527
|
+
|
|
528
|
+
|
|
422
529
|
def _entity_id(
|
|
423
530
|
group_id: str,
|
|
424
531
|
space_kind: str,
|
|
@@ -443,8 +550,15 @@ def _relationship_id(
|
|
|
443
550
|
key: str,
|
|
444
551
|
fact: str,
|
|
445
552
|
reference_time: str,
|
|
553
|
+
*,
|
|
554
|
+
workflow_session_authority: str | None = None,
|
|
446
555
|
) -> str:
|
|
447
556
|
parts = [group_id, 'relationship']
|
|
448
557
|
if space_kind == 'personal' and personal_project_id:
|
|
449
558
|
parts.extend(['personal-project', personal_project_id])
|
|
559
|
+
if workflow_session_authority:
|
|
560
|
+
parts.extend([
|
|
561
|
+
'workflow-session-authority',
|
|
562
|
+
workflow_session_authority,
|
|
563
|
+
])
|
|
450
564
|
return stable_uuid(*parts, key, fact, reference_time)
|