mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 00:16:23 +02:00
Fix incorrect property names in explainability (#698)
Remove type suffixes from explainability dataclass fields + fix show_explain_trace Rename dataclass fields to match KG property naming conventions: - Analysis: thought_uri/observation_uri → thought/observation - Synthesis/Conclusion/Reflection: document_uri → document Fix show_explain_trace for current API: - Resolve document content via librarian fetch instead of removed inline content fields (synthesis.content, conclusion.answer) - Add Grounding display for DocRAG traces - Update fetch_docrag_trace chain: Question → Grounding → Exploration → Synthesis - Pass api/explain_client to all print functions for content resolution Update all CLI tools and tests for renamed fields.
This commit is contained in:
parent
a115ec06ab
commit
c387670944
6 changed files with 122 additions and 62 deletions
|
|
@ -142,7 +142,7 @@ class TestExplainEntityFromTriples:
|
|||
]
|
||||
entity = ExplainEntity.from_triples("urn:syn:1", triples)
|
||||
assert isinstance(entity, Synthesis)
|
||||
assert entity.document_uri == "urn:doc:answer-1"
|
||||
assert entity.document == "urn:doc:answer-1"
|
||||
|
||||
def test_synthesis_no_document(self):
|
||||
triples = [
|
||||
|
|
@ -150,7 +150,7 @@ class TestExplainEntityFromTriples:
|
|||
]
|
||||
entity = ExplainEntity.from_triples("urn:syn:2", triples)
|
||||
assert isinstance(entity, Synthesis)
|
||||
assert entity.document_uri == ""
|
||||
assert entity.document == ""
|
||||
|
||||
def test_reflection_thought(self):
|
||||
triples = [
|
||||
|
|
@ -161,7 +161,7 @@ class TestExplainEntityFromTriples:
|
|||
entity = ExplainEntity.from_triples("urn:ref:1", triples)
|
||||
assert isinstance(entity, Reflection)
|
||||
assert entity.reflection_type == "thought"
|
||||
assert entity.document_uri == "urn:doc:thought-1"
|
||||
assert entity.document == "urn:doc:thought-1"
|
||||
|
||||
def test_reflection_observation(self):
|
||||
triples = [
|
||||
|
|
@ -172,7 +172,7 @@ class TestExplainEntityFromTriples:
|
|||
entity = ExplainEntity.from_triples("urn:ref:2", triples)
|
||||
assert isinstance(entity, Reflection)
|
||||
assert entity.reflection_type == "observation"
|
||||
assert entity.document_uri == "urn:doc:obs-1"
|
||||
assert entity.document == "urn:doc:obs-1"
|
||||
|
||||
def test_analysis(self):
|
||||
triples = [
|
||||
|
|
@ -186,8 +186,8 @@ class TestExplainEntityFromTriples:
|
|||
assert isinstance(entity, Analysis)
|
||||
assert entity.action == "graph-rag-query"
|
||||
assert entity.arguments == '{"query": "test"}'
|
||||
assert entity.thought_uri == "urn:ref:thought-1"
|
||||
assert entity.observation_uri == "urn:ref:obs-1"
|
||||
assert entity.thought == "urn:ref:thought-1"
|
||||
assert entity.observation == "urn:ref:obs-1"
|
||||
|
||||
def test_conclusion_with_document(self):
|
||||
triples = [
|
||||
|
|
@ -196,7 +196,7 @@ class TestExplainEntityFromTriples:
|
|||
]
|
||||
entity = ExplainEntity.from_triples("urn:conc:1", triples)
|
||||
assert isinstance(entity, Conclusion)
|
||||
assert entity.document_uri == "urn:doc:final"
|
||||
assert entity.document == "urn:doc:final"
|
||||
|
||||
def test_conclusion_no_document(self):
|
||||
triples = [
|
||||
|
|
@ -204,7 +204,7 @@ class TestExplainEntityFromTriples:
|
|||
]
|
||||
entity = ExplainEntity.from_triples("urn:conc:2", triples)
|
||||
assert isinstance(entity, Conclusion)
|
||||
assert entity.document_uri == ""
|
||||
assert entity.document == ""
|
||||
|
||||
def test_unknown_type(self):
|
||||
triples = [
|
||||
|
|
|
|||
|
|
@ -212,32 +212,32 @@ class Focus(ExplainEntity):
|
|||
@dataclass
|
||||
class Synthesis(ExplainEntity):
|
||||
"""Synthesis entity - the final answer."""
|
||||
document_uri: str = "" # Reference to librarian document
|
||||
document: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_triples(cls, uri: str, triples: List[Tuple[str, str, Any]]) -> "Synthesis":
|
||||
document_uri = ""
|
||||
document = ""
|
||||
|
||||
for s, p, o in triples:
|
||||
if p == TG_DOCUMENT:
|
||||
document_uri = o
|
||||
document = o
|
||||
|
||||
return cls(
|
||||
uri=uri,
|
||||
entity_type="synthesis",
|
||||
document_uri=document_uri
|
||||
document=document
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reflection(ExplainEntity):
|
||||
"""Reflection entity - intermediate commentary (Thought or Observation)."""
|
||||
document_uri: str = "" # Reference to content in librarian
|
||||
document: str = ""
|
||||
reflection_type: str = "" # "thought" or "observation"
|
||||
|
||||
@classmethod
|
||||
def from_triples(cls, uri: str, triples: List[Tuple[str, str, Any]]) -> "Reflection":
|
||||
document_uri = ""
|
||||
document = ""
|
||||
reflection_type = ""
|
||||
|
||||
types = [o for s, p, o in triples if p == RDF_TYPE]
|
||||
|
|
@ -249,12 +249,12 @@ class Reflection(ExplainEntity):
|
|||
|
||||
for s, p, o in triples:
|
||||
if p == TG_DOCUMENT:
|
||||
document_uri = o
|
||||
document = o
|
||||
|
||||
return cls(
|
||||
uri=uri,
|
||||
entity_type="reflection",
|
||||
document_uri=document_uri,
|
||||
document=document,
|
||||
reflection_type=reflection_type
|
||||
)
|
||||
|
||||
|
|
@ -264,15 +264,15 @@ class Analysis(ExplainEntity):
|
|||
"""Analysis entity - one think/act/observe cycle (Agent only)."""
|
||||
action: str = ""
|
||||
arguments: str = "" # JSON string
|
||||
thought_uri: str = "" # URI of thought sub-entity
|
||||
observation_uri: str = "" # URI of observation sub-entity
|
||||
thought: str = ""
|
||||
observation: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_triples(cls, uri: str, triples: List[Tuple[str, str, Any]]) -> "Analysis":
|
||||
action = ""
|
||||
arguments = ""
|
||||
thought_uri = ""
|
||||
observation_uri = ""
|
||||
thought = ""
|
||||
observation = ""
|
||||
|
||||
for s, p, o in triples:
|
||||
if p == TG_ACTION:
|
||||
|
|
@ -280,37 +280,37 @@ class Analysis(ExplainEntity):
|
|||
elif p == TG_ARGUMENTS:
|
||||
arguments = o
|
||||
elif p == TG_THOUGHT:
|
||||
thought_uri = o
|
||||
thought = o
|
||||
elif p == TG_OBSERVATION:
|
||||
observation_uri = o
|
||||
observation = o
|
||||
|
||||
return cls(
|
||||
uri=uri,
|
||||
entity_type="analysis",
|
||||
action=action,
|
||||
arguments=arguments,
|
||||
thought_uri=thought_uri,
|
||||
observation_uri=observation_uri
|
||||
thought=thought,
|
||||
observation=observation
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conclusion(ExplainEntity):
|
||||
"""Conclusion entity - final answer (Agent only)."""
|
||||
document_uri: str = "" # Reference to librarian document
|
||||
document: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_triples(cls, uri: str, triples: List[Tuple[str, str, Any]]) -> "Conclusion":
|
||||
document_uri = ""
|
||||
document = ""
|
||||
|
||||
for s, p, o in triples:
|
||||
if p == TG_DOCUMENT:
|
||||
document_uri = o
|
||||
document = o
|
||||
|
||||
return cls(
|
||||
uri=uri,
|
||||
entity_type="conclusion",
|
||||
document_uri=document_uri
|
||||
document=document
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -782,8 +782,8 @@ class ExplainabilityClient:
|
|||
"""
|
||||
Fetch the complete DocumentRAG trace starting from a question URI.
|
||||
|
||||
Follows the provenance chain: Question -> Exploration -> Synthesis
|
||||
(No Focus step for DocRAG since it doesn't do edge selection)
|
||||
Follows the provenance chain:
|
||||
Question -> Grounding -> Exploration -> Synthesis
|
||||
|
||||
Args:
|
||||
question_uri: The question entity URI
|
||||
|
|
@ -794,13 +794,14 @@ class ExplainabilityClient:
|
|||
max_content: Maximum content length for synthesis
|
||||
|
||||
Returns:
|
||||
Dict with question, exploration, synthesis entities
|
||||
Dict with question, grounding, exploration, synthesis entities
|
||||
"""
|
||||
if graph is None:
|
||||
graph = "urn:graph:retrieval"
|
||||
|
||||
trace = {
|
||||
"question": None,
|
||||
"grounding": None,
|
||||
"exploration": None,
|
||||
"synthesis": None,
|
||||
}
|
||||
|
|
@ -811,8 +812,8 @@ class ExplainabilityClient:
|
|||
return trace
|
||||
trace["question"] = question
|
||||
|
||||
# Find exploration: ?exploration prov:wasGeneratedBy question_uri
|
||||
exploration_triples = self.flow.triples_query(
|
||||
# Find grounding: ?grounding prov:wasGeneratedBy question_uri
|
||||
grounding_triples = self.flow.triples_query(
|
||||
p=PROV_WAS_GENERATED_BY,
|
||||
o=question_uri,
|
||||
g=graph,
|
||||
|
|
@ -821,6 +822,30 @@ class ExplainabilityClient:
|
|||
limit=10
|
||||
)
|
||||
|
||||
if grounding_triples:
|
||||
grounding_uris = [
|
||||
extract_term_value(t.get("s", {}))
|
||||
for t in grounding_triples
|
||||
]
|
||||
for gnd_uri in grounding_uris:
|
||||
grounding = self.fetch_entity(gnd_uri, graph, user, collection)
|
||||
if isinstance(grounding, Grounding):
|
||||
trace["grounding"] = grounding
|
||||
break
|
||||
|
||||
if not trace["grounding"]:
|
||||
return trace
|
||||
|
||||
# Find exploration: ?exploration prov:wasDerivedFrom grounding_uri
|
||||
exploration_triples = self.flow.triples_query(
|
||||
p=PROV_WAS_DERIVED_FROM,
|
||||
o=trace["grounding"].uri,
|
||||
g=graph,
|
||||
user=user,
|
||||
collection=collection,
|
||||
limit=10
|
||||
)
|
||||
|
||||
if exploration_triples:
|
||||
exploration_uris = [
|
||||
extract_term_value(t.get("s", {}))
|
||||
|
|
@ -836,7 +861,6 @@ class ExplainabilityClient:
|
|||
return trace
|
||||
|
||||
# Find synthesis: ?synthesis prov:wasDerivedFrom exploration_uri
|
||||
# (DocRAG goes directly from exploration to synthesis, no focus step)
|
||||
synthesis_triples = self.flow.triples_query(
|
||||
p=PROV_WAS_DERIVED_FROM,
|
||||
o=trace["exploration"].uri,
|
||||
|
|
|
|||
|
|
@ -204,15 +204,15 @@ def question_explainable(
|
|||
print(f"\n [iteration] {prov_id}", file=sys.stderr)
|
||||
if entity.action:
|
||||
print(f" Action: {entity.action}", file=sys.stderr)
|
||||
if entity.thought_uri:
|
||||
print(f" Thought: {entity.thought_uri}", file=sys.stderr)
|
||||
if entity.observation_uri:
|
||||
print(f" Observation: {entity.observation_uri}", file=sys.stderr)
|
||||
if entity.thought:
|
||||
print(f" Thought: {entity.thought}", file=sys.stderr)
|
||||
if entity.observation:
|
||||
print(f" Observation: {entity.observation}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Conclusion):
|
||||
print(f"\n [conclusion] {prov_id}", file=sys.stderr)
|
||||
if entity.document_uri:
|
||||
print(f" Document: {entity.document_uri}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
else:
|
||||
if debug:
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ def question_explainable(
|
|||
|
||||
elif isinstance(entity, Synthesis):
|
||||
print(f"\n [synthesis] {prov_id}", file=sys.stderr)
|
||||
if entity.document_uri:
|
||||
print(f" Document: {entity.document_uri}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
else:
|
||||
if debug:
|
||||
|
|
|
|||
|
|
@ -728,8 +728,8 @@ def _question_explainable_api(
|
|||
|
||||
elif isinstance(entity, Synthesis):
|
||||
print(f"\n [synthesis] {prov_id}", file=sys.stderr)
|
||||
if entity.document_uri:
|
||||
print(f" Document: {entity.document_uri}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
else:
|
||||
if debug:
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ def format_provenance_chain(chain):
|
|||
return " -> ".join(labels)
|
||||
|
||||
|
||||
def print_graphrag_text(trace, explain_client, flow, user, collection, show_provenance=False):
|
||||
def print_graphrag_text(trace, explain_client, flow, user, collection, api=None, show_provenance=False):
|
||||
"""Print GraphRAG trace in text format."""
|
||||
question = trace.get("question")
|
||||
|
||||
|
|
@ -224,17 +224,24 @@ def print_graphrag_text(trace, explain_client, flow, user, collection, show_prov
|
|||
print("--- Synthesis ---")
|
||||
synthesis = trace.get("synthesis")
|
||||
if synthesis:
|
||||
if synthesis.content:
|
||||
content = ""
|
||||
if synthesis.document and api:
|
||||
content = explain_client.fetch_document_content(
|
||||
synthesis.document, api, user
|
||||
)
|
||||
if content:
|
||||
print("Answer:")
|
||||
for line in synthesis.content.split("\n"):
|
||||
for line in content.split("\n"):
|
||||
print(f" {line}")
|
||||
elif synthesis.document:
|
||||
print(f"Document: {synthesis.document}")
|
||||
else:
|
||||
print("No answer content found")
|
||||
else:
|
||||
print("No synthesis data found")
|
||||
|
||||
|
||||
def print_docrag_text(trace):
|
||||
def print_docrag_text(trace, explain_client, api, user):
|
||||
"""Print DocRAG trace in text format."""
|
||||
question = trace.get("question")
|
||||
|
||||
|
|
@ -247,6 +254,13 @@ def print_docrag_text(trace):
|
|||
print(f"Time: {question.timestamp}")
|
||||
print()
|
||||
|
||||
# Grounding
|
||||
grounding = trace.get("grounding")
|
||||
if grounding:
|
||||
print("--- Grounding ---")
|
||||
print(f"Concepts: {', '.join(grounding.concepts)}")
|
||||
print()
|
||||
|
||||
# Exploration
|
||||
print("--- Exploration ---")
|
||||
exploration = trace.get("exploration")
|
||||
|
|
@ -260,17 +274,24 @@ def print_docrag_text(trace):
|
|||
print("--- Synthesis ---")
|
||||
synthesis = trace.get("synthesis")
|
||||
if synthesis:
|
||||
if synthesis.content:
|
||||
content = ""
|
||||
if synthesis.document and api:
|
||||
content = explain_client.fetch_document_content(
|
||||
synthesis.document, api, user
|
||||
)
|
||||
if content:
|
||||
print("Answer:")
|
||||
for line in synthesis.content.split("\n"):
|
||||
for line in content.split("\n"):
|
||||
print(f" {line}")
|
||||
elif synthesis.document:
|
||||
print(f"Document: {synthesis.document}")
|
||||
else:
|
||||
print("No answer content found")
|
||||
else:
|
||||
print("No synthesis data found")
|
||||
|
||||
|
||||
def print_agent_text(trace):
|
||||
def print_agent_text(trace, explain_client, api, user):
|
||||
"""Print Agent trace in text format."""
|
||||
question = trace.get("question")
|
||||
|
||||
|
|
@ -292,6 +313,7 @@ def print_agent_text(trace):
|
|||
print(f" Thought: {analysis.thought or 'N/A'}")
|
||||
print(f" Action: {analysis.action or 'N/A'}")
|
||||
|
||||
|
||||
if analysis.arguments:
|
||||
# Try to pretty-print JSON arguments
|
||||
try:
|
||||
|
|
@ -317,10 +339,20 @@ def print_agent_text(trace):
|
|||
# Conclusion
|
||||
print("--- Conclusion ---")
|
||||
conclusion = trace.get("conclusion")
|
||||
if conclusion and conclusion.answer:
|
||||
print("Answer:")
|
||||
for line in conclusion.answer.split("\n"):
|
||||
print(f" {line}")
|
||||
if conclusion:
|
||||
content = ""
|
||||
if conclusion.document and api:
|
||||
content = explain_client.fetch_document_content(
|
||||
conclusion.document, api, user
|
||||
)
|
||||
if content:
|
||||
print("Answer:")
|
||||
for line in content.split("\n"):
|
||||
print(f" {line}")
|
||||
elif conclusion.document:
|
||||
print(f"Document: {conclusion.document}")
|
||||
else:
|
||||
print("No conclusion recorded")
|
||||
else:
|
||||
print("No conclusion recorded")
|
||||
|
||||
|
|
@ -346,11 +378,12 @@ def trace_to_dict(trace, trace_type):
|
|||
],
|
||||
"conclusion": {
|
||||
"id": trace["conclusion"].uri,
|
||||
"answer": trace["conclusion"].answer,
|
||||
"document": trace["conclusion"].document,
|
||||
} if trace.get("conclusion") else None,
|
||||
}
|
||||
elif trace_type == "docrag":
|
||||
question = trace.get("question")
|
||||
grounding = trace.get("grounding")
|
||||
exploration = trace.get("exploration")
|
||||
synthesis = trace.get("synthesis")
|
||||
|
||||
|
|
@ -359,14 +392,17 @@ def trace_to_dict(trace, trace_type):
|
|||
"question_id": question.uri if question else None,
|
||||
"question": question.query if question else None,
|
||||
"time": question.timestamp if question else None,
|
||||
"grounding": {
|
||||
"id": grounding.uri,
|
||||
"concepts": grounding.concepts,
|
||||
} if grounding else None,
|
||||
"exploration": {
|
||||
"id": exploration.uri,
|
||||
"chunk_count": exploration.chunk_count,
|
||||
} if exploration else None,
|
||||
"synthesis": {
|
||||
"id": synthesis.uri,
|
||||
"document_uri": synthesis.document_uri,
|
||||
"answer": synthesis.content,
|
||||
"document": synthesis.document,
|
||||
} if synthesis else None,
|
||||
}
|
||||
else:
|
||||
|
|
@ -397,8 +433,7 @@ def trace_to_dict(trace, trace_type):
|
|||
} if focus else None,
|
||||
"synthesis": {
|
||||
"id": synthesis.uri,
|
||||
"document_uri": synthesis.document_uri,
|
||||
"answer": synthesis.content,
|
||||
"document": synthesis.document,
|
||||
} if synthesis else None,
|
||||
}
|
||||
|
||||
|
|
@ -496,7 +531,7 @@ def main():
|
|||
if args.format == 'json':
|
||||
print(json.dumps(trace_to_dict(trace, "agent"), indent=2))
|
||||
else:
|
||||
print_agent_text(trace)
|
||||
print_agent_text(trace, explain_client, api, args.user)
|
||||
|
||||
elif trace_type == "docrag":
|
||||
# Fetch and display DocRAG trace
|
||||
|
|
@ -512,7 +547,7 @@ def main():
|
|||
if args.format == 'json':
|
||||
print(json.dumps(trace_to_dict(trace, "docrag"), indent=2))
|
||||
else:
|
||||
print_docrag_text(trace)
|
||||
print_docrag_text(trace, explain_client, api, args.user)
|
||||
|
||||
else:
|
||||
# Fetch and display GraphRAG trace
|
||||
|
|
@ -531,6 +566,7 @@ def main():
|
|||
print_graphrag_text(
|
||||
trace, explain_client, flow,
|
||||
args.user, args.collection,
|
||||
api=api,
|
||||
show_provenance=args.show_provenance
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue