unaiverse 0.1.12__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.
Files changed (47) hide show
  1. unaiverse/__init__.py +19 -0
  2. unaiverse/agent.py +2226 -0
  3. unaiverse/agent_basics.py +2389 -0
  4. unaiverse/clock.py +234 -0
  5. unaiverse/dataprops.py +1282 -0
  6. unaiverse/hsm.py +2471 -0
  7. unaiverse/modules/__init__.py +18 -0
  8. unaiverse/modules/cnu/__init__.py +17 -0
  9. unaiverse/modules/cnu/cnus.py +536 -0
  10. unaiverse/modules/cnu/layers.py +261 -0
  11. unaiverse/modules/cnu/psi.py +60 -0
  12. unaiverse/modules/hl/__init__.py +15 -0
  13. unaiverse/modules/hl/hl_utils.py +411 -0
  14. unaiverse/modules/networks.py +1509 -0
  15. unaiverse/modules/utils.py +748 -0
  16. unaiverse/networking/__init__.py +16 -0
  17. unaiverse/networking/node/__init__.py +18 -0
  18. unaiverse/networking/node/connpool.py +1332 -0
  19. unaiverse/networking/node/node.py +2752 -0
  20. unaiverse/networking/node/profile.py +446 -0
  21. unaiverse/networking/node/tokens.py +79 -0
  22. unaiverse/networking/p2p/__init__.py +188 -0
  23. unaiverse/networking/p2p/go.mod +127 -0
  24. unaiverse/networking/p2p/go.sum +548 -0
  25. unaiverse/networking/p2p/golibp2p.py +18 -0
  26. unaiverse/networking/p2p/golibp2p.pyi +136 -0
  27. unaiverse/networking/p2p/lib.go +2765 -0
  28. unaiverse/networking/p2p/lib_types.py +311 -0
  29. unaiverse/networking/p2p/message_pb2.py +50 -0
  30. unaiverse/networking/p2p/messages.py +360 -0
  31. unaiverse/networking/p2p/mylogger.py +78 -0
  32. unaiverse/networking/p2p/p2p.py +900 -0
  33. unaiverse/networking/p2p/proto-go/message.pb.go +846 -0
  34. unaiverse/stats.py +1506 -0
  35. unaiverse/streamlib/__init__.py +15 -0
  36. unaiverse/streamlib/streamlib.py +210 -0
  37. unaiverse/streams.py +804 -0
  38. unaiverse/utils/__init__.py +16 -0
  39. unaiverse/utils/lone_wolf.json +28 -0
  40. unaiverse/utils/misc.py +441 -0
  41. unaiverse/utils/sandbox.py +292 -0
  42. unaiverse/world.py +384 -0
  43. unaiverse-0.1.12.dist-info/METADATA +366 -0
  44. unaiverse-0.1.12.dist-info/RECORD +47 -0
  45. unaiverse-0.1.12.dist-info/WHEEL +5 -0
  46. unaiverse-0.1.12.dist-info/licenses/LICENSE +177 -0
  47. unaiverse-0.1.12.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2752 @@
1
+ """
2
+ █████ █████ ██████ █████ █████ █████ █████ ██████████ ███████████ █████████ ██████████
3
+ ░░███ ░░███ ░░██████ ░░███ ░░███ ░░███ ░░███ ░░███░░░░░█░░███░░░░░███ ███░░░░░███░░███░░░░░█
4
+ ░███ ░███ ░███░███ ░███ ██████ ░███ ░███ ░███ ░███ █ ░ ░███ ░███ ░███ ░░░ ░███ █ ░
5
+ ░███ ░███ ░███░░███░███ ░░░░░███ ░███ ░███ ░███ ░██████ ░██████████ ░░█████████ ░██████
6
+ ░███ ░███ ░███ ░░██████ ███████ ░███ ░░███ ███ ░███░░█ ░███░░░░░███ ░░░░░░░░███ ░███░░█
7
+ ░███ ░███ ░███ ░░█████ ███░░███ ░███ ░░░█████░ ░███ ░ █ ░███ ░███ ███ ░███ ░███ ░ █
8
+ ░░████████ █████ ░░█████░░████████ █████ ░░███ ██████████ █████ █████░░█████████ ██████████
9
+ ░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░░ ░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░
10
+ A Collectionless AI Project (https://collectionless.ai)
11
+ Registration/Login: https://unaiverse.io
12
+ Code Repositories: https://github.com/collectionlessai/
13
+ Main Developers: Stefano Melacci (Project Leader), Christian Di Maio, Tommaso Guidi
14
+ """
15
+ import os
16
+ import sys
17
+ import ast
18
+ import cv2
19
+ import copy
20
+ import json
21
+ import math
22
+ import time
23
+ import html
24
+ import queue
25
+ import types
26
+ import asyncio
27
+ import requests
28
+ import threading
29
+ import traceback
30
+ from PIL import Image
31
+ from typing import Dict, Any
32
+ from collections import deque
33
+ from unaiverse.clock import Clock
34
+ from unaiverse.world import World
35
+ from unaiverse.agent import Agent
36
+ from datetime import datetime, timezone
37
+ from unaiverse.networking.p2p.messages import Msg
38
+ from unaiverse.networking.p2p import P2P, P2PError
39
+ from unaiverse.networking.node.connpool import NodeConn
40
+ from unaiverse.networking.node.profile import NodeProfile
41
+ from unaiverse.streams import DataProps, BufferedDataStream
42
+ from unaiverse.utils.misc import (GenException, get_key_considering_multiple_sources, save_node_addresses_to_file,
43
+ PolicyFilterHuman, prepare_app_dir)
44
+
45
+
46
+ class Node:
47
+
48
+ # Each node can host an agent or a world
49
+ AGENT = "agent" # Artificial agent
50
+ WORLD = "world" # World agent
51
+
52
+ # Each node outputs console text with a different color
53
+ TEXT_COLORS = ('\033[91m', '\033[94m', '\033[92m', '\033[93m')
54
+ TEXT_LAST_USED_COLOR = 0
55
+ TEXT_LOCK = threading.Lock()
56
+
57
+ def __init__(self,
58
+ hosted: Agent | World,
59
+ unaiverse_key: str | None = None,
60
+ node_name: str | None = None,
61
+ node_id: str | None = None,
62
+ hidden: bool = False,
63
+ clock_delta: float = 1. / 25.,
64
+ base_identity_dir: str | None = None,
65
+ only_certified_agents: bool = False,
66
+ allowed_node_ids: list[str] | set[str] = None, # Optional: it is loaded from the online profile
67
+ world_masters_node_ids: list[str] | set[str] = None, # Optional: it is loaded from the online profile
68
+ world_masters_node_names: list[str] | set[str] = None, # Optional: it will be converted to node IDs
69
+ allow_connection_through_relay: bool = True,
70
+ talk_to_relay_based_nodes: bool = True,
71
+ run_hook: callable = None,
72
+ send_stats_every: float = 30.,
73
+ save_checkpoint_every: float = -1.):
74
+ """Initializes a new instance of the Node class.
75
+
76
+ Args:
77
+ hosted: The Agent or World entity hosted by this node.
78
+ unaiverse_key: The UNaIVERSE key for authentication (if None, it will be loaded from env var or cache file,
79
+ or you will be asked for it).
80
+ node_name: A human-readable name for the node (using node ID is preferable; use this or node ID, not both).
81
+ node_id: A unique identifier for the node (use this or the node name, not both).
82
+ hidden: A flag to determine if the node is hidden (i.e., only the owner of the account can see it).
83
+ clock_delta: The minimum time delta for the node's clock.
84
+ only_certified_agents: A flag to allow only certified agents to connect.
85
+ allowed_node_ids: A list or set of allowed node IDs to connect (t is loaded from the online profile).
86
+ world_masters_node_ids: A list or set of world masters' node IDs (it is also loaded from online profile).
87
+ world_masters_node_names: A list or set of world masters' node names (using IDs is preferable).
88
+ allow_connection_through_relay: A flag to allow connections through a relay.
89
+ talk_to_relay_based_nodes: A flag to allow talking to relay-based nodes.
90
+ run_hook: A function taking the Node instance as argument, called every cycle.
91
+ send_stats_every: Send the stats update to the world every N seconds.
92
+ save_checkpoint_every: Time interval in seconds to save the hosted entity's state to disk (< 0. not to save).
93
+ """
94
+
95
+ # Checking main arguments
96
+ if not (isinstance(hosted, Agent) or isinstance(hosted, World)):
97
+ raise GenException("Invalid hosted entity, must be Agent or World")
98
+ if not (node_id is None or isinstance(node_id, str)):
99
+ raise GenException("Invalid node ID")
100
+ if not (node_name is None or isinstance(node_name, str)):
101
+ raise GenException("Invalid node name")
102
+ if not (node_name is None or node_id is None):
103
+ raise GenException("Cannot specify both node ID and node name")
104
+ if not (node_name is not None or node_id is not None):
105
+ raise GenException("You must specify either node ID or node name: both are missing")
106
+ if not (unaiverse_key is None or isinstance(unaiverse_key, str)):
107
+ raise GenException("Invalid UNaIVERSE key")
108
+
109
+ # Killing Go debug messages about HTTP
110
+ os.environ["GODEBUG"] = "http2debug=0"
111
+
112
+ # Main attributes
113
+ self.node_id = node_id
114
+ self.run_hook = run_hook
115
+ self.unaiverse_key = unaiverse_key
116
+ self.hosted = hosted
117
+ self.node_type = Node.AGENT if (isinstance(hosted, Agent) and not isinstance(hosted, World)) else Node.WORLD
118
+ self.agent = hosted if self.node_type is Node.AGENT else None
119
+ self.world = hosted if self.node_type is Node.WORLD else None
120
+ try:
121
+ self.clock = Clock(min_delta=clock_delta) # Node clock (synch by NTP servers)
122
+ except ValueError as e:
123
+ print(e)
124
+ go_ahead = False
125
+ while not go_ahead:
126
+ user_choice = input("Proceed with local time (strongly NOT suggested)? (y/n) ")
127
+ if user_choice.strip().lower() == 'y':
128
+ print("Proceeding with local time.")
129
+ go_ahead = True
130
+ elif user_choice.strip().lower() == 'n':
131
+ raise e
132
+ self.clock = Clock(min_delta=clock_delta,
133
+ current_time=datetime.now(timezone.utc).timestamp()) # Node clock (not synced at all!)
134
+ self.conn = None # Manages the network operations in the P2P network
135
+ self.talk_to_relay_based_nodes = talk_to_relay_based_nodes
136
+
137
+ # Expected properties of the nodes that will try to connect to this one
138
+ self.only_certified_agents = only_certified_agents
139
+ self.allowed_node_ids = set(allowed_node_ids) if allowed_node_ids is not None else None
140
+ self.world_masters_node_ids = set(world_masters_node_ids) if world_masters_node_ids is not None else None
141
+
142
+ # Profile
143
+ self.profile = None
144
+ self.send_dynamic_profile_every = 10. if self.node_type is Node.WORLD else 10. # Seconds
145
+ self.get_new_token_every = 23 * 60. * 60. + 30 * 60. # Seconds (23 hours and 30 minutes, safer)
146
+
147
+ # Rendezvous
148
+ self.publish_rendezvous_every = 10.
149
+ self.last_rendezvous_time = 0.
150
+
151
+ # Interview of newly connected nodes
152
+ self.interview_timeout = 60. # Seconds
153
+ self.connect_without_ack_retry_timeout = 30. # Seconds
154
+ self.connect_without_ack_total_timeout = 60. # Seconds
155
+ self.reconnected = set()
156
+
157
+ # Alive messaging
158
+ self.send_alive_every = 2.5 * 60. # Seconds
159
+ self.last_alive_time = 0.
160
+ self.skip_was_alive_check = os.getenv("NODE_IGNORE_ALIVE", "0") == "1"
161
+
162
+ # stats reporting agent -> world
163
+ self.send_stats_every = send_stats_every
164
+ self.save_stats_every = 10. # Seconds
165
+
166
+ # Save agent state
167
+ self.save_checkpoint_every = save_checkpoint_every
168
+
169
+ # Alive messaging
170
+ self.run_start_time = 0.
171
+
172
+ # Root server-related
173
+ self.root_endpoint = 'https://unaiverse.io/api' # WARNING: EDITING THIS ADDRESS VIOLATES THE LICENSE
174
+ self.node_token = ""
175
+ self.public_key = ""
176
+
177
+ # Output console text
178
+ print_level = int(os.getenv("NODE_PRINT", "0")) # 0, 1, 2
179
+ self.print_enabled = print_level > 0
180
+ self.cursor_hidden = False
181
+ NodeSynchronizer.DEBUG = print_level > 1
182
+ NodeConn.DEBUG = print_level > 1
183
+ if print_level == 0:
184
+ self.cursor_hidden = True
185
+ with Node.TEXT_LOCK:
186
+ self.text_color = Node.TEXT_COLORS[Node.TEXT_LAST_USED_COLOR]
187
+ Node.TEXT_LAST_USED_COLOR = (Node.TEXT_LAST_USED_COLOR + 1) % len(Node.TEXT_COLORS)
188
+
189
+ # Print-related logging (for inspector only)
190
+ self._output_messages = [""] * 20
191
+ self._output_messages_ids = [-1] * 20
192
+ self._output_messages_count = 0
193
+ self._output_messages_last_pos = -1
194
+
195
+ # Attributes: handshake-related
196
+ self.agents_to_interview: dict[str, [float, NodeProfile | None]] = {} # Peer_id -> [time, profile | None]
197
+ self.agents_expected_to_send_ack = {}
198
+ self.agents_that_provided_ping_pong = set()
199
+ self.last_rejected_agents = deque(maxlen=self.conn)
200
+ self.joining_world_info = None
201
+ self.first = True
202
+
203
+ # Inspector related
204
+ self.inspector_activated = False
205
+ self.inspector_peer_id = None
206
+ self.debug_server_running = False
207
+ self.__inspector_cache = {"behav": None, "known_streams_count": 0, "all_agents_count": 0}
208
+ self.__inspector_told_to_pause = False
209
+
210
+ # Get key
211
+ self.unaiverse_key = get_key_considering_multiple_sources(self.unaiverse_key)
212
+
213
+ # Getting node ID (retrieving by name), if it was not provided (the node is created if not existing)
214
+ if self.node_id is None:
215
+ node_ids, were_alive = self.get_node_id_by_name([node_name],
216
+ create_if_missing=True)
217
+ self.node_id = node_ids[0]
218
+ if were_alive[0] and not self.skip_was_alive_check:
219
+ raise GenException(f"Cannot access node {node_name}, it is already running! "
220
+ f"(set env variable NODE_IGNORE_ALIVE=1 to ignore this control)")
221
+
222
+ # Automatically create a unique data directory for this specific node
223
+ if base_identity_dir is None:
224
+ base_identity_dir = prepare_app_dir(app_name="unaiverse")
225
+ self.node_identity_dir = os.path.join(base_identity_dir, self.node_id)
226
+ p2p_u_identity_dir = os.path.join(self.node_identity_dir, "p2p_public")
227
+ p2p_w_identity_dir = os.path.join(self.node_identity_dir, "p2p_private")
228
+
229
+ # Getting node ID of world masters, if needed
230
+ if world_masters_node_names is not None and len(world_masters_node_names) > 0:
231
+ master_node_ids, were_alive = self.get_node_id_by_name(world_masters_node_names,
232
+ create_if_missing=True, node_type=Node.AGENT)
233
+ for master_node_name, master_node_id in zip(world_masters_node_names, master_node_ids):
234
+ if master_node_id is None:
235
+ raise GenException(f"Cannot find world master node ID given its name: {master_node_name}")
236
+ else:
237
+ if self.world_masters_node_ids is None:
238
+ self.world_masters_node_ids = set()
239
+ self.world_masters_node_ids.add(master_node_id)
240
+
241
+ # Here you can setup max_instances, max_channels, enable_logging at libp2p level etc.
242
+ P2P.setup_library(enable_logging=os.getenv("NODE_LIBP2PLOG", "0") == "1")
243
+
244
+ # Helper to parse env bools
245
+ env_is_isolated = os.getenv("NODE_IS_ISOLATED", "0") == "1"
246
+ env_is_public = os.getenv("NODE_IS_PUBLIC", "0") == "1"
247
+ env_is_public_relay = os.getenv("NODE_IS_PUBLIC_RELAY", "0") == "1"
248
+ env_use_tls = os.getenv("NODE_USE_TLS", "0") == "1"
249
+ env_start_port = int(os.getenv("NODE_STARTING_PORT", "0"))
250
+ env_domain = os.getenv("DOMAIN", None)
251
+ env_cert_path = os.getenv("TLS_CERT_PATH", None)
252
+ env_key_path = os.getenv("TLS_KEY_PATH", None)
253
+
254
+ # --- PARALLEL P2P NODE CREATION ---
255
+ # 1. Define configurations for both nodes
256
+ p2p_u_config = {
257
+ "identity_dir": p2p_u_identity_dir,
258
+ "port": env_start_port,
259
+ "ips": None,
260
+ "enable_relay_client": allow_connection_through_relay,
261
+ "enable_relay_service": env_is_public_relay,
262
+ "use_broad_limits": False,
263
+ "is_isolated": env_is_isolated,
264
+ "knows_is_public": env_is_public,
265
+ "enable_tls": env_use_tls,
266
+ "domain_name": env_domain,
267
+ "tls_cert_path": env_cert_path,
268
+ "tls_key_path": env_key_path,
269
+ "dht_enabled": True,
270
+ "dht_keep": True
271
+ }
272
+
273
+ p2p_w_config = {
274
+ "identity_dir": p2p_w_identity_dir,
275
+ "port": (env_start_port + 4) if env_start_port > 0 else 0,
276
+ "ips": None,
277
+ "enable_relay_client": allow_connection_through_relay,
278
+ "enable_relay_service": self.node_type is Node.WORLD,
279
+ "use_broad_limits": True,
280
+ "is_isolated": env_is_isolated,
281
+ "knows_is_public": env_is_public,
282
+ "enable_tls": env_use_tls,
283
+ "domain_name": env_domain,
284
+ "tls_cert_path": env_cert_path,
285
+ "tls_key_path": env_key_path,
286
+ "dht_enabled": True,
287
+ "dht_keep": False # close it after autonat
288
+ }
289
+
290
+ # 2. Prepare a dictionary to store results or exceptions
291
+ results = {
292
+ "p2p_u": None,
293
+ "p2p_w": None
294
+ }
295
+
296
+ # 3. Define the worker function for the threads
297
+ def create_p2p_instance(name: str, config: dict):
298
+ try:
299
+ # This is the slow, blocking call
300
+ instance = P2P(**config)
301
+ results[name] = instance
302
+ except Exception as _e:
303
+ # Store the exception if creation fails
304
+ results[name] = _e
305
+ return True
306
+
307
+ # 4. Create and start both threads
308
+ thread_u = threading.Thread(target=create_p2p_instance, args=("p2p_u", p2p_u_config))
309
+ thread_w = threading.Thread(target=create_p2p_instance, args=("p2p_w", p2p_w_config))
310
+
311
+ thread_u.start()
312
+ thread_w.start()
313
+
314
+ # 5. Wait for both threads to complete
315
+ # This BLOCKS the __init__ method until both are done.
316
+ thread_u.join()
317
+ thread_w.join()
318
+
319
+ # 6. Retrieve results and check for errors
320
+ p2p_u: P2P | None = results["p2p_u"]
321
+ p2p_w: P2P | None = results["p2p_w"]
322
+
323
+ if isinstance(p2p_u, Exception):
324
+ # We must re-raise the exception to fail the Node creation
325
+ raise P2PError(f"Failed to initialize public P2P node (p2p_u): {p2p_u}") from p2p_u
326
+ if isinstance(p2p_w, Exception):
327
+ raise P2PError(f"Failed to initialize private P2P node (p2p_w): {p2p_w}") from p2p_w
328
+ if p2p_u is None or p2p_w is None:
329
+ # This should not happen if threads ran, but it's a safe check
330
+ raise P2PError("P2P node creation did not complete, but no exception was caught.")
331
+
332
+ # Get first node token
333
+ self.get_node_token(peer_ids=[p2p_u.peer_id, p2p_w.peer_id]) # Passing both the peer IDs
334
+
335
+ # Get first badge token
336
+ if self.node_type is Node.WORLD:
337
+ self.badge_token = self.__root(api="account/node/cv/badge/token/get", payload={"node_id": self.node_id})
338
+ else:
339
+ self.badge_token = None
340
+
341
+ # Get profile (static)
342
+ profile_static = self.__root(api="/account/node/profile/static/get", payload={"node_id": self.node_id})
343
+
344
+ # Getting list of allowed nodes from the static profile,
345
+ # if we did not already specify it when creating the node in the code (the code has higher priority)
346
+ if (self.allowed_node_ids is None and 'allowed_node_ids' in profile_static and
347
+ profile_static['allowed_node_ids'] is not None and len(profile_static['allowed_node_ids']) > 0):
348
+ self.allowed_node_ids = set(profile_static['allowed_node_ids'])
349
+
350
+ # Getting list of world master nodes from the static profile,
351
+ # if we did not already specify it when creating the node in the code (the code has higher priority)
352
+ if self.node_type is Node.WORLD:
353
+ if (self.world_masters_node_ids is None and 'world_masters_node_ids' in profile_static and
354
+ profile_static['world_masters_node_ids'] is not None
355
+ and len(profile_static['world_masters_node_ids']) > 0):
356
+ self.world_masters_node_ids = set(profile_static['world_masters_node_ids'])
357
+ else:
358
+ self.world_masters_node_ids = None # Clearing this in case the user specified it for a non-world node
359
+
360
+ # Creating the connection manager
361
+ # guessing max number of connections (max number of valid
362
+ # the connection manager will ensure that this limit is fulfilled)
363
+ # however, the actual number of connection attempts handled by libp2p must be higher that
364
+ self.conn = NodeConn(max_connections=profile_static['max_nr_connections'],
365
+ p2p_u=p2p_u,
366
+ p2p_w=p2p_w,
367
+ is_world_node=self.node_type is Node.WORLD,
368
+ public_key=self.public_key,
369
+ token=self.node_token)
370
+
371
+ # Get CV
372
+ cv = self.get_cv()
373
+
374
+ # Creating full node profile putting together static info, dynamic profile, adding P2P node info, CV
375
+ self.profile = NodeProfile(static=profile_static,
376
+ dynamic={'peer_id': p2p_u.peer_id,
377
+ 'peer_addresses': p2p_u.addresses,
378
+ 'private_peer_id': p2p_w.peer_id,
379
+ 'private_peer_addresses': p2p_w.addresses,
380
+ 'connections': {
381
+ 'role': self.hosted.ROLE_BITS_TO_STR[self.hosted.ROLE_PUBLIC]
382
+ },
383
+ 'world_summary': {
384
+ 'world_name':
385
+ profile_static['node_name']
386
+ if self.node_type is Node.WORLD else None
387
+ },
388
+ "world_roles_fsm": None, # This will be filled later if this is a world
389
+ "hidden": hidden # Marking the node as hidden (or not)
390
+ },
391
+ cv=cv) # Adding CV here
392
+
393
+ # Sharing node-level info with the hosted entity
394
+ self.hosted.set_node_info(self.clock, self.conn, self.profile, self.out, self.ask_to_get_in_touch,
395
+ self.__purge, self.node_identity_dir, self.agents_expected_to_send_ack, print_level)
396
+
397
+ # Finally, sending dynamic profile to the root server
398
+ # (send AFTER set_node_info, not before, since set_node_info updates the profile,
399
+ # adding world roles and state machines)
400
+ self.send_dynamic_profile()
401
+
402
+ # Save public addresses
403
+ path_to_append_addresses = os.getenv("NODE_SAVE_RUNNING_ADDRESSES")
404
+ if path_to_append_addresses is not None and os.path.exists(path_to_append_addresses):
405
+ save_node_addresses_to_file(self, public=True, dir_path=path_to_append_addresses,
406
+ filename="running.csv", append=True)
407
+
408
+ # Update lone-wolf machines to replace default wildcards (like <agent>) - the private one will be handled when
409
+ # joining a world
410
+ if self.node_type is Node.AGENT:
411
+ self.agent.behav_lone_wolf.update_wildcard("<agent>", f"{self.get_public_peer_id()}")
412
+
413
+ def out(self, msg: str):
414
+ """Prints a formatted message to the console if printing is enabled.
415
+
416
+ Args:
417
+ msg: The message to be printed.
418
+ """
419
+ if self.print_enabled:
420
+ s = (f"{self.node_type[0:2]}: " +
421
+ ((self.hosted.get_name())[0:6] + ",").ljust(7) +
422
+ f" cy: {self.clock.get_cycle()}")
423
+ s = f"[{s}] {msg}"
424
+ print(f"{self.text_color}{s}\033[0m")
425
+
426
+ if self.inspector_activated or self.debug_server_running:
427
+ last_id = self._output_messages_ids[self._output_messages_last_pos]
428
+ self._output_messages_last_pos = (self._output_messages_last_pos + 1) % len(self._output_messages)
429
+ self._output_messages_count = min(self._output_messages_count + 1, len(self._output_messages))
430
+ self._output_messages_ids[self._output_messages_last_pos] = last_id + 1
431
+ self._output_messages[self._output_messages_last_pos] = html.escape(str(msg), quote=True)
432
+
433
+ def err(self, msg: str):
434
+ """Prints a formatted error message to the console.
435
+
436
+ Args:
437
+ msg: The error message to be printed.
438
+ """
439
+ when = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
440
+ if self.print_enabled:
441
+ self.out(f"<ERROR> [{when}] " + msg)
442
+ print(f"<ERROR> [{when}] " + msg)
443
+ else:
444
+ print(f"<ERROR> [{when}] " + msg)
445
+
446
+ def get_node_id_by_name(self, node_names: list[str], create_if_missing: bool = False,
447
+ node_type: str | None = None) -> tuple[list[str], list[bool]]:
448
+ """Retrieves the node ID by its name from the root server, creating a new node if it's missing and specified.
449
+
450
+ Args:
451
+ node_names: The list with the names of the nodes to retrieve.
452
+ create_if_missing: A flag to create the node if it doesn't exist (only valid for your own nodes).
453
+ node_type: The type of the node to create if missing (when create_if_missing is True) - default: the type of
454
+ the current node.
455
+
456
+ Returns:
457
+ The list of node IDs and the list of boolean flags telling if a node was already alive,
458
+ or an exception if an error occurs.
459
+ """
460
+ try:
461
+ response = self.__root("/account/node/get/id",
462
+ payload={"node_name": node_names,
463
+ "account_token": self.unaiverse_key})
464
+ node_ids = []
465
+ were_alive = []
466
+ missing = []
467
+ for i in range(0, len(response["nodes"])):
468
+ if response["nodes"][i] is not None:
469
+ node_ids.append(response["nodes"][i]["node_id"])
470
+ were_alive.append(response["nodes"][i]["was_alive"])
471
+ else:
472
+ node_ids.append(None)
473
+ were_alive.append(None)
474
+ missing.append(i)
475
+ except Exception as e:
476
+ raise GenException(f"Error while retrieving nodes named {node_names} from server! [{e}]")
477
+
478
+ if create_if_missing:
479
+ for i in missing:
480
+ node_name = node_names[i]
481
+ if "/" in node_name or "@" in node_name: # Cannot create nodes belonging to others
482
+ continue
483
+ try:
484
+ response = self.__root("/account/node/fast_register",
485
+ payload={"node_name": node_name,
486
+ "node_type": self.node_type if node_type is None else node_type,
487
+ "account_token": self.unaiverse_key})
488
+ node_ids[i] = response["node_id"]
489
+ were_alive[i] = False
490
+ except Exception as e:
491
+ raise GenException(f"Error while registering node named {node_name} in server! [{e}]")
492
+ return node_ids, were_alive
493
+
494
+ def send_alive(self) -> bool:
495
+ """Send an alive message to the root server.
496
+
497
+ Returns:
498
+ A boolean flag indicating whether the node was already live before sending this.
499
+ """
500
+ try:
501
+ response = self.__root("/account/node/alive",
502
+ payload={"node_id": self.node_id,
503
+ "account_token": self.unaiverse_key})
504
+ return response["was_alive"]
505
+ except Exception as e:
506
+ self.err(f"Error while sending alive message to server! [{e}]")
507
+
508
+ def get_node_token(self, peer_ids):
509
+ """Generates and retrieves a node token from the root server.
510
+
511
+ Args:
512
+ peer_ids: A list of public and private peer IDs.
513
+ """
514
+ response = None
515
+
516
+ for i in range(0, 3): # It will try 3 times before raising the exception...
517
+ try:
518
+ response = self.__root("/account/node/token/generate",
519
+ payload={"node_id": self.node_id,
520
+ "account_token": self.unaiverse_key
521
+ if self.node_token is None or len(self.node_token) == 0 else None,
522
+ "node_token": self.node_token, "peer_ids": json.dumps(peer_ids)})
523
+ break
524
+ except Exception as e:
525
+ if i < 2:
526
+ self.err(f"Error while getting token from server, retrying...")
527
+ time.sleep(1) # Wait a little bit
528
+ else:
529
+ raise GenException(f"Error while getting token from server [{e}]") # Raise the exception
530
+
531
+ self.node_token = response["token"]
532
+ self.public_key = response["public_key"]
533
+
534
+ # Sharing the token with the connection manager
535
+ if self.conn is not None:
536
+ self.conn.set_token(self.node_token)
537
+
538
+ def get_cv(self):
539
+ """Retrieves the node's CV (Curriculum Vitae) from the root server
540
+
541
+ Returns:
542
+ The node's CV as a dictionary.
543
+ """
544
+ for i in range(0, 3): # It will try 3 times before raising the exception...
545
+ try:
546
+ return self.__root(api="/account/node/cv/get", payload={"node_id": self.node_id})
547
+ except Exception as e:
548
+ self.err(f"Error while getting CV from server [{e}]")
549
+ if i < 2:
550
+ self.out("Retrying...")
551
+ time.sleep(1) # Wait a little bit
552
+ else:
553
+ raise GenException(f"Error while getting CV from server [{e}]")
554
+
555
+ def send_dynamic_profile(self):
556
+ """Sends the node's dynamic profile to the root server."""
557
+ try:
558
+ self.__root(api="/account/node/profile/dynamic/post", payload={"node_id": self.node_id,
559
+ "profile":
560
+ self.profile.get_dynamic_profile()})
561
+ except Exception as e:
562
+ self.err(f"Error while sending dynamic profile to root server [{e}]")
563
+
564
+ async def send_badges(self):
565
+ """Sends new badges assigned by a world node to the root server and notifies the agents (async)."""
566
+ if self.node_type is Node.WORLD:
567
+ peer_id_to_badges = self.world.get_all_badges()
568
+ if len(peer_id_to_badges) > 0:
569
+ self.out(f"Sending {len(peer_id_to_badges)} badges to root server")
570
+ for i in range(0, 3): # It will try 3 times before raising the exception...
571
+ try:
572
+ badges = [badge for _badges in peer_id_to_badges.values() for badge in _badges]
573
+ peer_ids = [peer_id for peer_id, _badges in peer_id_to_badges.items() for _ in _badges]
574
+
575
+ response = self.__root(api="/account/node/cv/badge/assign",
576
+ payload={"badges": badges,
577
+ "world_node_id": self.node_id,
578
+ "world_badge_token": self.badge_token})
579
+
580
+ # Getting the next badge token
581
+ self.badge_token = response["badge_token"]
582
+ badges_states = response["badges_states"] # List of booleans
583
+
584
+ # Check if posting went well and saving the set of peer IDs to contact
585
+ peer_ids_to_notify = set()
586
+ for z in range(0, len(badges_states)):
587
+ ret = badges_states[z]
588
+ if 'state' not in ret or 'code' not in ret['state'] or 'message' not in ret['state']:
589
+ self.err(f"Error while posting a badge assigned to {peer_ids[z]}. "
590
+ f"Badge: {badges[z]}. "
591
+ f"Error message: invalid response format")
592
+ else:
593
+ if ret['state']['code'] != "ok":
594
+ self.err(f"Error while posting a badge assigned to {peer_ids[z]}. "
595
+ f"Badge: {badges[z]}. "
596
+ f"Error message: {ret['state']['message']}")
597
+ else:
598
+ peer_ids_to_notify.add(peer_ids[z])
599
+
600
+ # Notify agents
601
+ for peer_id in peer_ids_to_notify:
602
+ if not (await self.conn.send(peer_id, channel_trail=None, content=None,
603
+ content_type=Msg.GET_CV_FROM_ROOT)):
604
+ self.err(f"Error while sending the request to re-download CV to peer {peer_id}")
605
+
606
+ # Clearing
607
+ self.world.clear_badges()
608
+ break
609
+ except Exception as e:
610
+ self.err(f"Error while sending badges to server or when notifying peers [{e}]")
611
+ if i < 2:
612
+ self.out("Retrying...")
613
+ time.sleep(1) # Wait a little bit
614
+ else:
615
+ self.err(f"Couldn't complete badge sending or notification procedure (stop trying)")
616
+
617
+ def get_public_addresses(self) -> list[str]:
618
+ """Returns the public addresses of the P2P node
619
+
620
+ Returns:
621
+ The list of public addresses.
622
+ """
623
+ return self.conn[NodeConn.P2P_PUBLIC].addresses
624
+
625
+ def get_world_addresses(self) -> list[str]:
626
+ """Returns the world addresses of the P2P node
627
+
628
+ Returns:
629
+ The list of world addresses.
630
+ """
631
+ return self.conn[NodeConn.P2P_WORLD].addresses
632
+
633
+ def get_public_peer_id(self) -> str:
634
+ """Returns the public peer ID of the P2P node
635
+
636
+ Returns:
637
+ The public peer ID.
638
+ """
639
+ return self.conn[NodeConn.P2P_PUBLIC].peer_id
640
+
641
+ def get_world_peer_id(self) -> str:
642
+ """Returns the world peer ID of the P2P node
643
+
644
+ Returns:
645
+ The world peer ID.
646
+ """
647
+ return self.conn[NodeConn.P2P_WORLD].peer_id
648
+
649
+ async def ask_to_get_in_touch(self, node_name: str | None = None, addresses: list[str] | None = None,
650
+ public: bool = True, before_updating_pools_fcn=None, run_count: int = 0):
651
+ """Tries to connect to another agent or world node (async).
652
+
653
+ Args:
654
+ node_name: Name of the node to join (alternative to addresses below)
655
+ addresses: A list of network addresses to connect to (alternative to node_name).
656
+ public: A boolean flag indicating whether to use the public or world P2P network.
657
+ before_updating_pools_fcn: A function to call before updating the connection pools.
658
+ run_count: The number of connection attempts made.
659
+
660
+ Returns:
661
+ The peer ID of the connected node if successful, otherwise None.
662
+ """
663
+
664
+ # Getting arguments
665
+ all_args = locals().copy()
666
+ del all_args['self']
667
+
668
+ # Checking arguments
669
+ if (node_name is None and addresses is None) or (node_name is not None and addresses is not None):
670
+ raise GenException("Cannot specify both node_name and addresses or none of them, check your code!")
671
+
672
+ # Getting addresses, if needed
673
+ if addresses is None:
674
+ try:
675
+ addresses = self.__root(api="account/node/get/addresses",
676
+ payload={"node_name": node_name,
677
+ "account_token": self.unaiverse_key})["addresses"]
678
+ except Exception as e:
679
+ GenException(f"Error while retrieving addresses of node named {node_name} [{e}]")
680
+
681
+ if addresses is None or len(addresses) == 0:
682
+ self.err(f"Addresses of {node_name} were not found, cannot connect!")
683
+ return None
684
+
685
+ # Connecting
686
+ self.out("Connecting to another agent/world...")
687
+ peer_id, through_relay = await self.conn.connect(addresses,
688
+ p2p_name=NodeConn.P2P_PUBLIC if public else NodeConn.P2P_WORLD)
689
+
690
+ if through_relay:
691
+ print("Warning: this connection goes through a relay-based circuit, "
692
+ "so a third-party node is involved in the communication")
693
+
694
+ if peer_id is not None and (not through_relay or self.talk_to_relay_based_nodes):
695
+
696
+ # Ping to test the readiness of the established connection
697
+ self.out(f"Connected, ping-pong...")
698
+ if not (await self.conn.send(peer_id, channel_trail=None, content_type=Msg.MISC,
699
+ content={"ping": "pong", "public": public},
700
+ p2p=self.conn.p2p_name_to_p2p[
701
+ NodeConn.P2P_PUBLIC if public else NodeConn.P2P_WORLD])):
702
+ if run_count < 2:
703
+ return await self.ask_to_get_in_touch(addresses=addresses, public=public,
704
+ before_updating_pools_fcn=before_updating_pools_fcn,
705
+ run_count=run_count + 1)
706
+ else:
707
+ self.err("Connection failed! (ping-pong max trials exceeded)")
708
+ return None
709
+
710
+ self.out("Connected, updating pools...")
711
+ if before_updating_pools_fcn is not None:
712
+ before_updating_pools_fcn(peer_id)
713
+ await self.conn.update()
714
+
715
+ if peer_id not in self.agents_expected_to_send_ack:
716
+ self.agents_expected_to_send_ack[peer_id] = {
717
+ "ask_time": self.clock.get_time(),
718
+ "peer_id": peer_id,
719
+ "retried": False,
720
+ "args_of_ask_to_get_in_touch": all_args
721
+ }
722
+
723
+ self.out(f"Current set of {len(self.agents_expected_to_send_ack)} connected peer IDs that will get our "
724
+ f"profile and are expected to send a confirmation: "
725
+ f"{list(self.agents_expected_to_send_ack.keys())}")
726
+ return peer_id
727
+ else:
728
+ self.err("Connection failed!")
729
+ return None
730
+
731
+ async def ask_to_join_world(self, node_name: str | None = None, addresses: list[str] | None = None, **kwargs):
732
+ """Initiates a request to join a world (async).
733
+
734
+ Args:
735
+ node_name: The name of the node hosting the world to join (alternative to addresses below).
736
+ addresses: A list of network addresses of the world node (alternative to world_name).
737
+ **kwargs: Additional options for joining the world.
738
+
739
+ Returns:
740
+ The public peer ID of the world node if the connection request is successful, otherwise None.
741
+ """
742
+ print("Asking to join world...")
743
+
744
+ # Leave an already entered world (if any)
745
+ world_peer_id = self.profile.get_dynamic_profile()['connections']['world_peer_id']
746
+ if world_peer_id is not None:
747
+ await self.leave(world_peer_id)
748
+
749
+ # Connecting to the world (public)
750
+ peer_id = await self.ask_to_get_in_touch(node_name=node_name, addresses=addresses, public=True)
751
+
752
+ # Saving info
753
+ if peer_id is not None:
754
+ print("Connected on the public network, waiting for handshake...")
755
+ self.joining_world_info = {"world_public_peer_id": peer_id, "options": kwargs}
756
+ else:
757
+ print("Failed to join world!")
758
+ return peer_id
759
+
760
+ async def leave(self, peer_id: str):
761
+ """Disconnects the node from a specific peer, typically a world (async).
762
+
763
+ Args:
764
+ peer_id: The peer ID of the node to leave.
765
+ """
766
+
767
+ if not isinstance(peer_id, str):
768
+ self.err(f"Invalid argument provided to leave(...): {peer_id}")
769
+ return
770
+
771
+ print(f"Leaving {peer_id}...")
772
+
773
+ dynamic_profile = self.profile.get_dynamic_profile()
774
+
775
+ if peer_id == dynamic_profile['connections']['world_peer_id']:
776
+ print("Leaving world...")
777
+
778
+ # Clearing world-related lists in the connection manager (to avoid world agent to connect again)
779
+ self.conn.set_world(None)
780
+ self.conn.set_world_agents_list(None)
781
+ self.conn.set_world_masters_list(None)
782
+
783
+ # Disconnecting all connected world-related agents, including world node (it clears roles too)
784
+ await self.conn.remove_all_world_agents()
785
+
786
+ # Better clear this as well
787
+ if peer_id in self.agents_expected_to_send_ack:
788
+ del self.agents_expected_to_send_ack[peer_id]
789
+
790
+ # Clear profile
791
+ dynamic_profile['connections']['world_peer_id'] = None
792
+ dynamic_profile['connections']['world_agents'] = None
793
+ dynamic_profile['connections']['world_masters'] = None
794
+ self.profile.mark_change_in_connections()
795
+
796
+ # Clearing agent-level info
797
+ await self.agent.clear_world_related_data()
798
+
799
+ # Clearing all joining options
800
+ self.joining_world_info = None
801
+ else:
802
+ if peer_id in self.hosted.all_agents:
803
+ await self.hosted.remove_agent(peer_id)
804
+ await self.conn.remove(peer_id)
805
+
806
+ async def leave_world(self):
807
+ """Initiates the process of leaving a world (async).
808
+
809
+ Returns:
810
+ None.
811
+ """
812
+ if self.profile.get_dynamic_profile()['connections']['world_peer_id'] is not None:
813
+ self.agent.accept_new_role(self.agent.ROLE_PUBLIC)
814
+ self.agent.world_profile = None
815
+ await self.leave(self.profile.get_dynamic_profile()['connections']['world_peer_id'])
816
+
817
+ def search(self, query_text: str, email: str | None = None) -> list[NodeProfile]:
818
+ try:
819
+ profiles_as_list_of_dict = self.__root(api="/discover/search/query", payload={
820
+ "query_text": query_text,
821
+ "email": email,
822
+ "account_token": self.unaiverse_key,
823
+ "peer_id": None, # unused
824
+ "node_id": None # unused
825
+ })
826
+ except Exception as e:
827
+ raise GenException(f"Error while searching! Query: {query_text}, email: {email} [{e}]")
828
+
829
+ try:
830
+ profiles = []
831
+ for p in profiles_as_list_of_dict:
832
+ profiles.append(NodeProfile.from_dict(json.loads(p)))
833
+ except Exception as e:
834
+ raise GenException(f"Error while converting data returned by 'search'! "
835
+ f"Query: {query_text}, email: {email} [{e}]")
836
+ return profiles
837
+
838
+ def run(self, *args, **kwargs):
839
+ """Starts the main execution loop for the node, calling method run_async(...) by means of asyncio.run.
840
+ See documentation of method run_async."""
841
+ try:
842
+ asyncio.run(self.run_async(*args, **kwargs))
843
+ except KeyboardInterrupt:
844
+ pass
845
+
846
+ async def run_async(self, cycles: int | None = None,
847
+ max_time: float | None = None,
848
+ interact_mode: bool = False,
849
+ resume_from_checkpoint: bool = False,
850
+ join_world: str | list[str] | None = None,
851
+ get_in_touch: str | list[str] | None = None,
852
+ **kwargs):
853
+ """Starts the main execution loop for the node (async).
854
+
855
+ Args:
856
+ cycles: The number of clock cycles to run the loop for. If None, runs indefinitely.
857
+ max_time: The maximum time in seconds to run the loop. If None, runs indefinitely.
858
+ interact_mode: A boolean value that turns interactive mode of (still experimental!).
859
+ resume_from_checkpoint: If True, we load the checkpoint saved (if present).
860
+ join_world: The name of the World to join or the list of its addresses.
861
+ get_in_touch: The name of Agent to connect to or the list of its addresses.
862
+ """
863
+
864
+ # Subscribing/creating our own pubsub
865
+ await self.hosted.subscribe_to_pubsub_owned_streams()
866
+
867
+ # Load checkpoint (if exists)
868
+ if resume_from_checkpoint:
869
+ try:
870
+ if not self.hosted.load():
871
+ self.out("No saved state found. Starting fresh.")
872
+ else:
873
+ self.out("Successfully loaded previous agent state.")
874
+ except Exception as e:
875
+ self.err(f"CRITICAL: Found a save file but failed to load it: {e}")
876
+ raise e
877
+
878
+ # Asking to join a World or connect to an Agent, if specified
879
+ joined_this_world = None
880
+ got_in_touch_with_this_lone_wolf = None
881
+ waiting_for_lone_wolves = False
882
+ if join_world is not None:
883
+ if isinstance(join_world, str):
884
+ ret = await self.ask_to_join_world(node_name=join_world, **kwargs)
885
+ elif isinstance(join_world, list):
886
+ ret = await self.ask_to_join_world(addresses=join_world, **kwargs)
887
+ else:
888
+ raise GenException("Invalid value for the 'join_world' argument")
889
+ if ret is None:
890
+ raise GenException(f"Unable to connect to world: {join_world}")
891
+ else:
892
+ joined_this_world = ret # saving peer ID
893
+ elif self.hosted.world_profile is not None:
894
+ # we resumed from a state in which we were in this world, so we reconnect
895
+ world_name = self.hosted.world_profile.get_static_profile()['node_name']
896
+ owner_email = self.hosted.world_profile.get_static_profile()['email']
897
+ ret = await self.ask_to_join_world(node_name=f'{owner_email}/{world_name}', **kwargs)
898
+ if ret is None:
899
+ raise GenException(f"Unable to connect to world: {join_world}")
900
+ else:
901
+ joined_this_world = ret # saving peer ID
902
+ elif get_in_touch is not None:
903
+ if isinstance(get_in_touch, str):
904
+ ret = await self.ask_to_get_in_touch(node_name=get_in_touch, **kwargs)
905
+ elif isinstance(get_in_touch, list):
906
+ ret = await self.ask_to_get_in_touch(addresses=get_in_touch, **kwargs)
907
+ else:
908
+ raise GenException("Invalid value for the 'get_in_touch' argument")
909
+ if ret is None:
910
+ raise GenException(f"Unable to get in touch with agent: {get_in_touch}")
911
+ else:
912
+ got_in_touch_with_this_lone_wolf = ret # saving peer ID
913
+ else:
914
+ waiting_for_lone_wolves = True
915
+
916
+ try:
917
+ if self.cursor_hidden:
918
+ sys.stdout.write("\033[?25l") # Hide cursor
919
+
920
+ last_dynamic_profile_time = self.clock.get_time()
921
+ last_get_token_time = self.clock.get_time()
922
+ last_stats_send_time = self.clock.get_time()
923
+ last_stats_save_time = self.clock.get_time()
924
+ last_state_save_time = self.clock.get_time()
925
+ if not (cycles is None or cycles > 0):
926
+ raise GenException("Invalid number of cycles")
927
+
928
+ # Interactive mode (useful when chatting with lone wolves)
929
+ keyboard_queue = None
930
+ keyboard_listener = None
931
+ processor_img_stream = None
932
+ processor_text_stream = None
933
+ processor_whatever_stream = None
934
+ last_tags = {'text': -1, 'img': -1, 'whatever': -1}
935
+ cap = None
936
+ splash_text_shown = False
937
+ interact_mode_opts: dict | None = None
938
+ log_interact_mode = True
939
+
940
+ if interact_mode:
941
+ from prompt_toolkit import prompt
942
+ from prompt_toolkit.patch_stdout import patch_stdout
943
+
944
+ if self.agent is None:
945
+ raise GenException("Interactive mode is only valid for agents")
946
+ pf = PolicyFilterHuman()
947
+ self.agent.set_policy_filter(pf, public=True)
948
+ self.agent.set_policy_filter(pf, public=False)
949
+ interact_mode_opts = {
950
+ "ready_to_interact": False,
951
+ "set_hsm_debug_state": Agent.get_hsm_debug_state()
952
+ }
953
+ if got_in_touch_with_this_lone_wolf is not None:
954
+ interact_mode_opts["lone_wolf_peer_id"] = got_in_touch_with_this_lone_wolf
955
+ elif joined_this_world is not None:
956
+ interact_mode_opts["world_peer_id"] = joined_this_world
957
+ elif waiting_for_lone_wolves:
958
+ interact_mode_opts["lone_wolf_peer_id"] = None
959
+
960
+ public_streams = "lone_wolf_peer_id" in interact_mode_opts
961
+ proc_streams = self.agent.owned_streams[self.agent.get_proc_input_net_hash(public=public_streams)]
962
+ for stream in proc_streams.values():
963
+ if processor_text_stream is None and stream.props.is_text():
964
+ processor_text_stream = stream
965
+ processor_text_stream.disable()
966
+ if processor_img_stream is None and stream.props.is_img():
967
+ processor_img_stream = stream
968
+ processor_img_stream.disable()
969
+ if processor_whatever_stream is None and (not stream.props.is_img() and not stream.props.is_text()):
970
+ processor_whatever_stream = stream
971
+ processor_whatever_stream.disable()
972
+
973
+ if processor_text_stream is None:
974
+ raise GenException("Interactive mode requires a processor that generates a text stream")
975
+
976
+ def keyboard_listener(k_queue):
977
+ with patch_stdout(raw=True): # type: ignore
978
+ while True:
979
+ webcam_shot = None
980
+ keyboard_msg = prompt("\n👉 ") # Get from keyboards
981
+ if cap is not None:
982
+ _ret, got_shot = cap.read() # Get from webcam
983
+ if _ret:
984
+ target_area = 224 * 224
985
+ webcam_shot = Image.fromarray(cv2.cvtColor(got_shot, cv2.COLOR_BGR2RGB))
986
+ width, height = webcam_shot.size
987
+ current_area = width * height
988
+
989
+ if current_area > target_area:
990
+ scale_factor = math.sqrt(target_area / current_area)
991
+ new_width = int(round(width * scale_factor))
992
+ new_height = int(round(height * scale_factor))
993
+ webcam_shot = webcam_shot.resize((new_width, new_height),
994
+ Image.Resampling.LANCZOS)
995
+
996
+ if keyboard_msg is not None and len(keyboard_msg) > 0:
997
+ k_queue.put((keyboard_msg, webcam_shot, "whatever")) # Store in the asynch queue
998
+
999
+ if keyboard_msg.strip() == "exit" or keyboard_msg.strip() == "quit":
1000
+ k_queue.put((keyboard_msg, webcam_shot, "whatever")) # Store in the asynch queue
1001
+ break
1002
+
1003
+ keyboard_queue = queue.Queue() # Create a thread-safe queue for communication
1004
+ keyboard_listener = threading.Thread(target=keyboard_listener, args=(keyboard_queue,), daemon=True)
1005
+
1006
+ if self.clock.get_cycle() == -1:
1007
+ print("Running " + ("agent node" if self.agent else "world node") + " " +
1008
+ f"(public: {self.get_public_peer_id()}, private: {self.get_world_peer_id()})...")
1009
+
1010
+ # Main loop
1011
+ must_quit = False
1012
+ self.run_start_time = self.clock.get_time()
1013
+ while not must_quit:
1014
+
1015
+ # Sending alive message every "K" seconds
1016
+ if self.clock.get_time() - self.last_alive_time >= self.send_alive_every:
1017
+ was_alive = self.send_alive()
1018
+
1019
+ # Checking only at the first run
1020
+ if self.last_alive_time == 0 and was_alive and not self.skip_was_alive_check:
1021
+ print(f"The node is already alive, maybe running in a different machine? "
1022
+ f"(set env variable NODE_IGNORE_ALIVE=1 to ignore this control)")
1023
+ break # Stopping the running cycle
1024
+ self.last_alive_time = self.clock.get_time()
1025
+
1026
+ # Check inspector
1027
+ if self.inspector_activated:
1028
+ if self.__inspector_told_to_pause:
1029
+ print("Paused by the inspector, waiting...")
1030
+
1031
+ while self.__inspector_told_to_pause:
1032
+ if not self.inspector_activated: # Disconnected
1033
+ self.__inspector_told_to_pause = False
1034
+ print("Inspector is not active/connected anymore, resuming...")
1035
+ break
1036
+
1037
+ public_messages = await self.conn.get_messages(p2p_name=NodeConn.P2P_PUBLIC)
1038
+ for msg in public_messages:
1039
+ if msg.content_type == Msg.INSPECT_CMD:
1040
+
1041
+ # Unpacking piggyback
1042
+ sender_node_id, sender_inspector_mode_on = (msg.piggyback[0:-1],
1043
+ msg.piggyback[-1] == "1")
1044
+
1045
+ # Is message from inspector?
1046
+ sender_is_inspector = (sender_node_id == self.profile.get_static_profile()[
1047
+ 'inspector_node_id'] and
1048
+ sender_inspector_mode_on)
1049
+
1050
+ if sender_is_inspector:
1051
+ await self.__handle_inspector_command(msg.content['cmd'], msg.content['arg'])
1052
+ else:
1053
+ self.err("Inspector command was not sent by the expected inspector node ID "
1054
+ "or no inspector connected")
1055
+ await self.__purge(msg.sender)
1056
+ time.sleep(0.1)
1057
+
1058
+ # Move to the next cycle
1059
+ while not self.clock.next_cycle():
1060
+ time.sleep(0.001) # Seconds (lowest possible granularity level)
1061
+
1062
+ self.out(f">>> Starting clock cycle {self.clock.get_cycle()} <<<")
1063
+
1064
+ # Handle new connections or lost connections
1065
+ await self.__handle_network_connections()
1066
+
1067
+ # Handle (read, execute) received network data/commands
1068
+ await self.__handle_network_messages(interact_mode_opts=interact_mode_opts)
1069
+
1070
+ # Stream live data (generated and environmental)
1071
+ if len(self.hosted.all_agents) > 0:
1072
+ if self.node_type is Node.WORLD:
1073
+ if self.first is True:
1074
+ self.first = False
1075
+ for net_hash, stream_dict in self.hosted.known_streams.items():
1076
+ for stream_obj in stream_dict.values():
1077
+ if isinstance(stream_obj, BufferedDataStream):
1078
+ stream_obj.restart()
1079
+ await self.hosted.send_stream_samples()
1080
+
1081
+ # Trigger HSM of the agent
1082
+ if self.node_type is Node.AGENT:
1083
+ if interact_mode and interact_mode_opts['ready_to_interact']:
1084
+ try:
1085
+ if not splash_text_shown:
1086
+ splash_text_shown = True
1087
+ if "lone_wolf_peer_id" in interact_mode_opts:
1088
+ self.agent.behav_lone_wolf.update_wildcard("<partner>",
1089
+ interact_mode_opts['lone_wolf_peer_id'])
1090
+ print(f"\n*** Connected to agent {interact_mode_opts['lone_wolf_peer_id']} ***")
1091
+ else:
1092
+ print(f"\n*** Connected to world {interact_mode_opts['world_peer_id']} ***")
1093
+ cap = cv2.VideoCapture(0) if processor_img_stream is not None else None
1094
+ print(f"*** Entering interactive mode ***\n")
1095
+ keyboard_listener.start()
1096
+ time.sleep(1)
1097
+
1098
+ original_stdout = sys.stdout # Valid screen-related stream
1099
+ if log_interact_mode:
1100
+ agent_name = self.profile.get_static_profile()['node_name']
1101
+ sys.stdout = open(f'interact_stdout_{agent_name}.txt', 'w', buffering=1)
1102
+ else:
1103
+ sys.stdout = open(os.devnull, 'w') # null stream
1104
+ interact_mode_opts["stdout"] = [original_stdout, sys.stdout]
1105
+
1106
+ self.agent.behav_lone_wolf.print_stream = interact_mode_opts["stdout"][1] # Output off
1107
+ self.agent.behav_lone_wolf.print_ending = "\n"
1108
+ self.agent.behav.print_stream = interact_mode_opts["stdout"][1] # Output off
1109
+ self.agent.behav.print_ending = "\n"
1110
+
1111
+ # Getting message from keyboard
1112
+ msg, image_pil, whatever = keyboard_queue.get_nowait()
1113
+ msg = msg.strip()
1114
+
1115
+ if msg.lower() == "exit" or msg.lower() == "quit":
1116
+
1117
+ # Quit?
1118
+ must_quit = True
1119
+ sys.stdout = interact_mode_opts["stdout"][0]
1120
+ sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
1121
+ interact_mode_opts["stdout"][1].close()
1122
+ if cap is not None:
1123
+ cap.release()
1124
+
1125
+ if self.agent.in_world():
1126
+ await self.leave_world()
1127
+ connected_peer_ids = list(self.agent.all_agents.keys())
1128
+ for peer_id in connected_peer_ids:
1129
+ await self.leave(peer_id)
1130
+ elif msg.lower() == "/debug":
1131
+ self.agent.behav_lone_wolf.set_debug_messages_active(
1132
+ not self.agent.behav_lone_wolf.are_debug_messages_active())
1133
+ self.agent.behav.set_debug_messages_active(
1134
+ not self.agent.behav.are_debug_messages_active())
1135
+ else:
1136
+
1137
+ # Putting message in the processor input stream
1138
+ processor_text_stream.enable()
1139
+ keep_tag = processor_text_stream.get_tag() != last_tags['text']
1140
+ processor_text_stream.set(msg, keep_existing_tag=keep_tag)
1141
+ last_tags['text'] = processor_text_stream.get_tag()
1142
+ processor_text_stream.disable()
1143
+ if processor_img_stream is not None:
1144
+ processor_img_stream.enable()
1145
+ keep_tag = processor_img_stream.get_tag() != last_tags['img']
1146
+ processor_img_stream.set(image_pil, keep_existing_tag=keep_tag)
1147
+ last_tags['img'] = processor_img_stream.get_tag()
1148
+ processor_img_stream.disable()
1149
+ if processor_whatever_stream is not None:
1150
+ processor_whatever_stream.enable()
1151
+ keep_tag = processor_whatever_stream.get_tag() != last_tags['whatever']
1152
+ processor_whatever_stream.set(whatever, keep_existing_tag=keep_tag)
1153
+ last_tags['whatever'] = processor_whatever_stream.get_tag()
1154
+ processor_whatever_stream.disable()
1155
+ except queue.Empty:
1156
+ pass # If nothing has been typed (+ enter)
1157
+
1158
+ if interact_mode and splash_text_shown:
1159
+ self.agent.set_hsm_debug_state(False)
1160
+ self.agent.behav_lone_wolf.print_stream = interact_mode_opts["stdout"][0] # Output on
1161
+ self.agent.behav.print_stream = interact_mode_opts["stdout"][0] # Output on
1162
+
1163
+ # Ordinary behaviour
1164
+ if not must_quit:
1165
+ await self.agent.behave()
1166
+
1167
+ if interact_mode and splash_text_shown:
1168
+ self.agent.behav_lone_wolf.print_stream = interact_mode_opts["stdout"][1] # Output off
1169
+ self.agent.behav.print_stream = interact_mode_opts["stdout"][1] # Output off
1170
+ self.agent.set_hsm_debug_state(interact_mode_opts["set_hsm_debug_state"])
1171
+
1172
+ # Periodic Save
1173
+ if self.save_checkpoint_every > 0.:
1174
+ if self.clock.get_time() - last_state_save_time >= self.save_checkpoint_every:
1175
+ try:
1176
+ self.out("Auto-saving state...")
1177
+ self.hosted.save()
1178
+ last_state_save_time = self.clock.get_time()
1179
+ except Exception as e:
1180
+ self.err(f"Auto-save failed: {e}")
1181
+
1182
+ # Send dynamic profile every "N" seconds
1183
+ if (self.clock.get_time() - last_dynamic_profile_time >= self.send_dynamic_profile_every
1184
+ and self.profile.connections_changed()):
1185
+ try:
1186
+ last_dynamic_profile_time = self.clock.get_time()
1187
+ self.profile.unmark_change_in_connections()
1188
+ await self.send_badges() # Sending and clearing badges
1189
+ self.send_dynamic_profile() # Sending
1190
+ except Exception as e:
1191
+ self.err(f"Error while sending the update dynamic profile (or badges) to the server "
1192
+ f"(trying to go ahead...) [{e}]")
1193
+
1194
+ # Getting a new token every "N" seconds
1195
+ if self.clock.get_time() - last_get_token_time >= self.get_new_token_every:
1196
+ self.get_node_token(peer_ids=[self.get_public_peer_id(), self.get_world_peer_id()])
1197
+ last_get_token_time = self.clock.get_time()
1198
+
1199
+ # Continuously check the addresses of the node for changes
1200
+ try:
1201
+ current_public_addrs = self.conn.p2p_public.addresses
1202
+ current_private_addrs = self.conn.p2p_world.addresses
1203
+ profile_public_addrs = self.profile.get_dynamic_profile().get('peer_addresses', [])
1204
+ profile_private_addrs = self.profile.get_dynamic_profile().get('private_peer_addresses', [])
1205
+
1206
+ if set(current_public_addrs) != set(profile_public_addrs):
1207
+ self.out(f"Address change detected for the public instance! "
1208
+ f"New addresses: {current_public_addrs}")
1209
+
1210
+ # Update profile in-place
1211
+ address_list = self.profile.get_dynamic_profile()['peer_addresses']
1212
+ address_list.clear()
1213
+ address_list.extend(current_public_addrs)
1214
+
1215
+ # mark as changed (-> sends the profile to the root)
1216
+ self.profile.mark_change_in_connections()
1217
+
1218
+ # If private addresses changed, update the profile and notify the world
1219
+ elif set(current_private_addrs) != set(profile_private_addrs):
1220
+ self.out(f"Address change detected for the private instance! "
1221
+ f"New addresses: {current_private_addrs}")
1222
+
1223
+ # Update profile in-place
1224
+ address_list = self.profile.get_dynamic_profile()['private_peer_addresses']
1225
+ address_list.clear()
1226
+ address_list.extend(current_private_addrs)
1227
+
1228
+ # mark as changed (-> sends the profile to the root)
1229
+ self.profile.mark_change_in_connections()
1230
+
1231
+ world_peer_id = self.profile.get_dynamic_profile().get('connections', {}).get('world_peer_id')
1232
+ if self.node_type is Node.AGENT and world_peer_id:
1233
+ self.out("Notifying world of address change...")
1234
+ await self.conn.send(
1235
+ world_peer_id, content_type=Msg.ADDRESS_UPDATE, channel_trail=None,
1236
+ content={'addresses': self.profile.get_dynamic_profile()['private_peer_addresses']}
1237
+ )
1238
+ else:
1239
+ self.out("No address changes detected.")
1240
+ except Exception as e:
1241
+ self.err(f"Failed to check for address updates: {e}")
1242
+
1243
+ # Send stats to the world
1244
+ if self.node_type is Node.AGENT and self.agent.in_world():
1245
+ if self.clock.get_time() - last_stats_send_time >= self.send_stats_every:
1246
+ try:
1247
+ self.out(f"[NODE] Sending stats update to the world...")
1248
+ last_stats_send_time = self.clock.get_time()
1249
+ await self.agent.send_stats_to_world()
1250
+ except Exception as e:
1251
+ self.err(f"Error while sending stats to the world (trying to go ahead...) [{e}]")
1252
+
1253
+ # Save stats to disk if this is the world node
1254
+ if self.node_type is Node.WORLD:
1255
+ if self.clock.get_time() - last_stats_save_time >= self.save_stats_every:
1256
+ try:
1257
+ self.out(f"[NODE] Saving stats to disk (world)...")
1258
+ last_stats_save_time = self.clock.get_time()
1259
+ self.world.stats.save_to_disk()
1260
+ except Exception as e:
1261
+ self.err(f"Error while saving stats to disk [{e}]")
1262
+
1263
+ # Taking to the inspector
1264
+ if self.inspector_activated:
1265
+ await self.__send_to_inspector()
1266
+
1267
+ # Execute User Callback
1268
+ if self.run_hook is not None:
1269
+ try:
1270
+ self.run_hook(self)
1271
+ # if asyncio.iscoroutinefunction(self.run_hook):
1272
+ # await self.run_hook(self)
1273
+ # else:
1274
+ # self.run_hook(self)
1275
+ except Exception as e:
1276
+ self.err(f"Error in step_callback: {e}")
1277
+
1278
+ # Stop conditions
1279
+ if cycles is not None and ((self.clock.get_cycle() + 1) >= cycles):
1280
+ break
1281
+ if max_time is not None and (self.clock.get_time() - self.run_start_time) >= max_time:
1282
+ break
1283
+
1284
+ except KeyboardInterrupt:
1285
+ if self.cursor_hidden:
1286
+ sys.stdout.write("\033[?25h") # Re-enabling cursor
1287
+ print("\nDetected Ctrl+C! Exiting gracefully...")
1288
+
1289
+ except asyncio.CancelledError:
1290
+ print("\nDetected process termination! Exiting gracefully...")
1291
+ raise
1292
+
1293
+ except Exception as e:
1294
+ if self.cursor_hidden:
1295
+ sys.stdout.write("\033[?25h") # Re-enabling cursor
1296
+ print(f"An error occurred: {e}")
1297
+ traceback.print_exc()
1298
+
1299
+ finally:
1300
+ if self.cursor_hidden:
1301
+ sys.stdout.write("\033[?25h") # Re-enabling cursor
1302
+
1303
+ try:
1304
+ if self.save_checkpoint_every > 0.:
1305
+ print("[NODE] Saving hosted agent state to disk...")
1306
+ self.hosted.save()
1307
+ except Exception as e:
1308
+ self.err(f"Error saving hosted agent state: {e}")
1309
+
1310
+ try:
1311
+ if self.node_type is Node.WORLD and self.world is not None:
1312
+ print("[NODE] Shutting down stats database...")
1313
+ self.world.stats.shutdown()
1314
+ except Exception as e:
1315
+ self.err(f"Error closing database: {e}")
1316
+
1317
+ try:
1318
+ if self.node_type is Node.AGENT and self.agent.in_world():
1319
+ await self.leave_world()
1320
+ except Exception:
1321
+ pass
1322
+
1323
+ finally:
1324
+ try:
1325
+ connected_peer_ids = list(self.hosted.all_agents.keys())
1326
+ for peer_id in connected_peer_ids:
1327
+ await self.leave(peer_id)
1328
+ except Exception:
1329
+ pass
1330
+
1331
+ async def __handle_network_connections(self):
1332
+ """Manages new and lost network connections (async)."""
1333
+
1334
+ # Getting fresh lists of existing world agents and world masters (from the rendezvous)
1335
+ if self.node_type is Node.AGENT:
1336
+ self.out("Updating list of world agents and world masters by using data from the rendezvous")
1337
+ await self.conn.set_world_agents_and_world_masters_lists_from_rendezvous()
1338
+
1339
+ # Updating connection pools, getting back the lists (well, dictionaries) of new agents and lost agents
1340
+ new_peer_ids_by_pool, removed_peer_ids_by_pool = await self.conn.update()
1341
+ if len(new_peer_ids_by_pool) > 0 or len(removed_peer_ids_by_pool) > 0:
1342
+ self.out("Current status of the pools, right after the update:\n" + str(self.conn))
1343
+
1344
+ # Checking if some peers were removed
1345
+ an_agent_left_the_world = False
1346
+ removed_peers = False
1347
+ for pool_name, removed_peer_ids in removed_peer_ids_by_pool.items():
1348
+ for peer_id in removed_peer_ids:
1349
+ removed_peers = True
1350
+ self.out("Removing a not-connected-anymore peer, "
1351
+ "pool_name: " + pool_name + ", peer_id: " + peer_id + "...")
1352
+ await self.__purge(peer_id)
1353
+
1354
+ # Checking if we removed an agent from this world
1355
+ if self.node_type is Node.WORLD and pool_name in self.conn.WORLD:
1356
+ an_agent_left_the_world = True
1357
+
1358
+ # Check if the world disconnected: in that case, disconnect all the other agents in the world and leave
1359
+ if self.node_type is Node.AGENT and pool_name in self.conn.WORLD_NODE:
1360
+ await self.leave_world()
1361
+
1362
+ # Checking if the inspector disconnected
1363
+ if peer_id == self.inspector_peer_id:
1364
+ self.inspector_activated = False
1365
+ self.inspector_peer_id = None
1366
+ self.__inspector_cache = {"behav": None, "known_streams_count": 0, "all_agents_count": 0}
1367
+ print("Inspector disconnected")
1368
+
1369
+ # Handling newly connected peers
1370
+ an_agent_joined_the_world = False
1371
+ added_peers = False
1372
+ for r in self.reconnected:
1373
+ pool_name = self.conn.get_pool_of(r)
1374
+ if pool_name is None:
1375
+ continue
1376
+ if pool_name not in new_peer_ids_by_pool:
1377
+ new_peer_ids_by_pool[pool_name] = {r}
1378
+ else:
1379
+ new_peer_ids_by_pool[pool_name].add(r)
1380
+ self.reconnected.clear()
1381
+ for pool_name, new_peer_ids in new_peer_ids_by_pool.items():
1382
+ for peer_id in new_peer_ids:
1383
+ added_peers = True
1384
+ self.out("Processing a newly connected peers, "
1385
+ "pool_name: " + pool_name + ", peer_id: " + peer_id + "...")
1386
+
1387
+ # If this is a world node, it is time to tell the world object that a new agent is there
1388
+ if self.node_type is Node.WORLD and pool_name in self.conn.WORLD:
1389
+ self.out("Not considering interviewing since this is a world and the considered peer is in the"
1390
+ " world pools")
1391
+
1392
+ if peer_id in self.agents_to_interview:
1393
+
1394
+ # Getting the new agent profile
1395
+ profile = self.agents_to_interview[peer_id][1] # [time, profile]
1396
+
1397
+ # Adding the new agent to the world object
1398
+ if not (await self.world.add_agent(peer_id=peer_id, profile=profile)):
1399
+ await self.__purge(peer_id)
1400
+ continue
1401
+
1402
+ # Clearing the profile from the interviews
1403
+ del self.agents_to_interview[peer_id] # Removing from the queue (private peer id)
1404
+ an_agent_joined_the_world = True
1405
+
1406
+ # Replacing multi-address with what comes from the profile (there are more addresses there!)
1407
+ self.conn.set_addresses_in_peer_info(peer_id,
1408
+ profile.get_dynamic_profile()['private_peer_addresses'])
1409
+ else:
1410
+
1411
+ # This agent tried to connect to a world "directly", without passing through the
1412
+ # public handshake
1413
+ await self.__purge(peer_id)
1414
+ continue
1415
+
1416
+ continue # Nothing else to do
1417
+
1418
+ # Both if this is an agent or a world, checks if the newly connected agent can be added or not to the
1419
+ # queue of agents to interview
1420
+ if pool_name not in self.conn.OUTGOING:
1421
+
1422
+ # Trying to add to the queue
1423
+ enqueued_for_interview = await self.__interview_enqueue(peer_id)
1424
+
1425
+ # If the agent is rejected at this stage, we disconnect from its peer
1426
+ if not enqueued_for_interview:
1427
+ self.out(f"Not enqueued for interview, removing peer (disconnecting {peer_id})")
1428
+ await self.__purge(peer_id)
1429
+ else:
1430
+ self.out("Enqueued for interview")
1431
+
1432
+ # Updating list of world agents & friends, if needed
1433
+ # (it happens only if the node hosts a world, otherwise 'an_agent_joined_the_world' and
1434
+ # 'an_agent_left_the_world' are certainly False)
1435
+ world_agents_peer_infos = None
1436
+ world_masters_peer_infos = None
1437
+ if self.node_type is Node.WORLD:
1438
+ enter_left = an_agent_joined_the_world or an_agent_left_the_world
1439
+ timeout = (self.clock.get_time() - self.last_rendezvous_time) >= self.publish_rendezvous_every
1440
+
1441
+ if enter_left or timeout or self.world.role_changed_by_world or self.world.received_address_update:
1442
+ if enter_left or self.world.role_changed_by_world:
1443
+
1444
+ # Updating world-node profile with the summary of currently connected agents in the world
1445
+ world_agents_peer_infos = self.conn.get_all_connected_peer_infos(NodeConn.WORLD_AGENTS)
1446
+ world_masters_peer_infos = self.conn.get_all_connected_peer_infos(NodeConn.WORLD_MASTERS)
1447
+
1448
+ dynamic_profile = self.profile.get_dynamic_profile()
1449
+ dynamic_profile['world_summary']['world_agents'] = world_agents_peer_infos
1450
+ dynamic_profile['world_summary']['world_masters'] = world_masters_peer_infos
1451
+ dynamic_profile['world_summary']["world_agents_count"] = len(world_agents_peer_infos)
1452
+ dynamic_profile['world_summary']["world_masters_count"] = len(world_masters_peer_infos)
1453
+ dynamic_profile['world_summary']["total_agents"] = (len(world_agents_peer_infos) +
1454
+ len(world_masters_peer_infos))
1455
+ self.profile.mark_change_in_connections()
1456
+
1457
+ # Publish updated list of (all) world agents (i.e., both agents and masters)
1458
+ world_all_peer_infos = self.conn.get_all_connected_peer_infos(NodeConn.WORLD)
1459
+ if not (await self.conn.publish(self.conn.p2p_world.peer_id,
1460
+ f"{self.conn.p2p_world.peer_id}::ps:rv",
1461
+ content_type=Msg.WORLD_AGENTS_LIST,
1462
+ content={"peers": world_all_peer_infos,
1463
+ "update_count": self.clock.get_cycle()})):
1464
+ self.err("Failed to publish the updated list of (all) world agents (ignoring)")
1465
+ else:
1466
+ self.last_rendezvous_time = self.clock.get_time()
1467
+ self.out(f"Rendezvous messages just published "
1468
+ f"(tag: {self.clock.get_cycle()}, peers: {len(world_all_peer_infos)})")
1469
+
1470
+ # Clearing
1471
+ self.world.role_changed_by_world = False
1472
+ self.world.received_address_update = False
1473
+
1474
+ # Updating list of node connections (being this a world or a plain agent)
1475
+ if added_peers or removed_peers:
1476
+
1477
+ # The following could have been already computed in the code above, let's reuse
1478
+ if world_agents_peer_infos is None:
1479
+ world_agents_peer_infos = self.conn.get_all_connected_peer_infos(NodeConn.WORLD_AGENTS)
1480
+ if world_masters_peer_infos is None:
1481
+ world_masters_peer_infos = self.conn.get_all_connected_peer_infos(NodeConn.WORLD_MASTERS)
1482
+ world_private_peer_id = self.conn.get_all_connected_peer_infos(NodeConn.WORLD_NODE)
1483
+ world_private_peer_id = world_private_peer_id[0]['id'] if len(world_private_peer_id) > 0 else None
1484
+
1485
+ # This is only computed here
1486
+ public_agents_peer_infos = self.conn.get_all_connected_peer_infos(NodeConn.PUBLIC)
1487
+
1488
+ # Updating node profile with the summary of currently connected peers
1489
+ dynamic_profile = self.profile.get_dynamic_profile()
1490
+ dynamic_profile['connections']['public_agents'] = public_agents_peer_infos
1491
+ dynamic_profile['connections']['world_agents'] = world_agents_peer_infos
1492
+ dynamic_profile['connections']['world_masters'] = world_masters_peer_infos
1493
+ dynamic_profile['connections']['world_peer_id'] = world_private_peer_id
1494
+ self.profile.mark_change_in_connections()
1495
+
1496
+ async def __handle_network_messages(self, interact_mode_opts=None):
1497
+ """Handles and processes all incoming network messages (async).
1498
+
1499
+ Args:
1500
+ interact_mode_opts: A dictionary of options for interactive mode.
1501
+ """
1502
+ # Fetching all messages,
1503
+ public_messages = await self.conn.get_messages(p2p_name=NodeConn.P2P_PUBLIC)
1504
+ world_messages = await self.conn.get_messages(p2p_name=NodeConn.P2P_WORLD)
1505
+ interact_mode = interact_mode_opts is not None
1506
+
1507
+ self.out("Got " + str(len(public_messages)) + " messages from the public net")
1508
+ self.out("Got " + str(len(world_messages)) + " messages from the world/private net")
1509
+
1510
+ # Sorting messages
1511
+ public_messages = self.__sort_messages_by_priority(public_messages)
1512
+ world_messages = self.__sort_messages_by_priority(world_messages)
1513
+
1514
+ # Process all messages
1515
+ all_messages = public_messages + world_messages
1516
+ if len(all_messages) > 0:
1517
+ self.out("Processing all messages...")
1518
+ is_private_message = False
1519
+
1520
+ for i, msg in enumerate(all_messages):
1521
+ if i < len(public_messages):
1522
+ self.out("Processing public message " + str(i + 1) + "/"
1523
+ + str(len(public_messages)) + ": " + str(msg))
1524
+ else:
1525
+ self.out("Processing world/private message " + str(i - len(public_messages) + 1)
1526
+ + "/" + str(len(world_messages)) + ": " + str(msg))
1527
+ is_private_message = True
1528
+
1529
+ # Checking
1530
+ if not isinstance(msg, Msg):
1531
+ self.err("Expected message of type Msg, got {} (skipping)".format(type(msg)))
1532
+ continue
1533
+
1534
+ # Unpacking piggyback
1535
+ sender_node_id, sender_inspector_mode_on = (msg.piggyback[0:-1],
1536
+ msg.piggyback[-1] == "1")
1537
+
1538
+ # Is message from inspector?
1539
+ sender_is_inspector = (sender_node_id == self.profile.get_static_profile()['inspector_node_id'] and
1540
+ sender_inspector_mode_on)
1541
+
1542
+ # (A) received a profile
1543
+ if msg.content_type == Msg.PROFILE:
1544
+ self.out("Received a profile...")
1545
+
1546
+ # Checking the received profile
1547
+ # (recall that a profile sent through the world connection to the world node will be considered
1548
+ # not acceptable)
1549
+ profile = NodeProfile.from_dict(msg.content)
1550
+ is_an_already_known_agent = msg.sender in self.hosted.all_agents
1551
+
1552
+ if is_an_already_known_agent:
1553
+ self.out("Editing information of an already added agent " + msg.sender)
1554
+
1555
+ if not (await self.hosted.add_agent(peer_id=msg.sender, profile=profile)):
1556
+ await self.__purge(msg.sender)
1557
+ else:
1558
+ is_expected_and_acceptable_profile = await self.__interview_check_profile(peer_id=msg.sender,
1559
+ node_id=sender_node_id,
1560
+ profile=profile)
1561
+
1562
+ if not is_expected_and_acceptable_profile:
1563
+ self.err("Unexpected or unacceptable profile, removing (disconnecting) " + msg.sender)
1564
+ await self.__purge(msg.sender)
1565
+ else:
1566
+
1567
+ # If the node hosts a world and gets an expected and acceptable profile from the public network,
1568
+ # assigns a role and sends the world profile (which includes private peer ID) and role to the
1569
+ # requester
1570
+ if (self.node_type is Node.WORLD and self.conn.is_public(peer_id=msg.sender) and
1571
+ not sender_is_inspector):
1572
+ self.out("Sending world approval message, profile, and assigned role to " + msg.sender +
1573
+ " (and switching peer ID in the interview queue)...")
1574
+ is_world_master = (self.world_masters_node_ids is not None and
1575
+ sender_node_id in self.world_masters_node_ids)
1576
+
1577
+ # Assigning a role
1578
+ role_str = self.world.assign_role(profile=profile, is_world_master=is_world_master)
1579
+ if role_str is None:
1580
+ self.err("Unable to determine what role to assign, removing (disconnecting) "
1581
+ + msg.sender)
1582
+ await self.__purge(msg.sender)
1583
+ else:
1584
+ role = self.world.ROLE_STR_TO_BITS[role_str] # The role is a bit-wise-interpretable int
1585
+ role = role | (Agent.ROLE_WORLD_MASTER if is_world_master else Agent.ROLE_WORLD_AGENT)
1586
+
1587
+ # Clearing temporary options (if any)
1588
+ dynamic_profile = profile.get_dynamic_profile()
1589
+ keys_to_delete = [key for key in dynamic_profile if key.startswith('tmp_')]
1590
+ for key in keys_to_delete:
1591
+ del dynamic_profile[key]
1592
+
1593
+ is_human = profile.get_static_profile()["node_type"] == self.hosted.HUMAN
1594
+ if not (await self.conn.send(msg.sender, channel_trail=None,
1595
+ content={
1596
+ 'world_profile': self.profile.get_all_profile(),
1597
+ 'rendezvous_tag': self.clock.get_cycle(),
1598
+ 'your_role': role,
1599
+ 'agent_actions': self.world.agent_actions,
1600
+ 'agent_stats_code': self.world.agent_stats_code,
1601
+ # 'initial_stats': self.world.stats.get_view()
1602
+ # if is_human else None,
1603
+ 'initial_stats': self.world.stats.plot()
1604
+ if is_human else None,
1605
+ },
1606
+ content_type=Msg.WORLD_APPROVAL)):
1607
+ self.err("Failed to send world approval, removing (disconnecting) " + msg.sender)
1608
+ await self.__purge(msg.sender)
1609
+ else:
1610
+ # update role also in the profile held by the world
1611
+ dynamic_profile['connections']['role'] = self.world.ROLE_BITS_TO_STR[role]
1612
+ private_peer_id = profile.get_dynamic_profile()['private_peer_id']
1613
+ private_addr = profile.get_dynamic_profile()['private_peer_addresses']
1614
+ if is_world_master:
1615
+ role = role | Agent.ROLE_WORLD_MASTER
1616
+ self.conn.add_to_world_masters_list(private_peer_id, private_addr, role)
1617
+ else:
1618
+ role = role | Agent.ROLE_WORLD_AGENT
1619
+ self.conn.add_to_world_agents_list(private_peer_id, private_addr, role)
1620
+
1621
+ # Removing from the queue of public interviews
1622
+ # and adding to the private ones (refreshing timer)
1623
+ del self.agents_to_interview[msg.sender] # Removing from public queue
1624
+ self.agents_to_interview[private_peer_id] = [self.clock.get_time(), profile] # Add
1625
+
1626
+ # If the node is an agent, it is time to tell the agent object that a new agent is now known,
1627
+ # and send our profile to the agent that asked for out contact
1628
+ elif self.node_type is Node.AGENT or sender_is_inspector:
1629
+ self.out("Sending agent approval message and profile...")
1630
+
1631
+ if not (await self.conn.send(msg.sender, channel_trail=None,
1632
+ content={
1633
+ 'my_profile': self.profile.get_all_profile()
1634
+ },
1635
+ content_type=Msg.AGENT_APPROVAL)):
1636
+ self.err("Failed to send agent approval, removing (disconnecting) " + msg.sender)
1637
+ await self.__purge(msg.sender)
1638
+ else:
1639
+ self.out("Adding known agent and removing it from the interview queue " + msg.sender)
1640
+ if not (await self.hosted.add_agent(peer_id=msg.sender,
1641
+ profile=profile)): # keep "hosted" here
1642
+ await self.__purge(msg.sender)
1643
+ else:
1644
+
1645
+ # Removing from the queues
1646
+ del self.agents_to_interview[msg.sender] # Removing from queue
1647
+
1648
+ # Enabling interactive mode, if public
1649
+ if (interact_mode and 'lone_wolf_peer_id' in interact_mode_opts and
1650
+ interact_mode_opts['lone_wolf_peer_id'] is None):
1651
+ interact_mode_opts['lone_wolf_peer_id'] = msg.sender
1652
+ interact_mode_opts['ready_to_interact'] = True
1653
+
1654
+ # (B) received a world-join-approval
1655
+ elif msg.content_type == Msg.WORLD_APPROVAL:
1656
+ self.out("Received a world-join-approval message...")
1657
+
1658
+ # Checking if it is the world we asked for
1659
+ # moreover, it must be on the public network, and this must not be a world-node (of course)
1660
+ # and you must not be already in another world
1661
+ if (not self.conn.is_public(peer_id=msg.sender) or self.node_type is Node.WORLD
1662
+ or msg.sender not in self.agents_expected_to_send_ack or
1663
+ self.profile.get_dynamic_profile()['connections']['world_peer_id'] is not None):
1664
+ self.err("Unexpected world approval, removing (disconnecting) " + msg.sender)
1665
+ await self.__purge(msg.sender)
1666
+ else:
1667
+ if msg.sender != self.joining_world_info["world_public_peer_id"]:
1668
+ self.err(f"Unexpected world approval: asked to join "
1669
+ f"{self.joining_world_info['world_public_peer_id']} got approval from {msg.sender} "
1670
+ f"(disconnecting)")
1671
+ await self.__purge(msg.sender)
1672
+ else:
1673
+
1674
+ # Getting world profile (includes private addresses) and connecting to the world (privately)
1675
+ await self.__join_world(profile=NodeProfile.from_dict(msg.content['world_profile']),
1676
+ role=msg.content['your_role'],
1677
+ agent_actions=msg.content['agent_actions'],
1678
+ agent_stats_code=msg.content.get('agent_stats_code', None),
1679
+ rendezvous_tag=msg.content['rendezvous_tag'],
1680
+ initial_stats=msg.content['initial_stats'])
1681
+
1682
+ # Enabling interactive mode, if public
1683
+ if interact_mode and 'world_peer_id' in interact_mode_opts:
1684
+ interact_mode_opts['ready_to_interact'] = True
1685
+
1686
+ # (C) received an agent-connect-approval
1687
+ elif msg.content_type == Msg.AGENT_APPROVAL:
1688
+ self.out("Received an agent-connect-approval message...")
1689
+
1690
+ # Checking if it is the agent we asked for
1691
+ if msg.sender not in self.agents_expected_to_send_ack:
1692
+ self.err("Unexpected agent-connect approval, removing (disconnecting) " + msg.sender)
1693
+ await self.__purge(msg.sender)
1694
+ else:
1695
+
1696
+ # Adding the agent
1697
+ await self.__join_agent(profile=NodeProfile.from_dict(msg.content['my_profile']),
1698
+ peer_id=msg.sender)
1699
+
1700
+ # Enabling interactive mode, if public
1701
+ if interact_mode and 'lone_wolf_peer_id' in interact_mode_opts:
1702
+ interact_mode_opts['ready_to_interact'] = True
1703
+
1704
+ # (D) requested for a profile
1705
+ elif msg.content_type == Msg.PROFILE_REQUEST:
1706
+ self.out("Received a profile request...")
1707
+
1708
+ # If this is a world-node, it expects profile requests only on the public network
1709
+ # if this is not a world or not, we only send profile to agents who are involved in the handshake
1710
+ if ((self.node_type is Node.WORLD and not self.conn.is_public(peer_id=msg.sender)) or
1711
+ (msg.sender not in self.agents_expected_to_send_ack)):
1712
+ self.err("Unexpected profile request, removing (disconnecting) " + msg.sender)
1713
+ await self.__purge(msg.sender)
1714
+ else:
1715
+
1716
+ # If a preference was defined, we temporarily add it to the profile
1717
+ if (self.joining_world_info is not None and
1718
+ msg.sender == self.joining_world_info["world_public_peer_id"] and
1719
+ self.joining_world_info["options"] is not None and
1720
+ len(self.joining_world_info["options"]) > 0):
1721
+ my_profile = copy.deepcopy(self.profile)
1722
+ for k, v in self.joining_world_info["options"].items():
1723
+ my_profile.get_dynamic_profile()['tmp_' + str(k)] = v
1724
+ my_profile = my_profile.get_all_profile()
1725
+ else:
1726
+ my_profile = self.profile.get_all_profile()
1727
+
1728
+ # Sending the profile
1729
+ self.out("Sending profile")
1730
+ if not (await self.conn.send(msg.sender, channel_trail=None,
1731
+ content=my_profile,
1732
+ content_type=Msg.PROFILE)):
1733
+ self.err("Failed to send profile, removing (disconnecting) " + msg.sender)
1734
+ await self.__purge(msg.sender)
1735
+
1736
+ # (E) the world node received an ADDRESS_UPDATE from an agent
1737
+ elif msg.content_type == Msg.ADDRESS_UPDATE:
1738
+ self.out("Received an address update from " + msg.sender)
1739
+
1740
+ if self.node_type is Node.WORLD and msg.sender in self.world.all_agents:
1741
+ all_addresses = msg.content.get('addresses')
1742
+ if all_addresses and isinstance(all_addresses, list):
1743
+
1744
+ # Update the address both in the connection and in the profile
1745
+ self.conn.set_addresses_in_peer_info(msg.sender, all_addresses)
1746
+ self.world.set_addresses_in_profile(msg.sender, all_addresses)
1747
+ self.out(f"Waiting rendezvous publish after address update from {msg.sender}")
1748
+
1749
+ # (F) got stream data
1750
+ elif msg.content_type == Msg.STREAM_SAMPLE:
1751
+ self.out("Received a stream sample...")
1752
+
1753
+ if self.node_type is Node.AGENT: # Handling the received samples
1754
+ self.agent.get_stream_sample(net_hash=msg.channel, sample_dict=msg.content)
1755
+
1756
+ # Printing messages to screen, if needed (useful when chatting with lone wolves)
1757
+ if interact_mode and "stdout" in interact_mode_opts:
1758
+ net_hash = DataProps.normalize_net_hash(msg.channel)
1759
+ if net_hash in self.agent.known_streams:
1760
+ stream_dict = self.agent.known_streams[net_hash]
1761
+ peer_id = DataProps.peer_id_from_net_hash(net_hash)
1762
+ group = DataProps.name_or_group_from_net_hash(net_hash)
1763
+ owner_account = self.agent.all_agents[peer_id].get_static_profile()['email']
1764
+ agent_name = self.agent.all_agents[peer_id].get_static_profile()['node_name']
1765
+ sys.stdout = interact_mode_opts["stdout"][0] # Output on
1766
+ for name, stream_obj in stream_dict.items():
1767
+ data = stream_obj.get(requested_by="print")
1768
+ if data is None:
1769
+ continue
1770
+ if stream_obj.props.is_text():
1771
+ msg = data # Getting message
1772
+ msg = "\n |".join([line[i:i + 120] for line in msg.splitlines()
1773
+ for i in range(0, len(line), 120)])
1774
+ print(f"\n💬 [{owner_account}/{agent_name}.{group}.{name}]\n |{msg}") # Printing
1775
+ elif stream_obj.props.is_img():
1776
+ img = data # Getting image
1777
+ filename = f"{net_hash.replace(':', '_')}.{name}.png"
1778
+ img.save(filename)
1779
+ print(f"\n🖼️ [{owner_account}/{agent_name}.{group}.{name}]\n "
1780
+ f"|Saved image to {filename})")
1781
+ else:
1782
+ msg = stream_obj.props.to_text(data)
1783
+ msg = "\n |".join([line[i:i + 120] for line in msg.splitlines()
1784
+ for i in range(0, len(line), 120)])
1785
+ print(f"\n🗂️ [{owner_account}/{agent_name}.{group}.{name}]\n "
1786
+ f"|Got a sample of type {stream_obj.props.data_type}, "
1787
+ f"tag {stream_obj.get_tag()}\n |{msg}")
1788
+ sys.stdout = interact_mode_opts["stdout"][1] # Output off
1789
+
1790
+ elif self.node_type is Node.WORLD:
1791
+ self.err("Unexpected stream samples received by this world node, sent by: " + msg.sender)
1792
+ await self.__purge(msg.sender)
1793
+
1794
+ # (G) got action request
1795
+ elif msg.content_type == Msg.ACTION_REQUEST:
1796
+ self.out("Received an action request...")
1797
+
1798
+ if self.node_type is Node.AGENT:
1799
+ if msg.sender not in self.agent.all_agents:
1800
+ self.err("Unexpected action request received by a unknown node: " + msg.sender)
1801
+ else:
1802
+ behav = self.agent.behav_lone_wolf \
1803
+ if msg.sender in self.agent.public_agents else self.agent.behav
1804
+ if not behav.request_action(action_name=msg.content['action_name'],
1805
+ args=msg.content['args'],
1806
+ signature=msg.sender,
1807
+ timestamp=self.clock.get_time(),
1808
+ uuid=msg.content['uuid'],
1809
+ from_state=msg.content.get('from_state', None),
1810
+ to_state=msg.content.get('to_state', None)):
1811
+ self.out("Cannot enqueue the request, incompatible action")
1812
+
1813
+ elif self.node_type is Node.WORLD:
1814
+ self.err("Unexpected action request received by this world node, sent by: " + msg.sender)
1815
+ await self.__purge(msg.sender)
1816
+
1817
+ # (H) got role suggestion
1818
+ elif msg.content_type == Msg.ROLE_SUGGESTION:
1819
+ self.out("Received a role suggestion/new role...")
1820
+
1821
+ if self.node_type is Node.AGENT:
1822
+ if msg.sender == self.conn.get_world_peer_id():
1823
+ new_role_indication = msg.content
1824
+ if new_role_indication['peer_id'] == self.get_world_peer_id():
1825
+ self.agent.accept_new_role(new_role_indication['role'])
1826
+
1827
+ self.agent.behav.update_wildcard("<agent>", f"{self.get_world_peer_id()}")
1828
+ self.agent.behav.update_wildcard("<world>", f"{msg.sender}")
1829
+
1830
+ elif self.node_type is Node.WORLD:
1831
+ if msg.sender in self.world.world_masters:
1832
+ for role_suggestion in msg.content:
1833
+ await self.world.set_role(peer_id=role_suggestion['peer_id'], role=role_suggestion['role'])
1834
+
1835
+ # (I) got request to alter the HSM
1836
+ elif msg.content_type == Msg.HSM:
1837
+ self.out("Received a request to alter the HSM...")
1838
+
1839
+ if self.node_type is Node.AGENT:
1840
+ if msg.sender in self.agent.world_masters: # This must be coherent with what we do in set_role
1841
+ ret = getattr(self.agent.behav, msg.content['method'])(*msg.content['args'])
1842
+ if not ret:
1843
+ self.err(f"Cannot run HSM action named {msg.content['method']} with args "
1844
+ f"{msg.content['args']}")
1845
+ else:
1846
+ self.err("Only world-master can alter HSMs of other agents: " + msg.sender) # No need to purge
1847
+
1848
+ elif self.node_type is Node.WORLD:
1849
+ self.err("Unexpected request to alter the HSM received by this world node, sent by: " + msg.sender)
1850
+ await self.__purge(msg.sender)
1851
+
1852
+ # (J) misc
1853
+ elif msg.content_type == Msg.MISC:
1854
+ self.out("Received a misc message...")
1855
+ self.out(msg.content)
1856
+ if (msg.content is not None and isinstance(msg.content, dict) and
1857
+ 'ping' in msg.content and msg.content['ping'] == 'pong'):
1858
+
1859
+ public_ping_pong = msg.content.get('public', None)
1860
+ if public_ping_pong is not None and public_ping_pong and is_private_message:
1861
+ self.err("Invalid format of ping-pong package")
1862
+ await self.__purge(msg.sender)
1863
+ else:
1864
+ if msg.sender not in self.agents_that_provided_ping_pong:
1865
+
1866
+ # First, expected, ping-pong
1867
+ self.agents_that_provided_ping_pong.add(msg.sender)
1868
+ else:
1869
+
1870
+ # Not expected ping-pong from an already fully connected (i.e., handshake done) agent
1871
+ handshake_already_completed = \
1872
+ ((msg.sender in self.hosted.public_agents) if not is_private_message else
1873
+ (msg.sender in self.hosted.world_agents or msg.sender in self.hosted.world_masters))
1874
+
1875
+ if handshake_already_completed:
1876
+ await self.hosted.remove_agent(msg.sender)
1877
+ self.reconnected.add(msg.sender)
1878
+ self.out(f"Reconnection detected for peer {msg.sender}, will start handshake again")
1879
+
1880
+ # (K) got a request to re-download the CV from the root server
1881
+ elif msg.content_type == Msg.GET_CV_FROM_ROOT:
1882
+ self.out("Received a notification to re-download the CV...")
1883
+
1884
+ # Downloading CV
1885
+ self.profile.update_cv(self.get_cv())
1886
+
1887
+ # Re-downloading token (it will include the new CV hash)
1888
+ self.get_node_token(peer_ids=[self.get_public_peer_id(), self.get_world_peer_id()])
1889
+
1890
+ # (L) got one or more badge suggestions
1891
+ elif msg.content_type == Msg.BADGE_SUGGESTIONS:
1892
+ self.out("Received badge suggestions...")
1893
+
1894
+ if self.node_type is Node.WORLD:
1895
+ for badge_dict in msg.content:
1896
+
1897
+ # Right now, we accept all the suggestions
1898
+ self.world.add_badge(**badge_dict) # Adding to the list of badges
1899
+ elif self.node_type is Node.AGENT:
1900
+ self.err("Receiving badge suggestions is not expected for an agent node")
1901
+
1902
+ # (M) got a special connection/presence message for an inspector
1903
+ elif msg.content_type == Msg.INSPECT_ON:
1904
+ self.out("Received an inspector-activation message...")
1905
+
1906
+ if sender_is_inspector:
1907
+ self.inspector_activated = True
1908
+ self.inspector_peer_id = msg.sender
1909
+ print("Inspector activated")
1910
+ else:
1911
+ self.err("Inspector-activation message was not sent by the expected inspector node ID")
1912
+ await self.__purge(msg.sender)
1913
+
1914
+ # (N) got a command from an inspector
1915
+ elif msg.content_type == Msg.INSPECT_CMD:
1916
+ self.out("Received a command from the inspector...")
1917
+
1918
+ if sender_is_inspector and self.inspector_activated:
1919
+ await self.__handle_inspector_command(msg.content['cmd'], msg.content['arg'])
1920
+ else:
1921
+ self.err("Inspector command was not sent by the expected inspector node ID "
1922
+ "or the inspector was not yet activated (Msg.INSPECT_ON not received yet)")
1923
+ await self.__purge(msg.sender)
1924
+
1925
+ # (O) world got stats update from an agent
1926
+ elif msg.content_type == Msg.STATS_UPDATE:
1927
+ self.out(f"[NODE] Received a stats update from " + msg.sender)
1928
+ if self.node_type is Node.WORLD:
1929
+ if msg.sender in self.world.all_agents:
1930
+ # This calls the world.add_stats_from_peer method
1931
+ self.world.add_peer_stats(msg.content)
1932
+ else:
1933
+ self.err(f"Received stats update from {msg.sender}, "
1934
+ f"but they are not a known agent in this world.")
1935
+ elif self.node_type is Node.AGENT:
1936
+ self.err("Receiving stats updates is not expected for an agent node.")
1937
+
1938
+ # (P) got a request for stats from an agent
1939
+ elif msg.content_type == Msg.STATS_REQUEST:
1940
+ self.out(f"[NODE] Received a stats request from " + msg.sender)
1941
+ if self.node_type is Node.WORLD:
1942
+ # 1. Extract filters from content
1943
+ filters = msg.content or {}
1944
+ # default values are added to query without any filter
1945
+ req_stats = filters.get('stat_names', [])
1946
+ req_peers = filters.get('peer_ids', [])
1947
+ # time_range = filters.get('time_range', None)
1948
+ time_range = filters.get('time_range', 0)
1949
+ value_range = filters.get('value_range', None) # The numeric filter
1950
+ limit = filters.get('limit', None)
1951
+
1952
+ # # This is a fine-grain request, so we query the db
1953
+ # response_payload = self.world.stats.query_history(
1954
+ # stat_names=req_stats,
1955
+ # peer_ids=req_peers,
1956
+ # time_range=time_range,
1957
+ # value_range=value_range,
1958
+ # limit=limit)
1959
+ response_payload = self.world.stats.plot(since_timestamp=time_range)
1960
+
1961
+ # Send back as STATS_RESPONSE
1962
+ await self.conn.send(msg.sender, channel_trail=None,
1963
+ content_type=Msg.STATS_RESPONSE,
1964
+ content=response_payload)
1965
+ elif self.node_type is Node.AGENT:
1966
+ self.err("Receiving stats request is not expected for an agent node.")
1967
+
1968
+ # (Q) agent got stats response from a world
1969
+ elif msg.content_type == Msg.STATS_RESPONSE:
1970
+ self.out(f"[NODE] Received a stats response from " + msg.sender)
1971
+ if self.node_type is Node.AGENT:
1972
+ if msg.sender == self.conn.get_world_peer_id():
1973
+ # self.agent.update_stats_view(msg.content, self.agent.overwrite_stats)
1974
+ pass
1975
+ else:
1976
+ self.err(f"Received stats response from {msg.sender}, but it is not the world.")
1977
+ elif self.node_type is Node.AGENT:
1978
+ self.err("Receiving stats response is not expected for a world node.")
1979
+
1980
+ await self.__interview_clean()
1981
+ await self.__handle_connected_without_ack()
1982
+
1983
+ async def __join_world(self, profile: NodeProfile, role: int,
1984
+ agent_actions: str | None, agent_stats_code: str | None,
1985
+ rendezvous_tag: int, initial_stats: Dict[str, Any] | None):
1986
+ """Performs the actual operation of joining a world after receiving confirmation (async).
1987
+
1988
+ Args:
1989
+ profile: The profile of the world to join.
1990
+ role: The role assigned to the agent in the world (int).
1991
+ agent_actions: A string of code defining the agent's actions.
1992
+ agent_stats_code: A string of code defining the statistics for this world.
1993
+ rendezvous_tag: The rendezvous tag from the world's profile.
1994
+ initial_stats: When joining a world we eventually receive the recent history.
1995
+
1996
+ Returns:
1997
+ True if the join operation is successful, otherwise False.
1998
+ """
1999
+ addresses = profile.get_dynamic_profile()['private_peer_addresses']
2000
+ world_public_peer_id = profile.get_dynamic_profile()['peer_id']
2001
+ self.out(f"Actually joining world, role will be {role}")
2002
+
2003
+ # Connecting to the world (private)
2004
+ # notice that we also communicate the world node private peer ID to the connection manager,
2005
+ # to avoid filtering it out when updating pools
2006
+ peer_id = await self.ask_to_get_in_touch(addresses=addresses, public=False,
2007
+ before_updating_pools_fcn=self.conn.set_world)
2008
+
2009
+ if peer_id is not None:
2010
+
2011
+ # Relay reservation logic for non-public peers
2012
+ if not self.conn.p2p_world.is_public and self.conn.p2p_world.relay_is_enabled:
2013
+ self.out("Node is not publicly reachable. Enabling Static AutoRelay on the world's private network.")
2014
+ try:
2015
+ self.conn.p2p_world.start_static_relay(peer_id, addresses)
2016
+ self.out("Static AutoRelay enabled. Reservation and renewal will be handled automatically.")
2017
+ except Exception as e:
2018
+ self.err(f"An error occurred enabling Static AutoRelay: {e}.")
2019
+
2020
+ # Load custom stats class if provided
2021
+ stats_class = None
2022
+ if agent_stats_code is not None and len(agent_stats_code) > 0:
2023
+ # Checking code
2024
+ if not Node.__analyze_code(agent_stats_code):
2025
+ self.err("Invalid agent stats code (syntax errors or unsafe code) was provided by the world, "
2026
+ "blocking the join operation")
2027
+ return False
2028
+ try:
2029
+ stats_mod = types.ModuleType("dynamic_stats_module")
2030
+ exec(agent_stats_code, stats_mod.__dict__)
2031
+ if not hasattr(stats_mod, 'WStats'):
2032
+ self.err("World sent stats.py, but it lacks a 'WStats' class. Using default Stats.")
2033
+ else:
2034
+ stats_class = stats_mod.WStats
2035
+ self.out("Loaded custom WStats class from world.")
2036
+ except Exception as e:
2037
+ self.err(f"Failed to exec custom stats.py from world: {e}. Using default Stats.")
2038
+
2039
+ # Subscribing to the world rendezvous topic, from which we will get fresh information
2040
+ # about the world agents and masters
2041
+ self.out("Subscribing to the world-members topic...")
2042
+ if not (await self.conn.subscribe(peer_id, channel=f"{peer_id}::ps:rv")): # Special rendezvous (ps:rv)
2043
+ await self.leave(peer_id) # If subscribing fails, we quit everything (safer)
2044
+ return False
2045
+
2046
+ # Killing the public connection to the world node
2047
+ self.out("Disconnecting from the public world network (since we joined the private one)")
2048
+ await self.__purge(world_public_peer_id)
2049
+
2050
+ # Removing the private world peer id from the list of connected-but-not-managed peer
2051
+ del self.agents_expected_to_send_ack[peer_id]
2052
+
2053
+ # Subscribing to all the other world topics, from which we will get fresh information
2054
+ # about the streams
2055
+ self.out("Subscribing to the world-streams topics...")
2056
+ dynamic_profile = profile.get_dynamic_profile()
2057
+ list_of_props = []
2058
+ list_of_props += dynamic_profile['streams'] if dynamic_profile['streams'] is not None else []
2059
+ list_of_props += dynamic_profile['proc_outputs'] if dynamic_profile['proc_outputs'] is not None else []
2060
+
2061
+ if not (await self.agent.add_compatible_streams(peer_id, list_of_props, buffered=False, public=False)):
2062
+ await self.leave(peer_id)
2063
+ return False
2064
+
2065
+ # Setting actions
2066
+ if agent_actions is not None and len(agent_actions) > 0:
2067
+
2068
+ # Checking code
2069
+ if not Node.__analyze_code(agent_actions):
2070
+ self.err("Invalid agent actions code (syntax errors or unsafe code) was provided by the world, "
2071
+ "blocking the join operation")
2072
+ return False
2073
+
2074
+ # Creating a new agent with the received actions
2075
+ mod = types.ModuleType("dynamic_module")
2076
+ exec(agent_actions, mod.__dict__)
2077
+ sys.modules["dynamic_module"] = mod
2078
+ new_agent = mod.WAgent(proc=None)
2079
+
2080
+ # Cloning attributes of the existing agent
2081
+ for key, value in self.agent.__dict__.items():
2082
+ if hasattr(new_agent, key): # This will skip ROLE_BITS_TO_STR, CUSTOM_ROLES, etc...
2083
+ if key == 'stats' and stats_class is not None:
2084
+ new_agent.stats = stats_class(is_world=False)
2085
+ else:
2086
+ setattr(new_agent, key, value)
2087
+
2088
+ # Telling the FSM that actions are related to this new agent
2089
+ new_agent.behav.set_actionable(new_agent)
2090
+ new_agent.behav_lone_wolf.set_actionable(new_agent)
2091
+
2092
+ # Inheriting the pre-defined policy filter (if any)
2093
+ new_agent.set_policy_filter(self.agent.policy_filter, public=False)
2094
+ new_agent.set_policy_filter(self.agent.policy_filter_lone_wolf, public=True)
2095
+
2096
+ # Setting up roles
2097
+ roles = profile.get_dynamic_profile()['world_roles_fsm'].keys()
2098
+ new_agent.CUSTOM_ROLES = roles
2099
+ new_agent.augment_roles()
2100
+
2101
+ # Updating node-level references
2102
+ old_agent = self.agent
2103
+ self.agent = new_agent
2104
+ self.hosted = new_agent
2105
+ else:
2106
+ old_agent = self.agent
2107
+ if stats_class is not None:
2108
+ self.out("Replacing default stats with custom WStats from world.")
2109
+ old_agent.stats = stats_class(is_world=False)
2110
+
2111
+ # inject the stats history
2112
+ if initial_stats is not None:
2113
+ self.agent.update_stats_view(initial_stats, overwrite=True)
2114
+
2115
+ # Saving the world profile
2116
+ self.agent.world_profile = profile
2117
+
2118
+ # Setting the assigned role and default behavior (do it after having recreated the new agent object)
2119
+ self.agent.accept_new_role(role) # Do this after having done 'self.agent.world_profile = profile'
2120
+
2121
+ # Updating wildcards
2122
+ self.agent.behav.update_wildcard("<agent>", f"{self.get_world_peer_id()}")
2123
+ self.agent.behav.update_wildcard("<world>", f"{peer_id}")
2124
+ self.agent.behav.add_wildcards(old_agent.behav_wildcards)
2125
+
2126
+ # Telling the connection manager the info needed to discriminate peers (getting them from the world profile)
2127
+ # notice that the world node private ID was already told to the connection manager (see a few lines above)
2128
+ self.out(f"Rendezvous tag received with profile: {rendezvous_tag} "
2129
+ f"(in conn pool: {self.conn.rendezvous_tag})")
2130
+ if self.conn.rendezvous_tag < rendezvous_tag:
2131
+ self.conn.rendezvous_tag = rendezvous_tag
2132
+ num_world_masters = len(dynamic_profile['world_summary']['world_masters']) \
2133
+ if dynamic_profile['world_summary']['world_masters'] is not None else 'none'
2134
+ num_world_agents = len(dynamic_profile['world_summary']['world_agents']) \
2135
+ if dynamic_profile['world_summary']['world_agents'] is not None else 'none'
2136
+ self.out(f"Rendezvous from profile (tag: {rendezvous_tag}), world masters: {num_world_masters}")
2137
+ self.out(f"Rendezvous from profile (tag: {rendezvous_tag}), world agents: {num_world_agents}")
2138
+ self.conn.set_world_masters_list(dynamic_profile['world_summary']['world_masters'])
2139
+ self.conn.set_world_agents_list(dynamic_profile['world_summary']['world_agents'])
2140
+
2141
+ # Updating our profile to set the world we are in
2142
+ self.profile.get_dynamic_profile()['connections']['world_peer_id'] = peer_id
2143
+ self.profile.mark_change_in_connections()
2144
+
2145
+ print("Handshake completed, world joined!")
2146
+ return True
2147
+ else:
2148
+ return False
2149
+
2150
+ async def __join_agent(self, profile: NodeProfile, peer_id: str):
2151
+ """Adds a new known agent after receiving an approval message (async).
2152
+
2153
+ Args:
2154
+ profile: The profile of the agent to join.
2155
+ peer_id: The peer ID of the agent.
2156
+
2157
+ Returns:
2158
+ True if the agent is successfully added, otherwise False.
2159
+ """
2160
+ self.out("Adding known agent " + peer_id)
2161
+ if not (await self.agent.add_agent(peer_id=peer_id, profile=profile)):
2162
+ await self.__purge(peer_id)
2163
+ return False
2164
+
2165
+ if self.conn.is_public(peer_id):
2166
+ self.agent.behav_lone_wolf.update_wildcard("<partner>", peer_id)
2167
+
2168
+ del self.agents_expected_to_send_ack[peer_id]
2169
+ return True
2170
+
2171
+ async def __interview_enqueue(self, peer_id: str):
2172
+ """Adds a newly connected peer to the queue of agents to be interviewed (async).
2173
+
2174
+ Args:
2175
+ peer_id: The peer ID of the agent to interview.
2176
+
2177
+ Returns:
2178
+ True if the agent is successfully enqueued, otherwise False.
2179
+ """
2180
+
2181
+ # If the peer_id is not in the same world were we are, we early stop the interview process
2182
+ if (not self.conn.is_public(peer_id) and peer_id not in self.conn.world_agents_list and
2183
+ peer_id not in self.conn.world_masters_list and peer_id != self.conn.world_node_peer_id):
2184
+ self.out(f"Interview failed: "
2185
+ f"peer ID {peer_id} is not in the world agents/masters list, and it is not the world node")
2186
+ return False
2187
+
2188
+ # Ask for the profile
2189
+ self.out("Sending profile request...")
2190
+ ret = await self.conn.send(peer_id, channel_trail=None,
2191
+ content_type=Msg.PROFILE_REQUEST, content=None)
2192
+ if not ret:
2193
+ self.out(f"Interview failed: "
2194
+ f"unable to send a profile request to peer ID {peer_id}")
2195
+ return False
2196
+ self.out(f"Interview list expanded: profile request sent to peer ID {peer_id}")
2197
+
2198
+ # Put the agent in the list of agents to interview (re-adding it if we get multiple requests from the same guy)
2199
+ self.agents_to_interview[peer_id] = [self.clock.get_time(), None] # Peer ID -> [time, profile]; no profile yet
2200
+ return True
2201
+
2202
+ async def __interview_check_profile(self, peer_id: str, node_id: str, profile: NodeProfile):
2203
+ """Checks if a received profile is acceptable and valid (async).
2204
+
2205
+ Args:
2206
+ peer_id: The peer ID of the node that sent the profile.
2207
+ node_id: The node ID of the node that sent the profile.
2208
+ profile: The NodeProfile object to be checked.
2209
+
2210
+ Returns:
2211
+ True if the profile is acceptable, otherwise False.
2212
+ """
2213
+
2214
+ # If the node ID was not on the list of allowed ones (if the list exists), then stop it
2215
+ # notice that we do not get the node ID from the profile, but from outside (it comes from the token, so safe)
2216
+ if ((self.allowed_node_ids is not None and node_id not in self.allowed_node_ids) or
2217
+ (peer_id not in self.agents_to_interview)):
2218
+ self.out(f"Profile of f{peer_id} not in the list of agents to interview or its node ID is not allowed")
2219
+ return False
2220
+ else:
2221
+
2222
+ # Getting the parts of profile needed
2223
+ eval_static_profile = profile.get_static_profile()
2224
+ eval_dynamic_profile = profile.get_dynamic_profile()
2225
+ my_dynamic_profile = self.profile.get_dynamic_profile()
2226
+
2227
+ # Checking if CV was altered
2228
+ cv_hash = await self.conn.get_cv_hash_from_last_token(peer_id)
2229
+ sanity_ok, pairs_of_hashes = profile.verify_cv_hash(cv_hash)
2230
+ if not sanity_ok:
2231
+ self.out(f"The CV in the profile of f{peer_id} failed the sanity check {pairs_of_hashes},"
2232
+ f" {profile.get_cv()}")
2233
+ return False
2234
+
2235
+ # Determining type of agent, checking the connection pools
2236
+ role = self.conn.get_role(peer_id)
2237
+
2238
+ if role & 1 == 0:
2239
+
2240
+ if self.node_type is Node.AGENT:
2241
+
2242
+ # Ensuring that the interviewed agent is out of every world
2243
+ # (if it were in the same world in which we are, it would connect in a private manner) and
2244
+ # possibly fulfilling the optional constraint of accepting only certified agent,
2245
+ # then asking the hosted entity for additional custom evaluation
2246
+ if (not self.only_certified_agents or 'certified' in eval_static_profile and
2247
+ eval_static_profile['certified'] is True):
2248
+ return self.hosted.evaluate_profile(role, profile)
2249
+ else:
2250
+ self.out(f"Peer f{peer_id} is not certified "
2251
+ f"and maybe I expect certified peers only")
2252
+ return False
2253
+
2254
+ elif self.node_type is Node.WORLD:
2255
+ if (eval_dynamic_profile['connections']['world_peer_id'] is not None and
2256
+ eval_dynamic_profile['connections']['world_peer_id'] != self.get_world_peer_id()):
2257
+ self.out(f"Peer f{peer_id} tried to connect to this world, but it is already part of another"
2258
+ f"world")
2259
+ return False
2260
+ else:
2261
+ return True
2262
+
2263
+ else:
2264
+
2265
+ if self.node_type is Node.AGENT:
2266
+
2267
+ # Ensuring that the interviewed agent is in the same world where we are and
2268
+ # possibly fulfilling the optional constraint of accepting only certified agent
2269
+ if (not self.only_certified_agents or 'certified' in eval_static_profile and
2270
+ eval_static_profile['certified'] is True):
2271
+ return self.hosted.evaluate_profile(role, profile)
2272
+ else:
2273
+ self.out(f"Peer f{peer_id} is not certified "
2274
+ f"and maybe I expect certified peers only")
2275
+ return False
2276
+
2277
+ elif self.node_type is Node.WORLD:
2278
+
2279
+ # If this node hosts a world, we do not expect to interview agents in the private world connection,
2280
+ # so something went wrong here, let's reject it
2281
+ self.out(f"Peer f{peer_id} sent a profile in the private network, unexpected")
2282
+ return False
2283
+
2284
+ async def __interview_clean(self):
2285
+ """Removes outdated or timed-out interview requests from the queue (async)."""
2286
+ cur_time = self.clock.get_time()
2287
+ agents_to_remove = []
2288
+ for peer_id, (profile_time, profile) in self.agents_to_interview.items():
2289
+
2290
+ # Checking timeout
2291
+ if (cur_time - profile_time) > self.interview_timeout:
2292
+ self.out("Removing (disconnecting) due to timeout in interview queue: " + peer_id)
2293
+ agents_to_remove.append(peer_id)
2294
+
2295
+ # Updating
2296
+ for peer_id in agents_to_remove:
2297
+ await self.__purge(peer_id) # This will also remove the peer from the queue of peers to interview
2298
+
2299
+ async def __handle_connected_without_ack(self):
2300
+ """Removes connected peers from the queue if they haven't sent an acknowledgment within
2301
+ the timeout period (async)."""
2302
+ cur_time = self.clock.get_time()
2303
+ agents_to_remove = []
2304
+ agents_to_retry = []
2305
+ for peer_id, connection_dict in self.agents_expected_to_send_ack.items():
2306
+
2307
+ # Checking timeout (to resend the request)
2308
+ if ((cur_time - connection_dict["ask_time"]) > self.connect_without_ack_retry_timeout and
2309
+ not connection_dict['retried']):
2310
+ self.out("Timeout in the connected-without-ack queue, I will try again: " + peer_id)
2311
+ agents_to_retry.append(peer_id)
2312
+ continue
2313
+
2314
+ # Checking timeout
2315
+ if (cur_time - connection_dict["ask_time"]) > self.connect_without_ack_total_timeout:
2316
+ self.out("Removing (disconnecting) due to timeout in the connected-without-ack queue: " + peer_id)
2317
+ agents_to_remove.append(peer_id)
2318
+
2319
+ # Updating (disconnected)
2320
+ for peer_id in agents_to_remove:
2321
+ await self.__purge(peer_id) # This will ALSO remove the peer from the connected-without-ack queue
2322
+
2323
+ # Updating (retry)
2324
+ for peer_id in agents_to_retry:
2325
+ connection_dict = self.agents_expected_to_send_ack[peer_id]
2326
+ connection_dict['retried'] = True
2327
+ self.out(f"Retrying to connect to {peer_id} with args {connection_dict['args_of_ask_to_get_in_touch']}")
2328
+ await self.ask_to_get_in_touch(**connection_dict["args_of_ask_to_get_in_touch"]) # Trying again
2329
+
2330
+ async def __purge(self, peer_id: str):
2331
+ """Removes a peer from all relevant connection lists and queues (async).
2332
+
2333
+ Args:
2334
+ peer_id: The peer ID of the node to purge.
2335
+ """
2336
+ await self.hosted.remove_agent(peer_id)
2337
+ await self.conn.remove(peer_id)
2338
+
2339
+ # Clearing also the contents of the list of interviews
2340
+ if peer_id in self.agents_to_interview:
2341
+ del self.agents_to_interview[peer_id]
2342
+
2343
+ # Clearing the temporary list of connected agents
2344
+ if peer_id in self.agents_expected_to_send_ack:
2345
+ del self.agents_expected_to_send_ack[peer_id]
2346
+
2347
+ # Clearing this set as well
2348
+ self.agents_that_provided_ping_pong.discard(peer_id)
2349
+
2350
+ @staticmethod
2351
+ def __sort_messages_by_priority(messages):
2352
+ """Sort messages by priority: world approval and agent approval first."""
2353
+
2354
+ _world_approval_messages = []
2355
+ _agent_approval_messages = []
2356
+ _action_messages = []
2357
+ _other_messages = []
2358
+ for _msg in messages:
2359
+ if _msg.content_type == Msg.WORLD_APPROVAL:
2360
+ _world_approval_messages.append(_msg)
2361
+ elif _msg.content_type == Msg.AGENT_APPROVAL:
2362
+ _agent_approval_messages.append(_msg)
2363
+ elif _msg.content_type == Msg.ACTION_REQUEST:
2364
+ _action_messages.append(_msg)
2365
+ else:
2366
+ _other_messages.append(_msg)
2367
+ return _world_approval_messages + _agent_approval_messages + _action_messages + _other_messages
2368
+
2369
+ def __root(self, api: str, payload: dict):
2370
+ """Sends a POST request to the root server's API endpoint.
2371
+
2372
+ Args:
2373
+ api: The API endpoint to send the request to.
2374
+ payload: The data to be sent in the request body.
2375
+
2376
+ Returns:
2377
+ The 'data' field from the server's JSON response.
2378
+ """
2379
+ response_fields = ["state", "flags", "data"]
2380
+
2381
+ try:
2382
+ api = self.root_endpoint + ("/" if self.root_endpoint[-1] != "/" and api[0] != "/" else "") + api
2383
+ payload["node_token"] = self.node_token # Adding token to let the server verify
2384
+ response = requests.post(api,
2385
+ json=payload,
2386
+ headers={"Content-Type": "application/json"})
2387
+
2388
+ if response.status_code == 200:
2389
+ ret = response.json()
2390
+ if response_fields is not None:
2391
+ for field in response_fields:
2392
+ if field not in ret:
2393
+ raise GenException(f"Missing key '{field}' in the response to {api}: {ret}")
2394
+ else:
2395
+ raise GenException(f"Request {api} failed with status code {response.status_code}")
2396
+ except Exception as e:
2397
+ self.err(f"An error occurred while making the POST request: {e}")
2398
+ raise GenException(f"An error occurred while making the POST request: {e}")
2399
+
2400
+ if ret['state']['code'] != "ok":
2401
+ raise GenException("[" + api + "] " + ret['state']['message'])
2402
+
2403
+ return ret['data']
2404
+
2405
+ @staticmethod
2406
+ def __analyze_code(file_in_memory):
2407
+ """Analyzes a string of Python code for dangerous or unsafe functions and modules.
2408
+
2409
+ Args:
2410
+ file_in_memory: The string of Python code to analyze.
2411
+
2412
+ Returns:
2413
+ True if the code is considered safe, otherwise False.
2414
+ """
2415
+ dangerous_functions = {"eval", "exec", "compile", "system", "__import__", "input"}
2416
+ dangerous_modules = {"subprocess"}
2417
+
2418
+ def is_suspicious(ast_node):
2419
+
2420
+ # Detect bare function calls like eval(...)
2421
+ if isinstance(ast_node, ast.Call):
2422
+ # case: eval(...) (ast.Name)
2423
+ if isinstance(ast_node.func, ast.Name):
2424
+ return ast_node.func.id in dangerous_functions
2425
+ # case: something.eval(...) (ast.Attribute)
2426
+ elif isinstance(ast_node.func, ast.Attribute):
2427
+ attr_name = ast_node.func.attr
2428
+ # 1) If attribute name is one of the dangerous_functions, only flag it
2429
+ # if the object is a suspicious module (os, subprocess, etc.)
2430
+ if attr_name in dangerous_functions:
2431
+ value = ast_node.func.value
2432
+ # example: os.system(...) => ast.Name(id='os')
2433
+ if isinstance(value, ast.Name):
2434
+ if value.id in dangerous_modules:
2435
+ return True
2436
+ # example: package.subpackage.func(...) => ast.Attribute
2437
+ # check top-level name if possible: walk down to the leftmost Name
2438
+ left = value
2439
+ while isinstance(left, ast.Attribute):
2440
+ left = left.value
2441
+ if isinstance(left, ast.Name) and left.id in dangerous_modules:
2442
+ return True
2443
+ # 2) Also catch explicit module imports used directly:
2444
+ # subprocess.run(...), os.system(...), etc.
2445
+ if isinstance(ast_node.func.value, ast.Name):
2446
+ if ast_node.func.value.id in dangerous_modules:
2447
+ # if the module is suspicious, any attribute call is risky
2448
+ return True
2449
+
2450
+ # Detect imports
2451
+ if isinstance(ast_node, (ast.Import, ast.ImportFrom)):
2452
+ for alias in ast_node.names:
2453
+ if alias.name.split('.')[0] in dangerous_modules:
2454
+ return True
2455
+
2456
+ return False
2457
+
2458
+ try:
2459
+ tree = ast.parse(file_in_memory)
2460
+ except SyntaxError:
2461
+ return False
2462
+
2463
+ for _ast_node in ast.walk(tree):
2464
+ if is_suspicious(_ast_node):
2465
+ return False
2466
+
2467
+ return True
2468
+
2469
+ async def __handle_inspector_command(self, cmd: str, arg):
2470
+ """Handles commands received from an inspector node (async).
2471
+
2472
+ Args:
2473
+ cmd: The command string.
2474
+ arg: The argument for the command.
2475
+ """
2476
+ self.out(f"Handling inspector message {cmd}, with arg {arg}")
2477
+
2478
+ if arg is not None and not isinstance(arg, str):
2479
+ self.err(f"Expecting a string argument from the inspector!")
2480
+ else:
2481
+ if cmd == "ask_to_join_world":
2482
+ print(f"Inspector asked to join world: {arg}")
2483
+ await self.ask_to_join_world(node_name=arg)
2484
+ elif cmd == "ask_to_get_in_touch":
2485
+ print(f"Inspector asked to get in touch with an agent: {arg}")
2486
+ await self.ask_to_get_in_touch(node_name=arg, public=True)
2487
+ elif cmd == "leave":
2488
+ print(f"Inspector asked to leave an agent: {arg}")
2489
+ await self.leave(arg)
2490
+ elif cmd == "leave_world":
2491
+ print(f"Inspector asked to leave the current world")
2492
+ await self.leave_world()
2493
+ elif cmd == "pause":
2494
+ print("Inspector asked to pause")
2495
+ self.__inspector_told_to_pause = True
2496
+ elif cmd == "play":
2497
+ print("Inspector asked to play")
2498
+ self.__inspector_told_to_pause = False
2499
+ elif cmd == "save":
2500
+ print("Inspector asked to save")
2501
+ self.hosted.save(arg)
2502
+ else:
2503
+ self.err("Unknown inspector command")
2504
+
2505
+ async def __send_to_inspector(self):
2506
+ """Sends status updates and data to the connected inspector node (async)."""
2507
+
2508
+ # Collecting console
2509
+ f = self._output_messages_last_pos - self._output_messages_count + 1 # Included
2510
+ t = self._output_messages_last_pos # Included
2511
+ ff = -1
2512
+ tt = -1
2513
+ if t >= 0 > f: # If there is something, and we incurred in the circular organization (t: valid; f: negative)
2514
+ ff = len(self._output_messages) + f # Included
2515
+ tt = len(self._output_messages) - 1 # Included
2516
+ f = 0
2517
+ elif t < 0: # If there are no messages at all (t: -1; f: 0 - due to the way we initialized class attributes)
2518
+ f = -1
2519
+ t = -1
2520
+ console = {'output_messages': self._output_messages[ff:tt+1] + self._output_messages[f:t+1]}
2521
+
2522
+ # Collecting the HSM
2523
+ if self.__inspector_cache['behav'] != self.hosted.behav:
2524
+ self.__inspector_cache['behav'] = self.hosted.behav
2525
+ behav = str(self.hosted.behav)
2526
+ else:
2527
+ behav = None
2528
+
2529
+ # Collecting status of the HSM
2530
+ if self.hosted.behav is not None:
2531
+ _behav = self.hosted.behav
2532
+ state = _behav.get_state().id if _behav.get_state() is not None else None
2533
+ action = _behav.get_action().id if _behav.get_action() is not None else None
2534
+ behav_status = {'state': state, 'action': action,
2535
+ 'state_with_action': _behav.get_state().has_action()
2536
+ if (state is not None) else False}
2537
+ else:
2538
+ behav_status = None
2539
+
2540
+ # Collecting known agents
2541
+ if self.__inspector_cache['all_agents_count'] != len(self.hosted.all_agents):
2542
+ self.__inspector_cache['all_agents_count'] = len(self.hosted.all_agents)
2543
+ all_agents_profiles = {k: v.get_all_profile() for k, v in self.hosted.all_agents.items()}
2544
+
2545
+ # Inspector expects also to have access to the profile of the world,
2546
+ # so we patch this thing by adding it here
2547
+ if self.hosted.in_world() and self.conn.world_node_peer_id is not None:
2548
+ all_agents_profiles[self.conn.world_node_peer_id] = self.hosted.world_profile.get_all_profile()
2549
+ else:
2550
+ all_agents_profiles = None
2551
+
2552
+ # Collecting known streams info
2553
+ if self.__inspector_cache['known_streams_count'] != len(self.hosted.known_streams):
2554
+ self.__inspector_cache['known_streams_count'] = len(self.hosted.known_streams)
2555
+ known_streams_props = {(k + "-" + name): v.get_props().to_dict() for k, stream_dict in
2556
+ self.hosted.known_streams.items() for name, v in stream_dict.items()}
2557
+ else:
2558
+ known_streams_props = None
2559
+
2560
+ # Packing console, HSM status, and possibly HSM
2561
+ console_behav_status_and_behav = {'console': console,
2562
+ 'behav': behav,
2563
+ 'behav_status': behav_status,
2564
+ 'all_agents_profiles': all_agents_profiles,
2565
+ 'known_streams_props': known_streams_props}
2566
+
2567
+ # Sending console, HSM status, and possibly HSM to the inspector
2568
+ if not (await self.conn.send(self.inspector_peer_id, channel_trail=None,
2569
+ content_type=Msg.CONSOLE_AND_BEHAV_STATUS,
2570
+ content=console_behav_status_and_behav)):
2571
+ self.err("Failed to send data to the inspector")
2572
+
2573
+ # Sending stream data (not pubsub) to the inspector
2574
+ my_peer_ids = (self.get_public_peer_id(), self.get_world_peer_id())
2575
+ for net_hash, streams_dict in self.hosted.known_streams.items():
2576
+ peer_id = DataProps.peer_id_from_net_hash(net_hash)
2577
+
2578
+ # Preparing sample dict
2579
+ something_to_send = False
2580
+ content = {name: {} for name in streams_dict.keys()}
2581
+ for name, stream in streams_dict.items():
2582
+ data = stream.get(requested_by="__send_to_inspector")
2583
+
2584
+ if data is not None:
2585
+ something_to_send = True
2586
+
2587
+ self.hosted.deb(f"[__send_to_inspector] Preparing to send stream samples from {net_hash}, {name}")
2588
+ content[(peer_id + "|" + name) if peer_id not in my_peer_ids else name] = \
2589
+ {'data': data, 'data_tag': stream.get_tag(), 'data_uuid': stream.get_uuid()}
2590
+
2591
+ # Checking if there is something valid in this group of streams to send to inspector
2592
+ if not something_to_send:
2593
+ self.hosted.deb(f"[__send_to_inspector] No stream samples to send to inspector for {net_hash}, "
2594
+ f"all internal streams returned None")
2595
+ continue
2596
+
2597
+ self.hosted.deb(f"[__send_to_inspector] Sending samples of {net_hash} by direct message, to inspector")
2598
+ name_or_group = DataProps.name_or_group_from_net_hash(net_hash)
2599
+ if not (await self.conn.send(self.inspector_peer_id, channel_trail=name_or_group,
2600
+ content_type=Msg.STREAM_SAMPLE, content=content)):
2601
+ self.err(f"Failed to send stream sample data to the inspector (hash: {net_hash})")
2602
+
2603
+
2604
+ class NodeSynchronizer:
2605
+ DEBUG = True
2606
+
2607
+ def __init__(self):
2608
+ """Initializes a new instance of the NodeSynchronizer class."""
2609
+ self.nodes = []
2610
+ self.agent_nodes = {}
2611
+ self.world_node = None # Added to allow get_console() to access the world node from server.py (synch only)
2612
+ self.streams = {}
2613
+ self.world = None
2614
+ self.world_masters = set()
2615
+ self.world_masters_node_ids = None
2616
+ self.agent_name_to_profile = {}
2617
+ self.clock = Clock()
2618
+ self.synch_cycle = -1
2619
+ self.synch_cycles = -1
2620
+
2621
+ # Visualization-related attributes
2622
+ self.using_server = False
2623
+ self.server_checkpoints = None
2624
+ self.skip_clear_for = 0
2625
+ self.step_event = None # Event that triggers a new step (manipulated by the server)
2626
+ self.wait_event = None # Event that triggers a new "wait-for-step-event" case (manipulated by the server)
2627
+ self.next_checkpoint = 0
2628
+ self.server_checkpoints = None
2629
+ self.gap = 0. # Seconds
2630
+
2631
+ def add_node(self, node: Node):
2632
+ """Adds a new node to the synchronizer.
2633
+
2634
+ Args:
2635
+ node: The node to add.
2636
+ """
2637
+ self.nodes.append(node)
2638
+
2639
+ if node.node_type == Node.AGENT:
2640
+ self.agent_nodes[node.agent.get_name()] = node
2641
+ if self.world_masters_node_ids is not None:
2642
+ if node.node_id in self.world_masters_node_ids:
2643
+ self.world_masters.add(node.agent.get_name())
2644
+ self.agent_name_to_profile[node.agent.get_name()] = node.agent.get_profile()
2645
+ elif node.node_type == Node.WORLD:
2646
+ self.world_node = node
2647
+ self.world = node.world
2648
+ self.world_masters_node_ids = node.world_masters_node_ids
2649
+ if self.world_masters_node_ids is None:
2650
+ self.world_masters_node_ids = set()
2651
+ for node in self.nodes:
2652
+ if node.node_id in self.world_masters_node_ids:
2653
+ self.world_masters.add(node.agent.get_name())
2654
+ node.debug_server_running = True
2655
+
2656
+ async def run(self, addresses: list[str] | None, synch_cycles: int | None = None):
2657
+ """Starts the main execution loop for the node (async).
2658
+
2659
+ Args:
2660
+ addresses: Addresses of the world to connect to.
2661
+ synch_cycles: The number of clock cycles to run the loop for. If None, runs indefinitely.
2662
+ """
2663
+ if self.world is None:
2664
+ raise GenException("Missing world node")
2665
+
2666
+ # External events
2667
+ if self.using_server:
2668
+ self.step_event = threading.Event()
2669
+ self.wait_event = threading.Event()
2670
+
2671
+ # Main loop
2672
+ self.synch_cycles = synch_cycles
2673
+ self.synch_cycle = 0
2674
+
2675
+ try:
2676
+ while True:
2677
+
2678
+ # In server mode, we wait for an external event to go ahead (step_event.set())
2679
+ if self.using_server:
2680
+ self.wait_event.set()
2681
+ self.step_event.wait()
2682
+ self.wait_event.clear()
2683
+
2684
+ state_changed = False
2685
+ world_node = None
2686
+ for node in self.nodes:
2687
+ if node.node_type == Node.AGENT:
2688
+ await node.run_async(cycles=1, join_world=addresses if self.synch_cycle == 0 else None)
2689
+ if self.gap > 0.:
2690
+ time.sleep(self.gap)
2691
+ state_changed = state_changed or node.agent.behav.get_state_changed()
2692
+ else:
2693
+ world_node = node
2694
+ if world_node is not None:
2695
+ await world_node.run_async(cycles=1)
2696
+ if self.gap > 0.:
2697
+ time.sleep(self.gap)
2698
+
2699
+ if NodeSynchronizer.DEBUG and state_changed:
2700
+ for node in self.nodes:
2701
+ if node.node_type == Node.AGENT:
2702
+ print(f"[DEBUG NODE SYNCHRONIZER] {node.agent.get_name()} "
2703
+ f"state: {node.agent.behav.get_state_name()}")
2704
+
2705
+ # Matching checkpoints
2706
+ if self.server_checkpoints is not None and self.server_checkpoints["current"] >= 0:
2707
+ self.server_checkpoints["matched"] = -1
2708
+ checkpoint = self.server_checkpoints["checkpoints"][self.server_checkpoints["current"]]
2709
+ agent = checkpoint["agent"]
2710
+ state = checkpoint["state"] if "state" in checkpoint else None
2711
+
2712
+ if agent not in self.nodes:
2713
+ raise GenException(f"Unknown agent in the checkpoint list: {agent}")
2714
+ behav = self.nodes[agent].agent.behav
2715
+ if not (state is None or state in behav.states):
2716
+ raise GenException(f"Unknown state in the checkpoint list: {state}")
2717
+
2718
+ if state is None or behav.state == state:
2719
+ if "skip" not in checkpoint:
2720
+ self.server_checkpoints["matched"] = self.server_checkpoints["current"]
2721
+ self.server_checkpoints["current"] += 1
2722
+ if self.server_checkpoints["current"] >= len(self.server_checkpoints["checkpoints"]):
2723
+ self.server_checkpoints["current"] = -1 # This means: no more checkpoints
2724
+ else:
2725
+ checkpoint["skip"] -= 1
2726
+ if checkpoint["skip"] <= 0:
2727
+ self.server_checkpoints["current"] += 1
2728
+ if self.server_checkpoints["current"] >= len(self.server_checkpoints["checkpoints"]):
2729
+ self.server_checkpoints["current"] = -1 # This means: no more checkpoints
2730
+
2731
+ # In step mode, we clear the external event to be able to wait for a new one
2732
+ if self.using_server:
2733
+ if self.skip_clear_for == 0:
2734
+ self.step_event.clear()
2735
+ elif self.skip_clear_for == -2: # Infinite play
2736
+ pass
2737
+ elif self.skip_clear_for == -1: # Play until next state
2738
+ if state_changed:
2739
+ self.step_event.clear()
2740
+ elif self.skip_clear_for == -3: # Play until next checkpoint:
2741
+ if self.server_checkpoints["matched"] >= 0:
2742
+ self.step_event.clear()
2743
+ else:
2744
+ self.skip_clear_for -= 1
2745
+
2746
+ self.synch_cycle += 1
2747
+
2748
+ # Stop condition on the number of cycles
2749
+ if self.synch_cycles is not None and self.synch_cycle == self.synch_cycles:
2750
+ break
2751
+ except KeyboardInterrupt:
2752
+ pass