Reified triples support

This commit is contained in:
Cyber MacGeddon 2026-03-05 21:17:07 +00:00
parent e2b481072d
commit d6d74fb2ef
6 changed files with 190 additions and 26 deletions

View file

@ -9,26 +9,52 @@ including LLM operations, RAG queries, knowledge graph management, and more.
import json
import base64
from .. knowledge import hash, Uri, Literal
from .. schema import IRI, LITERAL
from .. knowledge import hash, Uri, Literal, QuotedTriple
from .. schema import IRI, LITERAL, TRIPLE
from . types import Triple
from . exceptions import ProtocolException
def to_value(x):
"""Convert wire format to Uri or Literal."""
"""Convert wire format to Uri, Literal, or QuotedTriple."""
if x.get("t") == IRI:
return Uri(x.get("i", ""))
elif x.get("t") == LITERAL:
return Literal(x.get("v", ""))
elif x.get("t") == TRIPLE:
# Parse the nested triple from JSON or structured data
triple_data = x.get("v", "")
if isinstance(triple_data, str):
import json
try:
triple_data = json.loads(triple_data)
except json.JSONDecodeError:
return Literal(triple_data)
if isinstance(triple_data, dict):
return QuotedTriple(
s=to_value(triple_data.get("s", {})),
p=to_value(triple_data.get("p", {})),
o=to_value(triple_data.get("o", {})),
)
return Literal(str(triple_data))
# Fallback for any other type
return Literal(x.get("v", x.get("i", "")))
def from_value(v):
"""Convert Uri or Literal to wire format."""
"""Convert Uri, Literal, or QuotedTriple to wire format."""
if isinstance(v, Uri):
return {"t": IRI, "i": str(v)}
elif isinstance(v, QuotedTriple):
import json
return {
"t": TRIPLE,
"v": json.dumps({
"s": from_value(v.s),
"p": from_value(v.p),
"o": from_value(v.o),
})
}
else:
return {"t": LITERAL, "v": str(v)}

View file

@ -9,17 +9,33 @@ into flows for use in queries and RAG operations.
import json
import base64
from .. knowledge import hash, Uri, Literal
from .. schema import IRI, LITERAL
from .. knowledge import hash, Uri, Literal, QuotedTriple
from .. schema import IRI, LITERAL, TRIPLE
from . types import Triple
def to_value(x):
"""Convert wire format to Uri or Literal."""
"""Convert wire format to Uri, Literal, or QuotedTriple."""
if x.get("t") == IRI:
return Uri(x.get("i", ""))
elif x.get("t") == LITERAL:
return Literal(x.get("v", ""))
elif x.get("t") == TRIPLE:
# Parse the nested triple from JSON or structured data
triple_data = x.get("v", "")
if isinstance(triple_data, str):
import json
try:
triple_data = json.loads(triple_data)
except json.JSONDecodeError:
return Literal(triple_data)
if isinstance(triple_data, dict):
return QuotedTriple(
s=to_value(triple_data.get("s", {})),
p=to_value(triple_data.get("p", {})),
o=to_value(triple_data.get("o", {})),
)
return Literal(str(triple_data))
# Fallback for any other type
return Literal(x.get("v", x.get("i", "")))

View file

@ -12,8 +12,8 @@ import base64
import logging
from . types import DocumentMetadata, ProcessingMetadata, Triple
from .. knowledge import hash, Uri, Literal
from .. schema import IRI, LITERAL
from .. knowledge import hash, Uri, Literal, QuotedTriple
from .. schema import IRI, LITERAL, TRIPLE
from . exceptions import *
logger = logging.getLogger(__name__)
@ -27,19 +27,45 @@ DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024
def to_value(x):
"""Convert wire format to Uri or Literal."""
"""Convert wire format to Uri, Literal, or QuotedTriple."""
if x.get("t") == IRI:
return Uri(x.get("i", ""))
elif x.get("t") == LITERAL:
return Literal(x.get("v", ""))
elif x.get("t") == TRIPLE:
# Parse the nested triple from JSON or structured data
triple_data = x.get("v", "")
if isinstance(triple_data, str):
import json
try:
triple_data = json.loads(triple_data)
except json.JSONDecodeError:
return Literal(triple_data)
if isinstance(triple_data, dict):
return QuotedTriple(
s=to_value(triple_data.get("s", {})),
p=to_value(triple_data.get("p", {})),
o=to_value(triple_data.get("o", {})),
)
return Literal(str(triple_data))
# Fallback for any other type
return Literal(x.get("v", x.get("i", "")))
def from_value(v):
"""Convert Uri or Literal to wire format."""
"""Convert Uri, Literal, or QuotedTriple to wire format."""
if isinstance(v, Uri):
return {"t": IRI, "i": str(v)}
elif isinstance(v, QuotedTriple):
import json
return {
"t": TRIPLE,
"v": json.dumps({
"s": from_value(v.s),
"p": from_value(v.p),
"o": from_value(v.o),
})
}
else:
return {"t": LITERAL, "v": str(v)}

View file

