Deliver explainability triples inline in retrieval response stream

Provenance triples are now included directly in explain messages from
GraphRAG, DocumentRAG, and Agent services, eliminating the need for
follow-up knowledge graph queries to retrieve explainability details.

Each explain message in the response stream now carries:
- explain_id: root URI for this provenance step (unchanged)
- explain_graph: named graph where triples are stored (unchanged)
- explain_triples: the actual provenance triples for this step (new)

Changes across the stack:
- Schema: added explain_triples field to GraphRagResponse,
  DocumentRagResponse, and AgentResponse
- Services: all explain message call sites pass triples through
  (graph_rag, document_rag, agent react, agent orchestrator)
- Translators: encode explain_triples via TripleTranslator for
  gateway wire format
- Python SDK: ProvenanceEvent now includes parsed ExplainEntity
  and raw triples; expanded event_type detection
- CLI: invoke_graph_rag, invoke_agent, invoke_document_rag use
  inline entity when available, fall back to graph query
- Tech specs updated
This commit is contained in:
Cyber MacGeddon 2026-04-07 09:45:05 +01:00
parent 4acd853023
commit c6ef354290
15 changed files with 162 additions and 49 deletions

View file

@ -219,8 +219,8 @@ TG_ANSWER = TG + "answer"
| `trustgraph-base/trustgraph/provenance/triples.py` | Add TG types to GraphRAG triple builders, add Document RAG triple builders | | `trustgraph-base/trustgraph/provenance/triples.py` | Add TG types to GraphRAG triple builders, add Document RAG triple builders |
| `trustgraph-base/trustgraph/provenance/uris.py` | Add Document RAG URI generators | | `trustgraph-base/trustgraph/provenance/uris.py` | Add Document RAG URI generators |
| `trustgraph-base/trustgraph/provenance/__init__.py` | Export new types, predicates, and Document RAG functions | | `trustgraph-base/trustgraph/provenance/__init__.py` | Export new types, predicates, and Document RAG functions |
| `trustgraph-base/trustgraph/schema/services/retrieval.py` | Add explain_id and explain_graph to DocumentRagResponse | | `trustgraph-base/trustgraph/schema/services/retrieval.py` | Add explain_id, explain_graph, and explain_triples to DocumentRagResponse |
| `trustgraph-base/trustgraph/messaging/translators/retrieval.py` | Update DocumentRagResponseTranslator for explainability fields | | `trustgraph-base/trustgraph/messaging/translators/retrieval.py` | Update DocumentRagResponseTranslator for explainability fields including inline triples |
| `trustgraph-flow/trustgraph/agent/react/service.py` | Add explainability producer + recording logic | | `trustgraph-flow/trustgraph/agent/react/service.py` | Add explainability producer + recording logic |
| `trustgraph-flow/trustgraph/retrieval/document_rag/document_rag.py` | Add explainability callback and emit provenance triples | | `trustgraph-flow/trustgraph/retrieval/document_rag/document_rag.py` | Add explainability callback and emit provenance triples |
| `trustgraph-flow/trustgraph/retrieval/document_rag/rag.py` | Add explainability producer and wire up callback | | `trustgraph-flow/trustgraph/retrieval/document_rag/rag.py` | Add explainability producer and wire up callback |

View file

