lionagi 0.2.0__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/report/form.py +1 -1
- lionagi/core/session/directive_mixin.py +1 -1
- lionagi/core/unit/template/plan.py +1 -1
- lionagi/core/work/work.py +4 -2
- lionagi/core/work/work_edge.py +96 -0
- lionagi/core/work/work_function.py +36 -4
- lionagi/core/work/work_function_node.py +44 -0
- lionagi/core/work/work_queue.py +50 -26
- lionagi/core/work/work_task.py +155 -0
- lionagi/core/work/worker.py +225 -37
- lionagi/core/work/worker_engine.py +179 -0
- lionagi/core/work/worklog.py +9 -11
- 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.0.dist-info → lionagi-0.2.2.dist-info}/METADATA +5 -4
- {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/RECORD +32 -18
- {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/LICENSE +0 -0
- {lionagi-0.2.0.dist-info → lionagi-0.2.2.dist-info}/WHEEL +0 -0
- {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",
|
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
lionagi/core/report/form.py
CHANGED
@@ -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(
|
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
|
-
|
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 (
|
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:
|
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
|
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
|
-
|
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)
|
lionagi/core/work/work_queue.py
CHANGED
@@ -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
|
-
|
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.
|
36
|
-
self.
|
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
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|