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.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,290 @@
1
+ from collections import defaultdict
2
+
3
+ from fastapi import HTTPException
4
+
5
+ from .models import (
6
+ CollaborationContextResult,
7
+ CollaborationPreferenceConflict,
8
+ CollaborationPreferenceItem,
9
+ ConfirmationBasis,
10
+ _validate_confirmation_state,
11
+ )
12
+ from .personal_project_access import authorize_personal_project
13
+ from .provider_values import json_object, native_datetime as _native_datetime
14
+
15
+
16
+ async def read_collaboration_context(
17
+ store,
18
+ actor: dict,
19
+ space_id: str,
20
+ personal_project_id: str | None = None,
21
+ limit: int = 100,
22
+ ) -> CollaborationContextResult:
23
+ space = await store.authorize(actor, space_id, 'reader')
24
+ if space['kind'] != 'personal':
25
+ raise HTTPException(
26
+ status_code=422,
27
+ detail='collaboration preferences require a personal space',
28
+ )
29
+ if personal_project_id:
30
+ await authorize_personal_project(
31
+ store,
32
+ actor,
33
+ space,
34
+ personal_project_id,
35
+ )
36
+
37
+ candidate_limit = min(max(limit * 3, 100), store.settings.graph_limit)
38
+ node_records, _, _ = await store.runtime.driver.execute_query(
39
+ _node_query(),
40
+ group_id=space['group_id'],
41
+ project_id=personal_project_id,
42
+ limit=candidate_limit + 1,
43
+ routing_='r',
44
+ )
45
+ edge_records, _, _ = await store.runtime.driver.execute_query(
46
+ _edge_query(),
47
+ group_id=space['group_id'],
48
+ project_id=personal_project_id,
49
+ limit=candidate_limit + 1,
50
+ routing_='r',
51
+ )
52
+ truncated = (
53
+ len(node_records) > candidate_limit
54
+ or len(edge_records) > candidate_limit
55
+ )
56
+ nodes = [
57
+ item
58
+ for record in node_records[:candidate_limit]
59
+ if (item := _node_item(record)) is not None
60
+ ]
61
+ edge_pairs = [
62
+ (record, item)
63
+ for record in edge_records[:candidate_limit]
64
+ if (item := _edge_item(record)) is not None
65
+ ]
66
+ edges = [item for _, item in edge_pairs]
67
+ edge_endpoint_ids = {
68
+ endpoint_id
69
+ for record, _ in edge_pairs
70
+ for endpoint_id in (record.get('source_id'), record.get('target_id'))
71
+ }
72
+ items = [
73
+ *edges,
74
+ *(item for item in nodes if item.id not in edge_endpoint_ids),
75
+ ]
76
+ items.sort(key=_sort_key)
77
+ if len(items) > limit:
78
+ truncated = True
79
+ items = items[:limit]
80
+
81
+ global_preferences = [
82
+ item for item in items if item.preference_scope == 'global'
83
+ ]
84
+ project_preferences = [
85
+ item for item in items if item.preference_scope == 'project'
86
+ ]
87
+ global_effective, global_conflicts = _conflict_free(global_preferences)
88
+ project_effective, project_conflicts = _conflict_free(project_preferences)
89
+ project_keys = {item.preference_key for item in project_preferences}
90
+ overridden_global_ids = [
91
+ item.id
92
+ for item in global_effective
93
+ if item.preference_key in project_keys
94
+ ]
95
+ effective_preferences = [
96
+ item
97
+ for item in global_effective
98
+ if item.preference_key not in project_keys
99
+ ] + project_effective
100
+
101
+ return CollaborationContextResult(
102
+ personal_space_id=space_id,
103
+ personal_project_id=personal_project_id,
104
+ global_preferences=global_preferences,
105
+ project_preferences=project_preferences,
106
+ effective_preferences=effective_preferences,
107
+ conflicts=[*global_conflicts, *project_conflicts],
108
+ overridden_global_ids=overridden_global_ids,
109
+ truncated=truncated,
110
+ )
111
+
112
+
113
+ def _node_query() -> str:
114
+ return '''
115
+ MATCH (node:Entity {group_id: $group_id})
116
+ WHERE node.fuli_profile_aspect IS NOT NULL
117
+ AND node.fuli_invalid_at IS NULL
118
+ AND node.fuli_confirmation_status IN ['confirmed', 'agent_confirmed']
119
+ AND (
120
+ coalesce(node.fuli_preference_scope, 'global') = 'global'
121
+ OR (
122
+ node.fuli_preference_scope = 'project'
123
+ AND node.fuli_preference_project_id = $project_id
124
+ )
125
+ )
126
+ RETURN node.uuid AS id,
127
+ coalesce(node.fuli_key, node.uuid) AS key,
128
+ node.name AS title,
129
+ coalesce(node.summary, node.name) AS instruction,
130
+ node.fuli_profile_aspect AS profile_aspect,
131
+ coalesce(node.fuli_preference_scope, 'global') AS preference_scope,
132
+ node.fuli_preference_project_id AS preference_project_id,
133
+ node.fuli_attributes_json AS attributes_json,
134
+ node.fuli_confirmation_basis_json AS confirmation_basis_json,
135
+ node.fuli_confirmation_status AS confirmation_status,
136
+ node.created_at AS created_at
137
+ ORDER BY node.created_at DESC
138
+ LIMIT $limit
139
+ '''
140
+
141
+
142
+ def _edge_query() -> str:
143
+ return '''
144
+ MATCH (source:Entity)-[edge:RELATES_TO {group_id: $group_id}]->
145
+ (target:Entity)
146
+ WHERE edge.fuli_profile_aspect IS NOT NULL
147
+ AND edge.invalid_at IS NULL
148
+ AND edge.fuli_confirmation_status IN ['confirmed', 'agent_confirmed']
149
+ AND (
150
+ coalesce(edge.fuli_preference_scope, 'global') = 'global'
151
+ OR (
152
+ edge.fuli_preference_scope = 'project'
153
+ AND edge.fuli_preference_project_id = $project_id
154
+ )
155
+ )
156
+ RETURN edge.uuid AS id,
157
+ coalesce(edge.fuli_key, edge.uuid) AS key,
158
+ source.uuid AS source_id,
159
+ target.uuid AS target_id,
160
+ source.name AS source_name,
161
+ target.name AS target_name,
162
+ edge.fact AS instruction,
163
+ edge.fuli_profile_aspect AS profile_aspect,
164
+ coalesce(edge.fuli_preference_scope, 'global') AS preference_scope,
165
+ edge.fuli_preference_project_id AS preference_project_id,
166
+ edge.fuli_attributes_json AS attributes_json,
167
+ edge.fuli_confirmation_basis_json AS confirmation_basis_json,
168
+ edge.fuli_confirmation_status AS confirmation_status,
169
+ edge.created_at AS created_at
170
+ ORDER BY edge.created_at DESC
171
+ LIMIT $limit
172
+ '''
173
+
174
+
175
+ def _node_item(record) -> CollaborationPreferenceItem | None:
176
+ return _preference_item(
177
+ record,
178
+ item_kind='entity',
179
+ title=record.get('title') or record.get('key') or record.get('id'),
180
+ instruction=record.get('instruction') or record.get('title') or '',
181
+ )
182
+
183
+
184
+ def _edge_item(record) -> CollaborationPreferenceItem | None:
185
+ source = record.get('source_name') or '偏好'
186
+ target = record.get('target_name') or '适用场景'
187
+ return _preference_item(
188
+ record,
189
+ item_kind='relationship',
190
+ title=f'{source} → {target}',
191
+ instruction=record.get('instruction') or '',
192
+ )
193
+
194
+
195
+ def _preference_item(
196
+ record,
197
+ *,
198
+ item_kind: str,
199
+ title: str,
200
+ instruction: str,
201
+ ) -> CollaborationPreferenceItem | None:
202
+ basis_data = json_object(record.get('confirmation_basis_json'))
203
+ if not basis_data:
204
+ return None
205
+ try:
206
+ basis = ConfirmationBasis.model_validate(basis_data)
207
+ confirmation_status = record.get('confirmation_status') or 'confirmed'
208
+ _validate_confirmation_state(confirmation_status, basis)
209
+ except ValueError:
210
+ return None
211
+ if not basis.confirmed_by or not basis.confirmed_at:
212
+ return None
213
+ attributes = json_object(record.get('attributes_json'))
214
+ key = str(record.get('key') or record.get('id'))
215
+ preference_key = str(
216
+ attributes.get('preferenceKey')
217
+ or attributes.get('preference_key')
218
+ or key
219
+ )
220
+ return CollaborationPreferenceItem(
221
+ id=str(record['id']),
222
+ item_kind=item_kind,
223
+ key=key,
224
+ preference_key=preference_key,
225
+ title=str(title),
226
+ instruction=str(instruction),
227
+ profile_aspect=record['profile_aspect'],
228
+ preference_scope=record['preference_scope'],
229
+ preference_project_id=record.get('preference_project_id'),
230
+ attributes=attributes,
231
+ confirmation_status=confirmation_status,
232
+ confirmed_at=basis.confirmed_at,
233
+ created_at=_native_datetime(record.get('created_at')),
234
+ )
235
+
236
+
237
+ def _conflict_free(
238
+ items: list[CollaborationPreferenceItem],
239
+ ) -> tuple[
240
+ list[CollaborationPreferenceItem],
241
+ list[CollaborationPreferenceConflict],
242
+ ]:
243
+ grouped: dict[str, list[CollaborationPreferenceItem]] = defaultdict(list)
244
+ for item in items:
245
+ grouped[item.preference_key].append(item)
246
+
247
+ effective = []
248
+ conflicts = []
249
+ for preference_key, candidates in grouped.items():
250
+ strongest_status = (
251
+ 'confirmed'
252
+ if any(item.confirmation_status == 'confirmed' for item in candidates)
253
+ else 'agent_confirmed'
254
+ )
255
+ candidates = [
256
+ item for item in candidates
257
+ if item.confirmation_status == strongest_status
258
+ ]
259
+ instructions = {
260
+ _normalize(item.instruction)
261
+ for item in candidates
262
+ }
263
+ if len(instructions) <= 1:
264
+ effective.append(candidates[0])
265
+ continue
266
+ first = candidates[0]
267
+ conflicts.append(CollaborationPreferenceConflict(
268
+ preference_key=preference_key,
269
+ preference_scope=first.preference_scope,
270
+ preference_project_id=first.preference_project_id,
271
+ item_ids=[item.id for item in candidates],
272
+ ))
273
+ effective.sort(key=_sort_key)
274
+ return effective, conflicts
275
+
276
+
277
+ def _sort_key(item: CollaborationPreferenceItem) -> tuple:
278
+ created = -item.created_at.timestamp() if item.created_at else 0
279
+ return (
280
+ 0 if item.preference_scope == 'global' else 1,
281
+ 0 if item.confirmation_status == 'confirmed' else 1,
282
+ item.profile_aspect,
283
+ item.preference_key,
284
+ created,
285
+ item.id,
286
+ )
287
+
288
+
289
+ def _normalize(value: str) -> str:
290
+ return ' '.join(value.casefold().split())
@@ -0,0 +1,34 @@
1
+ from functools import lru_cache
2
+ from typing import Literal
3
+
4
+ from pydantic import Field, field_validator
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ model_config = SettingsConfigDict(env_prefix='FULI_', extra='ignore')
10
+
11
+ provider_mode: Literal['personal', 'workspace'] = 'personal'
12
+ provider_id: str = 'local-personal'
13
+ provider_name: str = 'Fuli Personal Provider'
14
+ bootstrap_token: str = Field(min_length=24)
15
+ neo4j_uri: str = 'bolt://neo4j:7687'
16
+ neo4j_user: str = 'neo4j'
17
+ neo4j_password: str = Field(min_length=8)
18
+ neo4j_database: str = 'neo4j'
19
+ embedding_dim: int = 384
20
+ graph_limit: int = 500
21
+ search_limit: int = 20
22
+
23
+ @field_validator('provider_id')
24
+ @classmethod
25
+ def validate_provider_id(cls, value: str) -> str:
26
+ normalized = value.strip()
27
+ if not normalized or len(normalized) > 128:
28
+ raise ValueError('provider_id must contain 1 to 128 characters')
29
+ return normalized
30
+
31
+
32
+ @lru_cache(maxsize=1)
33
+ def get_settings() -> Settings:
34
+ return Settings() # type: ignore[call-arg]
@@ -0,0 +1,123 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from pydantic import Field
5
+
6
+ from .models import (
7
+ ConfirmationBasis,
8
+ ConfirmationStatus,
9
+ EpistemicQuadrant,
10
+ EpistemicStatus,
11
+ GraphEvidence,
12
+ HumanChangeStatus,
13
+ KnowledgeAssignmentRecord,
14
+ KnowledgeAuditRecord,
15
+ KnowledgeInheritanceMode,
16
+ KnowledgeItemKind,
17
+ KnowledgeRevisionRecord,
18
+ PersonalProfileAspect,
19
+ PreferenceScope,
20
+ StrictModel,
21
+ )
22
+ from .project_action_models import (
23
+ KnowledgeConflictRecord,
24
+ KnowledgeProjectReferenceRecord,
25
+ )
26
+
27
+
28
+ class GraphNode(StrictModel):
29
+ id: str
30
+ name: str
31
+ type: str
32
+ group_id: str
33
+ summary: str
34
+ origin_quadrant: EpistemicQuadrant = 'known_known'
35
+ current_quadrant: EpistemicQuadrant = 'known_known'
36
+ epistemic_status: EpistemicStatus = 'confirmed'
37
+ epistemic_state_explicit: bool = True
38
+ confirmation_status: ConfirmationStatus = 'pending'
39
+ confirmation_state_explicit: bool = False
40
+ confirmation_basis: ConfirmationBasis | None = None
41
+ reasoning_summary: str | None = None
42
+ profile_aspect: PersonalProfileAspect | None = None
43
+ preference_scope: PreferenceScope | None = None
44
+ preference_project_id: str | None = None
45
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
46
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
47
+ human_edited: bool = False
48
+ human_change_status: HumanChangeStatus = 'none'
49
+ human_change_version: int = Field(default=0, ge=0)
50
+ last_human_changed_at: datetime | None = None
51
+ last_agent_viewed_at: datetime | None = None
52
+ last_agent_reviewed_at: datetime | None = None
53
+ utility_score: float = Field(default=0, ge=0, le=1)
54
+ confidence_score: float = Field(default=0.5, ge=0, le=1)
55
+ qualified_use_count: int = Field(default=0, ge=0)
56
+ distinct_task_count: int = Field(default=0, ge=0)
57
+ last_used_at: datetime | None = None
58
+ usage_generation: int = Field(default=1, ge=1)
59
+ attributes: dict[str, Any] = Field(default_factory=dict)
60
+ evidence: list[GraphEvidence] = Field(default_factory=list)
61
+ created_at: datetime | None = None
62
+ invalid_at: datetime | None = None
63
+ replaced_by_item_id: str | None = None
64
+ replaced_by_item_kind: KnowledgeItemKind | None = None
65
+ revisions: list[KnowledgeRevisionRecord] = Field(default_factory=list)
66
+ assignments: list[KnowledgeAssignmentRecord] = Field(default_factory=list)
67
+ project_references: list[KnowledgeProjectReferenceRecord] = Field(default_factory=list)
68
+ conflicts: list[KnowledgeConflictRecord] = Field(default_factory=list)
69
+ audit_events: list[KnowledgeAuditRecord] = Field(default_factory=list)
70
+
71
+
72
+ class GraphEdge(StrictModel):
73
+ id: str
74
+ source: str
75
+ target: str
76
+ type: str
77
+ fact: str
78
+ origin_quadrant: EpistemicQuadrant = 'known_known'
79
+ current_quadrant: EpistemicQuadrant = 'known_known'
80
+ epistemic_status: EpistemicStatus = 'confirmed'
81
+ epistemic_state_explicit: bool = True
82
+ confirmation_status: ConfirmationStatus = 'pending'
83
+ confirmation_state_explicit: bool = False
84
+ confirmation_basis: ConfirmationBasis | None = None
85
+ reasoning_summary: str | None = None
86
+ profile_aspect: PersonalProfileAspect | None = None
87
+ preference_scope: PreferenceScope | None = None
88
+ preference_project_id: str | None = None
89
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
90
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
91
+ human_edited: bool = False
92
+ human_change_status: HumanChangeStatus = 'none'
93
+ human_change_version: int = Field(default=0, ge=0)
94
+ last_human_changed_at: datetime | None = None
95
+ last_agent_viewed_at: datetime | None = None
96
+ last_agent_reviewed_at: datetime | None = None
97
+ utility_score: float = Field(default=0, ge=0, le=1)
98
+ confidence_score: float = Field(default=0.5, ge=0, le=1)
99
+ qualified_use_count: int = Field(default=0, ge=0)
100
+ distinct_task_count: int = Field(default=0, ge=0)
101
+ last_used_at: datetime | None = None
102
+ usage_generation: int = Field(default=1, ge=1)
103
+ valid_at: datetime | None
104
+ invalid_at: datetime | None
105
+ replaced_by_item_id: str | None = None
106
+ replaced_by_item_kind: KnowledgeItemKind | None = None
107
+ created_at: datetime | None = None
108
+ confidence: float | None = None
109
+ attributes: dict[str, Any] = Field(default_factory=dict)
110
+ episodes: list[str] = Field(default_factory=list)
111
+ evidence: list[GraphEvidence] = Field(default_factory=list)
112
+ revisions: list[KnowledgeRevisionRecord] = Field(default_factory=list)
113
+ assignments: list[KnowledgeAssignmentRecord] = Field(default_factory=list)
114
+ project_references: list[KnowledgeProjectReferenceRecord] = Field(default_factory=list)
115
+ conflicts: list[KnowledgeConflictRecord] = Field(default_factory=list)
116
+ audit_events: list[KnowledgeAuditRecord] = Field(default_factory=list)
117
+
118
+
119
+ class GraphResult(StrictModel):
120
+ space_id: str
121
+ nodes: list[GraphNode]
122
+ edges: list[GraphEdge]
123
+ truncated: bool = False