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:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user