pycityagent 2.0.0a65__cp312-cp312-macosx_11_0_arm64.whl → 2.0.0a67__cp312-cp312-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.
- pycityagent/agent/agent.py +157 -57
- pycityagent/agent/agent_base.py +316 -43
- pycityagent/cityagent/bankagent.py +49 -9
- pycityagent/cityagent/blocks/__init__.py +1 -2
- pycityagent/cityagent/blocks/cognition_block.py +54 -31
- pycityagent/cityagent/blocks/dispatcher.py +22 -17
- pycityagent/cityagent/blocks/economy_block.py +46 -32
- pycityagent/cityagent/blocks/mobility_block.py +209 -105
- pycityagent/cityagent/blocks/needs_block.py +101 -54
- pycityagent/cityagent/blocks/other_block.py +42 -33
- pycityagent/cityagent/blocks/plan_block.py +59 -42
- pycityagent/cityagent/blocks/social_block.py +167 -126
- pycityagent/cityagent/blocks/utils.py +13 -6
- pycityagent/cityagent/firmagent.py +17 -35
- pycityagent/cityagent/governmentagent.py +3 -3
- pycityagent/cityagent/initial.py +79 -49
- pycityagent/cityagent/memory_config.py +123 -94
- pycityagent/cityagent/message_intercept.py +0 -4
- pycityagent/cityagent/metrics.py +41 -0
- pycityagent/cityagent/nbsagent.py +24 -36
- pycityagent/cityagent/societyagent.py +9 -4
- pycityagent/cli/wrapper.py +2 -2
- pycityagent/economy/econ_client.py +407 -81
- pycityagent/environment/__init__.py +0 -3
- pycityagent/environment/sim/__init__.py +0 -3
- pycityagent/environment/sim/aoi_service.py +2 -2
- pycityagent/environment/sim/client.py +3 -31
- pycityagent/environment/sim/clock_service.py +2 -2
- pycityagent/environment/sim/lane_service.py +8 -8
- pycityagent/environment/sim/light_service.py +8 -8
- pycityagent/environment/sim/pause_service.py +9 -10
- pycityagent/environment/sim/person_service.py +20 -20
- pycityagent/environment/sim/road_service.py +2 -2
- pycityagent/environment/sim/sim_env.py +21 -5
- pycityagent/environment/sim/social_service.py +4 -4
- pycityagent/environment/simulator.py +249 -27
- pycityagent/environment/utils/__init__.py +2 -2
- pycityagent/environment/utils/geojson.py +2 -2
- pycityagent/environment/utils/grpc.py +4 -4
- pycityagent/environment/utils/map_utils.py +2 -2
- pycityagent/llm/embeddings.py +147 -28
- pycityagent/llm/llm.py +178 -111
- pycityagent/llm/llmconfig.py +5 -0
- pycityagent/llm/utils.py +4 -0
- pycityagent/memory/__init__.py +0 -4
- pycityagent/memory/const.py +2 -2
- pycityagent/memory/faiss_query.py +140 -61
- pycityagent/memory/memory.py +394 -91
- pycityagent/memory/memory_base.py +140 -34
- pycityagent/memory/profile.py +13 -13
- pycityagent/memory/self_define.py +13 -13
- pycityagent/memory/state.py +14 -14
- pycityagent/message/message_interceptor.py +253 -3
- pycityagent/message/messager.py +133 -6
- pycityagent/metrics/mlflow_client.py +47 -4
- pycityagent/pycityagent-sim +0 -0
- pycityagent/pycityagent-ui +0 -0
- pycityagent/simulation/__init__.py +3 -2
- pycityagent/simulation/agentgroup.py +150 -54
- pycityagent/simulation/simulation.py +276 -66
- pycityagent/survey/manager.py +45 -3
- pycityagent/survey/models.py +42 -2
- pycityagent/tools/__init__.py +1 -2
- pycityagent/tools/tool.py +93 -69
- pycityagent/utils/avro_schema.py +2 -2
- pycityagent/utils/parsers/code_block_parser.py +1 -1
- pycityagent/utils/parsers/json_parser.py +2 -2
- pycityagent/utils/parsers/parser_base.py +2 -2
- pycityagent/workflow/block.py +64 -13
- pycityagent/workflow/prompt.py +31 -23
- pycityagent/workflow/trigger.py +91 -24
- {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/METADATA +2 -2
- pycityagent-2.0.0a67.dist-info/RECORD +97 -0
- pycityagent/environment/interact/__init__.py +0 -0
- pycityagent/environment/interact/interact.py +0 -198
- pycityagent/environment/message/__init__.py +0 -0
- pycityagent/environment/sence/__init__.py +0 -0
- pycityagent/environment/sence/static.py +0 -416
- pycityagent/environment/sidecar/__init__.py +0 -8
- pycityagent/environment/sidecar/sidecarv2.py +0 -109
- pycityagent/environment/sim/economy_services.py +0 -192
- pycityagent/metrics/utils/const.py +0 -0
- pycityagent-2.0.0a65.dist-info/RECORD +0 -105
- {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/LICENSE +0 -0
- {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/WHEEL +0 -0
- {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/entry_points.txt +0 -0
- {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/top_level.txt +0 -0
@@ -19,6 +19,12 @@ From `{from_uuid}` To `{to_uuid}` abort due to block `{block_name}`
|
|
19
19
|
|
20
20
|
logger = logging.getLogger("message_interceptor")
|
21
21
|
|
22
|
+
__all__ = [
|
23
|
+
"MessageBlockBase",
|
24
|
+
"MessageInterceptor",
|
25
|
+
"MessageBlockListenerBase",
|
26
|
+
]
|
27
|
+
|
22
28
|
|
23
29
|
class MessageBlockBase(ABC):
|
24
30
|
"""
|
@@ -82,7 +88,7 @@ class MessageBlockBase(ABC):
|
|
82
88
|
@ray.remote
|
83
89
|
class MessageInterceptor:
|
84
90
|
"""
|
85
|
-
|
91
|
+
A class to intercept and process messages based on configured rules.
|
86
92
|
"""
|
87
93
|
|
88
94
|
def __init__(
|
@@ -92,6 +98,15 @@ class MessageInterceptor:
|
|
92
98
|
llm_config: Optional[dict] = None,
|
93
99
|
queue: Optional[Queue] = None,
|
94
100
|
) -> None:
|
101
|
+
"""
|
102
|
+
Initialize the MessageInterceptor with optional configuration.
|
103
|
+
|
104
|
+
- **Args**:
|
105
|
+
- `blocks` (Optional[list[MessageBlockBase]], optional): Initial list of message interception rules. Defaults to an empty list.
|
106
|
+
- `black_list` (Optional[list[tuple[str, str]]], optional): Initial blacklist of communication pairs. Defaults to an empty list.
|
107
|
+
- `llm_config` (Optional[dict], optional): Configuration dictionary for initializing the LLM instance. Defaults to None.
|
108
|
+
- `queue` (Optional[Queue], optional): Queue for message processing. Defaults to None.
|
109
|
+
"""
|
95
110
|
if blocks is not None:
|
96
111
|
self._blocks: list[MessageBlockBase] = blocks
|
97
112
|
else:
|
@@ -114,6 +129,18 @@ class MessageInterceptor:
|
|
114
129
|
def llm(
|
115
130
|
self,
|
116
131
|
) -> LLM:
|
132
|
+
"""
|
133
|
+
Access the Large Language Model instance.
|
134
|
+
|
135
|
+
- **Description**:
|
136
|
+
- Provides access to the internal LLM instance. Raises an error if accessed before assignment.
|
137
|
+
|
138
|
+
- **Raises**:
|
139
|
+
- `RuntimeError`: If accessed before setting the LLM.
|
140
|
+
|
141
|
+
- **Returns**:
|
142
|
+
- `LLM`: The Large Language Model instance.
|
143
|
+
"""
|
117
144
|
if self._llm is None:
|
118
145
|
raise RuntimeError(f"LLM access before assignment, please `set_llm` first!")
|
119
146
|
return self._llm
|
@@ -122,12 +149,30 @@ class MessageInterceptor:
|
|
122
149
|
async def blocks(
|
123
150
|
self,
|
124
151
|
) -> list[MessageBlockBase]:
|
152
|
+
"""
|
153
|
+
Retrieve the message interception rules.
|
154
|
+
|
155
|
+
- **Description**:
|
156
|
+
- Returns a copy of the current list of message interception rules.
|
157
|
+
|
158
|
+
- **Returns**:
|
159
|
+
- `list[MessageBlockBase]`: The list of message interception rules.
|
160
|
+
"""
|
125
161
|
return self._blocks
|
126
162
|
|
127
163
|
@lock_decorator
|
128
164
|
async def set_llm(self, llm: LLM):
|
129
165
|
"""
|
130
|
-
Set the
|
166
|
+
Set the Large Language Model instance.
|
167
|
+
|
168
|
+
- **Description**:
|
169
|
+
- Updates the internal LLM instance used for message processing.
|
170
|
+
|
171
|
+
- **Args**:
|
172
|
+
- `llm` (LLM): The LLM instance to be set.
|
173
|
+
|
174
|
+
- **Returns**:
|
175
|
+
- `None`
|
131
176
|
"""
|
132
177
|
if self._llm is None:
|
133
178
|
self._llm = llm
|
@@ -136,24 +181,64 @@ class MessageInterceptor:
|
|
136
181
|
async def violation_counts(
|
137
182
|
self,
|
138
183
|
) -> dict[str, int]:
|
184
|
+
"""
|
185
|
+
Retrieve the violation counts.
|
186
|
+
|
187
|
+
- **Description**:
|
188
|
+
- Returns a deep copy of the violation counts to prevent external modification of the original data.
|
189
|
+
|
190
|
+
- **Returns**:
|
191
|
+
- `dict[str, int]`: The dictionary of violation counts.
|
192
|
+
"""
|
139
193
|
return deepcopy(self._violation_counts)
|
140
194
|
|
141
195
|
@property
|
142
196
|
def has_llm(
|
143
197
|
self,
|
144
198
|
) -> bool:
|
199
|
+
"""
|
200
|
+
Check if a Large Language Model is configured.
|
201
|
+
|
202
|
+
- **Description**:
|
203
|
+
- Confirms whether a Large Language Model instance has been set.
|
204
|
+
|
205
|
+
- **Returns**:
|
206
|
+
- `bool`: True if an LLM is set, otherwise False.
|
207
|
+
"""
|
145
208
|
return self._llm is not None
|
146
209
|
|
147
210
|
@lock_decorator
|
148
211
|
async def black_list(
|
149
212
|
self,
|
150
213
|
) -> list[tuple[str, str]]:
|
214
|
+
"""
|
215
|
+
Retrieve the blacklist.
|
216
|
+
|
217
|
+
- **Description**:
|
218
|
+
- Returns a deep copy of the current blacklist to protect the original data from external modifications.
|
219
|
+
|
220
|
+
- **Returns**:
|
221
|
+
- `list[tuple[str, str]]`: The blacklist.
|
222
|
+
"""
|
151
223
|
return deepcopy(self._black_list)
|
152
224
|
|
153
225
|
@lock_decorator
|
154
226
|
async def add_to_black_list(
|
155
227
|
self, black_list: Union[list[tuple[str, str]], tuple[str, str]]
|
156
228
|
):
|
229
|
+
"""
|
230
|
+
Add entries to the blacklist.
|
231
|
+
|
232
|
+
- **Description**:
|
233
|
+
- Adds one or more entries to the blacklist, ensuring each entry's uniqueness.
|
234
|
+
|
235
|
+
- **Args**:
|
236
|
+
- `black_list` (Union[list[tuple[str, str]], tuple[str, str]]):
|
237
|
+
Can be a single tuple or a list of tuples indicating the entries to add to the blacklist.
|
238
|
+
|
239
|
+
- **Returns**:
|
240
|
+
- `None`
|
241
|
+
"""
|
157
242
|
if all(isinstance(s, str) for s in black_list):
|
158
243
|
# tuple[str,str]
|
159
244
|
_black_list = [black_list]
|
@@ -167,12 +252,30 @@ class MessageInterceptor:
|
|
167
252
|
def has_queue(
|
168
253
|
self,
|
169
254
|
) -> bool:
|
255
|
+
"""
|
256
|
+
Check if a queue is configured.
|
257
|
+
|
258
|
+
- **Description**:
|
259
|
+
- Confirms whether a queue instance has been set for message processing.
|
260
|
+
|
261
|
+
- **Returns**:
|
262
|
+
- `bool`: True if a queue is set, otherwise False.
|
263
|
+
"""
|
170
264
|
return self._queue is not None
|
171
265
|
|
172
266
|
@lock_decorator
|
173
267
|
async def set_queue(self, queue: Queue):
|
174
268
|
"""
|
175
269
|
Set the queue of the MessageInterceptor.
|
270
|
+
|
271
|
+
- **Description**:
|
272
|
+
- Assigns a queue to the MessageInterceptor for asynchronous message handling.
|
273
|
+
|
274
|
+
- **Args**:
|
275
|
+
- `queue` (Queue): The queue instance to be set.
|
276
|
+
|
277
|
+
- **Returns**:
|
278
|
+
- `None`
|
176
279
|
"""
|
177
280
|
self._queue = queue
|
178
281
|
|
@@ -180,6 +283,19 @@ class MessageInterceptor:
|
|
180
283
|
async def remove_from_black_list(
|
181
284
|
self, to_remove_black_list: Union[list[tuple[str, str]], tuple[str, str]]
|
182
285
|
):
|
286
|
+
"""
|
287
|
+
Remove entries from the blacklist.
|
288
|
+
|
289
|
+
- **Description**:
|
290
|
+
- Removes one or more entries from the blacklist, ensuring each entry's removal.
|
291
|
+
|
292
|
+
- **Args**:
|
293
|
+
- `to_remove_black_list` (Union[list[tuple[str, str]], tuple[str, str]]):
|
294
|
+
Can be a single tuple or a list of tuples indicating the entries to remove from the blacklist.
|
295
|
+
|
296
|
+
- **Returns**:
|
297
|
+
- `None`
|
298
|
+
"""
|
183
299
|
if all(isinstance(s, str) for s in to_remove_black_list):
|
184
300
|
# tuple[str,str]
|
185
301
|
_black_list = [to_remove_black_list]
|
@@ -192,6 +308,18 @@ class MessageInterceptor:
|
|
192
308
|
def queue(
|
193
309
|
self,
|
194
310
|
) -> Queue:
|
311
|
+
"""
|
312
|
+
Access the queue used for message processing.
|
313
|
+
|
314
|
+
- **Description**:
|
315
|
+
- Provides access to the internal queue. Raises an error if accessed before assignment.
|
316
|
+
|
317
|
+
- **Raises**:
|
318
|
+
- `RuntimeError`: If accessed before setting the queue.
|
319
|
+
|
320
|
+
- **Returns**:
|
321
|
+
- `Queue`: The queue instance.
|
322
|
+
"""
|
195
323
|
if self._queue is None:
|
196
324
|
raise RuntimeError(
|
197
325
|
f"Queue access before assignment, please `set_queue` first!"
|
@@ -200,12 +328,37 @@ class MessageInterceptor:
|
|
200
328
|
|
201
329
|
@lock_decorator
|
202
330
|
async def insert_block(self, block: MessageBlockBase, index: Optional[int] = None):
|
331
|
+
"""
|
332
|
+
Insert a message block into the blocks list at a specified position.
|
333
|
+
|
334
|
+
- **Description**:
|
335
|
+
- Inserts a new message interception rule into the list at the specified index or appends it if no index is provided.
|
336
|
+
|
337
|
+
- **Args**:
|
338
|
+
- `block` (MessageBlockBase): The message block to insert.
|
339
|
+
- `index` (Optional[int], optional): The position at which to insert the block. Defaults to appending at the end.
|
340
|
+
|
341
|
+
- **Returns**:
|
342
|
+
- `None`
|
343
|
+
"""
|
203
344
|
if index is None:
|
204
345
|
index = len(self._blocks)
|
205
346
|
self._blocks.insert(index, block)
|
206
347
|
|
207
348
|
@lock_decorator
|
208
349
|
async def pop_block(self, index: Optional[int] = None) -> MessageBlockBase:
|
350
|
+
"""
|
351
|
+
Remove and return a message block from the blocks list.
|
352
|
+
|
353
|
+
- **Description**:
|
354
|
+
- Removes and returns the message block at the specified index or the last one if no index is provided.
|
355
|
+
|
356
|
+
- **Args**:
|
357
|
+
- `index` (Optional[int], optional): The position of the block to remove. Defaults to removing the last element.
|
358
|
+
|
359
|
+
- **Returns**:
|
360
|
+
- `MessageBlockBase`: The removed message block.
|
361
|
+
"""
|
209
362
|
if index is None:
|
210
363
|
index = -1
|
211
364
|
return self._blocks.pop(index)
|
@@ -214,6 +367,19 @@ class MessageInterceptor:
|
|
214
367
|
async def set_black_list(
|
215
368
|
self, black_list: Union[list[tuple[str, str]], tuple[str, str]]
|
216
369
|
):
|
370
|
+
"""
|
371
|
+
Set the blacklist with new entries.
|
372
|
+
|
373
|
+
- **Description**:
|
374
|
+
- Updates the blacklist with new entries, ensuring each entry's uniqueness.
|
375
|
+
|
376
|
+
- **Args**:
|
377
|
+
- `black_list` (Union[list[tuple[str, str]], tuple[str, str]]):
|
378
|
+
Can be a single tuple or a list of tuples indicating the new blacklist entries.
|
379
|
+
|
380
|
+
- **Returns**:
|
381
|
+
- `None`
|
382
|
+
"""
|
217
383
|
if all(isinstance(s, str) for s in black_list):
|
218
384
|
# tuple[str,str]
|
219
385
|
_black_list = [black_list]
|
@@ -224,6 +390,18 @@ class MessageInterceptor:
|
|
224
390
|
|
225
391
|
@lock_decorator
|
226
392
|
async def set_blocks(self, blocks: list[MessageBlockBase]):
|
393
|
+
"""
|
394
|
+
Replace the current blocks list with a new list of message blocks.
|
395
|
+
|
396
|
+
- **Description**:
|
397
|
+
- Sets a new list of message interception rules, replacing the existing list.
|
398
|
+
|
399
|
+
- **Args**:
|
400
|
+
- `blocks` (list[MessageBlockBase]): The new list of message blocks to set.
|
401
|
+
|
402
|
+
- **Returns**:
|
403
|
+
- `None`
|
404
|
+
"""
|
227
405
|
self._blocks = blocks
|
228
406
|
|
229
407
|
@lock_decorator
|
@@ -233,6 +411,20 @@ class MessageInterceptor:
|
|
233
411
|
to_uuid: str,
|
234
412
|
msg: str,
|
235
413
|
):
|
414
|
+
"""
|
415
|
+
Forward a message through all message blocks.
|
416
|
+
|
417
|
+
- **Description**:
|
418
|
+
- Processes a message by passing it through all configured message blocks. Each block can modify the message or prevent its forwarding based on implemented logic.
|
419
|
+
|
420
|
+
- **Args**:
|
421
|
+
- `from_uuid` (str): The UUID of the sender.
|
422
|
+
- `to_uuid` (str): The UUID of the recipient.
|
423
|
+
- `msg` (str): The message content to forward.
|
424
|
+
|
425
|
+
- **Returns**:
|
426
|
+
- `bool`: True if the message was successfully processed by all blocks, otherwise False.
|
427
|
+
"""
|
236
428
|
for _block in self._blocks:
|
237
429
|
if not _block.has_llm and self.has_llm:
|
238
430
|
await _block.set_llm(self.llm)
|
@@ -274,11 +466,29 @@ class MessageInterceptor:
|
|
274
466
|
|
275
467
|
|
276
468
|
class MessageBlockListenerBase(ABC):
|
469
|
+
"""
|
470
|
+
Base class for message block listeners that can listen to a queue and process items.
|
471
|
+
|
472
|
+
- **Attributes**:
|
473
|
+
- `_queue` (Optional[Queue]): Queue from which the listener retrieves items.
|
474
|
+
- `_lock` (asyncio.Lock): Lock for thread-safe access in asynchronous environments.
|
475
|
+
- `_values_from_queue` (list[Any]): List of values retrieved from the queue if saving is enabled.
|
476
|
+
- `_save_queue_values` (bool): Flag indicating whether to save values from the queue.
|
477
|
+
- `_get_queue_period` (float): Period in seconds between queue retrieval attempts.
|
478
|
+
"""
|
479
|
+
|
277
480
|
def __init__(
|
278
481
|
self,
|
279
482
|
save_queue_values: bool = False,
|
280
483
|
get_queue_period: float = 0.1,
|
281
484
|
) -> None:
|
485
|
+
"""
|
486
|
+
Initialize the MessageBlockListenerBase with optional configuration.
|
487
|
+
|
488
|
+
- **Args**:
|
489
|
+
- `save_queue_values` (bool, optional): Whether to save values retrieved from the queue. Defaults to False.
|
490
|
+
- `get_queue_period` (float, optional): Time period in seconds between queue retrieval attempts. Defaults to 0.1.
|
491
|
+
"""
|
282
492
|
self._queue = None
|
283
493
|
self._lock = asyncio.Lock()
|
284
494
|
self._values_from_queue: list[Any] = []
|
@@ -289,6 +499,18 @@ class MessageBlockListenerBase(ABC):
|
|
289
499
|
def queue(
|
290
500
|
self,
|
291
501
|
) -> Queue:
|
502
|
+
"""
|
503
|
+
Access the queue used by the listener.
|
504
|
+
|
505
|
+
- **Description**:
|
506
|
+
- Provides access to the internal queue. Raises an error if accessed before assignment.
|
507
|
+
|
508
|
+
- **Raises**:
|
509
|
+
- `RuntimeError`: If accessed before setting the queue.
|
510
|
+
|
511
|
+
- **Returns**:
|
512
|
+
- `Queue`: The queue instance.
|
513
|
+
"""
|
292
514
|
if self._queue is None:
|
293
515
|
raise RuntimeError(
|
294
516
|
f"Queue access before assignment, please `set_queue` first!"
|
@@ -299,12 +521,30 @@ class MessageBlockListenerBase(ABC):
|
|
299
521
|
def has_queue(
|
300
522
|
self,
|
301
523
|
) -> bool:
|
524
|
+
"""
|
525
|
+
Check if a queue is configured.
|
526
|
+
|
527
|
+
- **Description**:
|
528
|
+
- Confirms whether a queue instance has been set for the listener.
|
529
|
+
|
530
|
+
- **Returns**:
|
531
|
+
- `b
|
532
|
+
"""
|
302
533
|
return self._queue is not None
|
303
534
|
|
304
535
|
@lock_decorator
|
305
536
|
async def set_queue(self, queue: Queue):
|
306
537
|
"""
|
307
|
-
Set the queue
|
538
|
+
Set the queue for the listener.
|
539
|
+
|
540
|
+
- **Description**:
|
541
|
+
- Assigns a queue to the listener for asynchronous item processing.
|
542
|
+
|
543
|
+
- **Args**:
|
544
|
+
- `queue` (Queue): The queue instance to be set.
|
545
|
+
|
546
|
+
- **Returns**:
|
547
|
+
- `None`
|
308
548
|
"""
|
309
549
|
self._queue = queue
|
310
550
|
|
@@ -312,6 +552,16 @@ class MessageBlockListenerBase(ABC):
|
|
312
552
|
async def forward(
|
313
553
|
self,
|
314
554
|
):
|
555
|
+
"""
|
556
|
+
Continuously retrieve items from the queue and process them.
|
557
|
+
|
558
|
+
- **Description**:
|
559
|
+
- Listens to the queue, retrieves items at intervals defined by `_get_queue_period`,
|
560
|
+
and processes each item. If `_save_queue_values` is True, it saves the items in `_values_from_queue`.
|
561
|
+
|
562
|
+
- **Returns**:
|
563
|
+
- `None`
|
564
|
+
"""
|
315
565
|
while True:
|
316
566
|
if self.has_queue:
|
317
567
|
value = await self.queue.get_async() # type: ignore
|
pycityagent/message/messager.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import asyncio
|
2
2
|
import json
|
3
3
|
import logging
|
4
|
+
import time
|
4
5
|
from typing import Any, Optional, Union
|
5
6
|
|
6
7
|
import ray
|
@@ -8,11 +9,26 @@ from aiomqtt import Client
|
|
8
9
|
|
9
10
|
from .message_interceptor import MessageInterceptor
|
10
11
|
|
12
|
+
__all__ = [
|
13
|
+
"Messager",
|
14
|
+
]
|
15
|
+
|
11
16
|
logger = logging.getLogger("pycityagent")
|
12
17
|
|
13
18
|
|
14
19
|
@ray.remote
|
15
20
|
class Messager:
|
21
|
+
"""
|
22
|
+
A class to manage message sending and receiving using an MQTT protocol.
|
23
|
+
|
24
|
+
- **Attributes**:
|
25
|
+
- `client` (Client): An instance of the MQTT client.
|
26
|
+
- `connected` (bool): Indicates whether the connection to the broker is established.
|
27
|
+
- `message_queue` (asyncio.Queue): Queue for storing received messages.
|
28
|
+
- `receive_messages_task` (Optional[Task]): Task for listening to incoming messages.
|
29
|
+
- `_message_interceptor` (Optional[ray.ObjectRef]): Reference to a remote message interceptor object.
|
30
|
+
"""
|
31
|
+
|
16
32
|
def __init__(
|
17
33
|
self,
|
18
34
|
hostname: str,
|
@@ -22,6 +38,17 @@ class Messager:
|
|
22
38
|
timeout=60,
|
23
39
|
message_interceptor: Optional[ray.ObjectRef] = None,
|
24
40
|
):
|
41
|
+
"""
|
42
|
+
Initialize the Messager with connection parameters.
|
43
|
+
|
44
|
+
- **Args**:
|
45
|
+
- `hostname` (str): The hostname or IP address of the MQTT broker.
|
46
|
+
- `port` (int, optional): Port number of the MQTT broker. Defaults to 1883.
|
47
|
+
- `username` (str, optional): Username for broker authentication.
|
48
|
+
- `password` (str, optional): Password for broker authentication.
|
49
|
+
- `timeout` (int, optional): Connection timeout in seconds. Defaults to 60.
|
50
|
+
- `message_interceptor` (Optional[ray.ObjectRef], optional): Reference to a message interceptor object.
|
51
|
+
"""
|
25
52
|
self.client = Client(
|
26
53
|
hostname, port=port, username=username, password=password, timeout=timeout
|
27
54
|
)
|
@@ -29,23 +56,61 @@ class Messager:
|
|
29
56
|
self.message_queue = asyncio.Queue() # 用于存储接收到的消息
|
30
57
|
self.receive_messages_task = None
|
31
58
|
self._message_interceptor = message_interceptor
|
59
|
+
self._log_list = []
|
32
60
|
|
33
61
|
@property
|
34
62
|
def message_interceptor(
|
35
63
|
self,
|
36
64
|
) -> Union[None, ray.ObjectRef]:
|
65
|
+
"""
|
66
|
+
Access the message interceptor reference.
|
67
|
+
|
68
|
+
- **Returns**:
|
69
|
+
- `Union[None, ray.ObjectRef]`: The message interceptor reference.
|
70
|
+
"""
|
37
71
|
return self._message_interceptor
|
72
|
+
|
73
|
+
def get_log_list(self):
|
74
|
+
return self._log_list
|
75
|
+
|
76
|
+
def clear_log_list(self):
|
77
|
+
self._log_list = []
|
38
78
|
|
39
79
|
def set_message_interceptor(self, message_interceptor: ray.ObjectRef):
|
80
|
+
"""
|
81
|
+
Set the message interceptor reference.
|
82
|
+
|
83
|
+
- **Args**:
|
84
|
+
- `message_interceptor` (ray.ObjectRef): The message interceptor reference to be set.
|
85
|
+
"""
|
40
86
|
self._message_interceptor = message_interceptor
|
41
87
|
|
42
88
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
89
|
+
"""
|
90
|
+
Asynchronous exit method to ensure proper cleanup when used in an async context manager.
|
91
|
+
|
92
|
+
- **Args**:
|
93
|
+
- `exc_type`, `exc_value`, `traceback`: Exception information if an exception occurred.
|
94
|
+
"""
|
43
95
|
await self.stop()
|
44
96
|
|
45
97
|
async def ping(self):
|
98
|
+
"""
|
99
|
+
Send a ping message to the MQTT broker.
|
100
|
+
|
101
|
+
- **Description**:
|
102
|
+
- Publishes a 'ping' message on the 'ping' topic with QoS level 1.
|
103
|
+
"""
|
46
104
|
await self.client.publish(topic="ping", payload="ping", qos=1)
|
47
105
|
|
48
106
|
async def connect(self):
|
107
|
+
"""
|
108
|
+
Attempt to connect to the MQTT broker up to three times.
|
109
|
+
|
110
|
+
- **Description**:
|
111
|
+
- Tries to establish a connection to the MQTT broker. Retries up to three times with delays between attempts.
|
112
|
+
- Logs success or failure accordingly.
|
113
|
+
"""
|
49
114
|
for i in range(3):
|
50
115
|
try:
|
51
116
|
await self.client.__aenter__()
|
@@ -59,18 +124,35 @@ class Messager:
|
|
59
124
|
logger.error("All connection attempts failed.")
|
60
125
|
|
61
126
|
async def disconnect(self):
|
127
|
+
"""
|
128
|
+
Disconnect from the MQTT broker.
|
129
|
+
|
130
|
+
- **Description**:
|
131
|
+
- Closes the connection to the MQTT broker and logs the disconnection.
|
132
|
+
"""
|
62
133
|
await self.client.__aexit__(None, None, None)
|
63
134
|
self.connected = False
|
64
135
|
logger.info("Disconnected from MQTT Broker")
|
65
136
|
|
66
137
|
async def is_connected(self):
|
67
|
-
"""
|
138
|
+
"""
|
139
|
+
Check if the connection to the broker is established.
|
140
|
+
|
141
|
+
- **Returns**:
|
142
|
+
- `bool`: True if connected, otherwise False.
|
143
|
+
"""
|
68
144
|
return self.connected
|
69
145
|
|
70
|
-
# TODO:add message interceptor
|
71
146
|
async def subscribe(
|
72
147
|
self, topics: Union[str, list[str]], agents: Union[Any, list[Any]]
|
73
148
|
):
|
149
|
+
"""
|
150
|
+
Subscribe to one or more MQTT topics.
|
151
|
+
|
152
|
+
- **Args**:
|
153
|
+
- `topics` (Union[str, list[str]]): Topic or list of topics to subscribe to.
|
154
|
+
- `agents` (Union[Any, list[Any]]): Agents or list of agents associated with the subscription.
|
155
|
+
"""
|
74
156
|
if not await self.is_connected():
|
75
157
|
logger.error(
|
76
158
|
f"Cannot subscribe to {topics} because not connected to the Broker."
|
@@ -83,12 +165,22 @@ class Messager:
|
|
83
165
|
await self.client.subscribe(topics, qos=1) # type: ignore
|
84
166
|
|
85
167
|
async def receive_messages(self):
|
86
|
-
"""
|
168
|
+
"""
|
169
|
+
Listen for incoming messages and store them in the queue.
|
170
|
+
|
171
|
+
- **Description**:
|
172
|
+
- Continuously listens for incoming messages and puts them into the message queue.
|
173
|
+
"""
|
87
174
|
async for message in self.client.messages:
|
88
175
|
await self.message_queue.put(message)
|
89
176
|
|
90
177
|
async def fetch_messages(self):
|
91
|
-
"""
|
178
|
+
"""
|
179
|
+
Retrieve all messages currently in the queue.
|
180
|
+
|
181
|
+
- **Returns**:
|
182
|
+
- `list[Any]`: List of messages retrieved from the queue.
|
183
|
+
"""
|
92
184
|
messages = []
|
93
185
|
while not self.message_queue.empty():
|
94
186
|
messages.append(await self.message_queue.get())
|
@@ -101,7 +193,28 @@ class Messager:
|
|
101
193
|
from_uuid: Optional[str] = None,
|
102
194
|
to_uuid: Optional[str] = None,
|
103
195
|
):
|
104
|
-
"""
|
196
|
+
"""
|
197
|
+
Send a message through the MQTT broker.
|
198
|
+
|
199
|
+
- **Args**:
|
200
|
+
- `topic` (str): Topic to which the message should be published.
|
201
|
+
- `payload` (dict): Payload of the message to send.
|
202
|
+
- `from_uuid` (Optional[str], optional): UUID of the sender. Required for interception.
|
203
|
+
- `to_uuid` (Optional[str], optional): UUID of the recipient. Required for interception.
|
204
|
+
|
205
|
+
- **Description**:
|
206
|
+
- Serializes the payload to JSON, checks it against the message interceptor (if any),
|
207
|
+
and publishes the message to the specified topic if valid.
|
208
|
+
"""
|
209
|
+
start_time = time.time()
|
210
|
+
log = {
|
211
|
+
"topic": topic,
|
212
|
+
"payload": payload,
|
213
|
+
"from_uuid": from_uuid,
|
214
|
+
"to_uuid": to_uuid,
|
215
|
+
"start_time": start_time,
|
216
|
+
"consumption": 0
|
217
|
+
}
|
105
218
|
message = json.dumps(payload, default=str)
|
106
219
|
interceptor = self.message_interceptor
|
107
220
|
is_valid: bool = True
|
@@ -114,15 +227,29 @@ class Messager:
|
|
114
227
|
logger.info(f"Message sent to {topic}: {message}")
|
115
228
|
else:
|
116
229
|
logger.info(f"Message not sent to {topic}: {message} due to interceptor")
|
230
|
+
log["consumption"] = time.time() - start_time
|
231
|
+
self._log_list.append(log)
|
117
232
|
|
118
233
|
async def start_listening(self):
|
119
|
-
"""
|
234
|
+
"""
|
235
|
+
Start the task for listening to incoming messages.
|
236
|
+
|
237
|
+
- **Description**:
|
238
|
+
- Starts a task that listens for incoming messages and puts them into the queue.
|
239
|
+
- Only starts the task if the connection to the broker is active.
|
240
|
+
"""
|
120
241
|
if await self.is_connected():
|
121
242
|
self.receive_messages_task = asyncio.create_task(self.receive_messages())
|
122
243
|
else:
|
123
244
|
logger.error("Cannot start listening because not connected to the Broker.")
|
124
245
|
|
125
246
|
async def stop(self):
|
247
|
+
"""
|
248
|
+
Stop the listener and disconnect from the MQTT broker.
|
249
|
+
|
250
|
+
- **Description**:
|
251
|
+
- Cancels the receive_messages_task and ensures the MQTT broker connection is closed.
|
252
|
+
"""
|
126
253
|
assert self.receive_messages_task is not None
|
127
254
|
self.receive_messages_task.cancel()
|
128
255
|
await asyncio.gather(self.receive_messages_task, return_exceptions=True)
|