mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-12 03:42:08 +00:00
feat(teams-pipeline): add plugin runtime and operator cli
Third slice of the Microsoft Teams meeting pipeline stack, salvaged onto current main. Adds the standalone teams_pipeline plugin that consumes Graph change notifications from the webhook listener, resolves meeting artifacts (transcript first, recording + STT fallback later), persists job state in a durable store, and exposes an operator CLI for inspection, replay, subscription management, and validation. Design choices follow maintainer review feedback on PR #19815: - Standalone plugin rather than bolted-on core surface (plugins/teams_pipeline/, kind: standalone in plugin.yaml). - Zero new model tools. The agent drives the pipeline by invoking the operator CLI via the terminal tool, guided by the skill that ships with a follow-up PR. - Reuses the existing msgraph_webhook gateway platform for Graph ingress. Pipeline runtime is wired in via bind_gateway_runtime and gated on plugins.enabled so gateways that don't run the plugin boot cleanly. Additions: - plugins/teams_pipeline/: runtime (gateway wiring + config builder), pipeline core, durable SQLite store, subscription maintenance helpers, Graph artifact resolution, operator CLI (list, show, run/replay, fetch dry-run, subscriptions list, subscribe, renew-subscription, delete-subscription, maintain-subscriptions, token-health, validate). - hermes_cli/main.py: second-pass plugin CLI discovery so any standalone plugin registered via ctx.register_cli_command() outside the memory-plugin convention path gets its subcommand wired into argparse without touching core. - gateway/run.py: _teams_pipeline_plugin_enabled() config gate, _wire_teams_pipeline_runtime() binding after adapter setup, and the two runner attributes used by the runtime. Credit to @dlkakbs for the entire plugin implementation.
This commit is contained in:
parent
ea86714cc0
commit
07bbd93337
14 changed files with 3332 additions and 1 deletions
185
tests/gateway/test_teams_pipeline_runtime_wiring.py
Normal file
185
tests/gateway/test_teams_pipeline_runtime_wiring.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
"""Tests for Teams pipeline runtime wiring into the gateway."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
from gateway.run import GatewayRunner
|
||||
from plugins.teams_pipeline.runtime import (
|
||||
bind_gateway_runtime,
|
||||
build_pipeline_runtime,
|
||||
build_pipeline_runtime_config,
|
||||
)
|
||||
|
||||
|
||||
def test_gateway_runner_wires_teams_pipeline_runtime(monkeypatch):
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
runner.adapters = {Platform.MSGRAPH_WEBHOOK: object()}
|
||||
runner._teams_pipeline_runtime_error = None
|
||||
|
||||
calls: list[object] = []
|
||||
|
||||
def _bind(gateway_runner):
|
||||
calls.append(gateway_runner)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("plugins.teams_pipeline.runtime.bind_gateway_runtime", _bind)
|
||||
|
||||
GatewayRunner._wire_teams_pipeline_runtime(runner)
|
||||
|
||||
assert calls == [runner]
|
||||
|
||||
|
||||
def test_gateway_runner_skips_wiring_without_msgraph_adapter(monkeypatch):
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
runner.adapters = {Platform.TELEGRAM: MagicMock()}
|
||||
runner._teams_pipeline_runtime_error = None
|
||||
|
||||
called = False
|
||||
|
||||
def _bind(_gateway_runner):
|
||||
nonlocal called
|
||||
called = True
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("plugins.teams_pipeline.runtime.bind_gateway_runtime", _bind)
|
||||
|
||||
GatewayRunner._wire_teams_pipeline_runtime(runner)
|
||||
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_gateway_runner_skips_wiring_when_teams_pipeline_plugin_disabled(monkeypatch):
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
runner.adapters = {Platform.MSGRAPH_WEBHOOK: object()}
|
||||
runner._teams_pipeline_runtime_error = None
|
||||
|
||||
called = False
|
||||
|
||||
def _bind(_gateway_runner):
|
||||
nonlocal called
|
||||
called = True
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("plugins.teams_pipeline.runtime.bind_gateway_runtime", _bind)
|
||||
monkeypatch.setattr(
|
||||
"gateway.run._load_gateway_config",
|
||||
lambda: {"plugins": {"enabled": []}},
|
||||
)
|
||||
|
||||
GatewayRunner._wire_teams_pipeline_runtime(runner)
|
||||
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_runtime_config_disables_teams_delivery_without_target():
|
||||
gateway_config = SimpleNamespace(
|
||||
platforms={
|
||||
Platform("teams"): PlatformConfig(enabled=True, extra={}),
|
||||
}
|
||||
)
|
||||
|
||||
config = build_pipeline_runtime_config(gateway_config)
|
||||
|
||||
assert "teams_delivery" not in config
|
||||
|
||||
|
||||
def test_build_pipeline_runtime_only_wires_sender_when_delivery_configured(monkeypatch):
|
||||
gateway = SimpleNamespace(
|
||||
config=SimpleNamespace(
|
||||
platforms={
|
||||
Platform("teams"): PlatformConfig(enabled=True, extra={}),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.build_graph_client",
|
||||
lambda: object(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.resolve_teams_pipeline_store_path",
|
||||
lambda: "/tmp/teams-pipeline-store.json",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.TeamsPipelineStore",
|
||||
lambda path: {"path": path},
|
||||
)
|
||||
|
||||
runtime = build_pipeline_runtime(gateway)
|
||||
|
||||
assert runtime.teams_sender is None
|
||||
|
||||
|
||||
def test_build_pipeline_runtime_skips_sender_when_adapter_layer_is_unavailable(monkeypatch):
|
||||
gateway = SimpleNamespace(
|
||||
config=SimpleNamespace(
|
||||
platforms={
|
||||
Platform("teams"): PlatformConfig(
|
||||
enabled=True,
|
||||
extra={
|
||||
"delivery_mode": "graph",
|
||||
"team_id": "team-1",
|
||||
"channel_id": "channel-1",
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.build_graph_client",
|
||||
lambda: object(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.resolve_teams_pipeline_store_path",
|
||||
lambda: "/tmp/teams-pipeline-store.json",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.TeamsPipelineStore",
|
||||
lambda path: {"path": path},
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"plugins.platforms.teams.adapter",
|
||||
ModuleType("plugins.platforms.teams.adapter"),
|
||||
)
|
||||
|
||||
runtime = build_pipeline_runtime(gateway)
|
||||
|
||||
assert runtime.teams_sender is None
|
||||
|
||||
|
||||
def test_bind_gateway_runtime_leaves_scheduler_unchanged_on_failure(monkeypatch):
|
||||
class FakeAdapter:
|
||||
def __init__(self):
|
||||
self.scheduler = None
|
||||
|
||||
def set_notification_scheduler(self, scheduler):
|
||||
self.scheduler = scheduler
|
||||
|
||||
gateway = SimpleNamespace(
|
||||
adapters={Platform.MSGRAPH_WEBHOOK: FakeAdapter()},
|
||||
config=SimpleNamespace(
|
||||
platforms={
|
||||
Platform("teams"): PlatformConfig(enabled=True, extra={}),
|
||||
}
|
||||
),
|
||||
_teams_pipeline_runtime=None,
|
||||
_teams_pipeline_runtime_error=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"plugins.teams_pipeline.runtime.build_pipeline_runtime",
|
||||
lambda _gateway: (_ for _ in ()).throw(RuntimeError("boom")),
|
||||
)
|
||||
|
||||
bound = bind_gateway_runtime(gateway)
|
||||
|
||||
assert bound is False
|
||||
assert gateway.adapters[Platform.MSGRAPH_WEBHOOK].scheduler is None
|
||||
assert gateway._teams_pipeline_runtime_error == "boom"
|
||||
Loading…
Add table
Add a link
Reference in a new issue