lionagi 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. lionagi/__init__.py +2 -1
  2. lionagi/core/generic/graph.py +10 -3
  3. lionagi/core/generic/node.py +5 -1
  4. lionagi/core/report/base.py +1 -0
  5. lionagi/core/report/form.py +1 -1
  6. lionagi/core/session/directive_mixin.py +1 -1
  7. lionagi/core/unit/template/plan.py +1 -1
  8. lionagi/core/work/work.py +4 -2
  9. lionagi/core/work/work_edge.py +96 -0
  10. lionagi/core/work/work_function.py +36 -4
  11. lionagi/core/work/work_function_node.py +44 -0
  12. lionagi/core/work/work_queue.py +50 -26
  13. lionagi/core/work/work_task.py +155 -0
  14. lionagi/core/work/worker.py +225 -37
  15. lionagi/core/work/worker_engine.py +179 -0
  16. lionagi/core/work/worklog.py +9 -11
  17. lionagi/tests/test_core/generic/test_structure.py +193 -0
  18. lionagi/tests/test_core/graph/__init__.py +0 -0
  19. lionagi/tests/test_core/graph/test_graph.py +70 -0
  20. lionagi/tests/test_core/graph/test_tree.py +75 -0
  21. lionagi/tests/test_core/mail/__init__.py +0 -0
  22. lionagi/tests/test_core/mail/test_mail.py +62 -0
  23. lionagi/tests/test_core/test_structure/__init__.py +0 -0
  24. lionagi/tests/test_core/test_structure/test_base_structure.py +196 -0
  25. lionagi/tests/test_core/test_structure/test_graph.py +54 -0
  26. lionagi/tests/test_core/test_structure/test_tree.py +48 -0
  27. lionagi/version.py +1 -1
  28. {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/METADATA +5 -4
  29. {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/RECORD +32 -18
  30. {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/LICENSE +0 -0
  31. {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/WHEEL +0 -0
  32. {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/top_level.txt +0 -0
lionagi/__init__.py CHANGED
@@ -27,7 +27,7 @@ from lionagi.core.action import func_to_tool
27
27
  from lionagi.core.report import Form, Report
28
28
  from lionagi.core.session.branch import Branch
29
29
  from lionagi.core.session.session import Session
30
- from lionagi.core.work.worker import work, Worker
30
+ from lionagi.core.work.worker import work, Worker, worklink
31
31
  from lionagi.integrations.provider.services import Services
32
32
  from lionagi.integrations.chunker.chunk import chunk
33
33
  from lionagi.integrations.loader.load import load
@@ -41,6 +41,7 @@ __all__ = [
41
41
  "pile",
42
42
  "iModel",
43
43
  "work",
44
+ "worklink",
44
45
  "Worker",
45
46
  "Branch",
46
47
  "Session",
@@ -44,6 +44,7 @@ class Graph(Node):
44
44
  condition: Condition | None = None,
45
45
  bundle=False,
46
46
  label=None,
47
+ edge_class=Edge,
47
48
  **kwargs,
48
49
  ):
49
50
  """Add an edge between two nodes in the graph."""
@@ -61,6 +62,7 @@ class Graph(Node):
61
62
  condition=condition,
62
63
  label=label,
63
64
  bundle=bundle,
65
+ edge_class=edge_class,
64
66
  **kwargs,
65
67
  )
66
68
 
@@ -184,19 +186,23 @@ class Graph(Node):
184
186
  node_info = node.to_dict()
185
187
  node_info.pop("ln_id")
186
188
  node_info.update({"class_name": node.class_name})
189
+ if hasattr(node, "name"):
190
+ node_info.update({"name": node.name})
187
191
  g.add_node(node.ln_id, **node_info)
188
192
 
189
193
  for _edge in self.internal_edges:
190
194
  edge_info = _edge.to_dict()
191
195
  edge_info.pop("ln_id")
192
196
  edge_info.update({"class_name": _edge.class_name})
197
+ if hasattr(_edge, "name"):
198
+ edge_info.update({"name": _edge.name})
193
199
  source_node_id = edge_info.pop("head")
194
200
  target_node_id = edge_info.pop("tail")
195
201
  g.add_edge(source_node_id, target_node_id, **edge_info)
196
202
 
197
203
  return g
198
204
 
199
- def display(self, **kwargs):
205
+ def display(self, node_label="class_name", edge_label="label", draw_kwargs={}, **kwargs):
200
206
  """Display the graph using NetworkX and Matplotlib."""
201
207
  from lionagi.libs import SysUtil
202
208
 
@@ -217,10 +223,11 @@ class Graph(Node):
217
223
  node_size=500,
218
224
  node_color="orange",
219
225
  alpha=0.9,
220
- labels=nx.get_node_attributes(g, "class_name"),
226
+ labels=nx.get_node_attributes(g, node_label),
227
+ **draw_kwargs
221
228
  )
222
229
 
223
- labels = nx.get_edge_attributes(g, "label")
230
+ labels = nx.get_edge_attributes(g, edge_label)
224
231
  labels = {k: v for k, v in labels.items() if v}
225
232
 
226
233
  if labels:
@@ -10,6 +10,7 @@ modifying, and removing edges, and querying related nodes and connections.
10
10
 
11
11
  from pydantic import Field
12
12
  from pandas import Series
13
+ from typing import Callable
13
14
 
14
15
  from lionagi.libs.ln_convert import to_list
15
16
 
@@ -122,6 +123,8 @@ class Node(Component, Relatable):
122
123
  condition: Condition | None = None,
123
124
  label: str | None = None,
124
125
  bundle: bool = False,
126
+ edge_class: Callable = Edge,
127
+ **kwargs
125
128
  ) -> None:
126
129
  """
127
130
  Establish directed relationship from this node to another.
@@ -141,12 +144,13 @@ class Node(Component, Relatable):
141
144
  f"Invalid value for direction: {direction}, " "must be 'in' or 'out'"
142
145
  )
143
146
 
144
- edge = Edge(
147
+ edge = edge_class(
145
148
  head=self if direction == "out" else node,
146
149
  tail=node if direction == "out" else self,
147
150
  condition=condition,
148
151
  bundle=bundle,
149
152
  label=label,
153
+ **kwargs
150
154
  )
151
155
 
152
156
  self.relations[direction].include(edge)
@@ -26,6 +26,7 @@ from typing import Any, List, Dict
26
26
  import contextlib
27
27
  from lionagi.core.collections.abc import Component, Field
28
28
  from ..collections.util import to_list_type
29
+ from lionagi.libs.ln_convert import to_str
29
30
 
30
31
 
31
32
  class BaseForm(Component):
@@ -177,7 +177,7 @@ class Form(BaseForm):
177
177
  f"""
178
178
  ## input: {i}:
179
179
  - description: {getattr(self._all_fields[i], "description", "N/A")}
180
- - value: {str(self.__getattribute__(self.input_fields[idx]))}
180
+ - value: {str(getattr(self, self.input_fields[idx]))}
181
181
  """
182
182
  for idx, i in enumerate(self.input_fields)
183
183
  )
@@ -27,7 +27,7 @@ class DirectiveMixin:
27
27
 
28
28
  async def chat(
29
29
  self,
30
- instruction, # additional instruction
30
+ instruction=None, # additional instruction
31
31
  context=None, # context to perform the instruction on
32
32
  system=None, # optionally swap system message
33
33
  sender=None, # sender of the instruction, default "user"
@@ -37,7 +37,7 @@ class PlanTemplate(BaseUnitForm):
37
37
  description="the generated step by step plan, return as a dictionary following {step_n: {plan: ..., reason: ...}} format",
38
38
  )
39
39
 
40
- signature: str = "task -> plan"
40
+ assignment: str = "task -> plan"
41
41
 
42
42
  @property
43
43
  def answer(self):
lionagi/core/work/work.py CHANGED
@@ -17,6 +17,7 @@ limitations under the License.
17
17
  from enum import Enum
18
18
  import asyncio
19
19
  from typing import Any
20
+ from collections.abc import Coroutine
20
21
 
21
22
  from lionagi.libs import SysUtil
22
23
  from lionagi.core.collections.abc import Component
@@ -39,7 +40,7 @@ class Work(Component):
39
40
  status (WorkStatus): The current status of the work.
40
41
  result (Any): The result of the work, if completed.
41
42
  error (Any): Any error encountered during the work.
42
- async_task (asyncio.Task | None): The asynchronous task associated with the work.
43
+ async_task (Coroutine | None): The asynchronous task associated with the work.
43
44
  completion_timestamp (str | None): The timestamp when the work was completed.
44
45
  duration (float | None): The duration of the work.
45
46
  """
@@ -47,7 +48,8 @@ class Work(Component):
47
48
  status: WorkStatus = WorkStatus.PENDING
48
49
  result: Any = None
49
50
  error: Any = None
50
- async_task: asyncio.Task | None = None
51
+ async_task: Coroutine | None = None
52
+ async_task_name: str | None = None
51
53
  completion_timestamp: str | None = None
52
54
  duration: float | None = None
53
55
 
@@ -0,0 +1,96 @@
1
+ from typing import Callable
2
+ from pydantic import Field, field_validator
3
+ import inspect
4
+
5
+ from lionagi.core.generic.edge import Edge
6
+ from lionagi.core.collections.abc.concepts import Progressable
7
+
8
+ from lionagi.core.work.worker import Worker
9
+
10
+
11
+ class WorkEdge(Edge, Progressable):
12
+ """
13
+ Represents a directed edge between work tasks, responsible for transforming
14
+ the result of one task into parameters for the next task.
15
+
16
+ Attributes:
17
+ convert_function (Callable): Function to transform the result of the previous
18
+ work into parameters for the next work. This function must be decorated
19
+ with the `worklink` decorator.
20
+ convert_function_kwargs (dict): Additional parameters for the convert_function
21
+ other than "from_work" and "from_result".
22
+ associated_worker (Worker): The worker to which this WorkEdge belongs.
23
+ """
24
+ convert_function: Callable = Field(
25
+ ...,
26
+ description="Function to transform the result of the previous work into parameters for the next work."
27
+ )
28
+
29
+ convert_function_kwargs: dict = Field(
30
+ {},
31
+ description="parameters for the worklink function other than \"from_work\" and \"from_result\""
32
+ )
33
+
34
+ associated_worker: Worker = Field(
35
+ ...,
36
+ description="The worker to which this WorkEdge belongs."
37
+ )
38
+
39
+ @field_validator("convert_function", mode="before")
40
+ def _validate_convert_funuction(cls, func):
41
+ """
42
+ Validates that the convert_function is decorated with the worklink decorator.
43
+
44
+ Args:
45
+ func (Callable): The function to validate.
46
+
47
+ Returns:
48
+ Callable: The validated function.
49
+
50
+ Raises:
51
+ ValueError: If the function is not decorated with the worklink decorator.
52
+ """
53
+ try:
54
+ getattr(func, "_worklink_decorator_params")
55
+ return func
56
+ except:
57
+ raise ValueError("convert_function must be a worklink decorated function")
58
+
59
+ @property
60
+ def name(self):
61
+ """
62
+ Returns the name of the convert_function.
63
+
64
+ Returns:
65
+ str: The name of the convert_function.
66
+ """
67
+ return self.convert_function.__name__
68
+
69
+ async def forward(self, task):
70
+ """
71
+ Transforms the result of the current work into parameters for the next work
72
+ and schedules the next work task.
73
+
74
+ Args:
75
+ task (Task): The task to process.
76
+
77
+ Returns:
78
+ Work: The next work task to be executed.
79
+
80
+ Raises:
81
+ StopIteration: If the task has no available steps left to proceed.
82
+ """
83
+ if task.available_steps == 0:
84
+ task.status_note = ("Task stopped proceeding further as all available steps have been used up, "
85
+ "but the task has not yet reached completion.")
86
+ return
87
+ func_signature = inspect.signature(self.convert_function)
88
+ kwargs = self.convert_function_kwargs.copy()
89
+ if "from_work" in func_signature.parameters:
90
+ kwargs = {"from_work": task.current_work} | kwargs
91
+ if "from_result" in func_signature.parameters:
92
+ kwargs = {"from_result": task.current_work.result} | kwargs
93
+
94
+ self.convert_function.auto_schedule = True
95
+ next_work = await self.convert_function(self=self.associated_worker, **kwargs)
96
+ return next_work
@@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
+ import asyncio
17
+ import logging
16
18
 
17
19
  from lionagi.libs.ln_func_call import rcall
18
20
  from lionagi.core.work.worklog import WorkLog
@@ -31,7 +33,7 @@ class WorkFunction:
31
33
  """
32
34
 
33
35
  def __init__(
34
- self, assignment, function, retry_kwargs=None, guidance=None, capacity=10
36
+ self, assignment, function, retry_kwargs=None, guidance=None, capacity=10, refresh_time=1
35
37
  ):
36
38
  """
37
39
  Initializes a WorkFunction instance.
@@ -43,12 +45,15 @@ class WorkFunction:
43
45
  Defaults to None.
44
46
  guidance (str, optional): The guidance or documentation for the function.
45
47
  Defaults to None.
46
- capacity (int, optional): The capacity of the work log. Defaults to 10.
48
+ capacity (int, optional): The capacity of the work queue batch processing.
49
+ Defaults to 10.
50
+ refresh_time (int, optional): The time interval to refresh the work log queue.
51
+ Defaults to 1.
47
52
  """
48
53
  self.assignment = assignment
49
54
  self.function = function
50
55
  self.retry_kwargs = retry_kwargs or {}
51
- self.worklog = WorkLog(capacity)
56
+ self.worklog = WorkLog(capacity, refresh_time=refresh_time)
52
57
  self.guidance = guidance or self.function.__doc__
53
58
 
54
59
  @property
@@ -61,6 +66,16 @@ class WorkFunction:
61
66
  """
62
67
  return self.function.__name__
63
68
 
69
+ @property
70
+ def execution_mode(self):
71
+ """
72
+ Gets the execution mode of the work function's queue.
73
+
74
+ Returns:
75
+ bool: The execution mode of the work function's queue.
76
+ """
77
+ return self.worklog.queue.execution_mode
78
+
64
79
  def is_progressable(self):
65
80
  """
66
81
  Checks if the work function is progressable.
@@ -86,7 +101,24 @@ class WorkFunction:
86
101
 
87
102
  async def forward(self):
88
103
  """
89
- Forwards the work log and processes the work queue.
104
+ Forward the work log to work queue.
90
105
  """
91
106
  await self.worklog.forward()
107
+
108
+ async def process(self):
109
+ """
110
+ Process the first capacity_size works in the work queue.
111
+ """
92
112
  await self.worklog.queue.process()
113
+
114
+ async def execute(self):
115
+ """
116
+ Starts the execution of the work function's queue.
117
+ """
118
+ asyncio.create_task(self.worklog.queue.execute())
119
+
120
+ async def stop(self):
121
+ """
122
+ Stops the execution of the work function's queue.
123
+ """
124
+ await self.worklog.stop()
@@ -0,0 +1,44 @@
1
+ from pydantic import Field
2
+ import asyncio
3
+
4
+ from lionagi.core.generic.node import Node
5
+ from lionagi.core.work.work_function import WorkFunction
6
+ from lionagi.core.collections import Exchange
7
+ from lionagi.core.mail.mail import Mail
8
+
9
+
10
+ class WorkFunctionNode(WorkFunction, Node):
11
+ """
12
+ A class representing a work function node, combining the functionality
13
+ of a WorkFunction and a Node.
14
+
15
+ Attributes:
16
+ assignment (str): The assignment description of the work function.
17
+ function (Callable): The function to be performed.
18
+ retry_kwargs (dict): The retry arguments for the function.
19
+ guidance (str): The guidance or documentation for the function.
20
+ capacity (int): The capacity of the work queue batch processing.
21
+ refresh_time (int): The time interval to refresh the work log queue.
22
+ """
23
+
24
+ def __init__(self, assignment, function, retry_kwargs=None, guidance=None, capacity=10, refresh_time=1, **kwargs):
25
+ """
26
+ Initializes a WorkFunctionNode instance.
27
+
28
+ Args:
29
+ assignment (str): The assignment description of the work function.
30
+ function (Callable): The function to be performed.
31
+ retry_kwargs (dict, optional): The retry arguments for the function. Defaults to None.
32
+ guidance (str, optional): The guidance or documentation for the function. Defaults to None.
33
+ capacity (int, optional): The capacity of the work queue batch processing. Defaults to 10.
34
+ refresh_time (int, optional): The time interval to refresh the work log queue. Defaults to 1.
35
+ **kwargs: Additional keyword arguments for the Node initialization.
36
+ """
37
+ Node.__init__(self, **kwargs)
38
+ WorkFunction.__init__(self,
39
+ assignment=assignment,
40
+ function=function,
41
+ retry_kwargs=retry_kwargs,
42
+ guidance=guidance,
43
+ capacity=capacity,
44
+ refresh_time=refresh_time)
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  """
16
16
 
17
17
  import asyncio
18
+ from lionagi.core.work.work import WorkStatus
18
19
 
19
20
 
20
21
  class WorkQueue:
@@ -24,16 +25,33 @@ class WorkQueue:
24
25
  Attributes:
25
26
  capacity (int): The maximum number of tasks the queue can handle.
26
27
  queue (asyncio.Queue): The queue holding the tasks.
27
- _stop_event (asyncio.Event): Event to signal stopping of the queue.
28
- semaphore (asyncio.Semaphore): Semaphore to control access based on capacity.
28
+ _stop_event (asyncio.Event): Event to signal stopping the execution of the queue.
29
+ available_capacity (int): The remaining number of tasks the queue can handle.
30
+ execution_mode (bool): If `execute` is running.
31
+ refresh_time (int): The time interval between task processing.
29
32
  """
30
33
 
31
- def __init__(self, capacity=5):
32
-
34
+ def __init__(self, capacity=5, refresh_time=1):
35
+ """
36
+ Initializes a new instance of WorkQueue.
37
+
38
+ Args:
39
+ capacity (int): The maximum number of tasks the queue can handle.
40
+ refresh_time (int): The time interval between task processing.
41
+
42
+ Raises:
43
+ ValueError: If capacity is less than 0 or refresh_time is negative.
44
+ """
45
+ if capacity < 0:
46
+ raise ValueError("initial capacity must be >= 0")
47
+ if refresh_time < 0:
48
+ raise ValueError("refresh time for execution can not be negative")
49
+ self.capacity = capacity
33
50
  self.queue = asyncio.Queue()
34
51
  self._stop_event = asyncio.Event()
35
- self.capacity = capacity
36
- self.semaphore = asyncio.Semaphore(capacity)
52
+ self.available_capacity = capacity
53
+ self.execution_mode = False
54
+ self.refresh_time = refresh_time
37
55
 
38
56
  async def enqueue(self, work) -> None:
39
57
  """Enqueue a work item."""
@@ -51,12 +69,6 @@ class WorkQueue:
51
69
  """Signal the queue to stop processing."""
52
70
  self._stop_event.set()
53
71
 
54
- @property
55
- def available_capacity(self):
56
- """Return the available capacity of the queue."""
57
- available = self.capacity - self.queue.qsize()
58
- return available if available > 0 else None
59
-
60
72
  @property
61
73
  def stopped(self) -> bool:
62
74
  """Return whether the queue has been stopped."""
@@ -65,17 +77,29 @@ class WorkQueue:
65
77
  async def process(self) -> None:
66
78
  """Process the work items in the queue."""
67
79
  tasks = set()
68
- while self.queue.qsize() > 0 and not self.stopped:
69
- if not self.available_capacity and tasks:
70
- _, done = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
71
- tasks.difference_update(done)
72
-
73
- async with self.semaphore:
74
- next = await self.dequeue()
75
- if next is None:
76
- break
77
- task = asyncio.create_task(next.perform())
78
- tasks.add(task)
79
-
80
- if tasks:
81
- await asyncio.wait(tasks)
80
+ while self.available_capacity > 0 and self.queue.qsize() > 0:
81
+ next = await self.dequeue()
82
+ next.status = WorkStatus.IN_PROGRESS
83
+ task = asyncio.create_task(next.perform())
84
+ tasks.add(task)
85
+ self.available_capacity -= 1
86
+
87
+ if tasks:
88
+ await asyncio.wait(tasks)
89
+ self.available_capacity = self.capacity
90
+
91
+ async def execute(self):
92
+ """
93
+ Continuously executes the process method at a specified refresh interval.
94
+
95
+ Args:
96
+ refresh_time (int, optional): The time in seconds to wait between
97
+ successive calls to `process`. Defaults to 1.
98
+ """
99
+ self.execution_mode = True
100
+ self._stop_event.clear()
101
+
102
+ while not self.stopped:
103
+ await self.process()
104
+ await asyncio.sleep(self.refresh_time)
105
+ self.execution_mode = False
@@ -0,0 +1,155 @@
1
+ import inspect
2
+ from typing import Any, Callable
3
+ from pydantic import Field, field_validator, model_validator
4
+
5
+ from lionagi.core.collections.abc.component import Component
6
+ from lionagi.core.work.work import WorkStatus, Work
7
+ from collections.abc import Coroutine
8
+
9
+
10
+ class WorkTask(Component):
11
+ """
12
+ A class representing a work task that can be processed in multiple steps.
13
+
14
+ Attributes:
15
+ name (str | None): The name of the task.
16
+ status (WorkStatus): The current status of the task.
17
+ status_note (str): A note for the task's current status.
18
+ work_history (list[Work]): A list of works processed in this task.
19
+ max_steps (int | None): The maximum number of works allowed in this task.
20
+ current_work (Work | None): The current work in progress.
21
+ post_processing (Callable | None): The post-processing function to be executed after the entire task is successfully completed.
22
+ """
23
+ name: str | None = Field(
24
+ None,
25
+ description="Name of the task"
26
+ )
27
+
28
+ status: WorkStatus = Field(
29
+ WorkStatus.PENDING,
30
+ description="The current status of the task"
31
+ )
32
+
33
+ status_note: str = Field(
34
+ None,
35
+ description="Note for tasks current status"
36
+ )
37
+
38
+ work_history: list[Work] = Field(
39
+ [],
40
+ description="List of works processed"
41
+ )
42
+
43
+ max_steps: int | None = Field(
44
+ 10,
45
+ description="Maximum number of works allowed"
46
+ )
47
+
48
+ current_work: Work | None = Field(
49
+ None,
50
+ description="The current work in progress"
51
+ )
52
+
53
+ post_processing: Callable | None = Field(
54
+ None,
55
+ description="The post-processing function to be executed after the entire task has been successfully completed."
56
+ )
57
+
58
+ @field_validator("max_steps", mode="before")
59
+ def _validate_max_steps(cls, value):
60
+ """
61
+ Validates that max_steps is a positive integer.
62
+
63
+ Args:
64
+ value (int): The value to validate.
65
+
66
+ Returns:
67
+ int: The validated value.
68
+
69
+ Raises:
70
+ ValueError: If value is not a positive integer.
71
+ """
72
+ if value <= 0:
73
+ raise ValueError("Invalid value: max_steps must be a positive integer.")
74
+ return value
75
+
76
+ @field_validator("post_processing", mode="before")
77
+ def _validate_prost_processing(cls, value):
78
+ """
79
+ Validates that post_processing is an asynchronous function.
80
+
81
+ Args:
82
+ value (Callable): The value to validate.
83
+
84
+ Returns:
85
+ Callable: The validated value.
86
+
87
+ Raises:
88
+ ValueError: If value is not an asynchronous function.
89
+ """
90
+ if value is not None and not inspect.iscoroutinefunction((value)):
91
+ raise ValueError("post_processing must be a async function")
92
+ return value
93
+
94
+ @property
95
+ def available_steps(self):
96
+ """
97
+ Calculates the number of available steps left in the task.
98
+
99
+ Returns:
100
+ int: The number of available steps.
101
+ """
102
+ return max(0, self.max_steps - len(self.work_history))
103
+
104
+ def clone(self):
105
+ """
106
+ Creates a clone of the current WorkTask instance.
107
+
108
+ Returns:
109
+ WorkTask: A new instance of WorkTask with the same attributes.
110
+ """
111
+ new_worktask = WorkTask(name=self.name, status=self.status, max_steps=self.max_steps, current_work=self.current_work)
112
+ for work in self.work_history:
113
+ new_worktask.work_history.append(work)
114
+ return new_worktask
115
+
116
+ async def process(self, current_func_node):
117
+ """
118
+ Processes the current work function node.
119
+
120
+ Args:
121
+ current_func_node (WorkFunctionNode): The current function node being processed.
122
+
123
+ Returns:
124
+ str | list[WorkTask]: Returns "COMPLETED", "FAILED", or a list of new WorkTask instances if there are next works to process.
125
+ """
126
+ if self.current_work.status == WorkStatus.FAILED:
127
+ self.status = WorkStatus.FAILED
128
+ self.status_note = f"Work {self.current_work.ln_id} failed. Error: {self.current_work.error}"
129
+ self.current_work = None
130
+ return "FAILED"
131
+ elif self.current_work.status == WorkStatus.COMPLETED:
132
+ next_works = []
133
+ if not current_func_node.relations["out"].is_empty():
134
+ for workedge in current_func_node.relations["out"]:
135
+ next_work = await workedge.forward(self)
136
+ if next_work is not None:
137
+ next_works.append(next_work)
138
+ if len(next_works) == 0:
139
+ self.current_work = None
140
+ self.status = WorkStatus.COMPLETED
141
+ if self.post_processing:
142
+ await self.post_processing(self)
143
+ return "COMPLETED"
144
+ else:
145
+ return_tasks = []
146
+ for i in reversed(range(len(next_works))):
147
+ if i == 0:
148
+ self.current_work = next_works[i]
149
+ self.work_history.append(next_works[i])
150
+ else:
151
+ clone_task = self.clone()
152
+ clone_task.current_work = next_works[i]
153
+ clone_task.work_history.append(next_works[i])
154
+ return_tasks.append(clone_task)
155
+ return return_tasks