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,251 @@
1
+ from uuid import uuid4
2
+
3
+ from fastapi import HTTPException
4
+
5
+ from .models import (
6
+ ProjectDeleteResult,
7
+ ProjectReleaseRecord,
8
+ SpaceCreate,
9
+ SpaceRecord,
10
+ )
11
+ from .provider_values import graphiti_group_id, now_utc, stable_uuid
12
+
13
+
14
+ async def publish_project(store, actor: dict, request: SpaceCreate) -> SpaceRecord:
15
+ existing = await _owned_publication(store, actor, request.publication_key)
16
+ if existing:
17
+ if request.release is None:
18
+ return store._space(
19
+ existing,
20
+ owner_id=actor['id'],
21
+ role='maintainer',
22
+ can_manage=True,
23
+ )
24
+ return await _update_project(store, actor, existing, request)
25
+ return await _create_project(store, actor, request)
26
+
27
+
28
+ async def list_project_releases(
29
+ store,
30
+ actor: dict,
31
+ project_id: str,
32
+ ) -> list[ProjectReleaseRecord]:
33
+ await store.authorize(actor, project_id, 'reader')
34
+ records, _, _ = await store.runtime.driver.execute_query(
35
+ '''
36
+ MATCH (:FuliSpace {id: $project_id, kind: 'project'})-
37
+ [:HAS_RELEASE]->(release:FuliProjectRelease)
38
+ RETURN release ORDER BY release.published_at DESC
39
+ ''',
40
+ project_id=project_id,
41
+ routing_='r',
42
+ )
43
+ return [store._project_release(record['release']) for record in records]
44
+
45
+
46
+ async def delete_project(store, actor: dict, project_id: str) -> ProjectDeleteResult:
47
+ store._require_workspace()
48
+ records, _, _ = await store.runtime.driver.execute_query(
49
+ '''
50
+ MATCH (space:FuliSpace {
51
+ id: $project_id,
52
+ kind: 'project',
53
+ visibility: 'public'
54
+ })
55
+ MATCH (owner:FuliPrincipal)-[:OWNS]->(space)
56
+ WHERE owner.id = $principal_id OR $provider_admin = true
57
+ RETURN space.name AS name, space.group_id AS group_id
58
+ ''',
59
+ project_id=project_id,
60
+ principal_id=actor['id'],
61
+ provider_admin=bool(actor.get('provider_admin')),
62
+ routing_='r',
63
+ )
64
+ if not records:
65
+ raise HTTPException(status_code=403, detail='project owner or provider administrator required')
66
+
67
+ project = records[0]
68
+ await store.runtime.driver.execute_query(
69
+ '''
70
+ MATCH (space:FuliSpace {id: $project_id, kind: 'project'})
71
+ OPTIONAL MATCH (artifact)
72
+ WHERE artifact <> space AND (
73
+ artifact.group_id = $group_id OR
74
+ (artifact.project_id = $project_id AND
75
+ (artifact:FuliProjectRelease OR artifact:FuliProposal))
76
+ )
77
+ WITH space, collect(DISTINCT artifact) AS artifacts
78
+ FOREACH (artifact IN artifacts | DETACH DELETE artifact)
79
+ DETACH DELETE space
80
+ ''',
81
+ project_id=project_id,
82
+ group_id=project['group_id'],
83
+ )
84
+ return ProjectDeleteResult(
85
+ project_id=project_id,
86
+ project_name=project['name'],
87
+ deleted=True,
88
+ )
89
+
90
+
91
+ async def _owned_publication(store, actor: dict, publication_key: str | None):
92
+ if not publication_key:
93
+ return None
94
+ records, _, _ = await store.runtime.driver.execute_query(
95
+ '''
96
+ MATCH (p:FuliPrincipal {id: $principal_id})-[:OWNS]->
97
+ (s:FuliSpace {
98
+ kind: 'project',
99
+ publication_key: $publication_key
100
+ })
101
+ RETURN s
102
+ ''',
103
+ principal_id=actor['id'],
104
+ publication_key=publication_key,
105
+ routing_='r',
106
+ )
107
+ return records[0]['s'] if records else None
108
+
109
+
110
+ async def _update_project(store, actor: dict, existing, request: SpaceCreate) -> SpaceRecord:
111
+ project_id = existing['id']
112
+ release = request.release
113
+ duplicate, _, _ = await store.runtime.driver.execute_query(
114
+ '''
115
+ MATCH (:FuliSpace {id: $project_id})-[:HAS_RELEASE]->
116
+ (release:FuliProjectRelease {version: $version})
117
+ RETURN release.id AS id
118
+ ''',
119
+ project_id=project_id,
120
+ version=release.version,
121
+ routing_='r',
122
+ )
123
+ if duplicate:
124
+ raise HTTPException(status_code=409, detail='project release version already exists')
125
+
126
+ published_at = now_utc()
127
+ release_id = stable_uuid(project_id, 'release', release.version)
128
+ records, _, _ = await store.runtime.driver.execute_query(
129
+ '''
130
+ MATCH (owner:FuliPrincipal {id: $principal_id})-[:OWNS]->
131
+ (space:FuliSpace {id: $project_id, kind: 'project'})
132
+ SET space.name = $name,
133
+ space.description = $description,
134
+ space.profile_json = $profile_json,
135
+ space.release_version = $version,
136
+ space.release_id = $release_id,
137
+ space.release_summary = $summary,
138
+ space.release_publisher_id = $principal_id,
139
+ space.release_publisher_name = $publisher_name,
140
+ space.released_at = $published_at,
141
+ space.updated_at = $published_at
142
+ CREATE (release:FuliProjectRelease {
143
+ id: $release_id,
144
+ project_id: $project_id,
145
+ version: $version,
146
+ summary: $summary,
147
+ publisher_id: $principal_id,
148
+ publisher_name: $publisher_name,
149
+ published_at: $published_at
150
+ })
151
+ CREATE (space)-[:HAS_RELEASE]->(release)
152
+ RETURN space
153
+ ''',
154
+ principal_id=actor['id'],
155
+ publisher_name=actor.get('name') or actor['id'],
156
+ project_id=project_id,
157
+ name=request.name,
158
+ description=request.description,
159
+ profile_json=request.profile.model_dump_json() if request.profile else None,
160
+ release_id=release_id,
161
+ version=release.version,
162
+ summary=release.summary,
163
+ published_at=published_at,
164
+ )
165
+ return store._space(
166
+ records[0]['space'],
167
+ owner_id=actor['id'],
168
+ role='maintainer',
169
+ can_manage=True,
170
+ )
171
+
172
+
173
+ async def _create_project(store, actor: dict, request: SpaceCreate) -> SpaceRecord:
174
+ space_id = str(uuid4())
175
+ group_id = graphiti_group_id(store.settings.provider_id, request.kind, space_id)
176
+ created_at = now_utc()
177
+ release = request.release
178
+ if release:
179
+ query = '''
180
+ MATCH (principal:FuliPrincipal {id: $principal_id})
181
+ CREATE (space:FuliSpace {
182
+ id: $id,
183
+ name: $name,
184
+ kind: 'project',
185
+ group_id: $group_id,
186
+ description: $description,
187
+ visibility: 'public',
188
+ publication_key: $publication_key,
189
+ profile_json: $profile_json,
190
+ release_version: $version,
191
+ release_id: $release_id,
192
+ release_summary: $summary,
193
+ release_publisher_id: $principal_id,
194
+ release_publisher_name: $publisher_name,
195
+ released_at: $created_at,
196
+ updated_at: $created_at,
197
+ created_at: $created_at
198
+ })
199
+ CREATE (principal)-[:OWNS {created_at: $created_at}]->(space)
200
+ CREATE (principal)-[:MEMBER_OF {role: 'maintainer', created_at: $created_at}]->(space)
201
+ CREATE (releaseNode:FuliProjectRelease {
202
+ id: $release_id,
203
+ project_id: $id,
204
+ version: $version,
205
+ summary: $summary,
206
+ publisher_id: $principal_id,
207
+ publisher_name: $publisher_name,
208
+ published_at: $created_at
209
+ })
210
+ CREATE (space)-[:HAS_RELEASE]->(releaseNode)
211
+ RETURN space
212
+ '''
213
+ else:
214
+ query = '''
215
+ MATCH (principal:FuliPrincipal {id: $principal_id})
216
+ CREATE (space:FuliSpace {
217
+ id: $id,
218
+ name: $name,
219
+ kind: 'project',
220
+ group_id: $group_id,
221
+ description: $description,
222
+ visibility: 'public',
223
+ publication_key: $publication_key,
224
+ profile_json: $profile_json,
225
+ created_at: $created_at
226
+ })
227
+ CREATE (principal)-[:OWNS {created_at: $created_at}]->(space)
228
+ CREATE (principal)-[:MEMBER_OF {role: 'maintainer', created_at: $created_at}]->(space)
229
+ RETURN space
230
+ '''
231
+ parameters = {
232
+ 'principal_id': actor['id'],
233
+ 'publisher_name': actor.get('name') or actor['id'],
234
+ 'id': space_id,
235
+ 'name': request.name,
236
+ 'group_id': group_id,
237
+ 'description': request.description,
238
+ 'publication_key': request.publication_key,
239
+ 'profile_json': request.profile.model_dump_json() if request.profile else None,
240
+ 'created_at': created_at,
241
+ 'release_id': stable_uuid(space_id, 'release', release.version) if release else None,
242
+ 'version': release.version if release else None,
243
+ 'summary': release.summary if release else None,
244
+ }
245
+ records, _, _ = await store.runtime.driver.execute_query(query, **parameters)
246
+ return store._space(
247
+ records[0]['space'],
248
+ owner_id=actor['id'],
249
+ role='maintainer',
250
+ can_manage=True,
251
+ )
@@ -0,0 +1,40 @@
1
+ import json
2
+ import re
3
+ from datetime import datetime, timezone
4
+ from uuid import NAMESPACE_URL, uuid5
5
+
6
+
7
+ def now_utc() -> datetime:
8
+ return datetime.now(timezone.utc)
9
+
10
+
11
+ def stable_uuid(*parts: str) -> str:
12
+ return str(uuid5(NAMESPACE_URL, ':'.join(parts)))
13
+
14
+
15
+ def graphiti_group_id(provider_id: str, kind: str, space_id: str) -> str:
16
+ value = f'{provider_id}-{kind}-{space_id}'
17
+ return re.sub(r'[^A-Za-z0-9_-]', '_', value)
18
+
19
+
20
+ def native_datetime(value):
21
+ if value is None or isinstance(value, datetime):
22
+ return value
23
+ to_native = getattr(value, 'to_native', None)
24
+ return to_native() if callable(to_native) else value
25
+
26
+
27
+ def json_object(value) -> dict:
28
+ if not value:
29
+ return {}
30
+ if isinstance(value, dict):
31
+ return value
32
+ try:
33
+ parsed = json.loads(value)
34
+ except (TypeError, ValueError):
35
+ return {}
36
+ return parsed if isinstance(parsed, dict) else {}
37
+
38
+
39
+ def normalized_text(value) -> str:
40
+ return ' '.join(str(value or '').strip().casefold().split())
@@ -0,0 +1,350 @@
1
+ import hashlib
2
+ import math
3
+ import os
4
+ import re
5
+ import unicodedata
6
+ from collections.abc import Iterable
7
+ from typing import Any
8
+
9
+ os.environ.setdefault('GRAPHITI_TELEMETRY_ENABLED', 'false')
10
+
11
+ from graphiti_core import Graphiti
12
+ from graphiti_core.cross_encoder.client import CrossEncoderClient
13
+ from graphiti_core.driver.neo4j_driver import Neo4jDriver
14
+ from graphiti_core.embedder.client import EmbedderClient
15
+ from graphiti_core.llm_client.client import LLMClient
16
+ from graphiti_core.llm_client.config import LLMConfig, ModelSize
17
+ from graphiti_core.prompts.models import Message
18
+ from pydantic import BaseModel
19
+
20
+ from .config import Settings
21
+
22
+
23
+ class AgentStructuredLLM(LLMClient):
24
+ """Guardrail that prevents the provider from sending knowledge to another LLM."""
25
+
26
+ def __init__(self):
27
+ super().__init__(
28
+ LLMConfig(
29
+ api_key='disabled',
30
+ model='agent-structured-input',
31
+ small_model='agent-structured-input',
32
+ max_tokens=1,
33
+ )
34
+ )
35
+
36
+ async def _generate_response(
37
+ self,
38
+ messages: list[Message],
39
+ response_model: type[BaseModel] | None = None,
40
+ max_tokens: int = 1,
41
+ model_size: ModelSize = ModelSize.medium,
42
+ ) -> dict[str, Any]:
43
+ del messages, response_model, max_tokens, model_size
44
+ raise RuntimeError(
45
+ 'This Fuli provider accepts agent-structured graph input and never invokes a second LLM'
46
+ )
47
+
48
+
49
+ class LocalHashEmbedder(EmbedderClient):
50
+ """Small deterministic multilingual embedder with no model or network dependency."""
51
+
52
+ def __init__(self, dimensions: int):
53
+ if dimensions < 64:
54
+ raise ValueError('Local hash embeddings require at least 64 dimensions')
55
+ self.dimensions = dimensions
56
+
57
+ async def create(
58
+ self,
59
+ input_data: str | list[str] | Iterable[int] | Iterable[Iterable[int]],
60
+ ) -> list[float]:
61
+ text = input_data if isinstance(input_data, str) else ' '.join(map(str, input_data))
62
+ return self._embed(text)
63
+
64
+ async def create_batch(self, input_data_list: list[str]) -> list[list[float]]:
65
+ return [self._embed(value) for value in input_data_list]
66
+
67
+ def _embed(self, value: str) -> list[float]:
68
+ normalized = unicodedata.normalize('NFKC', value).casefold()
69
+ compact = ''.join(character for character in normalized if not character.isspace())
70
+ features = re.findall(r'[\w.-]+', normalized, flags=re.UNICODE)
71
+ features.extend(compact[index : index + 2] for index in range(max(len(compact) - 1, 0)))
72
+ features.extend(compact[index : index + 3] for index in range(max(len(compact) - 2, 0)))
73
+ vector = [0.0] * self.dimensions
74
+ for feature in features:
75
+ if not feature:
76
+ continue
77
+ digest = hashlib.blake2b(
78
+ feature.encode('utf-8'),
79
+ digest_size=8,
80
+ person=b'fuli-embed-v1',
81
+ ).digest()
82
+ bucket = int.from_bytes(digest[:4], 'big') % self.dimensions
83
+ vector[bucket] += 1.0 if digest[4] & 1 else -1.0
84
+ magnitude = math.sqrt(sum(value * value for value in vector))
85
+ return vector if magnitude == 0 else [value / magnitude for value in vector]
86
+
87
+
88
+ class LocalLexicalReranker(CrossEncoderClient):
89
+ async def rank(self, query: str, passages: list[str]) -> list[tuple[str, float]]:
90
+ query_terms = set(query.casefold().split())
91
+
92
+ def score(passage: str) -> float:
93
+ normalized = passage.casefold()
94
+ if query.casefold() in normalized:
95
+ return 1.0
96
+ if not query_terms:
97
+ return 0.0
98
+ matches = sum(term in normalized for term in query_terms)
99
+ return matches / len(query_terms)
100
+
101
+ return sorted(
102
+ [(passage, score(passage)) for passage in passages],
103
+ key=lambda item: item[1],
104
+ reverse=True,
105
+ )
106
+
107
+
108
+ class GraphitiRuntime:
109
+ def __init__(self, settings: Settings):
110
+ self.settings = settings
111
+ self.embedder = LocalHashEmbedder(settings.embedding_dim)
112
+ driver = Neo4jDriver(
113
+ uri=settings.neo4j_uri,
114
+ user=settings.neo4j_user,
115
+ password=settings.neo4j_password,
116
+ database=settings.neo4j_database,
117
+ )
118
+ self.graphiti = Graphiti(
119
+ graph_driver=driver,
120
+ llm_client=AgentStructuredLLM(),
121
+ embedder=self.embedder,
122
+ cross_encoder=LocalLexicalReranker(),
123
+ store_raw_episode_content=False,
124
+ )
125
+
126
+ @property
127
+ def driver(self):
128
+ return self.graphiti.driver
129
+
130
+ async def initialize(self) -> None:
131
+ await self.graphiti.build_indices_and_constraints()
132
+ await self.driver.execute_query(
133
+ 'CREATE CONSTRAINT fuli_principal_id IF NOT EXISTS '
134
+ 'FOR (n:FuliPrincipal) REQUIRE n.id IS UNIQUE'
135
+ )
136
+ await self.driver.execute_query(
137
+ 'CREATE CONSTRAINT fuli_space_id IF NOT EXISTS '
138
+ 'FOR (n:FuliSpace) REQUIRE n.id IS UNIQUE'
139
+ )
140
+ await self.driver.execute_query(
141
+ 'CREATE CONSTRAINT fuli_subscription_id IF NOT EXISTS '
142
+ 'FOR (n:FuliSubscription) REQUIRE n.id IS UNIQUE'
143
+ )
144
+ await self.driver.execute_query(
145
+ 'CREATE CONSTRAINT fuli_proposal_id IF NOT EXISTS '
146
+ 'FOR (n:FuliProposal) REQUIRE n.id IS UNIQUE'
147
+ )
148
+ await self.driver.execute_query(
149
+ 'CREATE CONSTRAINT fuli_personal_project_id IF NOT EXISTS '
150
+ 'FOR (n:FuliPersonalProject) REQUIRE n.id IS UNIQUE'
151
+ )
152
+ await self.driver.execute_query(
153
+ 'CREATE CONSTRAINT fuli_publication_draft_id IF NOT EXISTS '
154
+ 'FOR (n:FuliPublicationDraft) REQUIRE n.id IS UNIQUE'
155
+ )
156
+ await self.driver.execute_query(
157
+ 'CREATE CONSTRAINT fuli_project_release_id IF NOT EXISTS '
158
+ 'FOR (n:FuliProjectRelease) REQUIRE n.id IS UNIQUE'
159
+ )
160
+ await self.driver.execute_query(
161
+ 'CREATE CONSTRAINT fuli_knowledge_reference_id IF NOT EXISTS '
162
+ 'FOR (n:FuliKnowledgeProjectReference) REQUIRE n.id IS UNIQUE'
163
+ )
164
+ await self.driver.execute_query(
165
+ 'CREATE CONSTRAINT fuli_knowledge_conflict_id IF NOT EXISTS '
166
+ 'FOR (n:FuliKnowledgeConflict) REQUIRE n.id IS UNIQUE'
167
+ )
168
+ await self.driver.execute_query(
169
+ 'CREATE CONSTRAINT fuli_knowledge_audit_id IF NOT EXISTS '
170
+ 'FOR (n:FuliKnowledgeAudit) REQUIRE n.id IS UNIQUE'
171
+ )
172
+ for query in (
173
+ 'CREATE INDEX fuli_entity_confirmation IF NOT EXISTS '
174
+ 'FOR (n:Entity) ON (n.group_id, n.fuli_confirmation_status)',
175
+ 'CREATE INDEX fuli_entity_usage_score IF NOT EXISTS '
176
+ 'FOR (n:Entity) ON (n.group_id, n.fuli_utility_score)',
177
+ 'CREATE INDEX fuli_relationship_confirmation IF NOT EXISTS '
178
+ 'FOR ()-[r:RELATES_TO]-() '
179
+ 'ON (r.group_id, r.fuli_confirmation_status)',
180
+ 'CREATE INDEX fuli_relationship_usage_score IF NOT EXISTS '
181
+ 'FOR ()-[r:RELATES_TO]-() ON (r.group_id, r.fuli_utility_score)',
182
+ 'CREATE INDEX fuli_knowledge_assignment_lookup IF NOT EXISTS '
183
+ 'FOR (n:FuliKnowledgeAssignment) '
184
+ 'ON (n.space_id, n.item_kind, n.item_id)',
185
+ 'CREATE INDEX fuli_knowledge_reference_lookup IF NOT EXISTS '
186
+ 'FOR (n:FuliKnowledgeProjectReference) '
187
+ 'ON (n.space_id, n.item_kind, n.item_id)',
188
+ 'CREATE INDEX fuli_knowledge_audit_lookup IF NOT EXISTS '
189
+ 'FOR (n:FuliKnowledgeAudit) '
190
+ 'ON (n.space_id, n.item_id, n.usage_generation)',
191
+ 'CREATE INDEX fuli_personal_project_relation_type IF NOT EXISTS '
192
+ 'FOR ()-[r:PERSONAL_PROJECT_RELATION]-() ON (r.relation_type)',
193
+ ):
194
+ await self.driver.execute_query(query)
195
+ await self._migrate_knowledge_lifecycle_defaults()
196
+ await self._migrate_space_visibility_and_owners()
197
+ await self._migrate_project_releases()
198
+ await self._migrate_legacy_group_ids()
199
+
200
+ async def _migrate_space_visibility_and_owners(self) -> None:
201
+ await self.driver.execute_query(
202
+ "MATCH (s:FuliSpace) WHERE s.visibility IS NULL "
203
+ "SET s.visibility = CASE s.kind WHEN 'project' THEN 'public' ELSE 'private' END"
204
+ )
205
+ records, _, _ = await self.driver.execute_query(
206
+ '''
207
+ MATCH (space:FuliSpace {kind: 'project'})
208
+ WHERE NOT EXISTS {
209
+ MATCH (:FuliPrincipal)-[:OWNS]->(space)
210
+ }
211
+ OPTIONAL MATCH (principal:FuliPrincipal)-[membership:MEMBER_OF]->(space)
212
+ WHERE membership.role = 'maintainer'
213
+ WITH space, principal, membership
214
+ ORDER BY coalesce(membership.created_at, space.created_at)
215
+ RETURN space.id AS space_id, head(collect(principal.id)) AS owner_id
216
+ ''',
217
+ routing_='r',
218
+ )
219
+ for record in records:
220
+ if not record['owner_id']:
221
+ continue
222
+ await self.driver.execute_query(
223
+ '''
224
+ MATCH (principal:FuliPrincipal {id: $owner_id})
225
+ MATCH (space:FuliSpace {id: $space_id})
226
+ MERGE (principal)-[:OWNS]->(space)
227
+ ''',
228
+ owner_id=record['owner_id'],
229
+ space_id=record['space_id'],
230
+ )
231
+
232
+ async def _migrate_project_releases(self) -> None:
233
+ await self.driver.execute_query(
234
+ '''
235
+ MATCH (owner:FuliPrincipal)-[:OWNS]->
236
+ (space:FuliSpace {kind: 'project', visibility: 'public'})
237
+ WHERE space.release_version IS NULL
238
+ WITH owner, space, 'legacy-release-' + space.id AS release_id
239
+ SET space.release_version = 'legacy',
240
+ space.release_id = release_id,
241
+ space.release_summary = '历史项目迁移,原始版本与更新内容未记录',
242
+ space.release_publisher_id = owner.id,
243
+ space.release_publisher_name = owner.name,
244
+ space.released_at = space.created_at,
245
+ space.updated_at = coalesce(space.updated_at, space.created_at)
246
+ MERGE (release:FuliProjectRelease {id: release_id})
247
+ ON CREATE SET release.project_id = space.id,
248
+ release.version = 'legacy',
249
+ release.summary = '历史项目迁移,原始版本与更新内容未记录',
250
+ release.publisher_id = owner.id,
251
+ release.publisher_name = owner.name,
252
+ release.published_at = space.created_at
253
+ MERGE (space)-[:HAS_RELEASE]->(release)
254
+ '''
255
+ )
256
+
257
+ async def _migrate_legacy_group_ids(self) -> None:
258
+ records, _, _ = await self.driver.execute_query(
259
+ "MATCH (s:FuliSpace) WHERE s.group_id CONTAINS ':' "
260
+ 'RETURN s.id AS id, s.group_id AS group_id',
261
+ routing_='r',
262
+ )
263
+ for record in records:
264
+ previous = record['group_id']
265
+ current = previous.replace(':', '-')
266
+ async with self.driver.transaction() as transaction:
267
+ await transaction.run(
268
+ 'MATCH (s:FuliSpace {id: $space_id}) SET s.group_id = $current',
269
+ space_id=record['id'],
270
+ current=current,
271
+ )
272
+ await transaction.run(
273
+ 'MATCH (n) WHERE n.group_id = $previous SET n.group_id = $current',
274
+ previous=previous,
275
+ current=current,
276
+ )
277
+ await transaction.run(
278
+ 'MATCH ()-[r]->() WHERE r.group_id = $previous SET r.group_id = $current',
279
+ previous=previous,
280
+ current=current,
281
+ )
282
+
283
+ async def _migrate_knowledge_lifecycle_defaults(self) -> None:
284
+ await self.driver.execute_query(
285
+ '''
286
+ MATCH (item:Entity)
287
+ WHERE item.fuli_utility_score IS NULL
288
+ OR item.fuli_confidence_score IS NULL
289
+ OR item.fuli_qualified_use_count IS NULL
290
+ OR item.fuli_distinct_task_count IS NULL
291
+ OR item.fuli_usage_generation IS NULL
292
+ OR item.fuli_inheritance_mode IS NULL
293
+ OR item.fuli_inherited_project_ids IS NULL
294
+ SET item.fuli_utility_score =
295
+ coalesce(item.fuli_utility_score, 0.0),
296
+ item.fuli_confidence_score = coalesce(
297
+ item.fuli_confidence_score,
298
+ CASE coalesce(item.fuli_confirmation_status, 'pending')
299
+ WHEN 'confirmed' THEN 1.0
300
+ WHEN 'agent_confirmed' THEN 0.75
301
+ ELSE 0.5
302
+ END
303
+ ),
304
+ item.fuli_qualified_use_count =
305
+ coalesce(item.fuli_qualified_use_count, 0),
306
+ item.fuli_distinct_task_count =
307
+ coalesce(item.fuli_distinct_task_count, 0),
308
+ item.fuli_usage_generation =
309
+ coalesce(item.fuli_usage_generation, 1),
310
+ item.fuli_inheritance_mode =
311
+ coalesce(item.fuli_inheritance_mode, 'local_only'),
312
+ item.fuli_inherited_project_ids =
313
+ coalesce(item.fuli_inherited_project_ids, [])
314
+ '''
315
+ )
316
+ await self.driver.execute_query(
317
+ '''
318
+ MATCH ()-[item:RELATES_TO]->()
319
+ WHERE item.fuli_utility_score IS NULL
320
+ OR item.fuli_confidence_score IS NULL
321
+ OR item.fuli_qualified_use_count IS NULL
322
+ OR item.fuli_distinct_task_count IS NULL
323
+ OR item.fuli_usage_generation IS NULL
324
+ OR item.fuli_inheritance_mode IS NULL
325
+ OR item.fuli_inherited_project_ids IS NULL
326
+ SET item.fuli_utility_score =
327
+ coalesce(item.fuli_utility_score, 0.0),
328
+ item.fuli_confidence_score = coalesce(
329
+ item.fuli_confidence_score,
330
+ CASE coalesce(item.fuli_confirmation_status, 'pending')
331
+ WHEN 'confirmed' THEN 1.0
332
+ WHEN 'agent_confirmed' THEN 0.75
333
+ ELSE 0.5
334
+ END
335
+ ),
336
+ item.fuli_qualified_use_count =
337
+ coalesce(item.fuli_qualified_use_count, 0),
338
+ item.fuli_distinct_task_count =
339
+ coalesce(item.fuli_distinct_task_count, 0),
340
+ item.fuli_usage_generation =
341
+ coalesce(item.fuli_usage_generation, 1),
342
+ item.fuli_inheritance_mode =
343
+ coalesce(item.fuli_inheritance_mode, 'local_only'),
344
+ item.fuli_inherited_project_ids =
345
+ coalesce(item.fuli_inherited_project_ids, [])
346
+ '''
347
+ )
348
+
349
+ async def close(self) -> None:
350
+ await self.graphiti.close()