pycityagent 2.0.0a65__cp39-cp39-macosx_11_0_arm64.whl → 2.0.0a67__cp39-cp39-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 +209 -105
  9. pycityagent/cityagent/blocks/needs_block.py +101 -54
  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 -126
  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 -49
  17. pycityagent/cityagent/memory_config.py +123 -94
  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 +9 -4
  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 +178 -111
  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 +394 -91
  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 +150 -54
  60. pycityagent/simulation/simulation.py +276 -66
  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.0a65.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.0a65.dist-info/RECORD +0 -105
  84. {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/LICENSE +0 -0
  85. {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/WHEEL +0 -0
  86. {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/entry_points.txt +0 -0
  87. {pycityagent-2.0.0a65.dist-info → pycityagent-2.0.0a67.dist-info}/top_level.txt +0 -0
@@ -20,19 +20,23 @@ from ..environment.sim.person_service import PersonService
20
20
  from ..llm import LLM
21
21
  from ..memory import Memory
22
22
  from ..message import MessageInterceptor, Messager
23
- from ..metrics import MlflowClient
24
23
  from ..utils import DIALOG_SCHEMA, SURVEY_SCHEMA, process_survey_for_llm
25
24
  from ..workflow import Block
26
25
 
27
26
  logger = logging.getLogger("pycityagent")
28
27
 
28
+ __all__ = [
29
+ "Agent",
30
+ "AgentType",
31
+ ]
32
+
29
33
 
30
34
  class AgentType(Enum):
31
35
  """
32
- Agent类型
36
+ Agent Type
33
37
 
34
38
  - Citizen, Citizen type agent
35
- - Institution, Orgnization or institution type agent
39
+ - Institution, Organization or institution type agent
36
40
  """
37
41
 
38
42
  Unspecified = "Unspecified"
@@ -63,18 +67,19 @@ class Agent(ABC):
63
67
  copy_writer: Optional[ray.ObjectRef] = None,
64
68
  ) -> None:
65
69
  """
66
- Initialize the Agent.
67
-
68
- Args:
69
- name (str): The name of the agent.
70
- type (AgentType): The type of the agent. Defaults to `AgentType.Unspecified`
71
- llm_client (LLM): The language model client. Defaults to None.
72
- economy_client (EconomyClient): The `EconomySim` client. Defaults to None.
73
- messager (Messager, optional): The messager object. Defaults to None.
74
- simulator (Simulator, optional): The simulator object. Defaults to None.
75
- memory (Memory, optional): The memory of the agent. Defaults to None.
76
- avro_file (dict[str, str], optional): The avro file of the agent. Defaults to None.
77
- copy_writer (ray.ObjectRef): The copy_writer of the agent. Defaults to None.
70
+ Initialize the `Agent`.
71
+
72
+ - **Args**:
73
+ - `name` (`str`): The name of the agent.
74
+ - `type` (`AgentType`): The type of the agent. Defaults to `AgentType.Unspecified`.
75
+ - `llm_client` (`Optional[LLM]`): The language model client used by the agent. Defaults to `None`.
76
+ - `economy_client` (`Optional[EconomyClient]`): The client for interacting with the economy simulation. Defaults to `None`.
77
+ - `messager` (`Optional[ray.ObjectRef]`): The object used for messaging between agents. Defaults to `None`.
78
+ - `message_interceptor` (`Optional[ray.ObjectRef]`): The object used for intercepting messages. Defaults to `None`.
79
+ - `simulator` (`Optional[Simulator]`): The simulation environment in which the agent operates. Defaults to `None`.
80
+ - `memory` (`Optional[Memory]`): The memory storage for the agent. Defaults to `None`.
81
+ - `avro_file` (`Optional[dict[str, str]]`): A dictionary representing an Avro file associated with the agent. Defaults to `None`.
82
+ - `copy_writer` (`Optional[ray.ObjectRef]`): The object responsible for writing copies. Defaults to `None`.
78
83
  """
79
84
  self._name = name
80
85
  self._type = type
@@ -104,6 +109,28 @@ class Agent(ABC):
104
109
 
105
110
  @classmethod
106
111
  def export_class_config(cls) -> dict[str, dict]:
112
+ """
113
+ Export the class configuration as a dictionary.
114
+
115
+ - **Args**:
116
+ - None. This method relies on class attributes and type hints.
117
+
118
+ - **Returns**:
119
+ - `dict[str, dict]`: A dictionary containing the class configuration information, including:
120
+ - `agent_name`: The name of the class.
121
+ - `config`: A mapping of configurable fields to their default values.
122
+ - `description`: A mapping of descriptions for each configurable field.
123
+ - `blocks`: A list of dictionaries with configuration information for fields that are of type `Block`, each containing:
124
+ - `name`: The name of the field.
125
+ - `config`: Configuration information for the Block.
126
+ - `description`: Description information for the Block.
127
+ - `children`: Configuration information for any child Blocks (if applicable).
128
+
129
+ - **Description**:
130
+ - This method parses the annotations within the class to identify and process all fields that inherit from the `Block` class.
131
+ - For each `Block`-typed field, it calls the corresponding `export_class_config` method to retrieve its configuration and adds it to the result.
132
+ - If there are child `Block`s, it recursively exports their configurations using the `_export_subblocks` method.
133
+ """
107
134
  result = {
108
135
  "agent_name": cls.__name__,
109
136
  "config": {},
@@ -153,12 +180,38 @@ class Agent(ABC):
153
180
 
154
181
  @classmethod
155
182
  def export_to_file(cls, filepath: str) -> None:
183
+ """
184
+ Export the class configuration to a JSON file.
185
+
186
+ - **Args**:
187
+ - `filepath` (`str`): The path where the JSON file will be saved.
188
+
189
+ - **Returns**:
190
+ - `None`
191
+
192
+ - **Description**:
193
+ - This method calls `export_class_config` to get the configuration dictionary and writes it to the specified file in JSON format with indentation for readability.
194
+ """
156
195
  config = cls.export_class_config()
157
196
  with open(filepath, "w") as f:
158
197
  json.dump(config, f, indent=4)
159
198
 
160
199
  @classmethod
161
- def import_block_config(cls, config: dict[str, Union[list[dict], str]]) -> Agent:
200
+ def import_block_config(cls, config: dict[str, Union[list[dict], str]]) -> "Agent":
201
+ """
202
+ Import an agent's configuration from a dictionary and initialize the Agent instance along with its Blocks.
203
+
204
+ - **Args**:
205
+ - `config` (`dict[str, Union[list[dict], str]]`): A dictionary containing the configuration of the agent and its blocks.
206
+
207
+ - **Returns**:
208
+ - `Agent`: An initialized Agent instance configured according to the provided configuration.
209
+
210
+ - **Description**:
211
+ - Initializes a new agent using the name found in the configuration.
212
+ - Dynamically creates Block instances based on the configuration data and assigns them to the agent.
213
+ - If a block is not found in the global namespace or cannot be created, this method may raise errors.
214
+ """
162
215
  agent = cls(name=config["agent_name"]) # type:ignore
163
216
 
164
217
  def build_block(block_data: dict[str, Any]) -> Block:
@@ -175,15 +228,40 @@ class Agent(ABC):
175
228
  return agent
176
229
 
177
230
  @classmethod
178
- def import_from_file(cls, filepath: str) -> Agent:
231
+ def import_from_file(cls, filepath: str) -> "Agent":
232
+ """
233
+ Load an agent's configuration from a JSON file and initialize the Agent instance.
234
+
235
+ - **Args**:
236
+ - `filepath` (`str`): The path to the JSON file containing the agent's configuration.
237
+
238
+ - **Returns**:
239
+ - `Agent`: An initialized Agent instance configured according to the loaded configuration.
240
+
241
+ - **Description**:
242
+ - Reads the JSON configuration from the given file path.
243
+ - Delegates the creation of the agent and its blocks to `import_block_config`.
244
+ """
179
245
  with open(filepath, "r") as f:
180
246
  config = json.load(f)
181
247
  return cls.import_block_config(config)
182
248
 
183
249
  def load_from_config(self, config: dict[str, list[dict]]) -> None:
184
250
  """
185
- 使用配置更新当前Agent实例的Block层次结构。
251
+ Update the current Agent instance's Block hierarchy using the provided configuration.
252
+
253
+ - **Args**:
254
+ - `config` (`dict[str, list[dict]]`): A dictionary containing the configuration for updating the agent and its blocks.
255
+
256
+ - **Returns**:
257
+ - `None`
258
+
259
+ - **Description**:
260
+ - Updates the base parameters of the current agent instance according to the provided configuration.
261
+ - Recursively updates or creates top-level Blocks as specified in the configuration.
262
+ - Raises a `KeyError` if a required Block is not found in the agent.
186
263
  """
264
+ # 使用配置更新当前Agent实例的Block层次结构。
187
265
  # 更新当前Agent的基础参数
188
266
  for field in self.configurable_fields:
189
267
  if field in config["config"]:
@@ -204,6 +282,20 @@ class Agent(ABC):
204
282
  )
205
283
 
206
284
  def load_from_file(self, filepath: str) -> None:
285
+ """
286
+ Load configuration from a JSON file and update the current Agent instance.
287
+
288
+ - **Args**:
289
+ - `filepath` (`str`): The path to the JSON file containing the agent's configuration.
290
+
291
+ - **Returns**:
292
+ - `None`
293
+
294
+ - **Description**:
295
+ - Reads the configuration from the specified JSON file.
296
+ - Uses the `load_from_config` method to apply the loaded configuration to the current Agent instance.
297
+ - This method is useful for restoring an Agent's state from a saved configuration file.
298
+ """
207
299
  with open(filepath, "r") as f:
208
300
  config = json.load(f)
209
301
  self.load_from_config(config)
@@ -289,7 +381,7 @@ class Agent(ABC):
289
381
  f"EconomyClient access before assignment, please `set_economy_client` first!"
290
382
  )
291
383
  return self._economy_client
292
-
384
+
293
385
  @property
294
386
  def memory(self):
295
387
  """The Agent's Memory"""
@@ -334,7 +426,7 @@ class Agent(ABC):
334
426
  f"Copy Writer access before assignment, please `set_pgsql_writer` first!"
335
427
  )
336
428
  return self._pgsql_writer
337
-
429
+
338
430
  @property
339
431
  def messager(self):
340
432
  if self._messager is None:
@@ -342,17 +434,39 @@ class Agent(ABC):
342
434
  return self._messager
343
435
 
344
436
  async def messager_ping(self):
437
+ """
438
+ Send a ping request to the connected Messager.
439
+
440
+ - **Raises**:
441
+ - `RuntimeError`: If the Messager is not set.
442
+
443
+ - **Returns**:
444
+ - The result of the remote ping call from the Messager.
445
+
446
+ - **Description**:
447
+ - This method checks if the `_messager` attribute is set. If it is, it sends a ping request asynchronously to the Messager and returns the response.
448
+ - If the Messager is not set, it raises a RuntimeError.
449
+ """
345
450
  if self._messager is None:
346
451
  raise RuntimeError("Messager is not set")
347
452
  return await self._messager.ping.remote() # type:ignore
348
453
 
349
454
  async def generate_user_survey_response(self, survey: dict) -> str:
350
- """生成回答 —— 可重写
351
- 基于智能体的记忆和当前状态,生成对问卷调查的回答。
352
- Args:
353
- survey: 需要回答的问卷 dict
354
- Returns:
355
- str: 智能体的回答
455
+ """
456
+ Generate a response to a user survey based on the agent's memory and current state.
457
+
458
+ - **Args**:
459
+ - `survey` (`dict`): The survey that needs to be answered.
460
+
461
+ - **Returns**:
462
+ - `str`: The generated response from the agent.
463
+
464
+ - **Description**:
465
+ - Prepares a prompt for the Language Model (LLM) based on the provided survey.
466
+ - Constructs a dialog including system prompts, relevant memory context, and the survey question itself.
467
+ - Uses the LLM client to generate a response asynchronously.
468
+ - If the LLM client is not available, it returns a default message indicating unavailability.
469
+ - This method can be overridden by subclasses to customize survey response generation.
356
470
  """
357
471
  survey_prompt = process_survey_for_llm(survey)
358
472
  dialog = []
@@ -385,6 +499,19 @@ class Agent(ABC):
385
499
  return response # type:ignore
386
500
 
387
501
  async def _process_survey(self, survey: dict):
502
+ """
503
+ Process a survey by generating a response and recording it in Avro format and PostgreSQL.
504
+
505
+ - **Args**:
506
+ - `survey` (`dict`): The survey data that includes an ID and other relevant information.
507
+
508
+ - **Description**:
509
+ - Generates a survey response using `generate_user_survey_response`.
510
+ - Records the response with metadata (such as timestamp, survey ID, etc.) in Avro format and appends it to an Avro file if `_avro_file` is set.
511
+ - Writes the response and metadata into a PostgreSQL database asynchronously through `_pgsql_writer`, ensuring any previous write operation has completed.
512
+ - Sends a message through the Messager indicating user feedback has been processed.
513
+ - Handles asynchronous tasks and ensures thread-safe operations when writing to PostgreSQL.
514
+ """
388
515
  survey_response = await self.generate_user_survey_response(survey)
389
516
  _date_time = datetime.now(timezone.utc)
390
517
  # Avro
@@ -430,16 +557,26 @@ class Agent(ABC):
430
557
  _data_tuples
431
558
  )
432
559
  )
433
- await self.messager.send_message.remote(f"exps/{self._exp_id}/user_payback", {"count": 1})# type:ignore
560
+ await self.messager.send_message.remote( # type:ignore
561
+ f"exps/{self._exp_id}/user_payback", {"count": 1}
562
+ )
434
563
 
435
564
  async def generate_user_chat_response(self, question: str) -> str:
436
- """生成回答 —— 可重写
437
- 基于智能体的记忆和当前状态,生成对问题的回答。
438
- Args:
439
- question: 需要回答的问题
565
+ """
566
+ Generate a response to a user's chat question based on the agent's memory and current state.
440
567
 
441
- Returns:
442
- str: 智能体的回答
568
+ - **Args**:
569
+ - `question` (`str`): The question that needs to be answered.
570
+
571
+ - **Returns**:
572
+ - `str`: The generated response from the agent.
573
+
574
+ - **Description**:
575
+ - Prepares a prompt for the Language Model (LLM) with a system prompt to guide the response style.
576
+ - Constructs a dialog including relevant memory context and the user's question.
577
+ - Uses the LLM client to generate a concise and clear response asynchronously.
578
+ - If the LLM client is not available, it returns a default message indicating unavailability.
579
+ - This method can be overridden by subclasses to customize chat response generation.
443
580
  """
