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,156 @@
|
|
|
1
|
+
from graphiti_core.edges import EntityEdge
|
|
2
|
+
|
|
3
|
+
from .models import (
|
|
4
|
+
ConfirmationBasis,
|
|
5
|
+
FactResult,
|
|
6
|
+
_validate_confirmation_state,
|
|
7
|
+
)
|
|
8
|
+
from .provider_values import json_object, native_datetime as _native_datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_default_retrievable(metadata: dict) -> bool:
|
|
12
|
+
return effective_confirmation_status(metadata) in {
|
|
13
|
+
'confirmed',
|
|
14
|
+
'agent_confirmed',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def effective_confirmation_status(metadata: dict) -> str:
|
|
19
|
+
status = metadata.get('confirmation_status', 'pending')
|
|
20
|
+
basis_data = json_object(metadata.get('confirmation_basis_json'))
|
|
21
|
+
if not basis_data:
|
|
22
|
+
return 'pending'
|
|
23
|
+
try:
|
|
24
|
+
basis = ConfirmationBasis.model_validate(basis_data)
|
|
25
|
+
_validate_confirmation_state(status, basis)
|
|
26
|
+
except ValueError:
|
|
27
|
+
return 'pending'
|
|
28
|
+
return status
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def read_edge_epistemic_metadata(store, edge_ids: list[str]) -> dict[str, dict]:
|
|
32
|
+
if not edge_ids:
|
|
33
|
+
return {}
|
|
34
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
35
|
+
'''
|
|
36
|
+
MATCH ()-[edge:RELATES_TO]->()
|
|
37
|
+
WHERE edge.uuid IN $edge_ids
|
|
38
|
+
RETURN edge.uuid AS id,
|
|
39
|
+
coalesce(edge.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
|
|
40
|
+
coalesce(edge.fuli_current_quadrant, 'known_known') AS current_quadrant,
|
|
41
|
+
coalesce(edge.fuli_epistemic_status, 'confirmed') AS epistemic_status,
|
|
42
|
+
coalesce(edge.fuli_confirmation_status, 'pending')
|
|
43
|
+
AS confirmation_status,
|
|
44
|
+
edge.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
45
|
+
edge.fuli_reasoning_summary AS reasoning_summary,
|
|
46
|
+
edge.fuli_profile_aspect AS profile_aspect,
|
|
47
|
+
CASE WHEN edge.fuli_profile_aspect IS NULL THEN NULL
|
|
48
|
+
ELSE coalesce(edge.fuli_preference_scope, 'global') END
|
|
49
|
+
AS preference_scope,
|
|
50
|
+
edge.fuli_preference_project_id AS preference_project_id,
|
|
51
|
+
edge.fuli_key AS key,
|
|
52
|
+
coalesce(edge.fuli_inheritance_mode, 'local_only')
|
|
53
|
+
AS inheritance_mode,
|
|
54
|
+
coalesce(edge.fuli_inherited_project_ids, [])
|
|
55
|
+
AS inherited_project_ids,
|
|
56
|
+
coalesce(edge.fuli_human_edited, false) AS human_edited,
|
|
57
|
+
coalesce(edge.fuli_human_change_status, 'none')
|
|
58
|
+
AS human_change_status,
|
|
59
|
+
coalesce(edge.fuli_human_change_version, 0)
|
|
60
|
+
AS human_change_version,
|
|
61
|
+
edge.fuli_last_human_changed_at AS last_human_changed_at,
|
|
62
|
+
edge.fuli_last_agent_viewed_at AS last_agent_viewed_at,
|
|
63
|
+
edge.fuli_last_agent_reviewed_at AS last_agent_reviewed_at
|
|
64
|
+
,coalesce(edge.fuli_utility_score, 0.0) AS utility_score
|
|
65
|
+
,coalesce(edge.fuli_confidence_score, 0.5) AS confidence_score
|
|
66
|
+
,coalesce(edge.fuli_qualified_use_count, 0)
|
|
67
|
+
AS qualified_use_count
|
|
68
|
+
,coalesce(edge.fuli_distinct_task_count, 0)
|
|
69
|
+
AS distinct_task_count
|
|
70
|
+
,edge.fuli_last_used_at AS last_used_at
|
|
71
|
+
''',
|
|
72
|
+
edge_ids=edge_ids,
|
|
73
|
+
routing_='r',
|
|
74
|
+
)
|
|
75
|
+
result = {}
|
|
76
|
+
for record in records:
|
|
77
|
+
value = dict(record)
|
|
78
|
+
effective_status = effective_confirmation_status(value)
|
|
79
|
+
if effective_status != value.get('confirmation_status'):
|
|
80
|
+
value['confidence_score'] = min(
|
|
81
|
+
float(value.get('confidence_score') or 0.5),
|
|
82
|
+
0.5,
|
|
83
|
+
)
|
|
84
|
+
value['confirmation_status'] = effective_status
|
|
85
|
+
result[record['id']] = value
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fact_result(
|
|
90
|
+
edge: EntityEdge,
|
|
91
|
+
names: dict[str, str],
|
|
92
|
+
space_id: str,
|
|
93
|
+
epistemic: dict | None = None,
|
|
94
|
+
) -> FactResult:
|
|
95
|
+
metadata = epistemic or {}
|
|
96
|
+
return FactResult(
|
|
97
|
+
id=edge.uuid,
|
|
98
|
+
space_id=space_id,
|
|
99
|
+
group_id=edge.group_id,
|
|
100
|
+
source_entity=names.get(edge.source_node_uuid, edge.source_node_uuid),
|
|
101
|
+
target_entity=names.get(edge.target_node_uuid, edge.target_node_uuid),
|
|
102
|
+
relationship=edge.name,
|
|
103
|
+
fact=edge.fact,
|
|
104
|
+
valid_at=edge.valid_at,
|
|
105
|
+
invalid_at=edge.invalid_at,
|
|
106
|
+
created_at=_native_datetime(edge.created_at),
|
|
107
|
+
episodes=edge.episodes,
|
|
108
|
+
origin_quadrant=metadata.get('origin_quadrant', 'known_known'),
|
|
109
|
+
current_quadrant=metadata.get('current_quadrant', 'known_known'),
|
|
110
|
+
epistemic_status=metadata.get('epistemic_status', 'confirmed'),
|
|
111
|
+
confirmation_status=metadata.get('confirmation_status', 'pending'),
|
|
112
|
+
confirmation_basis=_confirmation_basis(
|
|
113
|
+
metadata.get('confirmation_basis_json')
|
|
114
|
+
),
|
|
115
|
+
reasoning_summary=metadata.get('reasoning_summary'),
|
|
116
|
+
profile_aspect=metadata.get('profile_aspect'),
|
|
117
|
+
preference_scope=metadata.get('preference_scope'),
|
|
118
|
+
preference_project_id=metadata.get('preference_project_id'),
|
|
119
|
+
key=metadata.get('key'),
|
|
120
|
+
defined_project_id=metadata.get('defined_project_id'),
|
|
121
|
+
inheritance_mode=metadata.get('inheritance_mode') or 'local_only',
|
|
122
|
+
inherited_project_ids=metadata.get('inherited_project_ids') or [],
|
|
123
|
+
human_edited=metadata.get('human_edited') is True,
|
|
124
|
+
human_change_status=metadata.get('human_change_status') or 'none',
|
|
125
|
+
human_change_version=int(metadata.get('human_change_version') or 0),
|
|
126
|
+
last_human_changed_at=_native_datetime(
|
|
127
|
+
metadata.get('last_human_changed_at')
|
|
128
|
+
),
|
|
129
|
+
last_agent_viewed_at=_native_datetime(
|
|
130
|
+
metadata.get('last_agent_viewed_at')
|
|
131
|
+
),
|
|
132
|
+
last_agent_reviewed_at=_native_datetime(
|
|
133
|
+
metadata.get('last_agent_reviewed_at')
|
|
134
|
+
),
|
|
135
|
+
utility_score=float(metadata.get('utility_score') or 0),
|
|
136
|
+
confidence_score=float(
|
|
137
|
+
metadata['confidence_score']
|
|
138
|
+
if metadata.get('confidence_score') is not None else 0.5
|
|
139
|
+
),
|
|
140
|
+
qualified_use_count=int(metadata.get('qualified_use_count') or 0),
|
|
141
|
+
distinct_task_count=int(metadata.get('distinct_task_count') or 0),
|
|
142
|
+
last_used_at=_native_datetime(metadata.get('last_used_at')),
|
|
143
|
+
scope_distance=int(metadata.get('scope_distance') or 0),
|
|
144
|
+
inherited_from_project_id=metadata.get('inherited_from_project_id'),
|
|
145
|
+
scope_path=metadata.get('scope_path') or [],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _confirmation_basis(value) -> ConfirmationBasis | None:
|
|
150
|
+
data = json_object(value)
|
|
151
|
+
if not data:
|
|
152
|
+
return None
|
|
153
|
+
try:
|
|
154
|
+
return ConfirmationBasis.model_validate(data)
|
|
155
|
+
except ValueError:
|
|
156
|
+
return None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .config import Settings
|
|
2
|
+
from .provider_values import graphiti_group_id, native_datetime
|
|
3
|
+
from .runtime import GraphitiRuntime
|
|
4
|
+
from .store_identity import StoreIdentity
|
|
5
|
+
from .store_knowledge import StoreKnowledge
|
|
6
|
+
from .store_projects import StoreProjects
|
|
7
|
+
from .store_publication import StorePublication
|
|
8
|
+
from .store_records import StoreRecords
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GraphStore(
|
|
12
|
+
StoreIdentity,
|
|
13
|
+
StoreProjects,
|
|
14
|
+
StorePublication,
|
|
15
|
+
StoreKnowledge,
|
|
16
|
+
StoreRecords,
|
|
17
|
+
):
|
|
18
|
+
def __init__(self, runtime: GraphitiRuntime, settings: Settings):
|
|
19
|
+
self.runtime = runtime
|
|
20
|
+
self.settings = settings
|
|
21
|
+
self._group_locks = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ['GraphStore', 'graphiti_group_id', 'native_datetime']
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from uuid import uuid4
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException, status
|
|
4
|
+
|
|
5
|
+
from .auth import issue_access_token, token_hash
|
|
6
|
+
from .models import PrincipalResult, SpaceCreate, SpaceRecord
|
|
7
|
+
from .project_management import publish_project
|
|
8
|
+
from .provider_values import graphiti_group_id, now_utc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ROLE_RANK = {'reader': 1, 'contributor': 2, 'maintainer': 3}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StoreIdentity:
|
|
15
|
+
async def health(self) -> dict:
|
|
16
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
17
|
+
'RETURN 1 AS ready', routing_='r'
|
|
18
|
+
)
|
|
19
|
+
return {
|
|
20
|
+
'status': 'ready' if records and records[0]['ready'] == 1 else 'degraded',
|
|
21
|
+
'providerId': self.settings.provider_id,
|
|
22
|
+
'mode': self.settings.provider_mode,
|
|
23
|
+
'storage': 'graphiti-neo4j',
|
|
24
|
+
'llmExtraction': 'agent-structured',
|
|
25
|
+
'embedding': 'local-hash-v1',
|
|
26
|
+
'telemetry': False,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async def bootstrap(self, principal_name: str) -> tuple[str, str]:
|
|
30
|
+
existing, _, _ = await self.runtime.driver.execute_query(
|
|
31
|
+
'''
|
|
32
|
+
MATCH (p:FuliPrincipal {provider_admin: true})
|
|
33
|
+
RETURN p.id AS id ORDER BY p.created_at LIMIT 1
|
|
34
|
+
''',
|
|
35
|
+
routing_='r',
|
|
36
|
+
)
|
|
37
|
+
access_token = issue_access_token()
|
|
38
|
+
if existing:
|
|
39
|
+
principal_id = existing[0]['id']
|
|
40
|
+
await self.runtime.driver.execute_query(
|
|
41
|
+
'''
|
|
42
|
+
MATCH (p:FuliPrincipal {id: $id, provider_admin: true})
|
|
43
|
+
SET p.token_hash = $token_hash, p.active = true
|
|
44
|
+
''',
|
|
45
|
+
id=principal_id,
|
|
46
|
+
token_hash=token_hash(access_token),
|
|
47
|
+
)
|
|
48
|
+
return principal_id, access_token
|
|
49
|
+
principal_id = str(uuid4())
|
|
50
|
+
await self.runtime.driver.execute_query(
|
|
51
|
+
'''
|
|
52
|
+
CREATE (p:FuliPrincipal {
|
|
53
|
+
id: $id,
|
|
54
|
+
name: $name,
|
|
55
|
+
token_hash: $token_hash,
|
|
56
|
+
active: true,
|
|
57
|
+
provider_admin: true,
|
|
58
|
+
created_at: $created_at
|
|
59
|
+
})
|
|
60
|
+
''',
|
|
61
|
+
id=principal_id,
|
|
62
|
+
name=principal_name,
|
|
63
|
+
token_hash=token_hash(access_token),
|
|
64
|
+
created_at=now_utc(),
|
|
65
|
+
)
|
|
66
|
+
return principal_id, access_token
|
|
67
|
+
|
|
68
|
+
async def authenticate(self, access_token: str) -> dict:
|
|
69
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
70
|
+
'''
|
|
71
|
+
MATCH (p:FuliPrincipal {token_hash: $token_hash, active: true})
|
|
72
|
+
RETURN p.id AS id, p.name AS name, p.provider_admin AS provider_admin
|
|
73
|
+
''',
|
|
74
|
+
token_hash=token_hash(access_token),
|
|
75
|
+
routing_='r',
|
|
76
|
+
)
|
|
77
|
+
if not records:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
80
|
+
detail='invalid access token',
|
|
81
|
+
)
|
|
82
|
+
return dict(records[0])
|
|
83
|
+
|
|
84
|
+
async def create_principal(self, actor: dict, name: str) -> PrincipalResult:
|
|
85
|
+
if not actor.get('provider_admin'):
|
|
86
|
+
raise HTTPException(status_code=403, detail='provider administrator required')
|
|
87
|
+
principal_id = str(uuid4())
|
|
88
|
+
access_token = issue_access_token()
|
|
89
|
+
await self.runtime.driver.execute_query(
|
|
90
|
+
'''
|
|
91
|
+
CREATE (p:FuliPrincipal {
|
|
92
|
+
id: $id,
|
|
93
|
+
name: $name,
|
|
94
|
+
token_hash: $token_hash,
|
|
95
|
+
active: true,
|
|
96
|
+
provider_admin: false,
|
|
97
|
+
created_at: $created_at
|
|
98
|
+
})
|
|
99
|
+
''',
|
|
100
|
+
id=principal_id,
|
|
101
|
+
name=name,
|
|
102
|
+
token_hash=token_hash(access_token),
|
|
103
|
+
created_at=now_utc(),
|
|
104
|
+
)
|
|
105
|
+
return PrincipalResult(principal_id=principal_id, access_token=access_token)
|
|
106
|
+
|
|
107
|
+
async def create_space(self, actor: dict, request: SpaceCreate) -> SpaceRecord:
|
|
108
|
+
expected_kind = 'personal' if self.settings.provider_mode == 'personal' else 'project'
|
|
109
|
+
if request.kind != expected_kind:
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
112
|
+
detail=f'{self.settings.provider_mode} provider only accepts {expected_kind} spaces',
|
|
113
|
+
)
|
|
114
|
+
if request.kind == 'project' and request.profile:
|
|
115
|
+
if any(source.sensitivity != 'normal' for source in request.profile.sources):
|
|
116
|
+
raise HTTPException(
|
|
117
|
+
status_code=422,
|
|
118
|
+
detail='private or restricted project sources cannot be published',
|
|
119
|
+
)
|
|
120
|
+
if request.kind == 'project':
|
|
121
|
+
return await publish_project(self, actor, request)
|
|
122
|
+
space_id = str(uuid4())
|
|
123
|
+
group_id = graphiti_group_id(self.settings.provider_id, request.kind, space_id)
|
|
124
|
+
created_at = now_utc()
|
|
125
|
+
visibility = 'private' if request.kind == 'personal' else 'public'
|
|
126
|
+
query = '''
|
|
127
|
+
MATCH (p:FuliPrincipal {id: $principal_id})
|
|
128
|
+
CREATE (s:FuliSpace {
|
|
129
|
+
id: $id,
|
|
130
|
+
name: $name,
|
|
131
|
+
kind: $kind,
|
|
132
|
+
group_id: $group_id,
|
|
133
|
+
description: $description,
|
|
134
|
+
visibility: $visibility,
|
|
135
|
+
publication_key: $publication_key,
|
|
136
|
+
profile_json: $profile_json,
|
|
137
|
+
created_at: $created_at
|
|
138
|
+
})
|
|
139
|
+
CREATE (p)-[:OWNS {created_at: $created_at}]->(s)
|
|
140
|
+
RETURN s
|
|
141
|
+
'''
|
|
142
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
143
|
+
query,
|
|
144
|
+
principal_id=actor['id'],
|
|
145
|
+
id=space_id,
|
|
146
|
+
name=request.name,
|
|
147
|
+
kind=request.kind,
|
|
148
|
+
group_id=group_id,
|
|
149
|
+
description=request.description,
|
|
150
|
+
visibility=visibility,
|
|
151
|
+
publication_key=request.publication_key,
|
|
152
|
+
profile_json=request.profile.model_dump_json() if request.profile else None,
|
|
153
|
+
created_at=created_at,
|
|
154
|
+
)
|
|
155
|
+
return self._space(
|
|
156
|
+
records[0]['s'],
|
|
157
|
+
owner_id=actor['id'],
|
|
158
|
+
role='maintainer',
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def list_spaces(self, actor: dict) -> list[SpaceRecord]:
|
|
162
|
+
if self.settings.provider_mode == 'personal':
|
|
163
|
+
query = '''
|
|
164
|
+
MATCH (p:FuliPrincipal {id: $principal_id})-[:OWNS]->
|
|
165
|
+
(s:FuliSpace {kind: 'personal'})
|
|
166
|
+
RETURN s, p.id AS owner_id, 'maintainer' AS role
|
|
167
|
+
ORDER BY s.created_at
|
|
168
|
+
'''
|
|
169
|
+
else:
|
|
170
|
+
query = '''
|
|
171
|
+
MATCH (s:FuliSpace {kind: 'project', visibility: 'public'})
|
|
172
|
+
MATCH (owner:FuliPrincipal)-[:OWNS]->(s)
|
|
173
|
+
OPTIONAL MATCH (actor:FuliPrincipal {id: $principal_id})-
|
|
174
|
+
[membership:MEMBER_OF]->(s)
|
|
175
|
+
RETURN s, owner.id AS owner_id,
|
|
176
|
+
coalesce(membership.role, 'reader') AS role
|
|
177
|
+
ORDER BY s.created_at
|
|
178
|
+
'''
|
|
179
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
180
|
+
query,
|
|
181
|
+
principal_id=actor['id'],
|
|
182
|
+
routing_='r',
|
|
183
|
+
)
|
|
184
|
+
return [
|
|
185
|
+
self._space(
|
|
186
|
+
record['s'],
|
|
187
|
+
owner_id=record['owner_id'],
|
|
188
|
+
role=record['role'],
|
|
189
|
+
can_manage=(
|
|
190
|
+
bool(actor.get('provider_admin')) or record['owner_id'] == actor['id']
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
for record in records
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
async def authorize(self, actor: dict, space_id: str, required_role: str) -> dict:
|
|
197
|
+
if self.settings.provider_mode == 'personal':
|
|
198
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
199
|
+
'''
|
|
200
|
+
MATCH (principal:FuliPrincipal {id: $principal_id})-[:OWNS]->
|
|
201
|
+
(space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
202
|
+
RETURN space, 'maintainer' AS role
|
|
203
|
+
''',
|
|
204
|
+
principal_id=actor['id'],
|
|
205
|
+
space_id=space_id,
|
|
206
|
+
routing_='r',
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
210
|
+
'''
|
|
211
|
+
MATCH (space:FuliSpace {
|
|
212
|
+
id: $space_id,
|
|
213
|
+
kind: 'project',
|
|
214
|
+
visibility: 'public'
|
|
215
|
+
})
|
|
216
|
+
OPTIONAL MATCH (principal:FuliPrincipal {id: $principal_id})-
|
|
217
|
+
[membership:MEMBER_OF]->(space)
|
|
218
|
+
RETURN space, coalesce(membership.role, 'reader') AS role
|
|
219
|
+
''',
|
|
220
|
+
principal_id=actor['id'],
|
|
221
|
+
space_id=space_id,
|
|
222
|
+
routing_='r',
|
|
223
|
+
)
|
|
224
|
+
if not records:
|
|
225
|
+
raise HTTPException(status_code=403, detail='space access denied')
|
|
226
|
+
role = records[0]['role']
|
|
227
|
+
if ROLE_RANK.get(role, 0) < ROLE_RANK[required_role]:
|
|
228
|
+
raise HTTPException(status_code=403, detail=f'{required_role} role required')
|
|
229
|
+
return dict(records[0]['space'])
|
|
230
|
+
|
|
231
|
+
def _require_personal(self) -> None:
|
|
232
|
+
if self.settings.provider_mode != 'personal':
|
|
233
|
+
raise HTTPException(status_code=404, detail='personal provider endpoint')
|
|
234
|
+
|
|
235
|
+
def _require_workspace(self) -> None:
|
|
236
|
+
if self.settings.provider_mode != 'workspace':
|
|
237
|
+
raise HTTPException(status_code=404, detail='workspace provider endpoint')
|