pycityagent 2.0.0a47__cp310-cp310-macosx_11_0_arm64.whl → 2.0.0a49__cp310-cp310-macosx_11_0_arm64.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. pycityagent/__init__.py +3 -2
  2. pycityagent/agent.py +109 -4
  3. pycityagent/cityagent/__init__.py +20 -0
  4. pycityagent/cityagent/bankagent.py +54 -0
  5. pycityagent/cityagent/blocks/__init__.py +20 -0
  6. pycityagent/cityagent/blocks/cognition_block.py +304 -0
  7. pycityagent/cityagent/blocks/dispatcher.py +78 -0
  8. pycityagent/cityagent/blocks/economy_block.py +356 -0
  9. pycityagent/cityagent/blocks/mobility_block.py +258 -0
  10. pycityagent/cityagent/blocks/needs_block.py +305 -0
  11. pycityagent/cityagent/blocks/other_block.py +103 -0
  12. pycityagent/cityagent/blocks/plan_block.py +309 -0
  13. pycityagent/cityagent/blocks/social_block.py +345 -0
  14. pycityagent/cityagent/blocks/time_block.py +116 -0
  15. pycityagent/cityagent/blocks/utils.py +66 -0
  16. pycityagent/cityagent/firmagent.py +75 -0
  17. pycityagent/cityagent/governmentagent.py +60 -0
  18. pycityagent/cityagent/initial.py +98 -0
  19. pycityagent/cityagent/memory_config.py +202 -0
  20. pycityagent/cityagent/nbsagent.py +92 -0
  21. pycityagent/cityagent/societyagent.py +291 -0
  22. pycityagent/memory/memory.py +0 -18
  23. pycityagent/message/messager.py +6 -3
  24. pycityagent/simulation/agentgroup.py +123 -37
  25. pycityagent/simulation/simulation.py +311 -316
  26. pycityagent/workflow/block.py +66 -1
  27. pycityagent/workflow/tool.py +9 -4
  28. {pycityagent-2.0.0a47.dist-info → pycityagent-2.0.0a49.dist-info}/METADATA +2 -2
  29. {pycityagent-2.0.0a47.dist-info → pycityagent-2.0.0a49.dist-info}/RECORD +33 -14
  30. {pycityagent-2.0.0a47.dist-info → pycityagent-2.0.0a49.dist-info}/LICENSE +0 -0
  31. {pycityagent-2.0.0a47.dist-info → pycityagent-2.0.0a49.dist-info}/WHEEL +0 -0
  32. {pycityagent-2.0.0a47.dist-info → pycityagent-2.0.0a49.dist-info}/entry_points.txt +0 -0
  33. {pycityagent-2.0.0a47.dist-info → pycityagent-2.0.0a49.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,92 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ import numpy as np
5
+ from pycityagent import Simulator, InstitutionAgent
6
+ from pycityagent.llm.llm import LLM
7
+ from pycityagent.economy import EconomyClient
8
+ from pycityagent.message import Messager
9
+ from pycityagent.memory import Memory
10
+ from pycityagent.workflow.tool import ExportMlflowMetrics
11
+ import logging
12
+
13
+ logger = logging.getLogger("pycityagent")
14
+
15
+ class NBSAgent(InstitutionAgent):
16
+ export_metrics = ExportMlflowMetrics(log_batch_size=3)
17
+
18
+ def __init__(self,
19
+ name: str,
20
+ llm_client: Optional[LLM] = None,
21
+ simulator: Optional[Simulator] = None,
22
+ memory: Optional[Memory] = None,
23
+ economy_client: Optional[EconomyClient] = None,
24
+ messager: Optional[Messager] = None,
25
+ avro_file: Optional[dict] = None,
26
+ ) -> None:
27
+ super().__init__(name=name, llm_client=llm_client, simulator=simulator, memory=memory, economy_client=economy_client, messager=messager, avro_file=avro_file)
28
+ self.initailzed = False
29
+ self.last_time_trigger = None
30
+ self.time_diff = 30 * 24 * 60 * 60
31
+ self.forward_times = 0
32
+ self.num_labor_hours = 168
33
+ self.productivity_per_labor = 1
34
+ self.price = 1
35
+ async def month_trigger(self):
36
+ now_time = await self.simulator.get_time()
37
+ if self.last_time_trigger is None:
38
+ self.last_time_trigger = now_time
39
+ return False
40
+ if now_time - self.last_time_trigger >= self.time_diff:
41
+ self.last_time_trigger = now_time
42
+ return True
43
+ return False
44
+
45
+ async def gather_messages(self, agent_ids, content):
46
+ infos = await super().gather_messages(agent_ids, content)
47
+ return [info['content'] for info in infos]
48
+
49
+ async def forward(self):
50
+ if await self.month_trigger():
51
+ citizens = await self.memory.get("citizens")
52
+ while True:
53
+ agents_forward = await self.gather_messages(citizens, 'forward')
54
+ if np.all(np.array(agents_forward) > self.forward_times):
55
+ break
56
+ await asyncio.sleep(1)
57
+ work_propensity = await self.gather_messages(citizens, "work_propensity")
58
+ working_hours = np.mean(work_propensity)*self.num_labor_hours
59
+ firm_id = await self.memory.get("firm_id")
60
+ price = await self.economy_client.get(firm_id, "price")
61
+ prices = await self.economy_client.get(self._agent_id, "prices")
62
+ initial_price = prices[0]
63
+ nominal_gdp = working_hours*len(citizens)*self.productivity_per_labor*price
64
+ real_gdp = working_hours*len(citizens)*self.productivity_per_labor*initial_price
65
+ await self.economy_client.update(self._agent_id, 'nominal_gdp', [nominal_gdp], mode='merge')
66
+ await self.economy_client.update(self._agent_id, 'real_gdp', [real_gdp], mode='merge')
67
+ await self.economy_client.update(self._agent_id, 'working_hours', [working_hours], mode='merge')
68
+ await self.economy_client.update(self._agent_id, 'prices', [price], mode='merge')
69
+ depression = await self.gather_messages(citizens, 'depression')
70
+ depression = np.mean(depression)
71
+ await self.economy_client.update(self._agent_id, 'depression', [depression], mode='merge')
72
+ consumption_currency = await self.gather_messages(citizens, 'consumption_currency')
73
+ consumption_currency = np.mean(consumption_currency)
74
+ await self.economy_client.update(self._agent_id, 'consumption_currency', [consumption_currency], mode='merge')
75
+ income_currency = await self.gather_messages(citizens, 'income_currency')
76
+ income_currency = np.mean(income_currency)
77
+ await self.economy_client.update(self._agent_id, 'income_currency', [income_currency], mode='merge')
78
+ self.forward_times += 1
79
+ for uuid in citizens:
80
+ await self.send_message_to_agent(uuid, f"nbs_forward@{self.forward_times}")
81
+
82
+ metrics = {'nominal_gdp': nominal_gdp, 'working_hours': working_hours, 'price': price,
83
+ 'depression': depression, 'consumption': consumption_currency, 'income': income_currency}
84
+ for k, v in metrics.items():
85
+ await self.export_metrics(
86
+ metric={
87
+ "key": k,
88
+ "value": v,
89
+ "step": self.forward_times,
90
+ },
91
+ clear_cache=True,
92
+ )
@@ -0,0 +1,291 @@
1
+ import asyncio
2
+ import json
3
+ from typing import Optional
4
+ from pycityagent import Simulator, CitizenAgent
5
+ from pycityagent.agent import Agent
6
+ from pycityagent.llm.llm import LLM
7
+ from pycityagent.economy import EconomyClient
8
+ from pycityagent.message import Messager
9
+ from pycityagent.memory import Memory
10
+ from pycityagent.workflow.tool import UpdateWithSimulator
11
+ from pycityagent.workflow import Block
12
+ from .blocks import CognitionBlock, EconomyBlock, MobilityBlock, NeedsBlock, OtherBlock, PlanBlock, SocialBlock
13
+ from .blocks.economy_block import MonthPlanBlock
14
+ import logging
15
+
16
+ logger = logging.getLogger("pycityagent")
17
+
18
+ class PlanAndActionBlock(Block):
19
+ """主动工作流"""
20
+ longTermDecisionBlock: MonthPlanBlock
21
+ needsBlock: NeedsBlock
22
+ planBlock: PlanBlock
23
+ mobilityBlock: MobilityBlock
24
+ socialBlock: SocialBlock
25
+ economyBlock: EconomyBlock
26
+ otherBlock: OtherBlock
27
+
28
+ def __init__(self, agent: Agent, llm: LLM, memory: Memory, simulator: Simulator, economy_client: EconomyClient):
29
+ super().__init__(name="plan_and_action_block", llm=llm, memory=memory, simulator=simulator)
30
+ self._agent = agent
31
+ self.longTermDecisionBlock = MonthPlanBlock(llm=llm, memory=memory, simulator=simulator, economy_client=economy_client)
32
+ self.needsBlock = NeedsBlock(llm=llm, memory=memory, simulator=simulator)
33
+ self.planBlock = PlanBlock(llm=llm, memory=memory, simulator=simulator)
34
+ self.mobilityBlock = MobilityBlock(llm=llm, memory=memory, simulator=simulator)
35
+ self.socialBlock = SocialBlock(agent=self, llm=llm, memory=memory, simulator=simulator)
36
+ self.economyBlock = EconomyBlock(llm=llm, memory=memory, simulator=simulator, economy_client=economy_client)
37
+ self.otherBlock = OtherBlock(llm=llm, memory=memory)
38
+
39
+ async def check_and_update_step(self):
40
+ status = await self.memory.get('status')
41
+ if status == 2:
42
+ # 正在运动
43
+ logger.info("Agent is moving")
44
+ await asyncio.sleep(1)
45
+ return False
46
+
47
+ # 获取上一步信息
48
+ current_step = await self.memory.get("current_step")
49
+ if current_step['intention'] == "" or current_step['type'] == "":
50
+ # 没有上一步,直接返回
51
+ return True
52
+ time_now = int(await self.simulator.get_time())
53
+ step_start_time = current_step['start_time']
54
+ step_consumed_time = current_step['evaluation']['consumed_time']
55
+ time_end_plan = step_start_time + int(step_consumed_time)*60
56
+ if time_now >= time_end_plan:
57
+ # 上一步执行完成
58
+ current_plan = await self.memory.get("current_plan")
59
+ current_step['evaluation']['consumed_time'] = (time_now - step_start_time)/60
60
+ current_step_index = next((i for i, step in enumerate(current_plan["steps"]) if step['intention'] == current_step['intention'] and step['type'] == current_step['type']), None)
61
+ current_plan["steps"][current_step_index] = current_step
62
+ await self.memory.update("current_plan", current_plan)
63
+ if current_step_index is not None and current_step_index + 1 < len(current_plan["steps"]):
64
+ next_step = current_plan["steps"][current_step_index + 1]
65
+ await self.memory.update("current_step", next_step)
66
+ else:
67
+ # 标记计划完成
68
+ current_plan["completed"] = True
69
+ current_plan["end_time"] = await self.simulator.get_time(format_time=True)
70
+ await self.memory.update("current_plan", current_plan)
71
+ await self.memory.update("current_step", {"intention": "", "type": ""})
72
+ logger.info("Current plan execution completed.\n")
73
+ return True
74
+ # 上一步未执行完成
75
+ return False
76
+
77
+ async def forward(self):
78
+ # 与模拟器同步智能体的状态
79
+ await self._agent.update_with_sim()
80
+ # 检测上一步是否执行完成
81
+ if not await self.check_and_update_step():
82
+ return
83
+
84
+ # 长期决策
85
+ await self.longTermDecisionBlock.forward()
86
+
87
+ # 需求更新
88
+ time_now = await self.simulator.get_time(format_time=True)
89
+ logger.info(f"Current time: {time_now}")
90
+ await self.needsBlock.forward()
91
+ current_need = await self.memory.get("current_need")
92
+ logger.info(f"Current need: {current_need}")
93
+
94
+ # 计划生成
95
+ current_plan = await self.memory.get("current_plan")
96
+ if current_need != "none" and not current_plan:
97
+ await self.planBlock.forward()
98
+ current_plan = await self.memory.get("current_plan")
99
+ execution_context = await self.memory.get("execution_context")
100
+ current_step = await self.memory.get("current_step")
101
+ # 检查 current_step 是否有效(不为空)
102
+ if current_step and current_step.get("type") and current_step.get("intention"):
103
+ step_type = current_step.get("type")
104
+ position = await self.memory.get("position")
105
+ if 'aoi_position' in position:
106
+ current_step['position'] = position['aoi_position']['aoi_id']
107
+ current_step['start_time'] = int(await self.simulator.get_time())
108
+ logger.info(f"Executing step: {current_step['intention']} - Type: {step_type}")
109
+ result = None
110
+ if step_type == "mobility":
111
+ result = await self.mobilityBlock.forward(current_step, execution_context)
112
+ elif step_type == "social":
113
+ result = await self.socialBlock.forward(current_step, execution_context)
114
+ elif step_type == "economy":
115
+ result = await self.economyBlock.forward(current_step, execution_context)
116
+ elif step_type == "other":
117
+ result = await self.otherBlock.forward(current_step, execution_context)
118
+ if result != None:
119
+ logger.info(f"Execution result: {result}")
120
+ current_step['evaluation'] = result
121
+
122
+ # 更新current_step信息,plan信息以及execution_context信息
123
+ current_step_index = next((i for i, step in enumerate(current_plan["steps"]) if step['intention'] == current_step['intention'] and step['type'] == current_step['type']), None)
124
+ current_plan["steps"][current_step_index] = current_step
125
+ await self.memory.update("current_step", current_step)
126
+ await self.memory.update("current_plan", current_plan)
127
+ await self.memory.update("execution_context", execution_context)
128
+
129
+ class MindBlock(Block):
130
+ """认知工作流"""
131
+ cognitionBlock: CognitionBlock
132
+
133
+ def __init__(self, llm: LLM, memory: Memory, simulator: Simulator):
134
+ super().__init__(name="mind_block", llm=llm, memory=memory, simulator=simulator)
135
+ self.cognitionBlock = CognitionBlock(llm=llm, memory=memory, simulator=simulator)
136
+
137
+ async def forward(self):
138
+ await self.cognitionBlock.forward()
139
+
140
+ class SocietyAgent(CitizenAgent):
141
+ mindBlock: MindBlock
142
+ planAndActionBlock: PlanAndActionBlock
143
+ update_with_sim = UpdateWithSimulator()
144
+
145
+ def __init__(self,
146
+ name: str,
147
+ llm_client: Optional[LLM] = None,
148
+ simulator: Optional[Simulator] = None,
149
+ memory: Optional[Memory] = None,
150
+ economy_client: Optional[EconomyClient] = None,
151
+ ) -> None:
152
+ super().__init__(name=name, llm_client=llm_client, simulator=simulator, memory=memory, economy_client=economy_client)
153
+ self.mindBlock = MindBlock(llm=self._llm_client, memory=self._memory, simulator=self._simulator)
154
+ self.planAndActionBlock = PlanAndActionBlock(agent=self, llm=self._llm_client, memory=self._memory, simulator=self._simulator, economy_client=self._economy_client)
155
+ self.step_count = -1
156
+
157
+ # Main workflow
158
+ async def forward(self):
159
+ logger.info(f"Agent {self._uuid} forward")
160
+ self.step_count += 1
161
+ # 多工作流并发执行
162
+ task_list = [
163
+ asyncio.create_task(self.mindBlock.forward()),
164
+ asyncio.create_task(self.planAndActionBlock.forward()),
165
+ ]
166
+ await asyncio.gather(*task_list)
167
+
168
+ async def process_agent_chat_response(self, payload: dict) -> str:
169
+ if payload['type'] == 'social':
170
+ resp = f"Agent {self._uuid} received agent chat response: {payload}"
171
+ logger.info(resp)
172
+ try:
173
+ # Extract basic info
174
+ sender_id = payload.get("from")
175
+ if not sender_id:
176
+ return ""
177
+
178
+ raw_content = payload.get("content", "")
179
+
180
+ # Parse message content
181
+ try:
182
+ message_data = json.loads(raw_content)
183
+ content = message_data["content"]
184
+ propagation_count = message_data.get("propagation_count", 1)
185
+ except (json.JSONDecodeError, TypeError, KeyError):
186
+ content = raw_content
187
+ propagation_count = 1
188
+
189
+ if not content:
190
+ return ""
191
+
192
+ # Get chat histories and ensure proper format
193
+ chat_histories = await self._memory.get("chat_histories") or {}
194
+ if not isinstance(chat_histories, dict):
195
+ chat_histories = {}
196
+
197
+ # Update chat history with received message
198
+ if sender_id not in chat_histories:
199
+ chat_histories[sender_id] = ""
200
+ if chat_histories[sender_id]:
201
+ chat_histories[sender_id] += ","
202
+ chat_histories[sender_id] += f"them: {content}"
203
+
204
+ # Check propagation limit
205
+ if propagation_count > 5:
206
+ await self._memory.update("chat_histories", chat_histories)
207
+ logger.info(f"Message propagation limit reached ({propagation_count} > 5), stopping propagation")
208
+ return ""
209
+
210
+ # Get relationship score
211
+ relationships = await self._memory.get("relationships") or {}
212
+ relationship_score = relationships.get(sender_id, 50)
213
+
214
+ # Decision prompt
215
+ should_respond_prompt = f"""Based on:
216
+ - Received message: "{content}"
217
+ - Our relationship score: {relationship_score}/100
218
+ - My profile: {{
219
+ "gender": "{await self._memory.get("gender") or ""}",
220
+ "education": "{await self._memory.get("education") or ""}",
221
+ "personality": "{await self._memory.get("personality") or ""}",
222
+ "occupation": "{await self._memory.get("occupation") or ""}"
223
+ }}
224
+ - Recent chat history: {chat_histories.get(sender_id, "")}
225
+
226
+ Should I respond to this message? Consider:
227
+ 1. Is this a message that needs/deserves a response?
228
+ 2. Would it be natural for someone with my personality to respond?
229
+ 3. Is our relationship close enough to warrant a response?
230
+
231
+ Answer only YES or NO."""
232
+
233
+ should_respond = await self._llm_client.atext_request([
234
+ {"role": "system", "content": "You are helping decide whether to respond to a message."},
235
+ {"role": "user", "content": should_respond_prompt}
236
+ ])
237
+
238
+ if should_respond.strip().upper() != "YES":
239
+ await self._memory.update("chat_histories", chat_histories)
240
+ return ""
241
+
242
+ response_prompt = f"""Based on:
243
+ - Received message: "{content}"
244
+ - Our relationship score: {relationship_score}/100
245
+ - My profile: {{
246
+ "gender": "{await self._memory.get("gender") or ""}",
247
+ "education": "{await self._memory.get("education") or ""}",
248
+ "personality": "{await self._memory.get("personality") or ""}",
249
+ "occupation": "{await self._memory.get("occupation") or ""}"
250
+ }}
251
+ - Recent chat history: {chat_histories.get(sender_id, "")}
252
+
253
+ Generate an appropriate response that:
254
+ 1. Matches my personality and background
255
+ 2. Maintains natural conversation flow
256
+ 3. Is concise (under 100 characters)
257
+ 4. Reflects our relationship level
258
+
259
+ Response should be ONLY the message text, no explanations."""
260
+
261
+ response = await self._llm_client.atext_request([
262
+ {"role": "system", "content": "You are helping generate a chat response."},
263
+ {"role": "user", "content": response_prompt}
264
+ ])
265
+
266
+ if response:
267
+ # Update chat history with response
268
+ chat_histories[sender_id] += f",me: {response}"
269
+ await self._memory.update("chat_histories", chat_histories)
270
+
271
+ # Send response
272
+ serialized_response = json.dumps({
273
+ "content": response,
274
+ "propagation_count": propagation_count + 1
275
+ }, ensure_ascii=False)
276
+ await self.send_message_to_agent(sender_id, serialized_response)
277
+ logger.info('sender_id',sender_id)
278
+ logger.info('message',serialized_response)
279
+ return response
280
+
281
+ except Exception as e:
282
+ logger.warning(f"Error in process_agent_chat_response: {str(e)}")
283
+ return ""
284
+ else:
285
+ content = payload['content']
286
+ key, value = content.split("@")
287
+ if '.' in value:
288
+ value = float(value)
289
+ else:
290
+ value = int(value)
291
+ await self.memory.update(key, value)
@@ -395,24 +395,6 @@ class Memory:
395
395
  if _snapshot:
396
396
  await _mem.load(snapshots=_snapshot, reset_memory=reset_memory)
397
397
 
398
- # async def add(self, content: str, metadata: Optional[dict] = None) -> None:
399
- # """添加新的记忆
400
-
401
- # Args:
402
- # content: 记忆内容
403
- # metadata: 相关元数据,如时间、地点等
404
- # """
405
- # embedding = await self.embedding_model.aembed_query(content)
406
- # self.memories.append(
407
- # {
408
- # "content": content,
409
- # "metadata": metadata or {},
410
- # "timestamp": datetime.now(),
411
- # "embedding": embedding,
412
- # }
413
- # )
414
- # self.embeddings.append(embedding)
415
-
416
398
  @lock_decorator
417
399
  async def search(
418
400
  self, query: str, top_k: int = 3, filter: Optional[dict] = None
@@ -25,6 +25,9 @@ class Messager:
25
25
  async def __aexit__(self, exc_type, exc_value, traceback):
26
26
  await self.stop()
27
27
 
28
+ async def ping(self):
29
+ await self.client.publish(topic="ping", payload="ping", qos=1)
30
+
28
31
  async def connect(self):
29
32
  for i in range(3):
30
33
  try:
@@ -43,14 +46,14 @@ class Messager:
43
46
  self.connected = False
44
47
  logger.info("Disconnected from MQTT Broker")
45
48
 
46
- def is_connected(self):
49
+ async def is_connected(self):
47
50
  """检查是否成功连接到 Broker"""
48
51
  return self.connected
49
52
 
50
53
  async def subscribe(
51
54
  self, topics: Union[str, List[str]], agents: Union[Any, List[Any]]
52
55
  ):
53
- if not self.is_connected():
56
+ if not await self.is_connected():
54
57
  logger.error(
55
58
  f"Cannot subscribe to {topics} because not connected to the Broker."
56
59
  )
@@ -81,7 +84,7 @@ class Messager:
81
84
 
82
85
  async def start_listening(self):
83
86
  """启动消息监听任务"""
84
- if self.is_connected():
87
+ if await self.is_connected():
85
88
  self.receive_messages_task = asyncio.create_task(self.receive_messages())
86
89
  else:
87
90
  logger.error("Cannot start listening because not connected to the Broker.")