fuli-context 0.11.0 → 0.12.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/dist/web/assets/AboutPage-BAD0iPVX.js +1 -0
- package/dist/web/assets/AgentConversations-1iGTd4sz.js +2 -0
- package/dist/web/assets/AgentHand-D_RafKOM.js +1 -0
- package/dist/web/assets/AgentName-B5LKQ09q.js +1 -0
- package/dist/web/assets/AgentProfilePage-CioOdkBM.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +1 -0
- package/dist/web/assets/ConnectionsPage-Tr2rRVOZ.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +3 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQes4L1f.css +1 -0
- package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +1 -0
- package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-ccgscrPd.css +1 -0
- package/dist/web/assets/KnowledgePage-DGOcBwr8.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +4 -0
- package/dist/web/assets/NotFoundPage-fetSnp0N.js +1 -0
- package/dist/web/assets/OverviewPage-BD4yY2Xz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-BcxqmKXQ.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BWDJBESb.js +12 -0
- package/dist/web/assets/{ProjectAgentsPage-DNLNjYp-.css → ProjectAgentsPage-DWGkgIsr.css} +1 -1
- package/dist/web/assets/ProjectScopePicker-yodM7lF_.js +1 -0
- package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +1 -0
- package/dist/web/assets/ReviewPage-Cy0Rt4CO.js +1 -0
- package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +1 -0
- package/dist/web/assets/SettingsPage-CCZqZtnH.js +1 -0
- package/dist/web/assets/SettingsPage-DYz5bTwa.css +1 -0
- package/dist/web/assets/SharedJefaReportPage-C56kPciB.js +1 -0
- package/dist/web/assets/UiDialog-CvDxg3B1.js +1 -0
- package/dist/web/assets/UiDisclosure-D0iMTplF.js +1 -0
- package/dist/web/assets/UiSegmented-DIJW6wv3.js +1 -0
- package/dist/web/assets/UiSelect-CMR0QqF6.js +1 -0
- package/dist/web/assets/WritingTastePage-QgNzssfF.js +1 -0
- package/dist/web/assets/agent-pins-BcJ_qr8n.js +1 -0
- package/dist/web/assets/agent-work-summary-C-yW7x7A.js +1 -0
- package/dist/web/assets/{attention-store-BfxtL9EV.js → attention-store-B1DI8wmd.js} +1 -1
- package/dist/web/assets/{catalog-6SN9i3WR.js → catalog-DbXv0_bR.js} +1 -1
- package/dist/web/assets/client-CR-W5Wzp.js +1 -0
- package/dist/web/assets/console-D7GHOMEC.js +1 -0
- package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +1 -0
- package/dist/web/assets/{i18n-CCMR4gDS.js → i18n-CVFhT_k7.js} +3 -3
- package/dist/web/assets/{identity-C1eN-FSB.js → identity-CM6Dmy00.js} +1 -1
- package/dist/web/assets/index-CJ6Jmok9.css +1 -0
- package/dist/web/assets/index-CRa3GnYi.js +5 -0
- package/dist/web/assets/judgment-types-CJBEyJs1.js +1 -0
- package/dist/web/assets/model-WFhQZ3v_.js +1 -0
- package/dist/web/assets/{personal-profile-graph-BD1Y6TDS.js → personal-profile-graph-BwCh2M9G.js} +1 -1
- package/dist/web/assets/shared-report-CeIvrL9a.js +1 -0
- package/dist/web/assets/{agent-work-summary-LeW-Al_S.js → task-evidence-DTiPCs7r.js} +1 -1
- package/dist/web/assets/useAgentRoster-DCArHlxK.js +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-DlOiu8lP.js → useApi-CROJJdhE-BYhbjaZB.js} +1 -1
- package/dist/web/assets/vue-router-DaxrQwX3.js +1 -0
- package/dist/web/index.html +16 -12
- package/docs/agent-roundtable.md +17 -1
- package/docs/employee-agents.md +1 -1
- package/docs/judgment-and-autonomy.md +33 -0
- package/graph-provider/fuli_graph/ai_review.py +154 -0
- package/graph-provider/fuli_graph/common_knowledge.py +12 -0
- package/graph-provider/fuli_graph/graph_models.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +217 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +6 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +6 -0
- package/graph-provider/fuli_graph/knowledge_management.py +23 -0
- package/graph-provider/fuli_graph/knowledge_review.py +383 -4
- package/graph-provider/fuli_graph/knowledge_review_models.py +61 -2
- package/graph-provider/fuli_graph/personal_global_preferences.py +5 -1
- package/graph-provider/fuli_graph/preference_conflicts.py +91 -1
- package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
- package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
- package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +13 -0
- package/graph-provider/fuli_graph/project_agent_models.py +1 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +2 -0
- package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
- package/graph-provider/fuli_graph/project_knowledge.py +41 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_agent_verification.py +12 -5
- package/graph-provider/fuli_graph/store_knowledge.py +98 -58
- package/graph-provider/fuli_graph/store_project_agent_attention.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +26 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +5 -3
- package/graph-provider/fuli_graph/store_project_agents.py +31 -5
- package/graph-provider/fuli_graph/system_agents.py +23 -2
- package/graph-provider/fuli_graph/system_hr_identity.py +19 -5
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/src/agent-roundtable/service.js +34 -5
- package/src/agent-roundtable/wake.js +2 -1
- package/src/agent-tools/graph-definitions.js +2 -0
- package/src/agent-tools/graph-handlers.js +7 -0
- package/src/agent-tools/interface-catalog.js +6 -0
- package/src/agent-tools/judgment-definitions.js +12 -0
- package/src/agents/claude-code/wake.js +7 -0
- package/src/agents/codex/wake.js +7 -0
- package/src/agents/identity-receipt.js +20 -2
- package/src/employees/catalog/tonborg.json +20 -0
- package/src/employees/manifest.js +2 -2
- package/src/employees/recruitment.js +20 -1
- package/src/graphiti/agent-collaboration.js +8 -2
- package/src/graphiti/federated-application.js +11 -2
- package/src/graphiti/knowledge-review.js +7 -3
- package/src/graphiti/project-agent-mapping.js +4 -0
- package/src/graphiti/project-agent-task-entry.js +23 -5
- package/src/graphiti/project-agent-team-context.js +4 -1
- package/src/graphiti/provider-client.js +9 -0
- package/src/graphiti/task-entry-preferences.js +17 -4
- package/src/graphiti/task-entry-project-scope.js +67 -0
- package/src/http/api-router.js +2 -0
- package/src/judgment/acceptance.js +91 -0
- package/src/judgment/engine.js +168 -0
- package/src/judgment/http.js +24 -0
- package/src/judgment/model.js +24 -0
- package/src/judgment/routing.js +96 -0
- package/src/judgment/scheduler.js +25 -0
- package/src/judgment/service.js +66 -0
- package/src/judgment/store.js +123 -0
- package/src/mcp/session-id.js +1 -0
- package/src/mcp/tool-annotations.js +4 -2
- package/src/server.js +9 -2
- package/dist/web/assets/AboutPage-CKq4IRHd.js +0 -1
- package/dist/web/assets/AgentConversations-DfXiFvBN.js +0 -2
- package/dist/web/assets/AgentHand-BBM-sKh2.js +0 -1
- package/dist/web/assets/AgentName-C_6R80nP.js +0 -1
- package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +0 -1
- package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +0 -3
- package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +0 -1
- package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +0 -1
- package/dist/web/assets/KnowledgePage-kn8MqUCO.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +0 -4
- package/dist/web/assets/NotFoundPage-D-7jLz3o.js +0 -1
- package/dist/web/assets/OverviewPage-BHPc-XxY.js +0 -1
- package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +0 -12
- package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +0 -1
- package/dist/web/assets/ReviewPage-BZhvyDWc.js +0 -1
- package/dist/web/assets/RoundtablesPage-CVebTXQa.js +0 -1
- package/dist/web/assets/SettingsPage-1knvfOW6.js +0 -1
- package/dist/web/assets/SettingsPage-BmYkLul4.css +0 -1
- package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +0 -1
- package/dist/web/assets/UiDialog-Da1P10f4.js +0 -1
- package/dist/web/assets/UiDisclosure-8wE8YuE4.js +0 -1
- package/dist/web/assets/UiSegmented-BKZXc8QF.js +0 -1
- package/dist/web/assets/UiSelect-D5Xj5Ec4.js +0 -1
- package/dist/web/assets/WritingTastePage-B8-etOBn.js +0 -1
- package/dist/web/assets/client-CUTkF3t4.js +0 -1
- package/dist/web/assets/console-CG8tdtSs.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-CyZkLJg7.js +0 -1
- package/dist/web/assets/index-kPB0Cf8Q.css +0 -1
- package/dist/web/assets/index-sLDKI6R-.js +0 -5
- package/dist/web/assets/model-l-sAqtW_.js +0 -1
- package/dist/web/assets/shared-report-B46dxlCR.js +0 -1
- package/dist/web/assets/useAgentRoster-DdxoKDEE.js +0 -1
- package/dist/web/assets/vue-router-DjqEU4W2.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
|
-
from pydantic import Field, model_validator
|
|
4
|
+
from pydantic import Field, field_validator, model_validator
|
|
5
5
|
|
|
6
6
|
from .models import (
|
|
7
7
|
ConfirmationStatus,
|
|
@@ -11,6 +11,7 @@ from .models import (
|
|
|
11
11
|
PersonalProfileAspect,
|
|
12
12
|
PreferenceScope,
|
|
13
13
|
StrictModel,
|
|
14
|
+
SourceApplication,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
KnowledgeReviewScope = Literal[
|
|
@@ -23,7 +24,7 @@ KnowledgeReviewScope = Literal[
|
|
|
23
24
|
KnowledgeReviewStatus = Literal['active', 'paused', 'completed']
|
|
24
25
|
KnowledgeReviewDisposition = Literal['paused', 'completed']
|
|
25
26
|
KnowledgeReviewOutcome = Literal[
|
|
26
|
-
'confirmed', 'updated', 'invalidated', 'deferred', 'delegated_to_ai'
|
|
27
|
+
'confirmed', 'updated', 'invalidated', 'deferred', 'delegated_to_ai', 'ai_reviewed'
|
|
27
28
|
]
|
|
28
29
|
KnowledgeReviewReason = Literal[
|
|
29
30
|
'changed_since_last',
|
|
@@ -37,6 +38,7 @@ KnowledgeReviewReason = Literal[
|
|
|
37
38
|
class KnowledgeReviewStart(StrictModel):
|
|
38
39
|
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
39
40
|
scope: KnowledgeReviewScope
|
|
41
|
+
reviewer: Literal['human', 'tonborg'] = 'human'
|
|
40
42
|
personal_project_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
41
43
|
|
|
42
44
|
@model_validator(mode='after')
|
|
@@ -55,6 +57,7 @@ class KnowledgeReviewRun(StrictModel):
|
|
|
55
57
|
scope: KnowledgeReviewScope
|
|
56
58
|
personal_project_id: str | None = None
|
|
57
59
|
scope_key: str
|
|
60
|
+
reviewer: Literal['human', 'tonborg'] = 'human'
|
|
58
61
|
status: KnowledgeReviewStatus
|
|
59
62
|
previous_completed_at: datetime | None = None
|
|
60
63
|
review_cutoff_at: datetime
|
|
@@ -64,6 +67,25 @@ class KnowledgeReviewRun(StrictModel):
|
|
|
64
67
|
resumed: bool = False
|
|
65
68
|
|
|
66
69
|
|
|
70
|
+
class KnowledgeReviewAIAssessment(StrictModel):
|
|
71
|
+
"""The bounded, version-bound result returned by the Tonborg reviewer."""
|
|
72
|
+
|
|
73
|
+
outcome: Literal['approve', 'escalate']
|
|
74
|
+
summary: str = Field(min_length=1, max_length=2000)
|
|
75
|
+
evidence: list[str] = Field(min_length=1, max_length=8)
|
|
76
|
+
confidence: float = Field(ge=0, le=1)
|
|
77
|
+
client: SourceApplication | None = None
|
|
78
|
+
model: str | None = Field(default=None, min_length=1, max_length=256)
|
|
79
|
+
session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
80
|
+
|
|
81
|
+
@field_validator('evidence')
|
|
82
|
+
@classmethod
|
|
83
|
+
def validate_evidence(cls, value: list[str]) -> list[str]:
|
|
84
|
+
if any(not item or len(item) > 1000 for item in value):
|
|
85
|
+
raise ValueError('AI evidence entries must contain 1 to 1000 characters')
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
67
89
|
class KnowledgeReviewCandidate(StrictModel):
|
|
68
90
|
candidate_key: str
|
|
69
91
|
item_id: str
|
|
@@ -90,12 +112,18 @@ class KnowledgeReviewCandidate(StrictModel):
|
|
|
90
112
|
last_feedback_kind: KnowledgeFeedbackKind | None = None
|
|
91
113
|
distinct_session_count: int = Field(ge=0)
|
|
92
114
|
changed_at: datetime | None = None
|
|
115
|
+
ai_review_evidence_token: str = Field(
|
|
116
|
+
min_length=64,
|
|
117
|
+
max_length=64,
|
|
118
|
+
pattern=r'^[0-9a-f]{64}$',
|
|
119
|
+
)
|
|
93
120
|
|
|
94
121
|
|
|
95
122
|
class KnowledgeReviewCandidateRequest(StrictModel):
|
|
96
123
|
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
97
124
|
review_id: str = Field(min_length=1, max_length=256)
|
|
98
125
|
limit: int = Field(default=10, ge=1, le=50)
|
|
126
|
+
offset: int = Field(default=0, ge=0, le=1000000)
|
|
99
127
|
|
|
100
128
|
|
|
101
129
|
class KnowledgeReviewCandidatePage(StrictModel):
|
|
@@ -115,6 +143,28 @@ class KnowledgeReviewProgress(StrictModel):
|
|
|
115
143
|
)
|
|
116
144
|
outcome: KnowledgeReviewOutcome
|
|
117
145
|
note: str | None = Field(default=None, min_length=1, max_length=2000)
|
|
146
|
+
ai_review_evidence_token: str | None = Field(
|
|
147
|
+
default=None,
|
|
148
|
+
min_length=64,
|
|
149
|
+
max_length=64,
|
|
150
|
+
pattern=r'^[0-9a-f]{64}$',
|
|
151
|
+
)
|
|
152
|
+
ai_assessment: KnowledgeReviewAIAssessment | None = None
|
|
153
|
+
|
|
154
|
+
@model_validator(mode='after')
|
|
155
|
+
def validate_ai_review_fields(self):
|
|
156
|
+
has_token = self.ai_review_evidence_token is not None
|
|
157
|
+
has_assessment = self.ai_assessment is not None
|
|
158
|
+
if self.outcome == 'ai_reviewed':
|
|
159
|
+
if not has_token or not has_assessment:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
'ai_reviewed requires an evidence token and assessment'
|
|
162
|
+
)
|
|
163
|
+
elif has_token or has_assessment:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
'AI assessment fields are only valid for ai_reviewed outcomes'
|
|
166
|
+
)
|
|
167
|
+
return self
|
|
118
168
|
|
|
119
169
|
|
|
120
170
|
class KnowledgeReviewDecision(StrictModel):
|
|
@@ -125,6 +175,15 @@ class KnowledgeReviewDecision(StrictModel):
|
|
|
125
175
|
note: str | None = None
|
|
126
176
|
created_at: datetime
|
|
127
177
|
updated_at: datetime
|
|
178
|
+
ai_review_evidence_token: str | None = Field(
|
|
179
|
+
default=None,
|
|
180
|
+
min_length=64,
|
|
181
|
+
max_length=64,
|
|
182
|
+
pattern=r'^[0-9a-f]{64}$',
|
|
183
|
+
)
|
|
184
|
+
ai_assessment: KnowledgeReviewAIAssessment | None = None
|
|
185
|
+
ai_review_scope: KnowledgeReviewScope | None = None
|
|
186
|
+
ai_review_project_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
128
187
|
|
|
129
188
|
|
|
130
189
|
class KnowledgeReviewFinish(StrictModel):
|
|
@@ -1116,7 +1116,9 @@ OPTIONAL MATCH (old_assertion:Entity {
|
|
|
1116
1116
|
})
|
|
1117
1117
|
SET preview.used_at = $changed_at
|
|
1118
1118
|
FOREACH (_ IN CASE WHEN old_assertion IS NULL THEN [] ELSE [1] END |
|
|
1119
|
-
SET old_assertion.
|
|
1119
|
+
SET old_assertion.fuli_ai_review_lock_version =
|
|
1120
|
+
coalesce(old_assertion.fuli_ai_review_lock_version, 0) + 1,
|
|
1121
|
+
old_assertion.fuli_invalid_at = $changed_at,
|
|
1120
1122
|
old_assertion.expired_at = $changed_at
|
|
1121
1123
|
)
|
|
1122
1124
|
SET current.candidate_version = $candidate_version,
|
|
@@ -1176,6 +1178,8 @@ FOREACH (_ IN CASE WHEN $global_assertion_id IS NULL THEN [] ELSE [1] END |
|
|
|
1176
1178
|
assertion.fuli_distinct_task_count = 0,
|
|
1177
1179
|
assertion.fuli_usage_generation = 1,
|
|
1178
1180
|
assertion.fuli_invalid_at = null
|
|
1181
|
+
SET assertion.fuli_ai_review_lock_version =
|
|
1182
|
+
coalesce(assertion.fuli_ai_review_lock_version, 0) + 1
|
|
1179
1183
|
MERGE (episode:Episodic {uuid: $episode_id})
|
|
1180
1184
|
ON CREATE SET episode.group_id = $group_id,
|
|
1181
1185
|
episode.name = 'Personal-global preference human review',
|
|
@@ -19,6 +19,18 @@ from .models import (
|
|
|
19
19
|
from .provider_values import native_datetime as _native_datetime
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _item_match(item_kind: str, variable: str, id_parameter: str) -> str:
|
|
23
|
+
if item_kind == 'entity':
|
|
24
|
+
return (
|
|
25
|
+
f'MATCH ({variable}:Entity '
|
|
26
|
+
f'{{uuid: ${id_parameter}, group_id: $group_id}})'
|
|
27
|
+
)
|
|
28
|
+
return (
|
|
29
|
+
f'MATCH ()-[{variable}:RELATES_TO '
|
|
30
|
+
f'{{uuid: ${id_parameter}, group_id: $group_id}}]->()'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
async def defer_preference_conflict(
|
|
23
35
|
store,
|
|
24
36
|
actor: dict,
|
|
@@ -44,9 +56,42 @@ async def defer_preference_conflict(
|
|
|
44
56
|
)
|
|
45
57
|
_validate_pair_scope(left, right, request)
|
|
46
58
|
deferred_at = datetime.now(timezone.utc)
|
|
59
|
+
left_match = _item_match(
|
|
60
|
+
request.left_item_kind, 'left', 'left_item_id'
|
|
61
|
+
)
|
|
62
|
+
right_match = _item_match(
|
|
63
|
+
request.right_item_kind, 'right', 'right_item_id'
|
|
64
|
+
)
|
|
47
65
|
records, _, _ = await store.runtime.driver.execute_query(
|
|
48
66
|
'''
|
|
49
67
|
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
68
|
+
'''
|
|
69
|
+
+ left_match
|
|
70
|
+
+ '\n '
|
|
71
|
+
+ right_match
|
|
72
|
+
+ '''
|
|
73
|
+
WITH space,
|
|
74
|
+
CASE
|
|
75
|
+
WHEN $left_item_kind + ':' + left.uuid =
|
|
76
|
+
$right_item_kind + ':' + right.uuid
|
|
77
|
+
THEN [{kind: $left_item_kind, item: left}]
|
|
78
|
+
WHEN $left_item_kind + ':' + left.uuid <
|
|
79
|
+
$right_item_kind + ':' + right.uuid
|
|
80
|
+
THEN [
|
|
81
|
+
{kind: $left_item_kind, item: left},
|
|
82
|
+
{kind: $right_item_kind, item: right}
|
|
83
|
+
]
|
|
84
|
+
ELSE [
|
|
85
|
+
{kind: $right_item_kind, item: right},
|
|
86
|
+
{kind: $left_item_kind, item: left}
|
|
87
|
+
]
|
|
88
|
+
END AS lock_items
|
|
89
|
+
UNWIND lock_items AS lock_item
|
|
90
|
+
WITH space, lock_item
|
|
91
|
+
ORDER BY lock_item.kind + ':' + lock_item.item.uuid
|
|
92
|
+
SET lock_item.item.fuli_ai_review_lock_version =
|
|
93
|
+
coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
|
|
94
|
+
WITH space, collect(lock_item) AS _locked_items
|
|
50
95
|
MERGE (conflict:FuliPreferenceConflict {id: $conflict_id})
|
|
51
96
|
ON CREATE SET conflict.personal_space_id = $space_id,
|
|
52
97
|
conflict.preference_key = $preference_key,
|
|
@@ -70,6 +115,7 @@ async def defer_preference_conflict(
|
|
|
70
115
|
RETURN conflict
|
|
71
116
|
''',
|
|
72
117
|
space_id=space['id'],
|
|
118
|
+
group_id=space['group_id'],
|
|
73
119
|
conflict_id=request.conflict_id,
|
|
74
120
|
preference_key=request.preference_key,
|
|
75
121
|
preference_scope=request.preference_scope,
|
|
@@ -160,10 +206,12 @@ async def resolve_preference_conflict(
|
|
|
160
206
|
return await _complete(
|
|
161
207
|
store,
|
|
162
208
|
space['id'],
|
|
209
|
+
space['group_id'],
|
|
163
210
|
conflict_id,
|
|
164
211
|
request.resolution,
|
|
165
212
|
request.reason,
|
|
166
213
|
request.operation_actor,
|
|
214
|
+
conflict,
|
|
167
215
|
)
|
|
168
216
|
|
|
169
217
|
|
|
@@ -179,14 +227,16 @@ async def complete_preference_conflict(
|
|
|
179
227
|
request.personal_space_id,
|
|
180
228
|
'maintainer',
|
|
181
229
|
)
|
|
182
|
-
await _read_conflict(store, space['id'], conflict_id)
|
|
230
|
+
conflict = await _read_conflict(store, space['id'], conflict_id)
|
|
183
231
|
return await _complete(
|
|
184
232
|
store,
|
|
185
233
|
space['id'],
|
|
234
|
+
space['group_id'],
|
|
186
235
|
conflict_id,
|
|
187
236
|
request.resolution,
|
|
188
237
|
request.reason,
|
|
189
238
|
request.operation_actor,
|
|
239
|
+
conflict,
|
|
190
240
|
)
|
|
191
241
|
|
|
192
242
|
|
|
@@ -305,17 +355,52 @@ async def _split_scope(store, actor, space, conflict, left, right, request):
|
|
|
305
355
|
async def _complete(
|
|
306
356
|
store,
|
|
307
357
|
space_id,
|
|
358
|
+
group_id,
|
|
308
359
|
conflict_id,
|
|
309
360
|
resolution,
|
|
310
361
|
reason,
|
|
311
362
|
resolved_by,
|
|
363
|
+
conflict: PreferenceConflictRecord,
|
|
312
364
|
):
|
|
313
365
|
resolved_at = datetime.now(timezone.utc)
|
|
366
|
+
left_match = _item_match(
|
|
367
|
+
conflict.left_item_kind, 'left', 'left_item_id'
|
|
368
|
+
)
|
|
369
|
+
right_match = _item_match(
|
|
370
|
+
conflict.right_item_kind, 'right', 'right_item_id'
|
|
371
|
+
)
|
|
314
372
|
records, _, _ = await store.runtime.driver.execute_query(
|
|
315
373
|
'''
|
|
316
374
|
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
317
375
|
[:HAS_PREFERENCE_CONFLICT]->
|
|
318
376
|
(conflict:FuliPreferenceConflict {id: $conflict_id})
|
|
377
|
+
'''
|
|
378
|
+
+ left_match
|
|
379
|
+
+ '\n '
|
|
380
|
+
+ right_match
|
|
381
|
+
+ '''
|
|
382
|
+
WITH conflict,
|
|
383
|
+
CASE
|
|
384
|
+
WHEN $left_item_kind + ':' + left.uuid =
|
|
385
|
+
$right_item_kind + ':' + right.uuid
|
|
386
|
+
THEN [{kind: $left_item_kind, item: left}]
|
|
387
|
+
WHEN $left_item_kind + ':' + left.uuid <
|
|
388
|
+
$right_item_kind + ':' + right.uuid
|
|
389
|
+
THEN [
|
|
390
|
+
{kind: $left_item_kind, item: left},
|
|
391
|
+
{kind: $right_item_kind, item: right}
|
|
392
|
+
]
|
|
393
|
+
ELSE [
|
|
394
|
+
{kind: $right_item_kind, item: right},
|
|
395
|
+
{kind: $left_item_kind, item: left}
|
|
396
|
+
]
|
|
397
|
+
END AS lock_items
|
|
398
|
+
UNWIND lock_items AS lock_item
|
|
399
|
+
WITH conflict, lock_item
|
|
400
|
+
ORDER BY lock_item.kind + ':' + lock_item.item.uuid
|
|
401
|
+
SET lock_item.item.fuli_ai_review_lock_version =
|
|
402
|
+
coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
|
|
403
|
+
WITH conflict, collect(lock_item) AS _locked_items
|
|
319
404
|
SET conflict.status = 'resolved',
|
|
320
405
|
conflict.resolution = $resolution,
|
|
321
406
|
conflict.resolved_by = $resolved_by,
|
|
@@ -325,7 +410,12 @@ async def _complete(
|
|
|
325
410
|
RETURN conflict
|
|
326
411
|
''',
|
|
327
412
|
space_id=space_id,
|
|
413
|
+
group_id=group_id,
|
|
328
414
|
conflict_id=conflict_id,
|
|
415
|
+
left_item_id=conflict.left_item_id,
|
|
416
|
+
left_item_kind=conflict.left_item_kind,
|
|
417
|
+
right_item_id=conflict.right_item_id,
|
|
418
|
+
right_item_kind=conflict.right_item_kind,
|
|
329
419
|
resolution=resolution,
|
|
330
420
|
resolved_by=resolved_by,
|
|
331
421
|
reason=reason,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Task-entry selection and optional invocation claim; never spawns a worker."""
|
|
2
2
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
@@ -13,6 +13,8 @@ class ProjectAgentContextRequest(StrictModel):
|
|
|
13
13
|
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
14
14
|
personal_project_id: str = Field(min_length=1, max_length=128)
|
|
15
15
|
agent_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
16
|
+
delegation_token: str | None = Field(default=None, min_length=32, max_length=128)
|
|
17
|
+
delegation_session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
16
18
|
session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
17
19
|
turn_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
18
20
|
work_kind: str = Field(default='project_context', min_length=1, max_length=128)
|
|
@@ -26,4 +28,6 @@ class ProjectAgentContextResolution(StrictModel):
|
|
|
26
28
|
match_basis: list[str] = Field(default_factory=list)
|
|
27
29
|
candidate_count: int = 0
|
|
28
30
|
agent: ProjectAgentRecord | None = None
|
|
31
|
+
requested_agent_id: str | None = None
|
|
32
|
+
reporting_lead_agent_id: str | None = None
|
|
29
33
|
worker_started: Literal[False] = False
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Short-lived, session-bound capabilities for a real roundtable invocation."""
|
|
2
|
+
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from secrets import token_urlsafe
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from .models import SourceApplication, StrictModel
|
|
10
|
+
from .personal_project_access import authorize_personal_project
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentDelegationIssue(StrictModel):
|
|
14
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
15
|
+
task_context_token: str = Field(min_length=1, max_length=256)
|
|
16
|
+
source_application: SourceApplication
|
|
17
|
+
agent_id: str = Field(min_length=1, max_length=128)
|
|
18
|
+
target_application: SourceApplication
|
|
19
|
+
target_session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
20
|
+
lifetime_seconds: int = Field(default=300, ge=30, le=900)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentDelegationCheck(StrictModel):
|
|
24
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
25
|
+
token: str = Field(min_length=32, max_length=128)
|
|
26
|
+
session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def token_hash(token):
|
|
30
|
+
return sha256(token.encode()).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StoreAgentDelegations:
|
|
34
|
+
async def issue_agent_delegation(self, actor, request):
|
|
35
|
+
self._require_personal()
|
|
36
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
37
|
+
context = await self.get_task_context(actor, request.personal_space_id,
|
|
38
|
+
request.task_context_token, request.source_application)
|
|
39
|
+
project_id = context.get('personal_project_id')
|
|
40
|
+
if not project_id or (context.get('checkpoint') or {}).get('phase') == 'complete':
|
|
41
|
+
raise HTTPException(409, 'Delegation requires an active project task')
|
|
42
|
+
await authorize_personal_project(self, actor, space, project_id)
|
|
43
|
+
policy = await self.get_project_agent_coordination_policy(actor, request.personal_space_id, project_id)
|
|
44
|
+
lead = policy.team_lead_agent_id
|
|
45
|
+
sender = context.get('project_agent_id')
|
|
46
|
+
if not lead or not sender or (sender != lead and request.agent_id != lead):
|
|
47
|
+
raise HTTPException(403, 'Members must communicate through the project lead')
|
|
48
|
+
target = await self.get_project_agent(actor, request.personal_space_id, project_id, request.agent_id)
|
|
49
|
+
if (target.profile.status != 'active'
|
|
50
|
+
or request.target_application not in target.profile.allowed_clients):
|
|
51
|
+
raise HTTPException(403, 'The receiving Agent is unavailable in this client')
|
|
52
|
+
token = token_urlsafe(32)
|
|
53
|
+
# Only hashes are stored. A crash leaves a bounded lifetime, never a
|
|
54
|
+
# reusable authority derived from a public message or thread identifier.
|
|
55
|
+
await self.runtime.driver.execute_query(
|
|
56
|
+
'''MATCH (old:FuliAgentDelegation {space_id: $space_id})
|
|
57
|
+
WHERE old.expires_at <= datetime() DELETE old''', space_id=request.personal_space_id)
|
|
58
|
+
await self.runtime.driver.execute_query(
|
|
59
|
+
'''CREATE (:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id,
|
|
60
|
+
project_id: $project_id, agent_id: $agent_id, source_application: $application,
|
|
61
|
+
session_id: $session_id, sender_token: $sender_token, lead_agent_id: $lead,
|
|
62
|
+
expires_at: datetime() + duration({seconds: $lifetime}), redeemed: false})''',
|
|
63
|
+
token_hash=token_hash(token), space_id=request.personal_space_id, project_id=project_id,
|
|
64
|
+
agent_id=request.agent_id, application=request.target_application,
|
|
65
|
+
session_id=request.target_session_id, sender_token=request.task_context_token,
|
|
66
|
+
lead=lead, lifetime=request.lifetime_seconds)
|
|
67
|
+
return {'token': token}
|
|
68
|
+
|
|
69
|
+
async def redeem_agent_delegation(self, request, lead):
|
|
70
|
+
if not request.delegation_token or not request.delegation_session_id or not request.agent_id:
|
|
71
|
+
return False
|
|
72
|
+
# Lock before reading the session binding: two different sessions cannot
|
|
73
|
+
# both claim a fresh capability. Same-session hook/MCP retries are safe.
|
|
74
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
75
|
+
'''MATCH (:FuliSpace {id: $space_id})-[:CONTAINS_PROJECT]->
|
|
76
|
+
(:FuliPersonalProject {project_id: $project_id})-
|
|
77
|
+
[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy:FuliProjectAgentCoordinationPolicy)
|
|
78
|
+
SET policy._fuli_write_lock = true
|
|
79
|
+
REMOVE policy._fuli_write_lock
|
|
80
|
+
WITH policy WHERE policy.team_lead_agent_id = $lead
|
|
81
|
+
MATCH (grant:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id,
|
|
82
|
+
project_id: $project_id, agent_id: $agent_id, source_application: $application,
|
|
83
|
+
lead_agent_id: $lead})
|
|
84
|
+
SET grant.write_serial = coalesce(grant.write_serial, 0) + 1
|
|
85
|
+
WITH grant
|
|
86
|
+
WHERE grant.expires_at > datetime()
|
|
87
|
+
AND (grant.session_id IS NULL OR grant.session_id = $session_id)
|
|
88
|
+
MATCH (session:FuliTaskContextSession)-[:HAS_CONTEXT]->(task:FuliTaskContext)
|
|
89
|
+
WHERE task.token = grant.sender_token AND session.current_token = task.token
|
|
90
|
+
AND NOT task.completed AND task.personal_space_id = grant.space_id
|
|
91
|
+
AND task.personal_project_id = grant.project_id
|
|
92
|
+
AND (task.project_agent_id = grant.lead_agent_id OR grant.agent_id = grant.lead_agent_id)
|
|
93
|
+
SET grant.session_id = $session_id, grant.redeemed = true
|
|
94
|
+
RETURN grant.token_hash AS token_hash''',
|
|
95
|
+
token_hash=token_hash(request.delegation_token), space_id=request.personal_space_id,
|
|
96
|
+
project_id=request.personal_project_id, agent_id=request.agent_id,
|
|
97
|
+
application=request.source_application, lead=lead, session_id=request.delegation_session_id)
|
|
98
|
+
return bool(rows)
|
|
99
|
+
|
|
100
|
+
async def verify_agent_delegation(self, actor, request):
|
|
101
|
+
self._require_personal()
|
|
102
|
+
await self.authorize(actor, request.personal_space_id, 'reader')
|
|
103
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
104
|
+
'''MATCH (grant:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id,
|
|
105
|
+
session_id: $session_id, redeemed: true})
|
|
106
|
+
WHERE grant.expires_at > datetime()
|
|
107
|
+
RETURN grant.token_hash AS token_hash''',
|
|
108
|
+
token_hash=token_hash(request.token), space_id=request.personal_space_id,
|
|
109
|
+
session_id=request.session_id)
|
|
110
|
+
return {'redeemed': bool(rows)}
|
|
111
|
+
|
|
112
|
+
async def revoke_agent_delegation(self, actor, request):
|
|
113
|
+
self._require_personal()
|
|
114
|
+
await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
115
|
+
await self.runtime.driver.execute_query(
|
|
116
|
+
'''MATCH (grant:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id})
|
|
117
|
+
DELETE grant''', token_hash=token_hash(request.token), space_id=request.personal_space_id)
|
|
118
|
+
return {'revoked': True}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Space-wide FLA employee numbers, assigned in the identity write transaction.
|
|
2
|
+
|
|
3
|
+
Both identity creation paths take the space lock before touching an Agent.
|
|
4
|
+
The high-water mark survives archives and identity aliases; numbers are never
|
|
5
|
+
reused. Existing identities are backfilled in creation order on upgrade.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
EMPLOYEE_NUMBER_LOCK = '''
|
|
9
|
+
SET space._employee_number_lock = true
|
|
10
|
+
REMOVE space._employee_number_lock
|
|
11
|
+
WITH space
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
# Keep outer variables (notably ``agent``) intact. The subquery always returns
|
|
15
|
+
# one row, including when the space has no missing numbers.
|
|
16
|
+
ASSIGN_EMPLOYEE_NUMBERS = '''
|
|
17
|
+
WITH space, agent
|
|
18
|
+
CALL {
|
|
19
|
+
WITH space
|
|
20
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(employee:FuliProjectAgent)
|
|
21
|
+
WITH DISTINCT space, employee
|
|
22
|
+
ORDER BY employee.created_at, employee.agent_id
|
|
23
|
+
WITH space, collect(employee) AS employees
|
|
24
|
+
WITH space, employees,
|
|
25
|
+
[employee IN employees WHERE employee.employee_number IS NULL] AS missing,
|
|
26
|
+
reduce(high = coalesce(space.fla_employee_sequence, 0), employee IN employees |
|
|
27
|
+
CASE WHEN coalesce(employee.employee_number, 0) > high
|
|
28
|
+
THEN employee.employee_number ELSE high END) AS high
|
|
29
|
+
SET space.fla_employee_sequence = high + size(missing)
|
|
30
|
+
FOREACH (index IN range(0, size(missing) - 1) |
|
|
31
|
+
FOREACH (employee IN [missing[index]] |
|
|
32
|
+
SET employee.employee_number = high + index + 1))
|
|
33
|
+
FOREACH (employee IN [item IN employees WHERE item.employee_number_space_id IS NULL] |
|
|
34
|
+
SET employee.employee_number_space_id = space.id)
|
|
35
|
+
RETURN space.fla_employee_sequence AS employee_sequence
|
|
36
|
+
}
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def employee_number_label(number: int | None) -> str | None:
|
|
41
|
+
return f'{number:06d}' if number is not None else None
|
|
@@ -15,6 +15,7 @@ from .project_agent_context_models import (
|
|
|
15
15
|
from .agent_conversation_routes import register_agent_conversation_routes
|
|
16
16
|
from .default_project_lead import DefaultProjectLeadRequest
|
|
17
17
|
from .task_context_routes import register_task_context_routes
|
|
18
|
+
from .project_agent_delegations import AgentDelegationIssue, AgentDelegationCheck
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def register_project_agent_memory_routes(application, store, Actor):
|
|
@@ -26,6 +27,18 @@ def register_project_agent_memory_routes(application, store, Actor):
|
|
|
26
27
|
async def resolve_context(request: ProjectAgentContextRequest, actor: Actor):
|
|
27
28
|
return await store.resolve_project_agent_context(actor, request)
|
|
28
29
|
|
|
30
|
+
@application.post('/v1/project-agent-context/delegations')
|
|
31
|
+
async def issue_delegation(request: AgentDelegationIssue, actor: Actor):
|
|
32
|
+
return await store.issue_agent_delegation(actor, request)
|
|
33
|
+
|
|
34
|
+
@application.post('/v1/project-agent-context/delegations/verify')
|
|
35
|
+
async def verify_delegation(request: AgentDelegationCheck, actor: Actor):
|
|
36
|
+
return await store.verify_agent_delegation(actor, request)
|
|
37
|
+
|
|
38
|
+
@application.post('/v1/project-agent-context/delegations/revoke')
|
|
39
|
+
async def revoke_delegation(request: AgentDelegationCheck, actor: Actor):
|
|
40
|
+
return await store.revoke_agent_delegation(actor, request)
|
|
41
|
+
|
|
29
42
|
@application.post(
|
|
30
43
|
'/v1/project-agent-context/default-lead', response_model=ProjectAgentContextResolution,
|
|
31
44
|
)
|
|
@@ -277,6 +277,7 @@ class ProjectAgentAssignmentReplaceResult(StrictModel):
|
|
|
277
277
|
|
|
278
278
|
class ProjectAgentRecord(StrictModel):
|
|
279
279
|
agent_id: str
|
|
280
|
+
employee_number: str | None = None
|
|
280
281
|
legacy_agent_ids: list[str] = Field(default_factory=list)
|
|
281
282
|
personal_space_id: str
|
|
282
283
|
personal_project_id: str | None = None
|
|
@@ -494,6 +494,8 @@ class ProjectAgentTaskRecord(StrictModel):
|
|
|
494
494
|
|
|
495
495
|
class ProjectAgentTaskActivityCreate(StrictModel):
|
|
496
496
|
artifact_revision: str | None = Field(default=None, min_length=1, max_length=160)
|
|
497
|
+
expected_verification_attempt_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
498
|
+
judgment_task_context_token: str | None = Field(default=None, min_length=1, max_length=256)
|
|
497
499
|
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
498
500
|
personal_project_id: str = Field(min_length=1, max_length=128)
|
|
499
501
|
task_id: str = Field(min_length=1, max_length=128)
|
|
@@ -33,8 +33,8 @@ async def enroll_recruited_team_member(store, recruitment, profile):
|
|
|
33
33
|
policy.team_lead_agent_id IS NULL AS needs_lead,
|
|
34
34
|
coalesce(policy.team_lead_agent_id = agent.agent_id, false)
|
|
35
35
|
OR agent.agent_id IN members AS already_member
|
|
36
|
-
WITH *,
|
|
37
|
-
AND (needs_lead OR size(members) < 32) AS changed
|
|
36
|
+
WITH *, NOT already_member
|
|
37
|
+
AND (needs_lead OR (enabled AND size(members) < 32)) AS changed
|
|
38
38
|
FOREACH (_ IN CASE WHEN changed THEN [1] ELSE [] END |
|
|
39
39
|
SET policy.team_lead_agent_id = CASE WHEN needs_lead
|
|
40
40
|
THEN agent.agent_id ELSE policy.team_lead_agent_id END,
|
|
@@ -578,11 +578,25 @@ async def _upsert_reference(
|
|
|
578
578
|
space['id'], 'knowledge-reference', item_kind, item_id, project_id
|
|
579
579
|
)
|
|
580
580
|
changed_at = datetime.now(timezone.utc)
|
|
581
|
+
item_match = (
|
|
582
|
+
'MATCH (item:Entity {uuid: $item_id, group_id: $group_id})'
|
|
583
|
+
if item_kind == 'entity'
|
|
584
|
+
else (
|
|
585
|
+
'MATCH ()-[item:RELATES_TO '
|
|
586
|
+
'{uuid: $item_id, group_id: $group_id}]->()'
|
|
587
|
+
)
|
|
588
|
+
)
|
|
581
589
|
records, _, _ = await store.runtime.driver.execute_query(
|
|
582
590
|
'''
|
|
583
591
|
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
584
592
|
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
585
593
|
(target:FuliPersonalProject {project_id: $project_id})
|
|
594
|
+
'''
|
|
595
|
+
+ item_match
|
|
596
|
+
+ '''
|
|
597
|
+
SET item.fuli_ai_review_lock_version =
|
|
598
|
+
coalesce(item.fuli_ai_review_lock_version, 0) + 1
|
|
599
|
+
WITH space, target
|
|
586
600
|
MERGE (reference:FuliKnowledgeProjectReference {
|
|
587
601
|
space_id: $space_id,
|
|
588
602
|
item_kind: $item_kind,
|
|
@@ -604,6 +618,7 @@ async def _upsert_reference(
|
|
|
604
618
|
space_id=space['id'],
|
|
605
619
|
item_kind=item_kind,
|
|
606
620
|
item_id=item_id,
|
|
621
|
+
group_id=space['group_id'],
|
|
607
622
|
project_id=project_id,
|
|
608
623
|
reference_id=reference_id,
|
|
609
624
|
source_project_id=source_project_id,
|
|
@@ -635,6 +650,31 @@ async def _upsert_conflict(
|
|
|
635
650
|
records, _, _ = await store.runtime.driver.execute_query(
|
|
636
651
|
'''
|
|
637
652
|
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
653
|
+
MATCH (item:Entity {uuid: $item_id, group_id: $group_id})
|
|
654
|
+
MATCH (target_item:Entity {
|
|
655
|
+
uuid: $target_item_id,
|
|
656
|
+
group_id: $group_id
|
|
657
|
+
})
|
|
658
|
+
WITH space,
|
|
659
|
+
CASE
|
|
660
|
+
WHEN item.uuid = target_item.uuid
|
|
661
|
+
THEN [{kind: 'entity', item: item}]
|
|
662
|
+
WHEN item.uuid < target_item.uuid
|
|
663
|
+
THEN [
|
|
664
|
+
{kind: 'entity', item: item},
|
|
665
|
+
{kind: 'entity', item: target_item}
|
|
666
|
+
]
|
|
667
|
+
ELSE [
|
|
668
|
+
{kind: 'entity', item: target_item},
|
|
669
|
+
{kind: 'entity', item: item}
|
|
670
|
+
]
|
|
671
|
+
END AS lock_items
|
|
672
|
+
UNWIND lock_items AS lock_item
|
|
673
|
+
WITH space, lock_item
|
|
674
|
+
ORDER BY lock_item.kind + ':' + lock_item.item.uuid
|
|
675
|
+
SET lock_item.item.fuli_ai_review_lock_version =
|
|
676
|
+
coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
|
|
677
|
+
WITH space, collect(lock_item) AS _locked_items
|
|
638
678
|
MERGE (conflict:FuliKnowledgeConflict {id: $conflict_id})
|
|
639
679
|
ON CREATE SET conflict.created_at = $changed_at
|
|
640
680
|
SET conflict.space_id = $space_id,
|
|
@@ -651,6 +691,7 @@ async def _upsert_conflict(
|
|
|
651
691
|
RETURN conflict
|
|
652
692
|
''',
|
|
653
693
|
space_id=space['id'],
|
|
694
|
+
group_id=space['group_id'],
|
|
654
695
|
conflict_id=conflict_id,
|
|
655
696
|
item_id=item_id,
|
|
656
697
|
target_item_id=target_item_id,
|
|
@@ -226,6 +226,10 @@ class GraphitiRuntime:
|
|
|
226
226
|
'CREATE CONSTRAINT fuli_workflow_review_preview_token IF NOT EXISTS '
|
|
227
227
|
'FOR (n:FuliWorkflowReviewPreview) REQUIRE n.token_hash IS UNIQUE'
|
|
228
228
|
)
|
|
229
|
+
await self.driver.execute_query(
|
|
230
|
+
'CREATE CONSTRAINT fuli_agent_delegation_token IF NOT EXISTS '
|
|
231
|
+
'FOR (n:FuliAgentDelegation) REQUIRE n.token_hash IS UNIQUE'
|
|
232
|
+
)
|
|
229
233
|
await self.driver.execute_query(
|
|
230
234
|
'CREATE CONSTRAINT fuli_workflow_rule_id IF NOT EXISTS '
|
|
231
235
|
'FOR (n:FuliWorkflowRule) REQUIRE n.id IS UNIQUE'
|
|
@@ -285,6 +289,9 @@ class GraphitiRuntime:
|
|
|
285
289
|
'FOR (n:FuliConversationPolicy) REQUIRE n.id IS UNIQUE',
|
|
286
290
|
'CREATE CONSTRAINT fuli_project_agent_id IF NOT EXISTS '
|
|
287
291
|
'FOR (n:FuliProjectAgent) REQUIRE n.id IS UNIQUE',
|
|
292
|
+
'CREATE CONSTRAINT fuli_project_agent_employee_number IF NOT EXISTS '
|
|
293
|
+
'FOR (n:FuliProjectAgent) '
|
|
294
|
+
'REQUIRE (n.employee_number_space_id, n.employee_number) IS UNIQUE',
|
|
288
295
|
'CREATE CONSTRAINT fuli_agent_attention_id IF NOT EXISTS '
|
|
289
296
|
'FOR (n:FuliAgentAttention) REQUIRE n.id IS UNIQUE',
|
|
290
297
|
'CREATE CONSTRAINT fuli_project_agent_assignment_id IF NOT EXISTS '
|
|
@@ -649,6 +656,10 @@ class GraphitiRuntime:
|
|
|
649
656
|
OR item.fuli_usage_generation IS NULL
|
|
650
657
|
OR item.fuli_inheritance_mode IS NULL
|
|
651
658
|
OR item.fuli_inherited_project_ids IS NULL
|
|
659
|
+
WITH item
|
|
660
|
+
ORDER BY 'entity:' + item.uuid
|
|
661
|
+
SET item.fuli_ai_review_lock_version =
|
|
662
|
+
coalesce(item.fuli_ai_review_lock_version, 0) + 1
|
|
652
663
|
SET item.fuli_utility_score =
|
|
653
664
|
coalesce(item.fuli_utility_score, 0.0),
|
|
654
665
|
item.fuli_confidence_score = coalesce(
|
|
@@ -681,6 +692,10 @@ class GraphitiRuntime:
|
|
|
681
692
|
OR item.fuli_usage_generation IS NULL
|
|
682
693
|
OR item.fuli_inheritance_mode IS NULL
|
|
683
694
|
OR item.fuli_inherited_project_ids IS NULL
|
|
695
|
+
WITH item
|
|
696
|
+
ORDER BY 'relationship:' + item.uuid
|
|
697
|
+
SET item.fuli_ai_review_lock_version =
|
|
698
|
+
coalesce(item.fuli_ai_review_lock_version, 0) + 1
|
|
684
699
|
SET item.fuli_utility_score =
|
|
685
700
|
coalesce(item.fuli_utility_score, 0.0),
|
|
686
701
|
item.fuli_confidence_score = coalesce(
|