naeural-client 3.1.5__py3-none-any.whl → 3.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@ from time import sleep
19
19
  from time import time as tm
20
20
 
21
21
  from ..base_decentra_object import BaseDecentrAIObject
22
- from ..bc import DefaultBlockEngine, _DotDict
22
+ from ..bc import DefaultBlockEngine, _DotDict, EE_VPN_IMPL
23
23
  from ..const import (
24
24
  COMMANDS, ENVIRONMENT, HB, PAYLOAD_DATA, STATUS_TYPE,
25
25
  PLUGIN_SIGNATURES, DEFAULT_PIPELINES,
@@ -34,7 +34,9 @@ from .pipeline import Pipeline
34
34
  from .webapp_pipeline import WebappPipeline
35
35
  from .transaction import Transaction
36
36
  from ..utils.config import (
37
- load_user_defined_config, get_user_config_file, get_user_folder, seconds_to_short_format
37
+ load_user_defined_config, get_user_config_file, get_user_folder,
38
+ seconds_to_short_format, log_with_color, set_client_alias,
39
+ EE_SDK_ALIAS_ENV_KEY, EE_SDK_ALIAS_DEFAULT
38
40
  )
39
41
 
40
42
  # from ..default.instance import PLUGIN_TYPES # circular import
@@ -45,6 +47,8 @@ DEBUG_MQTT_SERVER = "r9092118.ala.eu-central-1.emqxsl.com"
45
47
  SDK_NETCONFIG_REQUEST_DELAY = 300
46
48
 
47
49
 
50
+
51
+
48
52
  class GenericSession(BaseDecentrAIObject):
49
53
  """
50
54
  A Session is a connection to a communication server which provides the channel to interact with nodes from the Naeural Edge Protocol network.
@@ -80,7 +84,7 @@ class GenericSession(BaseDecentrAIObject):
80
84
  user=None,
81
85
  pwd=None,
82
86
  secured=None,
83
- name='R1SDK',
87
+ name=None,
84
88
  encrypt_comms=True,
85
89
  config={},
86
90
  filter_workers=None,
@@ -89,9 +93,9 @@ class GenericSession(BaseDecentrAIObject):
89
93
  on_notification=None,
90
94
  on_heartbeat=None,
91
95
  debug_silent=True,
92
- debug=1,
93
- silent=False,
96
+ debug=1, # TODO: debug or verbosity - fix this
94
97
  verbosity=1,
98
+ silent=False,
95
99
  dotenv_path=None,
96
100
  show_commands=False,
97
101
  blockchain_config=BLOCKCHAIN_CONFIG,
@@ -114,32 +118,42 @@ class GenericSession(BaseDecentrAIObject):
114
118
  ----------
115
119
  host : str, optional
116
120
  The hostname of the server. If None, it will be retrieved from the environment variable AIXP_HOSTNAME
121
+
117
122
  port : int, optional
118
123
  The port. If None, it will be retrieved from the environment variable AIXP_PORT
124
+
119
125
  user : str, optional
120
126
  The user name. If None, it will be retrieved from the environment variable AIXP_USERNAME
127
+
121
128
  pwd : str, optional
122
129
  The password. If None, it will be retrieved from the environment variable AIXP_PASSWORD
130
+
123
131
  secured: bool, optional
124
132
  True if connection is secured, by default None
133
+
125
134
  name : str, optional
126
135
  The name of this connection, used to identify owned pipelines on a specific Naeural Edge Protocol edge node.
127
136
  The name will be used as `INITIATOR_ID` and `SESSION_ID` when communicating with Naeural Edge Protocol edge nodes, by default 'pySDK'
137
+
128
138
  config : dict, optional
129
139
  Configures the names of the channels this session will connect to.
130
140
  If using a Mqtt server, these channels are in fact topics.
131
141
  Modify this if you are absolutely certain of what you are doing.
132
142
  By default {}
143
+
133
144
  filter_workers: list, optional
134
145
  If set, process the messages that come only from the nodes from this list.
135
146
  Defaults to None
147
+
136
148
  show_commands : bool
137
149
  If True, will print the commands that are being sent to the Naeural Edge Protocol edge nodes.
138
150
  Defaults to False
151
+
139
152
  log : Logger, optional
140
153
  A logger object which implements basic logging functionality and some other utils stuff. Can be ignored for now.
141
154
  In the future, the documentation for the Logger base class will be available and developers will be able to use
142
155
  custom-made Loggers.
156
+
143
157
  on_payload : Callable[[Session, str, str, str, str, dict], None], optional
144
158
  Callback that handles all payloads received from this network.
145
159
  As arguments, it has a reference to this Session object, the node name, the pipeline, signature and instance, and the payload.
@@ -191,35 +205,45 @@ class GenericSession(BaseDecentrAIObject):
191
205
  If True, the SDK will use the home folder as the base folder for the local cache.
192
206
  NOTE: if you need to use development style ./_local_cache, set this to False.
193
207
  """
208
+
209
+ # TODO: clarify verbosity vs debug
210
+
194
211
  debug = debug or not debug_silent
195
212
  if isinstance(debug, bool):
196
213
  debug = 2 if debug else 0
214
+
215
+ if verbosity > 1 and debug <=1:
216
+ debug = 2
197
217
 
198
218
  self.__debug = int(debug) > 0
219
+ self._verbosity = verbosity
220
+
221
+ if self.__debug:
222
+ if not silent:
223
+ log_with_color(f"Debug mode enabled: {debug=}, {verbosity=}", color='y')
224
+
225
+ ### END verbosity fix needed
199
226
 
200
227
  self.__at_least_one_node_peered = False
201
228
  self.__at_least_a_netmon_received = False
202
229
 
203
230
  # TODO: maybe read config from file?
204
231
  self._config = {**self.default_config, **config}
205
-
206
- if root_topic is not None:
207
- for key in self._config.keys():
208
- if isinstance(self._config[key], dict) and 'TOPIC' in self._config[key]:
209
- if isinstance(self._config[key]["TOPIC"], str) and self._config[key]["TOPIC"].startswith("{}"):
210
- nr_empty = self._config[key]["TOPIC"].count("{}")
211
- self._config[key]["TOPIC"] = self._config[key]["TOPIC"].format(root_topic, *(["{}"] * (nr_empty - 1)))
212
- # end if root_topic
232
+
233
+
234
+
235
+ self.comms_root_topic = root_topic
213
236
 
214
237
  self.__auto_configuration = auto_configuration
215
238
 
216
239
  self.log = log
240
+
241
+
217
242
  self.name = name
218
243
  self.silent = silent
219
-
220
- self.__eth_enabled = eth_enabled
221
244
 
222
- self._verbosity = verbosity
245
+ self._eth_enabled = eth_enabled
246
+
223
247
  self.encrypt_comms = encrypt_comms
224
248
 
225
249
  self._dct_online_nodes_pipelines: dict[str, Pipeline] = {}
@@ -283,6 +307,9 @@ class GenericSession(BaseDecentrAIObject):
283
307
  # use_home_folder allows us to use the home folder as the base folder
284
308
  local_cache_base_folder = str(get_user_folder())
285
309
  # end if
310
+
311
+ ## 1st config step before anything else - we prepare config via ~/.ratio1/config or .env
312
+ self.__load_user_config(dotenv_path=self.__dotenv_path)
286
313
 
287
314
 
288
315
  super(GenericSession, self).__init__(
@@ -295,16 +322,15 @@ class GenericSession(BaseDecentrAIObject):
295
322
  )
296
323
  return
297
324
 
298
- def Pd(self, *args, **kwargs):
299
- if self.__debug:
325
+ def Pd(self, *args, verbosity=1, **kwargs):
326
+ if self.__debug and verbosity <= self._verbosity:
300
327
  kwargs["color"] = 'd' if kwargs.get("color") != 'r' else 'r'
301
- self.log.P(*args, **kwargs)
328
+ kwargs['forced_debug'] = True
329
+ self.D(*args, **kwargs)
302
330
  return
303
331
 
304
332
 
305
- def startup(self):
306
- ## 1st config step - we prepare config via ~/.naeural/config or .env
307
- self.__load_user_config(dotenv_path=self.__dotenv_path)
333
+ def startup(self):
308
334
 
309
335
  # TODO: needs refactoring - suboptimal design
310
336
  # start the blockchain engine assuming config is already set
@@ -319,11 +345,27 @@ class GenericSession(BaseDecentrAIObject):
319
345
  dauth_endp=None, # get from consts or env
320
346
  add_env=self.__auto_configuration,
321
347
  debug=False,
322
- sender_alias='SDK'
348
+ sender_alias=self.name
323
349
  )
324
350
  # end bc_engine
325
351
  # END TODO
326
352
 
353
+
354
+ str_topic = os.environ.get(ENVIRONMENT.EE_ROOT_TOPIC_ENV_KEY, self.comms_root_topic)
355
+
356
+ if str_topic != self.comms_root_topic:
357
+ self.P(f"Changing root topic from '{self.comms_root_topic}' to '{str_topic}'", color='y')
358
+ self.comms_root_topic = str_topic
359
+
360
+ if self.comms_root_topic is not None:
361
+ for key in self._config.keys():
362
+ if isinstance(self._config[key], dict) and 'TOPIC' in self._config[key]:
363
+ if isinstance(self._config[key]["TOPIC"], str) and self._config[key]["TOPIC"].startswith("{}"):
364
+ nr_empty = self._config[key]["TOPIC"].count("{}")
365
+ self._config[key]["TOPIC"] = self._config[key]["TOPIC"].format(self.comms_root_topic, *(["{}"] * (nr_empty - 1)))
366
+ # end if root_topic
367
+
368
+
327
369
  ## last config step
328
370
  self.__fill_config(
329
371
  host=self.__host,
@@ -371,6 +413,13 @@ class GenericSession(BaseDecentrAIObject):
371
413
  self.__start_main_loop_thread()
372
414
  super(GenericSession, self).startup()
373
415
 
416
+
417
+ def _shorten_addr(self, addr: str) -> str:
418
+ if not isinstance(addr, str) or len(addr) < 15 or '...' in addr:
419
+ return addr
420
+ return addr[:11] + '...' + addr[-4:]
421
+
422
+
374
423
  # Message callbacks
375
424
  if True:
376
425
  def __create_user_callback_threads(self):
@@ -578,19 +627,32 @@ class GenericSession(BaseDecentrAIObject):
578
627
  **kwargs
579
628
  )
580
629
  self.bc_engine.sign(msg_to_send)
581
- self.P(f'Sending encrypted payload to <{node_addr}>', color='d')
630
+ self.P(f'Sending encrypted payload to <{self._shorten_addr(node_addr)}>', color='d')
582
631
  self._send_payload(msg_to_send)
583
632
  return
584
633
 
585
- def __request_pipelines_from_net_config_monitor(self, node_addr):
634
+ def __request_pipelines_from_net_config_monitor(self, node_addr=None):
586
635
  """
587
- Request the pipelines for a node from the net-config monitor plugin instance.
636
+ Request the pipelines for a node sending the payload to the
637
+ the net-config monitor plugin instance of that given node or nodes.
638
+
639
+
588
640
  Parameters
589
641
  ----------
590
- node_addr : str or list
642
+ node_addr : str or list (optional)
591
643
  The address or list of the edge node(s) that sent the message.
644
+ If None, the request will be sent to all nodes that are allowed to receive messages.
645
+
646
+ OBSERVATION:
647
+ This method should be called without node_addr(s) as it will get all the known peered nodes
648
+ and request the pipelines from them. Formely, this method was called following a netmon message
649
+ however, this was not the best approach as the netmon message might contain limited amount of
650
+ peer information is some cases.
592
651
 
593
652
  """
