Skip to content

Commit 927de67

Browse files
authored
fix(nexus/graph): query instance detail across all selected graphs (#1051)
The Composer's instance inspector returned empty detail when an individual's triples were split across multiple selected graphs. The whole pipeline was single-graph: InstanceDrawer looped over the selected graphs and stopped at the first one returning any triple (e.g. the graph holding rdf:type), so data properties and relations living in another graph were never fetched. Make instance-detail span every selected graph in one request: - DiscoveryInstanceDetailRequest accepts graph_uris: list[str] (graph_uri kept for back-compat); a model_validator merges + dedupes and errors if none given. - _discover_instance_detail_sync expands all four VALUES ?g {...} clauses to range over every graph; add relation dedup since multi-graph can surface the same triple from two graphs. - FastAPI handler forwards payload.graph_uris. - fetchInstanceDetail takes graphUris: string[] and posts graph_uris. - InstanceDrawer makes one multi-graph call instead of the first-wins loop.
1 parent 6ec420e commit 927de67

5 files changed

Lines changed: 60 additions & 32 deletions

File tree

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/services/graph/adapters/primary/graph__primary_adapter__FastAPI.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,7 @@ async def discovery_instance_detail(
939939
try:
940940
detail = await graph_service.discover_instance_detail(
941941
workspace_id=payload.workspace_id,
942-
graph_uri=payload.graph_uri,
942+
graph_uris=payload.graph_uris,
943943
instance_uri=payload.instance_uri,
944944
)
945945
except Exception as exc:

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/services/graph/adapters/primary/graph__primary_adapter__schemas.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44
from typing import Any
55

6-
from pydantic import BaseModel, Field
6+
from pydantic import BaseModel, Field, model_validator
77

88

99
class GraphInfo(BaseModel):
@@ -244,9 +244,25 @@ class DiscoveryInstanceDetail(BaseModel):
244244

245245
class DiscoveryInstanceDetailRequest(BaseModel):
246246
workspace_id: str = Field(..., min_length=1, max_length=100)
247-
graph_uri: str = Field(..., min_length=1)
247+
# An instance's triples can be split across several named graphs (e.g. its
248+
# rdf:type in one graph, its data properties in another). Pass every selected
249+
# graph in `graph_uris` so the detail query spans them all. `graph_uri` is
250+
# kept for back-compat with older callers that send a single graph.
251+
graph_uri: str | None = Field(default=None, min_length=1)
252+
graph_uris: list[str] = Field(default_factory=list)
248253
instance_uri: str = Field(..., min_length=1)
249254

255+
@model_validator(mode="after")
256+
def _merge_graph_uris(self) -> DiscoveryInstanceDetailRequest:
257+
merged: list[str] = []
258+
for g in [*self.graph_uris, *([self.graph_uri] if self.graph_uri else [])]:
259+
if g and g not in merged:
260+
merged.append(g)
261+
if not merged:
262+
raise ValueError("at least one of graph_uri or graph_uris is required")
263+
self.graph_uris = merged
264+
return self
265+
250266

251267
class DiscoveryRelationType(BaseModel):
252268
uri: str

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/services/graph/service.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2747,21 +2747,25 @@ def _discover_instances_sync(
27472747
async def discover_instance_detail(
27482748
self,
27492749
workspace_id: str,
2750-
graph_uri: str,
2750+
graph_uris: list[str],
27512751
instance_uri: str,
27522752
) -> DiscoveryInstanceDetailData:
27532753
return await asyncio.to_thread(
2754-
self._discover_instance_detail_sync, workspace_id, graph_uri, instance_uri
2754+
self._discover_instance_detail_sync, workspace_id, graph_uris, instance_uri
27552755
)
27562756

27572757
def _discover_instance_detail_sync(
27582758
self,
27592759
workspace_id: str,
2760-
graph_uri: str,
2760+
graph_uris: list[str],
27612761
instance_uri: str,
27622762
) -> DiscoveryInstanceDetailData:
27632763
store = self._get_triple_store()
27642764

2765+
# The instance's triples may be spread across several named graphs, so every
2766+
# sub-query below ranges over all selected graphs via this VALUES list.
2767+
graph_values = " ".join(f"<{g}>" for g in graph_uris)
2768+
27652769
# Label and class
27662770
label = _get_ontology_label(store, instance_uri) or _uri_fragment(instance_uri)
27672771
class_uri = ""
@@ -2770,7 +2774,7 @@ def _discover_instance_detail_sync(
27702774
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
27712775
PREFIX owl: <http://www.w3.org/2002/07/owl#>
27722776
SELECT ?cls WHERE {{
2773-
VALUES ?g {{ <{graph_uri}> }}
2777+
VALUES ?g {{ {graph_values} }}
27742778
GRAPH ?g {{
27752779
<{instance_uri}> rdf:type ?cls .
27762780
FILTER(isIRI(?cls))
@@ -2796,7 +2800,7 @@ def _discover_instance_detail_sync(
27962800
seen_data_triples: set[tuple[str, str]] = set()
27972801
dp_query = f"""
27982802
SELECT ?p ?o WHERE {{
2799-
VALUES ?g {{ <{graph_uri}> }}
2803+
VALUES ?g {{ {graph_values} }}
28002804
GRAPH ?g {{
28012805
<{instance_uri}> ?p ?o .
28022806
FILTER(isLiteral(?o))
@@ -2831,11 +2835,12 @@ def _discover_instance_detail_sync(
28312835

28322836
# Outgoing relations (instance is domain/subject)
28332837
inspector_relations: list[DiscoveryInspectorRelation] = []
2838+
seen_relations: set[tuple[str, str, str]] = set()
28342839
out_query = f"""
28352840
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
28362841
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
28372842
SELECT ?p ?pLabel ?o ?oLabel WHERE {{
2838-
VALUES ?g {{ <{graph_uri}> }}
2843+
VALUES ?g {{ {graph_values} }}
28392844
GRAPH ?g {{
28402845
<{instance_uri}> ?p ?o .
28412846
FILTER(?p != rdf:type)
@@ -2855,6 +2860,10 @@ def _discover_instance_detail_sync(
28552860
continue
28562861
pred_uri = str(p)
28572862
other_uri = str(o)
2863+
rel_key = ("domain", pred_uri, other_uri)
2864+
if rel_key in seen_relations:
2865+
continue
2866+
seen_relations.add(rel_key)
28582867
p_label_val = getattr(row, "pLabel", None)
28592868
o_label_val = getattr(row, "oLabel", None)
28602869
inspector_relations.append(
@@ -2878,7 +2887,7 @@ def _discover_instance_detail_sync(
28782887
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
28792888
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
28802889
SELECT ?s ?sLabel ?p ?pLabel WHERE {{
2881-
VALUES ?g {{ <{graph_uri}> }}
2890+
VALUES ?g {{ {graph_values} }}
28822891
GRAPH ?g {{
28832892
?s ?p <{instance_uri}> .
28842893
FILTER(?p != rdf:type)
@@ -2898,6 +2907,10 @@ def _discover_instance_detail_sync(
28982907
continue
28992908
pred_uri = str(p)
29002909
other_uri = str(s)
2910+
rel_key = ("range", pred_uri, other_uri)
2911+
if rel_key in seen_relations:
2912+
continue
2913+
seen_relations.add(rel_key)
29012914
p_label_val = getattr(row, "pLabel", None)
29022915
s_label_val = getattr(row, "sLabel", None)
29032916
inspector_relations.append(

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/components/graph/explore/InstanceDrawer.tsx

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface InstanceDrawerProps {
99
workspaceId: string
1010
/** The inspect history stack of individual IRIs; the last entry is the one shown. */
1111
trail: string[]
12-
/** Graphs to look in (the Composer's selected graphs); the first that has it wins. */
12+
/** Graphs to look in (the Composer's selected graphs); the detail query unions across them all. */
1313
graphs: string[]
1414
onClose: () => void
1515
/** Escalate to the full Individuals page for this individual. */
@@ -29,7 +29,8 @@ const STORAGE_KEY = 'composer-inspect-width-v2'
2929
* Side panel that shows one individual's detail (data properties + relations). It sits as a
3030
* flex sibling of the table (pushing it, not overlaying), is resizable by dragging its left
3131
* edge, and keeps a breadcrumb trail so relation hops can be walked back. The grain query only
32-
* knows a row's URI, not its graph, so we resolve the graph by trying each selected graph.
32+
* knows a row's URI, not its graph, and an individual's triples can be split across graphs, so
33+
* the detail query spans every selected graph in one request.
3334
*/
3435
export function InstanceDrawer({
3536
workspaceId,
@@ -121,23 +122,16 @@ export function InstanceDrawer({
121122
setDetail(null)
122123
setNotFound(false)
123124
;(async () => {
124-
for (const g of graphList) {
125-
try {
126-
const d = await fetchInstanceDetail({ workspaceId, graphUri: g, instanceUri: uri })
127-
if (cancelled) return
128-
const found = !!d.class_uri || d.data_properties.length > 0 || d.relations.length > 0
129-
if (found) {
130-
setDetail(d)
131-
setLoading(false)
132-
return
133-
}
134-
} catch {
135-
/* not in this graph — try the next */
136-
}
137-
}
138-
if (!cancelled) {
139-
setNotFound(true)
140-
setLoading(false)
125+
try {
126+
const d = await fetchInstanceDetail({ workspaceId, graphUris: graphList, instanceUri: uri })
127+
if (cancelled) return
128+
const found = !!d.class_uri || d.data_properties.length > 0 || d.relations.length > 0
129+
if (found) setDetail(d)
130+
else setNotFound(true)
131+
} catch {
132+
if (!cancelled) setNotFound(true)
133+
} finally {
134+
if (!cancelled) setLoading(false)
141135
}
142136
})()
143137
return () => {

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/lib/graph-query/client.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,20 @@ export interface InstanceDetail {
177177
relations: InstanceRelation[]
178178
}
179179

180-
/** POST /api/graph/discovery/instance-detail — full detail for one individual in a graph. */
180+
/**
181+
* POST /api/graph/discovery/instance-detail — full detail for one individual.
182+
* An individual's triples can be split across several named graphs (its rdf:type in
183+
* one, its data properties/relations in another), so pass every selected graph and the
184+
* backend unions across them all.
185+
*/
181186
export function fetchInstanceDetail(params: {
182187
workspaceId: string
183-
graphUri: string
188+
graphUris: string[]
184189
instanceUri: string
185190
}): Promise<InstanceDetail> {
186191
return postJson<InstanceDetail>(`/api/graph/discovery/instance-detail`, {
187192
workspace_id: params.workspaceId,
188-
graph_uri: params.graphUri,
193+
graph_uris: params.graphUris,
189194
instance_uri: params.instanceUri,
190195
})
191196
}

0 commit comments

Comments
 (0)