lionagi 0.1.0__py3-none-any.whl → 0.1.1__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/core/agent/base_agent.py +2 -3
- lionagi/core/branch/base.py +1 -1
- lionagi/core/branch/branch.py +2 -1
- lionagi/core/branch/flow_mixin.py +1 -1
- lionagi/core/branch/util.py +1 -1
- lionagi/core/execute/base_executor.py +1 -4
- lionagi/core/execute/branch_executor.py +66 -3
- lionagi/core/execute/instruction_map_executor.py +48 -0
- lionagi/core/execute/neo4j_executor.py +381 -0
- lionagi/core/execute/structure_executor.py +99 -3
- lionagi/core/flow/monoflow/ReAct.py +18 -18
- lionagi/core/flow/monoflow/chat_mixin.py +1 -1
- lionagi/core/flow/monoflow/followup.py +11 -12
- lionagi/core/flow/polyflow/__init__.py +1 -1
- lionagi/core/generic/component.py +0 -2
- lionagi/core/generic/condition.py +1 -1
- lionagi/core/generic/edge.py +52 -0
- lionagi/core/mail/mail_manager.py +3 -2
- lionagi/core/session/session.py +1 -1
- lionagi/experimental/__init__.py +0 -0
- lionagi/experimental/directive/__init__.py +0 -0
- lionagi/experimental/directive/evaluator/__init__.py +0 -0
- lionagi/experimental/directive/evaluator/ast_evaluator.py +115 -0
- lionagi/experimental/directive/evaluator/base_evaluator.py +202 -0
- lionagi/experimental/directive/evaluator/sandbox_.py +14 -0
- lionagi/experimental/directive/evaluator/script_engine.py +83 -0
- lionagi/experimental/directive/parser/__init__.py +0 -0
- lionagi/experimental/directive/parser/base_parser.py +215 -0
- lionagi/experimental/directive/schema.py +36 -0
- lionagi/experimental/directive/template_/__init__.py +0 -0
- lionagi/experimental/directive/template_/base_template.py +63 -0
- lionagi/experimental/tool/__init__.py +0 -0
- lionagi/experimental/tool/function_calling.py +43 -0
- lionagi/experimental/tool/manual.py +66 -0
- lionagi/experimental/tool/schema.py +59 -0
- lionagi/experimental/tool/tool_manager.py +138 -0
- lionagi/experimental/tool/util.py +16 -0
- lionagi/experimental/work/__init__.py +0 -0
- lionagi/experimental/work/_logger.py +25 -0
- lionagi/experimental/work/exchange.py +0 -0
- lionagi/experimental/work/schema.py +30 -0
- lionagi/experimental/work/tests.py +72 -0
- lionagi/experimental/work/util.py +0 -0
- lionagi/experimental/work/work_function.py +89 -0
- lionagi/experimental/work/worker.py +12 -0
- lionagi/integrations/bridge/autogen_/__init__.py +0 -0
- lionagi/integrations/bridge/autogen_/autogen_.py +124 -0
- lionagi/integrations/bridge/llamaindex_/get_index.py +294 -0
- lionagi/integrations/bridge/llamaindex_/llama_pack.py +227 -0
- lionagi/integrations/bridge/transformers_/__init__.py +0 -0
- lionagi/integrations/bridge/transformers_/install_.py +36 -0
- lionagi/integrations/config/oai_configs.py +1 -1
- lionagi/integrations/config/ollama_configs.py +1 -1
- lionagi/integrations/config/openrouter_configs.py +1 -1
- lionagi/integrations/storage/__init__.py +3 -0
- lionagi/integrations/storage/neo4j.py +673 -0
- lionagi/integrations/storage/storage_util.py +289 -0
- lionagi/integrations/storage/to_csv.py +63 -0
- lionagi/integrations/storage/to_excel.py +67 -0
- lionagi/libs/ln_knowledge_graph.py +405 -0
- lionagi/libs/ln_queue.py +101 -0
- lionagi/libs/ln_tokenizer.py +57 -0
- lionagi/libs/sys_util.py +1 -1
- lionagi/lions/__init__.py +0 -0
- lionagi/lions/coder/__init__.py +0 -0
- lionagi/lions/coder/add_feature.py +20 -0
- lionagi/lions/coder/base_prompts.py +22 -0
- lionagi/lions/coder/coder.py +121 -0
- lionagi/lions/coder/util.py +91 -0
- lionagi/lions/researcher/__init__.py +0 -0
- lionagi/lions/researcher/data_source/__init__.py +0 -0
- lionagi/lions/researcher/data_source/finhub_.py +191 -0
- lionagi/lions/researcher/data_source/google_.py +199 -0
- lionagi/lions/researcher/data_source/wiki_.py +96 -0
- lionagi/lions/researcher/data_source/yfinance_.py +21 -0
- lionagi/tests/libs/test_queue.py +67 -0
- lionagi/tests/test_core/test_branch.py +0 -1
- lionagi/version.py +1 -1
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/METADATA +1 -1
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/RECORD +83 -29
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/LICENSE +0 -0
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/WHEEL +0 -0
- {lionagi-0.1.0.dist-info → lionagi-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Union, Callable, Any
|
3
|
+
|
4
|
+
|
5
|
+
class BaseManual:
|
6
|
+
def __init__(self, template_str: str):
|
7
|
+
self.template_str = template_str
|
8
|
+
|
9
|
+
def _evaluate_condition(self, match, context):
|
10
|
+
condition, text = match.groups()
|
11
|
+
# Future implementations might parse and evaluate the condition more thoroughly
|
12
|
+
return text if condition in context and context[condition] else ""
|
13
|
+
|
14
|
+
def _render_conditionals(self, context: Dict[str, Union[str, int, float]]) -> str:
|
15
|
+
conditional_pattern = re.compile(r"\{if (.*?)\}(.*?)\{endif\}", re.DOTALL)
|
16
|
+
return conditional_pattern.sub(
|
17
|
+
lambda match: self._evaluate_condition(match, context), self.template_str
|
18
|
+
)
|
19
|
+
|
20
|
+
def _replace_callable(self, match, context):
|
21
|
+
key = match.group(1)
|
22
|
+
if key in context:
|
23
|
+
value = context[key]
|
24
|
+
return str(value() if callable(value) else value)
|
25
|
+
return match.group(0) # Unmatched placeholders remain unchanged.
|
26
|
+
|
27
|
+
def _render_placeholders(
|
28
|
+
self,
|
29
|
+
rendered_template: str,
|
30
|
+
context: Dict[str, Union[str, int, float, Callable]],
|
31
|
+
) -> str:
|
32
|
+
return re.sub(
|
33
|
+
r"\{(\w+)\}",
|
34
|
+
lambda match: self._replace_callable(match, context),
|
35
|
+
rendered_template,
|
36
|
+
)
|
37
|
+
|
38
|
+
def generate(self, context: Dict[str, Union[str, int, float, Callable]]) -> str:
|
39
|
+
"""
|
40
|
+
Generates output by first processing conditionals, then rendering placeholders,
|
41
|
+
including executing callable objects for dynamic data generation.
|
42
|
+
"""
|
43
|
+
template_with_conditionals = self._render_conditionals(context)
|
44
|
+
final_output = self._render_placeholders(template_with_conditionals, context)
|
45
|
+
return final_output
|
46
|
+
|
47
|
+
|
48
|
+
# from experiments.executor.executor import SafeEvaluator
|
49
|
+
|
50
|
+
# class DecisionTreeManual:
|
51
|
+
# def __init__(self, root):
|
52
|
+
# self.root = root
|
53
|
+
# self.evaluator = SafeEvaluator()
|
54
|
+
|
55
|
+
# def evaluate(self, context):
|
56
|
+
# return self._traverse_tree(self.root, context)
|
57
|
+
|
58
|
+
# def _traverse_tree(self, node, context):
|
59
|
+
# if isinstance(node, CompositeActionNode) or isinstance(node, ActionNode):
|
60
|
+
# return node.execute(context)
|
61
|
+
# elif isinstance(node, DecisionNode):
|
62
|
+
# condition_result = self.evaluator.evaluate(node.condition, context)
|
63
|
+
# next_node = node.true_branch if condition_result else node.false_branch
|
64
|
+
# return self._traverse_tree(next_node, context)
|
65
|
+
# else:
|
66
|
+
# raise ValueError("Invalid node type.")
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from pydantic import field_serializer
|
3
|
+
from functools import singledispatchmethod
|
4
|
+
from lionagi import logging as _logging
|
5
|
+
from lionagi.libs import func_call, AsyncUtil
|
6
|
+
from lionagi.core.generic.node import Node
|
7
|
+
from .function_calling import FunctionCalling
|
8
|
+
|
9
|
+
|
10
|
+
class Tool(Node):
|
11
|
+
|
12
|
+
func: Any
|
13
|
+
schema_: dict | None = None
|
14
|
+
manual: Any | None = None
|
15
|
+
parser: Any | None = None
|
16
|
+
pre_processor: Any | None = None
|
17
|
+
post_processor: Any | None = None
|
18
|
+
|
19
|
+
@property
|
20
|
+
def name(self):
|
21
|
+
return self.schema_["function"]["name"]
|
22
|
+
|
23
|
+
@field_serializer("func")
|
24
|
+
def serialize_func(self, func):
|
25
|
+
return func.__name__
|
26
|
+
|
27
|
+
@singledispatchmethod
|
28
|
+
async def invoke(self, values: Any) -> Any:
|
29
|
+
raise TypeError(f"Unsupported type {type(values)}")
|
30
|
+
|
31
|
+
@invoke.register
|
32
|
+
async def _(self, kwargs: dict):
|
33
|
+
|
34
|
+
out = None
|
35
|
+
|
36
|
+
if self.pre_processor:
|
37
|
+
kwargs = await func_call.unified_call(self.pre_processor, kwargs)
|
38
|
+
try:
|
39
|
+
out = await func_call.unified_call(self.func, **kwargs)
|
40
|
+
|
41
|
+
except Exception as e:
|
42
|
+
_logging.error(f"Error invoking function {self.func_name}: {e}")
|
43
|
+
return None
|
44
|
+
|
45
|
+
if self.post_processor:
|
46
|
+
return await func_call.unified_call(self.post_processor, out)
|
47
|
+
|
48
|
+
return out
|
49
|
+
|
50
|
+
@invoke.register
|
51
|
+
async def _(self, function_calls: FunctionCalling):
|
52
|
+
return await self.invoke(function_calls.kwargs)
|
53
|
+
|
54
|
+
@invoke.register
|
55
|
+
async def _(self, values: list):
|
56
|
+
return await func_call.alcall(self.invoke, values)
|
57
|
+
|
58
|
+
|
59
|
+
TOOL_TYPE = bool | Tool | str | list[Tool | str | dict] | dict
|
@@ -0,0 +1,138 @@
|
|
1
|
+
import asyncio
|
2
|
+
from functools import singledispatchmethod
|
3
|
+
from collections import deque
|
4
|
+
from typing import Tuple, Any, TypeVar, Callable
|
5
|
+
from lionagi.libs import func_call, convert, ParseUtil
|
6
|
+
from lionagi import logging as _logging
|
7
|
+
from .schema import Tool, TOOL_TYPE
|
8
|
+
from .util import func_to_tool, parse_tool_response
|
9
|
+
from .function_calling import FunctionCalling
|
10
|
+
|
11
|
+
T = TypeVar("T", bound=Tool)
|
12
|
+
|
13
|
+
|
14
|
+
class ToolManager:
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
tool_registry: dict = {},
|
19
|
+
function_calling_tasks: dict[str : deque[FunctionCalling]] = {},
|
20
|
+
):
|
21
|
+
self.registry = tool_registry
|
22
|
+
self.function_calling_tasks = function_calling_tasks
|
23
|
+
|
24
|
+
@singledispatchmethod
|
25
|
+
def register_tools(self, tools: Any):
|
26
|
+
raise TypeError(f"Unsupported type {type(tools)}")
|
27
|
+
|
28
|
+
@register_tools.register(Tool)
|
29
|
+
def _(self, tools):
|
30
|
+
name = tools.schema_["function"]["name"]
|
31
|
+
if self._has_name(name):
|
32
|
+
err_msg = f"Function {name} is already registered."
|
33
|
+
_logging.error(err_msg)
|
34
|
+
raise ValueError(err_msg)
|
35
|
+
else:
|
36
|
+
self.registry[name] = tools
|
37
|
+
self.function_calling_tasks[name] = deque()
|
38
|
+
return True
|
39
|
+
|
40
|
+
@register_tools.register(Callable)
|
41
|
+
def _(self, tools):
|
42
|
+
tool = func_to_tool(tools)[0]
|
43
|
+
return self.register_tools(tool)
|
44
|
+
|
45
|
+
@register_tools.register(list)
|
46
|
+
def _(self, tools):
|
47
|
+
return func_call.lcall(tools, self.register_tools)
|
48
|
+
|
49
|
+
@singledispatchmethod
|
50
|
+
def register_function_calling(self, func_params: Any):
|
51
|
+
raise TypeError(f"Unsupported type {type(func_params)}")
|
52
|
+
|
53
|
+
@register_function_calling.register(tuple)
|
54
|
+
def _(self, func_params):
|
55
|
+
func = self.registry[func_params[0]].func
|
56
|
+
kwargs = func_params[1]
|
57
|
+
_function_calling = FunctionCalling(func=func, kwargs=kwargs)
|
58
|
+
self.function_calling_tasks[func.__name__].append(_function_calling)
|
59
|
+
return True
|
60
|
+
|
61
|
+
@register_function_calling.register(dict)
|
62
|
+
def _(self, response):
|
63
|
+
tuple_ = parse_tool_response(response)
|
64
|
+
return self.register_function_calling(tuple_)
|
65
|
+
|
66
|
+
@register_function_calling.register(list)
|
67
|
+
def _(self, func_params):
|
68
|
+
return func_call.lcall(func_params, self.register_function_calling)
|
69
|
+
|
70
|
+
async def invoke(self, func_params: Tuple[str, dict[str, Any]]) -> Any:
|
71
|
+
name, kwargs = func_params
|
72
|
+
if not self._has_name(name):
|
73
|
+
raise ValueError(f"Function {name} is not registered.")
|
74
|
+
tool = self.registry[name]
|
75
|
+
func = tool.func
|
76
|
+
parser = tool.parser
|
77
|
+
try:
|
78
|
+
out = await func_call.unified_call(func, **kwargs)
|
79
|
+
return parser(out) if parser else out
|
80
|
+
|
81
|
+
except Exception as e:
|
82
|
+
raise ValueError(
|
83
|
+
f"Error when invoking function {name} with arguments {kwargs} with error message {e}"
|
84
|
+
) from e
|
85
|
+
|
86
|
+
@property
|
87
|
+
def _schema_list(self) -> list[dict[str, Any]]:
|
88
|
+
return [tool.schema_ for tool in self.registry.values()]
|
89
|
+
|
90
|
+
def get_tool_schema(self, tools: TOOL_TYPE, **kwargs):
|
91
|
+
if isinstance(tools, bool):
|
92
|
+
tool_kwarg = {"tools": self._schema_list}
|
93
|
+
return tool_kwarg | kwargs
|
94
|
+
|
95
|
+
else:
|
96
|
+
if not isinstance(tools, list):
|
97
|
+
tools = [tools]
|
98
|
+
tool_kwarg = {"tools": self._get_tool_schema(tools)}
|
99
|
+
return tool_kwarg | kwargs
|
100
|
+
|
101
|
+
def _has_name(self, name: str) -> bool:
|
102
|
+
return name in self.registry
|
103
|
+
|
104
|
+
@singledispatchmethod
|
105
|
+
def _get_tool_schema(self, tool: Any) -> dict:
|
106
|
+
raise TypeError(f"Unsupported type {type(tool)}")
|
107
|
+
|
108
|
+
@_get_tool_schema.register(dict)
|
109
|
+
def _(self, tool):
|
110
|
+
"""
|
111
|
+
assuming that the tool is a schema
|
112
|
+
"""
|
113
|
+
return tool
|
114
|
+
|
115
|
+
@_get_tool_schema.register(Tool)
|
116
|
+
def _(self, tool):
|
117
|
+
if self._has_name(tool.name):
|
118
|
+
return self.registry[tool.name].schema_
|
119
|
+
else:
|
120
|
+
err_msg = f"Function {tool.name} is not registered."
|
121
|
+
_logging.error(err_msg)
|
122
|
+
raise ValueError(err_msg)
|
123
|
+
|
124
|
+
@_get_tool_schema.register(str)
|
125
|
+
def _(self, tool):
|
126
|
+
"""
|
127
|
+
assuming that the tool is a name
|
128
|
+
"""
|
129
|
+
if self._has_name(tool):
|
130
|
+
return self.registry[tool].schema_
|
131
|
+
else:
|
132
|
+
err_msg = f"Function {tool} is not registered."
|
133
|
+
_logging.error(err_msg)
|
134
|
+
raise ValueError(err_msg)
|
135
|
+
|
136
|
+
@_get_tool_schema.register(list)
|
137
|
+
def _(self, tools):
|
138
|
+
return func_call.lcall(tools, self._get_tool_schema)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from typing import Tuple
|
2
|
+
from lionagi.libs import convert
|
3
|
+
|
4
|
+
|
5
|
+
def parse_tool_response(response: dict) -> Tuple[str, dict]:
|
6
|
+
try:
|
7
|
+
func = response["action"][7:]
|
8
|
+
args = convert.to_dict(response["arguments"])
|
9
|
+
return func, args
|
10
|
+
except Exception:
|
11
|
+
try:
|
12
|
+
func = response["recipient_name"].split(".")[-1]
|
13
|
+
args = response["parameters"]
|
14
|
+
return func, args
|
15
|
+
except:
|
16
|
+
raise ValueError("response is not a valid function call")
|
File without changes
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from collections import deque
|
2
|
+
from typing import Dict
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
from .schema import Work, WorkStatus
|
5
|
+
|
6
|
+
|
7
|
+
class WorkLog(BaseModel):
|
8
|
+
"""Model to store and manage work logs."""
|
9
|
+
|
10
|
+
logs: Dict[str, Work] = Field(default={}, description="Logs of work items")
|
11
|
+
pending: deque = Field(
|
12
|
+
default_factory=deque, description="Priority queue of pending work items"
|
13
|
+
)
|
14
|
+
errored: deque = Field(
|
15
|
+
default_factory=deque, description="Queue of errored work items"
|
16
|
+
)
|
17
|
+
|
18
|
+
def append(self, work: Work):
|
19
|
+
"""Append a work item to the logs and pending queue."""
|
20
|
+
self.logs[str(work.form_id)] = work
|
21
|
+
self.pending.append(str(work.form_id))
|
22
|
+
|
23
|
+
def get_by_status(self, status: WorkStatus) -> Dict[str, Work]:
|
24
|
+
"""Get work items by their status."""
|
25
|
+
return {wid: work for wid, work in self.logs.items() if work.status == status}
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Any, Dict, List
|
3
|
+
from pydantic import Field
|
4
|
+
from lionagi.core.generic import BaseComponent
|
5
|
+
|
6
|
+
|
7
|
+
class WorkStatus(str, Enum):
|
8
|
+
"""Enum to represent different statuses of work."""
|
9
|
+
|
10
|
+
PENDING = "PENDING"
|
11
|
+
IN_PROGRESS = "IN_PROGRESS"
|
12
|
+
COMPLETED = "COMPLETED"
|
13
|
+
FAILED = "FAILED"
|
14
|
+
CANCELLED = "CANCELLED"
|
15
|
+
|
16
|
+
|
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
|
+
)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from .schema import Work, WorkStatus
|
2
|
+
from ._logger import WorkLog
|
3
|
+
from .work_function import WorkFunction
|
4
|
+
|
5
|
+
import unittest
|
6
|
+
from unittest.mock import AsyncMock, patch
|
7
|
+
|
8
|
+
from lionagi.libs import func_call
|
9
|
+
|
10
|
+
|
11
|
+
class TestWork(unittest.TestCase):
|
12
|
+
def setUp(self):
|
13
|
+
self.work = Work(form_id="123")
|
14
|
+
|
15
|
+
def test_initial_status(self):
|
16
|
+
"""Test the initial status is set to PENDING."""
|
17
|
+
self.assertEqual(self.work.status, WorkStatus.PENDING)
|
18
|
+
|
19
|
+
def test_initial_deliverables(self):
|
20
|
+
"""Test the initial deliverables are empty."""
|
21
|
+
self.assertEqual(self.work.deliverables, {})
|
22
|
+
|
23
|
+
def test_initial_dependencies(self):
|
24
|
+
"""Test the initial dependencies are empty."""
|
25
|
+
self.assertEqual(self.work.dependencies, [])
|
26
|
+
|
27
|
+
|
28
|
+
class TestWorkLog(unittest.TestCase):
|
29
|
+
def setUp(self):
|
30
|
+
self.work_log = WorkLog()
|
31
|
+
self.work = Work(form_id="123")
|
32
|
+
self.work_log.append(self.work)
|
33
|
+
|
34
|
+
def test_append_work(self):
|
35
|
+
"""Test appending work adds to logs and pending queue."""
|
36
|
+
self.assertIn("123", self.work_log.logs)
|
37
|
+
self.assertIn("123", self.work_log.pending)
|
38
|
+
|
39
|
+
def test_get_by_status(self):
|
40
|
+
"""Test retrieving works by status."""
|
41
|
+
result = self.work_log.get_by_status(WorkStatus.PENDING)
|
42
|
+
self.assertEqual(result, {"123": self.work})
|
43
|
+
|
44
|
+
|
45
|
+
class TestWorkFunction(unittest.TestCase):
|
46
|
+
def setUp(self):
|
47
|
+
self.work_function = WorkFunction(function=AsyncMock(return_value="result"))
|
48
|
+
self.work = Work(form_id="123")
|
49
|
+
self.work_log = WorkLog()
|
50
|
+
self.work_log.append(self.work)
|
51
|
+
self.work_function.worklog = self.work_log
|
52
|
+
|
53
|
+
@patch("asyncio.sleep", new_callable=AsyncMock)
|
54
|
+
async def test_execute(self, mocked_sleep):
|
55
|
+
"""Test executing work changes its status and handles results."""
|
56
|
+
with patch.object(func_call, "rcall", new_callable=AsyncMock) as mock_rcall:
|
57
|
+
mock_rcall.return_value = "completed"
|
58
|
+
await self.work_function.execute()
|
59
|
+
self.assertEqual(self.work.status, WorkStatus.COMPLETED)
|
60
|
+
self.assertNotIn("123", self.work_function.worklog.pending)
|
61
|
+
|
62
|
+
@patch("asyncio.sleep", new_callable=AsyncMock)
|
63
|
+
async def test_execute_failure(self, mocked_sleep):
|
64
|
+
"""Test handling failure during work execution."""
|
65
|
+
with patch.object(func_call, "rcall", side_effect=Exception("Error")):
|
66
|
+
await self.work_function.execute()
|
67
|
+
self.assertEqual(self.work.status, WorkStatus.FAILED)
|
68
|
+
self.assertIn("123", self.work_function.worklog.errored)
|
69
|
+
|
70
|
+
|
71
|
+
if __name__ == "__main__":
|
72
|
+
unittest.main()
|
File without changes
|
@@ -0,0 +1,89 @@
|
|
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
|
6
|
+
from lionagi.libs import func_call
|
7
|
+
from lionagi.core.generic import BaseComponent
|
8
|
+
|
9
|
+
from .schema import Work, WorkStatus
|
10
|
+
from ._logger import WorkLog
|
11
|
+
from .worker import Worker
|
12
|
+
|
13
|
+
|
14
|
+
class WorkFunction(BaseComponent):
|
15
|
+
"""Work function management and execution."""
|
16
|
+
|
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
|
+
|
29
|
+
@property
|
30
|
+
def name(self):
|
31
|
+
"""Get the name of the work function."""
|
32
|
+
return self.function.__name__
|
33
|
+
|
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."
|
50
|
+
)
|
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
|
@@ -0,0 +1,12 @@
|
|
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
|
+
)
|
File without changes
|
@@ -0,0 +1,124 @@
|
|
1
|
+
from typing import Dict, Union
|
2
|
+
|
3
|
+
|
4
|
+
def get_ipython_user_proxy():
|
5
|
+
|
6
|
+
try:
|
7
|
+
from lionagi.libs import SysUtil
|
8
|
+
|
9
|
+
SysUtil.check_import("autogen", pip_name="pyautogen")
|
10
|
+
|
11
|
+
import autogen
|
12
|
+
from IPython import get_ipython
|
13
|
+
except Exception as e:
|
14
|
+
raise ImportError(f"Please install autogen and IPython. {e}")
|
15
|
+
|
16
|
+
class IPythonUserProxyAgent(autogen.UserProxyAgent):
|
17
|
+
|
18
|
+
def __init__(self, name: str, **kwargs):
|
19
|
+
super().__init__(name, **kwargs)
|
20
|
+
self._ipython = get_ipython()
|
21
|
+
|
22
|
+
def generate_init_message(self, *args, **kwargs) -> Union[str, Dict]:
|
23
|
+
return (
|
24
|
+
super().generate_init_message(*args, **kwargs)
|
25
|
+
+ """If you suggest code, the code will be executed in IPython."""
|
26
|
+
)
|
27
|
+
|
28
|
+
def run_code(self, code, **kwargs):
|
29
|
+
result = self._ipython.run_cell("%%capture --no-display cap\n" + code)
|
30
|
+
log = self._ipython.ev("cap.stdout")
|
31
|
+
log += self._ipython.ev("cap.stderr")
|
32
|
+
if result.result is not None:
|
33
|
+
log += str(result.result)
|
34
|
+
exitcode = 0 if result.success else 1
|
35
|
+
if result.error_before_exec is not None:
|
36
|
+
log += f"\n{result.error_before_exec}"
|
37
|
+
exitcode = 1
|
38
|
+
if result.error_in_exec is not None:
|
39
|
+
log += f"\n{result.error_in_exec}"
|
40
|
+
exitcode = 1
|
41
|
+
return exitcode, log, None
|
42
|
+
|
43
|
+
return IPythonUserProxyAgent
|
44
|
+
|
45
|
+
|
46
|
+
def get_autogen_coder(
|
47
|
+
llm_config=None,
|
48
|
+
code_execution_config=None,
|
49
|
+
kernal="python",
|
50
|
+
config_list=None,
|
51
|
+
max_consecutive_auto_reply=15,
|
52
|
+
temperature=0,
|
53
|
+
cache_seed=42,
|
54
|
+
env_="local",
|
55
|
+
assistant_instruction=None,
|
56
|
+
):
|
57
|
+
assistant = ""
|
58
|
+
try:
|
59
|
+
from lionagi.libs import SysUtil
|
60
|
+
|
61
|
+
SysUtil.check_import("autogen", pip_name="pyautogen")
|
62
|
+
|
63
|
+
import autogen
|
64
|
+
from autogen.agentchat.contrib.gpt_assistant_agent import GPTAssistantAgent
|
65
|
+
except Exception as e:
|
66
|
+
raise ImportError(f"Please install autogen. {e}")
|
67
|
+
|
68
|
+
if env_ == "local":
|
69
|
+
assistant = autogen.AssistantAgent(
|
70
|
+
name="assistant",
|
71
|
+
llm_config=llm_config
|
72
|
+
or {
|
73
|
+
"cache_seed": cache_seed,
|
74
|
+
"config_list": config_list,
|
75
|
+
"temperature": temperature,
|
76
|
+
},
|
77
|
+
)
|
78
|
+
|
79
|
+
elif env_ == "oai_assistant":
|
80
|
+
assistant = GPTAssistantAgent(
|
81
|
+
name="Coder Assistant",
|
82
|
+
llm_config={
|
83
|
+
"tools": [{"type": "code_interpreter"}],
|
84
|
+
"config_list": config_list,
|
85
|
+
},
|
86
|
+
instructions=assistant_instruction,
|
87
|
+
)
|
88
|
+
|
89
|
+
if kernal == "python":
|
90
|
+
user_proxy = autogen.UserProxyAgent(
|
91
|
+
name="user_proxy",
|
92
|
+
human_input_mode="NEVER",
|
93
|
+
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
94
|
+
is_termination_msg=lambda x: x.get("content", "")
|
95
|
+
.rstrip()
|
96
|
+
.endswith("TERMINATE"),
|
97
|
+
code_execution_config=code_execution_config
|
98
|
+
or {
|
99
|
+
"work_dir": "coding",
|
100
|
+
"use_docker": False,
|
101
|
+
},
|
102
|
+
)
|
103
|
+
return user_proxy, assistant
|
104
|
+
|
105
|
+
elif kernal == "ipython":
|
106
|
+
user_proxy = get_ipython_user_proxy(
|
107
|
+
"ipython_user_proxy",
|
108
|
+
human_input_mode="NEVER",
|
109
|
+
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
110
|
+
is_termination_msg=lambda x: x.get("content", "")
|
111
|
+
.rstrip()
|
112
|
+
.endswith("TERMINATE")
|
113
|
+
or x.get("content", "").rstrip().endswith('"TERMINATE".'),
|
114
|
+
)
|
115
|
+
return user_proxy, assistant
|
116
|
+
|
117
|
+
# # Sample Usage Pattern
|
118
|
+
# context = "def my_function():\n pass\n"
|
119
|
+
# task1 = "I need help with the following code:\n"
|
120
|
+
# task2 = "Please write a function that returns the sum of two numbers."
|
121
|
+
|
122
|
+
# user_proxy, assistant = get_autogen_coder()
|
123
|
+
# user_proxy.initiate_chat(assistant, message=task1+context)
|
124
|
+
# user_proxy.send(recipient=assistant, message=task2)
|