653
+ if node_addr is None:
654
+ node_addr = [k for k, v in self._dct_can_send_to_node.items() if v]
655
+ # end if
594
656
  assert node_addr is not None, "Node address cannot be None"
595
657
  payload = {
596
658
  NET_CONFIG.NET_CONFIG_DATA: {
@@ -603,17 +665,53 @@ class GenericSession(BaseDecentrAIObject):
603
665
  }
604
666
  if isinstance(node_addr, str):
605
667
  node_addr = [node_addr]
606
- dest = [
607
- f"<{x}> '{self.__dct_node_address_to_alias.get(x, None)}'" for x in node_addr
608
- ]
609
- self.D(f"<NETCFG> Sending request to:\n{json.dumps(dest, indent=2)}", color='y')
610
- self.send_encrypted_payload(
611
- node_addr=node_addr, payload=payload,
612
- additional_data=additional_data
613
- )
614
- for node in node_addr:
615
- self._dct_netconfig_pipelines_requests[node] = tm()
668
+
669
+ # now we filter only the nodes that have not been requested recently
670
+ node_addr = [x for x in node_addr if self.__needs_netconfig_request(x)]
671
+
672
+ if len(node_addr) > 0:
673
+ dest = [
674
+ f"<{x}> '{self.__dct_node_address_to_alias.get(x, None)}'" for x in node_addr
675
+ ]
676
+ self.D(f"<NC> Sending request to:\n{json.dumps(dest, indent=2)}")
677
+
678
+ self.send_encrypted_payload(
679
+ node_addr=node_addr, payload=payload,
680
+ additional_data=additional_data
681
+ )
682
+ for node in node_addr:
683
+ self._dct_netconfig_pipelines_requests[node] = tm()
684
+ # end if
616
685
  return
686
+
687
+
688
+
689
+ def __needs_netconfig_request(self, node_addr : str) -> bool:
690
+ """
691
+ Check if a net-config request is needed for a node.
692
+
693
+ Parameters
694
+ ----------
695
+ node_addr : str
696
+ The address of the edge node.
697
+
698
+ Returns
699
+ -------
700
+ bool
701
+ True if a net-config request is needed, False otherwise
702
+ """
703
+ short_addr = self._shorten_addr(node_addr)
704
+ last_requested_by_netmon = self._dct_netconfig_pipelines_requests.get(node_addr, 0)
705
+ elapsed = tm() - last_requested_by_netmon
706
+ str_elapsed = f"{elapsed:.0f}s ago" if elapsed < 9999999 else "never"
707
+ needs_netconfig_request = elapsed > SDK_NETCONFIG_REQUEST_DELAY
708
+ if needs_netconfig_request:
709
+ self.D(f"<NC> Node <{short_addr}> needs update as last request was {str_elapsed} > {SDK_NETCONFIG_REQUEST_DELAY}")
710
+ else:
711
+ self.D(f"<NC> Node <{short_addr}> does NOT need update as last request was {str_elapsed} < {SDK_NETCONFIG_REQUEST_DELAY}")
712
+ return needs_netconfig_request
713
+
714
+
617
715
 
618
716
  def __track_allowed_node_by_netmon(self, node_addr, dict_msg):
619
717
  """
@@ -643,38 +741,47 @@ class GenericSession(BaseDecentrAIObject):
643
741
 
644
742
  client_is_allowed = self.bc_engine.contains_current_address(node_whitelist)
645
743
  can_send = not node_secured or client_is_allowed or self.bc_engine.address == node_addr
646
- if node_addr not in self._dct_can_send_to_node and can_send:
744
+ self._dct_can_send_to_node[node_addr] = can_send
745
+ short_addr = self._shorten_addr(node_addr)
746
+ if can_send:
647
747
  if node_online:
648
748
  # only attempt to request pipelines if the node is online and if not recently requested
649
- last_requested_by_netmon = self._dct_netconfig_pipelines_requests.get(node_addr, 0)
650
- if tm() - last_requested_by_netmon > SDK_NETCONFIG_REQUEST_DELAY:
651
- needs_netconfig = True
652
- else:
653
- self.D(f"Node <{node_addr}> is online but pipelines were recently requested", color='y')
749
+ needs_netconfig= self.__needs_netconfig_request(node_addr)
654
750
  else:
655
- self.D(f"Node <{node_addr}> is offline thus NOT sending net-config request", color='y')
751
+ self.D(f"<NC> Node <{short_addr}> is OFFLINE thus NOT sending net-config request")
656
752
  # endif node seen for the first time
657
-
658
- self._dct_can_send_to_node[node_addr] = can_send
659
753
  return needs_netconfig
660
754
 
661
755
 
662
- def __process_node_pipelines(self, node_addr, pipelines):
756
+ def __process_node_pipelines(
757
+ self,
758
+ node_addr : str,
759
+ pipelines : list,
760
+ plugins_statuses : list
761
+ ):
663
762
  """
664
- Given a list of pipeline configurations, create or update the pipelines for a node.
763
+ Given a list of pipeline configurations, create or update the pipelines for a node
764
+ including the liveness of the plugins required for app monitoring
665
765
  """
666
766
  new_pipelines = []
667
767
  if node_addr not in self._dct_online_nodes_pipelines:
668
768
  self._dct_online_nodes_pipelines[node_addr] = {}
669
769
  for config in pipelines:
670
770
  pipeline_name = config[PAYLOAD_DATA.NAME]
671
- pipeline: Pipeline = self._dct_online_nodes_pipelines[node_addr].get(pipeline_name, None)
771
+ pipeline: Pipeline = self._dct_online_nodes_pipelines[node_addr].get(
772
+ pipeline_name, None
773
+ )
672
774
  if pipeline is not None:
673
- pipeline._sync_configuration_with_remote({k.upper(): v for k, v in config.items()})
775
+ pipeline._sync_configuration_with_remote(
776
+ config={k.upper(): v for k, v in config.items()},
777
+ plugins_statuses=plugins_statuses,
778
+ )
674
779
  else:
675
- self._dct_online_nodes_pipelines[node_addr][pipeline_name] = self.__create_pipeline_from_config(
676
- node_addr, config)
677
- new_pipelines.append(self._dct_online_nodes_pipelines[node_addr][pipeline_name])
780
+ pipeline : Pipeline = self.__create_pipeline_from_config(
781
+ node_addr=node_addr, config=config, plugins_statuses=plugins_statuses
782
+ )
783
+ self._dct_online_nodes_pipelines[node_addr][pipeline_name] = pipeline
784
+ new_pipelines.append(pipeline)
678
785
  return new_pipelines
679
786
 
680
787
  def __on_heartbeat(self, dict_msg: dict, msg_node_addr, msg_pipeline, msg_signature, msg_instance):
@@ -685,12 +792,16 @@ class GenericSession(BaseDecentrAIObject):
685
792
  ----------
686
793
  dict_msg : dict
687
794
  The message received from the communication server
795
+
688
796
  msg_node_addr : str
689
797
  The address of the Naeural Edge Protocol edge node that sent the message.
798
+
690
799
  msg_pipeline : str
691
800
  The name of the pipeline that sent the message.
801
+
692
802
  msg_signature : str
693
803
  The signature of the plugin that sent the message.
804
+
694
805
  msg_instance : str
695
806
  The name of the instance that sent the message.
696
807
  """
@@ -714,22 +825,29 @@ class GenericSession(BaseDecentrAIObject):
714
825
  )
