mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-02 14:31:01 +02:00
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:
parent
89e13a756a
commit
153ae9ad30
28 changed files with 661 additions and 350 deletions
|
|
@ -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 ---------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue