mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-26 08:56:21 +02:00
Additional agent DAG tests (#750)
- test_agent_provenance.py: test_session_parent_uri, test_session_no_parent_uri, and 6 synthesis tests (types, single/multiple parents, document, label) - test_on_action_callback.py: 3 tests — fires before tool, skipped for Final, works when None - test_callback_message_id.py: 7 tests — message_id on think/observe/ answer callbacks (streaming + non-streaming) and send_final_response - test_parse_chunk_message_id.py (5 tests) - _parse_chunk propagates message_id for thought, observation, answer; handles missing gracefully - test_explainability_parsing.py (+1) - test_dispatches_analysis_with_tooluse - Analysis+ToolUse mixin still dispatches to Analysis - test_explainability.py (+1) - test_observation_found_via_subtrace_synthesis - chain walker follows from sub-trace Synthesis to find Observation and Conclusion in correct order - test_agent_provenance.py (+8) - session parent_uri (2), synthesis single/multiple parents, types, document, label (6)
This commit is contained in:
parent
3ba6a3238f
commit
dbf8daa74a
7 changed files with 733 additions and 1 deletions
122
tests/unit/test_agent/test_callback_message_id.py
Normal file
122
tests/unit/test_agent/test_callback_message_id.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""
|
||||
Tests that streaming callbacks set message_id on AgentResponse.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from trustgraph.agent.orchestrator.pattern_base import PatternBase
|
||||
from trustgraph.schema import AgentResponse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pattern():
|
||||
processor = MagicMock()
|
||||
return PatternBase(processor)
|
||||
|
||||
|
||||
class TestThinkCallbackMessageId:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_think_has_message_id(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
msg_id = "urn:trustgraph:agent:sess/i1/thought"
|
||||
think = pattern.make_think_callback(capture, streaming=True, message_id=msg_id)
|
||||
await think("hello", is_final=False)
|
||||
|
||||
assert len(responses) == 1
|
||||
assert responses[0].message_id == msg_id
|
||||
assert responses[0].chunk_type == "thought"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_streaming_think_has_message_id(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
msg_id = "urn:trustgraph:agent:sess/i1/thought"
|
||||
think = pattern.make_think_callback(capture, streaming=False, message_id=msg_id)
|
||||
await think("hello")
|
||||
|
||||
assert responses[0].message_id == msg_id
|
||||
assert responses[0].end_of_message is True
|
||||
|
||||
|
||||
class TestObserveCallbackMessageId:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_observe_has_message_id(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
msg_id = "urn:trustgraph:agent:sess/i1/observation"
|
||||
observe = pattern.make_observe_callback(capture, streaming=True, message_id=msg_id)
|
||||
await observe("result", is_final=True)
|
||||
|
||||
assert responses[0].message_id == msg_id
|
||||
assert responses[0].chunk_type == "observation"
|
||||
|
||||
|
||||
class TestAnswerCallbackMessageId:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_answer_has_message_id(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
msg_id = "urn:trustgraph:agent:sess/final"
|
||||
answer = pattern.make_answer_callback(capture, streaming=True, message_id=msg_id)
|
||||
await answer("the answer")
|
||||
|
||||
assert responses[0].message_id == msg_id
|
||||
assert responses[0].chunk_type == "answer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_message_id_default(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
answer = pattern.make_answer_callback(capture, streaming=True)
|
||||
await answer("the answer")
|
||||
|
||||
assert responses[0].message_id == ""
|
||||
|
||||
|
||||
class TestSendFinalResponseMessageId:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_final_has_message_id(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
msg_id = "urn:trustgraph:agent:sess/final"
|
||||
await pattern.send_final_response(
|
||||
capture, streaming=True, answer_text="answer",
|
||||
message_id=msg_id,
|
||||
)
|
||||
|
||||
# Should get content chunk + end-of-dialog marker
|
||||
assert all(r.message_id == msg_id for r in responses)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_streaming_final_has_message_id(self, pattern):
|
||||
responses = []
|
||||
async def capture(r):
|
||||
responses.append(r)
|
||||
|
||||
msg_id = "urn:trustgraph:agent:sess/final"
|
||||
await pattern.send_final_response(
|
||||
capture, streaming=False, answer_text="answer",
|
||||
message_id=msg_id,
|
||||
)
|
||||
|
||||
assert len(responses) == 1
|
||||
assert responses[0].message_id == msg_id
|
||||
assert responses[0].end_of_dialog is True
|
||||
|
|
@ -22,6 +22,7 @@ from trustgraph.api.explainability import (
|
|||
TG_SYNTHESIS,
|
||||
TG_ANSWER_TYPE,
|
||||
TG_OBSERVATION_TYPE,
|
||||
TG_TOOL_USE,
|
||||
TG_ANALYSIS,
|
||||
TG_CONCLUSION,
|
||||
TG_DOCUMENT,
|
||||
|
|
@ -76,6 +77,13 @@ class TestFromTriplesDispatch:
|
|||
entity = ExplainEntity.from_triples("urn:a", triples)
|
||||
assert isinstance(entity, Analysis)
|
||||
|
||||
def test_dispatches_analysis_with_tooluse(self):
|
||||
"""Analysis+ToolUse mixin still dispatches to Analysis."""
|
||||
triples = _make_triples("urn:a",
|
||||
[PROV_ENTITY, TG_ANALYSIS, TG_TOOL_USE])
|
||||
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)
|
||||
|
|
|
|||
132
tests/unit/test_agent/test_on_action_callback.py
Normal file
132
tests/unit/test_agent/test_on_action_callback.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
Tests for the on_action callback in react() — verifies that it fires
|
||||
after action selection but before tool execution.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from trustgraph.agent.react.agent_manager import AgentManager
|
||||
from trustgraph.agent.react.types import Action, Final, Tool, Argument
|
||||
|
||||
|
||||
class TestOnActionCallback:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_action_called_for_tool_use(self):
|
||||
"""on_action fires when react() selects a tool (not Final)."""
|
||||
call_log = []
|
||||
|
||||
async def fake_on_action(act):
|
||||
call_log.append(("on_action", act.name))
|
||||
|
||||
# Tool that records when it's invoked
|
||||
async def tool_invoke(**kwargs):
|
||||
call_log.append(("tool_invoke",))
|
||||
return "tool result"
|
||||
|
||||
tool_impl = MagicMock()
|
||||
tool_impl.return_value.invoke = AsyncMock(side_effect=tool_invoke)
|
||||
|
||||
tools = {
|
||||
"search": Tool(
|
||||
name="search",
|
||||
description="Search",
|
||||
implementation=tool_impl,
|
||||
arguments=[Argument(name="query", type="string", description="q")],
|
||||
config={},
|
||||
),
|
||||
}
|
||||
|
||||
agent = AgentManager(tools=tools)
|
||||
|
||||
# Mock reason() to return an Action
|
||||
action = Action(thought="thinking", name="search", arguments={"query": "test"}, observation="")
|
||||
agent.reason = AsyncMock(return_value=action)
|
||||
|
||||
think = AsyncMock()
|
||||
observe = AsyncMock()
|
||||
context = MagicMock()
|
||||
|
||||
await agent.react(
|
||||
question="test",
|
||||
history=[],
|
||||
think=think,
|
||||
observe=observe,
|
||||
context=context,
|
||||
on_action=fake_on_action,
|
||||
)
|
||||
|
||||
# on_action should fire before tool_invoke
|
||||
assert len(call_log) == 2
|
||||
assert call_log[0] == ("on_action", "search")
|
||||
assert call_log[1] == ("tool_invoke",)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_action_not_called_for_final(self):
|
||||
"""on_action does not fire when react() returns Final."""
|
||||
called = []
|
||||
|
||||
async def fake_on_action(act):
|
||||
called.append(act)
|
||||
|
||||
agent = AgentManager(tools={})
|
||||
agent.reason = AsyncMock(
|
||||
return_value=Final(thought="done", final="answer")
|
||||
)
|
||||
|
||||
think = AsyncMock()
|
||||
observe = AsyncMock()
|
||||
context = MagicMock()
|
||||
|
||||
result = await agent.react(
|
||||
question="test",
|
||||
history=[],
|
||||
think=think,
|
||||
observe=observe,
|
||||
context=context,
|
||||
on_action=fake_on_action,
|
||||
)
|
||||
|
||||
assert isinstance(result, Final)
|
||||
assert len(called) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_action_none_accepted(self):
|
||||
"""react() works fine when on_action is None (default)."""
|
||||
async def tool_invoke(**kwargs):
|
||||
return "result"
|
||||
|
||||
tool_impl = MagicMock()
|
||||
tool_impl.return_value.invoke = AsyncMock(side_effect=tool_invoke)
|
||||
|
||||
tools = {
|
||||
"search": Tool(
|
||||
name="search",
|
||||
description="Search",
|
||||
implementation=tool_impl,
|
||||
arguments=[],
|
||||
config={},
|
||||
),
|
||||
}
|
||||
|
||||
agent = AgentManager(tools=tools)
|
||||
agent.reason = AsyncMock(
|
||||
return_value=Action(thought="t", name="search", arguments={}, observation="")
|
||||
)
|
||||
|
||||
think = AsyncMock()
|
||||
observe = AsyncMock()
|
||||
context = MagicMock()
|
||||
|
||||
result = await agent.react(
|
||||
question="test",
|
||||
history=[],
|
||||
think=think,
|
||||
observe=observe,
|
||||
context=context,
|
||||
# on_action not passed — defaults to None
|
||||
)
|
||||
|
||||
assert isinstance(result, Action)
|
||||
assert result.observation == "result"
|
||||
74
tests/unit/test_agent/test_parse_chunk_message_id.py
Normal file
74
tests/unit/test_agent/test_parse_chunk_message_id.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
Tests that _parse_chunk propagates message_id from wire format
|
||||
to AgentThought, AgentObservation, and AgentAnswer.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from trustgraph.api.socket_client import SocketClient
|
||||
from trustgraph.api.types import AgentThought, AgentObservation, AgentAnswer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
# We only need _parse_chunk — don't connect
|
||||
c = object.__new__(SocketClient)
|
||||
return c
|
||||
|
||||
|
||||
class TestParseChunkMessageId:
|
||||
|
||||
def test_thought_message_id(self, client):
|
||||
resp = {
|
||||
"chunk_type": "thought",
|
||||
"content": "thinking...",
|
||||
"end_of_message": False,
|
||||
"message_id": "urn:trustgraph:agent:sess/i1/thought",
|
||||
}
|
||||
chunk = client._parse_chunk(resp)
|
||||
assert isinstance(chunk, AgentThought)
|
||||
assert chunk.message_id == "urn:trustgraph:agent:sess/i1/thought"
|
||||
|
||||
def test_observation_message_id(self, client):
|
||||
resp = {
|
||||
"chunk_type": "observation",
|
||||
"content": "result",
|
||||
"end_of_message": True,
|
||||
"message_id": "urn:trustgraph:agent:sess/i1/observation",
|
||||
}
|
||||
chunk = client._parse_chunk(resp)
|
||||
assert isinstance(chunk, AgentObservation)
|
||||
assert chunk.message_id == "urn:trustgraph:agent:sess/i1/observation"
|
||||
|
||||
def test_answer_message_id(self, client):
|
||||
resp = {
|
||||
"chunk_type": "answer",
|
||||
"content": "the answer",
|
||||
"end_of_message": False,
|
||||
"end_of_dialog": False,
|
||||
"message_id": "urn:trustgraph:agent:sess/final",
|
||||
}
|
||||
chunk = client._parse_chunk(resp)
|
||||
assert isinstance(chunk, AgentAnswer)
|
||||
assert chunk.message_id == "urn:trustgraph:agent:sess/final"
|
||||
|
||||
def test_thought_missing_message_id(self, client):
|
||||
resp = {
|
||||
"chunk_type": "thought",
|
||||
"content": "thinking...",
|
||||
"end_of_message": False,
|
||||
}
|
||||
chunk = client._parse_chunk(resp)
|
||||
assert isinstance(chunk, AgentThought)
|
||||
assert chunk.message_id == ""
|
||||
|
||||
def test_answer_missing_message_id(self, client):
|
||||
resp = {
|
||||
"chunk_type": "answer",
|
||||
"content": "answer",
|
||||
"end_of_message": True,
|
||||
"end_of_dialog": True,
|
||||
}
|
||||
chunk = client._parse_chunk(resp)
|
||||
assert isinstance(chunk, AgentAnswer)
|
||||
assert chunk.message_id == ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue