Split Analysis into Analysis+ToolUse and Observation, add message_id (#747)

Refactor agent provenance so that the decision (thought + tool
selection) and the result (observation) are separate DAG entities:

  Question ← Analysis+ToolUse ← Observation ← ... ← Conclusion

Analysis gains tg:ToolUse as a mixin RDF type and is emitted
before tool execution via an on_action callback in react().
This ensures sub-traces (e.g. GraphRAG) appear after their
parent Analysis in the streaming event order.

Observation becomes a standalone prov:Entity with tg:Observation
type, emitted after tool execution. The linear DAG chain runs
through Observation — subsequent iterations and the Conclusion
derive from it, not from the Analysis.

message_id is populated on streaming AgentResponse for thought
and observation chunks, using the provenance URI of the entity
being built. This lets clients group streamed chunks by entity.

Wire changes:
- provenance/agent.py: Add ToolUse type, new
  agent_observation_triples(), remove observation from iteration
- agent_manager.py: Add on_action callback between reason() and
  tool execution
- orchestrator/pattern_base.py: Split emit, wire message_id,
  chain through observation URIs
- orchestrator/react_pattern.py: Emit Analysis via on_action
  before tool runs
- agent/react/service.py: Same for non-orchestrator path
- api/explainability.py: New Observation class, updated dispatch
  and chain walker
- api/types.py: Add message_id to AgentThought/AgentObservation
- cli: Render Observation separately, [analysis: tool] labels
This commit is contained in:
cybermaggedon 2026-03-31 17:51:22 +01:00 committed by GitHub
parent 89e13a756a
commit 153ae9ad30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 661 additions and 350 deletions

View file

@ -27,6 +27,7 @@ from trustgraph.provenance import (
agent_synthesis_uri,
agent_session_triples,
agent_iteration_triples,
agent_observation_triples,
agent_final_triples,
agent_decomposition_triples,
agent_finding_triples,
@ -46,9 +47,12 @@ logger = logging.getLogger(__name__)
class UserAwareContext:
"""Wraps flow interface to inject user context for tools that need it."""
def __init__(self, flow, user):
def __init__(self, flow, user, respond=None, streaming=False):
self._flow = flow
self._user = user
self.respond = respond
self.streaming = streaming
self.current_explain_uri = None
def __call__(self, service_name):
client = self._flow(service_name)
@ -120,9 +124,9 @@ class PatternBase:
current_state=getattr(request, 'state', None),
)
def make_context(self, flow, user):
def make_context(self, flow, user, respond=None, streaming=False):
"""Create a user-aware context wrapper."""
return UserAwareContext(flow, user)
return UserAwareContext(flow, user, respond=respond, streaming=streaming)
def build_history(self, request):
"""Convert AgentStep history into Action objects."""
@ -140,7 +144,7 @@ class PatternBase:
# ---- Streaming callbacks ------------------------------------------------
def make_think_callback(self, respond, streaming):
def make_think_callback(self, respond, streaming, message_id=""):
"""Create the think callback for streaming/non-streaming."""
async def think(x, is_final=False):
logger.debug(f"Think: {x} (is_final={is_final})")
@ -150,6 +154,7 @@ class PatternBase:
content=x,
end_of_message=is_final,
end_of_dialog=False,
message_id=message_id,
)
else:
r = AgentResponse(
@ -157,11 +162,12 @@ class PatternBase:
content=x,
end_of_message=True,
end_of_dialog=False,
message_id=message_id,
)
await respond(r)
return think
def make_observe_callback(self, respond, streaming):
def make_observe_callback(self, respond, streaming, message_id=""):
"""Create the observe callback for streaming/non-streaming."""
async def observe(x, is_final=False):
logger.debug(f"Observe: {x} (is_final={is_final})")
@ -171,6 +177,7 @@ class PatternBase:
content=x,
end_of_message=is_final,
end_of_dialog=False,
message_id=message_id,
)
else:
r = AgentResponse(
@ -178,6 +185,7 @@ class PatternBase:
content=x,
end_of_message=True,
end_of_dialog=False,
message_id=message_id,
)
await respond(r)
return observe
@ -223,23 +231,23 @@ class PatternBase:
))
logger.debug(f"Emitted session triples for {session_uri}")
if streaming:
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=session_uri,
explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=session_uri,
explain_graph=GRAPH_RETRIEVAL,
))
async def emit_iteration_triples(self, flow, session_id, iteration_num,
session_uri, act, request, respond,
streaming):
"""Emit provenance triples for an iteration and save to librarian."""
"""Emit provenance triples for an iteration (Analysis+ToolUse)."""
iteration_uri = agent_iteration_uri(session_id, iteration_num)
if iteration_num > 1:
# Chain through previous Observation (last entity in prior cycle)
iter_question_uri = None
iter_previous_uri = agent_iteration_uri(session_id, iteration_num - 1)
iter_previous_uri = agent_observation_uri(session_id, iteration_num - 1)
else:
iter_question_uri = session_uri
iter_previous_uri = None
@ -261,25 +269,7 @@ class PatternBase:
logger.warning(f"Failed to save thought to librarian: {e}")
thought_doc_id = None
# Save observation to librarian
observation_doc_id = None
if act.observation:
observation_doc_id = (
f"urn:trustgraph:agent:{session_id}/i{iteration_num}/observation"
)
try:
await self.processor.save_answer_content(
doc_id=observation_doc_id,
user=request.user,
content=act.observation,
title=f"Agent Observation: {act.name}",
)
except Exception as e:
logger.warning(f"Failed to save observation to librarian: {e}")
observation_doc_id = None
thought_entity_uri = agent_thought_uri(session_id, iteration_num)
observation_entity_uri = agent_observation_uri(session_id, iteration_num)
iter_triples = set_graph(
agent_iteration_triples(
@ -290,8 +280,6 @@ class PatternBase:
arguments=act.arguments,
thought_uri=thought_entity_uri if thought_doc_id else None,
thought_document_id=thought_doc_id,
observation_uri=observation_entity_uri if observation_doc_id else None,
observation_document_id=observation_doc_id,
),
GRAPH_RETRIEVAL,
)
@ -305,13 +293,60 @@ class PatternBase:
))
logger.debug(f"Emitted iteration triples for {iteration_uri}")
if streaming:
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=iteration_uri,
explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=iteration_uri,
explain_graph=GRAPH_RETRIEVAL,
))
async def emit_observation_triples(self, flow, session_id, iteration_num,
observation_text, request, respond):
"""Emit provenance triples for a standalone Observation entity."""
iteration_uri = agent_iteration_uri(session_id, iteration_num)
observation_entity_uri = agent_observation_uri(session_id, iteration_num)
# Save observation to librarian
observation_doc_id = None
if observation_text:
observation_doc_id = (
f"urn:trustgraph:agent:{session_id}/i{iteration_num}/observation"
)
try:
await self.processor.save_answer_content(
doc_id=observation_doc_id,
user=request.user,
content=observation_text,
title=f"Agent Observation",
)
except Exception as e:
logger.warning(f"Failed to save observation to librarian: {e}")
observation_doc_id = None
obs_triples = set_graph(
agent_observation_triples(
observation_entity_uri,
iteration_uri,
document_id=observation_doc_id,
),
GRAPH_RETRIEVAL,
)
await flow("explainability").send(Triples(
metadata=Metadata(
id=observation_entity_uri,
user=request.user,
collection=getattr(request, 'collection', 'default'),
),
triples=obs_triples,
))
logger.debug(f"Emitted observation triples for {observation_entity_uri}")
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=observation_entity_uri,
explain_graph=GRAPH_RETRIEVAL,
))
async def emit_final_triples(self, flow, session_id, iteration_num,
session_uri, answer_text, request, respond,
@ -320,8 +355,9 @@ class PatternBase:
final_uri = agent_final_uri(session_id)
if iteration_num > 1:
# Chain through last Observation (last entity in prior cycle)
final_question_uri = None
final_previous_uri = agent_iteration_uri(session_id, iteration_num - 1)
final_previous_uri = agent_observation_uri(session_id, iteration_num - 1)
else:
final_question_uri = session_uri
final_previous_uri = None
@ -361,13 +397,12 @@ class PatternBase:
))
logger.debug(f"Emitted final triples for {final_uri}")
if streaming:
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=final_uri,
explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=final_uri,
explain_graph=GRAPH_RETRIEVAL,
))
# ---- Orchestrator provenance helpers ------------------------------------
@ -385,11 +420,10 @@ class PatternBase:
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_finding_triples(
self, flow, session_id, index, goal, answer_text, user, collection,
@ -418,11 +452,10 @@ class PatternBase:
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_plan_triples(
self, flow, session_id, session_uri, steps, user, collection,
@ -438,11 +471,10 @@ class PatternBase:
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_step_result_triples(
self, flow, session_id, index, goal, answer_text, user, collection,
@ -471,11 +503,10 @@ class PatternBase:
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_synthesis_triples(
self, flow, session_id, previous_uri, answer_text, user, collection,
@ -503,11 +534,10 @@ class PatternBase:
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
# ---- Response helpers ---------------------------------------------------

View file

@ -11,7 +11,11 @@ import uuid
from ... schema import AgentRequest, AgentResponse, AgentStep, PlanStep
from trustgraph.provenance import (
agent_step_result_uri as make_step_result_uri,
agent_thought_uri,
agent_observation_uri,
)
from . pattern_base import PatternBase
@ -101,7 +105,10 @@ class PlanThenExecutePattern(PatternBase):
tools = self.filter_tools(self.processor.agent.tools, request)
framing = getattr(request, 'framing', '')
context = self.make_context(flow, request.user)
context = self.make_context(
flow, request.user,
respond=respond, streaming=streaming,
)
client = context("prompt-request")
# Use the plan-create prompt template
@ -198,8 +205,11 @@ class PlanThenExecutePattern(PatternBase):
logger.info(f"Executing plan step {pending_idx}: {goal}")
think = self.make_think_callback(respond, streaming)
observe = self.make_observe_callback(respond, streaming)
thought_msg_id = agent_thought_uri(session_id, iteration_num)
observation_msg_id = agent_observation_uri(session_id, iteration_num)
think = self.make_think_callback(respond, streaming, message_id=thought_msg_id)
observe = self.make_observe_callback(respond, streaming, message_id=observation_msg_id)
# Gather results from dependencies
previous_results = []
@ -216,7 +226,16 @@ class PlanThenExecutePattern(PatternBase):
})
tools = self.filter_tools(self.processor.agent.tools, request)
context = self.make_context(flow, request.user)
context = self.make_context(
flow, request.user,
respond=respond, streaming=streaming,
)
# Set current explain URI so tools can link sub-traces
context.current_explain_uri = make_step_result_uri(
session_id, pending_idx,
)
client = context("prompt-request")
# Single-shot: ask LLM which tool + arguments to use for this goal
@ -316,7 +335,10 @@ class PlanThenExecutePattern(PatternBase):
think = self.make_think_callback(respond, streaming)
framing = getattr(request, 'framing', '')
context = self.make_context(flow, request.user)
context = self.make_context(
flow, request.user,
respond=respond, streaming=streaming,
)
client = context("prompt-request")
# Use the plan-synthesise prompt template
@ -342,8 +364,7 @@ class PlanThenExecutePattern(PatternBase):
)
# Emit synthesis provenance (links back to last step result)
from trustgraph.provenance import agent_step_result_uri
last_step_uri = agent_step_result_uri(session_id, len(plan) - 1)
last_step_uri = make_step_result_uri(session_id, len(plan) - 1)
await self.emit_synthesis_triples(
flow, session_id, last_step_uri,
response_text, request.user, collection, respond, streaming,

View file

@ -11,6 +11,12 @@ import uuid
from ... schema import AgentRequest, AgentResponse, AgentStep
from trustgraph.provenance import (
agent_iteration_uri,
agent_thought_uri,
agent_observation_uri,
)
from ..react.agent_manager import AgentManager
from ..react.types import Action, Final
from ..tool_filter import get_next_state
@ -51,9 +57,13 @@ class ReactPattern(PatternBase):
if len(history) >= self.processor.max_iterations:
raise RuntimeError("Too many agent iterations")
# Compute URIs upfront for message_id
thought_msg_id = agent_thought_uri(session_id, iteration_num)
observation_msg_id = agent_observation_uri(session_id, iteration_num)
# Build callbacks
think = self.make_think_callback(respond, streaming)
observe = self.make_observe_callback(respond, streaming)
think = self.make_think_callback(respond, streaming, message_id=thought_msg_id)
observe = self.make_observe_callback(respond, streaming, message_id=observation_msg_id)
answer_cb = self.make_answer_callback(respond, streaming)
# Filter tools
@ -75,7 +85,22 @@ class ReactPattern(PatternBase):
additional_context=additional_context,
)
context = self.make_context(flow, request.user)
context = self.make_context(
flow, request.user,
respond=respond, streaming=streaming,
)
# Set current explain URI so tools can link sub-traces
context.current_explain_uri = agent_iteration_uri(
session_id, iteration_num,
)
# Callback: emit Analysis+ToolUse triples before tool executes
async def on_action(act):
await self.emit_iteration_triples(
flow, session_id, iteration_num, session_uri,
act, request, respond, streaming,
)
act = await temp_agent.react(
question=request.question,
@ -85,6 +110,7 @@ class ReactPattern(PatternBase):
answer=answer_cb,
context=context,
streaming=streaming,
on_action=on_action,
)
logger.debug(f"Action: {act}")
@ -110,10 +136,10 @@ class ReactPattern(PatternBase):
)
return
# Not final — emit iteration provenance and send next request
await self.emit_iteration_triples(
flow, session_id, iteration_num, session_uri,
act, request, respond, streaming,
# Emit observation provenance after tool execution
await self.emit_observation_triples(
flow, session_id, iteration_num,
act.observation, request, respond,
)
history.append(act)

View file

@ -86,7 +86,10 @@ class SupervisorPattern(PatternBase):
tools = self.filter_tools(self.processor.agent.tools, request)
context = self.make_context(flow, request.user)
context = self.make_context(
flow, request.user,
respond=respond, streaming=streaming,
)
client = context("prompt-request")
# Use the supervisor-decompose prompt template
@ -182,7 +185,10 @@ class SupervisorPattern(PatternBase):
logger.warning("Synthesis called with no subagent results")
subagent_results = {"(no results)": "No subagent results available"}
context = self.make_context(flow, request.user)
context = self.make_context(
flow, request.user,
respond=respond, streaming=streaming,
)
client = context("prompt-request")
await think("Synthesising final answer from sub-agent results", is_final=True)

View file

@ -291,7 +291,8 @@ class AgentManager:
logger.error(f"Response was: {response_text}")
raise RuntimeError(f"Failed to parse agent response: {e}")
async def react(self, question, history, think, observe, context, streaming=False, answer=None):
async def react(self, question, history, think, observe, context,
streaming=False, answer=None, on_action=None):
act = await self.reason(
question = question,
@ -325,6 +326,10 @@ class AgentManager:
else:
raise RuntimeError(f"No action for {act.name}!")
# Notify caller before tool execution (for provenance)
if on_action:
await on_action(act)
resp = await action.implementation(context).invoke(
**act.arguments
)

View file

@ -36,6 +36,7 @@ from trustgraph.provenance import (
agent_final_uri,
agent_session_triples,
agent_iteration_triples,
agent_observation_triples,
agent_final_triples,
set_graph,
GRAPH_RETRIEVAL,
@ -465,13 +466,12 @@ class Processor(AgentService):
logger.debug(f"Emitted session triples for {session_uri}")
# Send explain event for session
if streaming:
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=session_uri,
explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=session_uri,
explain_graph=GRAPH_RETRIEVAL,
))
logger.info(f"Question: {request.question}")
@ -480,6 +480,9 @@ class Processor(AgentService):
logger.debug(f"History: {history}")
thought_msg_id = agent_thought_uri(session_id, iteration_num)
observation_msg_id = agent_observation_uri(session_id, iteration_num)
async def think(x, is_final=False):
logger.debug(f"Think: {x} (is_final={is_final})")
@ -490,6 +493,7 @@ class Processor(AgentService):
content=x,
end_of_message=is_final,
end_of_dialog=False,
message_id=thought_msg_id,
)
else:
r = AgentResponse(
@ -497,6 +501,7 @@ class Processor(AgentService):
content=x,
end_of_message=True,
end_of_dialog=False,
message_id=thought_msg_id,
)
await respond(r)
@ -511,6 +516,7 @@ class Processor(AgentService):
content=x,
end_of_message=is_final,
end_of_dialog=False,
message_id=observation_msg_id,
)
else:
r = AgentResponse(
@ -518,6 +524,7 @@ class Processor(AgentService):
content=x,
end_of_message=True,
end_of_dialog=False,
message_id=observation_msg_id,
)
await respond(r)
@ -572,6 +579,62 @@ class Processor(AgentService):
client._current_user = self._user
return client
# Callback: emit Analysis+ToolUse triples before tool executes
async def on_action(act_decision):
iter_uri = agent_iteration_uri(session_id, iteration_num)
if iteration_num > 1:
iter_q_uri = None
iter_prev_uri = agent_observation_uri(session_id, iteration_num - 1)
else:
iter_q_uri = session_uri
iter_prev_uri = None
# Save thought to librarian
t_doc_id = None
if act_decision.thought:
t_doc_id = f"urn:trustgraph:agent:{session_id}/i{iteration_num}/thought"
try:
await self.save_answer_content(
doc_id=t_doc_id,
user=request.user,
content=act_decision.thought,
title=f"Agent Thought: {act_decision.name}",
)
except Exception as e:
logger.warning(f"Failed to save thought to librarian: {e}")
t_doc_id = None
t_entity_uri = agent_thought_uri(session_id, iteration_num)
iter_triples = set_graph(
agent_iteration_triples(
iter_uri,
question_uri=iter_q_uri,
previous_uri=iter_prev_uri,
action=act_decision.name,
arguments=act_decision.arguments,
thought_uri=t_entity_uri if t_doc_id else None,
thought_document_id=t_doc_id,
),
GRAPH_RETRIEVAL
)
await flow("explainability").send(Triples(
metadata=Metadata(
id=iter_uri,
user=request.user,
collection=collection,
),
triples=iter_triples,
))
logger.debug(f"Emitted iteration triples for {iter_uri}")
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=iter_uri,
explain_graph=GRAPH_RETRIEVAL,
))
act = await temp_agent.react(
question = request.question,
history = history,
@ -580,6 +643,7 @@ class Processor(AgentService):
answer = answer,
context = UserAwareContext(flow, request.user),
streaming = streaming,
on_action = on_action,
)
logger.debug(f"Action: {act}")
@ -595,10 +659,10 @@ class Processor(AgentService):
# Emit final answer provenance triples
final_uri = agent_final_uri(session_id)
# No iterations: link to question; otherwise: link to last iteration
# No iterations: link to question; otherwise: link to last observation
if iteration_num > 1:
final_question_uri = None
final_previous_uri = agent_iteration_uri(session_id, iteration_num - 1)
final_previous_uri = agent_observation_uri(session_id, iteration_num - 1)
else:
final_question_uri = session_uri
final_previous_uri = None
@ -639,13 +703,12 @@ class Processor(AgentService):
logger.debug(f"Emitted final triples for {final_uri}")
# Send explain event for conclusion
if streaming:
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=final_uri,
explain_graph=GRAPH_RETRIEVAL,
))
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=final_uri,
explain_graph=GRAPH_RETRIEVAL,
))
if streaming:
# End-of-dialog marker — answer chunks already sent via callback
@ -671,33 +734,9 @@ class Processor(AgentService):
logger.debug("Send next...")
# Emit iteration provenance triples
# Emit standalone observation provenance (iteration was emitted in on_action)
iteration_uri = agent_iteration_uri(session_id, iteration_num)
# First iteration links to question, subsequent to previous
if iteration_num > 1:
iter_question_uri = None
iter_previous_uri = agent_iteration_uri(session_id, iteration_num - 1)
else:
iter_question_uri = session_uri
iter_previous_uri = None
# Save thought to librarian
thought_doc_id = None
if act.thought:
thought_doc_id = f"urn:trustgraph:agent:{session_id}/i{iteration_num}/thought"
try:
await self.save_answer_content(
doc_id=thought_doc_id,
user=request.user,
content=act.thought,
title=f"Agent Thought: {act.name}",
)
logger.debug(f"Saved thought to librarian: {thought_doc_id}")
except Exception as e:
logger.warning(f"Failed to save thought to librarian: {e}")
thought_doc_id = None
# Save observation to librarian
observation_entity_uri = agent_observation_uri(session_id, iteration_num)
observation_doc_id = None
if act.observation:
observation_doc_id = f"urn:trustgraph:agent:{session_id}/i{iteration_num}/observation"
@ -706,48 +745,38 @@ class Processor(AgentService):
doc_id=observation_doc_id,
user=request.user,
content=act.observation,
title=f"Agent Observation: {act.name}",
title=f"Agent Observation",
)
logger.debug(f"Saved observation to librarian: {observation_doc_id}")
except Exception as e:
logger.warning(f"Failed to save observation to librarian: {e}")
observation_doc_id = None
thought_entity_uri = agent_thought_uri(session_id, iteration_num)
observation_entity_uri = agent_observation_uri(session_id, iteration_num)
iter_triples = set_graph(
agent_iteration_triples(
obs_triples = set_graph(
agent_observation_triples(
observation_entity_uri,
iteration_uri,
question_uri=iter_question_uri,
previous_uri=iter_previous_uri,
action=act.name,
arguments=act.arguments,
thought_uri=thought_entity_uri if thought_doc_id else None,
thought_document_id=thought_doc_id,
observation_uri=observation_entity_uri if observation_doc_id else None,
observation_document_id=observation_doc_id,
document_id=observation_doc_id,
),
GRAPH_RETRIEVAL
)
await flow("explainability").send(Triples(
metadata=Metadata(
id=iteration_uri,
id=observation_entity_uri,
user=request.user,
collection=collection,
),
triples=iter_triples,
triples=obs_triples,
))
logger.debug(f"Emitted iteration triples for {iteration_uri}")
logger.debug(f"Emitted observation triples for {observation_entity_uri}")
# Send explain event for iteration
if streaming:
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=iteration_uri,
explain_graph=GRAPH_RETRIEVAL,
))
# Send explain event for observation
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=observation_entity_uri,
explain_graph=GRAPH_RETRIEVAL,
))
history.append(act)

