Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
"""
|
2026-03-13 21:56:12 -07:00
|
|
|
Tests for ManagedServer / tool-parser integration.
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
|
|
|
|
|
Validates that:
|
2026-03-13 21:56:12 -07:00
|
|
|
1. The installed atroposlib API still matches Hermes's expectations
|
|
|
|
|
2. Hermes's parser registry remains compatible with ManagedServer parsing
|
|
|
|
|
3. HermesAgentBaseEnv wires the selected parser into ServerManager correctly
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
|
|
|
|
|
These tests verify the contract between hermes-agent's environments/ code
|
|
|
|
|
and atroposlib's ManagedServer. They detect API incompatibilities early.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import inspect
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
|
|
2026-03-09 23:33:24 -05:00
|
|
|
try:
|
|
|
|
|
import atroposlib # noqa: F401
|
|
|
|
|
except ImportError:
|
|
|
|
|
pytest.skip("atroposlib not installed", allow_module_level=True)
|
|
|
|
|
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
|
|
|
|
|
class TestManagedServerAPI:
|
|
|
|
|
"""Test that ManagedServer's API matches what hermes-agent expects."""
|
|
|
|
|
|
|
|
|
|
def test_managed_server_init_signature(self):
|
|
|
|
|
"""ManagedServer should accept tool_call_parser parameter."""
|
|
|
|
|
from atroposlib.envs.server_handling.managed_server import ManagedServer
|
|
|
|
|
|
|
|
|
|
sig = inspect.signature(ManagedServer.__init__)
|
|
|
|
|
params = list(sig.parameters.keys())
|
|
|
|
|
|
|
|
|
|
# Core params that must exist
|
|
|
|
|
assert "self" in params
|
|
|
|
|
assert "server" in params
|
|
|
|
|
assert "tokenizer" in params
|
|
|
|
|
assert "track_tree" in params
|
|
|
|
|
|
|
|
|
|
# tool_call_parser — required for tool_call_support branch
|
|
|
|
|
# If this fails, atroposlib hasn't been updated to tool_call_support
|
|
|
|
|
has_tool_parser = "tool_call_parser" in params
|
|
|
|
|
if not has_tool_parser:
|
|
|
|
|
pytest.skip(
|
|
|
|
|
"ManagedServer does not have tool_call_parser param — "
|
|
|
|
|
"baseline atroposlib (pre tool_call_support branch)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_server_manager_managed_server_signature(self):
|
|
|
|
|
"""ServerManager.managed_server() should accept tool_call_parser."""
|
|
|
|
|
from atroposlib.envs.server_handling.server_manager import ServerManager
|
|
|
|
|
|
|
|
|
|
sig = inspect.signature(ServerManager.managed_server)
|
|
|
|
|
params = list(sig.parameters.keys())
|
|
|
|
|
|
|
|
|
|
assert "self" in params
|
|
|
|
|
assert "tokenizer" in params
|
|
|
|
|
|
|
|
|
|
has_tool_parser = "tool_call_parser" in params
|
|
|
|
|
if not has_tool_parser:
|
|
|
|
|
pytest.skip(
|
|
|
|
|
"ServerManager.managed_server() does not have tool_call_parser param — "
|
|
|
|
|
"baseline atroposlib (pre tool_call_support branch)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_managed_server_chat_template_kwargs(self):
|
|
|
|
|
"""ManagedServer should have CHAT_TEMPLATE_KWARGS for forwarding tools/thinking."""
|
|
|
|
|
from atroposlib.envs.server_handling.managed_server import ManagedServer
|
|
|
|
|
|
|
|
|
|
if not hasattr(ManagedServer, "CHAT_TEMPLATE_KWARGS"):
|
|
|
|
|
pytest.skip(
|
|
|
|
|
"ManagedServer does not have CHAT_TEMPLATE_KWARGS — "
|
|
|
|
|
"baseline atroposlib (pre tool_call_support branch)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
kwargs = ManagedServer.CHAT_TEMPLATE_KWARGS
|
|
|
|
|
assert "tools" in kwargs, "tools must be in CHAT_TEMPLATE_KWARGS"
|
|
|
|
|
|
|
|
|
|
def test_no_get_logprobs_method(self):
|
|
|
|
|
"""get_logprobs should be removed in tool_call_support branch."""
|
|
|
|
|
from atroposlib.envs.server_handling.managed_server import ManagedServer
|
|
|
|
|
|
|
|
|
|
# In baseline, get_logprobs exists. In tool_call_support, it's removed.
|
|
|
|
|
# We just note the state — not a hard fail either way.
|
|
|
|
|
has_get_logprobs = hasattr(ManagedServer, "get_logprobs")
|
|
|
|
|
if has_get_logprobs:
|
|
|
|
|
pytest.skip(
|
|
|
|
|
"ManagedServer still has get_logprobs — baseline atroposlib"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestParserCompatibility:
|
|
|
|
|
"""Test that hermes-agent's parsers match ManagedServer's expectations."""
|
|
|
|
|
|
|
|
|
|
def test_parser_parse_returns_correct_format(self):
|
|
|
|
|
"""
|
|
|
|
|
ManagedServer expects parser.parse(text) -> (content, tool_calls)
|
|
|
|
|
where tool_calls is a list of objects with .id, .function.name, .function.arguments
|
|
|
|
|
"""
|
|
|
|
|
from environments.tool_call_parsers import get_parser
|
|
|
|
|
|
|
|
|
|
parser = get_parser("hermes")
|
|
|
|
|
text = '<tool_call>{"name": "terminal", "arguments": {"command": "ls"}}</tool_call>'
|
|
|
|
|
content, tool_calls = parser.parse(text)
|
|
|
|
|
|
|
|
|
|
assert tool_calls is not None
|
|
|
|
|
assert len(tool_calls) == 1
|
|
|
|
|
|
|
|
|
|
tc = tool_calls[0]
|
|
|
|
|
# ManagedServer accesses these attrs directly
|
|
|
|
|
assert hasattr(tc, "id")
|
|
|
|
|
assert hasattr(tc, "function")
|
|
|
|
|
assert hasattr(tc.function, "name")
|
|
|
|
|
assert hasattr(tc.function, "arguments")
|
|
|
|
|
|
|
|
|
|
def test_parser_no_tools_returns_none(self):
|
|
|
|
|
"""ManagedServer checks `if parsed_tool_calls:` — None should be falsy."""
|
|
|
|
|
from environments.tool_call_parsers import get_parser
|
|
|
|
|
|
|
|
|
|
parser = get_parser("hermes")
|
|
|
|
|
content, tool_calls = parser.parse("Just text, no tools")
|
|
|
|
|
assert tool_calls is None
|
|
|
|
|
|
|
|
|
|
def test_parser_content_is_string_or_none(self):
|
|
|
|
|
"""ManagedServer uses `parsed_content or ""` — must be str or None."""
|
|
|
|
|
from environments.tool_call_parsers import get_parser
|
|
|
|
|
|
|
|
|
|
parser = get_parser("hermes")
|
|
|
|
|
|
|
|
|
|
# With tool calls
|
|
|
|
|
text = '<tool_call>{"name": "terminal", "arguments": {"command": "ls"}}</tool_call>'
|
|
|
|
|
content, _ = parser.parse(text)
|
|
|
|
|
assert content is None or isinstance(content, str)
|
|
|
|
|
|
|
|
|
|
# Without tool calls
|
|
|
|
|
content2, _ = parser.parse("Just text")
|
|
|
|
|
assert isinstance(content2, str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBaseEnvCompatibility:
|
2026-03-13 21:56:12 -07:00
|
|
|
"""Test that hermes_base_env.py's tool-parser wiring matches the current API."""
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
|
2026-03-13 21:56:12 -07:00
|
|
|
def test_hermes_base_env_sets_server_manager_tool_parser(self):
|
|
|
|
|
"""Hermes wires parser selection through ServerManager.tool_parser."""
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
import ast
|
|
|
|
|
|
|
|
|
|
base_env_path = Path(__file__).parent.parent / "environments" / "hermes_base_env.py"
|
|
|
|
|
source = base_env_path.read_text()
|
|
|
|
|
tree = ast.parse(source)
|
|
|
|
|
|
2026-03-13 21:56:12 -07:00
|
|
|
found_assignment = False
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
for node in ast.walk(tree):
|
2026-03-13 21:56:12 -07:00
|
|
|
if isinstance(node, ast.Assign):
|
|
|
|
|
for target in node.targets:
|
|
|
|
|
if isinstance(target, ast.Attribute) and target.attr == "tool_parser":
|
|
|
|
|
parent = target.value
|
|
|
|
|
if (
|
|
|
|
|
isinstance(parent, ast.Attribute)
|
|
|
|
|
and parent.attr == "server"
|
|
|
|
|
and isinstance(parent.value, ast.Name)
|
|
|
|
|
and parent.value.id == "self"
|
|
|
|
|
):
|
|
|
|
|
found_assignment = True
|
|
|
|
|
|
|
|
|
|
assert found_assignment, (
|
|
|
|
|
"hermes_base_env.py should set self.server.tool_parser from config.tool_call_parser"
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
)
|
|
|
|
|
|
2026-03-13 21:56:12 -07:00
|
|
|
def test_hermes_base_env_uses_config_tool_call_parser(self):
|
|
|
|
|
"""Verify hermes_base_env uses the config field rather than a local parser instance."""
|
Add tests for atropos tool calling integration
- test_tool_call_parsers.py: 16 tests for parser registry, hermes parser
(single/multiple/truncated/malformed), and ParseResult contract validation
- test_agent_loop.py: 21 tests for HermesAgentLoop with mock servers
(text responses, tool calls, max turns, unknown tools, API errors,
extra_body forwarding, managed state, blocked tools, reasoning extraction)
- test_managed_server_tool_support.py: 9 tests validating API compatibility
between hermes-agent and atroposlib's ManagedServer tool_call_parser support
(gracefully skips on baseline atroposlib, passes on tool_call_support branch)
2026-03-09 15:42:16 -05:00
|
|
|
base_env_path = Path(__file__).parent.parent / "environments" / "hermes_base_env.py"
|
|
|
|
|
source = base_env_path.read_text()
|
|
|
|
|
|
2026-03-13 21:56:12 -07:00
|
|
|
assert 'tool_call_parser: str = Field(' in source
|
|
|
|
|
assert 'self.server.tool_parser = config.tool_call_parser' in source
|