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

@ -9,7 +9,7 @@ Following the TEST_STRATEGY.md approach for integration testing.
import pytest
import json
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, ANY, patch
from trustgraph.agent.react.agent_manager import AgentManager
from trustgraph.agent.react.tools import KnowledgeQueryImpl, TextCompletionImpl, McpToolImpl
@ -187,7 +187,7 @@ Final Answer: Machine learning is a field of AI that enables computers to learn
# Verify tool was executed
graph_rag_client = mock_flow_context("graph-rag-request")
graph_rag_client.rag.assert_called_once_with("What is machine learning?", collection="default")
graph_rag_client.rag.assert_called_once_with("What is machine learning?", collection="default", explain_callback=ANY, parent_uri=ANY)
@pytest.mark.asyncio
async def test_agent_manager_react_with_final_answer(self, agent_manager, mock_flow_context):
@ -272,7 +272,7 @@ Args: {{
# Verify correct service was called
if tool_name == "knowledge_query":
mock_flow_context("graph-rag-request").rag.assert_called_with("test question", collection="default")
mock_flow_context("graph-rag-request").rag.assert_called_with("test question", collection="default", explain_callback=ANY, parent_uri=ANY)
elif tool_name == "text_completion":
mock_flow_context("prompt-request").question.assert_called()
@ -726,7 +726,7 @@ Final Answer: {
# Assert
graph_rag_client = mock_flow_context("graph-rag-request")
graph_rag_client.rag.assert_called_once_with("What is AI?", collection="default")
graph_rag_client.rag.assert_called_once_with("What is AI?", collection="default", explain_callback=ANY, parent_uri=ANY)
@pytest.mark.asyncio
async def test_knowledge_query_with_custom_collection(self, mock_flow_context):
@ -739,7 +739,7 @@ Final Answer: {
# Assert
graph_rag_client = mock_flow_context("graph-rag-request")
graph_rag_client.rag.assert_called_once_with("What is machine learning?", collection="custom_collection")
graph_rag_client.rag.assert_called_once_with("What is machine learning?", collection="custom_collection", explain_callback=ANY, parent_uri=ANY)
@pytest.mark.asyncio
async def test_knowledge_query_with_none_collection(self, mock_flow_context):
@ -752,7 +752,7 @@ Final Answer: {
# Assert
graph_rag_client = mock_flow_context("graph-rag-request")
graph_rag_client.rag.assert_called_once_with("Explain neural networks", collection="default")
graph_rag_client.rag.assert_called_once_with("Explain neural networks", collection="default", explain_callback=ANY, parent_uri=ANY)
@pytest.mark.asyncio
async def test_agent_manager_knowledge_query_collection_integration(self, mock_flow_context):
@ -810,7 +810,7 @@ Args: {
# Verify the custom collection was used
graph_rag_client = mock_flow_context("graph-rag-request")
graph_rag_client.rag.assert_called_once_with("Latest AI research?", collection="research_papers")
graph_rag_client.rag.assert_called_once_with("Latest AI research?", collection="research_papers", explain_callback=ANY, parent_uri=ANY)
@pytest.mark.asyncio
async def test_knowledge_query_multiple_collections(self, mock_flow_context):
@ -840,4 +840,4 @@ Args: {
# Verify correct collection was used
graph_rag_client = mock_flow_context("graph-rag-request")
graph_rag_client.rag.assert_called_once_with(question, collection=expected_collection)
graph_rag_client.rag.assert_called_once_with(question, collection=expected_collection, explain_callback=ANY, parent_uri=ANY)

View file

@ -39,7 +39,7 @@ class TestAgentServiceNonStreaming:
mock_agent_manager_class.return_value = mock_agent_instance
# Mock react to call think and observe callbacks
async def mock_react(question, history, think, observe, answer, context, streaming):
async def mock_react(question, history, think, observe, answer, context, streaming, on_action=None):
await think("I need to solve this.", is_final=True)
await observe("The answer is 4.", is_final=True)
return Final(thought="Final answer", final="4")
@ -76,11 +76,22 @@ class TestAgentServiceNonStreaming:
# Execute
await processor.on_request(msg, consumer, flow)
# Verify: should have 3 responses (thought, observation, answer)
assert len(sent_responses) == 3, f"Expected 3 responses, got {len(sent_responses)}"
# Filter out explain events — those are always sent now
content_responses = [
r for r in sent_responses if r.chunk_type != "explain"
]
explain_responses = [
r for r in sent_responses if r.chunk_type == "explain"
]
# Should have explain events for session, iteration, observation, and final
assert len(explain_responses) >= 1, "Expected at least 1 explain event"
# Should have 3 content responses (thought, observation, answer)
assert len(content_responses) == 3, f"Expected 3 content responses, got {len(content_responses)}"
# Check thought message
thought_response = sent_responses[0]
thought_response = content_responses[0]
assert isinstance(thought_response, AgentResponse)
assert thought_response.chunk_type == "thought"
assert thought_response.content == "I need to solve this."
@ -88,7 +99,7 @@ class TestAgentServiceNonStreaming:
assert thought_response.end_of_dialog is False, "Thought message must have end_of_dialog=False"
# Check observation message
observation_response = sent_responses[1]
observation_response = content_responses[1]
assert isinstance(observation_response, AgentResponse)
assert observation_response.chunk_type == "observation"
assert observation_response.content == "The answer is 4."
@ -120,7 +131,7 @@ class TestAgentServiceNonStreaming:
mock_agent_manager_class.return_value = mock_agent_instance
# Mock react to return Final directly
async def mock_react(question, history, think, observe, answer, context, streaming):
async def mock_react(question, history, think, observe, answer, context, streaming, on_action=None):
return Final(thought="Final answer", final="4")
mock_agent_instance.react = mock_react
@ -155,11 +166,22 @@ class TestAgentServiceNonStreaming:
# Execute
await processor.on_request(msg, consumer, flow)
# Verify: should have 1 response (final answer)
assert len(sent_responses) == 1, f"Expected 1 response, got {len(sent_responses)}"
# Filter out explain events — those are always sent now
content_responses = [
r for r in sent_responses if r.chunk_type != "explain"
]
explain_responses = [
r for r in sent_responses if r.chunk_type == "explain"
]
# Should have explain events for session and final
assert len(explain_responses) >= 1, "Expected at least 1 explain event"
# Should have 1 content response (final answer)
assert len(content_responses) == 1, f"Expected 1 content response, got {len(content_responses)}"
# Check final answer message
answer_response = sent_responses[0]
answer_response = content_responses[0]
assert isinstance(answer_response, AgentResponse)
assert answer_response.chunk_type == "answer"
assert answer_response.content == "4"

View file

@ -13,6 +13,7 @@ from trustgraph.api.explainability import (
StepResult,
Synthesis,
Analysis,
Observation,
Conclusion,
TG_DECOMPOSITION,
TG_FINDING,
@ -20,6 +21,7 @@ from trustgraph.api.explainability import (
TG_STEP_RESULT,
TG_SYNTHESIS,
TG_ANSWER_TYPE,
TG_OBSERVATION_TYPE,
TG_ANALYSIS,
TG_CONCLUSION,
TG_DOCUMENT,
@ -74,6 +76,11 @@ class TestFromTriplesDispatch:
entity = ExplainEntity.from_triples("urn:a", triples)
assert isinstance(entity, Analysis)
def test_dispatches_observation(self):
triples = _make_triples("urn:o", [PROV_ENTITY, TG_OBSERVATION_TYPE])
entity = ExplainEntity.from_triples("urn:o", triples)
assert isinstance(entity, Observation)
def test_dispatches_conclusion_unchanged(self):
triples = _make_triples("urn:c",
[PROV_ENTITY, TG_CONCLUSION, TG_ANSWER_TYPE])

View file

@ -14,7 +14,7 @@ from trustgraph.provenance import (
from trustgraph.provenance.namespaces import (
RDF_TYPE, RDFS_LABEL,
PROV_ENTITY, PROV_WAS_DERIVED_FROM, PROV_WAS_GENERATED_BY,
PROV_ENTITY, PROV_WAS_DERIVED_FROM,
TG_DECOMPOSITION, TG_FINDING, TG_PLAN_TYPE, TG_STEP_RESULT,
TG_SYNTHESIS, TG_ANSWER_TYPE, TG_DOCUMENT,
TG_SUBAGENT_GOAL, TG_PLAN_STEP,
@ -63,7 +63,7 @@ class TestDecompositionTriples:
"urn:decompose", "urn:session", ["goal-a"],
)
ts = _triple_set(triples)
assert ("urn:decompose", PROV_WAS_GENERATED_BY, "urn:session") in ts
assert ("urn:decompose", PROV_WAS_DERIVED_FROM, "urn:session") in ts
def test_includes_goals(self):
goals = ["What is X?", "What is Y?", "What is Z?"]
@ -141,7 +141,7 @@ class TestPlanTriples:
"urn:plan", "urn:session", ["step-a"],
)
ts = _triple_set(triples)
assert ("urn:plan", PROV_WAS_GENERATED_BY, "urn:session") in ts
assert ("urn:plan", PROV_WAS_DERIVED_FROM, "urn:session") in ts
def test_includes_steps(self):
steps = ["Define X", "Research Y", "Analyse Z"]

View file

@ -10,16 +10,18 @@ from trustgraph.schema import Triple, Term, IRI, LITERAL
from trustgraph.provenance.agent import (
agent_session_triples,
agent_iteration_triples,
agent_observation_triples,
agent_final_triples,
)
from trustgraph.provenance.namespaces import (
RDF_TYPE, RDFS_LABEL,
PROV_ACTIVITY, PROV_ENTITY, PROV_WAS_DERIVED_FROM,
PROV_WAS_GENERATED_BY, PROV_STARTED_AT_TIME,
TG_QUERY, TG_THOUGHT, TG_ACTION, TG_ARGUMENTS, TG_OBSERVATION,
PROV_ENTITY, PROV_WAS_DERIVED_FROM,
PROV_STARTED_AT_TIME,
TG_QUERY, TG_THOUGHT, TG_ACTION, TG_ARGUMENTS,
TG_QUESTION, TG_ANALYSIS, TG_CONCLUSION, TG_DOCUMENT,
TG_ANSWER_TYPE, TG_REFLECTION_TYPE, TG_THOUGHT_TYPE, TG_OBSERVATION_TYPE,
TG_TOOL_USE,
TG_AGENT_QUESTION,
)
@ -63,7 +65,7 @@ class TestAgentSessionTriples:
triples = agent_session_triples(
self.SESSION_URI, "What is X?", "2024-01-01T00:00:00Z"
)
assert has_type(triples, self.SESSION_URI, PROV_ACTIVITY)
assert has_type(triples, self.SESSION_URI, PROV_ENTITY)
assert has_type(triples, self.SESSION_URI, TG_QUESTION)
assert has_type(triples, self.SESSION_URI, TG_AGENT_QUESTION)
@ -121,19 +123,17 @@ class TestAgentIterationTriples:
)
assert has_type(triples, self.ITER_URI, PROV_ENTITY)
assert has_type(triples, self.ITER_URI, TG_ANALYSIS)
assert has_type(triples, self.ITER_URI, TG_TOOL_USE)
def test_first_iteration_generated_by_question(self):
"""First iteration uses wasGeneratedBy to link to question activity."""
def test_first_iteration_derived_from_question(self):
"""First iteration uses wasDerivedFrom to link to question entity."""
triples = agent_iteration_triples(
self.ITER_URI, question_uri=self.SESSION_URI,
action="search",
)
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.ITER_URI)
assert gen is not None
assert gen.o.iri == self.SESSION_URI
# Should NOT have wasDerivedFrom
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.ITER_URI)
assert derived is None
assert derived is not None
assert derived.o.iri == self.SESSION_URI
def test_subsequent_iteration_derived_from_previous(self):
"""Subsequent iterations use wasDerivedFrom to link to previous iteration."""
@ -144,9 +144,6 @@ class TestAgentIterationTriples:
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.ITER_URI)
assert derived is not None
assert derived.o.iri == self.PREV_URI
# Should NOT have wasGeneratedBy
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.ITER_URI)
assert gen is None
def test_iteration_label_includes_action(self):
triples = agent_iteration_triples(
@ -174,40 +171,24 @@ class TestAgentIterationTriples:
# Thought has correct types
assert has_type(triples, thought_uri, TG_REFLECTION_TYPE)
assert has_type(triples, thought_uri, TG_THOUGHT_TYPE)
# Thought was generated by iteration
gen = find_triple(triples, PROV_WAS_GENERATED_BY, thought_uri)
assert gen is not None
assert gen.o.iri == self.ITER_URI
# Thought was derived from iteration
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, thought_uri)
assert derived is not None
assert derived.o.iri == self.ITER_URI
# Thought has document reference
doc = find_triple(triples, TG_DOCUMENT, thought_uri)
assert doc is not None
assert doc.o.iri == thought_doc
def test_iteration_observation_sub_entity(self):
"""Observation is a sub-entity with Reflection and Observation types."""
obs_uri = "urn:trustgraph:agent:test-session/i1/observation"
obs_doc = "urn:doc:obs-1"
def test_iteration_no_observation_sub_entity(self):
"""Iteration no longer embeds observation — it's a separate entity."""
triples = agent_iteration_triples(
self.ITER_URI, question_uri=self.SESSION_URI,
action="search",
observation_uri=obs_uri,
observation_document_id=obs_doc,
)
# Iteration links to observation sub-entity
obs_link = find_triple(triples, TG_OBSERVATION, self.ITER_URI)
assert obs_link is not None
assert obs_link.o.iri == obs_uri
# Observation has correct types
assert has_type(triples, obs_uri, TG_REFLECTION_TYPE)
assert has_type(triples, obs_uri, TG_OBSERVATION_TYPE)
# Observation was generated by iteration
gen = find_triple(triples, PROV_WAS_GENERATED_BY, obs_uri)
assert gen is not None
assert gen.o.iri == self.ITER_URI
# Observation has document reference
doc = find_triple(triples, TG_DOCUMENT, obs_uri)
assert doc is not None
assert doc.o.iri == obs_doc
# No TG_OBSERVATION predicate on the iteration
for t in triples:
assert "observation" not in t.p.iri.lower() or "Observation" not in t.p.iri
def test_iteration_action_recorded(self):
triples = agent_iteration_triples(
@ -240,19 +221,17 @@ class TestAgentIterationTriples:
parsed = json.loads(arguments.o.value)
assert parsed == {}
def test_iteration_no_thought_or_observation(self):
"""Minimal iteration with just action — no thought or observation triples."""
def test_iteration_no_thought(self):
"""Minimal iteration with just action — no thought triples."""
triples = agent_iteration_triples(
self.ITER_URI, question_uri=self.SESSION_URI,
action="noop",
)
thought = find_triple(triples, TG_THOUGHT, self.ITER_URI)
obs = find_triple(triples, TG_OBSERVATION, self.ITER_URI)
assert thought is None
assert obs is None
def test_iteration_chaining(self):
"""First iteration uses wasGeneratedBy, second uses wasDerivedFrom."""
"""Both first and second iterations use wasDerivedFrom."""
iter1_uri = "urn:trustgraph:agent:sess/i1"
iter2_uri = "urn:trustgraph:agent:sess/i2"
@ -263,13 +242,62 @@ class TestAgentIterationTriples:
iter2_uri, previous_uri=iter1_uri, action="step2",
)
gen1 = find_triple(triples1, PROV_WAS_GENERATED_BY, iter1_uri)
assert gen1.o.iri == self.SESSION_URI
derived1 = find_triple(triples1, PROV_WAS_DERIVED_FROM, iter1_uri)
assert derived1.o.iri == self.SESSION_URI
derived2 = find_triple(triples2, PROV_WAS_DERIVED_FROM, iter2_uri)
assert derived2.o.iri == iter1_uri
# ---------------------------------------------------------------------------
# agent_observation_triples
# ---------------------------------------------------------------------------
class TestAgentObservationTriples:
OBS_URI = "urn:trustgraph:agent:test-session/i1/observation"
ITER_URI = "urn:trustgraph:agent:test-session/i1"
def test_observation_types(self):
triples = agent_observation_triples(
self.OBS_URI, self.ITER_URI,
)
assert has_type(triples, self.OBS_URI, PROV_ENTITY)
assert has_type(triples, self.OBS_URI, TG_OBSERVATION_TYPE)
def test_observation_derived_from_iteration(self):
triples = agent_observation_triples(
self.OBS_URI, self.ITER_URI,
)
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.OBS_URI)
assert derived is not None
assert derived.o.iri == self.ITER_URI
def test_observation_label(self):
triples = agent_observation_triples(
self.OBS_URI, self.ITER_URI,
)
label = find_triple(triples, RDFS_LABEL, self.OBS_URI)
assert label is not None
assert label.o.value == "Observation"
def test_observation_document(self):
doc_id = "urn:doc:obs-1"
triples = agent_observation_triples(
self.OBS_URI, self.ITER_URI, document_id=doc_id,
)
doc = find_triple(triples, TG_DOCUMENT, self.OBS_URI)
assert doc is not None
assert doc.o.iri == doc_id
def test_observation_no_document(self):
triples = agent_observation_triples(
self.OBS_URI, self.ITER_URI,
)
doc = find_triple(triples, TG_DOCUMENT, self.OBS_URI)
assert doc is None
# ---------------------------------------------------------------------------
# agent_final_triples
# ---------------------------------------------------------------------------
@ -296,19 +324,15 @@ class TestAgentFinalTriples:
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.FINAL_URI)
assert derived is not None
assert derived.o.iri == self.PREV_URI
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.FINAL_URI)
assert gen is None
def test_final_generated_by_question_when_no_iterations(self):
"""When agent answers immediately, final uses wasGeneratedBy."""
def test_final_derived_from_question_when_no_iterations(self):
"""When agent answers immediately, final uses wasDerivedFrom to question."""
triples = agent_final_triples(
self.FINAL_URI, question_uri=self.SESSION_URI,
)
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.FINAL_URI)
assert gen is not None
assert gen.o.iri == self.SESSION_URI
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.FINAL_URI)
assert derived is None
assert derived is not None
assert derived.o.iri == self.SESSION_URI
def test_final_label(self):
triples = agent_final_triples(

View file

@ -16,6 +16,7 @@ from trustgraph.api.explainability import (
Synthesis,
Reflection,
Analysis,
Observation,
Conclusion,
parse_edge_selection_triples,
extract_term_value,
@ -23,12 +24,12 @@ from trustgraph.api.explainability import (
ExplainabilityClient,
TG_QUERY, TG_EDGE_COUNT, TG_SELECTED_EDGE, TG_EDGE, TG_REASONING,
TG_DOCUMENT, TG_CHUNK_COUNT, TG_CONCEPT, TG_ENTITY,
TG_THOUGHT, TG_ACTION, TG_ARGUMENTS, TG_OBSERVATION,
TG_THOUGHT, TG_ACTION, TG_ARGUMENTS,
TG_QUESTION, TG_GROUNDING, TG_EXPLORATION, TG_FOCUS, TG_SYNTHESIS,
TG_ANALYSIS, TG_CONCLUSION,
TG_REFLECTION_TYPE, TG_THOUGHT_TYPE, TG_OBSERVATION_TYPE,
TG_GRAPH_RAG_QUESTION, TG_DOC_RAG_QUESTION, TG_AGENT_QUESTION,
PROV_STARTED_AT_TIME, PROV_WAS_DERIVED_FROM, PROV_WAS_GENERATED_BY,
PROV_STARTED_AT_TIME, PROV_WAS_DERIVED_FROM,
RDF_TYPE, RDFS_LABEL,
)
@ -180,14 +181,30 @@ class TestExplainEntityFromTriples:
("urn:ana:1", TG_ACTION, "graph-rag-query"),
("urn:ana:1", TG_ARGUMENTS, '{"query": "test"}'),
("urn:ana:1", TG_THOUGHT, "urn:ref:thought-1"),
("urn:ana:1", TG_OBSERVATION, "urn:ref:obs-1"),
]
entity = ExplainEntity.from_triples("urn:ana:1", triples)
assert isinstance(entity, Analysis)
assert entity.action == "graph-rag-query"
assert entity.arguments == '{"query": "test"}'
assert entity.thought == "urn:ref:thought-1"
assert entity.observation == "urn:ref:obs-1"
def test_observation(self):
triples = [
("urn:obs:1", RDF_TYPE, TG_OBSERVATION_TYPE),
("urn:obs:1", TG_DOCUMENT, "urn:doc:obs-content"),
]
entity = ExplainEntity.from_triples("urn:obs:1", triples)
assert isinstance(entity, Observation)
assert entity.document == "urn:doc:obs-content"
assert entity.entity_type == "observation"
def test_observation_no_document(self):
triples = [
("urn:obs:2", RDF_TYPE, TG_OBSERVATION_TYPE),
]
entity = ExplainEntity.from_triples("urn:obs:2", triples)
assert isinstance(entity, Observation)
assert entity.document == ""
def test_conclusion_with_document(self):
triples = [

View file

@ -500,7 +500,7 @@ class TestQuestionTriples:
def test_question_types(self):
triples = question_triples(self.Q_URI, "What is AI?", "2024-01-01T00:00:00Z")
assert has_type(triples, self.Q_URI, PROV_ACTIVITY)
assert has_type(triples, self.Q_URI, PROV_ENTITY)
assert has_type(triples, self.Q_URI, TG_QUESTION)
assert has_type(triples, self.Q_URI, TG_GRAPH_RAG_QUESTION)
@ -543,11 +543,11 @@ class TestGroundingTriples:
assert has_type(triples, self.GND_URI, PROV_ENTITY)
assert has_type(triples, self.GND_URI, TG_GROUNDING)
def test_grounding_generated_by_question(self):
def test_grounding_derived_from_question(self):
triples = grounding_triples(self.GND_URI, self.Q_URI, ["AI"])
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.GND_URI)
assert gen is not None
assert gen.o.iri == self.Q_URI
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.GND_URI)
assert derived is not None
assert derived.o.iri == self.Q_URI
def test_grounding_concepts(self):
triples = grounding_triples(self.GND_URI, self.Q_URI, ["AI", "ML", "robots"])
@ -730,7 +730,7 @@ class TestDocRagQuestionTriples:
def test_docrag_question_types(self):
triples = docrag_question_triples(self.Q_URI, "Find info", "2024-01-01T00:00:00Z")
assert has_type(triples, self.Q_URI, PROV_ACTIVITY)
assert has_type(triples, self.Q_URI, PROV_ENTITY)
assert has_type(triples, self.Q_URI, TG_QUESTION)
assert has_type(triples, self.Q_URI, TG_DOC_RAG_QUESTION)