mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
Addresses recommendations from the UX developer's agent experience report. Adds provenance predicates, DAG structure changes, error resilience, and a published OWL ontology. Explainability additions: - Tool candidates: tg:toolCandidate on Analysis events lists the tools visible to the LLM for each iteration (names only, descriptions in config) - Termination reason: tg:terminationReason on Conclusion/Synthesis events (final-answer, plan-complete, subagents-complete) - Step counter: tg:stepNumber on iteration events - Pattern decision: new tg:PatternDecision entity in the DAG between session and first iteration, carrying tg:pattern and tg:taskType - Latency: tg:llmDurationMs on Analysis events, tg:toolDurationMs on Observation events - Token counts on events: tg:inToken/tg:outToken/tg:llmModel on Grounding, Focus, Synthesis, and Analysis events - Tool/parse errors: tg:toolError on Observation events with tg:Error mixin type. Parse failures return as error observations instead of crashing the agent, giving it a chance to retry. Envelope unification: - Rename chunk_type to message_type across AgentResponse schema, translator, SDK types, socket clients, CLI, and all tests. Agent and RAG services now both use message_type on the wire. Ontology: - specs/ontology/trustgraph.ttl — OWL vocabulary covering all 26 classes, 7 object properties, and 36+ datatype properties including new predicates. DAG structure tests: - tests/unit/test_provenance/test_dag_structure.py verifies the wasDerivedFrom chain for GraphRAG, DocumentRAG, and all three agent patterns (react, plan, supervisor) including the pattern-decision link.
359 lines
11 KiB
Python
359 lines
11 KiB
Python
"""
|
|
Tests for inline explainability triples in response translators
|
|
and ProvenanceEvent parsing.
|
|
"""
|
|
|
|
import pytest
|
|
from trustgraph.schema import (
|
|
GraphRagResponse, DocumentRagResponse, AgentResponse,
|
|
Term, Triple, IRI, LITERAL, Error,
|
|
)
|
|
from trustgraph.messaging.translators.retrieval import (
|
|
GraphRagResponseTranslator,
|
|
DocumentRagResponseTranslator,
|
|
)
|
|
from trustgraph.messaging.translators.agent import (
|
|
AgentResponseTranslator,
|
|
)
|
|
from trustgraph.api.types import ProvenanceEvent
|
|
|
|
|
|
# --- Helpers ---
|
|
|
|
def make_triple(s_iri, p_iri, o_value, o_type=LITERAL):
|
|
"""Create a Triple with IRI subject/predicate and typed object."""
|
|
o = Term(type=IRI, iri=o_value) if o_type == IRI else Term(type=LITERAL, value=o_value)
|
|
return Triple(
|
|
s=Term(type=IRI, iri=s_iri),
|
|
p=Term(type=IRI, iri=p_iri),
|
|
o=o,
|
|
)
|
|
|
|
|
|
def sample_triples():
|
|
"""A few provenance triples for a question entity."""
|
|
return [
|
|
make_triple(
|
|
"urn:trustgraph:question:abc123",
|
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
|
"https://trustgraph.ai/ns/GraphRagQuestion",
|
|
o_type=IRI,
|
|
),
|
|
make_triple(
|
|
"urn:trustgraph:question:abc123",
|
|
"https://trustgraph.ai/ns/query",
|
|
"What is the internet?",
|
|
),
|
|
make_triple(
|
|
"urn:trustgraph:question:abc123",
|
|
"http://www.w3.org/ns/prov#startedAtTime",
|
|
"2026-04-07T09:00:00Z",
|
|
),
|
|
]
|
|
|
|
|
|
# --- GraphRag Translator ---
|
|
|
|
class TestGraphRagExplainTriples:
|
|
|
|
def test_explain_triples_encoded(self):
|
|
translator = GraphRagResponseTranslator()
|
|
triples = sample_triples()
|
|
|
|
response = GraphRagResponse(
|
|
message_type="explain",
|
|
explain_id="urn:trustgraph:question:abc123",
|
|
explain_graph="urn:graph:retrieval",
|
|
explain_triples=triples,
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
|
|
assert "explain_triples" in result
|
|
assert len(result["explain_triples"]) == 3
|
|
|
|
# Check first triple is properly encoded
|
|
t = result["explain_triples"][0]
|
|
assert t["s"]["t"] == "i"
|
|
assert t["s"]["i"] == "urn:trustgraph:question:abc123"
|
|
assert t["p"]["t"] == "i"
|
|
|
|
def test_explain_triples_empty_not_included(self):
|
|
translator = GraphRagResponseTranslator()
|
|
|
|
response = GraphRagResponse(
|
|
message_type="chunk",
|
|
response="Some answer text",
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
|
|
assert "explain_triples" not in result
|
|
|
|
def test_explain_with_completion_returns_not_final(self):
|
|
translator = GraphRagResponseTranslator()
|
|
|
|
response = GraphRagResponse(
|
|
message_type="explain",
|
|
explain_id="urn:trustgraph:question:abc123",
|
|
explain_triples=sample_triples(),
|
|
end_of_session=False,
|
|
)
|
|
|
|
result, is_final = translator.encode_with_completion(response)
|
|
assert is_final is False
|
|
|
|
def test_explain_id_and_graph_included(self):
|
|
translator = GraphRagResponseTranslator()
|
|
|
|
response = GraphRagResponse(
|
|
message_type="explain",
|
|
explain_id="urn:trustgraph:question:abc123",
|
|
explain_graph="urn:graph:retrieval",
|
|
explain_triples=sample_triples(),
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
assert result["explain_id"] == "urn:trustgraph:question:abc123"
|
|
assert result["explain_graph"] == "urn:graph:retrieval"
|
|
|
|
|
|
# --- DocumentRag Translator ---
|
|
|
|
class TestDocumentRagExplainTriples:
|
|
|
|
def test_explain_triples_encoded(self):
|
|
translator = DocumentRagResponseTranslator()
|
|
|
|
response = DocumentRagResponse(
|
|
response=None,
|
|
message_type="explain",
|
|
explain_id="urn:trustgraph:docrag:abc123",
|
|
explain_graph="urn:graph:retrieval",
|
|
explain_triples=sample_triples(),
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
|
|
assert "explain_triples" in result
|
|
assert len(result["explain_triples"]) == 3
|
|
|
|
def test_explain_triples_empty_not_included(self):
|
|
translator = DocumentRagResponseTranslator()
|
|
|
|
response = DocumentRagResponse(
|
|
response="Answer text",
|
|
message_type="chunk",
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
assert "explain_triples" not in result
|
|
|
|
|
|
# --- Agent Translator ---
|
|
|
|
class TestAgentExplainTriples:
|
|
|
|
def test_explain_triples_encoded(self):
|
|
translator = AgentResponseTranslator()
|
|
|
|
response = AgentResponse(
|
|
message_type="explain",
|
|
content="",
|
|
explain_id="urn:trustgraph:agent:session:abc123",
|
|
explain_graph="urn:graph:retrieval",
|
|
explain_triples=sample_triples(),
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
|
|
assert "explain_triples" in result
|
|
assert len(result["explain_triples"]) == 3
|
|
|
|
t = result["explain_triples"][1]
|
|
assert t["p"]["i"] == "https://trustgraph.ai/ns/query"
|
|
assert t["o"]["t"] == "l"
|
|
assert t["o"]["v"] == "What is the internet?"
|
|
|
|
def test_explain_triples_empty_not_included(self):
|
|
translator = AgentResponseTranslator()
|
|
|
|
response = AgentResponse(
|
|
message_type="thought",
|
|
content="I need to think...",
|
|
)
|
|
|
|
result = translator.encode(response)
|
|
assert "explain_triples" not in result
|
|
|
|
def test_explain_with_completion_not_final(self):
|
|
translator = AgentResponseTranslator()
|
|
|
|
response = AgentResponse(
|
|
message_type="explain",
|
|
explain_id="urn:trustgraph:agent:session:abc123",
|
|
explain_triples=sample_triples(),
|
|
end_of_dialog=False,
|
|
)
|
|
|
|
result, is_final = translator.encode_with_completion(response)
|
|
assert is_final is False
|
|
|
|
def test_explain_with_completion_final(self):
|
|
translator = AgentResponseTranslator()
|
|
|
|
response = AgentResponse(
|
|
message_type="answer",
|
|
content="The answer is...",
|
|
end_of_dialog=True,
|
|
)
|
|
|
|
result, is_final = translator.encode_with_completion(response)
|
|
assert is_final is True
|
|
|
|
|
|
# --- ProvenanceEvent ---
|
|
|
|
class TestProvenanceEvent:
|
|
|
|
def test_question_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:question:abc123",
|
|
)
|
|
assert event.event_type == "question"
|
|
|
|
def test_exploration_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:exploration:abc123",
|
|
)
|
|
assert event.event_type == "exploration"
|
|
|
|
def test_focus_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:focus:abc123",
|
|
)
|
|
assert event.event_type == "focus"
|
|
|
|
def test_synthesis_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:synthesis:abc123",
|
|
)
|
|
assert event.event_type == "synthesis"
|
|
|
|
def test_grounding_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:grounding:abc123",
|
|
)
|
|
assert event.event_type == "grounding"
|
|
|
|
def test_session_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:session:abc123",
|
|
)
|
|
assert event.event_type == "session"
|
|
|
|
def test_iteration_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:iteration:abc123:1",
|
|
)
|
|
assert event.event_type == "iteration"
|
|
|
|
def test_observation_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:observation:abc123:1",
|
|
)
|
|
assert event.event_type == "observation"
|
|
|
|
def test_conclusion_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:conclusion:abc123",
|
|
)
|
|
assert event.event_type == "conclusion"
|
|
|
|
def test_decomposition_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:decomposition:abc123",
|
|
)
|
|
assert event.event_type == "decomposition"
|
|
|
|
def test_finding_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:finding:abc123:0",
|
|
)
|
|
assert event.event_type == "finding"
|
|
|
|
def test_plan_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:plan:abc123",
|
|
)
|
|
assert event.event_type == "plan"
|
|
|
|
def test_step_result_event_type(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:agent:step-result:abc123:0",
|
|
)
|
|
assert event.event_type == "step-result"
|
|
|
|
def test_defaults(self):
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:question:abc123",
|
|
)
|
|
assert event.entity is None
|
|
assert event.triples == []
|
|
assert event.explain_graph == ""
|
|
|
|
def test_with_triples(self):
|
|
raw = [{"s": {"t": "i", "i": "urn:x"}, "p": {"t": "i", "i": "urn:y"}, "o": {"t": "l", "v": "z"}}]
|
|
event = ProvenanceEvent(
|
|
explain_id="urn:trustgraph:question:abc123",
|
|
triples=raw,
|
|
)
|
|
assert len(event.triples) == 1
|
|
|
|
|
|
# --- Build ProvenanceEvent with entity parsing ---
|
|
|
|
class TestBuildProvenanceEvent:
|
|
|
|
def _make_client(self):
|
|
"""Create a minimal WebSocketClient-like object with _build_provenance_event."""
|
|
from trustgraph.api.socket_client import WebSocketClient
|
|
# We can't instantiate WebSocketClient easily, so test the method logic directly
|
|
return None
|
|
|
|
def test_entity_parsed_from_wire_triples(self):
|
|
"""Test that wire-format triples are parsed into an ExplainEntity."""
|
|
from trustgraph.api.explainability import ExplainEntity
|
|
|
|
wire_triples = [
|
|
{
|
|
"s": {"t": "i", "i": "urn:trustgraph:question:abc123"},
|
|
"p": {"t": "i", "i": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"},
|
|
"o": {"t": "i", "i": "https://trustgraph.ai/ns/GraphRagQuestion"},
|
|
},
|
|
{
|
|
"s": {"t": "i", "i": "urn:trustgraph:question:abc123"},
|
|
"p": {"t": "i", "i": "https://trustgraph.ai/ns/query"},
|
|
"o": {"t": "l", "v": "What is the internet?"},
|
|
},
|
|
]
|
|
|
|
# Parse triples the same way _build_provenance_event does
|
|
parsed = []
|
|
for t in wire_triples:
|
|
s = t.get("s", {}).get("i", "")
|
|
p = t.get("p", {}).get("i", "")
|
|
o_term = t.get("o", {})
|
|
if o_term.get("t") == "i":
|
|
o = o_term.get("i", "")
|
|
else:
|
|
o = o_term.get("v", "")
|
|
parsed.append((s, p, o))
|
|
|
|
entity = ExplainEntity.from_triples(
|
|
"urn:trustgraph:question:abc123", parsed
|
|
)
|
|
|
|
assert entity.entity_type == "question"
|
|
assert entity.query == "What is the internet?"
|
|
assert entity.question_type == "graph-rag"
|