@ -63,7 +63,11 @@ Explainability events stream to client as the query executes:
3. Edges selected with reasoning → event emitted 3. Edges selected with reasoning → event emitted
4. Answer synthesized → event emitted 4. Answer synthesized → event emitted
Client receives `explain_id` and `explain_collection` to fetch full details. Client receives `explain_id`, `explain_graph`, and `explain_triples` inline
in each explain message. The triples contain the full provenance data for
that step — no follow-up graph query needed. The `explain_id` serves as
the root entity URI within the triples. Data is also written to the
knowledge graph for later audit/analysis.
## URI Structure ## URI Structure
@ -144,7 +148,8 @@ class GraphRagResponse:
response: str = "" response: str = ""
end_of_stream: bool = False end_of_stream: bool = False
explain_id: str | None = None explain_id: str | None = None
explain_collection: str | None = None explain_graph: str | None = None
explain_triples: list[Triple] = field(default_factory=list)
message_type: str = "" # "chunk" or "explain" message_type: str = "" # "chunk" or "explain"
end_of_session: bool = False end_of_session: bool = False
``` ```
@ -154,7 +159,7 @@ class GraphRagResponse:
| message_type | Purpose | | message_type | Purpose |
|--------------|---------| |--------------|---------|
| `chunk` | Response text (streaming or final) | | `chunk` | Response text (streaming or final) |
| `explain` | Explainability event with IRI reference | | `explain` | Explainability event with inline provenance triples |
### Session Lifecycle ### Session Lifecycle

View file

@ -366,19 +366,13 @@ class SocketClient:
# Handle GraphRAG/DocRAG message format with message_type # Handle GraphRAG/DocRAG message format with message_type
if message_type == "explain": if message_type == "explain":
if include_provenance: if include_provenance:
return ProvenanceEvent( return self._build_provenance_event(resp)
explain_id=resp.get("explain_id", ""),
explain_graph=resp.get("explain_graph", "")
)
return None return None
# Handle Agent message format with chunk_type="explain" # Handle Agent message format with chunk_type="explain"
if chunk_type == "explain": if chunk_type == "explain":
if include_provenance: if include_provenance:
return ProvenanceEvent( return self._build_provenance_event(resp)
explain_id=resp.get("explain_id", ""),
explain_graph=resp.get("explain_graph", "")
)
return None return None
if chunk_type == "thought": if chunk_type == "thought":
@ -413,6 +407,42 @@ class SocketClient:
error=None error=None
) )
def _build_provenance_event(self, resp: Dict[str, Any]) -> ProvenanceEvent:
"""Build a ProvenanceEvent from a response dict, parsing inline triples
into an ExplainEntity if available."""
explain_id = resp.get("explain_id", "")
explain_graph = resp.get("explain_graph", "")
raw_triples = resp.get("explain_triples", [])
entity = None
if raw_triples:
try:
from .explainability import ExplainEntity
# Convert wire-format triple dicts to (s, p, o) tuples
parsed = []
for t in raw_triples:
s = t.get("s", {}).get("i", "") if t.get("s") else ""
p = t.get("p", {}).get("i", "") if t.get("p") else ""
o_term = t.get("o", {})
if o_term:
if o_term.get("t") == "i":
o = o_term.get("i", "")
else:
o = o_term.get("v", "")
else:
o = ""
parsed.append((s, p, o))
entity = ExplainEntity.from_triples(explain_id, parsed)
except Exception:
pass
return ProvenanceEvent(
explain_id=explain_id,
explain_graph=explain_graph,
entity=entity,
triples=raw_triples,
)
def close(self) -> None: def close(self) -> None:
"""Close the persistent WebSocket connection.""" """Close the persistent WebSocket connection."""
if self._loop and not self._loop.is_closed(): if self._loop and not self._loop.is_closed():

View file

@ -213,25 +213,47 @@ class ProvenanceEvent:
""" """
Provenance event for explainability. Provenance event for explainability.
Emitted during GraphRAG queries when explainable mode is enabled. Emitted during retrieval queries when explainable mode is enabled.
Each event represents a provenance node created during query processing. Each event represents a provenance node created during query processing.
Attributes: Attributes:
explain_id: URI of the provenance node (e.g., urn:trustgraph:question:abc123) explain_id: URI of the provenance node (e.g., urn:trustgraph:question:abc123)
explain_graph: Named graph where provenance triples are stored (e.g., urn:graph:retrieval) explain_graph: Named graph where provenance triples are stored (e.g., urn:graph:retrieval)
event_type: Type of provenance event (question, exploration, focus, synthesis) event_type: Type of provenance event (question, exploration, focus, synthesis, etc.)
entity: Parsed ExplainEntity from inline triples (if available)
triples: Raw triples from the response (wire format dicts)
""" """
explain_id: str explain_id: str
explain_graph: str = "" explain_graph: str = ""
event_type: str = "" # Derived from explain_id event_type: str = "" # Derived from explain_id
entity: object = None # ExplainEntity (parsed from triples)
triples: list = dataclasses.field(default_factory=list) # Raw wire-format triple dicts
def __post_init__(self): def __post_init__(self):
# Extract event type from explain_id # Extract event type from explain_id
if "question" in self.explain_id: if "question" in self.explain_id:
self.event_type = "question" self.event_type = "question"
elif "grounding" in self.explain_id:
self.event_type = "grounding"
elif "exploration" in self.explain_id: elif "exploration" in self.explain_id:
self.event_type = "exploration" self.event_type = "exploration"
elif "focus" in self.explain_id: elif "focus" in self.explain_id:
self.event_type = "focus" self.event_type = "focus"
elif "synthesis" in self.explain_id: elif "synthesis" in self.explain_id:
self.event_type = "synthesis" self.event_type = "synthesis"
elif "iteration" in self.explain_id:
self.event_type = "iteration"
elif "observation" in self.explain_id:
self.event_type = "observation"
elif "conclusion" in self.explain_id:
self.event_type = "conclusion"
elif "decomposition" in self.explain_id:
self.event_type = "decomposition"
elif "finding" in self.explain_id:
self.event_type = "finding"
elif "plan" in self.explain_id:
self.event_type = "plan"
elif "step-result" in self.explain_id:
self.event_type = "step-result"
elif "session" in self.explain_id:
self.event_type = "session"

View file

@ -1,6 +1,7 @@
from typing import Dict, Any, Tuple from typing import Dict, Any, Tuple
from ...schema import AgentRequest, AgentResponse from ...schema import AgentRequest, AgentResponse
from .base import MessageTranslator from .base import MessageTranslator
from .primitives import TripleTranslator
class AgentRequestTranslator(MessageTranslator): class AgentRequestTranslator(MessageTranslator):
@ -50,6 +51,9 @@ class AgentRequestTranslator(MessageTranslator):
class AgentResponseTranslator(MessageTranslator): class AgentResponseTranslator(MessageTranslator):
"""Translator for AgentResponse schema objects""" """Translator for AgentResponse schema objects"""
def __init__(self):
self.triple_translator = TripleTranslator()
def decode(self, data: Dict[str, Any]) -> AgentResponse: def decode(self, data: Dict[str, Any]) -> AgentResponse:
raise NotImplementedError("Response translation to Pulsar not typically needed") raise NotImplementedError("Response translation to Pulsar not typically needed")
@ -75,6 +79,13 @@ class AgentResponseTranslator(MessageTranslator):
if explain_graph is not None: if explain_graph is not None:
result["explain_graph"] = explain_graph result["explain_graph"] = explain_graph
# Include explain_triples for explain messages
explain_triples = getattr(obj, "explain_triples", [])
if explain_triples:
result["explain_triples"] = [
self.triple_translator.encode(t) for t in explain_triples
]
# Always include error if present # Always include error if present
if hasattr(obj, 'error') and obj.error and obj.error.message: if hasattr(obj, 'error') and obj.error and obj.error.message:
result["error"] = {"message": obj.error.message, "code": obj.error.code} result["error"] = {"message": obj.error.message, "code": obj.error.code}

View file

