fuli-context 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/compose.graphiti.yml +86 -0
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
- package/dist/web/assets/console-tbvTUi6p.js +1 -0
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-B3s5CXhv.js +1 -0
- package/dist/web/assets/index-Devf7glZ.css +1 -0
- package/dist/web/assets/index-DwUDhQKc.js +6 -0
- package/dist/web/assets/model-DVT8sjy9.js +1 -0
- package/dist/web/assets/paths-B790iniO.js +1 -0
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
- package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
- package/dist/web/index.html +22 -0
- package/graph-provider/Dockerfile +16 -0
- package/graph-provider/fuli_graph/__init__.py +5 -0
- package/graph-provider/fuli_graph/app.py +547 -0
- package/graph-provider/fuli_graph/auth.py +17 -0
- package/graph-provider/fuli_graph/collaboration_context.py +290 -0
- package/graph-provider/fuli_graph/config.py +34 -0
- package/graph-provider/fuli_graph/graph_models.py +123 -0
- package/graph-provider/fuli_graph/graph_projection.py +405 -0
- package/graph-provider/fuli_graph/graph_query.py +561 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
- package/graph-provider/fuli_graph/knowledge_management.py +816 -0
- package/graph-provider/fuli_graph/knowledge_records.py +65 -0
- package/graph-provider/fuli_graph/knowledge_search.py +723 -0
- package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
- package/graph-provider/fuli_graph/model_validation.py +58 -0
- package/graph-provider/fuli_graph/models.py +1179 -0
- package/graph-provider/fuli_graph/personal_project_access.py +29 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
- package/graph-provider/fuli_graph/project_action_models.py +142 -0
- package/graph-provider/fuli_graph/project_knowledge.py +707 -0
- package/graph-provider/fuli_graph/project_management.py +251 -0
- package/graph-provider/fuli_graph/provider_values.py +40 -0
- package/graph-provider/fuli_graph/runtime.py +350 -0
- package/graph-provider/fuli_graph/search_projection.py +156 -0
- package/graph-provider/fuli_graph/store.py +24 -0
- package/graph-provider/fuli_graph/store_identity.py +237 -0
- package/graph-provider/fuli_graph/store_knowledge.py +447 -0
- package/graph-provider/fuli_graph/store_projects.py +242 -0
- package/graph-provider/fuli_graph/store_publication.py +370 -0
- package/graph-provider/fuli_graph/store_records.py +134 -0
- package/graph-provider/pyproject.toml +35 -0
- package/npm-shrinkwrap.json +6045 -0
- package/package.json +99 -0
- package/skills/capturing-session-knowledge/SKILL.md +226 -0
- package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
- package/skills/grilling-project/SKILL.md +129 -0
- package/skills/grilling-project/agents/openai.yaml +4 -0
- package/skills/grilling-project/references/project-profile.md +105 -0
- package/src/agent/candidate-read-service.js +53 -0
- package/src/agent/context-pack-service.js +83 -0
- package/src/agent/public-read-policy.js +39 -0
- package/src/agent/public-read-service.js +69 -0
- package/src/agent/read-projection.js +107 -0
- package/src/agent/search-service.js +61 -0
- package/src/agent/space-boundary.js +33 -0
- package/src/agent-tools/graph-definitions.js +479 -0
- package/src/agent-tools/graph-handlers.js +96 -0
- package/src/agent-tools/schema.js +37 -0
- package/src/agent-tools.js +16 -0
- package/src/app/application-error.js +13 -0
- package/src/app/bootstrap.js +8 -0
- package/src/app/create-application.js +143 -0
- package/src/app/state-service.js +38 -0
- package/src/candidates.js +78 -0
- package/src/classifier.js +63 -0
- package/src/cli/command-arguments.js +14 -0
- package/src/cli/command-registry.js +78 -0
- package/src/cli/commands/candidates.js +17 -0
- package/src/cli/commands/ingestion.js +32 -0
- package/src/cli/commands/reads.js +50 -0
- package/src/cli/commands/spaces.js +20 -0
- package/src/cli/invocation.js +47 -0
- package/src/cli/local-runtime-command.js +94 -0
- package/src/cli/local-runtime-options.js +64 -0
- package/src/cli/migrate-command.js +71 -0
- package/src/cli/migration-destination.js +117 -0
- package/src/cli/setup-agent-selection.js +195 -0
- package/src/cli/setup-command.js +85 -0
- package/src/cli/uninstall-command.js +67 -0
- package/src/cli.js +90 -0
- package/src/context-pack.js +206 -0
- package/src/defaults.js +1 -0
- package/src/extractor.js +77 -0
- package/src/fact-history.js +39 -0
- package/src/facts/fact-service.js +89 -0
- package/src/graphiti/agent-access-policy.js +88 -0
- package/src/graphiti/capture-policy.js +84 -0
- package/src/graphiti/federated-application.js +1292 -0
- package/src/graphiti/personal-context.js +29 -0
- package/src/graphiti/project-path-context.js +159 -0
- package/src/graphiti/provider-client.js +288 -0
- package/src/graphiti/runtime-config.js +85 -0
- package/src/graphiti/setup.js +43 -0
- package/src/graphiti/source-marker.js +142 -0
- package/src/http/api-router.js +133 -0
- package/src/http/bounded-json-array.js +19 -0
- package/src/http/error-mapping.js +36 -0
- package/src/http/graph-api-router.js +257 -0
- package/src/http/lens-route.js +50 -0
- package/src/http/request-policy.js +41 -0
- package/src/http/response.js +29 -0
- package/src/http/static-handler.js +55 -0
- package/src/ingestion.js +161 -0
- package/src/lens/lens-correction-service.js +81 -0
- package/src/lens/lens-fact-writer.js +85 -0
- package/src/lens/lens-input.js +62 -0
- package/src/lens/lens-query-projection.js +87 -0
- package/src/lens/lens-query-ranking.js +62 -0
- package/src/lens/lens-query.js +143 -0
- package/src/lens/lens-resource-projection.js +93 -0
- package/src/lens/lens-resource-selection.js +45 -0
- package/src/lens/lens-resource-service.js +103 -0
- package/src/lens/lens-search-query.js +122 -0
- package/src/lens/lens-service.js +183 -0
- package/src/local-runtime/lifecycle.js +254 -0
- package/src/mcp/create-mcp-server.js +85 -0
- package/src/mcp/instructions.js +10 -0
- package/src/mcp/project-action-preview-tokens.js +72 -0
- package/src/mcp/runtime.js +71 -0
- package/src/mcp/tool-annotations.js +63 -0
- package/src/mcp/tool-result.js +167 -0
- package/src/mcp/tool-schema.js +60 -0
- package/src/mcp-server.js +76 -0
- package/src/models.js +44 -0
- package/src/observer.js +83 -0
- package/src/package-metadata.js +6 -0
- package/src/project-rules.js +33 -0
- package/src/publication/canonical-json.js +39 -0
- package/src/publication/outbox-service.js +46 -0
- package/src/publication/publication-envelope-schema.js +75 -0
- package/src/publication/publication-envelope.js +37 -0
- package/src/publication/publication-policy.js +82 -0
- package/src/publication/publication-service.js +48 -0
- package/src/router.js +58 -0
- package/src/runtime-options.js +139 -0
- package/src/security/episode-visibility.js +14 -0
- package/src/security/fact-visibility.js +15 -0
- package/src/security/sensitive-content.js +18 -0
- package/src/security/source-metadata-policy.js +8 -0
- package/src/server/application-lifecycle.js +34 -0
- package/src/server/listen.js +24 -0
- package/src/server.js +135 -0
- package/src/setup/agent-installation-status.js +97 -0
- package/src/setup/agents.js +123 -0
- package/src/setup/codex-bootstrap.js +118 -0
- package/src/setup/codex-config.js +122 -0
- package/src/setup/config-backup.js +22 -0
- package/src/setup/container-runtime.js +388 -0
- package/src/setup/cursor-config.js +61 -0
- package/src/setup/graph-runtime.js +360 -0
- package/src/setup/node-runtime.js +24 -0
- package/src/setup/options.js +91 -0
- package/src/setup/paths.js +47 -0
- package/src/setup/session-skill.js +91 -0
- package/src/setup/setup.js +85 -0
- package/src/setup/uninstall.js +90 -0
- package/src/storage/episode-evidence-preview.js +100 -0
- package/src/storage/fact-search-order.js +18 -0
- package/src/storage/file/episode-evidence.js +37 -0
- package/src/storage/file/episode-index.js +59 -0
- package/src/storage/file/fact-search.js +60 -0
- package/src/storage/file-store.js +437 -0
- package/src/storage/import-json.js +70 -0
- package/src/storage/json-file.js +41 -0
- package/src/storage/legacy-record-validation.js +152 -0
- package/src/storage/legacy-snapshot-validation.js +32 -0
- package/src/storage/migrate.js +128 -0
- package/src/storage/migrations/001-initial.sql +83 -0
- package/src/storage/migrations/002-storage-integrity.sql +113 -0
- package/src/storage/record-validation.js +46 -0
- package/src/storage/snapshot-defaults.js +54 -0
- package/src/storage/sqlite/candidates-repository.js +84 -0
- package/src/storage/sqlite/episode-evidence-repository.js +154 -0
- package/src/storage/sqlite/episode-privacy-sql.js +33 -0
- package/src/storage/sqlite/episodes-repository.js +52 -0
- package/src/storage/sqlite/fact-search-repository.js +162 -0
- package/src/storage/sqlite/facts-repository.js +146 -0
- package/src/storage/sqlite/imports-repository.js +39 -0
- package/src/storage/sqlite/mapper.js +84 -0
- package/src/storage/sqlite/open-database.js +15 -0
- package/src/storage/sqlite/outbox-repository.js +103 -0
- package/src/storage/sqlite/snapshot-repository.js +69 -0
- package/src/storage/sqlite/spaces-repository.js +52 -0
- package/src/storage/sqlite/subscriptions-repository.js +54 -0
- package/src/storage/sqlite-store.js +159 -0
- package/src/storage/stable-json.js +24 -0
- package/src/storage/store-port.js +20 -0
- package/src/store.js +2 -0
- package/src/workspace-protocol/array-validation.js +19 -0
- package/src/workspace-protocol/base64url-contract.js +27 -0
- package/src/workspace-protocol/constants.js +37 -0
- package/src/workspace-protocol/error-contract.js +24 -0
- package/src/workspace-protocol/fact-contract.js +110 -0
- package/src/workspace-protocol/governance-contract.js +47 -0
- package/src/workspace-protocol/index.js +53 -0
- package/src/workspace-protocol/json-budget.js +19 -0
- package/src/workspace-protocol/manifest-contract.js +186 -0
- package/src/workspace-protocol/publication-contract.js +212 -0
- package/src/workspace-protocol/query-contract.js +149 -0
- package/src/workspace-protocol/scalar-contract.js +192 -0
- package/src/workspace-protocol/sync-contract.js +184 -0
- package/src/workspace-protocol/text-contract.js +39 -0
- package/src/workspace-protocol/validation.js +66 -0
|
@@ -0,0 +1,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()
|