From 1f5f9d57641718656058a0231c4e307a8c87208c Mon Sep 17 00:00:00 2001 From: Alex Nalin Date: Mon, 23 Mar 2026 12:06:11 +0000 Subject: [PATCH 1/2] feat: Add GENERICSYSTEMEVENT support to RealTimeEvent enum and RealTimeEventListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #383 ## What Adds native dispatch support for `GENERICSYSTEMEVENT` events received via the Symphony datahose. ## Why BDK 2.11.2 silently drops `GENERICSYSTEMEVENT` with "Received event with an unknown type" because the event is not registered in the `RealTimeEvent` enum. Bot developers who subscribe to this event type via `datahose.eventTypes` cannot handle it at all without monkey-patching the BDK internals. ## Changes - `abstract_datafeed_loop.py`: add `GENERICSYSTEMEVENT` to `RealTimeEvent` enum - `real_time_event_listener.py`: add `on_generic_system_event` no-op + import The generated models (`V4GenericSystemEvent`, `V4Payload.generic_system_event`) already exist — no gen/ changes required. ## Testing Verified end-to-end by subscribing to `GENERICSYSTEMEVENT` via `POST /agent/v5/events/read` and confirming `on_generic_system_event` is called on the listener with the correct `V4GenericSystemEvent` payload. --- .../service/datafeed/abstract_datafeed_loop.py | 4 +++- .../datafeed/real_time_event_listener.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/symphony/bdk/core/service/datafeed/abstract_datafeed_loop.py b/symphony/bdk/core/service/datafeed/abstract_datafeed_loop.py index aa9d0d9c..eea010b5 100644 --- a/symphony/bdk/core/service/datafeed/abstract_datafeed_loop.py +++ b/symphony/bdk/core/service/datafeed/abstract_datafeed_loop.py @@ -57,6 +57,7 @@ class RealTimeEvent(Enum): CONNECTIONACCEPTED = ("on_connection_accepted", "connection_accepted") SYMPHONYELEMENTSACTION = ("on_symphony_elements_action", "symphony_elements_action") MESSAGESUPPRESSED = ("on_message_suppressed", "message_suppressed") + GENERICSYSTEMEVENT = ("on_generic_system_event", "generic_system_event") def _set_context_var(current_task, event, listener): @@ -202,7 +203,8 @@ async def _run_listener_method(listener: RealTimeEventListener, event: V4Event): listener_method_name, payload_field_name = RealTimeEvent[event.type].value except KeyError: logger.info("Received event with an unknown type: %s", event.type) - return + return # <-- GENERICSYSTEMEVENT hits this branch + # GENERICSYSTEMEVENT is not in the enum, so every event is dropped. listener_method = getattr(listener, listener_method_name) event_field = getattr(event.payload, payload_field_name) diff --git a/symphony/bdk/core/service/datafeed/real_time_event_listener.py b/symphony/bdk/core/service/datafeed/real_time_event_listener.py index a982ca9a..b7d5430a 100644 --- a/symphony/bdk/core/service/datafeed/real_time_event_listener.py +++ b/symphony/bdk/core/service/datafeed/real_time_event_listener.py @@ -1,3 +1,4 @@ +from symphony.bdk.gen.agent_model.v4_generic_system_event import V4GenericSystemEvent from symphony.bdk.gen.agent_model.v4_connection_accepted import V4ConnectionAccepted from symphony.bdk.gen.agent_model.v4_connection_requested import V4ConnectionRequested from symphony.bdk.gen.agent_model.v4_event import V4Event @@ -166,3 +167,20 @@ async def on_symphony_elements_action( :param initiator: Event initiator. :param event: Symphony Elements Action payload. """ + + async def on_generic_system_event( + self, initiator: V4Initiator, event: V4GenericSystemEvent + ): + """ + Called when a GENERICSYSTEMEVENT event is received. + + Generic system events are platform-level notifications emitted by Symphony's internal Maestro event bus. + The ``event_subtype`` field on the payload identifies the specific event; ``parameters`` carries + subtype-specific data. + + Bot developers should filter on ``event.event_subtype`` rather than relying solely on the outer + ``GENERICSYSTEMEVENT`` type. + + :param initiator: Event initiator. + :param event: Generic system event payload (``V4GenericSystemEvent``). + """ \ No newline at end of file From 374571efd3f947ce08bccd37163b794039018a2a Mon Sep 17 00:00:00 2001 From: Alex Nalin Date: Mon, 23 Mar 2026 12:48:44 +0000 Subject: [PATCH 2/2] feat: Add GENERICSYSTEMEVENT support to RealTimeEvent enum and RealTimeEventListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #383 ## What Adds native dispatch support for `GENERICSYSTEMEVENT` events received via the Symphony datahose. ## Why BDK 2.11.2 silently drops `GENERICSYSTEMEVENT` with "Received event with an unknown type" because the event is not registered in the `RealTimeEvent` enum. Bot developers who subscribe to this event type via `datahose.eventTypes` cannot handle it at all without monkey-patching the BDK internals. ## Changes - `abstract_datafeed_loop.py`: add `GENERICSYSTEMEVENT` to `RealTimeEvent` enum - `real_time_event_listener.py`: add `on_generic_system_event` no-op + import The generated models (`V4GenericSystemEvent`, `V4Payload.generic_system_event`) already exist — no gen/ changes required. ## Testing Verified end-to-end by subscribing to `GENERICSYSTEMEVENT` via `POST /agent/v5/events/read` and confirming `on_generic_system_event` is called on the listener with the correct `V4GenericSystemEvent` payload. Use test_handle_generic_system_event ## Example/Sample code An example specifically for handling `GENERICSYSTEMEVENT` can be found https://github.com/finos/symphony-bdk-python/tree/main/examples/datafeed/datahose_generic_system_event.py --- docsrc/markdown/datafeed.md | 11 +-- .../datafeed/datahose_generic_system_event.py | 68 +++++++++++++++++++ .../datafeed/abstract_datafeed_loop_test.py | 17 +++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 examples/datafeed/datahose_generic_system_event.py diff --git a/docsrc/markdown/datafeed.md b/docsrc/markdown/datafeed.md index af3ca906..1cd627b1 100644 --- a/docsrc/markdown/datafeed.md +++ b/docsrc/markdown/datafeed.md @@ -197,7 +197,8 @@ datahose: eventTypes: # mandatory field, events you want to receive - INSTANTMESSAGECREATED - ROOMCREATED - - ROOMUPDATED + - ROOMUPDATED + - GENERICSYSTEMEVENT retry: # optional maxAttempts: 6 # maximum number of retry attempts @@ -206,10 +207,10 @@ datahose: maxIntervalMillis: 10000 # limit of the interval between two attempts ``` -The minimal configuration for the datahose service is the `eventTypes` field. It should contain at least one value -chosen among [_Real Time Events_](https://docs.developers.symphony.com/building-bots-on-symphony/datafeed/real-time-events) -list and that `MESSAGESENT`, `MESSAGESUPPRESSED` and `SYMPHONYELEMENTSACTION` values can be set only if the ceservice is -properly configured and running in your Symphony agent. +The minimal configuration for the datahose service is the `eventTypes` field. It should contain at least one value +chosen among [_Real Time Events_](...) list. Note that `MESSAGESENT`, `MESSAGESUPPRESSED`, `SYMPHONYELEMENTSACTION` +and `GENERICSYSTEMEVENT` values can be set only if the ceservice is properly configured and running in your +Symphony agent. The `tag` field is optional and is used when creating and reusing datahose feeds. If you have several instances of the same bot and want them to use the same datahose feed (so that events are spread over bot instances), diff --git a/examples/datafeed/datahose_generic_system_event.py b/examples/datafeed/datahose_generic_system_event.py new file mode 100644 index 00000000..787d5c1f --- /dev/null +++ b/examples/datafeed/datahose_generic_system_event.py @@ -0,0 +1,68 @@ +import asyncio +import logging +import logging.config +from pathlib import Path + +from symphony.bdk.core.config.loader import BdkConfigLoader +from symphony.bdk.core.service.datafeed.real_time_event_listener import RealTimeEventListener +from symphony.bdk.core.symphony_bdk import SymphonyBdk +from symphony.bdk.gen.agent_model.v4_generic_system_event import V4GenericSystemEvent +from symphony.bdk.gen.agent_model.v4_initiator import V4Initiator + +# Required config.yaml datahose section: +# +# datahose: +# tag: my-bot-tag +# eventTypes: +# - GENERICSYSTEMEVENT + + +async def run(): + config = BdkConfigLoader.load_from_symphony_dir("config.yaml") + + async with SymphonyBdk(config) as bdk: + datahose_loop = bdk.datahose() + datahose_loop.subscribe(RealTimeEventListenerImpl()) + await datahose_loop.start() + + +class RealTimeEventListenerImpl(RealTimeEventListener): + async def on_generic_system_event(self, initiator: V4Initiator, event: V4GenericSystemEvent): + """Called for every GENERICSYSTEMEVENT received from the datahose. + + GENERICSYSTEMEVENT is a platform-level envelope emitted by Symphony's internal + Maestro event bus. The ``event_subtype`` field identifies the specific event; + ``parameters`` carries subtype-specific data whose structure varies per subtype. + + Always filter on ``event_subtype`` — do not act on every generic event blindly. + """ + subtype = event.event_subtype + + # We do not recommend logging full events in production as it could expose sensitive data + logging.debug("GenericSystemEvent received — subtype: %s", subtype) + + # Filter on the specific subtype relevant to your use case. + # The subtype values are defined by your Symphony deployment; log them first + # to discover which ones are relevant before adding conditional logic. + if subtype == "CONNECTION_REQUEST_ALERT": + # Example: a federation connection lifecycle event. + # event.parameters contains subtype-specific fields. + logging.info("Connection request alert — parameters: %s", event.parameters) + + elif subtype is None: + logging.warning("Received GENERICSYSTEMEVENT with no event_subtype — skipping") + + else: + logging.debug("Unhandled GENERICSYSTEMEVENT subtype: %s", subtype) + + +logging.config.fileConfig( + Path(__file__).parent.parent / "logging.conf", disable_existing_loggers=False +) + + +try: + logging.info("Running datahose generic system event example...") + asyncio.run(run()) +except KeyboardInterrupt: + logging.info("Ending datahose generic system event example") diff --git a/tests/core/service/datafeed/abstract_datafeed_loop_test.py b/tests/core/service/datafeed/abstract_datafeed_loop_test.py index a6c86073..f599f7f7 100644 --- a/tests/core/service/datafeed/abstract_datafeed_loop_test.py +++ b/tests/core/service/datafeed/abstract_datafeed_loop_test.py @@ -1,6 +1,7 @@ # ruff: noqa import asyncio from unittest.mock import AsyncMock, call, patch +from symphony.bdk.gen.agent_model.v4_generic_system_event import V4GenericSystemEvent import pytest @@ -492,6 +493,22 @@ async def test_handle_symphony_element(df_loop, listener, initiator_userid): initiator_userid, payload.symphony_elements_action ) +# This test verifies the full dispatch path: enum lookup → payload extraction → listener method called with correct +# arguments +@pytest.mark.asyncio +async def test_handle_generic_system_event(df_loop, listener, initiator_userid): + payload = V4Payload(generic_system_event=V4GenericSystemEvent()) + event = V4Event( + type=RealTimeEvent.GENERICSYSTEMEVENT.name, + payload=payload, + initiator=initiator_userid, + ) + + await create_and_await_tasks(df_loop, [event]) + + listener.on_generic_system_event.assert_called_once_with( + initiator_userid, payload.generic_system_event + ) @pytest.mark.asyncio async def test_handle_unknown_type(df_loop, listener, initiator_userid):