Wire message_id on all answer chunks, fix DAG structure (#748)

Wire message_id on all answer chunks, fix DAG structure message_id:
- Add message_id to AgentAnswer dataclass and propagate in
  socket_client._parse_chunk
- Wire message_id into answer callbacks and send_final_response
  for all three patterns (react, plan-then-execute, supervisor)
- Supervisor decomposition thought and synthesis answer chunks
  now carry message_id

DAG structure fixes:
- Observation derives from sub-trace Synthesis (not Analysis)
  when a tool produces a sub-trace; tracked via
  last_sub_explain_uri on context
- Subagent sessions derive from parent's Decomposition via
  parent_uri on agent_session_triples
- Findings derive from subagent Conclusions (not Decomposition)
- Synthesis derives from all findings (multiple wasDerivedFrom)
  ensuring single terminal node
- agent_synthesis_triples accepts list of parent URIs
- Explainability chain walker follows from sub-trace terminal
  to find downstream Observation

Emit Analysis before tool execution:
- Add on_action callback to react() in agent_manager.py, called
  after reason() but before tool invocation
- Orchestrator and old service emit Analysis+ToolUse triples via
  on_action so sub-traces appear after their parent in the stream
This commit is contained in:
cybermaggedon 2026-04-01 13:27:41 +01:00 committed by GitHub
parent 153ae9ad30
commit 2bcf375103
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 134 additions and 28 deletions

View file

@ -1095,6 +1095,15 @@ class ExplainabilityClient:
"trace": sub_trace,
})
# Continue from the sub-trace's terminal entity
# (Observation may derive from Synthesis)
terminal = sub_trace.get("synthesis")
if terminal:
self._follow_provenance_chain(
terminal.uri, trace, graph, user, collection,
max_depth=max_depth - 1,
)
elif isinstance(entity, (Conclusion, Synthesis)):
trace["steps"].append(entity)

View file

@ -397,7 +397,8 @@ class SocketClient:
return AgentAnswer(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False),
end_of_dialog=resp.get("end_of_dialog", False)
end_of_dialog=resp.get("end_of_dialog", False),
message_id=resp.get("message_id", ""),
)
elif chunk_type == "action":
return AgentThought(

View file

@ -188,6 +188,7 @@ class AgentAnswer(StreamingChunk):
"""
chunk_type: str = "final-answer"
end_of_dialog: bool = False
message_id: str = ""
@dataclasses.dataclass
class RAGChunk(StreamingChunk):

View file

@ -51,6 +51,7 @@ def agent_session_triples(
session_uri: str,
query: str,
timestamp: Optional[str] = None,
parent_uri: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for an agent session start (Question).
@ -58,11 +59,13 @@ def agent_session_triples(
Creates:
- Activity declaration with tg:Question type
- Query text and timestamp
- wasDerivedFrom link to parent (for subagent sessions)
Args:
session_uri: URI of the session (from agent_session_uri)
query: The user's query text
timestamp: ISO timestamp (defaults to now)
parent_uri: URI of the parent entity (e.g. Decomposition) for subagents
Returns:
List of Triple objects
@ -70,7 +73,7 @@ def agent_session_triples(
if timestamp is None:
timestamp = datetime.utcnow().isoformat() + "Z"
return [
triples = [
_triple(session_uri, RDF_TYPE, _iri(PROV_ENTITY)),
_triple(session_uri, RDF_TYPE, _iri(TG_QUESTION)),
_triple(session_uri, RDF_TYPE, _iri(TG_AGENT_QUESTION)),
@ -79,6 +82,13 @@ def agent_session_triples(
_triple(session_uri, TG_QUERY, _literal(query)),
]
if parent_uri:
triples.append(
_triple(session_uri, PROV_WAS_DERIVED_FROM, _iri(parent_uri))
)
return triples
def agent_iteration_triples(
iteration_uri: str,
@ -308,17 +318,28 @@ def agent_step_result_triples(
def agent_synthesis_triples(
uri: str,
previous_uri: str,
previous_uris,
document_id: Optional[str] = None,
) -> List[Triple]:
"""Build triples for a synthesis answer."""
"""Build triples for a synthesis answer.
Args:
uri: URI of the synthesis entity
previous_uris: Single URI string or list of URIs to derive from
document_id: Librarian document ID for the answer content
"""
triples = [
_triple(uri, RDF_TYPE, _iri(PROV_ENTITY)),
_triple(uri, RDF_TYPE, _iri(TG_SYNTHESIS)),
_triple(uri, RDF_TYPE, _iri(TG_ANSWER_TYPE)),
_triple(uri, RDFS_LABEL, _literal("Synthesis")),
_triple(uri, PROV_WAS_DERIVED_FROM, _iri(previous_uri)),
]
if isinstance(previous_uris, str):
previous_uris = [previous_uris]
for prev in previous_uris:
triples.append(_triple(uri, PROV_WAS_DERIVED_FROM, _iri(prev)))
if document_id:
triples.append(_triple(uri, TG_DOCUMENT, _iri(document_id)))
return triples