pycityagent 2.0.0a18__py3-none-any.whl → 2.0.0a19__py3-none-any.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.py CHANGED
@@ -1,30 +1,28 @@
1
1
  """智能体模板类及其定义"""
2
2
 
3
- from abc import ABC, abstractmethod
4
3
  import asyncio
5
- from uuid import UUID
6
- from copy import deepcopy
7
- from datetime import datetime
8
- from enum import Enum
9
4
  import logging
10
5
  import random
11
6
  import uuid
12
- from typing import Dict, List, Optional,Any
7
+ from abc import ABC, abstractmethod
8
+ from copy import deepcopy
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional
12
+ from uuid import UUID
13
13
 
14
14
  import fastavro
15
-
16
- from pycityagent.environment.sim.person_service import PersonService
17
15
  from mosstool.util.format_converter import dict2pb
18
16
  from pycityproto.city.person.v2 import person_pb2 as person_pb2
19
- from pycityagent.utils import process_survey_for_llm
20
-
21
- from pycityagent.message.messager import Messager
22
- from pycityagent.utils import SURVEY_SCHEMA, DIALOG_SCHEMA
23
17
 
24
18
  from .economy import EconomyClient
25
19
  from .environment import Simulator
20
+ from .environment.sim.person_service import PersonService
26
21
  from .llm import LLM
27
22
  from .memory import Memory
23
+ from .message.messager import Messager
24
+ from .metrics import MlflowClient
25
+ from .utils import DIALOG_SCHEMA, SURVEY_SCHEMA, process_survey_for_llm
28
26
 
29
27
  logger = logging.getLogger("pycityagent")
30
28
 
@@ -55,6 +53,7 @@ class Agent(ABC):
55
53
  economy_client: Optional[EconomyClient] = None,
56
54
  messager: Optional[Messager] = None,
57
55
  simulator: Optional[Simulator] = None,
56
+ mlflow_client: Optional[MlflowClient] = None,
58
57
  memory: Optional[Memory] = None,
59
58
  avro_file: Optional[Dict[str, str]] = None,
60
59
  ) -> None:
@@ -68,6 +67,7 @@ class Agent(ABC):
68
67
  economy_client (EconomyClient): The `EconomySim` client. Defaults to None.
69
68
  messager (Messager, optional): The messager object. Defaults to None.
70
69
  simulator (Simulator, optional): The simulator object. Defaults to None.
70
+ mlflow_client (MlflowClient, optional): The Mlflow object. Defaults to None.
71
71
  memory (Memory, optional): The memory of the agent. Defaults to None.
72
72
  avro_file (Dict[str, str], optional): The avro file of the agent. Defaults to None.
73
73
  """
@@ -78,6 +78,7 @@ class Agent(ABC):
78
78
  self._economy_client = economy_client
79
79
  self._messager = messager
80
80
  self._simulator = simulator
81
+ self._mlflow_client = mlflow_client
81
82
  self._memory = memory
82
83
  self._exp_id = -1
83
84
  self._agent_id = -1
@@ -112,6 +113,12 @@ class Agent(ABC):
112
113
  """
113
114
  self._simulator = simulator
114
115
 
116
+ def set_mlflow_client(self, mlflow_client: MlflowClient):
117
+ """
118
+ Set the mlflow_client of the agent.
119
+ """
120
+ self._mlflow_client = mlflow_client
121
+
115
122
  def set_economy_client(self, economy_client: EconomyClient):
116
123
  """
117
124
  Set the economy_client of the agent.
@@ -164,6 +171,15 @@ class Agent(ABC):
164
171
  )
165
172
  return self._economy_client
166
173
 
174
+ @property
175
+ def mlflow_client(self):
176
+ """The Agent's MlflowClient"""
177
+ if self._mlflow_client is None:
178
+ raise RuntimeError(
179
+ f"MlflowClient access before assignment, please `set_mlflow_client` first!"
180
+ )
181
+ return self._mlflow_client
182
+
167
183
  @property
168
184
  def memory(self):
169
185
  """The Agent's Memory"""
@@ -218,19 +234,21 @@ class Agent(ABC):
218
234
  response = await self._llm_client.atext_request(dialog) # type:ignore
219
235
 
220
236
  return response # type:ignore
221
-
237
+
222
238
  async def _process_survey(self, survey: dict):
223
239
  survey_response = await self.generate_user_survey_response(survey)
224
240
  if self._avro_file is None:
225
241
  return
226
- response_to_avro = [{
227
- "id": self._uuid,
228
- "day": await self.simulator.get_simulator_day(),
229
- "t": await self.simulator.get_simulator_second_from_start_of_day(),
230
- "survey_id": survey["id"],
231
- "result": survey_response,
232
- "created_at": int(datetime.now().timestamp() * 1000),
233
- }]
242
+ response_to_avro = [
243
+ {
244
+ "id": self._uuid,
245
+ "day": await self.simulator.get_simulator_day(),
246
+ "t": await self.simulator.get_simulator_second_from_start_of_day(),
247
+ "survey_id": survey["id"],
248
+ "result": survey_response,
249
+ "created_at": int(datetime.now().timestamp() * 1000),
250
+ }
251
+ ]
234
252
  with open(self._avro_file["survey"], "a+b") as f:
235
253
  fastavro.writer(f, SURVEY_SCHEMA, response_to_avro, codec="snappy")
236
254
 
@@ -270,28 +288,32 @@ class Agent(ABC):
270
288
  response = await self._llm_client.atext_request(dialog) # type:ignore
271
289
 
272
290
  return response # type:ignore
273
-
291
+
274
292
  async def _process_interview(self, payload: dict):
275
- auros = [{
276
- "id": self._uuid,
277
- "day": await self.simulator.get_simulator_day(),
278
- "t": await self.simulator.get_simulator_second_from_start_of_day(),
279
- "type": 2,
280
- "speaker": "user",
281
- "content": payload["content"],
282
- "created_at": int(datetime.now().timestamp() * 1000),
283
- }]
293
+ auros = [
294
+ {
295
+ "id": self._uuid,
296
+ "day": await self.simulator.get_simulator_day(),
297
+ "t": await self.simulator.get_simulator_second_from_start_of_day(),
298
+ "type": 2,
299
+ "speaker": "user",
300
+ "content": payload["content"],
301
+ "created_at": int(datetime.now().timestamp() * 1000),
302
+ }
303
+ ]
284
304
  question = payload["content"]
285
305
  response = await self.generate_user_chat_response(question)
286
- auros.append({
287
- "id": self._uuid,
288
- "day": await self.simulator.get_simulator_day(),
289
- "t": await self.simulator.get_simulator_second_from_start_of_day(),
290
- "type": 2,
291
- "speaker": "",
292
- "content": response,
293
- "created_at": int(datetime.now().timestamp() * 1000),
294
- })
306
+ auros.append(
307
+ {
308
+ "id": self._uuid,
309
+ "day": await self.simulator.get_simulator_day(),
310
+ "t": await self.simulator.get_simulator_second_from_start_of_day(),
311
+ "type": 2,
312
+ "speaker": "",
313
+ "content": response,
314
+ "created_at": int(datetime.now().timestamp() * 1000),
315
+ }
316
+ )
295
317
  if self._avro_file is None:
296
318
  return
297
319
  with open(self._avro_file["dialog"], "a+b") as f:
@@ -303,15 +325,17 @@ class Agent(ABC):
303
325
  return resp
304
326
 
305
327
  async def _process_agent_chat(self, payload: dict):
306
- auros = [{
307
- "id": self._uuid,
308
- "day": payload["day"],
309
- "t": payload["t"],
310
- "type": 1,
311
- "speaker": payload["from"],
312
- "content": payload["content"],
313
- "created_at": int(datetime.now().timestamp() * 1000),
314
- }]
328
+ auros = [
329
+ {
330
+ "id": self._uuid,
331
+ "day": payload["day"],
332
+ "t": payload["t"],
333
+ "type": 1,
334
+ "speaker": payload["from"],
335
+ "content": payload["content"],
336
+ "created_at": int(datetime.now().timestamp() * 1000),
337
+ }
338
+ ]
315
339
  asyncio.create_task(self.process_agent_chat_response(payload))
316
340
  if self._avro_file is None:
317
341
  return
@@ -341,18 +365,14 @@ class Agent(ABC):
341
365
  raise NotImplementedError
342
366
 
343
367
  # MQTT send message
344
- async def _send_message(
345
- self, to_agent_uuid: str, payload: dict, sub_topic: str
346
- ):
368
+ async def _send_message(self, to_agent_uuid: str, payload: dict, sub_topic: str):
347
369
  """通过 Messager 发送消息"""
348
370
  if self._messager is None:
349
371
  raise RuntimeError("Messager is not set")
350
372
  topic = f"exps/{self._exp_id}/agents/{to_agent_uuid}/{sub_topic}"
351
373
  await self._messager.send_message(topic, payload)
352
374
 
353
- async def send_message_to_agent(
354
- self, to_agent_uuid: str, content: str
355
- ):
375
+ async def send_message_to_agent(self, to_agent_uuid: str, content: str):
356
376
  """通过 Messager 发送消息"""
357
377
  if self._messager is None:
358
378
  raise RuntimeError("Messager is not set")
@@ -364,15 +384,17 @@ class Agent(ABC):
364
384
  "t": await self.simulator.get_simulator_second_from_start_of_day(),
365
385
  }
366
386
  await self._send_message(to_agent_uuid, payload, "agent-chat")
367
- auros = [{
368
- "id": self._uuid,
369
- "day": await self.simulator.get_simulator_day(),
370
- "t": await self.simulator.get_simulator_second_from_start_of_day(),
371
- "type": 1,
372
- "speaker": self._uuid,
373
- "content": content,
374
- "created_at": int(datetime.now().timestamp() * 1000),
375
- }]
387
+ auros = [
388
+ {
389
+ "id": self._uuid,
390
+ "day": await self.simulator.get_simulator_day(),
391
+ "t": await self.simulator.get_simulator_second_from_start_of_day(),
392
+ "type": 1,
393
+ "speaker": self._uuid,
394
+ "content": content,
395
+ "created_at": int(datetime.now().timestamp() * 1000),
396
+ }
397
+ ]
376
398
  if self._avro_file is None:
377
399
  return
378
400
  with open(self._avro_file["dialog"], "a+b") as f:
@@ -403,20 +425,22 @@ class CitizenAgent(Agent):
403
425
  name: str,
404
426
  llm_client: Optional[LLM] = None,
405
427
  simulator: Optional[Simulator] = None,
428
+ mlflow_client: Optional[MlflowClient] = None,
406
429
  memory: Optional[Memory] = None,
407
430
  economy_client: Optional[EconomyClient] = None,
408
431
  messager: Optional[Messager] = None,
409
432
  avro_file: Optional[dict] = None,
410
433
  ) -> None:
411
434
  super().__init__(
412
- name,
413
- AgentType.Citizen,
414
- llm_client,
415
- economy_client,
416
- messager,
417
- simulator,
418
- memory,
419
- avro_file,
435
+ name=name,
436
+ type=AgentType.Citizen,
437
+ llm_client=llm_client,
438
+ economy_client=economy_client,
439
+ messager=messager,
440
+ simulator=simulator,
441
+ mlflow_client=mlflow_client,
442
+ memory=memory,
443
+ avro_file=avro_file,
420
444
  )
421
445
 
422
446
  async def bind_to_simulator(self):
@@ -464,9 +488,7 @@ class CitizenAgent(Agent):
464
488
  )
465
489
  person_id = resp["person_id"]
466
490
  await memory.update("id", person_id, protect_llm_read_only_fields=False)
467
- logger.debug(
468
- f"Binding to Person `{person_id}` just added to Simulator"
469
- )
491
+ logger.debug(f"Binding to Person `{person_id}` just added to Simulator")
470
492
  # 防止模拟器还没有到prepare阶段导致get_person出错
471
493
  self._has_bound_to_simulator = True
472
494
  self._agent_id = person_id
@@ -517,24 +539,26 @@ class InstitutionAgent(Agent):
517
539
  name: str,
518
540
  llm_client: Optional[LLM] = None,
519
541
  simulator: Optional[Simulator] = None,
542
+ mlflow_client: Optional[MlflowClient] = None,
520
543
  memory: Optional[Memory] = None,
521
544
  economy_client: Optional[EconomyClient] = None,
522
545
  messager: Optional[Messager] = None,
523
546
  avro_file: Optional[dict] = None,
524
547
  ) -> None:
525
548
  super().__init__(
526
- name,
527
- AgentType.Institution,
528
- llm_client,
529
- economy_client,
530
- messager,
531
- simulator,
532
- memory,
533
- avro_file,
549
+ name=name,
550
+ type=AgentType.Institution,
551
+ llm_client=llm_client,
552
+ economy_client=economy_client,
553
+ mlflow_client=mlflow_client,
554
+ messager=messager,
555
+ simulator=simulator,
556
+ memory=memory,
557
+ avro_file=avro_file,
534
558
  )