View file

@ -12,7 +12,7 @@ class KnowledgeQueryImpl:
def __init__(self, context, collection=None):
self.context = context
self.collection = collection
@staticmethod
def get_arguments():
return [
@ -22,13 +22,39 @@ class KnowledgeQueryImpl:
description="The question to ask the knowledge base"
)
]
async def invoke(self, **arguments):
client = self.context("graph-rag-request")
logger.debug("Graph RAG question...")
# Build explain_callback to forward sub-trace explain events
# to the agent's response stream
explain_callback = None
parent_uri = ""
respond = getattr(self.context, 'respond', None)
streaming = getattr(self.context, 'streaming', False)
current_uri = getattr(self.context, 'current_explain_uri', None)
if respond:
from ... schema import AgentResponse
async def explain_callback(explain_id, explain_graph):
await respond(AgentResponse(
chunk_type="explain",
content="",
explain_id=explain_id,
explain_graph=explain_graph,
))
if current_uri:
parent_uri = current_uri
return await client.rag(
arguments.get("question"),
collection=self.collection if self.collection else "default"
collection=self.collection if self.collection else "default",
explain_callback=explain_callback,
parent_uri=parent_uri,
)
# This tool implementation knows how to do text completion. This uses

View file

@ -555,6 +555,7 @@ class GraphRag:
streaming = False,
chunk_callback = None,
explain_callback = None, save_answer_callback = None,
parent_uri = "",
):
"""
Execute a GraphRAG query with real-time explainability tracking.
@ -593,7 +594,10 @@ class GraphRag:
# Emit question explainability immediately
if explain_callback:
q_triples = set_graph(
question_triples(q_uri, query, timestamp),
question_triples(
q_uri, query, timestamp,
parent_uri=parent_uri or None,
),
GRAPH_RETRIEVAL
)
await explain_callback(q_triples, q_uri)

View file

@ -342,6 +342,7 @@ class Processor(FlowProcessor):
chunk_callback = send_chunk,
explain_callback = send_explainability,
save_answer_callback = save_answer,
parent_uri = v.parent_uri,
)
else:
@ -355,6 +356,7 @@ class Processor(FlowProcessor):
edge_limit = edge_limit,
explain_callback = send_explainability,
save_answer_callback = save_answer,
parent_uri = v.parent_uri,
)
# Send chunk with response