pycityagent 2.0.0a66__cp311-cp311-macosx_11_0_arm64.whl → 2.0.0a67__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.
Files changed (87) hide show
  1. pycityagent/agent/agent.py +157 -57
  2. pycityagent/agent/agent_base.py +316 -43
  3. pycityagent/cityagent/bankagent.py +49 -9
  4. pycityagent/cityagent/blocks/__init__.py +1 -2
  5. pycityagent/cityagent/blocks/cognition_block.py +54 -31
  6. pycityagent/cityagent/blocks/dispatcher.py +22 -17
  7. pycityagent/cityagent/blocks/economy_block.py +46 -32
  8. pycityagent/cityagent/blocks/mobility_block.py +130 -100
  9. pycityagent/cityagent/blocks/needs_block.py +101 -44
  10. pycityagent/cityagent/blocks/other_block.py +42 -33
  11. pycityagent/cityagent/blocks/plan_block.py +59 -42
  12. pycityagent/cityagent/blocks/social_block.py +167 -116
  13. pycityagent/cityagent/blocks/utils.py +13 -6
  14. pycityagent/cityagent/firmagent.py +17 -35
  15. pycityagent/cityagent/governmentagent.py +3 -3
  16. pycityagent/cityagent/initial.py +79 -44
  17. pycityagent/cityagent/memory_config.py +108 -88
  18. pycityagent/cityagent/message_intercept.py +0 -4
  19. pycityagent/cityagent/metrics.py +41 -0
  20. pycityagent/cityagent/nbsagent.py +24 -36
  21. pycityagent/cityagent/societyagent.py +7 -3
  22. pycityagent/cli/wrapper.py +2 -2
  23. pycityagent/economy/econ_client.py +407 -81
  24. pycityagent/environment/__init__.py +0 -3
  25. pycityagent/environment/sim/__init__.py +0 -3
  26. pycityagent/environment/sim/aoi_service.py +2 -2
  27. pycityagent/environment/sim/client.py +3 -31
  28. pycityagent/environment/sim/clock_service.py +2 -2
  29. pycityagent/environment/sim/lane_service.py +8 -8
  30. pycityagent/environment/sim/light_service.py +8 -8
  31. pycityagent/environment/sim/pause_service.py +9 -10
  32. pycityagent/environment/sim/person_service.py +20 -20
  33. pycityagent/environment/sim/road_service.py +2 -2
  34. pycityagent/environment/sim/sim_env.py +21 -5
  35. pycityagent/environment/sim/social_service.py +4 -4
  36. pycityagent/environment/simulator.py +249 -27
  37. pycityagent/environment/utils/__init__.py +2 -2
  38. pycityagent/environment/utils/geojson.py +2 -2
  39. pycityagent/environment/utils/grpc.py +4 -4
  40. pycityagent/environment/utils/map_utils.py +2 -2
  41. pycityagent/llm/embeddings.py +147 -28
  42. pycityagent/llm/llm.py +122 -77
  43. pycityagent/llm/llmconfig.py +5 -0
  44. pycityagent/llm/utils.py +4 -0
  45. pycityagent/memory/__init__.py +0 -4
  46. pycityagent/memory/const.py +2 -2
  47. pycityagent/memory/faiss_query.py +140 -61
  48. pycityagent/memory/memory.py +393 -90
  49. pycityagent/memory/memory_base.py +140 -34
  50. pycityagent/memory/profile.py +13 -13
  51. pycityagent/memory/self_define.py +13 -13
  52. pycityagent/memory/state.py +14 -14
  53. pycityagent/message/message_interceptor.py +253 -3
  54. pycityagent/message/messager.py +133 -6
  55. pycityagent/metrics/mlflow_client.py +47 -4
  56. pycityagent/pycityagent-sim +0 -0
  57. pycityagent/pycityagent-ui +0 -0
  58. pycityagent/simulation/__init__.py +3 -2
  59. pycityagent/simulation/agentgroup.py +145 -52
  60. pycityagent/simulation/simulation.py +257 -62
  61. pycityagent/survey/manager.py +45 -3
  62. pycityagent/survey/models.py +42 -2
  63. pycityagent/tools/__init__.py +1 -2
  64. pycityagent/tools/tool.py +93 -69
  65. pycityagent/utils/avro_schema.py +2 -2
  66. pycityagent/utils/parsers/code_block_parser.py +1 -1
  67. pycityagent/utils/parsers/json_parser.py +2 -2
  68. pycityagent/utils/parsers/parser_base.py +2 -2
  69. pycityagent/workflow/block.py +64 -13
  70. pycityagent/workflow/prompt.py +31 -23
  71. pycityagent/workflow/trigger.py +91 -24
  72. {pycityagent-2.0.0a66.dist-info → pycityagent-2.0.0a67.dist-info}/METADATA +2 -2
  73. pycityagent-2.0.0a67.dist-info/RECORD +97 -0
  74. pycityagent/environment/interact/__init__.py +0 -0
  75. pycityagent/environment/interact/interact.py +0 -198
  76. pycityagent/environment/message/__init__.py +0 -0
  77. pycityagent/environment/sence/__init__.py +0 -0
  78. pycityagent/environment/sence/static.py +0 -416
  79. pycityagent/environment/sidecar/__init__.py +0 -8
  80. pycityagent/environment/sidecar/sidecarv2.py +0 -109
  81. pycityagent/environment/sim/economy_services.py +0 -192
  82. pycityagent/metrics/utils/const.py +0 -0
  83. pycityagent-2.0.0a66.dist-info/RECORD +0 -105
  84. {pycityagent-2.0.0a66.dist-info → pycityagent-2.0.0a67.dist-info}/LICENSE +0 -0
  85. {pycityagent-2.0.0a66.dist-info → pycityagent-2.0.0a67.dist-info}/WHEEL +0 -0
  86. {pycityagent-2.0.0a66.dist-info → pycityagent-2.0.0a67.dist-info}/entry_points.txt +0 -0
  87. {pycityagent-2.0.0a66.dist-info → pycityagent-2.0.0a67.dist-info}/top_level.txt +0 -0
@@ -15,6 +15,8 @@ from mlflow.entities import (Dataset, DatasetInput, Document, Experiment,
15
15
 
16
16
  from ..utils.decorators import lock_decorator
17
17
 
18
+ __all__ = ["init_mlflow_connection","MlflowClient",]
19
+
18
20
  logger = logging.getLogger("mlflow")
19
21
 
20
22
 
@@ -26,7 +28,20 @@ def init_mlflow_connection(
26
28
  experiment_description: Optional[str] = None,
27
29
  experiment_tags: Optional[dict[str, Any]] = None,
28
30
  ) -> tuple[str, tuple[str, mlflow.MlflowClient, Run, str]]:
29
-
31
+ """
32
+ Initialize an MLflow connection with a new or existing experiment and run.
33
+
34
+ - **Args**:
35
+ config (dict): Configuration dictionary containing MLflow credentials and URI.
36
+ experiment_uuid (str): A unique identifier for the experiment.
37
+ mlflow_run_name (str, optional): Name of the MLflow run. Defaults to a generated name.
38
+ experiment_name (str, optional): Name of the experiment. Defaults to a generated name.
39
+ experiment_description (str, optional): Description for the experiment. Defaults to None.
40
+ experiment_tags (dict, optional): Tags to associate with the experiment. Defaults to None.
41
+
42
+ - **Returns**:
43
+ tuple: A tuple containing the run_id and another tuple with the MLflow URI, client, run object, and run UUID.
44
+ """
30
45
  os.environ["MLFLOW_TRACKING_USERNAME"] = config.get("username", None)
31
46
  os.environ["MLFLOW_TRACKING_PASSWORD"] = config.get("password", None)
32
47
 
@@ -70,9 +85,7 @@ def init_mlflow_connection(
70
85
 
71
86
 
72
87
  class MlflowClient:
73
- """
74
- - Mlflow client
75
- """
88
+ """A wrapper around MLflow's MlflowClient for managing experiments and runs."""
76
89
 
77
90
  def __init__(
78
91
  self,
@@ -84,6 +97,18 @@ class MlflowClient:
84
97
  experiment_tags: Optional[dict[str, Any]] = None,
85
98
  run_id: Optional[str] = None,
86
99
  ) -> None:
100
+ """
101
+ Initialize the MlflowClient.
102
+
103
+ - **Args**:
104
+ config (dict): Configuration dictionary containing MLflow credentials and URI.
105
+ experiment_uuid (str): A unique identifier for the experiment.
106
+ mlflow_run_name (str, optional): Name of the MLflow run. Defaults to a generated name.
107
+ experiment_name (str, optional): Name of the experiment. Defaults to a generated name.
108
+ experiment_description (str, optional): Description for the experiment. Defaults to None.
109
+ experiment_tags (dict, optional): Tags to associate with the experiment. Defaults to None.
110
+ run_id (str, optional): Existing MLflow run ID to attach to. Defaults to None.
111
+ """
87
112
  if run_id is None:
88
113
  self._run_id, (
89
114
  self._mlflow_uri,
@@ -112,12 +137,14 @@ class MlflowClient:
112
137
  def client(
113
138
  self,
114
139
  ) -> mlflow.MlflowClient:
140
+ """Return the underlying MLflow client."""
115
141
  return self._client
116
142
 
117
143
  @property
118
144
  def run_id(
119
145
  self,
120
146
  ) -> str:
147
+ """Return the current run ID."""
121
148
  assert self._run_id is not None
122
149
  return self._run_id
123
150
 
@@ -128,6 +155,11 @@ class MlflowClient:
128
155
  params: Sequence[Param] = (),
129
156
  tags: Sequence[RunTag] = (),
130
157
  ):
158
+ """
159
+ Log a batch of metrics, parameters, and tags to the MLflow run.
160
+
161
+ This method is thread-safe due to the `@lock_decorator`.
162
+ """
131
163
  self.client.log_batch(
132
164
  run_id=self.run_id, metrics=metrics, params=params, tags=tags
133
165
  )
@@ -140,6 +172,17 @@ class MlflowClient:
140
172
  step: Optional[int] = None,
141
173
  timestamp: Optional[int] = None,
142
174
  ):
175
+ """
176
+ Log a single metric to the MLflow run.
177
+
178
+ This method is thread-safe due to the `@lock_decorator`.
179
+
180
+ - **Args**:
181
+ key (str): The name of the metric.
182
+ value (float): The value of the metric.
183
+ step (int, optional): The step at which the metric was recorded. Defaults to None.
184
+ timestamp (int, optional): The timestamp when the metric was recorded. Defaults to None.
185
+ """
143
186
  if timestamp is not None:
144
187
  timestamp = int(timestamp)
145
188
  self.client.log_metric(
Binary file
Binary file
@@ -3,6 +3,7 @@
3
3
  """
4
4
 
5
5
  from .simulation import AgentSimulation
6
- from .storage.pg import PgWriter, create_pg_tables
7
6
 
8
- __all__ = ["AgentSimulation", "PgWriter", "create_pg_tables"]
7
+ __all__ = [
8
+ "AgentSimulation",
9
+ ]
@@ -26,6 +26,7 @@ from ..utils import (DIALOG_SCHEMA, INSTITUTION_STATUS_SCHEMA, PROFILE_SCHEMA,
26
26
  STATUS_SCHEMA, SURVEY_SCHEMA)
27
27
 
28
28
  logger = logging.getLogger("pycityagent")
29
+ __all__ = ["AgentGroup"]
29
30
 
30
31
 
31
32
  @ray.remote
@@ -37,7 +38,7 @@ class AgentGroup:
37
38
  memory_config_function_group: dict[type[Agent], Callable],
38
39
  config: dict,
39
40
  exp_name: str,
40
- exp_id: str | UUID,
41
+ exp_id: Union[str, UUID],
41
42
  enable_avro: bool,
42
43
  avro_path: Path,
43
44
  enable_pgsql: bool,
@@ -49,6 +50,33 @@ class AgentGroup:
49
50
  agent_config_file: Optional[dict[type[Agent], str]] = None,
50
51
  environment: Optional[dict[str, str]] = None,
51
52
  ):
53
+ """
54
+ Represents a group of agents that can be deployed in a Ray distributed environment.
55
+
56
+ - **Description**:
57
+ - Manages the creation and initialization of multiple agents, each potentially of different types,
58
+ with associated memory configurations, and connects them to various services such as MLflow, MQTT messager,
59
+ PostgreSQL writer, message interceptor, and LLM client. It also sets up an economy client and simulator for
60
+ agent interaction within a simulated environment.
61
+
62
+ - **Args**:
63
+ - `agent_class` (Union[Type[Agent], List[Type[Agent]]]): A single or list of agent classes to instantiate.
64
+ - `number_of_agents` (Union[int, List[int]]): Number of instances to create for each agent class.
65
+ - `memory_config_function_group` (Dict[Type[Agent], Callable]): Functions to configure memory for each agent type.
66
+ - `config` (dict): Configuration settings for the agent group.
67
+ - `exp_name` (str): Name of the experiment.
68
+ - `exp_id` (str | UUID): Identifier for the experiment.
69
+ - `enable_avro` (bool): Flag to enable AVRO file support.
70
+ - `avro_path` (Path): Path where AVRO files will be stored.
71
+ - `enable_pgsql` (bool): Flag to enable PostgreSQL support.
72
+ - `pgsql_writer` (ray.ObjectRef): Reference to a PostgreSQL writer object.
73
+ - `message_interceptor` (ray.ObjectRef): Reference to a message interceptor object.
74
+ - `mlflow_run_id` (str): Run identifier for MLflow tracking.
75
+ - `embedding_model` (Embeddings): Model used for generating embeddings.
76
+ - `logging_level` (int): Logging level for the agent group.
77
+ - `agent_config_file` (Optional[Dict[Type[Agent], str]], optional): File paths for loading agent configurations. Defaults to None.
78
+ - `environment` (Optional[Dict[str, str]], optional): Environment variables for the simulator. Defaults to None.
79
+ """
52
80
  logger.setLevel(logging_level)
53
81
  self._uuid = str(uuid.uuid4())
54
82
  if not isinstance(agent_class, list):
@@ -112,12 +140,13 @@ class AgentGroup:
112
140
  llmConfig = LLMConfig(config["llm_request"])
113
141
  logger.info(f"-----Creating LLM client in AgentGroup {self._uuid} ...")
114
142
  self.llm = LLM(llmConfig)
143
+ self.llm.set_semaphore(200)
115
144
 
116
145
  # prepare Simulator
117
146
  logger.info(f"-----Creating Simulator in AgentGroup {self._uuid} ...")
118
147
  self.simulator = Simulator(config["simulator_request"])
119
148
  self.projector = pyproj.Proj(self.simulator.map.header["projection"])
120
- self.simulator.set_environment(environment)
149
+ self.simulator.set_environment(environment) # type:ignore
121
150
  # prepare Economy client
122
151
  logger.info(f"-----Creating Economy client in AgentGroup {self._uuid} ...")
123
152
  self.economy_client = EconomyClient(
@@ -135,7 +164,9 @@ class AgentGroup:
135
164
  agent_class_i = agent_class[i]
136
165
  number_of_agents_i = number_of_agents[i]
137
166
  for j in range(number_of_agents_i):
138
- memory_config_function_group_i = memory_config_function_group[agent_class_i]
167
+ memory_config_function_group_i = memory_config_function_group[
168
+ agent_class_i
169
+ ]
139
170
  extra_attributes, profile, base = memory_config_function_group_i()
140
171
  memory = Memory(config=extra_attributes, profile=profile, base=base)
141
172
  agent = agent_class_i(
@@ -154,7 +185,10 @@ class AgentGroup:
154
185
  agent.set_avro_file(self.avro_file) # type: ignore
155
186
  if self.enable_pgsql:
156
187
  agent.set_pgsql_writer(self._pgsql_writer)
157
- if self.agent_config_file is not None and self.agent_config_file[agent_class_i]:
188
+ if (
189
+ self.agent_config_file is not None
190
+ and self.agent_config_file[agent_class_i]
191
+ ):
158
192
  agent.load_from_file(self.agent_config_file[agent_class_i])
159
193
  if self._message_interceptor is not None:
160
194
  agent.set_message_interceptor(self._message_interceptor)
@@ -172,6 +206,12 @@ class AgentGroup:
172
206
  @property
173
207
  def agent_type(self):
174
208
  return self.agent_class
209
+
210
+ async def get_economy_ids(self):
211
+ return await self.economy_client.get_ids()
212
+
213
+ async def set_economy_ids(self, agent_ids: set[int], org_ids: set[int]):
214
+ await self.economy_client.set_ids(agent_ids, org_ids)
175
215
 
176
216
  def get_agent_count(self):
177
217
  return self.agent_count
@@ -314,6 +354,17 @@ class AgentGroup:
314
354
  keys: Optional[list[str]] = None,
315
355
  values: Optional[list[Any]] = None,
316
356
  ) -> list[str]:
357
+ """
358
+ Filters agents based on type and/or key-value pairs in their status.
359
+
360
+ - **Args**:
361
+ - `types` (Optional[List[Type[Agent]]]): A list of agent types to filter by.
362
+ - `keys` (Optional[List[str]]): A list of keys to check in the agent's status.
363
+ - `values` (Optional[List[Any]]): The corresponding values for each key in the `keys` list.
364
+
365
+ - **Returns**:
366
+ - `List[str]`: A list of UUIDs for agents that match the filter criteria.
367
+ """
317
368
  filtered_uuids = []
318
369
  for agent in self.agents:
319
370
  add = True
@@ -340,6 +391,16 @@ class AgentGroup:
340
391
  async def gather(
341
392
  self, content: str, target_agent_uuids: Optional[list[str]] = None
342
393
  ):
394
+ """
395
+ Gathers specific content from all or targeted agents within the group.
396
+
397
+ - **Args**:
398
+ - `content` (str): The key of the status content to gather from the agents.
399
+ - `target_agent_uuids` (Optional[List[str]]): A list of agent UUIDs to target. If None, targets all agents.
400
+
401
+ - **Returns**:
402
+ - `Dict[str, Any]`: A dictionary mapping agent UUIDs to the gathered content.
403
+ """
343
404
  logger.debug(f"-----Gathering {content} from all agents in group {self._uuid}")
344
405
  results = {}
345
406
  if target_agent_uuids is None:
@@ -350,6 +411,14 @@ class AgentGroup:
350
411
  return results
351
412
 
352
413
  async def update(self, target_agent_uuid: str, target_key: str, content: Any):
414
+ """
415
+ Updates a specific key in the status of a targeted agent.
416
+
417
+ - **Args**:
418
+ - `target_agent_uuid` (str): The UUID of the agent to update.
419
+ - `target_key` (str): The key in the agent's status to update.
420
+ - `content` (Any): The new value for the specified key.
421
+ """
353
422
  logger.debug(
354
423
  f"-----Updating {target_key} for agent {target_agent_uuid} in group {self._uuid}"
355
424
  )
@@ -357,9 +426,25 @@ class AgentGroup:
357
426
  await agent.status.update(target_key, content)
358
427
 
359
428
  async def update_environment(self, key: str, value: str):
429
+ """
430
+ Updates the environment with a given key-value pair.
431
+
432
+ - **Args**:
433
+ - `key` (str): The key to update in the environment.
434
+ - `value` (str): The value to set for the specified key.
435
+ """
360
436
  self.simulator.update_environment(key, value)
361
437
 
362
438
  async def message_dispatch(self):
439
+ """
440
+ Dispatches messages received via MQTT to the appropriate agents.
441
+
442
+ - **Description**:
443
+ - Continuously listens for incoming MQTT messages and dispatches them to the relevant agents based on the topic.
444
+ - Messages are expected to have a topic formatted as "exps/{exp_id}/agents/{agent_uuid}/{topic_type}".
445
+ - The payload is decoded from bytes to string and then parsed as JSON.
446
+ - Depending on the `topic_type`, different handler methods on the agent are called to process the message.
447
+ """
363
448
  logger.debug(f"-----Starting message dispatch for group {self._uuid}")
364
449
  while True:
365
450
  assert self.messager is not None
@@ -402,6 +487,13 @@ class AgentGroup:
402
487
  async def save_status(
403
488
  self, simulator_day: Optional[int] = None, simulator_t: Optional[int] = None
404
489
  ):
490
+ """
491
+ Saves the current status of the agents at a given point in the simulation.
492
+
493
+ - **Args**:
494
+ - `simulator_day` (Optional[int]): The day number in the simulation time.
495
+ - `simulator_t` (Optional[int]): The tick or time unit in the simulation day.
496
+ """
405
497
  _statuses_time_list: list[tuple[dict, datetime]] = []
406
498
  if self.enable_avro:
407
499
  logger.debug(f"-----Saving status for group {self._uuid} with Avro")
@@ -603,54 +695,18 @@ class AgentGroup:
603
695
  lng, lat = self.projector(x, y, inverse=True)
604
696
  # ATTENTION: no valid position for an institution
605
697
  parent_id = -1
606
- try:
607
- nominal_gdp = await agent.status.get("nominal_gdp")
608
- except:
609
- nominal_gdp = []
610
- try:
611
- real_gdp = await agent.status.get("real_gdp")
612
- except:
613
- real_gdp = []
614
- try:
615
- unemployment = await agent.status.get("unemployment")
616
- except:
617
- unemployment = []
618
- try:
619
- wages = await agent.status.get("wages")
620
- except:
621
- wages = []
622
- try:
623
- prices = await agent.status.get("prices")
624
- except:
625
- prices = []
626
- try:
627
- inventory = await agent.status.get("inventory")
628
- except:
629
- inventory = 0
630
- try:
631
- price = await agent.status.get("price")
632
- except:
633
- price = 0.0
634
- try:
635
- interest_rate = await agent.status.get("interest_rate")
636
- except:
637
- interest_rate = 0.0
638
- try:
639
- bracket_cutoffs = await agent.status.get("bracket_cutoffs")
640
- except:
641
- bracket_cutoffs = []
642
- try:
643
- bracket_rates = await agent.status.get("bracket_rates")
644
- except:
645
- bracket_rates = []
646
- try:
647
- employees = await agent.status.get("employees")
648
- except:
649
- employees = []
650
- try:
651
- friend_ids = await agent.status.get("friends")
652
- except:
653
- friend_ids = []
698
+ nominal_gdp = await agent.status.get("nominal_gdp", [])
699
+ real_gdp = await agent.status.get("real_gdp", [])
700
+ unemployment = await agent.status.get("unemployment", [])
701
+ wages = await agent.status.get("wages", [])
702
+ prices = await agent.status.get("prices", [])
703
+ inventory = await agent.status.get("inventory", 0)
704
+ price = await agent.status.get("price", 0.0)
705
+ interest_rate = await agent.status.get("interest_rate", 0.0)
706
+ bracket_cutoffs = await agent.status.get("bracket_cutoffs", [])
707
+ bracket_rates = await agent.status.get("bracket_rates", [])
708
+ employees = await agent.status.get("employees", [])
709
+ friend_ids = await agent.status.get("friends", [])
654
710
  _status_dict = {
655
711
  "id": agent._uuid,
656
712
  "day": _day,
@@ -706,12 +762,39 @@ class AgentGroup:
706
762
  )
707
763
 
708
764
  def get_llm_consumption(self):
765
+ """
766
+ Retrieves the consumption statistics from the LLM client.
767
+
768
+ - **Returns**:
769
+ - The consumption data provided by the LLM client.
770
+ """
709
771
  return self.llm.get_consumption()
710
772
 
711
773
  async def step(self):
774
+ """
775
+ Executes a single simulation step by running all agents concurrently.
776
+
777
+ - **Description**:
778
+ - This method initiates the `run` coroutine for each agent in parallel using asyncio.gather.
779
+ - Any exceptions raised during the execution are caught, logged, and re-raised as a RuntimeError.
780
+
781
+ - **Raises**:
782
+ - `RuntimeError`: If an exception occurs during the execution of any agent's `run` method.
783
+ """
712
784
  try:
713
785
  tasks = [agent.run() for agent in self.agents]
714
786
  await asyncio.gather(*tasks)
787
+ simulator_log = self.simulator.get_log_list() + self.economy_client.get_log_list()
788
+ group_logs = {
789
+ "llm_log": self.llm.get_log_list(),
790
+ "mqtt_log": ray.get(self.messager.get_log_list.remote()),
791
+ "simulator_log": simulator_log
792
+ }
793
+ self.llm.clear_log_list()
794
+ self.messager.clear_log_list.remote()
795
+ self.simulator.clear_log_list()
796
+ self.economy_client.clear_log_list()
797
+ return group_logs
715
798
  except Exception as e:
716
799
  import traceback
717
800
 
@@ -719,6 +802,16 @@ class AgentGroup:
719
802
  raise RuntimeError(str(e)) from e
720
803
 
721
804
  async def save(self, day: int, t: int):
805
+ """
806
+ Saves the current status of the agents at a given point in the simulation.
807
+
808
+ - **Args**:
809
+ - `day` (int): The day number in the simulation time.
810
+ - `t` (int): The tick or time unit within the simulation day.
811
+
812
+ - **Raises**:
813
+ - `RuntimeError`: If an exception occurs while saving the status.
814
+ """
722
815
  try:
723
816
  await self.save_status(day, t)
724
817
  except Exception as e: