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

@ -12,6 +12,7 @@ from trustgraph.api import (
ProvenanceEvent,
Question,
Analysis,
Observation,
Conclusion,
Decomposition,
Finding,
@ -206,13 +207,13 @@ def question_explainable(
print(f" Time: {entity.timestamp}", file=sys.stderr)
elif isinstance(entity, Analysis):
print(f"\n [iteration] {prov_id}", file=sys.stderr)
if entity.action:
print(f" Action: {entity.action}", file=sys.stderr)
if entity.thought:
print(f" Thought: {entity.thought}", file=sys.stderr)
if entity.observation:
print(f" Observation: {entity.observation}", file=sys.stderr)
action_label = f": {entity.action}" if entity.action else ""
print(f"\n [analysis{action_label}] {prov_id}", file=sys.stderr)
elif isinstance(entity, Observation):
print(f"\n [observation] {prov_id}", file=sys.stderr)
if entity.document:
print(f" Document: {entity.document}", file=sys.stderr)
elif isinstance(entity, Decomposition):
print(f"\n [decompose] {prov_id}", file=sys.stderr)

View file

@ -26,6 +26,7 @@ from trustgraph.api import (
Focus,
Synthesis,
Analysis,
Observation,
Conclusion,
Decomposition,
Finding,
@ -379,11 +380,13 @@ def print_agent_text(trace, explain_client, api, user):
print(f" {line}")
except Exception:
print(f" Arguments: {step.arguments}")
print()
obs = step.observation or 'N/A'
if obs and len(obs) > 200:
obs = obs[:200] + "... [truncated]"
print(f" Observation: {obs}")
elif isinstance(step, Observation):
print("--- Observation ---")
_print_document_content(
explain_client, api, user, step.document, "Content",
)
print()
elif isinstance(step, Synthesis):
@ -437,6 +440,12 @@ def trace_to_dict(trace, trace_type):
"step": step.step,
"document": step.document,
}
elif isinstance(step, Observation):
return {
"type": "observation",
"id": step.uri,
"document": step.document,
}
elif isinstance(step, Analysis):
return {
"type": "analysis",
@ -444,7 +453,6 @@ def trace_to_dict(trace, trace_type):
"action": step.action,
"arguments": step.arguments,
"thought": step.thought,
"observation": step.observation,
}
elif isinstance(step, Synthesis):
return {