@ -1,6 +1,7 @@
from typing import Dict, Any, Tuple from typing import Dict, Any, Tuple
from ...schema import DocumentRagQuery, DocumentRagResponse, GraphRagQuery, GraphRagResponse from ...schema import DocumentRagQuery, DocumentRagResponse, GraphRagQuery, GraphRagResponse
from .base import MessageTranslator from .base import MessageTranslator
from .primitives import TripleTranslator
class DocumentRagRequestTranslator(MessageTranslator): class DocumentRagRequestTranslator(MessageTranslator):
@ -28,6 +29,9 @@ class DocumentRagRequestTranslator(MessageTranslator):
class DocumentRagResponseTranslator(MessageTranslator): class DocumentRagResponseTranslator(MessageTranslator):
"""Translator for DocumentRagResponse schema objects""" """Translator for DocumentRagResponse schema objects"""
def __init__(self):
self.triple_translator = TripleTranslator()
def decode(self, data: Dict[str, Any]) -> DocumentRagResponse: def decode(self, data: Dict[str, Any]) -> DocumentRagResponse:
raise NotImplementedError("Response translation to Pulsar not typically needed") raise NotImplementedError("Response translation to Pulsar not typically needed")
@ -53,6 +57,13 @@ class DocumentRagResponseTranslator(MessageTranslator):
if explain_graph is not None: if explain_graph is not None:
result["explain_graph"] = explain_graph result["explain_graph"] = explain_graph
# Include explain_triples for explain messages
explain_triples = getattr(obj, "explain_triples", [])
if explain_triples:
result["explain_triples"] = [
self.triple_translator.encode(t) for t in explain_triples
]
# Include end_of_stream flag (LLM stream complete) # Include end_of_stream flag (LLM stream complete)
result["end_of_stream"] = getattr(obj, "end_of_stream", False) result["end_of_stream"] = getattr(obj, "end_of_stream", False)
@ -107,6 +118,9 @@ class GraphRagRequestTranslator(MessageTranslator):
class GraphRagResponseTranslator(MessageTranslator): class GraphRagResponseTranslator(MessageTranslator):
"""Translator for GraphRagResponse schema objects""" """Translator for GraphRagResponse schema objects"""
def __init__(self):
self.triple_translator = TripleTranslator()
def decode(self, data: Dict[str, Any]) -> GraphRagResponse: def decode(self, data: Dict[str, Any]) -> GraphRagResponse:
raise NotImplementedError("Response translation to Pulsar not typically needed") raise NotImplementedError("Response translation to Pulsar not typically needed")
@ -132,6 +146,13 @@ class GraphRagResponseTranslator(MessageTranslator):
if explain_graph is not None: if explain_graph is not None:
result["explain_graph"] = explain_graph result["explain_graph"] = explain_graph
# Include explain_triples for explain messages
explain_triples = getattr(obj, "explain_triples", [])
if explain_triples:
result["explain_triples"] = [
self.triple_translator.encode(t) for t in explain_triples
]
# Include end_of_stream flag (LLM stream complete) # Include end_of_stream flag (LLM stream complete)
result["end_of_stream"] = getattr(obj, "end_of_stream", False) result["end_of_stream"] = getattr(obj, "end_of_stream", False)

View file

@ -2,7 +2,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional
from ..core.primitives import Error from ..core.primitives import Error, Triple
############################################################################ ############################################################################
@ -57,8 +57,9 @@ class AgentResponse:
end_of_dialog: bool = False # Entire agent dialog is complete end_of_dialog: bool = False # Entire agent dialog is complete
# Explainability fields # Explainability fields
explain_id: str | None = None # Provenance URI (announced as created) explain_id: str | None = None # Root URI for this explain step
explain_graph: str | None = None # Named graph where explain was stored explain_graph: str | None = None # Named graph (e.g., urn:graph:retrieval)
explain_triples: list[Triple] = field(default_factory=list) # Provenance triples for this step
# Orchestration fields # Orchestration fields
message_id: str = "" # Unique ID for this response message message_id: str = "" # Unique ID for this response message

View file

@ -1,5 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from ..core.primitives import Error, Term from ..core.primitives import Error, Term, Triple
############################################################################ ############################################################################
@ -24,8 +24,9 @@ class GraphRagResponse:
error: Error | None = None error: Error | None = None
response: str = "" response: str = ""
end_of_stream: bool = False # LLM response stream complete end_of_stream: bool = False # LLM response stream complete
explain_id: str | None = None # Single explain URI (announced as created) explain_id: str | None = None # Root URI for this explain step
explain_graph: str | None = None # Named graph where explain was stored (e.g., urn:graph:retrieval) explain_graph: str | None = None # Named graph (e.g., urn:graph:retrieval)
explain_triples: list[Triple] = field(default_factory=list) # Provenance triples for this step
message_type: str = "" # "chunk" or "explain" message_type: str = "" # "chunk" or "explain"
end_of_session: bool = False # Entire session complete end_of_session: bool = False # Entire session complete
@ -46,7 +47,8 @@ class DocumentRagResponse:
error: Error | None = None error: Error | None = None
response: str | None = "" response: str | None = ""
end_of_stream: bool = False # LLM response stream complete end_of_stream: bool = False # LLM response stream complete
explain_id: str | None = None # Single explain URI (announced as created) explain_id: str | None = None # Root URI for this explain step
explain_graph: str | None = None # Named graph where explain was stored (e.g., urn:graph:retrieval) explain_graph: str | None = None # Named graph (e.g., urn:graph:retrieval)
explain_triples: list[Triple] = field(default_factory=list) # Provenance triples for this step
message_type: str = "" # "chunk" or "explain" message_type: str = "" # "chunk" or "explain"
end_of_session: bool = False # Entire session complete end_of_session: bool = False # Entire session complete

View file

@ -182,16 +182,18 @@ def question_explainable(
print(item.content, end="", flush=True) print(item.content, end="", flush=True)
elif isinstance(item, ProvenanceEvent): elif isinstance(item, ProvenanceEvent):
# Process provenance event immediately # Use inline entity if available, otherwise fetch from graph
prov_id = item.explain_id prov_id = item.explain_id
explain_graph = item.explain_graph or "urn:graph:retrieval" explain_graph = item.explain_graph or "urn:graph:retrieval"
entity = explain_client.fetch_entity( entity = item.entity
prov_id, if entity is None:
graph=explain_graph, entity = explain_client.fetch_entity(
user=user, prov_id,
collection=collection graph=explain_graph,
) user=user,
collection=collection
)
if entity is None: if entity is None:
if debug: if debug:

View file

@ -45,16 +45,18 @@ def question_explainable(
print(item.content, end="", flush=True) print(item.content, end="", flush=True)
elif isinstance(item, ProvenanceEvent): elif isinstance(item, ProvenanceEvent):
# Process provenance event immediately # Use inline entity if available, otherwise fetch from graph
prov_id = item.explain_id prov_id = item.explain_id
explain_graph = item.explain_graph or "urn:graph:retrieval" explain_graph = item.explain_graph or "urn:graph:retrieval"
entity = explain_client.fetch_entity( entity = item.entity
prov_id, if entity is None:
graph=explain_graph, entity = explain_client.fetch_entity(
user=user, prov_id,
collection=collection graph=explain_graph,
) user=user,
collection=collection
)
if entity is None: if entity is None:
if debug: if debug:

View file

@ -667,16 +667,18 @@ def _question_explainable_api(
print(item.content, end="", flush=True) print(item.content, end="", flush=True)
elif isinstance(item, ProvenanceEvent): elif isinstance(item, ProvenanceEvent):
# Process provenance event immediately # Use inline entity if available, otherwise fetch from graph
prov_id = item.explain_id prov_id = item.explain_id
explain_graph = item.explain_graph or "urn:graph:retrieval" explain_graph = item.explain_graph or "urn:graph:retrieval"
entity = explain_client.fetch_entity( entity = item.entity
prov_id, if entity is None:
graph=explain_graph, entity = explain_client.fetch_entity(
user=user, prov_id,
collection=collection graph=explain_graph,
) user=user,
collection=collection
)
if entity is None: if entity is None:
if debug: if debug:

View file

@ -243,6 +243,7 @@ class PatternBase:
content="", content="",
explain_id=session_uri, explain_id=session_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
async def emit_iteration_triples(self, flow, session_id, iteration_num, async def emit_iteration_triples(self, flow, session_id, iteration_num,
@ -305,6 +306,7 @@ class PatternBase:
content="", content="",
explain_id=iteration_uri, explain_id=iteration_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=iter_triples,
)) ))
async def emit_observation_triples(self, flow, session_id, iteration_num, async def emit_observation_triples(self, flow, session_id, iteration_num,
@ -360,6 +362,7 @@ class PatternBase:
content="", content="",
explain_id=observation_entity_uri, explain_id=observation_entity_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=obs_triples,
)) ))
async def emit_final_triples(self, flow, session_id, iteration_num, async def emit_final_triples(self, flow, session_id, iteration_num,
@ -416,6 +419,7 @@ class PatternBase:
content="", content="",
explain_id=final_uri, explain_id=final_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=final_triples,
)) ))
# ---- Orchestrator provenance helpers ------------------------------------ # ---- Orchestrator provenance helpers ------------------------------------
@ -437,6 +441,7 @@ class PatternBase:
await respond(AgentResponse( await respond(AgentResponse(
chunk_type="explain", content="", chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL, explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
async def emit_finding_triples( async def emit_finding_triples(
@ -475,6 +480,7 @@ class PatternBase:
await respond(AgentResponse( await respond(AgentResponse(
chunk_type="explain", content="", chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL, explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
async def emit_plan_triples( async def emit_plan_triples(
@ -494,6 +500,7 @@ class PatternBase:
await respond(AgentResponse( await respond(AgentResponse(
chunk_type="explain", content="", chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL, explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
async def emit_step_result_triples( async def emit_step_result_triples(
@ -526,6 +533,7 @@ class PatternBase:
await respond(AgentResponse( await respond(AgentResponse(
chunk_type="explain", content="", chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL, explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
async def emit_synthesis_triples( async def emit_synthesis_triples(
@ -557,6 +565,7 @@ class PatternBase:
await respond(AgentResponse( await respond(AgentResponse(
chunk_type="explain", content="", chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL, explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
# ---- Response helpers --------------------------------------------------- # ---- Response helpers ---------------------------------------------------

View file

@ -471,6 +471,7 @@ class Processor(AgentService):
content="", content="",
explain_id=session_uri, explain_id=session_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
)) ))
logger.info(f"Question: {request.question}") logger.info(f"Question: {request.question}")
@ -638,6 +639,7 @@ class Processor(AgentService):
content="", content="",
explain_id=iter_uri, explain_id=iter_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=iter_triples,
)) ))
user_context = UserAwareContext(flow, request.user) user_context = UserAwareContext(flow, request.user)
@ -715,6 +717,7 @@ class Processor(AgentService):
content="", content="",
explain_id=final_uri, explain_id=final_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=final_triples,
)) ))
if streaming: if streaming:
@ -791,6 +794,7 @@ class Processor(AgentService):
content="", content="",
explain_id=observation_entity_uri, explain_id=observation_entity_uri,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=obs_triples,
)) ))
history.append(act) history.append(act)

View file

@ -162,12 +162,13 @@ class Processor(FlowProcessor):
triples=triples, triples=triples,
)) ))
# Send explain ID and graph to response queue # Send explain data to response queue
await flow("response").send( await flow("response").send(
DocumentRagResponse( DocumentRagResponse(
response=None, response=None,
explain_id=explain_id, explain_id=explain_id,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
message_type="explain", message_type="explain",
), ),
properties={"id": id} properties={"id": id}

View file

@ -253,12 +253,13 @@ class Processor(FlowProcessor):
triples=triples, triples=triples,
)) ))
# Send explain ID and graph to response queue # Send explain data to response queue
await flow("response").send( await flow("response").send(
GraphRagResponse( GraphRagResponse(
message_type="explain", message_type="explain",
explain_id=explain_id, explain_id=explain_id,
explain_graph=GRAPH_RETRIEVAL, explain_graph=GRAPH_RETRIEVAL,
explain_triples=triples,
), ),
properties={"id": id} properties={"id": id}
) )