535
559
  # 添加响应收集器
536
560
  self._gather_responses: Dict[str, asyncio.Future] = {}
537
-
561
+
538
562
  async def bind_to_simulator(self):
539
563
  await self._bind_to_economy()
540
564
 
@@ -624,22 +648,24 @@ class InstitutionAgent(Agent):
624
648
  """处理收到的消息,识别发送者"""
625
649
  content = payload["content"]
626
650
  sender_id = payload["from"]
627
-
651
+
628
652
  # 将响应存储到对应的Future中
629
653
  response_key = str(sender_id)
630
654
  if response_key in self._gather_responses:
631
- self._gather_responses[response_key].set_result({
632
- "from": sender_id,
633
- "content": content,
634
- })
655
+ self._gather_responses[response_key].set_result(
656
+ {
657
+ "from": sender_id,
658
+ "content": content,
659
+ }
660
+ )
635
661
 
636
662
  async def gather_messages(self, agent_uuids: list[str], target: str) -> List[dict]:
637
663
  """从多个智能体收集消息
638
-
664
+
639
665
  Args:
640
666
  agent_uuids: 目标智能体UUID列表
641
667
  target: 要收集的信息类型
642
-
668
+
643
669
  Returns:
644
670
  List[dict]: 收集到的所有响应
645
671
  """
@@ -648,7 +674,7 @@ class InstitutionAgent(Agent):
648
674
  for agent_uuid in agent_uuids:
649
675
  futures[agent_uuid] = asyncio.Future()
650
676
  self._gather_responses[agent_uuid] = futures[agent_uuid]
651
-
677
+
652
678
  # 发送gather请求
653
679
  payload = {
654
680
  "from": self._uuid,
@@ -656,7 +682,7 @@ class InstitutionAgent(Agent):
656
682
  }
657
683
  for agent_uuid in agent_uuids:
658
684
  await self._send_message(agent_uuid, payload, "gather")
659
-
685
+
660
686
  try:
661
687
  # 等待所有响应
662
688
  responses = await asyncio.gather(*futures.values())
@@ -308,11 +308,11 @@ class EconomyClient:
308
308
  # current agent ids and org ids
309
309
  return (list(response.agent_ids), list(response.org_ids))
310
310
 
311
- async def get_org_entity_ids(self, org_type: economyv2.OrgType)->list[int]:
311
+ async def get_org_entity_ids(self, org_type: economyv2.OrgType) -> list[int]:
312
312
  request = org_service.GetOrgEntityIdsRequest(
313
313
  type=org_type,
314
314
  )
315
315
  response: org_service.GetOrgEntityIdsResponse = (
316
316
  await self._aio_stub.GetOrgEntityIds(request)
317
317
  )
318
- return list(response.org_ids)
318
+ return list(response.org_ids)
@@ -5,7 +5,7 @@ import logging
5
5
  import os
6
6
  from collections.abc import Sequence
7
7
  from datetime import datetime, timedelta
8
- from typing import Any, Optional, Tuple, Union, cast
8
+ from typing import Any, Optional, Union, cast
9
9
 
10
10
  from mosstool.type import TripMode
11
11
  from mosstool.util.format_converter import coll2pb
@@ -28,7 +28,7 @@ class Simulator:
28
28
  - Simulator Class
29
29
  """
30
30
 
31
- def __init__(self, config, secure: bool = False) -> None:
31
+ def __init__(self, config:dict, secure: bool = False) -> None:
32
32
  self.config = config
33
33
  """
34
34
  - 模拟器配置