715
826
 
716
827
  msg_active_configs = dict_msg.get(HB.CONFIG_STREAMS)
828
+ whitelist = dict_msg.get(HB.EE_WHITELIST, [])
829
+ is_allowed = self.bc_engine.contains_current_address(whitelist)
717
830
  if msg_active_configs is None:
718
831
  msg_active_configs = []
719
832
  # at this point we dont return if no active configs are present
720
833
  # as the protocol should NOT send a heartbeat with active configs to
721
834
  # the entire network, only to the interested parties via net-config
722
-
723
- self.D("<HB> Received {} with {} pipelines".format(
724
- msg_node_addr, len(msg_active_configs)), verbosity=2
835
+ short_addr = self._shorten_addr(msg_node_addr)
836
+ self.D("<HB> Received {} with {} pipelines (wl: {}, allowed: {})".format(
837
+ short_addr, len(msg_active_configs), len(whitelist), is_allowed
838
+ ), verbosity=2
725
839
  )
726
840
 
727
841
  if len(msg_active_configs) > 0:
728
842
  # this is for legacy and custom implementation where heartbeats still contain
729
843
  # the pipeline configuration.
730
844
  pipeline_names = [x.get(PAYLOAD_DATA.NAME, None) for x in msg_active_configs]
731
- self.D(f'<HB> Processing pipelines from <{msg_node_addr}>:{pipeline_names}', color='y')
732
- self.__process_node_pipelines(msg_node_addr, msg_active_configs)
845
+ received_plugins = dict_msg.get(HB.ACTIVE_PLUGINS, [])
846
+ self.D(f'<HB> Processing pipelines from <{short_addr}>:{pipeline_names}', color='y')
847
+ new_pipeliens = self.__process_node_pipelines(
848
+ node_addr=msg_node_addr, pipelines=msg_active_configs,
849
+ plugins_statuses=received_plugins,
850
+ )
733
851
 
734
852
  # TODO: move this call in `__on_message_default_callback`
735
853
  if self.__maybe_ignore_message(msg_node_addr):
@@ -780,7 +898,7 @@ class GenericSession(BaseDecentrAIObject):
780
898
  self.D("Received notification {} from <{}/{}>: {}"
781
899
  .format(
782
900
  notification_type,
783
- msg_node_addr,
901
+ self._shorten_addr(msg_node_addr),
784
902
  msg_pipeline,
785
903
  notification),
786
904
  color=color,
@@ -829,6 +947,8 @@ class GenericSession(BaseDecentrAIObject):
829
947
  online_addresses = []
830
948
  all_addresses = []
831
949
  lst_netconfig_request = []
950
+ short_addr = self._shorten_addr(sender_addr)
951
+ self.D(f"<NM> Processing {len(current_network)} from <{short_addr}> `{ee_id}`")
832
952
  for _ , node_data in current_network.items():
833
953
  needs_netconfig = False
834
954
  node_addr = node_data.get(PAYLOAD_DATA.NETMON_ADDRESS, None)
@@ -846,15 +966,22 @@ class GenericSession(BaseDecentrAIObject):
846
966
  if needs_netconfig:
847
967
  lst_netconfig_request.append(node_addr)
848
968
  # end for each node in network map
849
- self.Pd(f"Net mon from <{sender_addr}> `{ee_id}`: {len(online_addresses)}/{len(all_addresses)}")
850
- if len(lst_netconfig_request) > 0:
851
- self.__request_pipelines_from_net_config_monitor(lst_netconfig_request)
969
+ self.Pd(f"<NM> <{short_addr}> `{ee_id}`: {len(online_addresses)} online of total {len(all_addresses)} nodes")
970
+ first_request = len(self._dct_netconfig_pipelines_requests) == 0
971
+ if len(lst_netconfig_request) > 0 or first_request:
972
+ str_msg = "First request for" if first_request else "Requesting"
973
+ msg = f"<NC> {str_msg} pipelines from at least {len(lst_netconfig_request)} nodes"
974
+ if first_request:
975
+ self.P(msg, color='y')
976
+ else:
977
+ self.Pd(msg, verbosity=2)
978
+ self.__request_pipelines_from_net_config_monitor()
852
979
  # end if needs netconfig
853
980
  nr_peers = sum(self._dct_can_send_to_node.values())
854
981
  if nr_peers > 0 and not self.__at_least_one_node_peered:
855
982
  self.__at_least_one_node_peered = True
856
983
  self.P(
857
- f"Received {PLUGIN_SIGNATURES.NET_MON_01} from {sender_addr}, so far {nr_peers} peers that allow me: {json.dumps(self._dct_can_send_to_node, indent=2)}",
984
+ f"<NM> Received {PLUGIN_SIGNATURES.NET_MON_01} from {sender_addr}, so far {nr_peers} peers that allow me: {json.dumps(self._dct_can_send_to_node, indent=2)}",
858
985
  color='g'
859
986
  )
860
987
  # end for each node in network map
@@ -878,7 +1005,7 @@ class GenericSession(BaseDecentrAIObject):
878
1005
  sender_addr = dict_msg.get(PAYLOAD_DATA.EE_SENDER, None)
879
1006
  short_sender_addr = sender_addr[:8] + '...' + sender_addr[-4:]
880
1007
  if self.client_address == sender_addr:
881
- self.D("<NETCFG> Ignoring message from self", color='d')
1008
+ self.D("<NC> Ignoring message from self", color='d')
882
1009
  return
883
1010
  receiver = dict_msg.get(PAYLOAD_DATA.EE_DESTINATION, None)
884
1011
  if not isinstance(receiver, list):
@@ -888,26 +1015,32 @@ class GenericSession(BaseDecentrAIObject):
888
1015
  op = dict_msg.get(NET_CONFIG.NET_CONFIG_DATA, {}).get(NET_CONFIG.OPERATION, "UNKNOWN")
889
1016
  # drop any incoming request as we are not a net-config provider just a consumer
890
1017
  if op == NET_CONFIG.REQUEST_COMMAND:
891
- self.P(f"<NETCFG> Dropping request from <{short_sender_addr}> `{ee_id}`", color='d')
1018
+ self.Pd(f"<NC> Dropping request from <{short_sender_addr}> `{ee_id}`")
892
1019
  return
893
1020
 
894
1021
  # check if I am allowed to see this payload
895
1022
  if not self.bc_engine.contains_current_address(receiver):
896
- self.P(f"<NETCFG> Received `{op}` from <{short_sender_addr}> `{ee_id}` but I am not in the receiver list: {receiver}", color='d')
1023
+ self.P(f"<NC> Received `{op}` from <{short_sender_addr}> `{ee_id}` but I am not in the receiver list: {receiver}", color='d')
897
1024
  return
898
1025
 
899
1026
  # encryption check. By now all should be decrypted
900
1027
  is_encrypted = dict_msg.get(PAYLOAD_DATA.EE_IS_ENCRYPTED, False)
901
1028
  if not is_encrypted:
902
- self.P(f"<NETCFG> Received from <{short_sender_addr}> `{ee_id}` but it is not encrypted", color='r')
1029
+ self.P(f"<NC> Received from <{short_sender_addr}> `{ee_id}` but it is not encrypted", color='r')
903
1030
  return
904
1031
  net_config_data = dict_msg.get(NET_CONFIG.NET_CONFIG_DATA, {})
905
- received_pipelines = net_config_data.get('PIPELINES', [])
906
- self.D(f"<NETCFG> Received {len(received_pipelines)} pipelines from <{sender_addr}> `{ee_id}`")
907
- new_pipelines = self.__process_node_pipelines(sender_addr, received_pipelines)
1032
+ received_pipelines = net_config_data.get(NET_CONFIG.PIPELINES, [])
1033
+ received_plugins = net_config_data.get(NET_CONFIG.PLUGINS_STATUSES, [])
1034
+ self.D(f"<NC> Received {len(received_pipelines)} pipelines from <{sender_addr}> `{ee_id}`")
1035
+ if self._verbosity > 2:
1036
+ self.D(f"<NC> {ee_id} Netconfig data:\n{json.dumps(net_config_data, indent=2)}")
1037
+ new_pipelines = self.__process_node_pipelines(
1038
+ node_addr=sender_addr, pipelines=received_pipelines,
1039
+ plugins_statuses=received_plugins
1040
+ )
908
1041
  pipeline_names = [x.name for x in new_pipelines]
909
1042
  if len(new_pipelines) > 0:
910
- self.P(f'<NETCFG> Received NEW pipelines from <{sender_addr}> `{ee_id}`:{pipeline_names}', color='y')
1043
+ self.P(f'<NC> Received NEW pipelines from <{sender_addr}> `{ee_id}`:{pipeline_names}', color='y')
911
1044
  return True
912
1045
 
913
1046
 
@@ -996,19 +1129,23 @@ class GenericSession(BaseDecentrAIObject):
996
1129
  return
997
1130
 
998
1131
  try:
1132
+ if EE_VPN_IMPL and self._eth_enabled:
1133
+ self.P("Disabling ETH for VPN implementation", color='r')
1134
+ self._eth_enabled = False
1135
+
999
1136
  self.bc_engine = DefaultBlockEngine(
1000
1137
  log=self.log,
1001
1138
  name=self.name,
1002
1139
  config=blockchain_config,
1003
1140
  verbosity=self._verbosity,
1004
1141
  user_config=user_config,
1005
- eth_enabled=self.__eth_enabled,
1142
+ eth_enabled=self._eth_enabled,
1006
1143
  )
1007
1144
  except:
1008
1145
  raise ValueError("Failure in private blockchain setup:\n{}".format(traceback.format_exc()))
1009
1146
 
1010
1147
  # extra setup flag for re-connections with same multiton instance
1011
- self.bc_engine.set_eth_flag(self.__eth_enabled)
1148
+ self.bc_engine.set_eth_flag(self._eth_enabled)
1012
1149
  return
1013
1150
 
1014
1151
  def __start_main_loop_thread(self):
@@ -1329,8 +1466,8 @@ class GenericSession(BaseDecentrAIObject):
1329
1466
  if True:
1330
1467
 
1331
1468
  def __load_user_config(self, dotenv_path):
1332
- # if the ~/.naeural/config file exists, load the credentials from there else try to load them from .env
1333
- if not load_user_defined_config(verbose=not self.log.silent):
1469
+ # if the ~/.ratio1/config file exists, load the credentials from there else try to load them from .env
1470
+ if not load_user_defined_config(verbose=not self.silent):
1334
1471
  # this method will search for the credentials in the environment variables
1335
1472
  # the path to env file, if not specified, will be search in the following order:
1336
1473
  # 1. current working directory
@@ -1338,14 +1475,33 @@ class GenericSession(BaseDecentrAIObject):
1338
1475
  load_dotenv(dotenv_path=dotenv_path, verbose=False)
1339
1476
  if not self.silent:
1340
1477
  keys = [k for k in os.environ if k.startswith("EE_")]
1341
- print("Loaded credentials from environment variables: {keys}", flush=True)
1478
+ if not self.silent:
1479
+ log_with_color(f"Loaded credentials from environment variables: {keys}", color='y')
1342
1480
  self.__user_config_loaded = False
1343
1481
  else:
1344
1482
  if not self.silent:
1345
1483
  keys = [k for k in os.environ if k.startswith("EE_")]
1346
- print(f"Loaded credentials from `{get_user_config_file()}`: {keys}.", flush=True)
1484
+ if not self.silent:
1485
+ log_with_color(f"Loaded credentials from `{get_user_config_file()}`: {keys}.", color='y')
1347
1486
  self.__user_config_loaded = True
1348
1487
  # endif config loading from ~ or ./.env
1488
+
1489
+ if self.name is None:
1490
+ from naeural_client.logging.logger_mixins.utils_mixin import _UtilsMixin
1491
+ random_name = _UtilsMixin.get_random_name()
1492
+ default = EE_SDK_ALIAS_DEFAULT + '-' + random_name
1493
+ self.name = os.environ.get(EE_SDK_ALIAS_ENV_KEY, default)
1494
+ if EE_SDK_ALIAS_ENV_KEY not in os.environ:
1495
+ if not self.silent:
1496
+ log_with_color(f"Using default SDK alias: {self.name}. Writing the user config file...", color='y')
1497
+ set_client_alias(self.name)
1498
+ #end with
1499
+ else:
1500
+ if not self.silent:
1501
+ log_with_color(f"SDK Alias (from env): {self.name}.", color='y')
1502
+ #end if
1503
+ #end name is None
1504
+ return self.__user_config_loaded
1349
1505
 
1350
1506
  def __fill_config(self, host, port, user, pwd, secured):
1351
1507
  """
@@ -1758,7 +1914,12 @@ class GenericSession(BaseDecentrAIObject):
1758
1914
  self.__open_transactions.append(transaction)
1759
1915
  return transaction
1760
1916
 
1761
- def __create_pipeline_from_config(self, node_addr, config):
1917
+ def __create_pipeline_from_config(
1918
+ self,
1919
+ node_addr : str,
1920
+ config : dict,
1921
+ plugins_statuses : list = None,
1922
+ ):
1762
1923
  pipeline_config = {k.lower(): v for k, v in config.items()}
1763
1924
  name = pipeline_config.pop('name', None)
1764
1925
  plugins = pipeline_config.pop('plugins', None)
@@ -1771,6 +1932,7 @@ class GenericSession(BaseDecentrAIObject):
1771
1932
  name=name,
1772
1933
  plugins=plugins,
1773
1934
  existing_config=pipeline_config,
1935
+ plugins_statuses=plugins_statuses,
1774
1936
  )
1775
1937
 
1776
1938
  return pipeline
@@ -2288,9 +2450,9 @@ class GenericSession(BaseDecentrAIObject):
2288
2450
  bool
2289
2451
  True if the node is online, False otherwise.
2290
2452
  """
2291
-
2453
+ short_addr = self._shorten_addr(node)
2292
2454
  if verbose:
2293
- self.P("Waiting for node '{}' to appear online...".format(node))
2455
+ self.Pd("Waiting for node '{}' to appear online...".format(short_addr))
2294
2456
 
2295
2457
  _start = tm()
2296
2458
  found = self.check_node_online(node)
@@ -2301,9 +2463,9 @@ class GenericSession(BaseDecentrAIObject):
2301
2463
 
2302
2464
  if verbose:
2303
2465
  if found:
2304
- self.P("Node '{}' is online.".format(node))
2466
+ self.P("Node '{}' is online.".format(short_addr))
2305
2467
  else:
2306
- self.P("Node '{}' did not appear online in {:.1f}s.".format(node, tm() - _start), color='r')
2468
+ self.P("Node '{}' did not appear online in {:.1f}s.".format(short_addr, tm() - _start), color='r')
2307
2469
  return found
2308
2470
 
2309
2471
  def wait_for_node_configs(
@@ -2328,8 +2490,8 @@ class GenericSession(BaseDecentrAIObject):
2328
2490
  bool
2329
2491
  True if the node has its configurations loaded, False otherwise.
2330
2492
  """
2331
-
2332
- self.P("Waiting for node '{}' to have its configurations loaded...".format(node))
2493
+ short_addr = self._shorten_addr(node)
2494
+ self.P("Waiting for node '{}' to have its configurations loaded...".format(short_addr))
2333
2495
 
2334
2496
  _start = tm()
2335
2497
  found = self.check_node_config_received(node)
@@ -2339,7 +2501,7 @@ class GenericSession(BaseDecentrAIObject):
2339
2501
  sleep(0.1)
2340
2502
  found = self.check_node_config_received(node)
2341
2503
  if not found and not additional_request_sent and (tm() - _start) > request_time_thr and attempt_additional_requests:
2342
- self.P("Re-requesting configurations of node '{}'...".format(node), show=True)
2504
+ self.P("Re-requesting configurations of node '{}'...".format(short_addr), show=True)
2343
2505
  node_addr = self.__get_node_address(node)
2344
2506
  self.__request_pipelines_from_net_config_monitor(node_addr)
2345
2507
  additional_request_sent = True
@@ -2347,9 +2509,9 @@ class GenericSession(BaseDecentrAIObject):
2347
2509
 
2348
2510
  if verbose:
2349
2511
  if found:
2350
- self.P(f"Received configurations of node '{node}'.")
2512
+ self.P(f"Received configurations of node '{short_addr}'.")
2351
2513
  else:
2352
- self.P(f"Node '{node}' did not send configs in {(tm() - _start)}. Client might not be authorized!", color='r')
2514
+ self.P(f"Node '{short_addr}' did not send configs in {(tm() - _start)}. Client might not be authorized!", color='r')
2353
2515
  return found
2354
2516
 
2355
2517
  def check_node_config_received(self, node):
@@ -2624,6 +2786,7 @@ class GenericSession(BaseDecentrAIObject):
2624
2786
  name,
2625
2787
  signature=PLUGIN_SIGNATURES.TELEGRAM_BASIC_BOT_01,
2626
2788
  message_handler=None,
2789
+ processor_handler=None,
2627
2790
  telegram_bot_token=None,
2628
2791
  telegram_bot_token_env_key=ENVIRONMENT.TELEGRAM_BOT_TOKEN_ENV_KEY,
2629
2792
  telegram_bot_name=None,
@@ -2648,6 +2811,10 @@ class GenericSession(BaseDecentrAIObject):
2648
2811
  message_handler : callable, optional
2649
2812
  The message handler function that will be called when a message is received. Defaults to None.
2650
2813
 
2814
+ processor_handler : callable, optional
2815
+ The processor handler function that will be called in a processing loop within the
2816
+ Telegram bot plugin in parallel with the message handler. Defaults to None.
2817
+
2651
2818
  telegram_bot_token : str, optional
2652
2819
  The Telegram bot token. Defaults to None.
2653
2820
 
@@ -2687,6 +2854,11 @@ class GenericSession(BaseDecentrAIObject):
2687
2854
  )
2688
2855
 
2689
2856
  func_name, func_args, func_base64_code = pipeline._get_method_data(message_handler)
2857
+
2858
+ proc_func_args, proc_func_base64_code =[], None
2859
+ if processor_handler is not None:
2860
+ _, proc_func_args, proc_func_base64_code = pipeline._get_method_data(processor_handler)
2861
+
2690
2862
  if len(func_args) != 2:
2691
2863
  raise ValueError("The message handler function must have exactly 3 arguments: `plugin`, `message` and `user`.")
2692
2864
 
@@ -2700,6 +2872,8 @@ class GenericSession(BaseDecentrAIObject):
2700
2872
  message_handler=func_base64_code,
2701
2873
  message_handler_args=func_args, # mandatory message and user
2702
2874
  message_handler_name=func_name, # not mandatory
2875
+ processor_handler=proc_func_base64_code, # not mandatory
2876
+ processor_handler_args=proc_func_args, # not mandatory
2703
2877
  **kwargs
2704
2878
  )
2705
2879
  return pipeline, instance
@@ -2714,8 +2888,8 @@ class GenericSession(BaseDecentrAIObject):
2714
2888
  telegram_bot_token=None,
2715
2889
  telegram_bot_token_env_key=ENVIRONMENT.TELEGRAM_BOT_TOKEN_ENV_KEY,
2716
2890
  telegram_bot_name=None,
2717
- telegram_bot_name_env_key=ENVIRONMENT.TELEGRAM_BOT_NAME_ENV_KEY,
2718
-
2891
+ telegram_bot_name_env_key=ENVIRONMENT.TELEGRAM_BOT_NAME_ENV_KEY,
2892
+ processor_handler=None,
2719
2893
  system_prompt=None,
2720
2894
  agent_type="API",
2721
2895
  api_token_env_key=ENVIRONMENT.TELEGRAM_API_AGENT_TOKEN_ENV_KEY,
@@ -2789,6 +2963,7 @@ class GenericSession(BaseDecentrAIObject):
2789
2963
  if telegram_bot_name is None:
2790
2964
  message = f"Warning! No Telegram bot name provided as via env {ENVIRONMENT.TELEGRAM_BOT_NAME_ENV_KEY} or explicitly as `telegram_bot_name` param."
2791
2965
  raise ValueError(message)
2966
+
2792
2967
 
2793
2968
 
2794
2969
  pipeline: Pipeline = self.create_pipeline(
@@ -2796,6 +2971,10 @@ class GenericSession(BaseDecentrAIObject):
2796
2971
  name=name,
2797
2972
  # default TYPE is "Void"
2798
2973
  )
2974
+
2975
+ proc_func_args, proc_func_base64_code =[], None
2976
+ if processor_handler is not None:
2977
+ _, proc_func_args, proc_func_base64_code = pipeline._get_method_data(processor_handler)
2799
2978
 
2800
2979
 
2801
2980
  obfuscated_token = telegram_bot_token[:4] + "*" * (len(telegram_bot_token) - 4)
@@ -2803,8 +2982,13 @@ class GenericSession(BaseDecentrAIObject):
2803
2982
  instance = pipeline.create_plugin_instance(
2804
2983
  signature=signature,
2805
2984
  instance_id=self.log.get_unique_id(),
2985
+
2806
2986
  telegram_bot_token=telegram_bot_token,
2807
2987
  telegram_bot_name=telegram_bot_name,
2988
+
2989
+ processor_handler=proc_func_base64_code, # not mandatory
2990
+ processor_handler_args=proc_func_args, # not mandatory
2991
+
2808
2992
  system_prompt=system_prompt,
2809
2993
  agent_type=agent_type,
2810
2994
  api_token=api_token,
@@ -3133,3 +3317,132 @@ class GenericSession(BaseDecentrAIObject):
3133
3317
  if df_only:
3134
3318
  return dct_result[SESSION_CT.NETSTATS_REPORT]
3135
3319
  return dct_result
3320
+
3321
+
3322
+ def get_nodes_apps(
3323
+ self,
3324
+ node=None,
3325
+ owner=None,
3326
+ show_full=False,
3327
+ as_json=False,
3328
+ show_errors=False,
3329
+ as_df=False
3330
+ ):
3331
+ """
3332
+ Get the workload status of a node.
3333
+
3334
+ Parameters
3335
+ ----------
3336
+
3337
+ node : str, optional
3338
+ The address or name of the Naeural Edge Protocol edge node. Defaults to None.
3339
+
3340
+ owner : str, optional
3341
+ The owner of the apps to filter. Defaults to None.
3342
+
3343
+ show_full : bool, optional
3344
+ If True, will show the full configuration of the apps. Defaults to False.
3345
+
3346
+ as_json : bool, optional
3347
+ If True, will return the result as a JSON. Defaults to False.
3348
+
3349
+ show_errors : bool, optional
3350
+ If True, will show the errors. Defaults to False.
3351
+
3352
+ as_df : bool, optional
3353
+ If True, will return the result as a Pandas DataFrame. Defaults to False.
3354
+
3355
+
3356
+ Returns
3357
+ -------
3358
+
3359
+ list
3360
+ A list of dictionaries containing the workload status
3361
+ of the specified node.
3362
+
3363
+
3364
+ """
3365
+ lst_plugin_instance_data = []
3366
+ if node is None:
3367
+ nodes = self.get_active_nodes()
3368
+ else:
3369
+ nodes = [node]
3370
+ found_nodes = []
3371
+ for node in nodes:
3372
+ short_addr = self._shorten_addr(node)
3373
+ # 2. Wait for node to appear online
3374
+ node_found = self.wait_for_node(node)
3375
+ if node_found:
3376
+ found_nodes.append(node)
3377
+
3378
+ # 3. Check if the node is peered with the client
3379
+ is_allowed = self.is_peered(node)
3380
+ if not is_allowed:
3381
+ if show_errors:
3382
+ log_with_color(f"Node {short_addr} is not peered with this client. Skipping..", color='r')
3383
+ continue
3384
+
3385
+ # 4. Wait for node to send the configuration.
3386
+ self.wait_for_node_configs(node)
3387
+ apps = self.get_active_pipelines(node)
3388
+ if apps is None:
3389
+ if show_errors:
3390
+ log_with_color(f"No apps found on node {short_addr}. Client might not be authorized", color='r')
3391
+ continue
3392
+
3393
+ # 5. Maybe exclude admin application.
3394
+ if not show_full:
3395
+ apps = {k: v for k, v in apps.items() if str(k).lower() != 'admin_pipeline'}
3396
+
3397
+ # 6. Show the apps
3398
+ if as_json:
3399
+ # Will print a big JSON with all the app configurations.
3400
+ lst_plugin_instance_data.append({k: v.get_full_config() for k, v in apps.items()})
3401
+ else:
3402
+ for pipeline_name, pipeline in apps.items():
3403
+ pipeline_owner = pipeline.config.get("INITIATOR_ADDR")
3404
+ if owner is not None and owner != pipeline_owner:
3405
+ continue
3406
+ pipeline_alias = pipeline.config.get("INITIATOR_ID")
3407
+ for instance in pipeline.lst_plugin_instances:
3408
+ instance_status = instance.get_status()
3409
+ if len(instance_status) == 0:
3410
+ # this instance is only present in config but is NOT loaded so ignore it
3411
+ continue
3412
+ start_time = instance_status.get('INIT_TIMESTAMP')
3413
+ last_probe = instance_status.get('EXEC_TIMESTAMP')
3414
+ last_data = instance_status.get('LAST_PAYLOAD_TIME')
3415
+ dates = [start_time, last_probe, last_data]
3416
+ for i in range(len(dates)):
3417
+ if isinstance(dates[i], str):
3418
+ if dates[i].startswith('1970'):
3419
+ dates[i] = 'Never'
3420
+ elif '.' in dates[i]:
3421
+ dates[i] = dates[i].split('.')[0]
3422
+ lst_plugin_instance_data.append({
3423
+ 'Node' : node,
3424
+ 'Owner' : pipeline_owner,
3425
+ 'Alias' : pipeline_alias,
3426
+ 'App': pipeline_name,
3427
+ 'Plugin': instance.signature,
3428
+ 'Id': instance.instance_id,
3429
+ 'Start' : dates[0],
3430
+ 'Probe' : dates[1],
3431
+ 'Data' : dates[2],
3432
+ })
3433
+ # endfor instances in app
3434
+ # endfor apps
3435
+ # endif as_json or as dict-for-df
3436
+ # endfor nodes
3437
+ if len(found_nodes) == 0:
3438
+ log_with_color(f'Node(s) {nodes} not found. Please check the configuration.', color='r')
3439
+ return
3440
+ if as_df:
3441
+ df = pd.DataFrame(lst_plugin_instance_data)
3442
+ if not (df.empty or df.shape[0] == 0):
3443
+ df['Node'] = df['Node'].apply(lambda x: self._shorten_addr(x))
3444
+ df['Owner'] = df['Owner'].apply(lambda x: self._shorten_addr(x))
3445
+ # end if not empty
3446
+ return df
3447
+ return lst_plugin_instance_data
3448
+