mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-06-26 23:19:38 +02:00
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:
parent
89e13a756a
commit
153ae9ad30
28 changed files with 661 additions and 350 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue