pycityagent 2.0.0a65__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.
- 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
pycityagent/agent/agent_base.py
CHANGED
@@ -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,
|
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
|
73
|
-
messager (
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
|
657
|
-
|
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
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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)
|