444
581
  dialog = []
445
582
 
@@ -471,6 +608,20 @@ class Agent(ABC):
471
608
  return response # type:ignore
472
609
 
473
610
  async def _process_interview(self, payload: dict):
611
+ """
612
+ Process an interview interaction by generating a response and recording it in Avro format and PostgreSQL.
613
+
614
+ - **Args**:
615
+ - `payload` (`dict`): The interview data containing the content of the user's message.
616
+
617
+ - **Description**:
618
+ - Logs the user's message as part of the interview process.
619
+ - Generates a response to the user's question using `generate_user_chat_response`.
620
+ - Records both the user's message and the generated response with metadata (such as timestamp, speaker, etc.) in Avro format and appends it to an Avro file if `_avro_file` is set.
621
+ - Writes the messages and metadata into a PostgreSQL database asynchronously through `_pgsql_writer`, ensuring any previous write operation has completed.
622
+ - Sends a message through the Messager indicating that user feedback has been processed.
623
+ - Handles asynchronous tasks and ensures thread-safe operations when writing to PostgreSQL.
624
+ """
474
625
  pg_list: list[tuple[dict, datetime]] = []
475
626
  auros: list[dict] = []
476
627
  _date_time = datetime.now(timezone.utc)
@@ -517,15 +668,43 @@ class Agent(ABC):
517
668
  _data
518
669
  )
519
670
  )
520
- await self.messager.send_message.remote(f"exps/{self._exp_id}/user_payback", {"count": 1})# type:ignore
671
+ await self.messager.send_message.remote( # type:ignore
672
+ f"exps/{self._exp_id}/user_payback", {"count": 1}
673
+ )
521
674
  print(f"Sent payback message to {self._exp_id}")
522
675
 
523
676
  async def process_agent_chat_response(self, payload: dict) -> str:
677
+ """
678
+ Log the reception of an agent chat response.
679
+
680
+ - **Args**:
681
+ - `payload` (`dict`): The chat response data received from another agent.
682
+
683
+ - **Returns**:
684
+ - `str`: A log message indicating the reception of the chat response.
685
+
686
+ - **Description**:
687
+ - Logs the receipt of a chat response from another agent.
688
+ - Returns a formatted string for logging purposes.
689
+ """
524
690
  resp = f"Agent {self._uuid} received agent chat response: {payload}"
525
691
  logger.info(resp)
526
692
  return resp
527
693
 
528
694
  async def _process_agent_chat(self, payload: dict):
695
+ """
696
+ Process a chat message received from another agent and record it.
697
+
698
+ - **Args**:
699
+ - `payload` (`dict`): The chat message data received from another agent.
700
+
701
+ - **Description**:
702
+ - Logs the incoming chat message from another agent.
703
+ - Prepares the chat message for storage in Avro format and PostgreSQL.
704
+ - Asynchronously logs the processing of the chat response using `process_agent_chat_response`.
705
+ - Writes the chat message and metadata into an Avro file if `_avro_file` is set.
706
+ - Ensures thread-safe operations when writing to PostgreSQL by waiting for any previous write task to complete before starting a new one.
707
+ """
529
708
  pg_list: list[tuple[dict, datetime]] = []
