unaiverse 0.1.11__cp311-cp311-macosx_11_0_arm64.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.

Potentially problematic release.


This version of unaiverse might be problematic. Click here for more details.

Files changed (50) hide show
  1. unaiverse/__init__.py +19 -0
  2. unaiverse/agent.py +2090 -0
  3. unaiverse/agent_basics.py +1948 -0
  4. unaiverse/clock.py +221 -0
  5. unaiverse/dataprops.py +1236 -0
  6. unaiverse/hsm.py +1892 -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 +710 -0
  16. unaiverse/networking/__init__.py +16 -0
  17. unaiverse/networking/node/__init__.py +18 -0
  18. unaiverse/networking/node/connpool.py +1308 -0
  19. unaiverse/networking/node/node.py +2499 -0
  20. unaiverse/networking/node/profile.py +446 -0
  21. unaiverse/networking/node/tokens.py +79 -0
  22. unaiverse/networking/p2p/__init__.py +187 -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 +135 -0
  27. unaiverse/networking/p2p/lib.go +2662 -0
  28. unaiverse/networking/p2p/lib.go.sha256 +1 -0
  29. unaiverse/networking/p2p/lib_types.py +312 -0
  30. unaiverse/networking/p2p/message_pb2.py +50 -0
  31. unaiverse/networking/p2p/messages.py +362 -0
  32. unaiverse/networking/p2p/mylogger.py +77 -0
  33. unaiverse/networking/p2p/p2p.py +871 -0
  34. unaiverse/networking/p2p/proto-go/message.pb.go +846 -0
  35. unaiverse/networking/p2p/unailib.cpython-311-darwin.so +0 -0
  36. unaiverse/stats.py +1481 -0
  37. unaiverse/streamlib/__init__.py +15 -0
  38. unaiverse/streamlib/streamlib.py +210 -0
  39. unaiverse/streams.py +776 -0
  40. unaiverse/utils/__init__.py +16 -0
  41. unaiverse/utils/lone_wolf.json +24 -0
  42. unaiverse/utils/misc.py +310 -0
  43. unaiverse/utils/sandbox.py +293 -0
  44. unaiverse/utils/server.py +435 -0
  45. unaiverse/world.py +335 -0
  46. unaiverse-0.1.11.dist-info/METADATA +367 -0
  47. unaiverse-0.1.11.dist-info/RECORD +50 -0
  48. unaiverse-0.1.11.dist-info/WHEEL +6 -0
  49. unaiverse-0.1.11.dist-info/licenses/LICENSE +43 -0
  50. unaiverse-0.1.11.dist-info/top_level.txt +1 -0
unaiverse/world.py ADDED
@@ -0,0 +1,335 @@
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 json
16
+ import math
17
+ import bisect
18
+ from unaiverse.stats import Stats
19
+ from unaiverse.agent import AgentBasics
20
+ from unaiverse.hsm import HybridStateMachine
21
+ from typing import List, Dict, Any
22
+ from unaiverse.networking.p2p.messages import Msg
23
+ from unaiverse.networking.node.profile import NodeProfile
24
+
25
+
26
+ class World(AgentBasics):
27
+
28
+ def __init__(self, world_folder: str, merge_flat_stream_labels: bool = False, stats: Stats | None = None):
29
+ """Initializes a World object, which acts as a special agent without a processor or behavior.
30
+
31
+ Args:
32
+ world_folder: The path of the world folder, with JSON files of the behaviors (per role) and agent.py.
33
+ """
34
+
35
+ # Creating a "special" agent with no processor and no behavior, but with a "world_folder", which is our world
36
+ super().__init__(proc=None, proc_inputs=None, proc_outputs=None, proc_opts=None, behav=None,
37
+ world_folder=world_folder, merge_flat_stream_labels=merge_flat_stream_labels)
38
+
39
+ # Clearing processor (world must have no processor, and, maybe, a dummy processor was allocated when building
40
+ # the agent in the init call above)
41
+ self.proc = None
42
+ self.proc_inputs = [] # Do not set it to None
43
+ self.proc_outputs = [] # Do not set it to None
44
+ self.compat_in_streams = None
45
+ self.compat_out_streams = None
46
+
47
+ # Stats
48
+ if stats is not None:
49
+ self.stats = stats
50
+ else:
51
+ # fallback to default Stats class
52
+ self.stats = Stats(is_world=True, db_path=f"{self.world_folder}/stats/world_stats.db",
53
+ cache_window_hours=2.0)
54
+
55
+ def assign_role(self, profile: NodeProfile, is_world_master: bool) -> str:
56
+ """Assigns an initial role to a newly connected agent.
57
+
58
+ In this basic implementation, the role is determined based on whether the agent is a world master or a regular
59
+ world agent, ensuring there's only one master.
60
+
61
+ Args:
62
+ profile: The NodeProfile of the new agent.
63
+ is_world_master: A boolean indicating if the new agent is attempting to be a master.
64
+
65
+ Returns:
66
+ A string representing the assigned role.
67
+ """
68
+ assert self.is_world, "Assigning a role is expected to be done by the world"
69
+
70
+ if profile.get_dynamic_profile()['guessed_location'] == 'Some Dummy Location, Just An Example Here':
71
+ pass
72
+
73
+ # Currently, roles are only world masters and world agents
74
+ if is_world_master:
75
+ if len(self.world_masters) <= 1:
76
+ return AgentBasics.ROLE_BITS_TO_STR[AgentBasics.ROLE_WORLD_MASTER]
77
+ else:
78
+ return AgentBasics.ROLE_BITS_TO_STR[AgentBasics.ROLE_WORLD_AGENT]
79
+ else:
80
+ return AgentBasics.ROLE_BITS_TO_STR[AgentBasics.ROLE_WORLD_AGENT]
81
+
82
+ async def set_role(self, peer_id: str, role: int):
83
+ """Sets a new role for a specific agent and broadcasts this change to the agent (async).
84
+
85
+ It computes the new role and sends a message containing the new role and the corresponding default behavior
86
+ for that role.
87
+
88
+ Args:
89
+ peer_id: The ID of the agent whose role is to be set.
90
+ role: The new role to be assigned (as an integer).
91
+ """
92
+ assert self.is_world, "Setting the role is expected to be done by the world, which will broadcast such info"
93
+
94
+ # Computing new role (keeping the first two bits as before)
95
+ cur_role = self._node_conn.get_role(peer_id)
96
+ new_role_without_base_int = (role >> 2) << 2
97
+ new_role = (cur_role & 3) | new_role_without_base_int
98
+
99
+ if new_role != role:
100
+ self._node_conn.set_role(peer_id, new_role)
101
+ self.out("Telling an agent that his role changed")
102
+ if not (await self._node_conn.send(peer_id, channel_trail=None,
103
+ content={'peer_id': peer_id, 'role': new_role,
104
+ 'default_behav':
105
+ self.role_to_behav[
106
+ self.ROLE_BITS_TO_STR[new_role_without_base_int]]
107
+ if self.role_to_behav is not None else
108
+ str(HybridStateMachine(None))},
109
+ content_type=Msg.ROLE_SUGGESTION)):
110
+ self.err("Failed to send role change, removing (disconnecting) " + peer_id)
111
+ await self._node_purge_fcn(peer_id)
112
+ else:
113
+ self.role_changed_by_world = True
114
+
115
+ def set_addresses_in_profile(self, peer_id, addresses):
116
+ """Updates the network addresses in an agent's profile.
117
+
118
+ Args:
119
+ peer_id: The ID of the agent whose profile is being updated.
120
+ addresses: A list of new addresses to set.
121
+ """
122
+ if peer_id in self.all_agents:
123
+ profile = self.all_agents[peer_id]
124
+ addrs = profile.get_dynamic_profile()['private_peer_addresses']
125
+ addrs.clear() # Warning: do not allocate a new list, keep the current one (it is referenced by others)
126
+ for _addrs in addresses:
127
+ addrs.append(_addrs)
128
+ self.received_address_update = True
129
+ else:
130
+ self.err(f"Cannot set addresses in profile, unknown peer_id {peer_id}")
131
+
132
+ def add_badge(self, peer_id: str, score: float, badge_type: str, agent_token: str,
133
+ badge_description: str | None = None):
134
+ """Requests a badge for a specific agent, which can be used to track and reward agent performance.
135
+ It validates the score and badge type and stores the badge information in an internal dictionary.
136
+
137
+ Args:
138
+ peer_id: The ID of the agent for whom the badge is requested.
139
+ score: The score associated with the badge (must be in [0, 1]).
140
+ badge_type: The type of badge to be awarded.
141
+ agent_token: The token of the agent receiving the badge.
142
+ badge_description: An optional text description for the badge.
143
+ """
144
+
145
+ # Validate score
146
+ if score < 0. or score > 1.:
147
+ raise ValueError(f"Score must be in [0.0, 1.0], got {score}")
148
+
149
+ # Validate badge_type
150
+ if badge_type not in AgentBasics.BADGE_TYPES:
151
+ raise ValueError(f"Invalid badge_type '{badge_type}'. Must be one of {AgentBasics.BADGE_TYPES}.")
152
+
153
+ if badge_description is None:
154
+ badge_description = ""
155
+
156
+ # The world not necessarily knows the token of the agents, since they usually do not send messages to the world
157
+ badge = {
158
+ 'agent_node_id': self.all_agents[peer_id].get_static_profile()['node_id'],
159
+ 'agent_token': agent_token,
160
+ 'badge_type': badge_type,
161
+ 'score': score,
162
+ 'badge_description': badge_description,
163
+ 'last_edit_utc': self._node_clock.get_time_as_string(),
164
+ }
165
+
166
+ if peer_id not in self.agent_badges:
167
+ self.agent_badges[peer_id] = [badge]
168
+ else:
169
+ self.agent_badges[peer_id].append(badge)
170
+
171
+ # This will force the sending of the dynamic profile at the defined time instants
172
+ self._node_profile.mark_change_in_connections()
173
+
174
+ # Get all the badges requested by the world
175
+ def get_all_badges(self):
176
+ """Retrieves all badges that have been added to the world's record for all agents.
177
+ This provides a central log of achievements or performance metrics.
178
+
179
+ Returns:
180
+ A dictionary where keys are agent peer IDs and values are lists of badge dictionaries.
181
+ """
182
+ return self.agent_badges
183
+
184
+ def clear_badges(self):
185
+ """Clears all badge records from the world's memory.
186
+ This can be used to reset competition results or clean up state after a specific event.
187
+ """
188
+ self.agent_badges = {}
189
+
190
+ def collect_and_store_own_stats(self):
191
+ """Collects this world's own stats and pushes them to the stats recorder."""
192
+ if self.stats is None:
193
+ return
194
+
195
+ t = self._node_clock.get_time_ms()
196
+ _, own_private_pid = self.get_peer_ids()
197
+
198
+ # Helper to add if value changed
199
+ def store_if_changed(stat_name, new_value):
200
+ last_value = self.stats.get_last_value(stat_name)
201
+ if last_value != new_value:
202
+ # Note: We pass the world's *own* peer_id for its *own* stats
203
+ self.stats.store_stat(stat_name, new_value, peer_id=own_private_pid, timestamp=t)
204
+
205
+ try:
206
+ store_if_changed("world_masters", len(self.world_masters))
207
+ store_if_changed("world_agents", len(self.world_agents))
208
+ store_if_changed("human_agents", len(self.human_agents))
209
+ store_if_changed("artificial_agents", len(self.artificial_agents))
210
+ except Exception as e:
211
+ self.err(f"[Stats] Error updating own world stats: {e}")
212
+
213
+ def _process_custom_stat(self, stat_name, value, peer_id, timestamp) -> bool:
214
+ """Hook for subclasses to intercept a stat. Return True if handled."""
215
+ return False
216
+
217
+ def _extract_graph_node_info(self, peer_id: str) -> Dict[str, Any]:
218
+ """Helper to extract lightweight visualization data from NodeProfile."""
219
+ profile = None
220
+ if peer_id == self.get_peer_ids()[1]:
221
+ # this is the world itself
222
+ profile = self._node_profile
223
+ else:
224
+ profile = self.all_agents.get(peer_id)
225
+ if profile is None:
226
+ return {}
227
+
228
+ # Accessing the inner private dict of NodeProfile based on your class structure
229
+ static_profile = profile.get_static_profile()
230
+ dynamic_profile = profile.get_dynamic_profile()
231
+
232
+ return {
233
+ 'Name': static_profile.get('node_name', '~'),
234
+ 'Owner': static_profile.get('email', '~'),
235
+ 'Role': dynamic_profile.get('connections', {}).get('role', 'unknown').split('~')[-1],
236
+ 'Type': static_profile.get('node_type', '~'),
237
+ 'Number of Badges': len(dynamic_profile.get('cv', [])),
238
+ 'Current Action': self.stats.get_last_value('action', peer_id=peer_id) or '~',
239
+ 'Current State': self.stats.get_last_value('state', peer_id=peer_id) or '~',
240
+ }
241
+
242
+ def _update_graph(self, peer_id: str, connected_peers_list: List[str], timestamp: int):
243
+ """Updates both graph connectivity (edges) and node metadata."""
244
+
245
+ # 1. initialize structure if missing (e.g. first run or after DB load)
246
+ graph_stat = self.stats._stats.setdefault("graph", {'nodes': {}, 'edges': {}})
247
+
248
+ # Ensure sub-dicts exist (defensive programming against malformed DB loads)
249
+ if 'nodes' not in graph_stat: graph_stat['nodes'] = {}
250
+ if 'edges' not in graph_stat: graph_stat['edges'] = {}
251
+
252
+ nodes = graph_stat['nodes']
253
+ edges = graph_stat['edges']
254
+
255
+ # 2. Update Node Metadata
256
+ # We update the sender's info
257
+ nodes[peer_id] = self._extract_graph_node_info(peer_id)
258
+
259
+ # We also ensure connected peers exist in 'nodes', even if we don't have their full profile yet
260
+ connected_peers = set(connected_peers_list)
261
+ for target_id in connected_peers:
262
+ if target_id not in nodes:
263
+ # Try to fetch profile if we have it, otherwise placeholder
264
+ nodes[target_id] = self._extract_graph_node_info(target_id)
265
+
266
+ # 3. Update Edges (Logic adapted from your previous code)
267
+ prev_connected_peers = edges.setdefault(peer_id, set())
268
+
269
+ # Add reverse connections (Undirected/Bidirectional logic)
270
+ for _peer_id in connected_peers:
271
+ edges.setdefault(_peer_id, set()).add(peer_id)
272
+
273
+ # Remove dropped reverse connections
274
+ to_remove = prev_connected_peers - connected_peers
275
+ for _peer_id in to_remove:
276
+ if _peer_id in edges and peer_id in edges[_peer_id]:
277
+ edges[_peer_id].remove(peer_id)
278
+
279
+ # Update peer's own forward connections
280
+ edges[peer_id] = connected_peers
281
+
282
+ # 4. Store
283
+ world_peer_id = self.get_peer_ids()[1]
284
+ self.stats.store_stat('graph', graph_stat, peer_id=world_peer_id, timestamp=timestamp)
285
+
286
+ def add_peer_stats(self, peer_stats_batch: List[Dict[str, Any]], sender_peer_id: str | None = None):
287
+ """(World-only) Processes a batch of stats received from a peer."""
288
+
289
+ # 1. Update own stats (this logic is now in the World)
290
+ self.collect_and_store_own_stats()
291
+
292
+ # 2. Process peer stats
293
+ connected_peers = []
294
+ for update in peer_stats_batch:
295
+ try:
296
+ p_id = update['peer_id']
297
+ if p_id != sender_peer_id:
298
+ # TODO: decide if we want to filter the stats
299
+ pass
300
+ stat_name = update['stat_name']
301
+ t = int(update['timestamp'])
302
+ v = update['value']
303
+
304
+ # Call the hook (which also lives in the World now)
305
+ if self._process_custom_stat(stat_name, v, p_id, t):
306
+ continue # The custom processor handled it
307
+
308
+ # Generate the graph and sicard the connected_peers stat
309
+ if stat_name == 'connected_peers':
310
+ # We need to wait for all the info to arrive before updating the graph.
311
+ # Otherwise _extract_graph_node_info may not find data yet.
312
+ connected_peers.append((p_id, v, t))
313
+ continue
314
+
315
+ # 3. Push to the "dumb" Stats recorder
316
+ if stat_name in self.stats._all_keys:
317
+ self.stats.store_stat(stat_name, v, peer_id=p_id, timestamp=t)
318
+ else:
319
+ self.err(f"[World] Unknown stat received: {stat_name}")
320
+
321
+ except Exception as e:
322
+ self.err(f"[World] Error processing stats update {update}: {e}")
323
+
324
+ # Now update the graph for all collected connected_peers stats
325
+ for p_id, v, t in connected_peers:
326
+ self._update_graph(p_id, v, t)
327
+
328
+ def debug_stats_dashboard(self):
329
+ """Helper to verify the dashboard looks correct during development."""
330
+ import plotly.io as pio
331
+
332
+ print("[DEBUG] Rendering Dashboard...")
333
+ json_str = self.stats.plot()
334
+ if json_str:
335
+ pio.from_json(json_str).show()