PyoxigraphExplorer Guide¶
This notebook demonstrates how to use PyoxigraphExplorer, a pyoxigraph-backed RDF graph explorer for the AI Atlas Nexus knowledge graph.
Overview¶
PyoxigraphExplorer provides:
- Efficient graph queries via an indexed RDF store
- Raw SPARQL access for advanced queries
- Natural language queries — translate plain English to SPARQL via an LLM
- Hybrid architecture — RDF store for traversal, Pydantic objects for data
Setup¶
from ai_atlas_nexus import AIAtlasNexus
from ai_atlas_nexus.blocks.graph_explorer import PyoxigraphExplorer, AtlasExplorer
# Load the ontology
nexus = AIAtlasNexus()
print(f"Loaded ontology with {len(nexus.get_all_risks())} risks")
[2026-05-28 16:29:51:214] - INFO - AIAtlasNexus - Created AIAtlasNexus instance. Base_dir: None
Loaded ontology with 546 risks
Part 1: Using OxigraphExplorer Standalone¶
You can create an OxigraphExplorer instance directly from a Container object:
# Create PyoxigraphExplorer instance
ox = PyoxigraphExplorer(nexus._ontology)
print("PyoxigraphExplorer initialized")
# Let's create an AtlasExplorer instance so we can compare them
atlas = AtlasExplorer(nexus._ontology)
print("AtlasExplorer initialized")
PyoxigraphExplorer initialized AtlasExplorer initialized
Basic Queries¶
# Get all classes in the knowledge graph
classes = ox.get_all_classes()
print(f"Available classes ({len(classes)}): {classes[:5]}...")
#print(classes)
Available classes (34): ['Action', 'Adapter', 'AiEval', 'AiEvalResult', 'AiTask']...
# Get all risks by collection key
risks = ox.get_all("risks")
print(f"Retrieved {len(risks)} risks via collection key 'risks'")
# Show first risk (returns typed Pydantic object)
if risks:
first_risk = risks[0]
print(f"\nFirst risk:")
print(f" Type: {type(first_risk).__name__}")
print(f" ID: {first_risk.id}")
print(f" Name: {first_risk.name}")
print(f" Taxonomy: {first_risk.isDefinedByTaxonomy}")
Retrieved 546 risks via collection key 'risks' First risk: Type: Risk ID: atlas-evasion-attack Name: Evasion attack Taxonomy: ibm-risk-atlas
# You can also request by class name (singular or plural)
risks_by_class = ox.get_all("Risk")
risks_by_class_plural = ox.get_all("Risks")
print(f"Risks by class name 'Risk': {len(risks_by_class)}")
print(f"Risks by class name 'Risks': {len(risks_by_class_plural)}")
print(f"Match collection key results: {len(risks_by_class) == len(risks) and len(risks_by_class_plural) == len(risks)}")
Risks by class name 'Risk': 546 Risks by class name 'Risks': 546 Match collection key results: True
# Get all actions
actions = ox.get_all("actions")
actions_by_class = ox.get_all("Action")
print(f"Retrieved {len(actions)} actions via collection key")
print(f"Retrieved {len(actions_by_class)} actions via class name")
print(f"Match: {len(actions) == len(actions_by_class)}")
Retrieved 1085 actions via collection key Retrieved 1085 actions via class name Match: True
# Get by ID
risk_id = risks[0].id
retrieved = ox.get_by_id(None, risk_id)
print(f"Retrieved risk by ID: {retrieved.id}")
print(f" Name: {retrieved.name}")
print(f" Match: {retrieved.id == risk_id}")
Retrieved risk by ID: atlas-evasion-attack Name: Evasion attack Match: True
# Filter by attribute
nist_actions = ox.get_by_attribute("actions", "isDefinedByTaxonomy", "nist-ai-rmf")
print(f"Actions from NIST AI RMF taxonomy: {len(nist_actions)}")
if nist_actions:
print(f"\nFirst NIST action:")
print(f" ID: {nist_actions[0].id}")
print(f" Name: {nist_actions[0].name}")
Actions from NIST AI RMF taxonomy: 212 First NIST action: ID: MG-4.3-003 Name: MG-4.3-003
# Filter by multiple criteria
filtered = ox.filter_instances(
"risks",
{
"isDefinedByTaxonomy": "ibm-risk-atlas",
"name":"Evasion attack"
}
)
print(f"Filtered risks (IBM Risk Atlas): {len(filtered)}")
Filtered risks (IBM Risk Atlas): 1
Advanced: Raw SPARQL Queries¶
The sparql_query() method gives you direct access to the RDF graph:
# Find all risks in the knowledge graph via SPARQL
sparql = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX nexus: <https://w3id.org/ai-atlas-nexus/
SELECT ?s WHERE {
?s rdf:type nexus:Risk .
}
LIMIT 5
"""
results = ox.sparql_query(sparql)
print(f"First 5 risks (raw SPARQL results):")
for r in results:
uri = r['?s']
# Extract ID from URI
risk_id = uri.split('/')[-1].rstrip('>')
print(f" {risk_id}")
First 5 risks (raw SPARQL results): credo-risk-049 credo-risk-048 credo-risk-047 credo-risk-046 credo-risk-045
# Find relationships: which actions target which risks?
sparql_relationships = """
PREFIX nexus: <https://w3id.org/ai-atlas-nexus/
SELECT ?action ?risk WHERE {
?action nexus:hasRelatedRisk ?risk .
}
LIMIT 5
"""
relationships = ox.sparql_query(sparql_relationships)
print(f"Action-Risk relationships (sample):")
for rel in relationships:
action_id = rel['?action'].split('/')[-1].rstrip('>')
risk_id = rel['?risk'].split('/')[-1].rstrip('>')
print(f" {action_id} -> {risk_id}")
Action-Risk relationships (sample): credo-act-control-042 -> credo-risk-046 credo-act-control-041 -> credo-risk-046 credo-act-control-040 -> credo-risk-046 credo-act-control-037 -> credo-risk-009 credo-act-control-037 -> credo-risk-003
Part 2: Comparing PyoxigraphExplorer with the Library¶
The AIAtlasNexus library uses AtlasExplorer internally for optimization. You can use PyoxigraphExplorer directly for the same data with graph query capabilities:
# Compare library methods with direct explorer usage
ox = PyoxigraphExplorer(nexus._ontology)
atlas = AtlasExplorer(nexus._ontology)
print("Comparing risk retrieval methods:")
print()
# Library method
lib_risks = nexus.get_all_risks()
print(f"1. nexus.get_all_risks():")
print(f" Returns {len(lib_risks)} risks")
print()
# Direct PyoxigraphExplorer
ox_risks = ox.get_all("risks")
print(f"2. PyoxigraphExplorer.get_all('risks'):")
print(f" Returns {len(ox_risks)} risks")
print()
# Direct PyoxigraphExplorer by class name
ox_risks_class = ox.get_all("Risk")
print(f"3. PyoxigraphExplorer.get_all('Risk') [by class name]:")
print(f" Returns {len(ox_risks_class)} risks")
print()
# Direct AtlasExplorer
atlas_risks = atlas.get_all("risks")
print(f"4. AtlasExplorer.get_all('risks'):")
print(f" Returns {len(atlas_risks)} risks")
print()
# Verify results match for critical lookups
print("Verification - get_by_id consistency:")
test_id = lib_risks[0].id
ox_obj = ox.get_by_id(None, test_id)
atlas_obj = atlas.get_by_id(None, test_id)
lib_obj = nexus.get_risk(id=test_id)
print(f" Risk ID: {test_id}")
print(f" PyoxigraphExplorer found: {ox_obj is not None}")
print(f" AtlasExplorer found: {atlas_obj is not None}")
print(f" Library found: {lib_obj is not None}")
print(f" All match: {ox_obj is not None and atlas_obj is not None and lib_obj is not None}")
Comparing risk retrieval methods:
1. nexus.get_all_risks():
Returns 546 risks
2. PyoxigraphExplorer.get_all('risks'):
Returns 546 risks
3. PyoxigraphExplorer.get_all('Risk') [by class name]:
Returns 546 risks
4. AtlasExplorer.get_all('risks'):
Returns 546 risks
Verification - get_by_id consistency:
Risk ID: atlas-evasion-attack
PyoxigraphExplorer found: True
AtlasExplorer found: True
Library found: True
All match: True
Part 3: Custom SPARQL queries¶
It could be suitable for adding more complex graph SPARQL queries
# Example: Complex graph query via SPARQL
# "Find all risks that are mitigated by actions in the NIST AI RMF"
sparql_complex = """
PREFIX nexus: <https://w3id.org/ai-atlas-nexus/
SELECT ?risk ?action WHERE {
?action nexus:hasRelatedRisk ?risk .
?action nexus:isDefinedByTaxonomy "nist-ai-rmf" .
}
LIMIT 10
"""
print("Complex query: Risks mitigated by NIST AI RMF actions")
results = ox.sparql_query(sparql_complex)
print(f"Found {len(results)} risk-action pairs:")
for i, rel in enumerate(results[:3], 1):
risk_id = rel['?risk'].split('/')[-1].rstrip('>')
action_id = rel['?action'].split('/')[-1].rstrip('>')
print(f" {i}. Risk: {risk_id} <- Action: {action_id}")
Complex query: Risks mitigated by NIST AI RMF actions Found 10 risk-action pairs: 1. Risk: nist-information-security <- Action: MG-4.3-003 2. Risk: nist-data-privacy <- Action: MG-4.3-003 3. Risk: nist-information-integrity <- Action: MG-4.3-002
Part 4: Natural Language Queries¶
natural_language_query() lets you query the knowledge graph in plain English. It uses an LLM inference engine to translate your question into SPARQL, then executes the query against the store and returns results in the same format as sparql_query().
How it works:
- The method builds a prompt that includes the graph schema (classes, predicates, few-shot SPARQL examples)
- The LLM generates a SPARQL SELECT query
- The query is executed on the pyoxigraph store
- Results are returned as
list[dict]— same format assparql_query()
Requirements: Any configured InferenceEngine (Ollama, RITS, WML, vLLM, HF). The examples below use Ollama with granite3.3:8b running locally.
from ai_atlas_nexus.blocks.inference import OllamaInferenceEngine
# Configure an inference engine — swap for RITSInferenceEngine, WMLInferenceEngine, etc.
# Requires Ollama running locally: https://ollama.com
engine = OllamaInferenceEngine(
model_name_or_path="granite3.3:8b",
credentials={"api_url": "http://localhost:11434"},
)
print("Inference engine ready:", type(engine).__name__)
[2026-05-28 16:29:52:540] - INFO - AIAtlasNexus - ✓ Created OLLAMA inference engine for model: granite3.3:8b, backend - DEFAULT
Inference engine ready: OllamaInferenceEngine
# Basic example: find all risks
question = "Find all risks"
results = ox.natural_language_query(question, engine)
print(f"Question: {question!r}")
print(f"Results: {len(results)} rows")
print()
# Resolve IDs back to Pydantic objects
resolved = [r for r in [ox._uri_to_pydantic(solution["?s"]) for solution in results if "?s" in solution] if r is not None]
print(f"Resolved {len(resolved)} risk objects")
if resolved:
print(f"First result: {resolved[0].id} — {resolved[0].name}")
Inferring with OLLAMA, backend - DEFAULT: 100%|██████████| 1/1 [00:07<00:00, 7.64s/it]
Question: 'Find all risks' Results: 546 rows Resolved 546 risk objects First result: credo-risk-049 — Vendor lock-in and innovation barriers (AI, 2023)
# More specific question with a filter
question = "Find all actions from the NIST AI RMF taxonomy"
results = ox.natural_language_query(question, engine)
print(f"Question: {question!r}")
print(f"Results: {len(results)} rows")
if results:
ids = [r.id for r in [ox._uri_to_pydantic(solution["?s"]) for solution in results if "?s" in solution] if r is not None]
print(f"Sample IDs: {ids}")
Inferring with OLLAMA, backend - DEFAULT: 100%|██████████| 1/1 [00:01<00:00, 1.22s/it]
Question: 'Find all actions from the NIST AI RMF taxonomy' Results: 212 rows Sample IDs: ['MG-4.3-003', 'MG-4.3-002', 'MG-4.3-001', 'MG-4.2-003', 'MG-4.2-002', 'MG-4.2-001', 'MG-4.1-007', 'MG-4.1-006', 'MG-4.1-005', 'MG-4.1-004', 'MG-4.1-003', 'MG-4.1-002', 'MG-4.1-001', 'MG-3.2-009', 'MG-3.2-008', 'MG-3.2-007', 'MG-3.2-006', 'MG-3.2-005', 'MG-3.2-004', 'MG-3.2-003', 'MG-3.2-002', 'MG-3.2-001', 'MG-3.1-005', 'MG-3.1-004', 'MG-3.1-003', 'MG-3.1-002', 'MG-3.1-001', 'MG-2.4-004', 'MG-2.4-003', 'MG-2.4-002', 'MG-2.4-001', 'MG-2.3-001', 'MG-2.2-009', 'MG-2.2-008', 'MG-2.2-007', 'MG-2.2-006', 'MG-2.2-005', 'MG-2.2-004', 'MG-2.2-003', 'MG-2.2-002', 'MG-2.2-001', 'MG-1.3-002', 'MG-1.3-001', 'MS-4.2-005', 'MS-4.2-004', 'MS-4.2-003', 'MS-4.2-002', 'MS-4.2-001', 'MS-3.3-005', 'MS-3.3-004', 'MS-3.3-003', 'MS-3.3-002', 'MS-3.3-001', 'MS-3.2-001', 'MS-2.13-001', 'MS-2.12-004', 'MS-2.12-003', 'MS-2.12-002', 'MS-2.12-001', 'MS-2.11-005', 'MS-2.11-004', 'MS-2.11-003', 'MS-2.11-002', 'MS-2.11-001', 'MS-2.10-003', 'MS-2.10-002', 'MS-2.10-001', 'MS-2.9-002', 'MS-2.9-001', 'MS-2.8-004', 'MS-2.8-003', 'MS-2.8-002', 'MS-2.8-001', 'MS-2.7-009', 'MS-2.7-008', 'MS-2.7-007', 'MS-2.7-006', 'MS-2.7-005', 'MS-2.7-004', 'MS-2.7-003', 'MS-2.7-002', 'MS-2.7-001', 'MS-2.6-007', 'MS-2.6-006', 'MS-2.6-005', 'MS-2.6-004', 'MS-2.6-003', 'MS-2.6-002', 'MS-2.6-001', 'MS-2.5-006', 'MS-2.5-005', 'MS-2.5-004', 'MS-2.5-003', 'MS-2.5-002', 'MS-2.5-001', 'MS-2.3-004', 'MS-2.3-003', 'MS-2.3-002', 'MS-2.3-001', 'MS-2.2-004', 'MS-2.2-003', 'MS-2.2-002', 'MS-2.2-001', 'MS-1.3-003', 'MS-1.3-002', 'MS-1.3-001', 'MS-1.1-009', 'MS-1.1-008', 'MS-1.1-007', 'MS-1.1-006', 'MS-1.1-005', 'MS-1.1-004', 'MS-1.1-003', 'MS-1.1-002', 'MS-1.1-001', 'MP-5.2-002', 'MP-5.2-001', 'MP-5.1-006', 'MP-5.1-005', 'MP-5.1-004', 'MP-5.1-003', 'MP-5.1-002', 'MP-5.1-001', 'MP-4.1-010', 'MP-4.1-009', 'MP-4.1-008', 'MP-4.1-007', 'MP-4.1-006', 'MP-4.1-005', 'MP-4.1-004', 'MP-4.1-003', 'MP-4.1-002', 'MP-4.1-001', 'MP-3.4-006', 'MP-3.4-005', 'MP-3.4-004', 'MP-3.4-003', 'MP-3.4-002', 'MP-3.4-001', 'MP-2.3-005', 'MP-2.3-004', 'MP-2.3-003', 'MP-2.3-002', 'MP-2.3-001', 'MP-2.2-002', 'MP-2.2-001', 'MP-2.1-002', 'MP-2.1-001', 'MP-1.2-002', 'MP-1.2-001', 'MP-1.1-004', 'MP-1.1-003', 'MP-1.1-002', 'MP-1.1-001', 'GV-6.2-007', 'GV-6.2-006', 'GV-6.2-005', 'GV-6.2-004', 'GV-6.2-003', 'GV-6.2-002', 'GV-6.2-001', 'GV-6.1-010', 'GV-6.1-009', 'GV-6.1-008', 'GV-6.1-007', 'GV-6.1-006', 'GV-6.1-005', 'GV-6.1-004', 'GV-6.1-003', 'GV-6.1-002', 'GV-6.1-001', 'GV-5.1-002', 'GV-5.1-001', 'GV-4.3-003', 'GV-4.3-002', 'GV-4.3-001', 'GV-4.2-003', 'GV-4.2-002', 'GV-4.2-001', 'GV-4.1-003', 'GV-4.1-002', 'GV-4.1-001', 'GV-3.2-005', 'GV-3.2-004', 'GV-3.2-003', 'GV-3.2-002', 'GV-3.2-001', 'GV-2.1-005', 'GV-2.1-004', 'GV-2.1-003', 'GV-2.1-002', 'GV-2.1-001', 'GV-1.7-002', 'GV-1.7-001', 'GV-1.6-003', 'GV-1.6-002', 'GV-1.6-001', 'GV-1.5-003', 'GV-1.5-002', 'GV-1.5-001', 'GV-1.4-002', 'GV-1.4-001', 'GV-1.3-007', 'GV-1.3-006', 'GV-1.3-005', 'GV-1.3-004', 'GV-1.3-003', 'GV-1.3-002', 'GV-1.3-001', 'GV-1.2-002', 'GV-1.2-001', 'GV-1.1-001']
# Relationship query: actions and the risks they address
question = "Which actions relate to which risks? Show me action-risk pairs defined by the NIST AI RMF taxonomy "
results = ox.natural_language_query(question, engine)
print(f"Question: {question!r}")
print(f"Results: {len(results)} action-risk pairs")
print()
if results:
print("Sample pairs:")
for row in results[:5]:
# Keys depend on the variable names the LLM chose
keys = list(row.keys())
vals = [row[k].split("/")[-1] for k in keys if row.get(k)]
print(f" {' -> '.join(vals)}")
Inferring with OLLAMA, backend - DEFAULT: 100%|██████████| 1/1 [00:01<00:00, 1.45s/it]
Question: 'Which actions relate to which risks? Show me action-risk pairs defined by the NIST AI RMF taxonomy ' Results: 358 action-risk pairs Sample pairs: MG-4.3-003> -> nist-information-security MG-4.3-003> -> nist-data-privacy MG-4.3-002> -> nist-information-integrity MG-4.3-002> -> nist-confabulation MG-4.3-001> -> nist-information-security
Notes on natural_language_query¶
- Result format is the same as
sparql_query(): alist[dict]where keys are SPARQL variable names and values are URI strings or literal values. Useox.get_by_id(None, id)to resolve URIs to Pydantic objects. - Graceful failure: if the LLM generates invalid SPARQL, the method logs a warning and returns
[]rather than raising an exception. - SPARQL knowledge is not required — but understanding the result structure helps when resolving objects. The variable names in results (
"s","action","risk", etc.) are chosen by the LLM and may vary. - Engine agnostic: swap
OllamaInferenceEnginefor any other supported engine (RITS, WML, vLLM, HF) without changing the call site.
Summary¶
PyoxigraphExplorer can be used alternative to AtlasExplorer that combines RDF graph capabilities with Pydantic object return types. This guide has covered:
Core Functionality:
- Basic queries:
get_all(),get_by_id(),get_by_attribute() - Multi-criteria filtering:
filter_instances(),query()with kwargs - Graph queries:
sparql_query()for complex relationships - Natural language queries:
natural_language_query()for LLM-powered SPARQL generation
Utility Methods:
get_attribute()— Retrieve single field from an objectfilter_ids_by_type()— Exclude IDs by typearrange_ids_by_type()— Organize IDs by entity typeclear_cache()— Cache management