-
Notifications
You must be signed in to change notification settings - Fork 2
Add server-side BFS entity graph endpoint #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
damienriehl
wants to merge
21
commits into
dev
Choose a base branch
from
entity-graph-endpoint
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
458992a
feat: add server-side BFS entity graph endpoint
damienriehl f2bd047
fix(graph): include reverse seeAlso connections in BFS traversal
damienriehl fa6b26d
fix: use query params for graph routes to avoid greedy :path capture
JohnRDOrazio b3a04fc
fix: add input validation to build_entity_graph parameters
JohnRDOrazio bc0f56c
fix: include allValuesFrom and hasValue in reverse seeAlso discovery
JohnRDOrazio e1dfe13
fix: only compute is_root for class-type nodes in entity graph
JohnRDOrazio abba247
test: make classification assertions non-conditional and exact
JohnRDOrazio 9ae0dd4
fix: only count seeAlso edges toward per-node budget when actually added
JohnRDOrazio 964a086
test: add coverage for input validation and reverse restriction variants
JohnRDOrazio 5634ecb
test: add route-level tests for entity graph endpoints
JohnRDOrazio cab395f
test: cover remaining edge-case branches in build_entity_graph
JohnRDOrazio 7a70638
refactor: remove dead focus-node guard in build_entity_graph
JohnRDOrazio 9b2f96e
perf: use deque for BFS queues in build_entity_graph
JohnRDOrazio 38f8713
fix: prevent double-counting in total_discovered and ensure seeAlso a…
JohnRDOrazio 3eef6ba
test: fix duplicate seeAlso edge test to actually trigger dedup path
JohnRDOrazio c2644c0
refactor: move EXTERNAL_NAMESPACES to module level and deduplicate se…
JohnRDOrazio 533452a
fix(graph): delete unauthed /ontologies/{id}/classes/graph route (B1)
damienriehl 9c6bcee
fix(graph): tighten node_type/edge_type to Literal unions (H1)
damienriehl 236aad7
refactor(graph): extract seeAlso helpers to module scope (H4)
damienriehl 29868b9
fix(graph): thread label_preferences through entity graph (B2/H2/H3/H4)
damienriehl 1feeea8
test(graph): cover label_preferences threading and definition languag…
damienriehl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| """Pydantic models for the Entity Graph API.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Literal | ||
|
|
||
| from pydantic import BaseModel | ||
|
|
||
| # Node type values produced by the BFS in `OntologyService.build_entity_graph`. | ||
| # Frontend mirror: `GraphNodeType` in `lib/graph/types.ts`. | ||
| GraphNodeType = Literal[ | ||
| "focus", | ||
| "root", | ||
| "secondary_root", | ||
| "class", | ||
| "individual", | ||
| "property", | ||
| "external", | ||
| ] | ||
|
|
||
| # Edge type values produced by the BFS. Frontend mirror: `GraphEdgeType`. | ||
| GraphEdgeType = Literal[ | ||
| "subClassOf", | ||
| "equivalentClass", | ||
| "disjointWith", | ||
| "seeAlso", | ||
| ] | ||
|
|
||
|
|
||
| class GraphNode(BaseModel): | ||
| """A node in the entity graph.""" | ||
|
|
||
| id: str | ||
| label: str | ||
| iri: str | ||
| definition: str | None = None | ||
| is_focus: bool = False | ||
| is_root: bool = False | ||
| depth: int = 0 | ||
| node_type: GraphNodeType = "class" | ||
| child_count: int | None = None | ||
|
|
||
|
|
||
| class GraphEdge(BaseModel): | ||
| """An edge in the entity graph.""" | ||
|
|
||
| id: str | ||
| source: str | ||
| target: str | ||
| edge_type: GraphEdgeType | ||
| label: str | None = None | ||
|
|
||
|
|
||
| class EntityGraphResponse(BaseModel): | ||
| """Complete graph response.""" | ||
|
|
||
| focus_iri: str | ||
| focus_label: str | ||
| nodes: list[GraphNode] | ||
| edges: list[GraphEdge] | ||
| truncated: bool = False | ||
| total_concept_count: int = 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """Helpers for entity-graph BFS — extracted from `OntologyService.build_entity_graph`. | ||
|
|
||
| These functions live at module scope so they can be unit-tested directly without | ||
| constructing an `OntologyService` and a loaded ontology graph. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from rdflib import Graph, URIRef | ||
| from rdflib.namespace import OWL, RDF, RDFS | ||
|
|
||
|
|
||
| def get_see_also_targets(graph: Graph, uri: URIRef) -> list[URIRef]: | ||
| """Extract seeAlso targets from both direct triples and OWL restrictions. | ||
|
|
||
| FOLIO encodes seeAlso as ``owl:Restriction`` with ``owl:someValuesFrom`` | ||
| inside ``rdfs:subClassOf``, not as direct ``rdfs:seeAlso`` triples — both | ||
| forms are returned, deduplicated, in discovery order. | ||
| """ | ||
| seen: set[URIRef] = set() | ||
| targets: list[URIRef] = [] | ||
|
|
||
| def _add(ref: URIRef) -> None: | ||
| if ref not in seen: | ||
| seen.add(ref) | ||
| targets.append(ref) | ||
|
|
||
| # Direct rdfs:seeAlso triples | ||
| for obj in graph.objects(uri, RDFS.seeAlso): | ||
| if isinstance(obj, URIRef): | ||
| _add(obj) | ||
|
|
||
| # OWL restrictions: subClassOf -> Restriction(onProperty=seeAlso, someValuesFrom=X) | ||
| for sc in graph.objects(uri, RDFS.subClassOf): | ||
| if isinstance(sc, URIRef): | ||
| continue # Named superclass, not a restriction | ||
| # sc is a blank node (restriction) | ||
| on_prop = next(graph.objects(sc, OWL.onProperty), None) | ||
| if on_prop == RDFS.seeAlso: | ||
| for predicate in (OWL.someValuesFrom, OWL.allValuesFrom, OWL.hasValue): | ||
| for val in graph.objects(sc, predicate): | ||
| if isinstance(val, URIRef): | ||
| _add(val) | ||
|
|
||
| return targets | ||
|
|
||
|
|
||
| def get_see_also_referrers(graph: Graph, uri: URIRef) -> list[URIRef]: | ||
| """Find classes that reference ``uri`` via seeAlso (direct or restriction). | ||
|
|
||
| Reverse of :func:`get_see_also_targets`. Only returns classes (subjects with | ||
| ``rdf:type owl:Class``) so callers don't surface arbitrary blank nodes. | ||
| """ | ||
| seen: set[URIRef] = set() | ||
| referrers: list[URIRef] = [] | ||
|
|
||
| def _add(ref: URIRef) -> None: | ||
| if ref not in seen: | ||
| seen.add(ref) | ||
| referrers.append(ref) | ||
|
|
||
| # Direct reverse rdfs:seeAlso | ||
| for subj in graph.subjects(RDFS.seeAlso, uri): | ||
| if isinstance(subj, URIRef): | ||
| _add(subj) | ||
|
|
||
| # Find restrictions that reference uri via someValuesFrom/allValuesFrom/hasValue | ||
| for predicate in (OWL.someValuesFrom, OWL.allValuesFrom, OWL.hasValue): | ||
| for restriction in graph.subjects(predicate, uri): | ||
| on_prop = next(graph.objects(restriction, OWL.onProperty), None) | ||
| if on_prop == RDFS.seeAlso: | ||
| for cls in graph.subjects(RDFS.subClassOf, restriction): | ||
| if isinstance(cls, URIRef) and (cls, RDF.type, OWL.Class) in graph: | ||
| _add(cls) | ||
|
|
||
| return referrers | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make direct and restriction referrer filtering agree.
The direct reverse branch adds any
URIRef, while the restriction branch only returnsowl:Classsubjects. That means the graph shape changes based on RDF encoding for the sameseeAlsolink, and it also contradicts this helper’s docstring. Either filter the direct path to classes too, or relax the contract/tests in both places.Suggested fix
# Direct reverse rdfs:seeAlso for subj in graph.subjects(RDFS.seeAlso, uri): - if isinstance(subj, URIRef): + if isinstance(subj, URIRef) and (subj, RDF.type, OWL.Class) in graph: _add(subj)🤖 Prompt for AI Agents