@@ -0,0 +1,5 @@
1
+ from .mlflow_client import MlflowClient
2
+
3
+ __all__ = [
4
+ "MlflowClient",
5
+ ]
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import uuid
5
+ from collections.abc import Sequence
6
+ from typing import Any, Optional, Union
7
+
8
+ import mlflow
9
+ from mlflow.entities import (Dataset, DatasetInput, Document, Experiment,
10
+ ExperimentTag, FileInfo, InputTag, LifecycleStage,
11
+ LiveSpan, Metric, NoOpSpan, Param, Run, RunData,
12
+ RunInfo, RunInputs, RunStatus, RunTag, SourceType,
13
+ Span, SpanEvent, SpanStatus, SpanStatusCode,
14
+ SpanType, Trace, TraceData, TraceInfo, ViewType)
15
+
16
+ from ..utils.decorators import lock_decorator
17
+
18
+ logger = logging.getLogger("mlflow")
19
+
20
+
21
+ class MlflowClient:
22
+ """
23
+ - Mlflow client
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ config: dict,
29
+ mlflow_run_name: Optional[str] = None,
30
+ experiment_name: Optional[str] = None,
31
+ experiment_description: Optional[str] = None,
32
+ experiment_tags: Optional[dict[str, Any]] = None,
33
+ ) -> None:
34
+ os.environ["MLFLOW_TRACKING_USERNAME"] = config.get("username", None)
35
+ os.environ["MLFLOW_TRACKING_PASSWORD"] = config.get("password", None)
36
+ self._mlflow_uri = uri = config["mlflow_uri"]
37
+ self._client = client = mlflow.MlflowClient(tracking_uri=uri)
38
+ self._run_uuid = run_uuid = str(uuid.uuid4())
39
+ self._lock = asyncio.Lock()
40
+ # run name
41
+ if mlflow_run_name is None:
42
+ mlflow_run_name = f"exp_{run_uuid}"
43
+
44
+ # exp name
45
+ if experiment_name is None:
46
+ experiment_name = f"run_{run_uuid}"
47
+
48
+ # tags
49
+ if experiment_tags is None:
50
+ experiment_tags = {}
51
+ if experiment_description is not None:
52
+ experiment_tags["mlflow.note.content"] = experiment_description
53
+
54
+ try:
55
+ self._experiment_id = experiment_id = client.create_experiment(
56
+ name=experiment_name,
57
+ tags=experiment_tags,
58
+ )
59
+ except Exception as e:
60
+ experiment = client.get_experiment_by_name(experiment_name)
61
+ if experiment is None:
62
+ raise e
63
+ self._experiment_id = experiment_id = experiment.experiment_id
64
+
65
+ self._run = run = client.create_run(
66
+ experiment_id=experiment_id, run_name=mlflow_run_name
67
+ )
68
+ self._run_id = run.info.run_id
69
+
70
+ @property
71
+ def client(
72
+ self,
73
+ ) -> mlflow.MlflowClient:
74
+ return self._client
75
+
76
+ @property
77
+ def run_id(
78
+ self,
79
+ ) -> str:
80
+ return self._run_id
81
+
82
+ @lock_decorator
83
+ async def log_batch(
84
+ self,
85
+ metrics: Sequence[Metric] = (),
86
+ params: Sequence[Param] = (),
87
+ tags: Sequence[RunTag] = (),
88
+ ):
89
+ self.client.log_batch(
90
+ run_id=self.run_id, metrics=metrics, params=params, tags=tags
91
+ )
92
+
93
+ @lock_decorator
94
+ async def log_metric(
95
+ self,
96
+ key: str,
97
+ value: float,
98
+ step: Optional[int] = None,
99
+ timestamp: Optional[int] = None,
100
+ ):
101
+ if timestamp is not None:
102
+ timestamp = int(timestamp)
103
+ self.client.log_metric(
104
+ run_id=self.run_id,
105
+ key=key,
106
+ value=value,
107
+ timestamp=timestamp,
108
+ step=step,
109
+ )
File without changes
@@ -1,34 +1,51 @@
1
1
  import asyncio
2
- from datetime import datetime
3
2
  import json
4
3
  import logging
5
- from pathlib import Path
4
+ import time
6
5
  import uuid
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from uuid import UUID
10
+
7
11
  import fastavro
8
12
  import ray
9
- from uuid import UUID
10
- from pycityagent.agent import Agent, CitizenAgent, InstitutionAgent
11
- from pycityagent.economy.econ_client import EconomyClient
12
- from pycityagent.environment.simulator import Simulator
13
- from pycityagent.llm.llm import LLM
14
- from pycityagent.llm.llmconfig import LLMConfig
15
- from pycityagent.message import Messager
16
- from pycityagent.utils import STATUS_SCHEMA, PROFILE_SCHEMA, DIALOG_SCHEMA, SURVEY_SCHEMA, INSTITUTION_STATUS_SCHEMA
17
- from typing import Any
13
+
14
+ from ..agent import Agent, CitizenAgent, InstitutionAgent
15
+ from ..economy.econ_client import EconomyClient
16
+ from ..environment.simulator import Simulator
17
+ from ..llm.llm import LLM
18
+ from ..llm.llmconfig import LLMConfig
19
+ from ..message import Messager
20
+ from ..metrics import MlflowClient
21
+ from ..utils import (DIALOG_SCHEMA, INSTITUTION_STATUS_SCHEMA, PROFILE_SCHEMA,
22
+ STATUS_SCHEMA, SURVEY_SCHEMA)
18
23
 
19
24
  logger = logging.getLogger("pycityagent")
20
25
 
26
+
21
27
  @ray.remote
22
28
  class AgentGroup:
23
- def __init__(self, agents: list[Agent], config: dict, exp_id: str|UUID, enable_avro: bool, avro_path: Path, logging_level: int):
29
+ def __init__(
30
+ self,
31
+ agents: list[Agent],
32
+ config: dict,
33
+ exp_id: str | UUID,
34
+ exp_name: str,
35
+ enable_avro: bool,
36
+ avro_path: Path,
37
+ enable_pgsql: bool,
38
+ pgsql_args: tuple[str, str, str, str, str],
39
+ logging_level: int,
40
+ ):
24
41
  logger.setLevel(logging_level)
25
42
  self._uuid = str(uuid.uuid4())
26
43
  self.agents = agents
27
44
  self.config = config
28
45
  self.exp_id = exp_id
29
46
  self.enable_avro = enable_avro
30
- self.avro_path = avro_path / f"{self._uuid}"
31
47
  if enable_avro:
48
+ self.avro_path = avro_path / f"{self._uuid}"
32
49
  self.avro_path.mkdir(parents=True, exist_ok=True)
33
50
  self.avro_file = {
34
51
  "profile": self.avro_path / f"profile.avro",
@@ -36,7 +53,7 @@ class AgentGroup:
36
53
  "status": self.avro_path / f"status.avro",
37
54
  "survey": self.avro_path / f"survey.avro",
38
55
  }
39
-
56
+
40
57
  self.messager = Messager(
41
58
  hostname=config["simulator_request"]["mqtt"]["server"],
42
59
  port=config["simulator_request"]["mqtt"]["port"],
@@ -63,21 +80,35 @@ class AgentGroup:
63
80
  else:
64
81
  self.economy_client = None
65
82
 
83
+ # Mlflow
84
+ _mlflow_config = config.get("metric_request", {}).get("mlflow")
85
+ if _mlflow_config:
86
+ logger.info(f"-----Creating Mlflow client in AgentGroup {self._uuid} ...")
87
+ self.mlflow_client = MlflowClient(
88
+ config=_mlflow_config,
89
+ mlflow_run_name=f"EXP_{exp_name}_{1000*int(time.time())}",
90
+ experiment_name=exp_name,
91
+ )
92
+ else:
93
+ self.mlflow_client = None
94
+
66
95
  for agent in self.agents:
67
- agent.set_exp_id(self.exp_id) # type: ignore
96
+ agent.set_exp_id(self.exp_id) # type: ignore
68
97
  agent.set_llm_client(self.llm)
69
98
  agent.set_simulator(self.simulator)
70
99
  if self.economy_client is not None:
71
100
  agent.set_economy_client(self.economy_client)
101
+ if self.mlflow_client is not None:
102
+ agent.set_mlflow_client(self.mlflow_client)
72
103
  agent.set_messager(self.messager)
73
104
  if self.enable_avro:
74
- agent.set_avro_file(self.avro_file) # type: ignore
105
+ agent.set_avro_file(self.avro_file) # type: ignore
75
106
 
76
107
  async def init_agents(self):
77
108
  logger.debug(f"-----Initializing Agents in AgentGroup {self._uuid} ...")
78
109
  logger.debug(f"-----Binding Agents to Simulator in AgentGroup {self._uuid} ...")
79
110
  for agent in self.agents:
80
- await agent.bind_to_simulator() # type: ignore
111
+ await agent.bind_to_simulator() # type: ignore
81
112
  self.id2agent = {agent._uuid: agent for agent in self.agents}
82
113
  logger.debug(f"-----Binding Agents to Messager in AgentGroup {self._uuid} ...")
83
114
  await self.messager.connect()
@@ -104,7 +135,7 @@ class AgentGroup:
104
135
  for agent in self.agents:
105
136
  profile = await agent.memory._profile.export()
106
137
  profile = profile[0]
107
- profile['id'] = agent._uuid
138
+ profile["id"] = agent._uuid
108
139
  profiles.append(profile)
109
140
  fastavro.writer(f, PROFILE_SCHEMA, profiles)
110
141
 
@@ -139,7 +170,9 @@ class AgentGroup:
139
170
  return results
140
171
 
141
172
  async def update(self, target_agent_uuid: str, target_key: str, content: Any):
142
- logger.debug(f"-----Updating {target_key} for agent {target_agent_uuid} in group {self._uuid}")
173
+ logger.debug(
174
+ f"-----Updating {target_key} for agent {target_agent_uuid} in group {self._uuid}"
175
+ )
143
176
  agent = self.id2agent[target_agent_uuid]
144
177
  await agent.memory.update(target_key, content)
145
178
 
@@ -147,7 +180,9 @@ class AgentGroup:
147
180
  logger.debug(f"-----Starting message dispatch for group {self._uuid}")
148
181
  while True:
149
182
  if not self.messager.is_connected():
150
- logger.warning("Messager is not connected. Skipping message processing.")
183
+ logger.warning(
184
+ "Messager is not connected. Skipping message processing."
185
+ )
151
186
 
152
187
  # Step 1: 获取消息
153
188
  messages = await self.messager.fetch_messages()
@@ -165,7 +200,7 @@ class AgentGroup:
165
200
 
166
201
  # 提取 agent_id(主题格式为 "exps/{exp_id}/agents/{agent_uuid}/{topic_type}")
167
202
  _, _, _, agent_uuid, topic_type = topic.strip("/").split("/")
168
-
203
+
169
204
  if agent_uuid in self.id2agent:
170
205
  agent = self.id2agent[agent_uuid]
171
206
  # topic_type: agent-chat, user-chat, user-survey, gather
@@ -4,22 +4,21 @@ import logging
4
4
  import os
5
5
  import random
6
6
  import uuid
7
- from collections.abc import Sequence
7
+ from collections.abc import Callable, Sequence
8
8
  from concurrent.futures import ThreadPoolExecutor
9
9
  from datetime import datetime, timezone
10
10
  from pathlib import Path
11
- from typing import Any, Callable, Dict, List, Optional, Union
11
+ from typing import Any, Optional, Union
12
12
 
13
13
  import pycityproto.city.economy.v2.economy_pb2 as economyv2
14
14
  import yaml
15
15
  from mosstool.map._map_util.const import AOI_START_ID
16
16
 
17
- from pycityagent.environment.simulator import Simulator
18
- from pycityagent.memory.memory import Memory
19
- from pycityagent.message.messager import Messager
20
- from pycityagent.survey import Survey
21
-
22
17
  from ..agent import Agent, InstitutionAgent
18
+ from ..environment.simulator import Simulator
19
+ from ..memory.memory import Memory
20
+ from ..message.messager import Messager
21
+ from ..survey import Survey
23
22
  from .agentgroup import AgentGroup
24
23
 
25
24
  logger = logging.getLogger("pycityagent")
@@ -50,15 +49,16 @@ class AgentSimulation:
50
49
  self.agent_class = [agent_class]
51
50
  self.logging_level = logging_level
52
51
  self.config = config
52
+ self.exp_name = exp_name
53
53
  self._simulator = Simulator(config["simulator_request"])
54
54
  self.agent_prefix = agent_prefix
55
- self._agents: Dict[uuid.UUID, Agent] = {}
56
- self._groups: Dict[str, AgentGroup] = {} # type:ignore
57
- self._agent_uuid2group: Dict[uuid.UUID, AgentGroup] = {} # type:ignore
58
- self._agent_uuids: List[uuid.UUID] = []
59
- self._user_chat_topics: Dict[uuid.UUID, str] = {}
60
- self._user_survey_topics: Dict[uuid.UUID, str] = {}
61
- self._user_interview_topics: Dict[uuid.UUID, str] = {}
55
+ self._agents: dict[uuid.UUID, Agent] = {}
56
+ self._groups: dict[str, AgentGroup] = {} # type:ignore
57
+ self._agent_uuid2group: dict[uuid.UUID, AgentGroup] = {} # type:ignore
58
+ self._agent_uuids: list[uuid.UUID] = []
59
+ self._user_chat_topics: dict[uuid.UUID, str] = {}
60
+ self._user_survey_topics: dict[uuid.UUID, str] = {}
61
+ self._user_interview_topics: dict[uuid.UUID, str] = {}
62
62
  self._loop = asyncio.get_event_loop()
63
63
 
64
64
  self._messager = Messager(
@@ -69,18 +69,37 @@ class AgentSimulation:
69
69
  )
70
70
  asyncio.create_task(self._messager.connect())
71
71
 
72
- self._enable_avro = config["storage"]["avro"]["enabled"]
72
+ # storage
73
+ _storage_config: dict[str, Any] = config.get("storage", {})
74
+ # avro
75
+ _avro_config: dict[str, Any] = _storage_config.get("avro", {})
76
+ self._enable_avro = _avro_config.get("enabled", False)
73
77
  if not self._enable_avro:
78
+ self._avro_path = None
74
79
  logger.warning("AVRO is not enabled, NO AVRO LOCAL STORAGE")
75
- self._avro_path = Path(config["storage"]["avro"]["path"]) / f"{self.exp_id}"
76
- self._avro_path.mkdir(parents=True, exist_ok=True)
77
-
78
- self._enable_pgsql = config["storage"]["pgsql"]["enabled"]
79
- self._pgsql_host = config["storage"]["pgsql"]["host"]
80
- self._pgsql_port = config["storage"]["pgsql"]["port"]
81
- self._pgsql_database = config["storage"]["pgsql"]["database"]
82
- self._pgsql_user = config["storage"]["pgsql"]["user"]
83
- self._pgsql_password = config["storage"]["pgsql"]["password"]
80
+ else:
81
+ self._avro_path = Path(_avro_config["path"]) / f"{self.exp_id}"
82
+ self._avro_path.mkdir(parents=True, exist_ok=True)
83
+
84
+ # pg
85
+ _pgsql_config: dict[str, Any] = _storage_config.get("pgsql", {})
86
+ self._enable_pgsql = _pgsql_config.get("enabled", False)
87
+ if not self._enable_pgsql:
88
+ logger.warning("PostgreSQL is not enabled, NO POSTGRESQL DATABASE STORAGE")
89
+ self._pgsql_args = ("", "", "", "", "")
90
+ else:
91
+ self._pgsql_host = _pgsql_config["host"]
92
+ self._pgsql_port = _pgsql_config["port"]
93
+ self._pgsql_database = _pgsql_config["database"]
94
+ self._pgsql_user = _pgsql_config.get("user", None)
95
+ self._pgsql_password = _pgsql_config.get("password", None)
96
+ self._pgsql_args: tuple[str, str, str, str, str] = (
97
+ self._pgsql_host,
98
+ self._pgsql_port,
99
+ self._pgsql_database,
100
+ self._pgsql_user,
101
+ self._pgsql_password,
102
+ )
84
103
 
85
104
  # 添加实验信息相关的属性
86
105
  self._exp_info = {
@@ -96,14 +115,34 @@ class AgentSimulation:
96
115
  }
97
116
 
98
117
  # 创建异步任务保存实验信息
99
- self._exp_info_file = self._avro_path / "experiment_info.yaml"
100
- with open(self._exp_info_file, "w") as f:
101
- yaml.dump(self._exp_info, f)
118
+ if self._enable_avro:
119
+ assert self._avro_path is not None
120
+ self._exp_info_file = self._avro_path / "experiment_info.yaml"
121
+ with open(self._exp_info_file, "w") as f:
122
+ yaml.dump(self._exp_info, f)
123
+
124
+ @property
125
+ def enable_avro(
126
+ self,
127
+ ) -> bool:
128
+ return self._enable_avro
129
+
130
+ @property
131
+ def enable_pgsql(
132
+ self,
133
+ ) -> bool:
134
+ return self._enable_pgsql
102
135
 
103
136
  @property
104
- def agents(self):
137
+ def agents(self) -> dict[uuid.UUID, Agent]:
105
138
  return self._agents
106
139
 
140
+ @property
141
+ def avro_path(
142
+ self,
143
+ ) -> Path:
144
+ return self._avro_path # type:ignore
145
+
107
146
  @property
108
147
  def groups(self):
109
148
  return self._groups
@@ -122,13 +161,24 @@ class AgentSimulation:
122
161
  agents: list[Agent],
123
162
  config: dict,
124
163
  exp_id: str,
164
+ exp_name: str,
125
165
  enable_avro: bool,
126
166
  avro_path: Path,
167
+ enable_pgsql: bool,
168
+ pgsql_args: tuple[str, str, str, str, str],
127
169
  logging_level: int = logging.WARNING,
128
170
  ):
129
171
  """创建远程组"""
130
172
  group = AgentGroup.remote(
131
- agents, config, exp_id, enable_avro, avro_path, logging_level
173
+ agents,
174
+ config,
175
+ exp_id,
176
+ exp_name,
177
+ enable_avro,
178
+ avro_path,
179
+ enable_pgsql,
180
+ pgsql_args,
181
+ logging_level,
132
182
  )
133
183
  return group_name, group, agents
134
184
 
@@ -174,7 +224,6 @@ class AgentSimulation:
174
224
  memory_config_func.append(self.default_memory_config_institution)
175
225
  else:
176
226
  memory_config_func.append(self.default_memory_config_citizen)
177
-
178
227
  # 使用线程池并行创建 AgentGroup
179
228
  group_creation_params = []
180
229
  class_init_index = 0
@@ -226,8 +275,11 @@ class AgentSimulation:
226
275
  agents,
227
276
  self.config,
228
277
  self.exp_id,
229
- self._enable_avro,
230
- self._avro_path,
278
+ self.exp_name,
279
+ self.enable_avro,
280
+ self.avro_path,
281
+ self.enable_pgsql,
282
+ self._pgsql_args,
231
283
  self.logging_level,
232
284
  )
233
285
  creation_tasks.append((group_name, group, agents))
@@ -393,7 +445,7 @@ class AgentSimulation:
393
445
  return EXTRA_ATTRIBUTES, PROFILE, BASE
394
446
 
395
447
  async def send_survey(
396
- self, survey: Survey, agent_uuids: Optional[List[uuid.UUID]] = None
448
+ self, survey: Survey, agent_uuids: Optional[list[uuid.UUID]] = None
397
449
  ):
398
450
  """发送问卷"""
399
451
  survey_dict = survey.to_dict()
@@ -410,7 +462,7 @@ class AgentSimulation:
410
462
  await self._messager.send_message(topic, payload)
411
463
 
412
464
  async def send_interview_message(
413
- self, content: str, agent_uuids: Union[uuid.UUID, List[uuid.UUID]]
465
+ self, content: str, agent_uuids: Union[uuid.UUID, list[uuid.UUID]]
414
466
  ):
415
467
  """发送面试消息"""
416
468
  payload = {
@@ -438,10 +490,17 @@ class AgentSimulation:
438
490
  async def _save_exp_info(self) -> None:
439
491
  """异步保存实验信息到YAML文件"""
440
492
  try:
441
- with open(self._exp_info_file, "w") as f:
442
- yaml.dump(self._exp_info, f)
493
+ if self.enable_avro:
494
+ with open(self._exp_info_file, "w") as f:
495
+ yaml.dump(self._exp_info, f)
443
496
  except Exception as e:
444
- logger.error(f"保存实验信息失败: {str(e)}")
497
+ logger.error(f"Avro保存实验信息失败: {str(e)}")
498
+ try:
499
+ if self.enable_pgsql:
500
+ # TODO
501
+ pass
502
+ except Exception as e:
503
+ logger.error(f"PostgreSQL保存实验信息失败: {str(e)}")
445
504
 
446
505
  async def _update_exp_status(self, status: int, error: str = "") -> None:
447
506
  """更新实验状态并保存"""
@@ -492,7 +551,6 @@ class AgentSimulation:
492
551
  tasks = []
493
552
  for group in self._groups.values():
494
553
  tasks.append(group.run.remote())
495
-
496
554
  # 等待所有group运行完成
497
555
  await asyncio.gather(*tasks)
498
556
 
@@ -4,14 +4,16 @@
4
4
  This module contains classes for creating blocks and running workflows.
5
5
  """
6
6
 
7
- from .block import Block, log_and_check, log_and_check_with_memory, trigger_class
7
+ from .block import (Block, log_and_check, log_and_check_with_memory,
8
+ trigger_class)
8
9
  from .prompt import FormatPrompt
9
- from .tool import GetMap, SencePOI, Tool
10
- from .trigger import MemoryChangeTrigger, TimeTrigger, EventTrigger
10
+ from .tool import ExportMlflowMetrics, GetMap, SencePOI, Tool
11
+ from .trigger import EventTrigger, MemoryChangeTrigger, TimeTrigger
11
12
 
12
13
  __all__ = [
13
14
  "SencePOI",
14
15
  "Tool",
16
+ "ExportMlflowMetrics",
15
17
  "GetMap",
16
18
  "MemoryChangeTrigger",
17
19
  "TimeTrigger",
@@ -3,12 +3,11 @@ import functools
3
3
  import inspect
4
4
  from typing import Any, Callable, Coroutine, Optional, Union
5
5
 
6
- from pycityagent.environment.simulator import Simulator
7
- from pycityagent.workflow.trigger import EventTrigger
8
-
6
+ from ..environment.simulator import Simulator
9
7
  from ..llm import LLM
10
8
  from ..memory import Memory
11
9
  from ..utils.decorators import record_call_aio
10
+ from ..workflow.trigger import EventTrigger
12
11
 
13
12
  TRIGGER_INTERVAL = 1
14
13
 
@@ -1,7 +1,11 @@
1
+ import time
1
2
  from typing import Any, Callable, Dict, List, Optional, Union
2
3
 
4
+ from mlflow.entities import Metric
5
+
3
6
  from ..agent import Agent
4
- from ..environment import LEVEL_ONE_PRE, POI_TYPE_DICT, AoiService, PersonService
7
+ from ..environment import (LEVEL_ONE_PRE, POI_TYPE_DICT, AoiService,
8
+ PersonService)
5
9
  from ..workflow import Block
6
10
 
7
11
 
@@ -139,7 +143,7 @@ class UpdateWithSimulator(Tool):
139
143
  if agent._simulator is None:
140
144
  return
141
145
  if not agent._has_bound_to_simulator:
142
- await agent._bind_to_simulator() # type: ignore
146
+ await agent._bind_to_simulator() # type: ignore
143
147
  simulator = agent.simulator
144
148
  memory = agent.memory
145
149
  person_id = await memory.get("id")
@@ -181,3 +185,48 @@ class ResetAgentPosition(Tool):
181
185
  lane_id=lane_id,
182
186
  s=s,
183
187
  )
188
+
189
+
190
+ class ExportMlflowMetrics(Tool):
191
+ def __init__(self, log_batch_size: int = 100) -> None:
192
+ self._log_batch_size = log_batch_size
193
+ # TODO:support other log types
194
+ self.metric_log_cache: list[Metric] = []
195
+
196
+ async def __call__(
197
+ self,
198
+ metric: Union[Metric, dict],
199
+ clear_cache: bool = False,
200
+ ):
201
+ agent = self.agent
202
+ batch_size = self._log_batch_size
203
+ if len(self.metric_log_cache) > batch_size:
204
+ client = agent.mlflow_client
205
+ await client.log_batch(
206
+ metrics=self.metric_log_cache[:batch_size],
207
+ )
208
+ self.metric_log_cache = self.metric_log_cache[batch_size:]
209
+ else:
210
+ if isinstance(metric, Metric):
211
+ self.metric_log_cache.append(metric)
212
+ else:
213
+ _metric = Metric(
214
+ key=metric["key"],
215
+ value=metric["value"],
216
+ timestamp=metric.get("timestamp", int(1000 * time.time())),
217
+ step=metric["step"],
218
+ )
219
+ self.metric_log_cache.append(_metric)
220
+ if clear_cache:
221
+ await self._clear_cache()
222
+
223
+ async def _clear_cache(
224
+ self,
225
+ ):
226
+ agent = self.agent
227
+ client = agent.mlflow_client
228
+ if len(self.metric_log_cache) > 0:
229
+ await client.log_batch(
230
+ metrics=self.metric_log_cache,
231
+ )
232
+ self.metric_log_cache = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycityagent
3
- Version: 2.0.0a18
3
+ Version: 2.0.0a19
4
4
  Summary: LLM-based城市环境agent构建库
5
5
  License: MIT
6
6
  Author: Yuwei Yan
@@ -26,6 +26,7 @@ Requires-Dist: gradio (>=5.7.1,<6.0.0)
26
26
  Requires-Dist: grpcio (==1.67.1)
27
27
  Requires-Dist: langchain-core (>=0.3.28,<0.4.0)
28
28
  Requires-Dist: matplotlib (==3.8.3)
29
+ Requires-Dist: mlflow (>=2.19.0,<3.0.0)
29
30
  Requires-Dist: mosstool (==1.0.24)
30
31
  Requires-Dist: networkx (==3.2.1)
31
32
  Requires-Dist: numpy (>=1.20.0,<2.0.0)
@@ -1,7 +1,7 @@
1
1
  pycityagent/__init__.py,sha256=EDxt3Su3lH1IMh9suNw7GeGL7UrXeWiZTw5KWNznDzc,637
2
- pycityagent/agent.py,sha256=t9W9sKxtQ0EkMxL78kAjAu-rXigEK6gyLY0IEA4DbnQ,23143
2
+ pycityagent/agent.py,sha256=gy9OlFpnEla7wK45k4m7_ySLp67mBKoG50vxUEmxbJE,24280
3
3
  pycityagent/economy/__init__.py,sha256=aonY4WHnx-6EGJ4WKrx4S-2jAkYNLtqUA04jp6q8B7w,75
4
- pycityagent/economy/econ_client.py,sha256=DE11Ng_NO_foW65A-LxFW0VED-HLrnn4GwUf_Xn-Tlg,11189
4
+ pycityagent/economy/econ_client.py,sha256=EZDGxM7K83ucYZQ5qdv6HA-jhRCWbR1u5q-kLMqelKc,11192
5
5
  pycityagent/environment/__init__.py,sha256=awHxlOud-btWbk0FCS4RmGJ13W84oVCkbGfcrhKqihA,240
6
6
  pycityagent/environment/interact/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  pycityagent/environment/interact/interact.py,sha256=ifxPPzuHeqLHIZ_6zvfXMoBOnBsXNIP4bYp7OJ7pnEQ,6588
@@ -21,7 +21,7 @@ pycityagent/environment/sim/person_service.py,sha256=nIvOsoBoqOTDYtsiThg07-4ZBgk
21
21
  pycityagent/environment/sim/road_service.py,sha256=phKTwTyhc_6Ht2mddEXpdENfl-lRXIVY0CHAlw1yHjI,1264
22
22
  pycityagent/environment/sim/sim_env.py,sha256=HI1LcS_FotDKQ6vBnx0e49prXSABOfA20aU9KM-ZkCY,4625
23
23
  pycityagent/environment/sim/social_service.py,sha256=6Iqvq6dz8H2jhLLdtaITc6Js9QnQw-Ylsd5AZgUj3-E,1993
24
- pycityagent/environment/simulator.py,sha256=K7IyhiGC9BxanW28bpML4M0YREdMp1h7yMoWBlbf3RY,12504
24
+ pycityagent/environment/simulator.py,sha256=XjcxbyBIbB3Ht9z087z_oWIPAN6pP5Eq1lyf4W5atb8,12502
25
25
  pycityagent/environment/utils/__init__.py,sha256=1m4Q1EfGvNpUsa1bgQzzCyWhfkpElnskNImjjFD3Znc,237
26
26
  pycityagent/environment/utils/base64.py,sha256=hoREzQo3FXMN79pqQLO2jgsDEvudciomyKii7MWljAM,374
27
27
  pycityagent/environment/utils/const.py,sha256=3RMNy7_bE7-23K90j9DFW_tWEzu8s7hSTgKbV-3BFl4,5327
@@ -45,9 +45,12 @@ pycityagent/memory/state.py,sha256=5W0c1yJ-aaPpE74B2LEcw3Ygpm77tyooHv8NylyrozE,5
45
45
  pycityagent/memory/utils.py,sha256=wLNlNlZ-AY9VB8kbUIy0UQSYh26FOQABbhmKQkit5o8,850
46
46
  pycityagent/message/__init__.py,sha256=TCjazxqb5DVwbTu1fF0sNvaH_EPXVuj2XQ0p6W-QCLU,55
47
47
  pycityagent/message/messager.py,sha256=W_OVlNGcreHSBf6v-DrEnfNCXExB78ySr0w26MSncfU,2541
48
+ pycityagent/metrics/__init__.py,sha256=IrlwXs_733b3PlMV99Ogh6R8sGCEalF2qIT2mQwdPjU,75
49
+ pycityagent/metrics/mlflow_client.py,sha256=BH-YVMlYr-eTFdrCZjgMZWi8ptUEm7todOXCpYixAgU,3311
50
+ pycityagent/metrics/utils/const.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
51
  pycityagent/simulation/__init__.py,sha256=jYaqaNpzM5M_e_ykISS_M-mIyYdzJXJWhgpfBpA6l5k,111
49
- pycityagent/simulation/agentgroup.py,sha256=M19XWJRWyjMAYS0_RIOBQ2C7I1MuVYIaX3DgehGZL2Y,12541
50
- pycityagent/simulation/simulation.py,sha256=9VH-VaRufUcQcVhqXcQJTMtrquns8pbDpRTfK6736Io,19318
52
+ pycityagent/simulation/agentgroup.py,sha256=H2E_YQ3ir3gQmPWsupHXA_LSBVkffzkXWl4UgZ9qLOc,13327
53
+ pycityagent/simulation/simulation.py,sha256=ANf04GqaK7gLT50F_ZcP2UcpS_uGx12pLqkrK6eA2L8,21014
51
54
  pycityagent/survey/__init__.py,sha256=rxwou8U9KeFSP7rMzXtmtp2fVFZxK4Trzi-psx9LPIs,153
52
55
  pycityagent/survey/manager.py,sha256=S5IkwTdelsdtZETChRcfCEczzwSrry_Fly9MY4s3rbk,1681
53
56
  pycityagent/survey/models.py,sha256=YE50UUt5qJ0O_lIUsSY6XFCGUTkJVNu_L1gAhaCJ2fs,3546
@@ -59,11 +62,11 @@ pycityagent/utils/parsers/code_block_parser.py,sha256=Cs2Z_hm9VfNCpPPll1TwteaJF-
59
62
  pycityagent/utils/parsers/json_parser.py,sha256=FZ3XN1g8z4Dr2TFraUOoah1oQcze4fPd2m01hHoX0Mo,2917
60
63
  pycityagent/utils/parsers/parser_base.py,sha256=k6DVqwAMK3jJdOP4IeLE-aFPm3V2F-St5qRBuRdx4aU,1742
61
64
  pycityagent/utils/survey_util.py,sha256=Be9nptmu2JtesFNemPgORh_2GsN7rcDYGQS9Zfvc5OI,2169
62
- pycityagent/workflow/__init__.py,sha256=EyCcjB6LyBim-5iAOPe4m2qfvghEPqu1ZdGfy4KPeZ8,551
63
- pycityagent/workflow/block.py,sha256=6EmiRMLdOZC1wMlmLMIjfrp9TuiI7Gw4s3nnXVMbrnw,6031
65
+ pycityagent/workflow/__init__.py,sha256=QNkUV-9mACMrR8c0cSKna2gC1mMZdxXbxWzjE-Uods0,621
66
+ pycityagent/workflow/block.py,sha256=WkE2On97DCZS_9n8aIgT8wxv9Oaff4Fdf2tLqbKfMtE,6010
64
67
  pycityagent/workflow/prompt.py,sha256=tY69nDO8fgYfF_dOA-iceR8pAhkYmCqoox8uRPqEuGY,2956
65
- pycityagent/workflow/tool.py,sha256=_bCluIX8HTC8ZW6a-wrMB3Uhx2yzD8sM8XFDI3vd0MM,6642
68
+ pycityagent/workflow/tool.py,sha256=uaB0dV35jA9v2UqWw9L8iPM-HJW5e9BlgFVgOMf9jvw,8201
66
69
  pycityagent/workflow/trigger.py,sha256=t5X_i0WtL32bipZSsq_E3UUyYYudYLxQUpvxbgClp2s,5683
67
- pycityagent-2.0.0a18.dist-info/METADATA,sha256=iajlG1hMfjQA2qBuTYITWemGBg4TEPU8hHpSU1l0MDs,7760
68
- pycityagent-2.0.0a18.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
- pycityagent-2.0.0a18.dist-info/RECORD,,
70
+ pycityagent-2.0.0a19.dist-info/METADATA,sha256=x0DbwNQ4Zj0rswWGIv6MA0zSBBEiExXLIdErYA0bhnE,7800
71
+ pycityagent-2.0.0a19.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
72
+ pycityagent-2.0.0a19.dist-info/RECORD,,