Add request-scoped plugin lifecycle hooks
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"""Tests for model_tools.py — function call dispatch, agent-loop interception, legacy toolsets."""
|
||||
|
||||
import json
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from model_tools import (
|
||||
@@ -38,6 +40,40 @@ class TestHandleFunctionCall:
|
||||
assert len(parsed["error"]) > 0
|
||||
assert "error" in parsed["error"].lower() or "failed" in parsed["error"].lower()
|
||||
|
||||
def test_tool_hooks_receive_session_and_tool_call_ids(self):
|
||||
with (
|
||||
patch("model_tools.registry.dispatch", return_value='{"ok":true}'),
|
||||
patch("hermes_cli.plugins.invoke_hook") as mock_invoke_hook,
|
||||
):
|
||||
result = handle_function_call(
|
||||
"web_search",
|
||||
{"q": "test"},
|
||||
task_id="task-1",
|
||||
tool_call_id="call-1",
|
||||
session_id="session-1",
|
||||
)
|
||||
|
||||
assert result == '{"ok":true}'
|
||||
assert mock_invoke_hook.call_args_list == [
|
||||
call(
|
||||
"pre_tool_call",
|
||||
tool_name="web_search",
|
||||
args={"q": "test"},
|
||||
task_id="task-1",
|
||||
session_id="session-1",
|
||||
tool_call_id="call-1",
|
||||
),
|
||||
call(
|
||||
"post_tool_call",
|
||||
tool_name="web_search",
|
||||
args={"q": "test"},
|
||||
result='{"ok":true}',
|
||||
task_id="task-1",
|
||||
session_id="session-1",
|
||||
tool_call_id="call-1",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Agent loop tools
|
||||
|
||||
@@ -196,6 +196,10 @@ class TestPluginLoading:
|
||||
class TestPluginHooks:
|
||||
"""Tests for lifecycle hook registration and invocation."""
|
||||
|
||||
def test_valid_hooks_include_request_scoped_llm_hooks(self):
|
||||
assert "pre_llm_request" in VALID_HOOKS
|
||||
assert "post_llm_request" in VALID_HOOKS
|
||||
|
||||
def test_register_and_invoke_hook(self, tmp_path, monkeypatch):
|
||||
"""Registered hooks are called on invoke_hook()."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
@@ -262,6 +266,28 @@ class TestPluginHooks:
|
||||
user_message="hi", assistant_response="bye", model="test")
|
||||
assert results == []
|
||||
|
||||
def test_request_hooks_are_invokeable(self, tmp_path, monkeypatch):
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
_make_plugin_dir(
|
||||
plugins_dir, "request_hook",
|
||||
register_body='ctx.register_hook("pre_llm_request", lambda **kw: {"seen": kw.get("api_call_count")})',
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
results = mgr.invoke_hook(
|
||||
"pre_llm_request",
|
||||
session_id="s1",
|
||||
task_id="t1",
|
||||
model="test",
|
||||
api_call_count=2,
|
||||
messages=[],
|
||||
tools=[],
|
||||
)
|
||||
assert results == [{"seen": 2}]
|
||||
|
||||
def test_invalid_hook_name_warns(self, tmp_path, monkeypatch, caplog):
|
||||
"""Registering an unknown hook name logs a warning."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
|
||||
@@ -1258,6 +1258,8 @@ class TestConcurrentToolExecution:
|
||||
result = agent._invoke_tool("web_search", {"q": "test"}, "task-1")
|
||||
mock_hfc.assert_called_once_with(
|
||||
"web_search", {"q": "test"}, "task-1",
|
||||
tool_call_id=None,
|
||||
session_id=agent.session_id,
|
||||
enabled_tools=list(agent.valid_tool_names),
|
||||
|
||||
)
|
||||
@@ -1441,7 +1443,7 @@ class TestRunConversation:
|
||||
resp2 = _mock_response(content="Done searching", finish_reason="stop")
|
||||
agent.client.chat.completions.create.side_effect = [resp1, resp2]
|
||||
with (
|
||||
patch("run_agent.handle_function_call", return_value="search result"),
|
||||
patch("run_agent.handle_function_call", return_value="search result") as mock_handle_function_call,
|
||||
patch.object(agent, "_persist_session"),
|
||||
patch.object(agent, "_save_trajectory"),
|
||||
patch.object(agent, "_cleanup_task_resources"),
|
||||
@@ -1449,6 +1451,39 @@ class TestRunConversation:
|
||||
result = agent.run_conversation("search something")
|
||||
assert result["final_response"] == "Done searching"
|
||||
assert result["api_calls"] == 2
|
||||
assert mock_handle_function_call.call_args.kwargs["tool_call_id"] == "c1"
|
||||
assert mock_handle_function_call.call_args.kwargs["session_id"] == agent.session_id
|
||||
|
||||
def test_request_scoped_llm_hooks_fire_for_each_api_call(self, agent):
|
||||
self._setup_agent(agent)
|
||||
tc = _mock_tool_call(name="web_search", arguments="{}", call_id="c1")
|
||||
resp1 = _mock_response(content="", finish_reason="tool_calls", tool_calls=[tc])
|
||||
resp2 = _mock_response(content="Done searching", finish_reason="stop")
|
||||
agent.client.chat.completions.create.side_effect = [resp1, resp2]
|
||||
|
||||
hook_calls = []
|
||||
|
||||
def _record_hook(name, **kwargs):
|
||||
hook_calls.append((name, kwargs))
|
||||
return []
|
||||
|
||||
with (
|
||||
patch("run_agent.handle_function_call", return_value="search result"),
|
||||
patch("hermes_cli.plugins.invoke_hook", side_effect=_record_hook),
|
||||
patch.object(agent, "_persist_session"),
|
||||
patch.object(agent, "_save_trajectory"),
|
||||
patch.object(agent, "_cleanup_task_resources"),
|
||||
):
|
||||
result = agent.run_conversation("search something")
|
||||
|
||||
assert result["final_response"] == "Done searching"
|
||||
pre_request_calls = [kw for name, kw in hook_calls if name == "pre_llm_request"]
|
||||
post_request_calls = [kw for name, kw in hook_calls if name == "post_llm_request"]
|
||||
assert len(pre_request_calls) == 2
|
||||
assert len(post_request_calls) == 2
|
||||
assert [call["api_call_count"] for call in pre_request_calls] == [1, 2]
|
||||
assert [call["api_call_count"] for call in post_request_calls] == [1, 2]
|
||||
assert all(call["session_id"] == agent.session_id for call in pre_request_calls)
|
||||
|
||||
def test_interrupt_breaks_loop(self, agent):
|
||||
self._setup_agent(agent)
|
||||
|
||||
Reference in New Issue
Block a user