530
709
  auros: list[dict] = []
531
710
  _date_time = datetime.now(timezone.utc)
@@ -562,29 +741,90 @@ class Agent(ABC):
562
741
 
563
742
  # Callback functions for MQTT message
564
743
  async def handle_agent_chat_message(self, payload: dict):
565
- """处理收到的消息,识别发送者"""
744
+ """
745
+ Handle an incoming chat message from another agent.
746
+
747
+ - **Args**:
748
+ - `payload` (`dict`): The received message payload containing the chat data.
749
+
750
+ - **Description**:
751
+ - Logs receipt of a chat message from another agent.
752
+ - Delegates the processing of the chat message to `_process_agent_chat`.
753
+ - This method is typically used as a callback function for MQTT messages.
754
+ """
755
+ # 处理收到的消息,识别发送者
566
756
  # 从消息中解析发送者 ID 和消息内容
567
757
  logger.info(f"Agent {self._uuid} received agent chat message: {payload}")
568
758
  asyncio.create_task(self._process_agent_chat(payload))
569
759
 
570
760
  async def handle_user_chat_message(self, payload: dict):
571
- """处理收到的消息,识别发送者"""
761
+ """
762
+ Handle an incoming chat message from a user.
763
+
764
+ - **Args**:
765
+ - `payload` (`dict`): The received message payload containing the chat data.
766
+
767
+ - **Description**:
768
+ - Logs receipt of a chat message from a user.
769
+ - Delegates the processing of the interview (which includes generating a response) to `_process_interview`.
770
+ - This method is typically used as a callback function for MQTT messages.
771
+ """
772
+ # 处理收到的消息,识别发送者
572
773
  # 从消息中解析发送者 ID 和消息内容
573
774
  logger.info(f"Agent {self._uuid} received user chat message: {payload}")
574
775
  asyncio.create_task(self._process_interview(payload))
575
776
 
576
777
  async def handle_user_survey_message(self, payload: dict):
577
- """处理收到的消息,识别发送者"""
778
+ """
779
+ Handle an incoming survey message from a user.
780
+
781
+ - **Args**:
782
+ - `payload` (`dict`): The received message payload containing the survey data.
783
+
784
+ - **Description**:
785
+ - Logs receipt of a survey message from a user.
786
+ - Extracts the survey data from the payload and delegates its processing to `_process_survey`.
787
+ - This method is typically used as a callback function for MQTT messages.
788
+ """
789
+ # 处理收到的消息,识别发送者
578
790
  # 从消息中解析发送者 ID 和消息内容
579
791
  logger.info(f"Agent {self._uuid} received user survey message: {payload}")
580
792
  asyncio.create_task(self._process_survey(payload["data"]))
581
793
 
582
794
  async def handle_gather_message(self, payload: Any):
795
+ """
796
+ Placeholder for handling gather messages.
797
+
798
+ - **Args**:
799
+ - `payload` (`Any`): The received message payload.
800
+
801
+ - **Raises**:
802
+ - `NotImplementedError`: As this method is not implemented.
803
+
804
+ - **Description**:
805
+ - This method is intended to handle specific types of gather messages but has not been implemented yet.
806
+ """
583
807
  raise NotImplementedError
584
808
 
585
809
  # MQTT send message
586
810
  async def _send_message(self, to_agent_uuid: str, payload: dict, sub_topic: str):
587
- """通过 Messager 发送消息"""
811
+ """
812
+ Send a message to another agent through the Messager.
813
+
814
+ - **Args**:
815
+ - `to_agent_uuid` (`str`): The UUID of the recipient agent.
816
+ - `payload` (`dict`): The content of the message to send.
817
+ - `sub_topic` (`str`): The sub-topic for the MQTT topic structure.
818
+
819
+ - **Raises**:
820
+ - `RuntimeError`: If the Messager is not set.
821
+
822
+ - **Description**:
823
+ - Constructs the full MQTT topic based on the experiment ID, recipient UUID, and sub-topic.
824
+ - Sends the message asynchronously through the Messager.
825
+ - Used internally by other methods like `send_message_to_agent`.
826
+ """
827
+ # 通过 Messager 发送消息
588
828
  if self._messager is None:
589
829
  raise RuntimeError("Messager is not set")
590
830
  topic = f"exps/{self._exp_id}/agents/{to_agent_uuid}/{sub_topic}"
@@ -598,7 +838,25 @@ class Agent(ABC):
598
838
  async def send_message_to_agent(
599
839
  self, to_agent_uuid: str, content: str, type: str = "social"
600
840
  ):
601
- """通过 Messager 发送消息"""
841
+ """
842
+ Send a social or economy message to another agent.
843
+
844
+ - **Args**:
845
+ - `to_agent_uuid` (`str`): The UUID of the recipient agent.
846
+ - `content` (`str`): The content of the message to send.
847
+ - `type` (`str`, optional): The type of the message ("social" or "economy"). Defaults to "social".
848
+
849
+ - **Raises**:
850
+ - `RuntimeError`: If the Messager is not set.
851
+
852
+ - **Description**:
853
+ - Validates the message type and logs a warning if it's invalid.
854
+ - Prepares the message payload with necessary metadata such as sender ID, timestamp, etc.
855
+ - Sends the message asynchronously using `_send_message`.
856
+ - Optionally records the message in Avro format and PostgreSQL if it's a "social" type message.
857
+ - Ensures thread-safe operations when writing to PostgreSQL by waiting for any previous write task to complete before starting a new one.
858
+ """
859
+ # 通过 Messager 发送消息
602
860
  if self._messager is None:
603
861
  raise RuntimeError("Messager is not set")
604
862
  if type not in ["social", "economy"]:
@@ -648,15 +906,30 @@ class Agent(ABC):
648
906
  # Agent logic
649
907
  @abstractmethod
650
908
  async def forward(self) -> None:
651
- """智能体行为逻辑"""
909
+ """
910
+ Define the behavior logic of the agent.
911
+
912
+ - **Raises**:
913
+ - `NotImplementedError`: As this method must be implemented by subclasses.
914
+
915
+ - **Description**:
916
+ - This abstract method should contain the core logic for what the agent does at each step of its operation.
917
+ - It is intended to be overridden by subclasses to define specific behaviors.
918
+ """
919
+ # 智能体行为逻辑
652
920
  raise NotImplementedError
653
921
 
654
922
  async def run(self) -> None:
655
923
  """
656
- 统一的Agent执行入口
657
- 当_blocked为True时,不执行forward方法
924
+ Unified entry point for executing the agent's logic.
925
+
926
+ - **Description**:
927
+ - Checks if the `_messager` is set and sends a ping request to ensure communication is established.
928
+ - If the agent is not blocked (`_blocked` is False), it calls the `forward` method to execute the agent's behavior logic.
929
+ - Acts as the main control flow for the agent, coordinating when and how the agent performs its actions.
658
930
  """
659
931
  if self._messager is not None:
660
932
  await self._messager.ping.remote() # type:ignore
661
933
  if not self._blocked:
662
- await self.forward()
934
+ async with self.llm.semaphore:
935
+ await self.forward()
@@ -8,11 +8,39 @@ from pycityagent.economy import EconomyClient
8
8
  from pycityagent.message import Messager
9
9
  from pycityagent.memory import Memory
10
10
  import logging
11
+ import pycityproto.city.economy.v2.economy_pb2 as economyv2
11
12
 
12
13
  logger = logging.getLogger("pycityagent")
13
14
 
15
+ def calculate_inflation(prices):
16
+ # Make sure the length of price data is a multiple of 12
17
+ length = len(prices)
18
+ months_in_year = 12
19
+ full_years = length // months_in_year # Calculate number of complete years
20
+
21
+ # Remaining data not used in calculation
22
+ prices = prices[:full_years * months_in_year]
23
+
24
+ # Group by year, calculate average price for each year
25
+ annual_avg_prices = np.mean(np.reshape(prices, (-1, months_in_year)), axis=1)
26
+
27
+ # Calculate annual inflation rates
28
+ inflation_rates = []
29
+ for i in range(1, full_years):
30
+ inflation_rate = ((annual_avg_prices[i] - annual_avg_prices[i - 1]) / annual_avg_prices[i - 1]) * 100
31
+ inflation_rates.append(inflation_rate)
32
+
33
+ return inflation_rates
14
34
 
15
35
  class BankAgent(InstitutionAgent):
36
+ configurable_fields = ["time_diff"]
37
+ default_values = {
38
+ "time_diff": 30 * 24 * 60 * 60,
39
+ }
40
+ fields_description = {
41
+ "time_diff": "Time difference between each forward, day * hour * minute * second",
42
+ }
43
+
16
44
  def __init__(
17
45
  self,
18
46
  name: str,
@@ -53,12 +81,24 @@ class BankAgent(InstitutionAgent):
53
81
 
54
82
  async def forward(self):
55
83
  if await self.month_trigger():
56
- citizens = await self.memory.status.get("citizens")
57
- agents_forward = []
58
- if not np.all(np.array(agents_forward) > self.forward_times):
59
- return
60
- self.forward_times += 1
61
- for uuid in citizens:
62
- await self.send_message_to_agent(
63
- uuid, f"bank_forward@{self.forward_times}", "economy"
64
- )
84
+ interest_rate = await self.economy_client.get(self._agent_id, 'interest_rate')
85
+ citizens = await self.economy_client.get(self._agent_id, 'citizens')
86
+ for citizen in citizens:
87
+ wealth = self.economy_client.get(citizen, 'currency')
88
+ await self.economy_client.add_delta_value(citizen, 'currency', interest_rate*wealth)
89
+
90
+ nbs_id = await self.economy_client.get_org_entity_ids(economyv2.ORG_TYPE_NBS)
91
+ nbs_id = nbs_id[0]
92
+ prices = await self.economy_client.get(nbs_id, 'prices')
93
+ inflations = calculate_inflation(prices)
94
+ natural_interest_rate = 0.01
95
+ target_inflation = 0.02
96
+ if len(inflations) > 0:
97
+ # natural_unemployment_rate = 0.04
98
+ inflation_coeff, unemployment_coeff = 0.5, 0.5
99
+ tao = 1
100
+ avg_inflation = np.mean(inflations[-tao:])
101
+ interest_rate = natural_interest_rate + target_inflation + inflation_coeff * (avg_inflation - target_inflation)
102
+ else:
103
+ interest_rate = natural_interest_rate + target_inflation
104
+ await self.economy_client.update(self._agent_id, 'interest_rate', interest_rate)
@@ -14,5 +14,4 @@ __all__ = [
14
14
  "SocialBlock",
15
15
  "EconomyBlock",
16
16
  "OtherBlock",
17
- "LongTermDecisionBlock",
18
- ]
17
+ ]