camel-ai 0.1.9__py3-none-any.whl → 0.2.3__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.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (102) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +334 -113
  3. camel/agents/knowledge_graph_agent.py +4 -6
  4. camel/bots/__init__.py +34 -0
  5. camel/bots/discord_app.py +138 -0
  6. camel/bots/slack/__init__.py +30 -0
  7. camel/bots/slack/models.py +158 -0
  8. camel/bots/slack/slack_app.py +255 -0
  9. camel/bots/telegram_bot.py +82 -0
  10. camel/configs/__init__.py +1 -2
  11. camel/configs/anthropic_config.py +2 -5
  12. camel/configs/base_config.py +6 -6
  13. camel/configs/gemini_config.py +1 -1
  14. camel/configs/groq_config.py +2 -3
  15. camel/configs/ollama_config.py +1 -2
  16. camel/configs/openai_config.py +2 -23
  17. camel/configs/samba_config.py +2 -2
  18. camel/configs/togetherai_config.py +1 -1
  19. camel/configs/vllm_config.py +1 -1
  20. camel/configs/zhipuai_config.py +2 -3
  21. camel/embeddings/openai_embedding.py +2 -2
  22. camel/loaders/__init__.py +2 -0
  23. camel/loaders/chunkr_reader.py +163 -0
  24. camel/loaders/firecrawl_reader.py +13 -45
  25. camel/loaders/unstructured_io.py +65 -29
  26. camel/messages/__init__.py +1 -0
  27. camel/messages/func_message.py +2 -2
  28. camel/models/__init__.py +2 -4
  29. camel/models/anthropic_model.py +32 -26
  30. camel/models/azure_openai_model.py +39 -36
  31. camel/models/base_model.py +31 -20
  32. camel/models/gemini_model.py +37 -29
  33. camel/models/groq_model.py +29 -23
  34. camel/models/litellm_model.py +44 -61
  35. camel/models/mistral_model.py +33 -30
  36. camel/models/model_factory.py +66 -76
  37. camel/models/nemotron_model.py +33 -23
  38. camel/models/ollama_model.py +42 -47
  39. camel/models/{openai_compatibility_model.py → openai_compatible_model.py} +36 -41
  40. camel/models/openai_model.py +60 -25
  41. camel/models/reka_model.py +30 -28
  42. camel/models/samba_model.py +82 -177
  43. camel/models/stub_model.py +2 -2
  44. camel/models/togetherai_model.py +37 -43
  45. camel/models/vllm_model.py +43 -50
  46. camel/models/zhipuai_model.py +33 -27
  47. camel/retrievers/auto_retriever.py +28 -10
  48. camel/retrievers/vector_retriever.py +72 -44
  49. camel/societies/babyagi_playing.py +6 -3
  50. camel/societies/role_playing.py +17 -3
  51. camel/storages/__init__.py +2 -0
  52. camel/storages/graph_storages/__init__.py +2 -0
  53. camel/storages/graph_storages/graph_element.py +3 -5
  54. camel/storages/graph_storages/nebula_graph.py +547 -0
  55. camel/storages/key_value_storages/json.py +6 -1
  56. camel/tasks/task.py +11 -4
  57. camel/tasks/task_prompt.py +4 -0
  58. camel/toolkits/__init__.py +28 -24
  59. camel/toolkits/arxiv_toolkit.py +155 -0
  60. camel/toolkits/ask_news_toolkit.py +653 -0
  61. camel/toolkits/base.py +2 -3
  62. camel/toolkits/code_execution.py +6 -7
  63. camel/toolkits/dalle_toolkit.py +6 -6
  64. camel/toolkits/{openai_function.py → function_tool.py} +34 -11
  65. camel/toolkits/github_toolkit.py +9 -10
  66. camel/toolkits/google_maps_toolkit.py +7 -14
  67. camel/toolkits/google_scholar_toolkit.py +146 -0
  68. camel/toolkits/linkedin_toolkit.py +7 -10
  69. camel/toolkits/math_toolkit.py +8 -8
  70. camel/toolkits/open_api_toolkit.py +5 -8
  71. camel/toolkits/reddit_toolkit.py +7 -10
  72. camel/toolkits/retrieval_toolkit.py +5 -9
  73. camel/toolkits/search_toolkit.py +9 -9
  74. camel/toolkits/slack_toolkit.py +11 -14
  75. camel/toolkits/twitter_toolkit.py +377 -454
  76. camel/toolkits/weather_toolkit.py +6 -6
  77. camel/toolkits/whatsapp_toolkit.py +177 -0
  78. camel/types/__init__.py +6 -1
  79. camel/types/enums.py +43 -85
  80. camel/types/openai_types.py +3 -0
  81. camel/types/unified_model_type.py +104 -0
  82. camel/utils/__init__.py +0 -2
  83. camel/utils/async_func.py +7 -7
  84. camel/utils/commons.py +40 -4
  85. camel/utils/token_counting.py +38 -214
  86. camel/workforce/__init__.py +6 -6
  87. camel/workforce/base.py +9 -5
  88. camel/workforce/prompts.py +179 -0
  89. camel/workforce/role_playing_worker.py +181 -0
  90. camel/workforce/{single_agent_node.py → single_agent_worker.py} +49 -23
  91. camel/workforce/task_channel.py +7 -8
  92. camel/workforce/utils.py +20 -50
  93. camel/workforce/{worker_node.py → worker.py} +15 -12
  94. camel/workforce/workforce.py +456 -19
  95. camel_ai-0.2.3.dist-info/LICENSE +201 -0
  96. {camel_ai-0.1.9.dist-info → camel_ai-0.2.3.dist-info}/METADATA +40 -65
  97. {camel_ai-0.1.9.dist-info → camel_ai-0.2.3.dist-info}/RECORD +98 -86
  98. {camel_ai-0.1.9.dist-info → camel_ai-0.2.3.dist-info}/WHEEL +1 -1
  99. camel/models/open_source_model.py +0 -170
  100. camel/workforce/manager_node.py +0 -299
  101. camel/workforce/role_playing_node.py +0 -168
  102. camel/workforce/workforce_prompt.py +0 -125