@ -26,8 +26,40 @@ KEYWORD = 'https://schema.org/keywords'
class Uri(str):
def is_uri(self): return True
def is_literal(self): return False
def is_triple(self): return False
class Literal(str):
def is_uri(self): return False
def is_literal(self): return True
def is_triple(self): return False
class QuotedTriple:
"""
RDF-star quoted triple (reification).
Represents a triple that can be used as the object of another triple,
enabling statements about statements.
Example:
# stmt:123 tg:reifies <<:Hope skos:definition "A feeling...">>
qt = QuotedTriple(
s=Uri("https://example.org/Hope"),
p=Uri("http://www.w3.org/2004/02/skos/core#definition"),
o=Literal("A feeling of expectation")
)
"""
def __init__(self, s, p, o):
self.s = s # Uri, Literal, or QuotedTriple
self.p = p # Uri
self.o = o # Uri, Literal, or QuotedTriple
def is_uri(self): return False
def is_literal(self): return False
def is_triple(self): return True
def __repr__(self):
return f"<<{self.s} {self.p} {self.o}>>"
def __str__(self):
return f"<<{self.s} {self.p} {self.o}>>"

View file

@ -1,6 +1,6 @@
"""
Connects to the graph query service and dumps all graph edges in Turtle
format.
format with RDF-star support for quoted triples.
"""
import rdflib
@ -10,11 +10,37 @@ import argparse
import os
from trustgraph.api import Api, Uri
from trustgraph.knowledge import QuotedTriple
default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/')
default_user = 'trustgraph'
default_collection = 'default'
def value_to_rdflib(val):
"""Convert a TrustGraph value to an rdflib term."""
if isinstance(val, Uri):
# Skip malformed URLs with spaces
if " " in val:
return None
return rdflib.term.URIRef(val)
elif isinstance(val, QuotedTriple):
# RDF-star quoted triple
s_term = value_to_rdflib(val.s)
p_term = value_to_rdflib(val.p)
o_term = value_to_rdflib(val.o)
if s_term is None or p_term is None or o_term is None:
return None
# rdflib 6.x+ supports Triple as a term type
try:
return rdflib.term.Triple((s_term, p_term, o_term))
except AttributeError:
# Fallback for older rdflib versions - represent as string
return rdflib.term.Literal(f"<<{val.s} {val.p} {val.o}>>")
else:
return rdflib.term.Literal(str(val))
def show_graph(url, flow_id, user, collection):
api = Api(url).flow().id(flow_id)
@ -30,18 +56,10 @@ def show_graph(url, flow_id, user, collection):
sv = rdflib.term.URIRef(row.s)
pv = rdflib.term.URIRef(row.p)
ov = value_to_rdflib(row.o)
if isinstance(row.o, Uri):
# Skip malformed URLs with spaces in
if " " in row.o:
continue
ov = rdflib.term.URIRef(row.o)
else:
ov = rdflib.term.Literal(row.o)
if ov is None:
continue
g.add((sv, pv, ov))

View file

@ -6,11 +6,13 @@ null. Output is a list of quads.
import logging
import json
from .... direct.cassandra_kg import (
EntityCentricKnowledgeGraph, GRAPH_WILDCARD, DEFAULT_GRAPH
)
from .... schema import TriplesQueryRequest, TriplesQueryResponse, Error
from .... schema import Term, Triple, IRI, LITERAL
from .... schema import Term, Triple, IRI, LITERAL, TRIPLE
from .... base import TriplesQueryService
from .... base.cassandra_config import add_cassandra_args, resolve_cassandra_config
@ -33,6 +35,36 @@ def get_term_value(term):
return term.id or term.value
def deserialize_term(term_dict):
"""Deserialize a term from JSON structure."""
if term_dict is None:
return None
term_type = term_dict.get("type", "")
if term_type == IRI:
return Term(type=IRI, iri=term_dict.get("iri", ""))
elif term_type == LITERAL:
return Term(
type=LITERAL,
value=term_dict.get("value", ""),
datatype=term_dict.get("datatype", ""),
language=term_dict.get("language", "")
)
elif term_type == TRIPLE:
# Recursive for nested triples
nested = term_dict.get("triple")
if nested:
return Term(
type=TRIPLE,
triple=Triple(
s=deserialize_term(nested.get("s")),
p=deserialize_term(nested.get("p")),
o=deserialize_term(nested.get("o")),
)
)
# Fallback
return Term(type=LITERAL, value=str(term_dict))
def create_term(value, otype=None, dtype=None, lang=None):
"""
Create a Term from a string value, optionally using type metadata.
@ -57,8 +89,22 @@ def create_term(value, otype=None, dtype=None, lang=None):
language=lang or ""
)
elif otype == 't':
# Triple/reification - treat as IRI for now
return Term(type=IRI, iri=value)
# Triple/reification - parse JSON and create nested Triple
try:
triple_data = json.loads(value) if isinstance(value, str) else value
if isinstance(triple_data, dict):
return Term(
type=TRIPLE,
triple=Triple(
s=deserialize_term(triple_data.get("s")),
p=deserialize_term(triple_data.get("p")),
o=deserialize_term(triple_data.get("o")),
)
)
except (json.JSONDecodeError, TypeError) as e:
logger.warning(f"Failed to parse triple JSON: {e}")
# Fallback if parsing fails
return Term(type=LITERAL, value=str(value))
else:
# Unknown otype, fall back to heuristic
pass