lionagi 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. lionagi/core/execute/structure_executor.py +21 -1
  2. lionagi/core/flow/monoflow/ReAct.py +3 -1
  3. lionagi/core/flow/monoflow/followup.py +3 -1
  4. lionagi/core/generic/component.py +197 -120
  5. lionagi/core/generic/condition.py +2 -0
  6. lionagi/core/generic/edge.py +33 -33
  7. lionagi/core/graph/graph.py +1 -1
  8. lionagi/core/tool/tool_manager.py +10 -9
  9. lionagi/experimental/report/form.py +64 -0
  10. lionagi/experimental/report/report.py +138 -0
  11. lionagi/experimental/report/util.py +47 -0
  12. lionagi/experimental/tool/schema.py +3 -3
  13. lionagi/experimental/tool/tool_manager.py +1 -1
  14. lionagi/experimental/validator/rule.py +139 -0
  15. lionagi/experimental/validator/validator.py +56 -0
  16. lionagi/experimental/work/__init__.py +10 -0
  17. lionagi/experimental/work/async_queue.py +54 -0
  18. lionagi/experimental/work/schema.py +60 -17
  19. lionagi/experimental/work/work_function.py +55 -77
  20. lionagi/experimental/work/worker.py +56 -12
  21. lionagi/experimental/work2/__init__.py +0 -0
  22. lionagi/experimental/work2/form.py +371 -0
  23. lionagi/experimental/work2/report.py +289 -0
  24. lionagi/experimental/work2/schema.py +30 -0
  25. lionagi/experimental/{work → work2}/tests.py +1 -1
  26. lionagi/experimental/work2/util.py +0 -0
  27. lionagi/experimental/work2/work.py +0 -0
  28. lionagi/experimental/work2/work_function.py +89 -0
  29. lionagi/experimental/work2/worker.py +12 -0
  30. lionagi/integrations/storage/storage_util.py +4 -4
  31. lionagi/integrations/storage/structure_excel.py +268 -0
  32. lionagi/integrations/storage/to_excel.py +18 -9
  33. lionagi/libs/__init__.py +4 -0
  34. lionagi/tests/test_core/generic/__init__.py +0 -0
  35. lionagi/tests/test_core/generic/test_component.py +89 -0
  36. lionagi/version.py +1 -1
  37. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/METADATA +1 -1
  38. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/RECORD +43 -27
  39. lionagi/experimental/work/_logger.py +0 -25
  40. /lionagi/experimental/{work/exchange.py → report/__init__.py} +0 -0
  41. /lionagi/experimental/{work/util.py → validator/__init__.py} +0 -0
  42. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/LICENSE +0 -0
  43. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/WHEEL +0 -0
  44. {lionagi-0.1.1.dist-info → lionagi-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,54 @@
1
+ import asyncio
2
+
3
+
4
+ class WorkQueue:
5
+
6
+ def __init__(self, capacity=5):
7
+
8
+ self.queue = asyncio.Queue()
9
+ self._stop_event = asyncio.Event()
10
+ self.capacity = capacity
11
+ self.semaphore = asyncio.Semaphore(capacity)
12
+
13
+ async def enqueue(self, work) -> None:
14
+ await self.queue.put(work)
15
+
16
+ async def dequeue(self):
17
+ return await self.queue.get()
18
+
19
+ async def join(self) -> None:
20
+ await self.queue.join()
21
+
22
+ async def stop(self) -> None:
23
+ self._stop_event.set()
24
+
25
+ @property
26
+ def available_capacity(self):
27
+ if (a:= self.capacity - self.queue.qsize()) > 0:
28
+ return a
29
+ return None
30
+
31
+ @property
32
+ def stopped(self) -> bool:
33
+ return self._stop_event.is_set()
34
+
35
+
36
+ async def process(self, refresh_time=1) -> None:
37
+ tasks = set()
38
+ while self.queue.qsize() > 0 and not self.stopped:
39
+ if not self.available_capacity and tasks:
40
+ _, done = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
41
+ tasks.difference_update(done)
42
+
43
+ async with self.semaphore:
44
+ next = await self.dequeue()
45
+ if next is None:
46
+ break
47
+ task = asyncio.create_task(next.perform())
48
+ tasks.add(task)
49
+
50
+ if tasks:
51
+ await asyncio.wait(tasks)
52
+ await asyncio.sleep(refresh_time)
53
+
54
+
@@ -1,8 +1,12 @@
1
+ from collections import deque
1
2
  from enum import Enum
2
- from typing import Any, Dict, List
3
- from pydantic import Field
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from lionagi.libs import SysUtil
4
7
  from lionagi.core.generic import BaseComponent
5
8
 
9
+ from .async_queue import WorkQueue
6
10
 
7
11
  class WorkStatus(str, Enum):
8
12
  """Enum to represent different statuses of work."""
@@ -11,20 +15,59 @@ class WorkStatus(str, Enum):
11
15
  IN_PROGRESS = "IN_PROGRESS"
12
16
  COMPLETED = "COMPLETED"
13
17
  FAILED = "FAILED"
14
- CANCELLED = "CANCELLED"
18
+
19
+
20
+ class Work(BaseComponent):
21
+ status: WorkStatus = WorkStatus.PENDING
22
+ result: Any = None
23
+ error: Any = None
24
+ async_task: asyncio.Task | None = None
25
+ completion_timestamp: str | None = None
26
+
27
+ async def perform(self):
28
+ try:
29
+ result = await self.async_task
30
+ self.result = result
31
+ self.status = WorkStatus.COMPLETED
32
+ self.async_task = None
33
+ except Exception as e:
34
+ self.error = e
35
+ self.status = WorkStatus.FAILED
36
+ finally:
37
+ self.completion_timestamp = SysUtil.get_timestamp()
15
38
 
16
39
 
17
- class Work(BaseComponent):
18
- """Base component for handling individual units of work."""
19
-
20
- form_id: str = Field(..., description="ID of the form for this work")
21
- priority: int = Field(default=0, description="Priority of the work")
22
- status: WorkStatus = Field(
23
- default=WorkStatus.PENDING, description="Current status of the work"
24
- )
25
- deliverables: Dict[str, Any] | list = Field(
26
- default={}, description="Deliverables produced by the work"
27
- )
28
- dependencies: List["Work"] = Field(
29
- default_factory=list, description="List of work items this work depends on"
30
- )
40
+ def __str__(self):
41
+ return f"Work(id={self.id_}, status={self.status}, created_at={self.timestamp}, completed_at={self.completion_timestamp})"
42
+
43
+ class WorkLog:
44
+
45
+ def __init__(self, capacity=5, pile=None):
46
+ self.pile = pile or {}
47
+ self.pending_sequence = deque()
48
+ self.queue = WorkQueue(capacity=capacity)
49
+
50
+ async def append(self, work: Work):
51
+ self.pile[work.id_] = work
52
+ self.pending_sequence.append(work.id_)
53
+
54
+ async def forward(self):
55
+ if not self.queue.available_capacity:
56
+ return
57
+ else:
58
+ while self.pending_sequence and self.queue.available_capacity:
59
+ work = self.pile[self.pending_sequence.popleft()]
60
+ work.status = WorkStatus.IN_PROGRESS
61
+ await self.queue.enqueue(work)
62
+
63
+
64
+ async def stop(self):
65
+ await self.queue.stop()
66
+
67
+ @property
68
+ def stopped(self):
69
+ return self.queue.stopped
70
+
71
+ @property
72
+ def completed_work(self):
73
+ return {k: v for k, v in self.pile.items() if v.status == WorkStatus.COMPLETED}
@@ -1,89 +1,67 @@
1
1
  import asyncio
2
- from typing import Any, Callable, Dict, List
3
- from pydantic import Field
4
- from functools import wraps
5
- from lionagi import logging as _logging
2
+ from typing import Callable, Any
6
3
  from lionagi.libs import func_call
7
- from lionagi.core.generic import BaseComponent
4
+ from functools import wraps
5
+ from pydantic import Field
8
6
 
9
- from .schema import Work, WorkStatus
10
- from ._logger import WorkLog
11
- from .worker import Worker
7
+ from lionagi.core.generic import BaseComponent
12
8
 
9
+ from .schema import Work, WorkLog
13
10
 
14
- class WorkFunction(BaseComponent):
15
- """Work function management and execution."""
16
11
 
17
- function: Callable
18
- args: List[Any] = Field(default_factory=list)
19
- kwargs: Dict[str, Any] = Field(default_factory=dict)
20
- retry_kwargs: Dict[str, Any] = Field(default_factory=dict)
21
- worklog: WorkLog = Field(default_factory=WorkLog)
22
- instruction: str = Field(
23
- default="", description="Instruction for the work function"
24
- )
25
- refresh_time: float = Field(
26
- default=0.5, description="Time to wait before checking for pending work"
27
- )
28
12
 
13
+ class WorkFunction:
14
+
15
+ def __init__(
16
+ self, assignment, function, retry_kwargs=None,
17
+ instruction = None, capacity=5
18
+ ):
19
+
20
+ self.assignment = assignment
21
+ self.function = function
22
+ self.retry_kwargs = retry_kwargs or {}
23
+ self.instruction = instruction or function.__doc__
24
+ self.worklog = WorkLog(capacity=capacity)
25
+
26
+
29
27
  @property
30
28
  def name(self):
31
- """Get the name of the work function."""
32
29
  return self.function.__name__
33
30
 
34
- async def execute(self):
35
- """Execute pending work items."""
36
- while self.worklog.pending:
37
- work_id = self.worklog.pending.popleft()
38
- work = self.worklog.logs[work_id]
39
- if work.status == WorkStatus.PENDING:
40
- try:
41
- await func_call.rcall(self._execute, work, **work.retry_kwargs)
42
- except Exception as e:
43
- work.status = WorkStatus.FAILED
44
- _logging.error(f"Work {work.id_} failed with error: {e}")
45
- self.worklog.errored.append(work.id_)
46
- else:
47
- _logging.warning(
48
- f"Work {work.id_} is in {work.status} state "
49
- "and cannot be executed."
31
+ async def perform(self, *args, **kwargs):
32
+ kwargs = {**self.retry_kwargs, **kwargs}
33
+ return await func_call.rcall(self.function, *args, **kwargs)
34
+
35
+ async def process(self, refresh_time=1):
36
+ await self.worklog.process(refresh_time=refresh_time)
37
+
38
+ async def stop(self):
39
+ await self.worklog.queue.stop()
40
+
41
+
42
+
43
+ def work(assignment, capacity=5):
44
+ def decorator(func):
45
+ @wraps(func)
46
+ async def wrapper(self, *args, retry_kwargs=None, instruction=None, **kwargs):
47
+ if getattr(self, "work_functions", None) is None:
48
+ self.work_functions = {}
49
+
50
+ if func.__name__ not in self.work_functions:
51
+ self.work_functions[func.__name__] = WorkFunction(
52
+ assignment=assignment,
53
+ function=func,
54
+ retry_kwargs=retry_kwargs or {},
55
+ instruction=instruction or func.__doc__,
56
+ capacity=capacity
50
57
  )
51
- await asyncio.sleep(self.refresh_time)
52
-
53
- async def _execute(self, work: Work):
54
- """Execute a single work item."""
55
- work.status = WorkStatus.IN_PROGRESS
56
- result = await self.function(*self.args, **self.kwargs)
57
- work.deliverables = result
58
- work.status = WorkStatus.COMPLETED
59
- return result
60
-
61
-
62
- def workfunc(func):
63
-
64
- @wraps(func)
65
- async def wrapper(self: Worker, *args, **kwargs):
66
- # Retrieve the worker instance ('self')
67
- if not hasattr(self, "work_functions"):
68
- self.work_functions = {}
69
-
70
- if func.__name__ not in self.work_functions:
71
- # Create WorkFunction with the function and its docstring as instruction
72
- self.work_functions[func.__name__] = WorkFunction(
73
- function=func,
74
- instruction=func.__doc__,
75
- args=args,
76
- kwargs=kwargs,
77
- retry_kwargs=kwargs.pop("retry_kwargs", {}),
78
- )
79
-
80
- # Retrieve the existing WorkFunction
81
- work_function: WorkFunction = self.work_functions[func.__name__]
82
- # Update args and kwargs for this call
83
- work_function.args = args
84
- work_function.kwargs = kwargs
85
-
86
- # Execute the function using WorkFunction's managed execution process
87
- return await work_function.execute()
88
-
89
- return wrapper
58
+
59
+ work_func: WorkFunction = self.work_functions[func.__name__]
60
+ task = asyncio.create_task(work_func.perform(*args, **kwargs))
61
+ work = Work(async_task=task)
62
+ work_func: WorkFunction = self.work_functions[func.__name__]
63
+ await work_func.worklog.append(work)
64
+ return True
65
+
66
+ return wrapper
67
+ return decorator
@@ -1,12 +1,56 @@
1
- from abc import ABC
2
- from pydantic import Field
3
- from lionagi.core.generic import BaseComponent
4
-
5
-
6
- class Worker(BaseComponent, ABC):
7
- form_templates: dict = Field(
8
- default={}, description="The form templates of the worker"
9
- )
10
- work_functions: dict = Field(
11
- default={}, description="The work functions of the worker"
12
- )
1
+ from abc import ABC, abstractmethod
2
+ from lionagi import logging as _logging
3
+ from .work_function import WorkFunction
4
+ import asyncio
5
+
6
+ class Worker(ABC):
7
+ # This is a class that will be used to create a worker object
8
+ # work_functions are keyed by assignment {assignment: WorkFunction}
9
+
10
+ name: str = "Worker"
11
+ work_functions: dict[str, WorkFunction] = {}
12
+
13
+ def __init__(self) -> None:
14
+ self.stopped = False
15
+
16
+ async def stop(self):
17
+ self.stopped = True
18
+ _logging.info(f"Stopping worker {self.name}")
19
+ non_stopped_ = []
20
+
21
+ for func in self.work_functions.values():
22
+ worklog = func.worklog
23
+ await worklog.stop()
24
+ if not worklog.stopped:
25
+ non_stopped_.append(func.name)
26
+
27
+ if len(non_stopped_) > 0:
28
+ _logging.error(f"Could not stop worklogs: {non_stopped_}")
29
+
30
+ _logging.info(f"Stopped worker {self.name}")
31
+
32
+
33
+ async def process(self, refresh_time=1):
34
+ while not self.stopped:
35
+ tasks = [
36
+ asyncio.create_task(func.process(refresh_time=refresh_time))
37
+ for func in self.work_functions.values()
38
+ ]
39
+ await asyncio.wait(tasks)
40
+ await asyncio.sleep(refresh_time)
41
+
42
+
43
+ # # Example
44
+ # from lionagi import Session
45
+ # from lionagi.experimental.work.work_function import work
46
+
47
+
48
+ # class MyWorker(Worker):
49
+
50
+ # @work(assignment="instruction, context -> response")
51
+ # async def chat(instruction=None, context=None):
52
+ # session = Session()
53
+ # return await session.chat(instruction=instruction, context=context)
54
+
55
+
56
+ # await a.chat(instruction="Hello", context={})
File without changes