lionagi 0.2.1__py3-none-any.whl → 0.2.2__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.
- lionagi/__init__.py +2 -1
- lionagi/core/generic/graph.py +10 -3
- lionagi/core/generic/node.py +5 -1
- lionagi/core/report/base.py +1 -0
- lionagi/core/work/work_edge.py +96 -0
- lionagi/core/work/work_function.py +29 -6
- lionagi/core/work/work_function_node.py +44 -0
- lionagi/core/work/work_queue.py +20 -18
- lionagi/core/work/work_task.py +155 -0
- lionagi/core/work/worker.py +164 -39
- lionagi/core/work/worker_engine.py +179 -0
- lionagi/core/work/worklog.py +5 -3
- lionagi/tests/test_core/generic/test_structure.py +193 -0
- lionagi/tests/test_core/graph/__init__.py +0 -0
- lionagi/tests/test_core/graph/test_graph.py +70 -0
- lionagi/tests/test_core/graph/test_tree.py +75 -0
- lionagi/tests/test_core/mail/__init__.py +0 -0
- lionagi/tests/test_core/mail/test_mail.py +62 -0
- lionagi/tests/test_core/test_structure/__init__.py +0 -0
- lionagi/tests/test_core/test_structure/test_base_structure.py +196 -0
- lionagi/tests/test_core/test_structure/test_graph.py +54 -0
- lionagi/tests/test_core/test_structure/test_tree.py +48 -0
- lionagi/version.py +1 -1
- {lionagi-0.2.1.dist-info → lionagi-0.2.2.dist-info}/METADATA +5 -4
- {lionagi-0.2.1.dist-info → lionagi-0.2.2.dist-info}/RECORD +28 -14
- {lionagi-0.2.1.dist-info → lionagi-0.2.2.dist-info}/LICENSE +0 -0
- {lionagi-0.2.1.dist-info → lionagi-0.2.2.dist-info}/WHEEL +0 -0
- {lionagi-0.2.1.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",
|
lionagi/core/generic/graph.py
CHANGED
@@ -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,
|
226
|
+
labels=nx.get_node_attributes(g, node_label),
|
227
|
+
**draw_kwargs
|
221
228
|
)
|
222
229
|
|
223
|
-
labels = nx.get_edge_attributes(g,
|
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:
|
lionagi/core/generic/node.py
CHANGED
@@ -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 =
|
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)
|
lionagi/core/report/base.py
CHANGED
@@ -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,14 +13,12 @@ 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
|
16
17
|
import logging
|
17
18
|
|
18
19
|
from lionagi.libs.ln_func_call import rcall
|
19
|
-
from lionagi.libs.ln_parse import ParseUtil
|
20
20
|
from lionagi.core.work.worklog import WorkLog
|
21
21
|
|
22
|
-
from lionagi.core.report.form import Form
|
23
|
-
|
24
22
|
|
25
23
|
class WorkFunction:
|
26
24
|
"""
|
@@ -35,7 +33,7 @@ class WorkFunction:
|
|
35
33
|
"""
|
36
34
|
|
37
35
|
def __init__(
|
38
|
-
self, assignment, function, retry_kwargs=None, guidance=None, capacity=10
|
36
|
+
self, assignment, function, retry_kwargs=None, guidance=None, capacity=10, refresh_time=1
|
39
37
|
):
|
40
38
|
"""
|
41
39
|
Initializes a WorkFunction instance.
|
@@ -47,12 +45,15 @@ class WorkFunction:
|
|
47
45
|
Defaults to None.
|
48
46
|
guidance (str, optional): The guidance or documentation for the function.
|
49
47
|
Defaults to None.
|
50
|
-
capacity (int, optional): The capacity of the work
|
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.
|
51
52
|
"""
|
52
53
|
self.assignment = assignment
|
53
54
|
self.function = function
|
54
55
|
self.retry_kwargs = retry_kwargs or {}
|
55
|
-
self.worklog = WorkLog(capacity)
|
56
|
+
self.worklog = WorkLog(capacity, refresh_time=refresh_time)
|
56
57
|
self.guidance = guidance or self.function.__doc__
|
57
58
|
|
58
59
|
@property
|
@@ -65,6 +66,16 @@ class WorkFunction:
|
|
65
66
|
"""
|
66
67
|
return self.function.__name__
|
67
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
|
+
|
68
79
|
def is_progressable(self):
|
69
80
|
"""
|
70
81
|
Checks if the work function is progressable.
|
@@ -99,3 +110,15 @@ class WorkFunction:
|
|
99
110
|
Process the first capacity_size works in the work queue.
|
100
111
|
"""
|
101
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)
|
lionagi/core/work/work_queue.py
CHANGED
@@ -25,19 +25,33 @@ class WorkQueue:
|
|
25
25
|
Attributes:
|
26
26
|
capacity (int): The maximum number of tasks the queue can handle.
|
27
27
|
queue (asyncio.Queue): The queue holding the tasks.
|
28
|
-
_stop_event (asyncio.Event): Event to signal stopping
|
28
|
+
_stop_event (asyncio.Event): Event to signal stopping the execution of the queue.
|
29
29
|
available_capacity (int): The remaining number of tasks the queue can handle.
|
30
30
|
execution_mode (bool): If `execute` is running.
|
31
|
+
refresh_time (int): The time interval between task processing.
|
31
32
|
"""
|
32
33
|
|
33
|
-
def __init__(self, capacity=5):
|
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
|
+
"""
|
34
45
|
if capacity < 0:
|
35
46
|
raise ValueError("initial capacity must be >= 0")
|
47
|
+
if refresh_time < 0:
|
48
|
+
raise ValueError("refresh time for execution can not be negative")
|
36
49
|
self.capacity = capacity
|
37
50
|
self.queue = asyncio.Queue()
|
38
51
|
self._stop_event = asyncio.Event()
|
39
52
|
self.available_capacity = capacity
|
40
53
|
self.execution_mode = False
|
54
|
+
self.refresh_time = refresh_time
|
41
55
|
|
42
56
|
async def enqueue(self, work) -> None:
|
43
57
|
"""Enqueue a work item."""
|
@@ -60,20 +74,6 @@ class WorkQueue:
|
|
60
74
|
"""Return whether the queue has been stopped."""
|
61
75
|
return self._stop_event.is_set()
|
62
76
|
|
63
|
-
# async def process(self):
|
64
|
-
# async def _parse_work(work):
|
65
|
-
# async with self.semaphore:
|
66
|
-
# await work.perform()
|
67
|
-
#
|
68
|
-
# tasks = set()
|
69
|
-
# while self.queue.qsize() > 0:
|
70
|
-
# next = await self.dequeue()
|
71
|
-
# next.status = WorkStatus.IN_PROGRESS
|
72
|
-
# task = asyncio.create_task(_parse_work(next))
|
73
|
-
# tasks.add(task)
|
74
|
-
#
|
75
|
-
# await asyncio.wait(tasks)
|
76
|
-
|
77
77
|
async def process(self) -> None:
|
78
78
|
"""Process the work items in the queue."""
|
79
79
|
tasks = set()
|
@@ -88,7 +88,7 @@ class WorkQueue:
|
|
88
88
|
await asyncio.wait(tasks)
|
89
89
|
self.available_capacity = self.capacity
|
90
90
|
|
91
|
-
async def execute(self
|
91
|
+
async def execute(self):
|
92
92
|
"""
|
93
93
|
Continuously executes the process method at a specified refresh interval.
|
94
94
|
|
@@ -97,7 +97,9 @@ class WorkQueue:
|
|
97
97
|
successive calls to `process`. Defaults to 1.
|
98
98
|
"""
|
99
99
|
self.execution_mode = True
|
100
|
+
self._stop_event.clear()
|
101
|
+
|
100
102
|
while not self.stopped:
|
101
103
|
await self.process()
|
102
|
-
await asyncio.sleep(refresh_time)
|
104
|
+
await asyncio.sleep(self.refresh_time)
|
103
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
|