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,723 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
from graphiti_core.edges import EntityEdge
|
|
6
|
+
|
|
7
|
+
from .models import ConfirmationBasis, EntitySearchResult, SearchRequest, SearchResult
|
|
8
|
+
from .personal_project_access import authorize_personal_project
|
|
9
|
+
from .provider_values import (
|
|
10
|
+
json_object,
|
|
11
|
+
native_datetime as _native_datetime,
|
|
12
|
+
normalized_text as _normalized,
|
|
13
|
+
)
|
|
14
|
+
from .search_projection import (
|
|
15
|
+
effective_confirmation_status,
|
|
16
|
+
fact_result,
|
|
17
|
+
is_default_retrievable,
|
|
18
|
+
read_edge_epistemic_metadata,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
MIN_TERM_COVERAGE = 0.25
|
|
22
|
+
LATIN_QUERY_FILLERS = {
|
|
23
|
+
'a', 'an', 'and', 'are', 'can', 'could', 'do', 'does', 'find', 'for',
|
|
24
|
+
'from', 'give', 'how', 'i', 'in', 'is', 'it', 'me', 'of', 'on', 'please',
|
|
25
|
+
'tell', 'that', 'the', 'this', 'to', 'was', 'what', 'when', 'where', 'which',
|
|
26
|
+
'why', 'with', 'you',
|
|
27
|
+
}
|
|
28
|
+
CHINESE_QUERY_FILLERS = (
|
|
29
|
+
'请问', '麻烦', '帮忙', '帮我', '告诉我', '查找', '查询', '是什么', '在哪里',
|
|
30
|
+
'在哪儿', '为什么', '怎么样', '怎样', '怎么', '哪里', '哪个', '多少', '有没有',
|
|
31
|
+
'是否', '一下',
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def search_knowledge(store, actor: dict, request: SearchRequest) -> SearchResult:
|
|
36
|
+
spaces = [await store.authorize(actor, space_id, 'reader') for space_id in request.space_ids]
|
|
37
|
+
personal_space = await _personal_search_space(store, actor, spaces, request)
|
|
38
|
+
space_ids_by_group = {space['group_id']: space['id'] for space in spaces}
|
|
39
|
+
group_ids = [space['group_id'] for space in spaces]
|
|
40
|
+
edges = await store.runtime.graphiti.search(
|
|
41
|
+
request.query,
|
|
42
|
+
group_ids=group_ids,
|
|
43
|
+
num_results=min(max(request.limit * 8, 40), 100),
|
|
44
|
+
)
|
|
45
|
+
if not request.include_historical:
|
|
46
|
+
edges = [edge for edge in edges if edge.invalid_at is None]
|
|
47
|
+
project_scopes = {}
|
|
48
|
+
if personal_space:
|
|
49
|
+
project_scopes = await personal_project_scopes(
|
|
50
|
+
store,
|
|
51
|
+
personal_space,
|
|
52
|
+
request,
|
|
53
|
+
)
|
|
54
|
+
allowed = await personal_edge_scopes(
|
|
55
|
+
store,
|
|
56
|
+
personal_space,
|
|
57
|
+
request,
|
|
58
|
+
project_scopes,
|
|
59
|
+
request.include_personal_global,
|
|
60
|
+
[edge.uuid for edge in edges],
|
|
61
|
+
)
|
|
62
|
+
edges = [edge for edge in edges if edge.uuid in allowed]
|
|
63
|
+
epistemic = await read_edge_epistemic_metadata(store, [edge.uuid for edge in edges])
|
|
64
|
+
if personal_space:
|
|
65
|
+
epistemic = {
|
|
66
|
+
edge_id: {
|
|
67
|
+
**metadata,
|
|
68
|
+
**allowed.get(edge_id, {}),
|
|
69
|
+
}
|
|
70
|
+
for edge_id, metadata in epistemic.items()
|
|
71
|
+
}
|
|
72
|
+
if not request.include_pending:
|
|
73
|
+
edges = [
|
|
74
|
+
edge for edge in edges
|
|
75
|
+
if is_default_retrievable(epistemic.get(edge.uuid, {}))
|
|
76
|
+
]
|
|
77
|
+
names = await _entity_names(store, edges)
|
|
78
|
+
ranked_edges = sorted(
|
|
79
|
+
(
|
|
80
|
+
(
|
|
81
|
+
_ranked_relevance(
|
|
82
|
+
_edge_relevance(
|
|
83
|
+
request.query,
|
|
84
|
+
edge,
|
|
85
|
+
names,
|
|
86
|
+
position,
|
|
87
|
+
len(edges),
|
|
88
|
+
),
|
|
89
|
+
epistemic.get(edge.uuid, {}),
|
|
90
|
+
),
|
|
91
|
+
edge,
|
|
92
|
+
)
|
|
93
|
+
for position, edge in enumerate(edges)
|
|
94
|
+
),
|
|
95
|
+
key=lambda item: item[0],
|
|
96
|
+
reverse=True,
|
|
97
|
+
)
|
|
98
|
+
ranked_edges = [item for item in ranked_edges if item[0] > 0]
|
|
99
|
+
if personal_space:
|
|
100
|
+
ranked_edges = _dedupe_ranked_edges(
|
|
101
|
+
ranked_edges,
|
|
102
|
+
epistemic,
|
|
103
|
+
request.active_personal_project_id,
|
|
104
|
+
)
|
|
105
|
+
ranked_edges = ranked_edges[: request.limit]
|
|
106
|
+
entities = (
|
|
107
|
+
await _personal_entities(
|
|
108
|
+
store,
|
|
109
|
+
personal_space,
|
|
110
|
+
request,
|
|
111
|
+
project_scopes,
|
|
112
|
+
)
|
|
113
|
+
if personal_space else []
|
|
114
|
+
)
|
|
115
|
+
return SearchResult(
|
|
116
|
+
facts=[
|
|
117
|
+
fact_result(
|
|
118
|
+
edge,
|
|
119
|
+
names,
|
|
120
|
+
space_ids_by_group[edge.group_id],
|
|
121
|
+
epistemic.get(edge.uuid, {}),
|
|
122
|
+
).model_copy(
|
|
123
|
+
update={'score': score}
|
|
124
|
+
)
|
|
125
|
+
for score, edge in ranked_edges
|
|
126
|
+
],
|
|
127
|
+
entities=entities,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def personal_edge_ids(
|
|
132
|
+
store,
|
|
133
|
+
space: dict,
|
|
134
|
+
project_ids: list[str],
|
|
135
|
+
include_personal_global: bool,
|
|
136
|
+
edge_ids: list[str],
|
|
137
|
+
) -> set[str]:
|
|
138
|
+
request = SearchRequest(
|
|
139
|
+
space_ids=[space['id']],
|
|
140
|
+
query='scope',
|
|
141
|
+
personal_project_ids=project_ids,
|
|
142
|
+
include_personal_global=include_personal_global,
|
|
143
|
+
)
|
|
144
|
+
scopes = {
|
|
145
|
+
project_id: {
|
|
146
|
+
'scope_distance': 0,
|
|
147
|
+
'scope_path': [project_id],
|
|
148
|
+
'inherited': False,
|
|
149
|
+
}
|
|
150
|
+
for project_id in project_ids
|
|
151
|
+
}
|
|
152
|
+
return set(await personal_edge_scopes(
|
|
153
|
+
store,
|
|
154
|
+
space,
|
|
155
|
+
request,
|
|
156
|
+
scopes,
|
|
157
|
+
include_personal_global,
|
|
158
|
+
edge_ids,
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def personal_edge_scopes(
|
|
163
|
+
store,
|
|
164
|
+
space: dict,
|
|
165
|
+
request: SearchRequest,
|
|
166
|
+
project_scopes: dict[str, dict],
|
|
167
|
+
include_personal_global: bool,
|
|
168
|
+
edge_ids: list[str],
|
|
169
|
+
) -> dict[str, dict]:
|
|
170
|
+
if not edge_ids or (not project_scopes and not include_personal_global):
|
|
171
|
+
return {}
|
|
172
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
173
|
+
'''
|
|
174
|
+
MATCH ()-[edge:RELATES_TO {group_id: $group_id}]->()
|
|
175
|
+
WHERE edge.uuid IN $edge_ids
|
|
176
|
+
OPTIONAL MATCH (assignment:FuliKnowledgeAssignment {
|
|
177
|
+
space_id: $space_id,
|
|
178
|
+
item_kind: 'relationship',
|
|
179
|
+
item_id: edge.uuid
|
|
180
|
+
})
|
|
181
|
+
WITH edge, assignment
|
|
182
|
+
OPTIONAL MATCH (episode:Episodic {group_id: $group_id})
|
|
183
|
+
WHERE episode.uuid IN coalesce(edge.episodes, [])
|
|
184
|
+
WITH edge, assignment, collect(DISTINCT episode) AS episodes
|
|
185
|
+
OPTIONAL MATCH (:FuliSpace {id: $space_id})-[:HAS_KNOWLEDGE_REFERENCE]->
|
|
186
|
+
(reference:FuliKnowledgeProjectReference {
|
|
187
|
+
item_kind: 'relationship', item_id: edge.uuid, status: 'active'
|
|
188
|
+
})
|
|
189
|
+
RETURN edge.uuid AS id,
|
|
190
|
+
edge.fuli_profile_aspect AS profile_aspect,
|
|
191
|
+
coalesce(edge.fuli_preference_scope, 'global')
|
|
192
|
+
AS preference_scope,
|
|
193
|
+
edge.fuli_preference_project_id AS preference_project_id,
|
|
194
|
+
assignment.project_id AS assignment_project_id,
|
|
195
|
+
[episode IN episodes
|
|
196
|
+
WHERE episode.fuli_personal_project_id IS NOT NULL |
|
|
197
|
+
episode.fuli_personal_project_id] AS episode_project_ids,
|
|
198
|
+
any(episode IN episodes
|
|
199
|
+
WHERE episode.fuli_personal_project_id IS NULL)
|
|
200
|
+
AS has_global_episode,
|
|
201
|
+
collect(DISTINCT reference.project_id) AS reference_project_ids,
|
|
202
|
+
coalesce(edge.fuli_inheritance_mode, 'local_only')
|
|
203
|
+
AS inheritance_mode,
|
|
204
|
+
coalesce(edge.fuli_inherited_project_ids, [])
|
|
205
|
+
AS inherited_project_ids
|
|
206
|
+
''',
|
|
207
|
+
group_id=space['group_id'],
|
|
208
|
+
space_id=space['id'],
|
|
209
|
+
project_ids=list(project_scopes),
|
|
210
|
+
include_personal_global=include_personal_global,
|
|
211
|
+
edge_ids=edge_ids,
|
|
212
|
+
routing_='r',
|
|
213
|
+
)
|
|
214
|
+
return {
|
|
215
|
+
record['id']: scope
|
|
216
|
+
for record in records
|
|
217
|
+
if (
|
|
218
|
+
scope := _item_scope_metadata(
|
|
219
|
+
record,
|
|
220
|
+
request,
|
|
221
|
+
project_scopes,
|
|
222
|
+
include_personal_global,
|
|
223
|
+
)
|
|
224
|
+
) is not None
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def personal_project_scopes(
|
|
229
|
+
store,
|
|
230
|
+
space: dict,
|
|
231
|
+
request: SearchRequest,
|
|
232
|
+
) -> dict[str, dict]:
|
|
233
|
+
scopes = {
|
|
234
|
+
project_id: {
|
|
235
|
+
'scope_distance': 0,
|
|
236
|
+
'scope_path': [project_id],
|
|
237
|
+
'inherited': False,
|
|
238
|
+
}
|
|
239
|
+
for project_id in request.personal_project_ids
|
|
240
|
+
}
|
|
241
|
+
active_id = request.active_personal_project_id
|
|
242
|
+
if (
|
|
243
|
+
not active_id
|
|
244
|
+
or not request.inherit_project_knowledge
|
|
245
|
+
):
|
|
246
|
+
return scopes
|
|
247
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
248
|
+
'''
|
|
249
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
250
|
+
[:CONTAINS_PROJECT]->
|
|
251
|
+
(active:FuliPersonalProject {project_id: $active_project_id})
|
|
252
|
+
MATCH path=(active)-[:PERSONAL_PROJECT_RELATION*1..2]->
|
|
253
|
+
(source:FuliPersonalProject)
|
|
254
|
+
WHERE all(
|
|
255
|
+
relation IN relationships(path)
|
|
256
|
+
WHERE relation.relation_type IN ['PART_OF', 'USES_KNOWLEDGE_FROM']
|
|
257
|
+
)
|
|
258
|
+
AND EXISTS {
|
|
259
|
+
MATCH (space)-[:CONTAINS_PROJECT]->(source)
|
|
260
|
+
}
|
|
261
|
+
RETURN source.project_id AS project_id,
|
|
262
|
+
[node IN nodes(path) | node.project_id] AS scope_path,
|
|
263
|
+
length(path) AS scope_distance
|
|
264
|
+
ORDER BY scope_distance ASC
|
|
265
|
+
LIMIT $inheritance_limit
|
|
266
|
+
''',
|
|
267
|
+
space_id=space['id'],
|
|
268
|
+
active_project_id=active_id,
|
|
269
|
+
inheritance_limit=min(
|
|
270
|
+
int(getattr(getattr(store, 'settings', None), 'graph_limit', 500)),
|
|
271
|
+
64,
|
|
272
|
+
),
|
|
273
|
+
routing_='r',
|
|
274
|
+
)
|
|
275
|
+
for record in records:
|
|
276
|
+
project_id = record['project_id']
|
|
277
|
+
distance = int(record['scope_distance'])
|
|
278
|
+
existing = scopes.get(project_id)
|
|
279
|
+
if existing is not None and existing['scope_distance'] <= distance:
|
|
280
|
+
continue
|
|
281
|
+
path = list(record.get('scope_path') or [])
|
|
282
|
+
if len(path) != len(set(path)):
|
|
283
|
+
continue
|
|
284
|
+
scopes[project_id] = {
|
|
285
|
+
'scope_distance': distance,
|
|
286
|
+
'scope_path': path,
|
|
287
|
+
'inherited': True,
|
|
288
|
+
}
|
|
289
|
+
return scopes
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def _personal_search_space(store, actor, spaces, request):
|
|
293
|
+
personal_spaces = [space for space in spaces if space['kind'] == 'personal']
|
|
294
|
+
if not personal_spaces:
|
|
295
|
+
if request.personal_project_ids or request.include_personal_global:
|
|
296
|
+
raise HTTPException(
|
|
297
|
+
status_code=422,
|
|
298
|
+
detail='personal context can only be used with one personal space',
|
|
299
|
+
)
|
|
300
|
+
return None
|
|
301
|
+
if len(spaces) != 1:
|
|
302
|
+
raise HTTPException(
|
|
303
|
+
status_code=422,
|
|
304
|
+
detail='personal context search requires exactly one personal space',
|
|
305
|
+
)
|
|
306
|
+
space = personal_spaces[0]
|
|
307
|
+
for project_id in request.personal_project_ids:
|
|
308
|
+
await authorize_personal_project(store, actor, space, project_id)
|
|
309
|
+
return space
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def _personal_entities(
|
|
313
|
+
store,
|
|
314
|
+
space,
|
|
315
|
+
request: SearchRequest,
|
|
316
|
+
project_scopes: dict[str, dict],
|
|
317
|
+
) -> list[EntitySearchResult]:
|
|
318
|
+
if not project_scopes and not request.include_personal_global:
|
|
319
|
+
return []
|
|
320
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
321
|
+
'''
|
|
322
|
+
MATCH (node:Entity {group_id: $group_id})
|
|
323
|
+
OPTIONAL MATCH (episode:Episodic {group_id: $group_id})-[:MENTIONS]->(node)
|
|
324
|
+
WITH node, collect(DISTINCT episode) AS episodes
|
|
325
|
+
OPTIONAL MATCH (assignment:FuliKnowledgeAssignment {
|
|
326
|
+
space_id: $space_id,
|
|
327
|
+
item_kind: 'entity',
|
|
328
|
+
item_id: node.uuid
|
|
329
|
+
})
|
|
330
|
+
OPTIONAL MATCH (:FuliSpace {id: $space_id})-[:HAS_KNOWLEDGE_REFERENCE]->
|
|
331
|
+
(reference:FuliKnowledgeProjectReference {
|
|
332
|
+
item_kind: 'entity', item_id: node.uuid, status: 'active'
|
|
333
|
+
})
|
|
334
|
+
WITH node, episodes, assignment, collect(DISTINCT reference) AS references
|
|
335
|
+
WHERE ($include_historical OR node.fuli_invalid_at IS NULL)
|
|
336
|
+
AND (
|
|
337
|
+
$include_pending
|
|
338
|
+
OR coalesce(node.fuli_confirmation_status, 'pending')
|
|
339
|
+
IN ['confirmed', 'agent_confirmed']
|
|
340
|
+
)
|
|
341
|
+
AND NOT EXISTS {
|
|
342
|
+
MATCH (:FuliSpace {id: $space_id})-[:HAS_KNOWLEDGE_CONFLICT]->
|
|
343
|
+
(conflict:FuliKnowledgeConflict)
|
|
344
|
+
WHERE conflict.target_item_id = node.uuid
|
|
345
|
+
AND conflict.target_project_id IN $project_ids
|
|
346
|
+
AND conflict.status = 'resolved'
|
|
347
|
+
AND conflict.resolution = 'use_source'
|
|
348
|
+
}
|
|
349
|
+
RETURN node.uuid AS id, node.group_id AS group_id,
|
|
350
|
+
node.name AS name, coalesce(node.fuli_type, 'Entity') AS type,
|
|
351
|
+
coalesce(node.summary, '') AS summary,
|
|
352
|
+
node.created_at AS created_at,
|
|
353
|
+
coalesce(node.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
|
|
354
|
+
coalesce(node.fuli_current_quadrant, 'known_known') AS current_quadrant,
|
|
355
|
+
coalesce(node.fuli_epistemic_status, 'confirmed') AS epistemic_status,
|
|
356
|
+
coalesce(node.fuli_confirmation_status, 'pending')
|
|
357
|
+
AS confirmation_status,
|
|
358
|
+
node.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
359
|
+
node.fuli_reasoning_summary AS reasoning_summary,
|
|
360
|
+
node.fuli_profile_aspect AS profile_aspect,
|
|
361
|
+
CASE WHEN node.fuli_profile_aspect IS NULL THEN NULL
|
|
362
|
+
ELSE coalesce(node.fuli_preference_scope, 'global') END
|
|
363
|
+
AS preference_scope,
|
|
364
|
+
node.fuli_preference_project_id AS preference_project_id,
|
|
365
|
+
coalesce(node.fuli_key, node.uuid) AS key,
|
|
366
|
+
assignment.project_id AS assignment_project_id,
|
|
367
|
+
[episode IN episodes
|
|
368
|
+
WHERE episode.fuli_personal_project_id IS NOT NULL |
|
|
369
|
+
episode.fuli_personal_project_id] AS episode_project_ids,
|
|
370
|
+
any(episode IN episodes
|
|
371
|
+
WHERE episode.fuli_personal_project_id IS NULL)
|
|
372
|
+
AS has_global_episode,
|
|
373
|
+
[reference IN references | reference.project_id]
|
|
374
|
+
AS reference_project_ids,
|
|
375
|
+
coalesce(node.fuli_inheritance_mode, 'local_only')
|
|
376
|
+
AS inheritance_mode,
|
|
377
|
+
coalesce(node.fuli_inherited_project_ids, [])
|
|
378
|
+
AS inherited_project_ids,
|
|
379
|
+
coalesce(node.fuli_human_edited, false) AS human_edited,
|
|
380
|
+
coalesce(node.fuli_human_change_status, 'none')
|
|
381
|
+
AS human_change_status,
|
|
382
|
+
coalesce(node.fuli_human_change_version, 0)
|
|
383
|
+
AS human_change_version,
|
|
384
|
+
node.fuli_last_human_changed_at AS last_human_changed_at,
|
|
385
|
+
node.fuli_last_agent_viewed_at AS last_agent_viewed_at,
|
|
386
|
+
node.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
|
|
387
|
+
coalesce(node.fuli_utility_score, 0.0) AS utility_score,
|
|
388
|
+
coalesce(node.fuli_confidence_score, 0.5) AS confidence_score,
|
|
389
|
+
coalesce(node.fuli_qualified_use_count, 0)
|
|
390
|
+
AS qualified_use_count,
|
|
391
|
+
coalesce(node.fuli_distinct_task_count, 0)
|
|
392
|
+
AS distinct_task_count,
|
|
393
|
+
node.fuli_last_used_at AS last_used_at
|
|
394
|
+
ORDER BY node.name, node.uuid
|
|
395
|
+
LIMIT $candidate_limit
|
|
396
|
+
''',
|
|
397
|
+
group_id=space['group_id'],
|
|
398
|
+
space_id=space['id'],
|
|
399
|
+
project_ids=list(project_scopes),
|
|
400
|
+
include_historical=request.include_historical,
|
|
401
|
+
include_pending=request.include_pending,
|
|
402
|
+
candidate_limit=store.settings.graph_limit,
|
|
403
|
+
routing_='r',
|
|
404
|
+
)
|
|
405
|
+
scoped_records = []
|
|
406
|
+
for record in records:
|
|
407
|
+
record = dict(record)
|
|
408
|
+
effective_status = effective_confirmation_status(record)
|
|
409
|
+
if effective_status != record.get('confirmation_status'):
|
|
410
|
+
record['confidence_score'] = min(
|
|
411
|
+
float(record.get('confidence_score') or 0.5),
|
|
412
|
+
0.5,
|
|
413
|
+
)
|
|
414
|
+
record['confirmation_status'] = effective_status
|
|
415
|
+
if (
|
|
416
|
+
not request.include_pending
|
|
417
|
+
and not is_default_retrievable(record)
|
|
418
|
+
):
|
|
419
|
+
continue
|
|
420
|
+
scope = _item_scope_metadata(
|
|
421
|
+
record,
|
|
422
|
+
request,
|
|
423
|
+
project_scopes,
|
|
424
|
+
request.include_personal_global,
|
|
425
|
+
)
|
|
426
|
+
if scope is not None:
|
|
427
|
+
scoped_records.append({**record, **scope})
|
|
428
|
+
ranked = sorted(
|
|
429
|
+
(
|
|
430
|
+
(
|
|
431
|
+
record,
|
|
432
|
+
_ranked_relevance(
|
|
433
|
+
_relevance(request.query, record),
|
|
434
|
+
record,
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
for record in scoped_records
|
|
438
|
+
),
|
|
439
|
+
key=lambda item: (item[1], _timestamp(item[0].get('created_at'))),
|
|
440
|
+
reverse=True,
|
|
441
|
+
)
|
|
442
|
+
ranked = [item for item in ranked if item[1] > 0]
|
|
443
|
+
ranked = _dedupe_ranked_entities(
|
|
444
|
+
ranked,
|
|
445
|
+
request.active_personal_project_id,
|
|
446
|
+
)[: request.limit]
|
|
447
|
+
return [
|
|
448
|
+
_entity_search_result(record, score, space['id'])
|
|
449
|
+
for record, score in ranked
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _item_scope_metadata(
|
|
454
|
+
record,
|
|
455
|
+
request: SearchRequest,
|
|
456
|
+
project_scopes: dict[str, dict],
|
|
457
|
+
include_personal_global: bool,
|
|
458
|
+
) -> dict | None:
|
|
459
|
+
if not any(
|
|
460
|
+
field in record
|
|
461
|
+
for field in (
|
|
462
|
+
'profile_aspect',
|
|
463
|
+
'assignment_project_id',
|
|
464
|
+
'episode_project_ids',
|
|
465
|
+
'reference_project_ids',
|
|
466
|
+
'has_global_episode',
|
|
467
|
+
)
|
|
468
|
+
):
|
|
469
|
+
# Backward-compatible projection for older Provider clients that returned
|
|
470
|
+
# only already-filtered item IDs.
|
|
471
|
+
return _global_scope_metadata()
|
|
472
|
+
profile_aspect = record.get('profile_aspect')
|
|
473
|
+
if profile_aspect:
|
|
474
|
+
preference_scope = record.get('preference_scope') or 'global'
|
|
475
|
+
if preference_scope == 'global':
|
|
476
|
+
if not include_personal_global:
|
|
477
|
+
return None
|
|
478
|
+
return _global_scope_metadata()
|
|
479
|
+
project_id = record.get('preference_project_id')
|
|
480
|
+
if project_id not in request.personal_project_ids:
|
|
481
|
+
return None
|
|
482
|
+
return _project_scope_metadata(project_id, project_scopes[project_id])
|
|
483
|
+
|
|
484
|
+
assignment_project_id = record.get('assignment_project_id')
|
|
485
|
+
episode_project_ids = list(record.get('episode_project_ids') or [])
|
|
486
|
+
reference_project_ids = list(record.get('reference_project_ids') or [])
|
|
487
|
+
project_ids = (
|
|
488
|
+
[assignment_project_id]
|
|
489
|
+
if assignment_project_id else episode_project_ids
|
|
490
|
+
)
|
|
491
|
+
project_ids = list(dict.fromkeys([
|
|
492
|
+
*project_ids,
|
|
493
|
+
*reference_project_ids,
|
|
494
|
+
]))
|
|
495
|
+
candidates = []
|
|
496
|
+
active_id = request.active_personal_project_id
|
|
497
|
+
inheritance_mode = record.get('inheritance_mode') or 'local_only'
|
|
498
|
+
selected_project_ids = set(record.get('inherited_project_ids') or [])
|
|
499
|
+
for project_id in project_ids:
|
|
500
|
+
scope = project_scopes.get(project_id)
|
|
501
|
+
if scope is None:
|
|
502
|
+
continue
|
|
503
|
+
if scope.get('inherited'):
|
|
504
|
+
eligible = (
|
|
505
|
+
inheritance_mode == 'descendants'
|
|
506
|
+
or (
|
|
507
|
+
inheritance_mode == 'selected_projects'
|
|
508
|
+
and active_id in selected_project_ids
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
if not eligible:
|
|
512
|
+
continue
|
|
513
|
+
candidates.append((project_id, scope))
|
|
514
|
+
if candidates:
|
|
515
|
+
project_id, scope = min(
|
|
516
|
+
candidates,
|
|
517
|
+
key=lambda item: (
|
|
518
|
+
0 if item[0] == active_id else 1,
|
|
519
|
+
1 if item[1].get('inherited') else 0,
|
|
520
|
+
int(item[1].get('scope_distance') or 0),
|
|
521
|
+
item[0],
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
return _project_scope_metadata(project_id, scope)
|
|
525
|
+
if include_personal_global and record.get('has_global_episode'):
|
|
526
|
+
return _global_scope_metadata()
|
|
527
|
+
return None
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _global_scope_metadata() -> dict:
|
|
531
|
+
return {
|
|
532
|
+
'defined_project_id': None,
|
|
533
|
+
'scope_distance': 0,
|
|
534
|
+
'inherited_from_project_id': None,
|
|
535
|
+
'scope_path': [],
|
|
536
|
+
'inherited': False,
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _project_scope_metadata(project_id: str, scope: dict) -> dict:
|
|
541
|
+
inherited = scope.get('inherited') is True
|
|
542
|
+
return {
|
|
543
|
+
'defined_project_id': project_id,
|
|
544
|
+
'scope_distance': int(scope.get('scope_distance') or 0),
|
|
545
|
+
'inherited_from_project_id': project_id if inherited else None,
|
|
546
|
+
'scope_path': list(scope.get('scope_path') or [project_id]),
|
|
547
|
+
'inherited': inherited,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _ranked_relevance(base_score: float, metadata) -> float:
|
|
552
|
+
if base_score <= 0:
|
|
553
|
+
return 0.0
|
|
554
|
+
status_factor = {
|
|
555
|
+
'confirmed': 1.0,
|
|
556
|
+
'agent_confirmed': 0.88,
|
|
557
|
+
'pending': 0.72,
|
|
558
|
+
}.get(metadata.get('confirmation_status'), 0.72)
|
|
559
|
+
confidence = float(
|
|
560
|
+
metadata['confidence_score']
|
|
561
|
+
if metadata.get('confidence_score') is not None else 0.5
|
|
562
|
+
)
|
|
563
|
+
utility = float(metadata.get('utility_score') or 0)
|
|
564
|
+
scope_distance = int(metadata.get('scope_distance') or 0)
|
|
565
|
+
confidence_factor = 0.75 + min(max(confidence, 0), 1) * 0.25
|
|
566
|
+
utility_factor = 0.95 + min(max(utility, 0), 1) * 0.1
|
|
567
|
+
scope_factor = max(0.65, 1.0 - scope_distance * 0.15)
|
|
568
|
+
return round(
|
|
569
|
+
base_score
|
|
570
|
+
* status_factor
|
|
571
|
+
* confidence_factor
|
|
572
|
+
* utility_factor
|
|
573
|
+
* scope_factor,
|
|
574
|
+
4,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _scope_precedence(metadata, active_project_id: str | None) -> tuple:
|
|
579
|
+
project_id = metadata.get('defined_project_id')
|
|
580
|
+
return (
|
|
581
|
+
0 if active_project_id and project_id == active_project_id else 1,
|
|
582
|
+
1 if metadata.get('inherited_from_project_id') else 0,
|
|
583
|
+
int(metadata.get('scope_distance') or 0),
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _dedupe_ranked_edges(ranked, metadata_by_id, active_project_id):
|
|
588
|
+
selected = {}
|
|
589
|
+
for score, edge in ranked:
|
|
590
|
+
metadata = metadata_by_id.get(edge.uuid, {})
|
|
591
|
+
key = metadata.get('key') or edge.uuid
|
|
592
|
+
quality = (_scope_precedence(metadata, active_project_id), -score)
|
|
593
|
+
current = selected.get(key)
|
|
594
|
+
if current is None or quality < current[0]:
|
|
595
|
+
selected[key] = (quality, score, edge)
|
|
596
|
+
return sorted(
|
|
597
|
+
((score, edge) for _, score, edge in selected.values()),
|
|
598
|
+
key=lambda item: item[0],
|
|
599
|
+
reverse=True,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _dedupe_ranked_entities(ranked, active_project_id):
|
|
604
|
+
selected = {}
|
|
605
|
+
for record, score in ranked:
|
|
606
|
+
key = record.get('key') or record.get('id')
|
|
607
|
+
quality = (_scope_precedence(record, active_project_id), -score)
|
|
608
|
+
current = selected.get(key)
|
|
609
|
+
if current is None or quality < current[0]:
|
|
610
|
+
selected[key] = (quality, record, score)
|
|
611
|
+
return sorted(
|
|
612
|
+
((record, score) for _, record, score in selected.values()),
|
|
613
|
+
key=lambda item: (item[1], _timestamp(item[0].get('created_at'))),
|
|
614
|
+
reverse=True,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _entity_search_result(record, score: float, space_id: str) -> EntitySearchResult:
|
|
619
|
+
value = dict(record)
|
|
620
|
+
basis = json_object(value.pop('confirmation_basis_json', None))
|
|
621
|
+
created_at = _native_datetime(value.pop('created_at', None))
|
|
622
|
+
last_human_changed_at = _native_datetime(
|
|
623
|
+
value.pop('last_human_changed_at', None)
|
|
624
|
+
)
|
|
625
|
+
last_agent_viewed_at = _native_datetime(
|
|
626
|
+
value.pop('last_agent_viewed_at', None)
|
|
627
|
+
)
|
|
628
|
+
last_agent_reviewed_at = _native_datetime(
|
|
629
|
+
value.pop('last_agent_reviewed_at', None)
|
|
630
|
+
)
|
|
631
|
+
last_used_at = _native_datetime(value.pop('last_used_at', None))
|
|
632
|
+
for field in (
|
|
633
|
+
'assignment_project_id',
|
|
634
|
+
'episode_project_ids',
|
|
635
|
+
'has_global_episode',
|
|
636
|
+
'reference_project_ids',
|
|
637
|
+
'inherited',
|
|
638
|
+
):
|
|
639
|
+
value.pop(field, None)
|
|
640
|
+
return EntitySearchResult(
|
|
641
|
+
**value,
|
|
642
|
+
space_id=space_id,
|
|
643
|
+
created_at=created_at,
|
|
644
|
+
last_human_changed_at=last_human_changed_at,
|
|
645
|
+
last_agent_viewed_at=last_agent_viewed_at,
|
|
646
|
+
last_agent_reviewed_at=last_agent_reviewed_at,
|
|
647
|
+
last_used_at=last_used_at,
|
|
648
|
+
confirmation_basis=(
|
|
649
|
+
ConfirmationBasis.model_validate(basis) if basis else None
|
|
650
|
+
),
|
|
651
|
+
score=score,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
async def _entity_names(store, edges: list[EntityEdge]) -> dict[str, str]:
|
|
656
|
+
uuids = list({
|
|
657
|
+
uuid
|
|
658
|
+
for edge in edges
|
|
659
|
+
for uuid in (edge.source_node_uuid, edge.target_node_uuid)
|
|
660
|
+
})
|
|
661
|
+
if not uuids:
|
|
662
|
+
return {}
|
|
663
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
664
|
+
'MATCH (n:Entity) WHERE n.uuid IN $uuids RETURN n.uuid AS uuid, n.name AS name',
|
|
665
|
+
uuids=uuids,
|
|
666
|
+
routing_='r',
|
|
667
|
+
)
|
|
668
|
+
return {record['uuid']: record['name'] for record in records}
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _relevance(query: str, record) -> float:
|
|
672
|
+
needle = _normalized(query)
|
|
673
|
+
haystack = _normalized(' '.join(filter(None, [
|
|
674
|
+
record.get('name'), record.get('type'), record.get('summary'),
|
|
675
|
+
record.get('reasoning_summary'), record.get('profile_aspect'),
|
|
676
|
+
])))
|
|
677
|
+
if not needle:
|
|
678
|
+
return 0.0
|
|
679
|
+
score = 8.0 if needle in haystack else 0.0
|
|
680
|
+
terms = _terms(needle)
|
|
681
|
+
if terms:
|
|
682
|
+
coverage = len(terms & _terms(haystack)) / len(terms)
|
|
683
|
+
if not score and coverage < MIN_TERM_COVERAGE:
|
|
684
|
+
return 0.0
|
|
685
|
+
score += coverage * 4.0
|
|
686
|
+
if score and record.get('profile_aspect'):
|
|
687
|
+
score += 0.25
|
|
688
|
+
return round(score, 4)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _edge_relevance(query: str, edge, names: dict[str, str], position: int, total: int) -> float:
|
|
692
|
+
lexical = _relevance(query, {
|
|
693
|
+
'name': edge.name,
|
|
694
|
+
'type': 'Relationship',
|
|
695
|
+
'summary': ' '.join(filter(None, [
|
|
696
|
+
edge.fact,
|
|
697
|
+
names.get(edge.source_node_uuid),
|
|
698
|
+
names.get(edge.target_node_uuid),
|
|
699
|
+
])),
|
|
700
|
+
'reasoning_summary': None,
|
|
701
|
+
'profile_aspect': None,
|
|
702
|
+
})
|
|
703
|
+
if lexical == 0:
|
|
704
|
+
return 0.0
|
|
705
|
+
semantic_order = max(0.0, (total - position) / max(total, 1)) * 0.5
|
|
706
|
+
return round(lexical + semantic_order, 4)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _terms(value: str) -> set[str]:
|
|
710
|
+
latin = {
|
|
711
|
+
term for term in re.findall(r'[a-z0-9_]+', value)
|
|
712
|
+
if term not in LATIN_QUERY_FILLERS
|
|
713
|
+
}
|
|
714
|
+
chinese = ''.join(re.findall(r'[\u4e00-\u9fff]', value))
|
|
715
|
+
for filler in CHINESE_QUERY_FILLERS:
|
|
716
|
+
chinese = chinese.replace(filler, '')
|
|
717
|
+
grams = {chinese[index:index + 2] for index in range(max(0, len(chinese) - 1))}
|
|
718
|
+
return latin | grams
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _timestamp(value) -> float:
|
|
722
|
+
native = _native_datetime(value)
|
|
723
|
+
return native.timestamp() if isinstance(native, datetime) else 0.0
|