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:
cybermaggedon 2026-03-16 14:47:37 +00:00 committed by GitHub
parent a115ec06ab
commit c387670944
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 62 deletions

View file

@ -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 = [

View file

@ -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,

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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
)