mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 00:16:23 +02:00
agent-orchestrator: add explainability provenance for all patterns (#744)
agent-orchestrator: add explainability provenance for all agent patterns Extend the provenance/explainability system to provide human-readable reasoning traces for the orchestrator's three agent patterns. Previously only ReAct emitted provenance (session, iteration, conclusion). Now each pattern records its cognitive steps as typed RDF entities in the knowledge graph, using composable mixin types (e.g. Finding + Answer). New provenance chains: - Supervisor: Question → Decomposition → Finding ×N → Synthesis - Plan-then-Execute: Question → Plan → StepResult ×N → Synthesis - ReAct: Question → Analysis ×N → Conclusion (unchanged) New RDF types: Decomposition, Finding, Plan, StepResult. New predicates: tg:subagentGoal, tg:planStep. Reuses existing Synthesis + Answer mixin for final answers. Provenance library (trustgraph-base): - Triple builders, URI generators, vocabulary labels for new types - Client dataclasses with from_triples() dispatch - fetch_agent_trace() follows branching provenance chains - API exports updated Orchestrator (trustgraph-flow): - PatternBase emit methods for decomposition, finding, plan, step result, and synthesis - SupervisorPattern emits decomposition during fan-out - PlanThenExecutePattern emits plan and step results - Service emits finding triples on subagent completion - Synthesis provenance replaces generic final triples CLI (trustgraph-cli): - invoke_agent -x displays new entity types inline
This commit is contained in:
parent
e65ea217a2
commit
7b734148b3
12 changed files with 560 additions and 82 deletions
|
|
@ -20,9 +20,19 @@ from trustgraph.provenance import (
|
|||
agent_thought_uri,
|
||||
agent_observation_uri,
|
||||
agent_final_uri,
|
||||
agent_decomposition_uri,
|
||||
agent_finding_uri,
|
||||
agent_plan_uri,
|
||||
agent_step_result_uri,
|
||||
agent_synthesis_uri,
|
||||
agent_session_triples,
|
||||
agent_iteration_triples,
|
||||
agent_final_triples,
|
||||
agent_decomposition_triples,
|
||||
agent_finding_triples,
|
||||
agent_plan_triples,
|
||||
agent_step_result_triples,
|
||||
agent_synthesis_triples,
|
||||
set_graph,
|
||||
GRAPH_RETRIEVAL,
|
||||
)
|
||||
|
|
@ -359,6 +369,146 @@ class PatternBase:
|
|||
explain_graph=GRAPH_RETRIEVAL,
|
||||
))
|
||||
|
||||
# ---- Orchestrator provenance helpers ------------------------------------
|
||||
|
||||
async def emit_decomposition_triples(
|
||||
self, flow, session_id, session_uri, goals, user, collection,
|
||||
respond, streaming,
|
||||
):
|
||||
"""Emit provenance for a supervisor decomposition step."""
|
||||
uri = agent_decomposition_uri(session_id)
|
||||
triples = set_graph(
|
||||
agent_decomposition_triples(uri, session_uri, goals),
|
||||
GRAPH_RETRIEVAL,
|
||||
)
|
||||
await flow("explainability").send(Triples(
|
||||
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,
|
||||
))
|
||||
|
||||
async def emit_finding_triples(
|
||||
self, flow, session_id, index, goal, answer_text, user, collection,
|
||||
respond, streaming,
|
||||
):
|
||||
"""Emit provenance for a subagent finding."""
|
||||
uri = agent_finding_uri(session_id, index)
|
||||
decomposition_uri = agent_decomposition_uri(session_id)
|
||||
|
||||
doc_id = f"urn:trustgraph:agent:{session_id}/finding/{index}/doc"
|
||||
try:
|
||||
await self.processor.save_answer_content(
|
||||
doc_id=doc_id, user=user,
|
||||
content=answer_text,
|
||||
title=f"Finding: {goal[:60]}",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save finding to librarian: {e}")
|
||||
doc_id = None
|
||||
|
||||
triples = set_graph(
|
||||
agent_finding_triples(uri, decomposition_uri, goal, doc_id),
|
||||
GRAPH_RETRIEVAL,
|
||||
)
|
||||
await flow("explainability").send(Triples(
|
||||
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,
|
||||
))
|
||||
|
||||
async def emit_plan_triples(
|
||||
self, flow, session_id, session_uri, steps, user, collection,
|
||||
respond, streaming,
|
||||
):
|
||||
"""Emit provenance for a plan creation."""
|
||||
uri = agent_plan_uri(session_id)
|
||||
triples = set_graph(
|
||||
agent_plan_triples(uri, session_uri, steps),
|
||||
GRAPH_RETRIEVAL,
|
||||
)
|
||||
await flow("explainability").send(Triples(
|
||||
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,
|
||||
))
|
||||
|
||||
async def emit_step_result_triples(
|
||||
self, flow, session_id, index, goal, answer_text, user, collection,
|
||||
respond, streaming,
|
||||
):
|
||||
"""Emit provenance for a plan step result."""
|
||||
uri = agent_step_result_uri(session_id, index)
|
||||
plan_uri = agent_plan_uri(session_id)
|
||||
|
||||
doc_id = f"urn:trustgraph:agent:{session_id}/step/{index}/doc"
|
||||
try:
|
||||
await self.processor.save_answer_content(
|
||||
doc_id=doc_id, user=user,
|
||||
content=answer_text,
|
||||
title=f"Step result: {goal[:60]}",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save step result to librarian: {e}")
|
||||
doc_id = None
|
||||
|
||||
triples = set_graph(
|
||||
agent_step_result_triples(uri, plan_uri, goal, doc_id),
|
||||
GRAPH_RETRIEVAL,
|
||||
)
|
||||
await flow("explainability").send(Triples(
|
||||
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,
|
||||
))
|
||||
|
||||
async def emit_synthesis_triples(
|
||||
self, flow, session_id, previous_uri, answer_text, user, collection,
|
||||
respond, streaming,
|
||||
):
|
||||
"""Emit provenance for a synthesis answer."""
|
||||
uri = agent_synthesis_uri(session_id)
|
||||
|
||||
doc_id = f"urn:trustgraph:agent:{session_id}/synthesis/doc"
|
||||
try:
|
||||
await self.processor.save_answer_content(
|
||||
doc_id=doc_id, user=user,
|
||||
content=answer_text,
|
||||
title="Synthesis",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save synthesis to librarian: {e}")
|
||||
doc_id = None
|
||||
|
||||
triples = set_graph(
|
||||
agent_synthesis_triples(uri, previous_uri, doc_id),
|
||||
GRAPH_RETRIEVAL,
|
||||
)
|
||||
await flow("explainability").send(Triples(
|
||||
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,
|
||||
))
|
||||
|
||||
# ---- Response helpers ---------------------------------------------------
|
||||
|
||||
async def prompt_as_answer(self, client, prompt_id, variables,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import uuid
|
|||
|
||||
from ... schema import AgentRequest, AgentResponse, AgentStep, PlanStep
|
||||
|
||||
from ..react.types import Action
|
||||
|
||||
|
||||
from . pattern_base import PatternBase
|
||||
|
||||
|
|
@ -126,6 +126,13 @@ class PlanThenExecutePattern(PatternBase):
|
|||
thought_text = f"Created plan with {len(plan_steps)} steps"
|
||||
await think(thought_text, is_final=True)
|
||||
|
||||
# Emit plan provenance
|
||||
step_goals = [ps.get("goal", "") for ps in plan_steps]
|
||||
await self.emit_plan_triples(
|
||||
flow, session_id, session_uri, step_goals,
|
||||
request.user, collection, respond, streaming,
|
||||
)
|
||||
|
||||
# Build PlanStep objects
|
||||
plan_agent_steps = [
|
||||
PlanStep(
|
||||
|
|
@ -263,16 +270,10 @@ class PlanThenExecutePattern(PatternBase):
|
|||
result=step_result,
|
||||
)
|
||||
|
||||
# Emit iteration provenance
|
||||
prov_act = Action(
|
||||
thought=f"Plan step {pending_idx}: {goal}",
|
||||
name=tool_name,
|
||||
arguments=tool_arguments,
|
||||
observation=step_result,
|
||||
)
|
||||
await self.emit_iteration_triples(
|
||||
flow, session_id, iteration_num, session_uri,
|
||||
prov_act, request, respond, streaming,
|
||||
# Emit step result provenance
|
||||
await self.emit_step_result_triples(
|
||||
flow, session_id, pending_idx, goal, step_result,
|
||||
request.user, collection, respond, streaming,
|
||||
)
|
||||
|
||||
# Build execution step for history
|
||||
|
|
@ -340,9 +341,12 @@ class PlanThenExecutePattern(PatternBase):
|
|||
streaming=streaming,
|
||||
)
|
||||
|
||||
await self.emit_final_triples(
|
||||
flow, session_id, iteration_num, session_uri,
|
||||
response_text, request, respond, streaming,
|
||||
# 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)
|
||||
await self.emit_synthesis_triples(
|
||||
flow, session_id, last_step_uri,
|
||||
response_text, request.user, collection, respond, streaming,
|
||||
)
|
||||
|
||||
if self.is_subagent(request):
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ class Processor(AgentService):
|
|||
|
||||
correlation_id = request.correlation_id
|
||||
subagent_goal = getattr(request, 'subagent_goal', '')
|
||||
parent_session_id = getattr(request, 'parent_session_id', '')
|
||||
|
||||
# Extract the answer from the completion step
|
||||
answer_text = ""
|
||||
|
|
@ -451,13 +452,26 @@ class Processor(AgentService):
|
|||
)
|
||||
return
|
||||
|
||||
# Emit finding provenance for this subagent
|
||||
template = self.aggregator.get_original_request(correlation_id)
|
||||
if template and parent_session_id:
|
||||
entry = self.aggregator.correlations.get(correlation_id)
|
||||
finding_index = len(entry["results"]) - 1 if entry else 0
|
||||
collection = getattr(template, 'collection', 'default')
|
||||
|
||||
await self.supervisor_pattern.emit_finding_triples(
|
||||
flow, parent_session_id, finding_index,
|
||||
subagent_goal, answer_text,
|
||||
template.user, collection,
|
||||
respond, template.streaming,
|
||||
)
|
||||
|
||||
if all_done:
|
||||
logger.info(
|
||||
f"All subagents complete for {correlation_id}, "
|
||||
f"dispatching synthesis"
|
||||
)
|
||||
|
||||
template = self.aggregator.get_original_request(correlation_id)
|
||||
if template is None:
|
||||
logger.error(
|
||||
f"No template for correlation {correlation_id}"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import uuid
|
|||
|
||||
from ... schema import AgentRequest, AgentResponse, AgentStep
|
||||
|
||||
from ..react.types import Action, Final
|
||||
from trustgraph.provenance import agent_finding_uri
|
||||
|
||||
from . pattern_base import PatternBase
|
||||
|
||||
|
|
@ -121,15 +121,9 @@ class SupervisorPattern(PatternBase):
|
|||
correlation_id = str(uuid.uuid4())
|
||||
|
||||
# Emit decomposition provenance
|
||||
decompose_act = Action(
|
||||
thought=f"Decomposed into {len(goals)} sub-goals",
|
||||
name="decompose",
|
||||
arguments={"goals": json.dumps(goals), "correlation_id": correlation_id},
|
||||
observation=f"Fanning out {len(goals)} subagents",
|
||||
)
|
||||
await self.emit_iteration_triples(
|
||||
flow, session_id, iteration_num, session_uri,
|
||||
decompose_act, request, respond, streaming,
|
||||
await self.emit_decomposition_triples(
|
||||
flow, session_id, session_uri, goals,
|
||||
request.user, collection, respond, streaming,
|
||||
)
|
||||
|
||||
# Fan out: emit a subagent request for each goal
|
||||
|
|
@ -207,10 +201,15 @@ class SupervisorPattern(PatternBase):
|
|||
streaming=streaming,
|
||||
)
|
||||
|
||||
await self.emit_final_triples(
|
||||
flow, session_id, iteration_num, session_uri,
|
||||
response_text, request, respond, streaming,
|
||||
# Emit synthesis provenance (links back to last finding)
|
||||
last_finding_uri = agent_finding_uri(
|
||||
session_id, len(subagent_results) - 1
|
||||
)
|
||||
await self.emit_synthesis_triples(
|
||||
flow, session_id, last_finding_uri,
|
||||
response_text, request.user, collection, respond, streaming,
|
||||
)
|
||||
|
||||
await self.send_final_response(
|
||||
respond, streaming, response_text, already_streamed=streaming,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue