lionagi 0.1.1__py3-none-any.whl → 0.1.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.
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