refactor(trigger): name CLI trigger source as local
maintainer edit: cron is also a trigger source, so keep the new CLI-delivered source explicitly named as local trigger across backend, WebUI, docs, and tests.
This commit is contained in:
@@ -68,7 +68,7 @@ class ChannelManager:
|
||||
*,
|
||||
session_manager: "SessionManager | None" = None,
|
||||
cron_service: Any | None = None,
|
||||
external_trigger_store: Any | None = None,
|
||||
local_trigger_store: Any | None = None,
|
||||
webui_runtime_model_name: Callable[[], str | None] | None = None,
|
||||
webui_cron_pending_job_ids: Callable[[str], set[str]] | None = None,
|
||||
webui_static_dist: bool = True,
|
||||
@@ -79,7 +79,7 @@ class ChannelManager:
|
||||
self.bus = bus
|
||||
self._session_manager = session_manager
|
||||
self._cron_service = cron_service
|
||||
self._external_trigger_store = external_trigger_store
|
||||
self._local_trigger_store = local_trigger_store
|
||||
self._webui_runtime_model_name = webui_runtime_model_name
|
||||
self._webui_cron_pending_job_ids = webui_cron_pending_job_ids
|
||||
self._webui_static_dist = webui_static_dist
|
||||
@@ -141,7 +141,7 @@ class ChannelManager:
|
||||
runtime_surface=self._webui_runtime_surface,
|
||||
runtime_capabilities_overrides=self._webui_runtime_capabilities,
|
||||
cron_service=self._cron_service,
|
||||
external_trigger_store=self._external_trigger_store,
|
||||
local_trigger_store=self._local_trigger_store,
|
||||
cron_pending_job_ids=self._webui_cron_pending_job_ids,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
+10
-10
@@ -772,8 +772,8 @@ def trigger(
|
||||
config: str | None = typer.Option(None, "--config", "-c", help="Config file path"),
|
||||
):
|
||||
"""Deliver a local trigger message to its bound chat session."""
|
||||
from nanobot.triggers.store import (
|
||||
ExternalTriggerStore,
|
||||
from nanobot.triggers.local_store import (
|
||||
LocalTriggerStore,
|
||||
TriggerDisabledError,
|
||||
TriggerNotFoundError,
|
||||
TriggerStoreError,
|
||||
@@ -781,7 +781,7 @@ def trigger(
|
||||
|
||||
runtime_config = _load_runtime_config(config, workspace)
|
||||
content = _read_trigger_cli_message(message)
|
||||
store = ExternalTriggerStore(runtime_config.workspace_path)
|
||||
store = LocalTriggerStore(runtime_config.workspace_path)
|
||||
try:
|
||||
delivery = store.enqueue(trigger_id, content)
|
||||
except (TriggerNotFoundError, TriggerDisabledError) as exc:
|
||||
@@ -909,8 +909,8 @@ def _run_gateway(
|
||||
from nanobot.providers.image_generation import image_gen_provider_configs
|
||||
from nanobot.session.manager import SessionManager
|
||||
from nanobot.session.webui_turns import WebuiTurnCoordinator
|
||||
from nanobot.triggers.runner import run_external_trigger_queue
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_runner import run_local_trigger_queue
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
from nanobot.webui.token_usage import TokenUsageHook
|
||||
|
||||
port = port if port is not None else config.gateway.port
|
||||
@@ -933,7 +933,7 @@ def _run_gateway(
|
||||
# Create cron service with workspace-scoped store
|
||||
cron_store_path = config.workspace_path / "cron" / "jobs.json"
|
||||
cron = CronService(cron_store_path)
|
||||
trigger_store = ExternalTriggerStore(config.workspace_path)
|
||||
trigger_store = LocalTriggerStore(config.workspace_path)
|
||||
|
||||
# Create agent with cron service
|
||||
agent = AgentLoop.from_config(
|
||||
@@ -954,7 +954,7 @@ def _run_gateway(
|
||||
sessions=session_manager,
|
||||
schedule_background=lambda coro: agent._schedule_background(coro),
|
||||
).subscribe(runtime_events)
|
||||
agent.external_trigger_store = trigger_store
|
||||
agent.local_trigger_store = trigger_store
|
||||
|
||||
from nanobot.bus.events import OutboundMessage
|
||||
from nanobot.session.keys import session_key_for_channel
|
||||
@@ -1151,7 +1151,7 @@ def _run_gateway(
|
||||
bus,
|
||||
session_manager=session_manager,
|
||||
cron_service=cron,
|
||||
external_trigger_store=trigger_store,
|
||||
local_trigger_store=trigger_store,
|
||||
webui_runtime_model_name=_webui_runtime_model_name,
|
||||
webui_cron_pending_job_ids=getattr(agent, "pending_cron_job_ids_for_session", None),
|
||||
webui_static_dist=webui_static_dist,
|
||||
@@ -1295,8 +1295,8 @@ def _run_gateway(
|
||||
asyncio.create_task(agent.run(), name="nanobot-agent-loop"),
|
||||
asyncio.create_task(channels.start_all(), name="nanobot-channels"),
|
||||
asyncio.create_task(
|
||||
run_external_trigger_queue(store=trigger_store, bus=bus),
|
||||
name="nanobot-external-triggers",
|
||||
run_local_trigger_queue(store=trigger_store, bus=bus),
|
||||
name="nanobot-local-triggers",
|
||||
),
|
||||
]
|
||||
if health_server_enabled:
|
||||
|
||||
@@ -740,7 +740,7 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||
metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"},
|
||||
)
|
||||
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
loop = ctx.loop
|
||||
workspace = getattr(loop, "workspace", None)
|
||||
@@ -749,9 +749,9 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||
if workspace is None:
|
||||
raise RuntimeError("workspace unavailable for trigger creation")
|
||||
|
||||
store = getattr(loop, "external_trigger_store", None)
|
||||
store = getattr(loop, "local_trigger_store", None)
|
||||
if store is None:
|
||||
store = ExternalTriggerStore(workspace)
|
||||
store = LocalTriggerStore(workspace)
|
||||
|
||||
trigger = store.create(
|
||||
name=name,
|
||||
|
||||
@@ -56,9 +56,9 @@ def automation_history_overrides_for_spec(
|
||||
def _automation_specs() -> tuple[AutomationTurnSpec, ...]:
|
||||
# Source modules import the generic helpers above, so keep spec loading lazy.
|
||||
from nanobot.cron.session_turns import CRON_AUTOMATION_SPEC
|
||||
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_AUTOMATION_SPEC
|
||||
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_AUTOMATION_SPEC
|
||||
|
||||
return (CRON_AUTOMATION_SPEC, EXTERNAL_TRIGGER_AUTOMATION_SPEC)
|
||||
return (CRON_AUTOMATION_SPEC, LOCAL_TRIGGER_AUTOMATION_SPEC)
|
||||
|
||||
|
||||
def automation_history_overrides(
|
||||
@@ -87,4 +87,6 @@ def is_automation_history_message(message: Mapping[str, Any] | None) -> bool:
|
||||
|
||||
|
||||
def is_automation_kind(value: Any) -> bool:
|
||||
return isinstance(value, str) and any(spec.kind == value for spec in _automation_specs())
|
||||
return isinstance(value, str) and (
|
||||
value == "trigger" or any(spec.kind == value for spec in _automation_specs())
|
||||
)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""Local external trigger support."""
|
||||
"""Local trigger support."""
|
||||
|
||||
from nanobot.triggers.store import (
|
||||
ExternalTriggerStore,
|
||||
from nanobot.triggers.local_store import (
|
||||
LocalTriggerStore,
|
||||
TriggerDisabledError,
|
||||
TriggerNotFoundError,
|
||||
TriggerStoreError,
|
||||
)
|
||||
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery, TriggerRunRecord
|
||||
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery, TriggerRunRecord
|
||||
|
||||
__all__ = [
|
||||
"ExternalTrigger",
|
||||
"ExternalTriggerStore",
|
||||
"LocalTrigger",
|
||||
"LocalTriggerStore",
|
||||
"TriggerDelivery",
|
||||
"TriggerDisabledError",
|
||||
"TriggerNotFoundError",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Gateway delivery loop for local external triggers."""
|
||||
"""Gateway delivery loop for local triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,21 +10,21 @@ from loguru import logger
|
||||
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_META
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery
|
||||
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_META
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery
|
||||
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
|
||||
|
||||
|
||||
async def run_external_trigger_queue(
|
||||
async def run_local_trigger_queue(
|
||||
*,
|
||||
store: ExternalTriggerStore,
|
||||
store: LocalTriggerStore,
|
||||
bus: MessageBus,
|
||||
poll_interval_s: float = 0.5,
|
||||
batch_size: int = 20,
|
||||
) -> None:
|
||||
"""Poll local trigger deliveries and publish them as normal inbound messages."""
|
||||
logger.info("External trigger queue started")
|
||||
logger.info("Local trigger queue started")
|
||||
recovered = store.recover_processing_deliveries()
|
||||
if recovered:
|
||||
logger.warning(
|
||||
@@ -80,7 +80,7 @@ class _TerminalDeliveryError(RuntimeError):
|
||||
|
||||
|
||||
async def _publish_delivery(
|
||||
store: ExternalTriggerStore,
|
||||
store: LocalTriggerStore,
|
||||
bus: MessageBus,
|
||||
delivery: TriggerDelivery,
|
||||
) -> None:
|
||||
@@ -107,9 +107,9 @@ async def _publish_delivery(
|
||||
)
|
||||
|
||||
|
||||
def _delivery_metadata(trigger: ExternalTrigger, delivery: TriggerDelivery) -> dict[str, Any]:
|
||||
def _delivery_metadata(trigger: LocalTrigger, delivery: TriggerDelivery) -> dict[str, Any]:
|
||||
metadata = dict(trigger.origin_metadata or {})
|
||||
metadata[EXTERNAL_TRIGGER_META] = {
|
||||
metadata[LOCAL_TRIGGER_META] = {
|
||||
"trigger_id": trigger.id,
|
||||
"trigger_name": trigger.name,
|
||||
"delivery_id": delivery.id,
|
||||
@@ -118,7 +118,7 @@ def _delivery_metadata(trigger: ExternalTrigger, delivery: TriggerDelivery) -> d
|
||||
if trigger.channel == "websocket":
|
||||
metadata.pop(WEBUI_TURN_METADATA_KEY, None)
|
||||
metadata[WEBUI_TURN_METADATA_KEY] = f"trigger:{trigger.id}:{uuid.uuid4().hex}"
|
||||
source: dict[str, str] = {"kind": "trigger"}
|
||||
source: dict[str, str] = {"kind": "local_trigger"}
|
||||
if trigger.name:
|
||||
source["label"] = trigger.name
|
||||
metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = source
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Shared metadata helpers for local trigger session turns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from nanobot.session.automation_turns import (
|
||||
AutomationTurnSpec,
|
||||
automation_history_overrides_for_spec,
|
||||
automation_trigger,
|
||||
)
|
||||
|
||||
LOCAL_TRIGGER_META = "_local_trigger"
|
||||
|
||||
|
||||
def _local_trigger_history_text(trigger: Mapping[str, Any]) -> str:
|
||||
name = trigger.get("trigger_name")
|
||||
trigger_id = trigger.get("trigger_id")
|
||||
label = name if isinstance(name, str) and name.strip() else trigger_id
|
||||
return (
|
||||
f"Local trigger received: {label}"
|
||||
if isinstance(label, str) and label.strip()
|
||||
else "Local trigger received"
|
||||
)
|
||||
|
||||
|
||||
LOCAL_TRIGGER_AUTOMATION_SPEC = AutomationTurnSpec(
|
||||
kind="local_trigger",
|
||||
trigger_meta_key=LOCAL_TRIGGER_META,
|
||||
history_fields={
|
||||
"trigger_id": "trigger_id",
|
||||
"trigger_name": "trigger_name",
|
||||
"trigger_delivery_id": "delivery_id",
|
||||
},
|
||||
text_builder=_local_trigger_history_text,
|
||||
)
|
||||
|
||||
|
||||
def local_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""Return structured local trigger metadata when present."""
|
||||
return automation_trigger(metadata, LOCAL_TRIGGER_AUTOMATION_SPEC)
|
||||
|
||||
|
||||
def local_trigger_history_overrides(
|
||||
metadata: Mapping[str, Any] | None,
|
||||
) -> tuple[str | None, dict[str, Any]]:
|
||||
"""Return session-history text/metadata overrides for a local trigger turn."""
|
||||
return automation_history_overrides_for_spec(
|
||||
metadata,
|
||||
LOCAL_TRIGGER_AUTOMATION_SPEC,
|
||||
)
|
||||
@@ -14,7 +14,7 @@ from typing import Any
|
||||
from filelock import FileLock
|
||||
from loguru import logger
|
||||
|
||||
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery, TriggerRunRecord
|
||||
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery, TriggerRunRecord
|
||||
|
||||
_TRIGGER_ID_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
_MAX_RUN_HISTORY = 20
|
||||
@@ -34,7 +34,7 @@ class TriggerDisabledError(TriggerStoreError):
|
||||
"""Raised when a trigger is disabled."""
|
||||
|
||||
|
||||
class ExternalTriggerStore:
|
||||
class LocalTriggerStore:
|
||||
"""Persistent local triggers for one workspace."""
|
||||
|
||||
def __init__(self, workspace_path: Path):
|
||||
@@ -55,8 +55,8 @@ class ExternalTriggerStore:
|
||||
session_key: str,
|
||||
sender_id: str = "trigger",
|
||||
origin_metadata: dict[str, Any] | None = None,
|
||||
) -> ExternalTrigger:
|
||||
"""Create a new session-bound external trigger."""
|
||||
) -> LocalTrigger:
|
||||
"""Create a new session-bound local trigger."""
|
||||
clean_name = _clean_name(name)
|
||||
channel = channel.strip()
|
||||
chat_id = chat_id.strip()
|
||||
@@ -70,7 +70,7 @@ class ExternalTriggerStore:
|
||||
triggers = self._load_triggers_unlocked()
|
||||
existing_ids = {trigger.id for trigger in triggers}
|
||||
trigger_id = _new_trigger_id(existing_ids)
|
||||
trigger = ExternalTrigger(
|
||||
trigger = LocalTrigger(
|
||||
id=trigger_id,
|
||||
name=clean_name,
|
||||
enabled=True,
|
||||
@@ -86,7 +86,7 @@ class ExternalTriggerStore:
|
||||
self._save_triggers_unlocked(triggers)
|
||||
return trigger
|
||||
|
||||
def list_triggers(self, *, include_disabled: bool = False) -> list[ExternalTrigger]:
|
||||
def list_triggers(self, *, include_disabled: bool = False) -> list[LocalTrigger]:
|
||||
"""List triggers in this workspace."""
|
||||
self._ensure_dirs()
|
||||
with self._lock:
|
||||
@@ -100,7 +100,7 @@ class ExternalTriggerStore:
|
||||
session_key: str,
|
||||
*,
|
||||
include_disabled: bool = True,
|
||||
) -> list[ExternalTrigger]:
|
||||
) -> list[LocalTrigger]:
|
||||
"""List triggers bound to one session key."""
|
||||
return [
|
||||
trigger
|
||||
@@ -108,13 +108,13 @@ class ExternalTriggerStore:
|
||||
if trigger.session_key == session_key
|
||||
]
|
||||
|
||||
def get(self, trigger_id: str) -> ExternalTrigger | None:
|
||||
def get(self, trigger_id: str) -> LocalTrigger | None:
|
||||
"""Return one trigger by ID."""
|
||||
self._ensure_dirs()
|
||||
with self._lock:
|
||||
return self._find_unlocked(self._load_triggers_unlocked(), trigger_id)
|
||||
|
||||
def enable(self, trigger_id: str, *, enabled: bool) -> ExternalTrigger | None:
|
||||
def enable(self, trigger_id: str, *, enabled: bool) -> LocalTrigger | None:
|
||||
"""Enable or disable a trigger."""
|
||||
self._ensure_dirs()
|
||||
with self._lock:
|
||||
@@ -127,7 +127,7 @@ class ExternalTriggerStore:
|
||||
self._save_triggers_unlocked(triggers)
|
||||
return trigger
|
||||
|
||||
def update(self, trigger_id: str, *, name: str | None = None) -> ExternalTrigger | None:
|
||||
def update(self, trigger_id: str, *, name: str | None = None) -> LocalTrigger | None:
|
||||
"""Update mutable trigger fields."""
|
||||
self._ensure_dirs()
|
||||
with self._lock:
|
||||
@@ -267,13 +267,13 @@ class ExternalTriggerStore:
|
||||
self.processing_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.failed_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _load_triggers_unlocked(self) -> list[ExternalTrigger]:
|
||||
def _load_triggers_unlocked(self) -> list[LocalTrigger]:
|
||||
if not self.store_path.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(self.store_path.read_text(encoding="utf-8"))
|
||||
return [
|
||||
ExternalTrigger.from_dict(raw)
|
||||
LocalTrigger.from_dict(raw)
|
||||
for raw in data.get("triggers", [])
|
||||
if isinstance(raw, dict)
|
||||
]
|
||||
@@ -288,7 +288,7 @@ class ExternalTriggerStore:
|
||||
"as a .corrupt-<ts> backup"
|
||||
) from exc
|
||||
|
||||
def _save_triggers_unlocked(self, triggers: list[ExternalTrigger]) -> None:
|
||||
def _save_triggers_unlocked(self, triggers: list[LocalTrigger]) -> None:
|
||||
payload = {
|
||||
"version": 1,
|
||||
"triggers": [trigger.to_dict() for trigger in triggers],
|
||||
@@ -297,9 +297,9 @@ class ExternalTriggerStore:
|
||||
|
||||
@staticmethod
|
||||
def _find_unlocked(
|
||||
triggers: list[ExternalTrigger],
|
||||
triggers: list[LocalTrigger],
|
||||
trigger_id: str,
|
||||
) -> ExternalTrigger | None:
|
||||
) -> LocalTrigger | None:
|
||||
return next((trigger for trigger in triggers if trigger.id == trigger_id), None)
|
||||
|
||||
def _move_bad_delivery_unlocked(self, path: Path) -> None:
|
||||
@@ -356,7 +356,7 @@ def _new_trigger_id(existing_ids: set[str]) -> str:
|
||||
|
||||
def _clean_name(name: str) -> str:
|
||||
stripped = " ".join(name.strip().split())
|
||||
return (stripped or "External trigger")[:120]
|
||||
return (stripped or "Local trigger")[:120]
|
||||
|
||||
|
||||
def _now_ms() -> int:
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Persistent types for local external triggers."""
|
||||
"""Persistent types for local triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -40,7 +40,7 @@ class TriggerRunRecord:
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExternalTrigger:
|
||||
class LocalTrigger:
|
||||
"""A session-bound local trigger."""
|
||||
|
||||
id: str
|
||||
@@ -59,7 +59,7 @@ class ExternalTrigger:
|
||||
run_history: list[TriggerRunRecord] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ExternalTrigger":
|
||||
def from_dict(cls, data: dict[str, Any]) -> "LocalTrigger":
|
||||
history = [
|
||||
record if isinstance(record, TriggerRunRecord) else TriggerRunRecord.from_dict(record)
|
||||
for record in data.get("runHistory", data.get("run_history", []))
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Shared metadata helpers for local external trigger session turns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from nanobot.session.automation_turns import (
|
||||
AutomationTurnSpec,
|
||||
automation_history_overrides_for_spec,
|
||||
automation_trigger,
|
||||
)
|
||||
|
||||
EXTERNAL_TRIGGER_META = "_external_trigger"
|
||||
|
||||
|
||||
def _external_trigger_history_text(trigger: Mapping[str, Any]) -> str:
|
||||
name = trigger.get("trigger_name")
|
||||
trigger_id = trigger.get("trigger_id")
|
||||
label = name if isinstance(name, str) and name.strip() else trigger_id
|
||||
return (
|
||||
f"External trigger received: {label}"
|
||||
if isinstance(label, str) and label.strip()
|
||||
else "External trigger received"
|
||||
)
|
||||
|
||||
|
||||
EXTERNAL_TRIGGER_AUTOMATION_SPEC = AutomationTurnSpec(
|
||||
kind="trigger",
|
||||
trigger_meta_key=EXTERNAL_TRIGGER_META,
|
||||
history_fields={
|
||||
"trigger_id": "trigger_id",
|
||||
"trigger_name": "trigger_name",
|
||||
"trigger_delivery_id": "delivery_id",
|
||||
},
|
||||
text_builder=_external_trigger_history_text,
|
||||
)
|
||||
|
||||
|
||||
def external_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""Return structured external trigger metadata when present."""
|
||||
return automation_trigger(metadata, EXTERNAL_TRIGGER_AUTOMATION_SPEC)
|
||||
|
||||
|
||||
def external_trigger_history_overrides(
|
||||
metadata: Mapping[str, Any] | None,
|
||||
) -> tuple[str | None, dict[str, Any]]:
|
||||
"""Return session-history text/metadata overrides for an external trigger turn."""
|
||||
return automation_history_overrides_for_spec(
|
||||
metadata,
|
||||
EXTERNAL_TRIGGER_AUTOMATION_SPEC,
|
||||
)
|
||||
@@ -26,7 +26,7 @@ class GatewayServices:
|
||||
workspaces: WebUIWorkspaceController
|
||||
session_manager: Any | None
|
||||
cron_service: Any | None
|
||||
external_trigger_store: Any | None
|
||||
local_trigger_store: Any | None
|
||||
cron_pending_job_ids: Callable[[str], set[str]] | None
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def build_gateway_services(
|
||||
runtime_capabilities_overrides: dict[str, Any] | None,
|
||||
disabled_skills: set[str] | None = None,
|
||||
cron_service: Any | None = None,
|
||||
external_trigger_store: Any | None = None,
|
||||
local_trigger_store: Any | None = None,
|
||||
cron_pending_job_ids: Callable[[str], set[str]] | None = None,
|
||||
logger: Any = default_logger,
|
||||
) -> GatewayServices:
|
||||
@@ -72,7 +72,7 @@ def build_gateway_services(
|
||||
skills_workspace_path=workspace_path,
|
||||
disabled_skills=disabled_skills,
|
||||
cron_service=cron_service,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
cron_pending_job_ids=cron_pending_job_ids,
|
||||
log=logger,
|
||||
)
|
||||
@@ -84,6 +84,6 @@ def build_gateway_services(
|
||||
workspaces=workspaces,
|
||||
session_manager=session_manager,
|
||||
cron_service=cron_service,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
cron_pending_job_ids=cron_pending_job_ids,
|
||||
)
|
||||
|
||||
@@ -8,9 +8,9 @@ from typing import Any, Protocol
|
||||
from nanobot.cron.types import CronJob
|
||||
from nanobot.session.automation_turns import is_automation_history_message
|
||||
from nanobot.session.manager import _message_preview_text
|
||||
from nanobot.triggers.types import ExternalTrigger
|
||||
from nanobot.triggers.local_types import LocalTrigger
|
||||
|
||||
AutomationJob = CronJob | ExternalTrigger
|
||||
AutomationJob = CronJob | LocalTrigger
|
||||
|
||||
|
||||
class _CronServiceLike(Protocol):
|
||||
@@ -24,15 +24,15 @@ class _CronServiceLike(Protocol):
|
||||
) -> list[CronJob]: ...
|
||||
|
||||
|
||||
class _ExternalTriggerStoreLike(Protocol):
|
||||
def list_triggers(self, *, include_disabled: bool = False) -> list[ExternalTrigger]: ...
|
||||
class _LocalTriggerStoreLike(Protocol):
|
||||
def list_triggers(self, *, include_disabled: bool = False) -> list[LocalTrigger]: ...
|
||||
|
||||
def list_for_session(
|
||||
self,
|
||||
session_key: str,
|
||||
*,
|
||||
include_disabled: bool = True,
|
||||
) -> list[ExternalTrigger]: ...
|
||||
) -> list[LocalTrigger]: ...
|
||||
|
||||
|
||||
class _SessionManagerLike(Protocol):
|
||||
@@ -43,7 +43,7 @@ def session_automation_jobs(
|
||||
cron_service: _CronServiceLike | None,
|
||||
session_key: str,
|
||||
*,
|
||||
external_trigger_store: _ExternalTriggerStoreLike | None = None,
|
||||
local_trigger_store: _LocalTriggerStoreLike | None = None,
|
||||
) -> list[AutomationJob]:
|
||||
"""Return user automations attached to the WebUI session."""
|
||||
jobs: list[AutomationJob] = []
|
||||
@@ -54,9 +54,9 @@ def session_automation_jobs(
|
||||
include_disabled=True,
|
||||
)
|
||||
)
|
||||
if external_trigger_store is not None:
|
||||
if local_trigger_store is not None:
|
||||
jobs.extend(
|
||||
external_trigger_store.list_for_session(
|
||||
local_trigger_store.list_for_session(
|
||||
session_key,
|
||||
include_disabled=True,
|
||||
)
|
||||
@@ -68,7 +68,7 @@ def session_automations_payload(
|
||||
cron_service: _CronServiceLike | None,
|
||||
session_key: str,
|
||||
*,
|
||||
external_trigger_store: _ExternalTriggerStoreLike | None = None,
|
||||
local_trigger_store: _LocalTriggerStoreLike | None = None,
|
||||
pending_job_ids: Collection[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return user-created automation jobs attached to a WebUI session."""
|
||||
@@ -77,7 +77,7 @@ def session_automations_payload(
|
||||
session_automation_jobs(
|
||||
cron_service,
|
||||
session_key,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
),
|
||||
pending_job_ids=pending_job_ids,
|
||||
)
|
||||
@@ -87,7 +87,7 @@ def session_automations_payload(
|
||||
def all_automations_payload(
|
||||
cron_service: _CronServiceLike | None,
|
||||
*,
|
||||
external_trigger_store: _ExternalTriggerStoreLike | None = None,
|
||||
local_trigger_store: _LocalTriggerStoreLike | None = None,
|
||||
session_manager: _SessionManagerLike | None = None,
|
||||
pending_job_ids: Collection[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -95,8 +95,8 @@ def all_automations_payload(
|
||||
jobs: list[AutomationJob] = []
|
||||
if cron_service is not None:
|
||||
jobs.extend(cron_service.list_jobs(include_disabled=True))
|
||||
if external_trigger_store is not None:
|
||||
jobs.extend(external_trigger_store.list_triggers(include_disabled=True))
|
||||
if local_trigger_store is not None:
|
||||
jobs.extend(local_trigger_store.list_triggers(include_disabled=True))
|
||||
return {
|
||||
"jobs": serialize_automation_jobs(
|
||||
jobs,
|
||||
@@ -132,7 +132,7 @@ def _serialize_job(
|
||||
include_details: bool = False,
|
||||
session_manager: _SessionManagerLike | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if isinstance(job, ExternalTrigger):
|
||||
if isinstance(job, LocalTrigger):
|
||||
return _serialize_trigger(
|
||||
job,
|
||||
include_details=include_details,
|
||||
@@ -187,7 +187,7 @@ def _serialize_job(
|
||||
|
||||
|
||||
def _serialize_trigger(
|
||||
trigger: ExternalTrigger,
|
||||
trigger: LocalTrigger,
|
||||
*,
|
||||
include_details: bool = False,
|
||||
session_manager: _SessionManagerLike | None = None,
|
||||
@@ -197,16 +197,16 @@ def _serialize_trigger(
|
||||
"id": trigger.id,
|
||||
"name": trigger.name,
|
||||
"enabled": trigger.enabled,
|
||||
"kind": "external_trigger",
|
||||
"kind": "local_trigger",
|
||||
"schedule": {
|
||||
"kind": "external",
|
||||
"kind": "local",
|
||||
"at_ms": None,
|
||||
"every_ms": None,
|
||||
"expr": None,
|
||||
"tz": None,
|
||||
},
|
||||
"payload": {
|
||||
"kind": "external_trigger",
|
||||
"kind": "local_trigger",
|
||||
"message": command,
|
||||
"command": command,
|
||||
},
|
||||
@@ -273,7 +273,7 @@ def _origin_payload(
|
||||
|
||||
|
||||
def _trigger_origin_payload(
|
||||
trigger: ExternalTrigger,
|
||||
trigger: LocalTrigger,
|
||||
session_manager: _SessionManagerLike | None,
|
||||
) -> dict[str, Any] | None:
|
||||
channel = trigger.channel
|
||||
|
||||
+24
-24
@@ -26,7 +26,7 @@ from websockets.http11 import Response
|
||||
from nanobot.command.builtin import builtin_command_palette
|
||||
from nanobot.cron.session_turns import is_bound_cron_job
|
||||
from nanobot.cron.types import CronJob, CronSchedule
|
||||
from nanobot.triggers.types import ExternalTrigger
|
||||
from nanobot.triggers.local_types import LocalTrigger
|
||||
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
|
||||
from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload
|
||||
from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload
|
||||
@@ -90,7 +90,7 @@ if TYPE_CHECKING:
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.cron.service import CronService
|
||||
from nanobot.session.manager import SessionManager
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
|
||||
def _decode_api_key(raw_key: str) -> str | None:
|
||||
@@ -155,7 +155,7 @@ class GatewayHTTPHandler:
|
||||
skills_workspace_path: Path,
|
||||
disabled_skills: set[str] | None = None,
|
||||
cron_service: CronService | None = None,
|
||||
external_trigger_store: ExternalTriggerStore | None = None,
|
||||
local_trigger_store: LocalTriggerStore | None = None,
|
||||
cron_pending_job_ids: Callable[[str], set[str]] | None = None,
|
||||
log: Any = logger,
|
||||
) -> None:
|
||||
@@ -170,7 +170,7 @@ class GatewayHTTPHandler:
|
||||
self.skills_workspace_path = skills_workspace_path
|
||||
self.disabled_skills = disabled_skills or set()
|
||||
self.cron_service = cron_service
|
||||
self.external_trigger_store = external_trigger_store
|
||||
self.local_trigger_store = local_trigger_store
|
||||
self.cron_pending_job_ids = cron_pending_job_ids
|
||||
self._log = log
|
||||
self._runtime_surface = runtime_surface
|
||||
@@ -494,7 +494,7 @@ class GatewayHTTPHandler:
|
||||
session_automations_payload(
|
||||
self.cron_service,
|
||||
decoded_key,
|
||||
external_trigger_store=self.external_trigger_store,
|
||||
local_trigger_store=self.local_trigger_store,
|
||||
pending_job_ids=pending_job_ids,
|
||||
)
|
||||
)
|
||||
@@ -514,7 +514,7 @@ class GatewayHTTPHandler:
|
||||
automation_jobs = session_automation_jobs(
|
||||
self.cron_service,
|
||||
decoded_key,
|
||||
external_trigger_store=self.external_trigger_store,
|
||||
local_trigger_store=self.local_trigger_store,
|
||||
)
|
||||
if automation_jobs and delete_automations not in {"1", "true", "yes"}:
|
||||
return _http_json_response(
|
||||
@@ -526,9 +526,9 @@ class GatewayHTTPHandler:
|
||||
)
|
||||
if automation_jobs:
|
||||
for job in automation_jobs:
|
||||
if isinstance(job, ExternalTrigger):
|
||||
if self.external_trigger_store is not None:
|
||||
self.external_trigger_store.delete(job.id)
|
||||
if isinstance(job, LocalTrigger):
|
||||
if self.local_trigger_store is not None:
|
||||
self.local_trigger_store.delete(job.id)
|
||||
elif self.cron_service is not None:
|
||||
self.cron_service.remove_job(job.id)
|
||||
deleted = self.session_manager.delete_session(decoded_key)
|
||||
@@ -567,7 +567,7 @@ class GatewayHTTPHandler:
|
||||
return _http_json_response(
|
||||
all_automations_payload(
|
||||
self.cron_service,
|
||||
external_trigger_store=self.external_trigger_store,
|
||||
local_trigger_store=self.local_trigger_store,
|
||||
session_manager=self.session_manager,
|
||||
pending_job_ids=self._pending_cron_job_ids_for_all(),
|
||||
)
|
||||
@@ -580,16 +580,16 @@ class GatewayHTTPHandler:
|
||||
) -> Response:
|
||||
if not self.check_api_token(request):
|
||||
return _http_error(401, "Unauthorized")
|
||||
if self.cron_service is None and self.external_trigger_store is None:
|
||||
if self.cron_service is None and self.local_trigger_store is None:
|
||||
return _http_error(503, "automation service unavailable")
|
||||
|
||||
query = _parse_query(request.path)
|
||||
job_id = (_query_first(query, "id") or _query_first(query, "job_id") or "").strip()
|
||||
if not job_id:
|
||||
return _http_error(400, "missing automation id")
|
||||
trigger = self.external_trigger_store.get(job_id) if self.external_trigger_store else None
|
||||
trigger = self.local_trigger_store.get(job_id) if self.local_trigger_store else None
|
||||
if trigger is not None:
|
||||
return self._handle_external_trigger_action(request, action, trigger)
|
||||
return self._handle_local_trigger_action(request, action, trigger)
|
||||
|
||||
if self.cron_service is None:
|
||||
return _http_error(404, "automation not found")
|
||||
@@ -638,34 +638,34 @@ class GatewayHTTPHandler:
|
||||
|
||||
return self._handle_webui_automations(request)
|
||||
|
||||
def _handle_external_trigger_action(
|
||||
def _handle_local_trigger_action(
|
||||
self,
|
||||
request: WsRequest,
|
||||
action: str,
|
||||
trigger: ExternalTrigger,
|
||||
trigger: LocalTrigger,
|
||||
) -> Response:
|
||||
if self.external_trigger_store is None:
|
||||
if self.local_trigger_store is None:
|
||||
return _http_error(503, "trigger service unavailable")
|
||||
if action == "enable":
|
||||
if self.external_trigger_store.enable(trigger.id, enabled=True) is None:
|
||||
if self.local_trigger_store.enable(trigger.id, enabled=True) is None:
|
||||
return _http_error(404, "automation not found")
|
||||
elif action == "disable":
|
||||
if self.external_trigger_store.enable(trigger.id, enabled=False) is None:
|
||||
if self.local_trigger_store.enable(trigger.id, enabled=False) is None:
|
||||
return _http_error(404, "automation not found")
|
||||
elif action == "delete":
|
||||
if not self.external_trigger_store.delete(trigger.id):
|
||||
if not self.local_trigger_store.delete(trigger.id):
|
||||
return _http_error(404, "automation not found")
|
||||
elif action == "run":
|
||||
return _http_error(409, "external trigger requires a CLI message")
|
||||
return _http_error(409, "local trigger requires a CLI message")
|
||||
elif action == "update":
|
||||
values = _automation_values_from_request(request)
|
||||
if values is None:
|
||||
return _http_error(400, "invalid automation update payload")
|
||||
parsed = _parse_external_trigger_update(values)
|
||||
parsed = _parse_local_trigger_update(values)
|
||||
if isinstance(parsed, str):
|
||||
return _http_error(400, parsed)
|
||||
if parsed:
|
||||
if self.external_trigger_store.update(trigger.id, **parsed) is None:
|
||||
if self.local_trigger_store.update(trigger.id, **parsed) is None:
|
||||
return _http_error(404, "automation not found")
|
||||
else:
|
||||
return _http_error(404, "unknown automation action")
|
||||
@@ -884,7 +884,7 @@ def _parse_automation_update(
|
||||
return update
|
||||
|
||||
|
||||
def _parse_external_trigger_update(values: dict[str, Any]) -> dict[str, Any] | str:
|
||||
def _parse_local_trigger_update(values: dict[str, Any]) -> dict[str, Any] | str:
|
||||
update: dict[str, Any] = {}
|
||||
if "name" in values:
|
||||
raw_name = values.get("name")
|
||||
@@ -896,7 +896,7 @@ def _parse_external_trigger_update(values: dict[str, Any]) -> dict[str, Any] | s
|
||||
update["name"] = name
|
||||
forbidden = [key for key in ("message", "schedule") if key in values]
|
||||
if forbidden:
|
||||
return "external trigger updates only support name"
|
||||
return "local trigger updates only support name"
|
||||
return update
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user