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/core/work/worker.py
CHANGED
@@ -16,11 +16,12 @@ limitations under the License.
|
|
16
16
|
|
17
17
|
from abc import ABC
|
18
18
|
from functools import wraps
|
19
|
-
import
|
19
|
+
import inspect
|
20
20
|
from lionagi import logging as _logging
|
21
|
-
from lionagi.libs.ln_func_call import pcall
|
22
21
|
from lionagi.core.work.work_function import WorkFunction
|
23
22
|
from lionagi.core.work.work import Work
|
23
|
+
from lionagi.core.report.form import Form
|
24
|
+
from lionagi.core.collections.abc import get_lion_id
|
24
25
|
|
25
26
|
|
26
27
|
class Worker(ABC):
|
@@ -30,19 +31,30 @@ class Worker(ABC):
|
|
30
31
|
Attributes:
|
31
32
|
name (str): The name of the worker.
|
32
33
|
work_functions (dict[str, WorkFunction]): Dictionary mapping assignments to WorkFunction objects.
|
34
|
+
forms (dict[str, Form]): Dictionary mapping form identifier to Form objects.
|
35
|
+
default_form (str|None): The default form to be used by the worker.
|
33
36
|
"""
|
34
37
|
|
35
38
|
name: str = "Worker"
|
36
|
-
work_functions: dict[str, WorkFunction] = {}
|
37
39
|
|
38
|
-
def __init__(self) -> None:
|
39
|
-
|
40
|
+
def __init__(self, forms=None, default_form=None) -> None:
|
41
|
+
"""
|
42
|
+
Initializes a new instance of Worker.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
forms (dict[str, Form], optional): Dictionary mapping form identifier to Form objects.
|
46
|
+
default_form (str|None, optional): The default form to be used by the worker.
|
47
|
+
"""
|
48
|
+
self.work_functions: dict[str, WorkFunction] = {}
|
49
|
+
self.forms: dict[str, Form] = forms or {}
|
50
|
+
self.default_form = default_form
|
51
|
+
self._validate_worklink_functions()
|
40
52
|
|
41
53
|
async def stop(self):
|
42
54
|
"""
|
43
55
|
Stops the worker and all associated work functions.
|
44
56
|
"""
|
45
|
-
self.stopped = True
|
57
|
+
# self.stopped = True
|
46
58
|
_logging.info(f"Stopping worker {self.name}")
|
47
59
|
non_stopped_ = []
|
48
60
|
|
@@ -69,36 +81,73 @@ class Worker(ABC):
|
|
69
81
|
and not self.stopped
|
70
82
|
)
|
71
83
|
|
72
|
-
async def
|
84
|
+
async def change_default_form(self, form_key):
|
73
85
|
"""
|
74
|
-
|
86
|
+
Changes the default form to the specified form key.
|
75
87
|
|
76
88
|
Args:
|
77
|
-
|
89
|
+
form_key (str): The key of the form to set as the default.
|
90
|
+
|
91
|
+
Raises:
|
92
|
+
ValueError: If the form key does not exist in the forms dictionary.
|
93
|
+
|
94
|
+
"""
|
95
|
+
if form_key not in self.forms.keys():
|
96
|
+
raise ValueError(f"Unable to change default form. Key {form_key} does not exist.")
|
97
|
+
self.default_form = self.forms[form_key]
|
98
|
+
|
99
|
+
def _get_decorated_functions(self, decorator_attr, name_only=True):
|
78
100
|
"""
|
79
|
-
|
80
|
-
|
81
|
-
|
101
|
+
Retrieves decorated functions based on the specified decorator attribute.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
decorator_attr (str): The attribute name of the decorator.
|
105
|
+
name_only (bool, optional): Whether to return only the function names. Defaults to True.
|
82
106
|
|
83
|
-
|
107
|
+
Returns:
|
108
|
+
list: List of decorated function names or tuples containing function details.
|
109
|
+
"""
|
110
|
+
decorated_functions = []
|
111
|
+
for name, func in inspect.getmembers(self.__class__, predicate=inspect.isfunction):
|
112
|
+
if hasattr(func, decorator_attr):
|
113
|
+
if name_only:
|
114
|
+
decorated_functions.append(name)
|
115
|
+
else:
|
116
|
+
decorator_params = getattr(func, decorator_attr)
|
117
|
+
decorated_functions.append((name, func, decorator_params))
|
118
|
+
return decorated_functions
|
119
|
+
|
120
|
+
def _validate_worklink_functions(self):
|
121
|
+
"""
|
122
|
+
Validates worklink functions to ensure they have the required parameters.
|
123
|
+
"""
|
124
|
+
worklink_decorated_function = self._get_decorated_functions(decorator_attr="_worklink_decorator_params", name_only=False)
|
125
|
+
for func_name, func, _ in worklink_decorated_function:
|
126
|
+
func_signature = inspect.signature(func)
|
127
|
+
if "from_work" not in func_signature.parameters and "from_result" not in func_signature.parameters:
|
128
|
+
raise ValueError(f"Either \"from_work\" or \"from_result\" must be a parameter in function {func_name}")
|
84
129
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
130
|
+
def construct_all_work_functions(self):
|
131
|
+
"""
|
132
|
+
Constructs all work functions for the worker.
|
133
|
+
"""
|
134
|
+
if getattr(self, "work_functions", None) is None:
|
135
|
+
self.work_functions = {}
|
136
|
+
work_decorated_function = self._get_decorated_functions(decorator_attr="_work_decorator_params", name_only=False)
|
137
|
+
for func_name, func, dec_params in work_decorated_function:
|
138
|
+
if func_name not in self.work_functions:
|
139
|
+
self.work_functions[func_name] = WorkFunction(**dec_params)
|
93
140
|
|
94
|
-
async def
|
141
|
+
async def _work_wrapper(
|
95
142
|
self,
|
96
143
|
*args,
|
97
|
-
|
144
|
+
function=None,
|
98
145
|
assignment=None,
|
146
|
+
form_param_key=None,
|
99
147
|
capacity=None,
|
100
148
|
retry_kwargs=None,
|
101
149
|
guidance=None,
|
150
|
+
refresh_time=1,
|
102
151
|
**kwargs,
|
103
152
|
):
|
104
153
|
"""
|
@@ -107,47 +156,90 @@ class Worker(ABC):
|
|
107
156
|
Args:
|
108
157
|
func (Callable): The function to be executed.
|
109
158
|
assignment (str): The assignment description.
|
110
|
-
|
159
|
+
form_param_key (str): The key to identify the form parameter in
|
160
|
+
the function's signature. This parameter is used to locate and fill
|
161
|
+
the appropriate form according to the assignment. Raises an error
|
162
|
+
if the form parameter key is not found in the function's signature.
|
163
|
+
capacity (int): Capacity for the work queue batch processing.
|
111
164
|
retry_kwargs (dict): Retry arguments for the function.
|
112
165
|
guidance (str): Guidance or documentation for the function.
|
113
166
|
"""
|
114
167
|
if getattr(self, "work_functions", None) is None:
|
115
168
|
self.work_functions = {}
|
116
169
|
|
117
|
-
if
|
118
|
-
self.work_functions[
|
170
|
+
if function.__name__ not in self.work_functions:
|
171
|
+
self.work_functions[function.__name__] = WorkFunction(
|
119
172
|
assignment=assignment,
|
120
|
-
function=
|
173
|
+
function=function,
|
121
174
|
retry_kwargs=retry_kwargs or {},
|
122
|
-
guidance=guidance or
|
175
|
+
guidance=guidance or function.__doc__,
|
123
176
|
capacity=capacity,
|
177
|
+
refresh_time=refresh_time
|
124
178
|
)
|
125
179
|
|
126
|
-
work_func: WorkFunction = self.work_functions[
|
127
|
-
|
128
|
-
|
180
|
+
work_func: WorkFunction = self.work_functions[function.__name__]
|
181
|
+
|
182
|
+
# locate form that should be filled according to the assignment
|
183
|
+
if form_param_key:
|
184
|
+
func_signature = inspect.signature(function)
|
185
|
+
if form_param_key not in func_signature.parameters:
|
186
|
+
raise KeyError(f"Failed to locate form. \"{form_param_key}\" is not defined in the function.")
|
187
|
+
if "self" in func_signature.parameters:
|
188
|
+
bound_args = func_signature.bind(None, *args, **kwargs)
|
189
|
+
else:
|
190
|
+
bound_args = func_signature.bind(*args, **kwargs)
|
191
|
+
bound_args.apply_defaults()
|
192
|
+
arguments = bound_args.arguments
|
193
|
+
|
194
|
+
form_key = arguments.get(form_param_key)
|
195
|
+
try:
|
196
|
+
form_key = get_lion_id(form_key)
|
197
|
+
except:
|
198
|
+
pass
|
199
|
+
form = self.forms.get(form_key) or self.default_form
|
200
|
+
|
201
|
+
if form:
|
202
|
+
subform = form.__class__(assignment=work_func.assignment, task=work_func.guidance)
|
203
|
+
for k in subform.input_fields:
|
204
|
+
v = getattr(form, k, None)
|
205
|
+
setattr(subform, k, v)
|
206
|
+
subform.origin = form
|
207
|
+
kwargs = {"form": subform} | kwargs
|
208
|
+
else:
|
209
|
+
raise ValueError(f"Cannot locate form in Worker's forms and default_form is not available.")
|
210
|
+
|
211
|
+
task = work_func.perform(self, *args, **kwargs)
|
212
|
+
work = Work(async_task=task, async_task_name=work_func.name)
|
129
213
|
await work_func.worklog.append(work)
|
130
|
-
return
|
214
|
+
return work
|
131
215
|
|
132
216
|
|
133
217
|
def work(
|
134
218
|
assignment=None,
|
219
|
+
form_param_key=None,
|
135
220
|
capacity=10,
|
136
221
|
guidance=None,
|
137
222
|
retry_kwargs=None,
|
138
|
-
refresh_time=1,
|
139
223
|
timeout=10,
|
224
|
+
refresh_time=1
|
140
225
|
):
|
141
226
|
"""
|
142
227
|
Decorator to mark a method as a work function.
|
143
228
|
|
144
229
|
Args:
|
145
230
|
assignment (str): The assignment description of the work function.
|
146
|
-
|
231
|
+
form_param_key (str): The key to identify the form parameter in
|
232
|
+
the function's signature. This parameter is used to locate and fill
|
233
|
+
the appropriate form according to the assignment. Raises an error
|
234
|
+
if the form parameter key is not found in the function's signature.
|
235
|
+
capacity (int): Capacity for the work queue batch processing.
|
147
236
|
guidance (str): Guidance or documentation for the work function.
|
148
237
|
retry_kwargs (dict): Retry arguments for the work function.
|
149
|
-
refresh_time (int): Time interval between each process cycle.
|
150
238
|
timeout (int): Timeout for the work function.
|
239
|
+
refresh_time (int, optional): Refresh time for the work log queue.
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
Callable: The decorated function.
|
151
243
|
"""
|
152
244
|
|
153
245
|
def decorator(func):
|
@@ -157,22 +249,118 @@ def work(
|
|
157
249
|
*args,
|
158
250
|
func=func,
|
159
251
|
assignment=assignment,
|
252
|
+
form_param_key=form_param_key,
|
160
253
|
capacity=capacity,
|
161
254
|
retry_kwargs=retry_kwargs,
|
162
255
|
guidance=guidance,
|
256
|
+
refresh_time=refresh_time,
|
163
257
|
**kwargs,
|
164
258
|
):
|
259
|
+
if not inspect.iscoroutinefunction(func):
|
260
|
+
raise TypeError(f"{func.__name__} must be an asynchronous function")
|
165
261
|
retry_kwargs = retry_kwargs or {}
|
166
262
|
retry_kwargs["timeout"] = retry_kwargs.get("timeout", timeout)
|
167
|
-
return await self.
|
263
|
+
return await self._work_wrapper(
|
168
264
|
*args,
|
169
|
-
|
265
|
+
function=func,
|
170
266
|
assignment=assignment,
|
267
|
+
form_param_key=form_param_key,
|
171
268
|
capacity=capacity,
|
172
269
|
retry_kwargs=retry_kwargs,
|
173
270
|
guidance=guidance,
|
271
|
+
refresh_time=refresh_time,
|
174
272
|
**kwargs,
|
175
273
|
)
|
274
|
+
wrapper._work_decorator_params = {"assignment": assignment,
|
275
|
+
"function": func,
|
276
|
+
"retry_kwargs": retry_kwargs,
|
277
|
+
"guidance": guidance,
|
278
|
+
"capacity": capacity,
|
279
|
+
"refresh_time": refresh_time}
|
280
|
+
|
281
|
+
return wrapper
|
282
|
+
|
283
|
+
return decorator
|
284
|
+
|
285
|
+
|
286
|
+
def worklink(
|
287
|
+
from_: str,
|
288
|
+
to_: str,
|
289
|
+
auto_schedule: bool = True
|
290
|
+
):
|
291
|
+
"""
|
292
|
+
Decorator to create a link between two work functions.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
from_ (str): The name of the source work function.
|
296
|
+
to_ (str): The name of the target work function.
|
297
|
+
auto_schedule (bool, optional): Whether to automatically schedule the next work function. Defaults to True.
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
Callable: The decorated function.
|
301
|
+
"""
|
302
|
+
def decorator(func):
|
303
|
+
@wraps(func)
|
304
|
+
async def wrapper(
|
305
|
+
self: Worker,
|
306
|
+
*args,
|
307
|
+
func=func,
|
308
|
+
from_=from_,
|
309
|
+
to_=to_,
|
310
|
+
**kwargs
|
311
|
+
):
|
312
|
+
if not inspect.iscoroutinefunction(func):
|
313
|
+
raise TypeError(f"{func.__name__} must be an asynchronous function")
|
314
|
+
|
315
|
+
work_funcs = self._get_decorated_functions(decorator_attr="_work_decorator_params")
|
316
|
+
if from_ not in work_funcs or to_ not in work_funcs:
|
317
|
+
raise ValueError("Invalid link. 'from_' and 'to_' must be the name of work decorated functions.")
|
318
|
+
|
319
|
+
func_signature = inspect.signature(func)
|
320
|
+
if "from_work" not in func_signature.parameters and "from_result" not in func_signature.parameters:
|
321
|
+
raise ValueError(f"Either \"from_work\" or \"from_result\" must be a parameter in function {func.__name__}")
|
322
|
+
|
323
|
+
if "self" in func_signature.parameters:
|
324
|
+
bound_args = func_signature.bind(None, *args, **kwargs)
|
325
|
+
else:
|
326
|
+
bound_args = func_signature.bind(*args, **kwargs)
|
327
|
+
bound_args.apply_defaults()
|
328
|
+
arguments = bound_args.arguments
|
329
|
+
if "kwargs" in arguments:
|
330
|
+
arguments.update(arguments.pop("kwargs"))
|
331
|
+
|
332
|
+
if from_work := arguments.get("from_work"):
|
333
|
+
if not isinstance(from_work, Work):
|
334
|
+
raise ValueError("Invalid type for from_work. Only work objects are accepted.")
|
335
|
+
if from_work.async_task_name != from_:
|
336
|
+
raise ValueError(f"Invalid work object in from_work. "
|
337
|
+
f"async_task_name \"{from_work.async_task_name}\" does not match from_ \"{from_}\"")
|
338
|
+
|
339
|
+
next_params = await func(self, *args, **kwargs)
|
340
|
+
to_work_func = getattr(self, to_)
|
341
|
+
if next_params is None:
|
342
|
+
return
|
343
|
+
if isinstance(next_params, list):
|
344
|
+
if wrapper.auto_schedule:
|
345
|
+
return await to_work_func(*next_params)
|
346
|
+
elif isinstance(next_params, dict):
|
347
|
+
if wrapper.auto_schedule:
|
348
|
+
return await to_work_func(**next_params)
|
349
|
+
elif isinstance(next_params, tuple) and len(next_params) == 2:
|
350
|
+
if isinstance(next_params[0], list) and isinstance(next_params[1], dict):
|
351
|
+
if wrapper.auto_schedule:
|
352
|
+
return await to_work_func(*next_params[0], **next_params[1])
|
353
|
+
else:
|
354
|
+
raise TypeError(f"Invalid return type {func.__name__}")
|
355
|
+
else:
|
356
|
+
raise TypeError(f"Invalid return type {func.__name__}")
|
357
|
+
|
358
|
+
return next_params
|
359
|
+
|
360
|
+
wrapper.auto_schedule = auto_schedule
|
361
|
+
wrapper._worklink_decorator_params = {"func": func,
|
362
|
+
"from_": from_,
|
363
|
+
"to_": to_}
|
176
364
|
|
177
365
|
return wrapper
|
178
366
|
|
@@ -0,0 +1,179 @@
|
|
1
|
+
import asyncio
|
2
|
+
from lionagi.core.work.work import WorkStatus
|
3
|
+
from lionagi.core.work.worker import Worker
|
4
|
+
from lionagi.core.work.work_task import WorkTask
|
5
|
+
from lionagi.core.work.work_edge import WorkEdge
|
6
|
+
from lionagi.core.work.work_function_node import WorkFunctionNode
|
7
|
+
|
8
|
+
from lionagi.core.collections.pile import pile
|
9
|
+
from lionagi.core.generic.graph import Graph
|
10
|
+
|
11
|
+
|
12
|
+
class WorkerEngine:
|
13
|
+
"""
|
14
|
+
A class representing an engine that manages and executes work tasks for a worker.
|
15
|
+
|
16
|
+
Attributes:
|
17
|
+
worker (Worker): The worker instance that the engine manages.
|
18
|
+
tasks (Pile): A pile of tasks to be executed.
|
19
|
+
active_tasks (Pile): A pile of currently active tasks.
|
20
|
+
failed_tasks (Pile): A pile of tasks that have failed.
|
21
|
+
worker_graph (Graph): A graph representing the relationships between work functions.
|
22
|
+
refresh_time (int): The time interval for refreshing the work log queue.
|
23
|
+
_stop_event (asyncio.Event): An event to signal stopping the execution.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, worker: Worker, refresh_time=1):
|
27
|
+
"""
|
28
|
+
Initializes a new instance of WorkerEngine.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
worker (Worker): The worker instance to be managed.
|
32
|
+
refresh_time (int): The time interval for refreshing the work log queue.
|
33
|
+
"""
|
34
|
+
self.worker = worker
|
35
|
+
self.tasks = pile()
|
36
|
+
self.active_tasks = pile()
|
37
|
+
self.failed_tasks = pile()
|
38
|
+
self.worker_graph = Graph()
|
39
|
+
self._construct_work_functions()
|
40
|
+
self._construct_workedges()
|
41
|
+
self.refresh_time = refresh_time
|
42
|
+
self._stop_event = asyncio.Event()
|
43
|
+
|
44
|
+
async def add_task(self, *args, task_function: str, task_name=None, task_max_steps=10, task_post_processing=None, **kwargs):
|
45
|
+
"""
|
46
|
+
Adds a new task to the task queue.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
task_function (str): The name of the task function to execute.
|
50
|
+
task_name (str, optional): The name of the task.
|
51
|
+
task_max_steps (int, optional): The maximum number of steps for the task.
|
52
|
+
task_post_processing (Callable, optional): The post-processing function for the task.
|
53
|
+
*args: Positional arguments for the task function.
|
54
|
+
**kwargs: Keyword arguments for the task function.
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
WorkTask: The newly created task.
|
58
|
+
"""
|
59
|
+
task = WorkTask(name=task_name, max_steps=task_max_steps, post_processing=task_post_processing)
|
60
|
+
self.tasks.append(task)
|
61
|
+
function = getattr(self.worker, task_function)
|
62
|
+
work = await function(*args, **kwargs)
|
63
|
+
task.current_work = work
|
64
|
+
task.work_history.append(work)
|
65
|
+
return task
|
66
|
+
|
67
|
+
async def activate_work_queues(self):
|
68
|
+
"""
|
69
|
+
Activates the work queues for all work functions.
|
70
|
+
"""
|
71
|
+
for work_function in self.worker.work_functions.values():
|
72
|
+
if not work_function.worklog.queue.execution_mode:
|
73
|
+
asyncio.create_task(work_function.worklog.queue.execute())
|
74
|
+
|
75
|
+
async def stop(self):
|
76
|
+
"""
|
77
|
+
Stops the execution of tasks.
|
78
|
+
"""
|
79
|
+
self._stop_event.set()
|
80
|
+
|
81
|
+
@property
|
82
|
+
def stopped(self) -> bool:
|
83
|
+
"""
|
84
|
+
Checks if the execution has been stopped.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
bool: True if stopped, otherwise False.
|
88
|
+
"""
|
89
|
+
return self._stop_event.is_set()
|
90
|
+
|
91
|
+
async def process(self, task: WorkTask):
|
92
|
+
"""
|
93
|
+
Processes a single task.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
task (WorkTask): The task to be processed.
|
97
|
+
"""
|
98
|
+
current_work_func_name = task.current_work.async_task_name
|
99
|
+
current_work_func = self.worker.work_functions[current_work_func_name]
|
100
|
+
updated_task = await task.process(current_work_func)
|
101
|
+
if updated_task == "COMPLETED":
|
102
|
+
self.active_tasks.pop(task)
|
103
|
+
elif updated_task == "FAILED":
|
104
|
+
self.active_tasks.pop(task)
|
105
|
+
self.failed_tasks.append(task)
|
106
|
+
elif isinstance(updated_task, list):
|
107
|
+
self.tasks.include(updated_task)
|
108
|
+
self.active_tasks.include(updated_task)
|
109
|
+
else:
|
110
|
+
await asyncio.sleep(self.refresh_time)
|
111
|
+
|
112
|
+
async def execute(self, stop_queue=True):
|
113
|
+
"""
|
114
|
+
Executes all tasks in the task queue.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
stop_queue (bool, optional): Whether to stop the queue after execution. Defaults to True.
|
118
|
+
"""
|
119
|
+
for task in self.tasks:
|
120
|
+
if task.status == WorkStatus.PENDING:
|
121
|
+
task.status = WorkStatus.IN_PROGRESS
|
122
|
+
self.active_tasks.append(task)
|
123
|
+
|
124
|
+
await self.activate_work_queues()
|
125
|
+
self._stop_event.clear()
|
126
|
+
|
127
|
+
while len(self.active_tasks) > 0 and not self.stopped:
|
128
|
+
for work_function in self.worker.work_functions.values():
|
129
|
+
if len(work_function.worklog.pending) > 0:
|
130
|
+
await work_function.worklog.forward()
|
131
|
+
tasks = list(self.active_tasks)
|
132
|
+
await asyncio.gather(*[self.process(task) for task in tasks])
|
133
|
+
await asyncio.sleep(self.refresh_time)
|
134
|
+
|
135
|
+
if stop_queue:
|
136
|
+
await self.stop()
|
137
|
+
await self.worker.stop()
|
138
|
+
|
139
|
+
async def execute_lasting(self):
|
140
|
+
"""
|
141
|
+
Executes tasks continuously until stopped.
|
142
|
+
"""
|
143
|
+
self._stop_event.clear()
|
144
|
+
|
145
|
+
async def execute_lasting_inner():
|
146
|
+
while not self.stopped:
|
147
|
+
await self.execute(stop_queue=False)
|
148
|
+
await asyncio.sleep(self.refresh_time)
|
149
|
+
asyncio.create_task(execute_lasting_inner())
|
150
|
+
|
151
|
+
def _construct_work_functions(self):
|
152
|
+
"""
|
153
|
+
Constructs work functions for the worker.
|
154
|
+
"""
|
155
|
+
if getattr(self.worker, "work_functions", None) is None:
|
156
|
+
self.worker.work_functions = {}
|
157
|
+
work_decorated_function = self.worker._get_decorated_functions(decorator_attr="_work_decorator_params",
|
158
|
+
name_only=False)
|
159
|
+
for func_name, func, dec_params in work_decorated_function:
|
160
|
+
if func_name not in self.worker.work_functions:
|
161
|
+
self.worker.work_functions[func_name] = WorkFunctionNode(**dec_params)
|
162
|
+
self.worker_graph.add_node(self.worker.work_functions[func_name])
|
163
|
+
else:
|
164
|
+
if not isinstance(self.worker.work_functions[func_name], WorkFunctionNode):
|
165
|
+
raise TypeError(f"WorkFunction {func_name} already exists but is not a WorkFunctionNode. "
|
166
|
+
f"If you would like to use it in WorkerEngine, please convert it to a "
|
167
|
+
f"WorkFunctionNode, or initiate a new worker, or pop it from work_function dict")
|
168
|
+
|
169
|
+
def _construct_workedges(self):
|
170
|
+
"""
|
171
|
+
Constructs work edges for the worker graph.
|
172
|
+
"""
|
173
|
+
worklink_decorated_function = self.worker._get_decorated_functions(decorator_attr="_worklink_decorator_params",
|
174
|
+
name_only=False)
|
175
|
+
|
176
|
+
for func_name, func, dec_params in worklink_decorated_function:
|
177
|
+
head = self.worker.work_functions[dec_params["from_"]]
|
178
|
+
tail = self.worker.work_functions[dec_params["to_"]]
|
179
|
+
self.worker_graph.add_edge(head=head, tail=tail, convert_function=func, associated_worker=self.worker, edge_class=WorkEdge)
|
lionagi/core/work/worklog.py
CHANGED
@@ -30,19 +30,21 @@ class WorkLog(Progressable):
|
|
30
30
|
queue (WorkQueue): A queue to manage the execution of work items.
|
31
31
|
"""
|
32
32
|
|
33
|
-
def __init__(self, capacity=10, workpile=None):
|
33
|
+
def __init__(self, capacity=10, workpile=None, refresh_time=1):
|
34
34
|
"""
|
35
35
|
Initializes a new instance of WorkLog.
|
36
36
|
|
37
37
|
Args:
|
38
|
-
capacity (int): The capacity of the work queue.
|
38
|
+
capacity (int): The capacity of the work queue batch processing.
|
39
39
|
workpile (Pile, optional): An optional pile of initial work items.
|
40
|
+
refresh_time (int, optional): The time interval to refresh the work log queue.
|
41
|
+
Defaults to 1.
|
40
42
|
"""
|
41
43
|
self.pile = (
|
42
44
|
workpile if workpile and isinstance(workpile, Pile) else pile({}, Work)
|
43
45
|
)
|
44
46
|
self.pending = progression(workpile) if workpile else progression()
|
45
|
-
self.queue = WorkQueue(capacity=capacity)
|
47
|
+
self.queue = WorkQueue(capacity=capacity, refresh_time=refresh_time)
|
46
48
|
|
47
49
|
async def append(self, work: Work):
|
48
50
|
"""
|
@@ -56,15 +58,11 @@ class WorkLog(Progressable):
|
|
56
58
|
|
57
59
|
async def forward(self):
|
58
60
|
"""
|
59
|
-
Forwards pending work items to the queue
|
61
|
+
Forwards pending work items to the queue.
|
60
62
|
"""
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
while len(self.pending) > 0 and self.queue.available_capacity:
|
65
|
-
work: Work = self.pile[self.pending.popleft()]
|
66
|
-
work.status = WorkStatus.IN_PROGRESS
|
67
|
-
await self.queue.enqueue(work)
|
63
|
+
while len(self.pending) > 0:
|
64
|
+
work: Work = self.pile[self.pending.popleft()]
|
65
|
+
await self.queue.enqueue(work)
|
68
66
|
|
69
67
|
async def stop(self):
|
70
68
|
"""
|