@@ -11,39 +11,476 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
  # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. ===========
14
+ from __future__ import annotations
15
+
16
+ import ast
14
17
  import asyncio
18
+ import logging
19
+ from collections import deque
20
+ from typing import Deque, Dict, List, Optional
21
+
22
+ from colorama import Fore
15
23
 
16
- from camel.tasks import Task
17
- from camel.workforce.manager_node import ManagerNode
24
+ from camel.agents import ChatAgent
25
+ from camel.configs import ChatGPTConfig
26
+ from camel.messages.base import BaseMessage
27
+ from camel.models import ModelFactory
28
+ from camel.tasks.task import Task, TaskState
29
+ from camel.toolkits import SEARCH_FUNCS, WEATHER_FUNCS, GoogleMapsToolkit
30
+ from camel.types import ModelPlatformType, ModelType
31
+ from camel.workforce.base import BaseNode
32
+ from camel.workforce.prompts import (
33
+ ASSIGN_TASK_PROMPT,
34
+ CREATE_NODE_PROMPT,
35
+ WF_TASK_DECOMPOSE_PROMPT,
36
+ )
37
+ from camel.workforce.role_playing_worker import RolePlayingWorker
38
+ from camel.workforce.single_agent_worker import SingleAgentWorker
18
39
  from camel.workforce.task_channel import TaskChannel
40
+ from camel.workforce.utils import (
41
+ TaskAssignResult,
42
+ WorkerConf,
43
+ check_if_running,
44
+ )
45
+ from camel.workforce.worker import Worker
19
46
 
47
+ logger = logging.getLogger(__name__)
20
48
 
21
- class Workforce:
22
- r"""A class representing a workforce system.
49
+
50
+ class Workforce(BaseNode):
51
+ r"""A system where multiple workder nodes (agents) cooperate together
52
+ to solve tasks. It can assign tasks to workder nodes and also take
53
+ strategies such as create new worker, decompose tasks, etc. to handle
54
+ situations when the task fails.
23
55
 
24
56
  Args:
