mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 23:11:37 +08:00
Compare commits
1 Commits
skill/gith
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16807b94c9 |
@@ -484,6 +484,10 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
Supports dot-notation access into nested dicts:
|
Supports dot-notation access into nested dicts:
|
||||||
``{pull_request.title}`` → ``payload["pull_request"]["title"]``
|
``{pull_request.title}`` → ``payload["pull_request"]["title"]``
|
||||||
|
|
||||||
|
Special token ``{__raw__}`` dumps the entire payload as indented
|
||||||
|
JSON (truncated to 4000 chars). Useful for monitoring alerts or
|
||||||
|
any webhook where the agent needs to see the full payload.
|
||||||
"""
|
"""
|
||||||
if not template:
|
if not template:
|
||||||
truncated = json.dumps(payload, indent=2)[:4000]
|
truncated = json.dumps(payload, indent=2)[:4000]
|
||||||
@@ -494,6 +498,9 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
def _resolve(match: re.Match) -> str:
|
def _resolve(match: re.Match) -> str:
|
||||||
key = match.group(1)
|
key = match.group(1)
|
||||||
|
# Special token: dump the entire payload as JSON
|
||||||
|
if key == "__raw__":
|
||||||
|
return json.dumps(payload, indent=2)[:4000]
|
||||||
value: Any = payload
|
value: Any = payload
|
||||||
for part in key.split("."):
|
for part in key.split("."):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -613,4 +620,10 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||||||
error=f"No chat_id or home channel for {platform_name}",
|
error=f"No chat_id or home channel for {platform_name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return await adapter.send(chat_id, content)
|
# Pass thread_id from deliver_extra so Telegram forum topics work
|
||||||
|
metadata = None
|
||||||
|
thread_id = extra.get("message_thread_id") or extra.get("thread_id")
|
||||||
|
if thread_id:
|
||||||
|
metadata = {"thread_id": thread_id}
|
||||||
|
|
||||||
|
return await adapter.send(chat_id, content, metadata=metadata)
|
||||||
|
|||||||
@@ -617,3 +617,107 @@ class TestCheckRequirements:
|
|||||||
@patch("gateway.platforms.webhook.AIOHTTP_AVAILABLE", False)
|
@patch("gateway.platforms.webhook.AIOHTTP_AVAILABLE", False)
|
||||||
def test_returns_false_without_aiohttp(self):
|
def test_returns_false_without_aiohttp(self):
|
||||||
assert check_webhook_requirements() is False
|
assert check_webhook_requirements() is False
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# __raw__ template token
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestRawTemplateToken:
|
||||||
|
"""Tests for the {__raw__} special token in _render_prompt."""
|
||||||
|
|
||||||
|
def test_raw_resolves_to_full_json_payload(self):
|
||||||
|
"""{__raw__} in a template dumps the entire payload as JSON."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
payload = {"action": "opened", "number": 42}
|
||||||
|
result = adapter._render_prompt(
|
||||||
|
"Payload: {__raw__}", payload, "push", "test"
|
||||||
|
)
|
||||||
|
expected_json = json.dumps(payload, indent=2)
|
||||||
|
assert result == f"Payload: {expected_json}"
|
||||||
|
|
||||||
|
def test_raw_truncated_at_4000_chars(self):
|
||||||
|
"""{__raw__} output is truncated at 4000 characters for large payloads."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
# Build a payload whose JSON repr exceeds 4000 chars
|
||||||
|
payload = {"data": "x" * 5000}
|
||||||
|
result = adapter._render_prompt("{__raw__}", payload, "push", "test")
|
||||||
|
assert len(result) <= 4000
|
||||||
|
|
||||||
|
def test_raw_mixed_with_other_variables(self):
|
||||||
|
"""{__raw__} can be mixed with regular template variables."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
payload = {"action": "closed", "number": 7}
|
||||||
|
result = adapter._render_prompt(
|
||||||
|
"Action={action} Raw={__raw__}", payload, "push", "test"
|
||||||
|
)
|
||||||
|
assert result.startswith("Action=closed Raw=")
|
||||||
|
assert '"action": "closed"' in result
|
||||||
|
assert '"number": 7' in result
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Cross-platform delivery thread_id passthrough
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeliverCrossPlatformThreadId:
|
||||||
|
"""Tests for thread_id passthrough in _deliver_cross_platform."""
|
||||||
|
|
||||||
|
def _setup_adapter_with_mock_target(self):
|
||||||
|
"""Set up a webhook adapter with a mocked gateway_runner and target adapter."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
mock_target = AsyncMock()
|
||||||
|
mock_target.send = AsyncMock(return_value=SendResult(success=True))
|
||||||
|
|
||||||
|
mock_runner = MagicMock()
|
||||||
|
mock_runner.adapters = {Platform("telegram"): mock_target}
|
||||||
|
mock_runner.config.get_home_channel.return_value = None
|
||||||
|
|
||||||
|
adapter.gateway_runner = mock_runner
|
||||||
|
return adapter, mock_target
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_thread_id_passed_as_metadata(self):
|
||||||
|
"""thread_id from deliver_extra is passed as metadata to adapter.send()."""
|
||||||
|
adapter, mock_target = self._setup_adapter_with_mock_target()
|
||||||
|
delivery = {
|
||||||
|
"deliver_extra": {
|
||||||
|
"chat_id": "12345",
|
||||||
|
"thread_id": "999",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await adapter._deliver_cross_platform("telegram", "hello", delivery)
|
||||||
|
mock_target.send.assert_awaited_once_with(
|
||||||
|
"12345", "hello", metadata={"thread_id": "999"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_message_thread_id_passed_as_thread_id(self):
|
||||||
|
"""message_thread_id from deliver_extra is mapped to thread_id in metadata."""
|
||||||
|
adapter, mock_target = self._setup_adapter_with_mock_target()
|
||||||
|
delivery = {
|
||||||
|
"deliver_extra": {
|
||||||
|
"chat_id": "12345",
|
||||||
|
"message_thread_id": "888",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await adapter._deliver_cross_platform("telegram", "hello", delivery)
|
||||||
|
mock_target.send.assert_awaited_once_with(
|
||||||
|
"12345", "hello", metadata={"thread_id": "888"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_no_thread_id_sends_no_metadata(self):
|
||||||
|
"""When no thread_id is present, metadata is None."""
|
||||||
|
adapter, mock_target = self._setup_adapter_with_mock_target()
|
||||||
|
delivery = {
|
||||||
|
"deliver_extra": {
|
||||||
|
"chat_id": "12345",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await adapter._deliver_cross_platform("telegram", "hello", delivery)
|
||||||
|
mock_target.send.assert_awaited_once_with(
|
||||||
|
"12345", "hello", metadata=None
|
||||||
|
)
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ class TestCrossPlatformDelivery:
|
|||||||
|
|
||||||
assert result.success is True
|
assert result.success is True
|
||||||
mock_tg_adapter.send.assert_awaited_once_with(
|
mock_tg_adapter.send.assert_awaited_once_with(
|
||||||
"12345", "I've acknowledged the alert."
|
"12345", "I've acknowledged the alert.", metadata=None
|
||||||
)
|
)
|
||||||
# Delivery info should be cleaned up
|
# Delivery info should be cleaned up
|
||||||
assert chat_id not in adapter._delivery_info
|
assert chat_id not in adapter._delivery_info
|
||||||
|
|||||||
Reference in New Issue
Block a user