lionagi 0.2.1__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.
@@ -16,11 +16,8 @@ limitations under the License.
16
16
 
17
17
  from abc import ABC
18
18
  from functools import wraps
19
- from typing import Callable
20
- import asyncio
21
19
  import inspect
22
20
  from lionagi import logging as _logging
23
- from lionagi.libs.ln_func_call import pcall
24
21
  from lionagi.core.work.work_function import WorkFunction
25
22
  from lionagi.core.work.work import Work
26
23
  from lionagi.core.report.form import Form
@@ -39,12 +36,19 @@ class Worker(ABC):
39
36
  """
40
37
 
41
38
  name: str = "Worker"
42
- work_functions: dict[str, WorkFunction] = {}
43
39
 
44
40
  def __init__(self, forms=None, default_form=None) -> None:
45
- # self.stopped = False
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] = {}
46
49
  self.forms: dict[str, Form] = forms or {}
47
50
  self.default_form = default_form
51
+ self._validate_worklink_functions()
48
52
 
49
53
  async def stop(self):
50
54
  """
@@ -92,37 +96,58 @@ class Worker(ABC):
92
96
  raise ValueError(f"Unable to change default form. Key {form_key} does not exist.")
93
97
  self.default_form = self.forms[form_key]
94
98
 
95
- # async def process(self, refresh_time=1):
96
- # """
97
- # Processes all work functions periodically.
98
- #
99
- # Args:
100
- # refresh_time (int): Time interval between each process cycle.
101
- # """
102
- # while await self.is_progressable():
103
- # await pcall([i.process(refresh_time) for i in self.work_functions.values()])
104
- # await asyncio.sleep(refresh_time)
105
-
106
- # TODO: Implement process method
107
-
108
- # async def process(self, refresh_time=1):
109
- # while not self.stopped:
110
- # tasks = [
111
- # asyncio.create_task(func.process(refresh_time=refresh_time))
112
- # for func in self.work_functions.values()
113
- # ]
114
- # await asyncio.wait(tasks)
115
- # await asyncio.sleep(refresh_time)
116
-
117
- async def _wrapper(
99
+ def _get_decorated_functions(self, decorator_attr, name_only=True):
100
+ """
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.
106
+
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}")
129
+
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)
140
+
141
+ async def _work_wrapper(
118
142
  self,
119
143
  *args,
120
- func=None,
144
+ function=None,
121
145
  assignment=None,
122
146
  form_param_key=None,
123
147
  capacity=None,
124
148
  retry_kwargs=None,
125
149
  guidance=None,
150
+ refresh_time=1,
126
151
  **kwargs,
127
152
  ):
128
153
  """
@@ -135,27 +160,28 @@ class Worker(ABC):
135
160
  the function's signature. This parameter is used to locate and fill
136
161
  the appropriate form according to the assignment. Raises an error
137
162
  if the form parameter key is not found in the function's signature.
138
- capacity (int): Capacity for the work log.
163
+ capacity (int): Capacity for the work queue batch processing.
139
164
  retry_kwargs (dict): Retry arguments for the function.
140
165
  guidance (str): Guidance or documentation for the function.
141
166
  """
142
167
  if getattr(self, "work_functions", None) is None:
143
168
  self.work_functions = {}
144
169
 
145
- if func.__name__ not in self.work_functions:
146
- self.work_functions[func.__name__] = WorkFunction(
170
+ if function.__name__ not in self.work_functions:
171
+ self.work_functions[function.__name__] = WorkFunction(
147
172
  assignment=assignment,
148
- function=func,
173
+ function=function,
149
174
  retry_kwargs=retry_kwargs or {},
150
- guidance=guidance,
175
+ guidance=guidance or function.__doc__,
151
176
  capacity=capacity,
177
+ refresh_time=refresh_time
152
178
  )
153
179
 
154
- work_func: WorkFunction = self.work_functions[func.__name__]
180
+ work_func: WorkFunction = self.work_functions[function.__name__]
155
181
 
156
182
  # locate form that should be filled according to the assignment
157
183
  if form_param_key:
158
- func_signature = inspect.signature(func)
184
+ func_signature = inspect.signature(function)
159
185
  if form_param_key not in func_signature.parameters:
160
186
  raise KeyError(f"Failed to locate form. \"{form_param_key}\" is not defined in the function.")
161
187
  if "self" in func_signature.parameters:
@@ -195,6 +221,7 @@ def work(
195
221
  guidance=None,
196
222
  retry_kwargs=None,
197
223
  timeout=10,
224
+ refresh_time=1
198
225
  ):
199
226
  """
200
227
  Decorator to mark a method as a work function.
@@ -205,10 +232,14 @@ def work(
205
232
  the function's signature. This parameter is used to locate and fill
206
233
  the appropriate form according to the assignment. Raises an error
207
234
  if the form parameter key is not found in the function's signature.
208
- capacity (int): Capacity for the work log.
235
+ capacity (int): Capacity for the work queue batch processing.
209
236
  guidance (str): Guidance or documentation for the work function.
210
237
  retry_kwargs (dict): Retry arguments for the work function.
211
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.
212
243
  """
213
244
 
214
245
  def decorator(func):
@@ -222,20 +253,114 @@ def work(
222
253
  capacity=capacity,
223
254
  retry_kwargs=retry_kwargs,
224
255
  guidance=guidance,
256
+ refresh_time=refresh_time,
225
257
  **kwargs,
226
258
  ):
259
+ if not inspect.iscoroutinefunction(func):
260
+ raise TypeError(f"{func.__name__} must be an asynchronous function")
227
261
  retry_kwargs = retry_kwargs or {}
228
262
  retry_kwargs["timeout"] = retry_kwargs.get("timeout", timeout)
229
- return await self._wrapper(
263
+ return await self._work_wrapper(
230
264
  *args,
231
- func=func,
265
+ function=func,
232
266
  assignment=assignment,
233
267
  form_param_key=form_param_key,
234
268
  capacity=capacity,
235
269
  retry_kwargs=retry_kwargs,
236
270
  guidance=guidance,
271
+ refresh_time=refresh_time,
237
272
  **kwargs,
238
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_}
239
364
 
240
365
  return wrapper
241
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)
@@ -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
  """