25
- name (str, optional): The name of the workforce system. Defaults to
26
- `"CAMEL Workforce"`.
27
- description (str, optional): A description of the workforce system.
28
- Defaults to `"A workforce system for managing tasks."`.
57
+ description (str): Description of the node.
58
+ children (Optional[List[BaseNode]], optional): List of child nodes
59
+ under this node. Each child node can be a worker node or
60
+ another workforce node. (default: :obj:`None`)
61
+ coordinator_agent_kwargs (Optional[Dict], optional): Keyword
62
+ arguments for the coordinator agent, e.g. `model`, `api_key`,
63
+ `tools`, etc. (default: :obj:`None`)
64
+ task_agent_kwargs (Optional[Dict], optional): Keyword arguments for
65
+ the task agent, e.g. `model`, `api_key`, `tools`, etc.
66
+ (default: :obj:`None`)
67
+ new_worker_agent_kwargs (Optional[Dict]): Default keyword arguments
68
+ for the worker agent that will be created during runtime to
69
+ handle failed tasks, e.g. `model`, `api_key`, `tools`, etc.
70
+ (default: :obj:`None`)
29
71
  """
30
72
 
31
73
  def __init__(
32
74
  self,
33
- root_node: ManagerNode,
34
- name: str = "CAMEL Workforce",
35
- description: str = "A workforce system for managing tasks.",
75
+ description: str,
76
+ children: Optional[List[BaseNode]] = None,
77
+ coordinator_agent_kwargs: Optional[Dict] = None,
78
+ task_agent_kwargs: Optional[Dict] = None,
79
+ new_worker_agent_kwargs: Optional[Dict] = None,
36
80
  ) -> None:
37
- self.name = name
38
- self.description = description
39
- self._root_node = root_node
81
+ super().__init__(description)
82
+ self._child_listening_tasks: Deque[asyncio.Task] = deque()
83
+ self._children = children or []
84
+ self.new_worker_agent_kwargs = new_worker_agent_kwargs
85
+
86
+ coord_agent_sys_msg = BaseMessage.make_assistant_message(
87
+ role_name="Workforce Manager",
88
+ content="You are coordinating a group of workers. A worker can be "
89
+ "a group of agents or a single agent. Each worker is "
90
+ "created to solve a specific kind of task. Your job "
91
+ "includes assigning tasks to a existing worker, creating "
92
+ "a new worker for a task, etc.",
93
+ )
94
+ self.coordinator_agent = ChatAgent(
95
+ coord_agent_sys_msg, **(coordinator_agent_kwargs or {})
96
+ )
97
+
98
+ task_sys_msg = BaseMessage.make_assistant_message(
99
+ role_name="Task Planner",
100
+ content="You are going to compose and decompose tasks.",
101
+ )
102
+ self.task_agent = ChatAgent(task_sys_msg, **(task_agent_kwargs or {}))
103
+
104
+ # If there is one, will set by the workforce class wrapping this
105
+ self._task: Optional[Task] = None
106
+ self._pending_tasks: Deque[Task] = deque()
107
+
108
+ def __repr__(self):
109
+ return f"Workforce {self.node_id} ({self.description})"
110
+
111
+ def _decompose_task(self, task: Task) -> List[Task]:
112
+ r"""Decompose the task into subtasks. This method will also set the
113
+ relationship between the task and its subtasks.
40
114
 
115
+ Returns:
116
+ List[Task]: The subtasks.
117
+ """
118
+ decompose_prompt = WF_TASK_DECOMPOSE_PROMPT.format(
119
+ content=task.content,
120
+ child_nodes_info=self._get_child_nodes_info(),
121
+ additional_info=task.additional_info,
122
+ )
123
+ self.task_agent.reset()
124
+ subtasks = task.decompose(self.task_agent, decompose_prompt)
125
+ task.subtasks = subtasks
126
+ for subtask in subtasks:
127
+ subtask.parent = task
128
+
129
+ return subtasks
130
+
131
+ @check_if_running(False)
41
132
  def process_task(self, task: Task) -> Task:
42
- self._root_node.set_main_task(task)
43
- shared_channel = TaskChannel()
44
- self._root_node.set_channel(shared_channel)
133
+ r"""The main entry point for the workforce to process a task. It will
134
+ start the workforce and all the child nodes under it, process the
135
+ task provided and return the updated task.
136
+
137
+ Args:
138
+ task (Task): The task to be processed.
139
+
140
+ Returns:
141
+ Task: The updated task.
142
+ """
143
+ self.reset()
144
+ self._task = task
145
+ task.state = TaskState.FAILED
146
+ self._pending_tasks.append(task)
147
+ # The agent tend to be overconfident on the whole task, so we
148
+ # decompose the task into subtasks first
149
+ subtasks = self._decompose_task(task)
150
+ self._pending_tasks.extendleft(reversed(subtasks))
151
+ self.set_channel(TaskChannel())
45
152
 
46
- # start the root workforce
47
- asyncio.run(self._root_node.start())
153
+ asyncio.run(self.start())
48
154
 
49
155
  return task
156
+
157
+ @check_if_running(False)
158
+ def add_single_agent_worker(
159
+ self, description: str, worker: ChatAgent
160
+ ) -> Workforce:
161
+ r"""Add a worker node to the workforce that uses a single agent.
162
+
163
+ Args:
164
+ description (str): Description of the worker node.
165
+ worker (ChatAgent): The agent to be added.
166
+
167
+ Returns:
168
+ Workforce: The workforce node itself.
169
+ """
170
+ worker_node = SingleAgentWorker(description, worker)
171
+ self._children.append(worker_node)
172
+ return self
173
+
174
+ @check_if_running(False)
175
+ def add_role_playing_worker(
176
+ self,
177
+ description: str,
178
+ assistant_role_name: str,
179
+ user_role_name: str,
180
+ assistant_agent_kwargs: Optional[Dict] = None,
181
+ user_agent_kwargs: Optional[Dict] = None,
182
+ chat_turn_limit: int = 3,
183
+ ) -> Workforce:
184
+ r"""Add a worker node to the workforce that uses `RolePlaying` system.
185
+
186
+ Args:
187
+ description (str): Description of the node.
188
+ assistant_role_name (str): The role name of the assistant agent.
189
+ user_role_name (str): The role name of the user agent.
190
+ assistant_agent_kwargs (Optional[Dict], optional): The keyword
191
+ arguments to initialize the assistant agent in the role
192
+ playing, like the model name, etc. Defaults to `None`.
193
+ user_agent_kwargs (Optional[Dict], optional): The keyword arguments
194
+ to initialize the user agent in the role playing, like the
195
+ model name, etc. Defaults to `None`.
196
+ chat_turn_limit (int, optional): The maximum number of chat turns
197
+ in the role playing. Defaults to 3.
198
+
199
+ Returns:
200
+ Workforce: The workforce node itself.
201
+ """
202
+ worker_node = RolePlayingWorker(
203
+ description,
204
+ assistant_role_name,
205
+ user_role_name,
206
+ assistant_agent_kwargs,
207
+ user_agent_kwargs,
208
+ chat_turn_limit,
209
+ )
210
+ self._children.append(worker_node)
211
+ return self
212
+
213
+ @check_if_running(False)
214
+ def add_workforce(self, workforce: Workforce) -> Workforce:
215
+ r"""Add a workforce node to the workforce.
216
+
217
+ Args:
218
+ workforce (Workforce): The workforce node to be added.
219
+
220
+ Returns:
221
+ Workforce: The workforce node itself.
222
+ """
223
+ self._children.append(workforce)
224
+ return self
225
+
226
+ @check_if_running(False)
227
+ def reset(self) -> None:
228
+ r"""Reset the workforce and all the child nodes under it. Can only
229
+ be called when the workforce is not running."""
230
+ super().reset()
231
+ self._task = None
232
+ self._pending_tasks.clear()
233
+ self._child_listening_tasks.clear()
234
+ self.coordinator_agent.reset()
235
+ self.task_agent.reset()
236
+ for child in self._children:
237
+ child.reset()
238
+
239
+ @check_if_running(False)
240
+ def set_channel(self, channel: TaskChannel) -> None:
241
+ r"""Set the channel for the node and all the child nodes under it."""
242
+ self._channel = channel
243
+ for child in self._children:
244
+ child.set_channel(channel)
245
+
246
+ def _get_child_nodes_info(self) -> str:
247
+ r"""Get the information of all the child nodes under this node."""
248
+ info = ""
249
+ for child in self._children:
250
+ if isinstance(child, Workforce):
251
+ additional_info = "A Workforce node"
252
+ elif isinstance(child, SingleAgentWorker):
253
+ additional_info = "tools: " + (
254
+ ", ".join(child.worker.func_dict.keys())
255
+ )
256
+ elif isinstance(child, RolePlayingWorker):
257
+ additional_info = "A Role playing node"
258
+ else:
259
+ additional_info = "Unknown node"
260
+ info += (
261
+ f"<{child.node_id}>:<{child.description}>:<"
262
+ f"{additional_info}>\n"
263
+ )
264
+ return info
265
+
266
+ def _find_assignee(
267
+ self,
268
+ task: Task,
269
+ ) -> str:
270
+ r"""Assigns a task to a worker node with the best capability.
271
+
272
+ Parameters:
273
+ task (Task): The task to be assigned.
274
+
275
+ Returns:
276
+ str: ID of the worker node to be assigned.
277
+ """
278
+ self.coordinator_agent.reset()
279
+ prompt = ASSIGN_TASK_PROMPT.format(
280
+ content=task.content,
281
+ child_nodes_info=self._get_child_nodes_info(),
282
+ additional_info=task.additional_info,
283
+ )
284
+ req = BaseMessage.make_user_message(
285
+ role_name="User",
286
+ content=prompt,
287
+ )
288
+
289
+ response = self.coordinator_agent.step(
290
+ req, response_format=TaskAssignResult
291
+ )
292
+ result_dict = ast.literal_eval(response.msg.content)
293
+ task_assign_result = TaskAssignResult(**result_dict)
294
+ return task_assign_result.assignee_id
295
+
296
+ async def _post_task(self, task: Task, assignee_id: str) -> None:
297
+ await self._channel.post_task(task, self.node_id, assignee_id)
298
+
299
+ async def _post_dependency(self, dependency: Task) -> None:
300
+ await self._channel.post_dependency(dependency, self.node_id)
301
+
302
+ def _create_worker_node_for_task(self, task: Task) -> Worker:
303
+ r"""Creates a new worker node for a given task and add it to the
304
+ children list of this node. This is one of the actions that
305
+ the coordinator can take when a task has failed.
306
+
307
+ Args:
308
+ task (Task): The task for which the worker node is created.
309
+
310
+ Returns:
311
+ Worker: The created worker node.
312
+ """
313
+ prompt = CREATE_NODE_PROMPT.format(
314
+ content=task.content,
315
+ child_nodes_info=self._get_child_nodes_info(),
316
+ additional_info=task.additional_info,
317
+ )
318
+ req = BaseMessage.make_user_message(
319
+ role_name="User",
320
+ content=prompt,
321
+ )
322
+ response = self.coordinator_agent.step(req, response_format=WorkerConf)
323
+ result_dict = ast.literal_eval(response.msg.content)
324
+ new_node_conf = WorkerConf(**result_dict)
325
+
326
+ new_agent = self._create_new_agent(
327
+ new_node_conf.role,
328
+ new_node_conf.sys_msg,
329
+ )
330
+
331
+ new_node = SingleAgentWorker(
332
+ description=new_node_conf.description,
333
+ worker=new_agent,
334
+ )
335
+ new_node.set_channel(self._channel)
336
+
337
+ print(f"{Fore.CYAN}{new_node} created.{Fore.RESET}")
338
+
339
+ self._children.append(new_node)
340
+ self._child_listening_tasks.append(
341
+ asyncio.create_task(new_node.start())
342
+ )
343
+ return new_node
344
+
345
+ def _create_new_agent(self, role: str, sys_msg: str) -> ChatAgent:
346
+ worker_sys_msg = BaseMessage.make_assistant_message(
347
+ role_name=role,
348
+ content=sys_msg,
349
+ )
350
+
351
+ if self.new_worker_agent_kwargs is not None:
352
+ return ChatAgent(worker_sys_msg, **self.new_worker_agent_kwargs)
353
+
354
+ # Default tools for a new agent
355
+ function_list = [
356
+ *SEARCH_FUNCS,
357
+ *WEATHER_FUNCS,
358
+ *GoogleMapsToolkit().get_tools(),
359
+ ]
360
+
361
+ model_config_dict = ChatGPTConfig(
362
+ tools=function_list,
363
+ temperature=0.0,
364
+ ).as_dict()
365
+
366
+ model = ModelFactory.create(
367
+ model_platform=ModelPlatformType.DEFAULT,
368
+ model_type=ModelType.DEFAULT,
369
+ model_config_dict=model_config_dict,
370
+ )
371
+
372
+ return ChatAgent(worker_sys_msg, model=model, tools=function_list)
373
+
374
+ async def _get_returned_task(self) -> Task:
375
+ r"""Get the task that's published by this node and just get returned
376
+ from the assignee.
377
+ """
378
+ return await self._channel.get_returned_task_by_publisher(self.node_id)
379
+
380
+ async def _post_ready_tasks(self) -> None:
381
+ r"""Send all the pending tasks that have all the dependencies met to
382
+ the channel, or directly return if there is none. For now, we will
383
+ directly send the first task in the pending list because all the tasks
384
+ are linearly dependent."""
385
+
386
+ if not self._pending_tasks:
387
+ return
388
+
389
+ ready_task = self._pending_tasks[0]
390
+
391
+ # If the task has failed previously, just compose and send the task
392
+ # to the channel as a dependency
393
+ if ready_task.state == TaskState.FAILED:
394
+ # TODO: the composing of tasks seems not work very well
395
+ self.task_agent.reset()
396
+ ready_task.compose(self.task_agent)
397
+ # Remove the subtasks from the channel
398
+ for subtask in ready_task.subtasks:
399
+ await self._channel.remove_task(subtask.id)
400
+ # Send the task to the channel as a dependency
401
+ await self._post_dependency(ready_task)
402
+ self._pending_tasks.popleft()
403
+ # Try to send the next task in the pending list
404
+ await self._post_ready_tasks()
405
+ else:
406
+ # Directly post the task to the channel if it's a new one
407
+ # Find a node to assign the task
408
+ assignee_id = self._find_assignee(task=ready_task)
409
+ await self._post_task(ready_task, assignee_id)
410
+
411
+ async def _handle_failed_task(self, task: Task) -> bool:
412
+ if task.failure_count >= 3:
413
+ return True
414
+ task.failure_count += 1
415
+ # Remove the failed task from the channel
416
+ await self._channel.remove_task(task.id)
417
+ if task.get_depth() >= 3:
418
+ # Create a new worker node and reassign
419
+ assignee = self._create_worker_node_for_task(task)
420
+ await self._post_task(task, assignee.node_id)
421
+ else:
422
+ subtasks = self._decompose_task(task)
423
+ # Insert packets at the head of the queue
424
+ self._pending_tasks.extendleft(reversed(subtasks))
425
+ await self._post_ready_tasks()
426
+ return False
427
+
428
+ async def _handle_completed_task(self, task: Task) -> None:
429
+ # archive the packet, making it into a dependency
430
+ self._pending_tasks.popleft()
431
+ await self._channel.archive_task(task.id)
432
+ await self._post_ready_tasks()
433
+
434
+ @check_if_running(False)
435
+ async def _listen_to_channel(self) -> None:
436
+ r"""Continuously listen to the channel, post task to the channel and
437
+ track the status of posted tasks.
438
+ """
439
+
440
+ self._running = True
441
+ logger.info(f"Workforce {self.node_id} started.")
442
+
443
+ await self._post_ready_tasks()
444
+
445
+ while self._task is None or self._pending_tasks:
446
+ returned_task = await self._get_returned_task()
447
+ if returned_task.state == TaskState.DONE:
448
+ await self._handle_completed_task(returned_task)
449
+ elif returned_task.state == TaskState.FAILED:
450
+ halt = await self._handle_failed_task(returned_task)
451
+ if not halt:
452
+ continue
453
+ print(
454
+ f"{Fore.RED}Task {returned_task.id} has failed "
455
+ f"for 3 times, halting the workforce.{Fore.RESET}"
456
+ )
457
+ break
458
+ elif returned_task.state == TaskState.OPEN:
459
+ # TODO: multi-layer workforce
460
+ pass
461
+ else:
462
+ raise ValueError(
463
+ f"Task {returned_task.id} has an unexpected state."
464
+ )
465
+
466
+ # shut down the whole workforce tree
467
+ self.stop()
468
+
469
+ @check_if_running(False)
470
+ async def start(self) -> None:
471
+ r"""Start itself and all the child nodes under it."""
472
+ for child in self._children:
473
+ child_listening_task = asyncio.create_task(child.start())
474
+ self._child_listening_tasks.append(child_listening_task)
475
+ await self._listen_to_channel()
476
+
477
+ @check_if_running(True)
478
+ def stop(self) -> None:
479
+ r"""Stop all the child nodes under it. The node itself will be stopped
480
+ by its parent node.
481
+ """
482
+ for child in self._children:
483
+ child.stop()
484
+ for child_task in self._child_listening_tasks:
485
+ child_task.cancel()
486
+ self._running = False