mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-21 19:21:03 +02:00
Reified triples support
This commit is contained in:
parent
e2b481072d
commit
d6d74fb2ef
6 changed files with 190 additions and 26 deletions
|
|
@ -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)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", "")))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>>"
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue