lionagi 0.6.0__py3-none-any.whl → 0.7.0__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 -0
- lionagi/libs/token_transform/__init__.py +0 -0
- lionagi/libs/token_transform/llmlingua.py +1 -0
- lionagi/libs/token_transform/perplexity.py +439 -0
- lionagi/libs/token_transform/synthlang.py +409 -0
- lionagi/operations/ReAct/ReAct.py +126 -0
- lionagi/operations/ReAct/utils.py +28 -0
- lionagi/operations/__init__.py +1 -9
- lionagi/operations/_act/act.py +73 -0
- lionagi/operations/chat/__init__.py +3 -0
- lionagi/operations/chat/chat.py +173 -0
- lionagi/operations/communicate/__init__.py +0 -0
- lionagi/operations/communicate/communicate.py +167 -0
- lionagi/operations/instruct/__init__.py +3 -0
- lionagi/operations/instruct/instruct.py +29 -0
- lionagi/operations/interpret/__init__.py +3 -0
- lionagi/operations/interpret/interpret.py +40 -0
- lionagi/operations/operate/__init__.py +3 -0
- lionagi/operations/operate/operate.py +189 -0
- lionagi/operations/parse/__init__.py +3 -0
- lionagi/operations/parse/parse.py +125 -0
- lionagi/operations/plan/plan.py +3 -3
- lionagi/operations/select/__init__.py +0 -4
- lionagi/operations/select/select.py +11 -30
- lionagi/operations/select/utils.py +13 -2
- lionagi/operations/translate/__init__.py +0 -0
- lionagi/operations/translate/translate.py +47 -0
- lionagi/operations/types.py +25 -3
- lionagi/operatives/action/function_calling.py +1 -1
- lionagi/operatives/action/manager.py +22 -26
- lionagi/operatives/action/tool.py +1 -1
- lionagi/operatives/strategies/__init__.py +3 -0
- lionagi/{operations → operatives}/strategies/params.py +18 -2
- lionagi/protocols/adapters/__init__.py +0 -0
- lionagi/protocols/adapters/adapter.py +95 -0
- lionagi/protocols/adapters/json_adapter.py +101 -0
- lionagi/protocols/adapters/pandas_/__init__.py +0 -0
- lionagi/protocols/adapters/pandas_/csv_adapter.py +50 -0
- lionagi/protocols/adapters/pandas_/excel_adapter.py +52 -0
- lionagi/protocols/adapters/pandas_/pd_dataframe_adapter.py +31 -0
- lionagi/protocols/adapters/pandas_/pd_series_adapter.py +17 -0
- lionagi/protocols/adapters/types.py +18 -0
- lionagi/protocols/generic/pile.py +22 -1
- lionagi/protocols/graph/node.py +17 -1
- lionagi/protocols/types.py +3 -3
- lionagi/service/__init__.py +1 -14
- lionagi/service/endpoints/base.py +1 -1
- lionagi/service/endpoints/rate_limited_processor.py +2 -1
- lionagi/service/manager.py +1 -1
- lionagi/service/types.py +18 -0
- lionagi/session/branch.py +1098 -929
- lionagi/version.py +1 -1
- {lionagi-0.6.0.dist-info → lionagi-0.7.0.dist-info}/METADATA +4 -4
- {lionagi-0.6.0.dist-info → lionagi-0.7.0.dist-info}/RECORD +66 -38
- lionagi/libs/compress/models.py +0 -66
- lionagi/libs/compress/utils.py +0 -69
- lionagi/operations/select/prompt.py +0 -5
- lionagi/protocols/_adapter.py +0 -224
- /lionagi/{libs/compress → operations/ReAct}/__init__.py +0 -0
- /lionagi/operations/{strategies → _act}/__init__.py +0 -0
- /lionagi/{operations → operatives}/strategies/base.py +0 -0
- /lionagi/{operations → operatives}/strategies/concurrent.py +0 -0
- /lionagi/{operations → operatives}/strategies/concurrent_chunk.py +0 -0
- /lionagi/{operations → operatives}/strategies/concurrent_sequential_chunk.py +0 -0
- /lionagi/{operations → operatives}/strategies/sequential.py +0 -0
- /lionagi/{operations → operatives}/strategies/sequential_chunk.py +0 -0
- /lionagi/{operations → operatives}/strategies/sequential_concurrent_chunk.py +0 -0
- /lionagi/{operations → operatives}/strategies/utils.py +0 -0
- {lionagi-0.6.0.dist-info → lionagi-0.7.0.dist-info}/WHEEL +0 -0
- {lionagi-0.6.0.dist-info → lionagi-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from lionagi.libs.validate.fuzzy_validate_mapping import fuzzy_validate_mapping
|
10
|
+
from lionagi.operatives.types import Operative
|
11
|
+
from lionagi.utils import breakdown_pydantic_annotation
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from lionagi.session.branch import Branch
|
15
|
+
|
16
|
+
|
17
|
+
async def parse(
|
18
|
+
branch: "Branch",
|
19
|
+
text: str,
|
20
|
+
handle_validation: Literal[
|
21
|
+
"raise", "return_value", "return_none"
|
22
|
+
] = "return_value",
|
23
|
+
max_retries: int = 3,
|
24
|
+
request_type: type[BaseModel] = None,
|
25
|
+
operative: Operative = None,
|
26
|
+
similarity_algo="jaro_winkler",
|
27
|
+
similarity_threshold: float = 0.85,
|
28
|
+
fuzzy_match: bool = True,
|
29
|
+
handle_unmatched: Literal[
|
30
|
+
"ignore", "raise", "remove", "fill", "force"
|
31
|
+
] = "force",
|
32
|
+
fill_value: Any = None,
|
33
|
+
fill_mapping: dict[str, Any] | None = None,
|
34
|
+
strict: bool = False,
|
35
|
+
suppress_conversion_errors: bool = False,
|
36
|
+
response_format=None,
|
37
|
+
):
|
38
|
+
"""Attempts to parse text into a structured Pydantic model.
|
39
|
+
|
40
|
+
Uses optional fuzzy matching to handle partial or unclear fields.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
text (str): The raw text to parse.
|
44
|
+
handle_validation (Literal["raise","return_value","return_none"]):
|
45
|
+
What to do if parsing fails. Defaults to "return_value".
|
46
|
+
max_retries (int):
|
47
|
+
How many times to retry parsing if it fails.
|
48
|
+
request_type (type[BaseModel], optional):
|
49
|
+
The Pydantic model to parse into.
|
50
|
+
operative (Operative, optional):
|
51
|
+
If provided, uses its model and max_retries setting.
|
52
|
+
similarity_algo (str):
|
53
|
+
The similarity algorithm for fuzzy field matching.
|
54
|
+
similarity_threshold (float):
|
55
|
+
A threshold for fuzzy matching (0.0 - 1.0).
|
56
|
+
fuzzy_match (bool):
|
57
|
+
If True, tries to match unrecognized keys to known ones.
|
58
|
+
handle_unmatched (Literal["ignore","raise","remove","fill","force"]):
|
59
|
+
How to handle unmatched fields.
|
60
|
+
fill_value (Any):
|
61
|
+
A default value used when fill is needed.
|
62
|
+
fill_mapping (dict[str, Any] | None):
|
63
|
+
A mapping from field -> fill value override.
|
64
|
+
strict (bool):
|
65
|
+
If True, raises errors on ambiguous fields or data types.
|
66
|
+
suppress_conversion_errors (bool):
|
67
|
+
If True, logs or ignores errors during data conversion.
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
BaseModel | Any | None:
|
71
|
+
The parsed model instance, or a dict/string/None depending
|
72
|
+
on the handling mode.
|
73
|
+
"""
|
74
|
+
_should_try = True
|
75
|
+
num_try = 0
|
76
|
+
response_model = text
|
77
|
+
if operative is not None:
|
78
|
+
max_retries = operative.max_retries
|
79
|
+
response_format = operative.request_type
|
80
|
+
|
81
|
+
while (
|
82
|
+
_should_try
|
83
|
+
and num_try < max_retries
|
84
|
+
and not isinstance(response_model, BaseModel)
|
85
|
+
):
|
86
|
+
num_try += 1
|
87
|
+
_, res = await branch.chat(
|
88
|
+
instruction="reformat text into specified model",
|
89
|
+
guidane="follow the required response format, using the model schema as a guide",
|
90
|
+
context=[{"text_to_format": text}],
|
91
|
+
response_format=response_format or request_type,
|
92
|
+
sender=branch.user,
|
93
|
+
recipient=branch.id,
|
94
|
+
imodel=branch.parse_model,
|
95
|
+
return_ins_res_message=True,
|
96
|
+
)
|
97
|
+
if operative is not None:
|
98
|
+
response_model = operative.update_response_model(res.response)
|
99
|
+
else:
|
100
|
+
response_model = fuzzy_validate_mapping(
|
101
|
+
res.response,
|
102
|
+
breakdown_pydantic_annotation(request_type),
|
103
|
+
similarity_algo=similarity_algo,
|
104
|
+
similarity_threshold=similarity_threshold,
|
105
|
+
fuzzy_match=fuzzy_match,
|
106
|
+
handle_unmatched=handle_unmatched,
|
107
|
+
fill_value=fill_value,
|
108
|
+
fill_mapping=fill_mapping,
|
109
|
+
strict=strict,
|
110
|
+
suppress_conversion_errors=suppress_conversion_errors,
|
111
|
+
)
|
112
|
+
response_model = request_type.model_validate(response_model)
|
113
|
+
|
114
|
+
if not isinstance(response_model, BaseModel):
|
115
|
+
match handle_validation:
|
116
|
+
case "return_value":
|
117
|
+
return response_model
|
118
|
+
case "return_none":
|
119
|
+
return None
|
120
|
+
case "raise":
|
121
|
+
raise ValueError(
|
122
|
+
"Failed to parse response into request format"
|
123
|
+
)
|
124
|
+
|
125
|
+
return response_model
|
lionagi/operations/plan/plan.py
CHANGED
@@ -27,9 +27,9 @@ from .prompt import EXPANSION_PROMPT, PLAN_PROMPT
|
|
27
27
|
class PlanOperation(BaseModel):
|
28
28
|
"""
|
29
29
|
Stores all relevant outcomes for a multi-step Plan:
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
* initial: The result of the initial plan prompt
|
31
|
+
* plan: A list of plan steps (Instruct objects) generated from the initial planning
|
32
|
+
* execute: Any responses from executing those plan steps
|
33
33
|
"""
|
34
34
|
|
35
35
|
initial: Any
|
@@ -2,56 +2,37 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
|
5
|
-
|
6
5
|
from enum import Enum
|
7
|
-
from typing import Any
|
6
|
+
from typing import TYPE_CHECKING, Any
|
8
7
|
|
9
|
-
from pydantic import BaseModel
|
8
|
+
from pydantic import BaseModel
|
10
9
|
|
11
|
-
from lionagi import Branch
|
12
10
|
from lionagi.operatives.types import Instruct
|
13
11
|
|
14
|
-
from .
|
15
|
-
from .utils import parse_selection, parse_to_representation
|
16
|
-
|
12
|
+
from .utils import SelectionModel
|
17
13
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
selected: list[Any] = Field(default_factory=list)
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from lionagi.session.branch import Branch
|
22
16
|
|
23
17
|
|
24
18
|
async def select(
|
19
|
+
branch: "Branch",
|
25
20
|
instruct: Instruct | dict[str, Any],
|
26
21
|
choices: list[str] | type[Enum] | dict[str, Any],
|
27
22
|
max_num_selections: int = 1,
|
28
|
-
branch: Branch | None = None,
|
29
23
|
branch_kwargs: dict[str, Any] | None = None,
|
30
24
|
return_branch: bool = False,
|
31
25
|
verbose: bool = False,
|
32
26
|
**kwargs: Any,
|
33
|
-
) -> SelectionModel | tuple[SelectionModel, Branch]:
|
34
|
-
"""Perform a selection operation from given choices.
|
35
|
-
|
36
|
-
Args:
|
37
|
-
instruct: Instruction model or dictionary.
|
38
|
-
choices: Options to select from.
|
39
|
-
max_num_selections: Maximum selections allowed.
|
40
|
-
branch: Existing branch or None to create a new one.
|
41
|
-
branch_kwargs: Additional arguments for branch creation.
|
42
|
-
return_branch: If True, return the branch with the selection.
|
43
|
-
verbose: Whether to enable verbose output.
|
44
|
-
**kwargs: Additional keyword arguments.
|
45
|
-
|
46
|
-
Returns:
|
47
|
-
A SelectionModel instance, optionally with the branch.
|
48
|
-
"""
|
27
|
+
) -> SelectionModel | tuple[SelectionModel, "Branch"]:
|
49
28
|
if verbose:
|
50
29
|
print(f"Starting selection with up to {max_num_selections} choices.")
|
51
30
|
|
31
|
+
from .utils import SelectionModel, parse_selection, parse_to_representation
|
32
|
+
|
52
33
|
branch = branch or Branch(**(branch_kwargs or {}))
|
53
34
|
selections, contents = parse_to_representation(choices)
|
54
|
-
prompt = PROMPT.format(
|
35
|
+
prompt = SelectionModel.PROMPT.format(
|
55
36
|
max_num_selections=max_num_selections, choices=selections
|
56
37
|
)
|
57
38
|
|
@@ -73,7 +54,7 @@ async def select(
|
|
73
54
|
instruct["context"] = context
|
74
55
|
|
75
56
|
response_model: SelectionModel = await branch.operate(
|
76
|
-
|
57
|
+
response_format=SelectionModel,
|
77
58
|
**kwargs,
|
78
59
|
**instruct,
|
79
60
|
)
|
@@ -4,14 +4,25 @@
|
|
4
4
|
|
5
5
|
import inspect
|
6
6
|
from enum import Enum
|
7
|
-
from typing import Any
|
7
|
+
from typing import Any, ClassVar
|
8
8
|
|
9
|
-
from pydantic import BaseModel, JsonValue
|
9
|
+
from pydantic import BaseModel, Field, JsonValue
|
10
10
|
|
11
11
|
from lionagi.libs.validate.string_similarity import string_similarity
|
12
12
|
from lionagi.utils import is_same_dtype
|
13
13
|
|
14
14
|
|
15
|
+
# TODO: Make select a field to be added into a model, much like reason and action
|
16
|
+
class SelectionModel(BaseModel):
|
17
|
+
"""Model representing the selection output."""
|
18
|
+
|
19
|
+
PROMPT: ClassVar[str] = (
|
20
|
+
"Please select up to {max_num_selections} items from the following list {choices}. Provide the selection(s) into appropriate field in format required, and no comments from you"
|
21
|
+
)
|
22
|
+
|
23
|
+
selected: list[Any] = Field(default_factory=list)
|
24
|
+
|
25
|
+
|
15
26
|
def parse_to_representation(
|
16
27
|
choices: Enum | dict | list | tuple | set,
|
17
28
|
) -> tuple[list[str], JsonValue]:
|
File without changes
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Literal
|
2
|
+
|
3
|
+
from lionagi.service.imodel import iModel
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from lionagi.session.branch import Branch
|
7
|
+
|
8
|
+
|
9
|
+
async def translate(
|
10
|
+
branch: "Branch",
|
11
|
+
text: str,
|
12
|
+
technique: Literal["SynthLang"] = "SynthLang",
|
13
|
+
technique_kwargs: dict = None,
|
14
|
+
compress: bool = False,
|
15
|
+
chat_model: iModel = None,
|
16
|
+
compress_model: iModel = None,
|
17
|
+
compression_ratio: float = 0.2,
|
18
|
+
compress_kwargs=None,
|
19
|
+
verbose: bool = True,
|
20
|
+
new_branch: bool = True,
|
21
|
+
**kwargs,
|
22
|
+
):
|
23
|
+
if technique == "SynthLang":
|
24
|
+
from lionagi.libs.token_transform.synthlang import (
|
25
|
+
translate_to_synthlang,
|
26
|
+
)
|
27
|
+
|
28
|
+
if not technique_kwargs:
|
29
|
+
technique_kwargs = {}
|
30
|
+
if not technique_kwargs.get("template_name"):
|
31
|
+
technique_kwargs["template_name"] = "symbolic_systems"
|
32
|
+
|
33
|
+
technique_kwargs = {**technique_kwargs, **kwargs}
|
34
|
+
|
35
|
+
return await translate_to_synthlang(
|
36
|
+
text=text,
|
37
|
+
compress=compress,
|
38
|
+
chat_model=chat_model or branch.chat_model,
|
39
|
+
compress_model=compress_model,
|
40
|
+
compression_ratio=compression_ratio,
|
41
|
+
compress_kwargs=compress_kwargs,
|
42
|
+
verbose=verbose,
|
43
|
+
branch=branch if not new_branch else None,
|
44
|
+
**technique_kwargs,
|
45
|
+
)
|
46
|
+
|
47
|
+
raise ValueError(f"Technique {technique} is not supported.")
|
lionagi/operations/types.py
CHANGED
@@ -2,6 +2,28 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
|
5
|
-
from .brainstorm import
|
6
|
-
from .
|
7
|
-
from .
|
5
|
+
from .brainstorm.brainstorm import brainstorm
|
6
|
+
from .chat.chat import chat
|
7
|
+
from .communicate.communicate import communicate
|
8
|
+
from .instruct.instruct import instruct
|
9
|
+
from .interpret.interpret import interpret
|
10
|
+
from .operate.operate import operate
|
11
|
+
from .parse.parse import parse
|
12
|
+
from .plan.plan import plan
|
13
|
+
from .ReAct.ReAct import ReAct
|
14
|
+
from .select.select import select
|
15
|
+
from .translate.translate import translate
|
16
|
+
|
17
|
+
__all__ = (
|
18
|
+
"brainstorm",
|
19
|
+
"plan",
|
20
|
+
"select",
|
21
|
+
"chat",
|
22
|
+
"communicate",
|
23
|
+
"instruct",
|
24
|
+
"interpret",
|
25
|
+
"operate",
|
26
|
+
"parse",
|
27
|
+
"ReAct",
|
28
|
+
"translate",
|
29
|
+
)
|
@@ -7,7 +7,7 @@ from typing import Any, Self
|
|
7
7
|
|
8
8
|
from pydantic import Field, model_validator
|
9
9
|
|
10
|
-
from lionagi.protocols.
|
10
|
+
from lionagi.protocols.generic.event import Event, EventStatus
|
11
11
|
from lionagi.utils import is_coro_func
|
12
12
|
|
13
13
|
from .tool import Tool
|
@@ -4,13 +4,9 @@
|
|
4
4
|
|
5
5
|
from typing import Any
|
6
6
|
|
7
|
-
from lionagi.protocols.
|
8
|
-
|
9
|
-
|
10
|
-
Execution,
|
11
|
-
Log,
|
12
|
-
Manager,
|
13
|
-
)
|
7
|
+
from lionagi.protocols._concepts import Manager
|
8
|
+
from lionagi.protocols.generic.event import Execution
|
9
|
+
from lionagi.protocols.messages.action_request import ActionRequest
|
14
10
|
from lionagi.utils import to_list
|
15
11
|
|
16
12
|
from .function_calling import FunctionCalling
|
@@ -116,19 +112,28 @@ class ActionManager(Manager):
|
|
116
112
|
]
|
117
113
|
|
118
114
|
def match_tool(
|
119
|
-
self, action_request: ActionRequest | ActionRequestModel
|
115
|
+
self, action_request: ActionRequest | ActionRequestModel | dict
|
120
116
|
) -> FunctionCalling:
|
121
|
-
if not isinstance(
|
117
|
+
if not isinstance(
|
118
|
+
action_request, ActionRequest | ActionRequestModel | dict
|
119
|
+
):
|
122
120
|
raise TypeError(f"Unsupported type {type(action_request)}")
|
123
121
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
122
|
+
func = (
|
123
|
+
action_request["function"]
|
124
|
+
if isinstance(action_request, dict)
|
125
|
+
else action_request.function
|
126
|
+
)
|
127
|
+
args = (
|
128
|
+
action_request["arguments"]
|
129
|
+
if isinstance(action_request, dict)
|
130
|
+
else action_request.arguments
|
131
131
|
)
|
132
|
+
tool = self.registry.get(func, None)
|
133
|
+
|
134
|
+
if not isinstance(tool, Tool):
|
135
|
+
raise ValueError(f"Function {func} is not registered.")
|
136
|
+
return FunctionCalling(func_tool=tool, arguments=args)
|
132
137
|
|
133
138
|
async def invoke(
|
134
139
|
self, func_call: ActionRequestModel | ActionRequest
|
@@ -151,16 +156,7 @@ class ActionManager(Manager):
|
|
151
156
|
Raises:
|
152
157
|
ValueError: If function not registered or call format invalid.
|
153
158
|
"""
|
154
|
-
|
155
|
-
function_calling = self.match_tool(func_call)
|
156
|
-
except ValueError as e:
|
157
|
-
return Log(
|
158
|
-
content={
|
159
|
-
"event_type": "function_call",
|
160
|
-
"status": EventStatus.FAILED,
|
161
|
-
"error": str(e),
|
162
|
-
}
|
163
|
-
)
|
159
|
+
function_calling = self.match_tool(func_call)
|
164
160
|
await function_calling.invoke()
|
165
161
|
return function_calling
|
166
162
|
|
@@ -10,7 +10,7 @@ from pydantic import Field, field_validator, model_validator
|
|
10
10
|
|
11
11
|
from lionagi.libs.schema.function_to_schema import function_to_schema
|
12
12
|
from lionagi.libs.validate.common_field_validators import validate_callable
|
13
|
-
from lionagi.protocols.
|
13
|
+
from lionagi.protocols.generic.element import Element
|
14
14
|
|
15
15
|
__all__ = (
|
16
16
|
"Tool",
|
@@ -10,9 +10,25 @@ from lionagi.operatives.instruct.instruct import (
|
|
10
10
|
LIST_INSTRUCT_FIELD_MODEL,
|
11
11
|
Instruct,
|
12
12
|
)
|
13
|
-
from lionagi.
|
13
|
+
from lionagi.operatives.types import FieldModel, SchemaModel
|
14
14
|
from lionagi.session.session import Branch, Session
|
15
|
-
|
15
|
+
|
16
|
+
|
17
|
+
class RCallParams(SchemaModel):
|
18
|
+
"""Parameters for remote function calls."""
|
19
|
+
|
20
|
+
timeout: float = Field(
|
21
|
+
default=60, description="Timeout for remote function call"
|
22
|
+
)
|
23
|
+
max_retries: int = Field(
|
24
|
+
default=3, description="Maximum number of retries"
|
25
|
+
)
|
26
|
+
retry_delay: float = Field(
|
27
|
+
default=0.5, description="Delay between retries"
|
28
|
+
)
|
29
|
+
retry_backoff: float = Field(
|
30
|
+
default=2, description="Backoff factor for retry delay"
|
31
|
+
)
|
16
32
|
|
17
33
|
|
18
34
|
class StrategyParams(SchemaModel):
|
File without changes
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import Any, Protocol, TypeVar, runtime_checkable
|
7
|
+
|
8
|
+
from typing_extensions import get_protocol_members
|
9
|
+
|
10
|
+
T = TypeVar("T")
|
11
|
+
|
12
|
+
__all__ = (
|
13
|
+
"Adapter",
|
14
|
+
"ADAPTER_MEMBERS",
|
15
|
+
"AdapterRegistry",
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
@runtime_checkable
|
20
|
+
class Adapter(Protocol):
|
21
|
+
|
22
|
+
obj_key: str
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def from_obj(
|
26
|
+
cls,
|
27
|
+
subj_cls: type[T],
|
28
|
+
obj: Any,
|
29
|
+
/,
|
30
|
+
*,
|
31
|
+
many: bool,
|
32
|
+
**kwargs,
|
33
|
+
) -> dict | list[dict]: ...
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def to_obj(
|
37
|
+
cls,
|
38
|
+
subj: T,
|
39
|
+
/,
|
40
|
+
*,
|
41
|
+
many: bool,
|
42
|
+
**kwargs,
|
43
|
+
) -> Any: ...
|
44
|
+
|
45
|
+
|
46
|
+
ADAPTER_MEMBERS = get_protocol_members(Adapter) # duck typing
|
47
|
+
|
48
|
+
|
49
|
+
class AdapterRegistry:
|
50
|
+
|
51
|
+
_adapters: dict[str, Adapter] = {}
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
def list_adapters(cls) -> list[tuple[str | type, ...]]:
|
55
|
+
return list(cls._adapters.keys())
|
56
|
+
|
57
|
+
@classmethod
|
58
|
+
def register(cls, adapter: type[Adapter]) -> None:
|
59
|
+
for member in ADAPTER_MEMBERS:
|
60
|
+
if not hasattr(adapter, member):
|
61
|
+
_str = getattr(adapter, "obj_key", None) or repr(adapter)
|
62
|
+
_str = _str[:50] if len(_str) > 50 else _str
|
63
|
+
raise AttributeError(
|
64
|
+
f"Adapter {_str} missing required methods."
|
65
|
+
)
|
66
|
+
|
67
|
+
if isinstance(adapter, type):
|
68
|
+
cls._adapters[adapter.obj_key] = adapter()
|
69
|
+
else:
|
70
|
+
cls._adapters[adapter.obj_key] = adapter
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def get(cls, obj_key: type | str) -> Adapter:
|
74
|
+
try:
|
75
|
+
return cls._adapters[obj_key]
|
76
|
+
except Exception as e:
|
77
|
+
logging.error(f"Error getting adapter for {obj_key}. Error: {e}")
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def adapt_from(
|
81
|
+
cls, subj_cls: type[T], obj: Any, obj_key: type | str, **kwargs
|
82
|
+
) -> dict | list[dict]:
|
83
|
+
try:
|
84
|
+
return cls.get(obj_key).from_obj(subj_cls, obj, **kwargs)
|
85
|
+
except Exception as e:
|
86
|
+
logging.error(f"Error adapting data from {obj_key}. Error: {e}")
|
87
|
+
raise e
|
88
|
+
|
89
|
+
@classmethod
|
90
|
+
def adapt_to(cls, subj: T, obj_key: type | str, **kwargs) -> Any:
|
91
|
+
try:
|
92
|
+
return cls.get(obj_key).to_obj(subj, **kwargs)
|
93
|
+
except Exception as e:
|
94
|
+
logging.error(f"Error adapting data to {obj_key}. Error: {e}")
|
95
|
+
raise e
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from lionagi.protocols._concepts import Collective
|
6
|
+
|
7
|
+
from .adapter import Adapter, T
|
8
|
+
|
9
|
+
|
10
|
+
class JsonAdapter(Adapter):
|
11
|
+
|
12
|
+
obj_key = "json"
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def from_obj(
|
16
|
+
cls,
|
17
|
+
subj_cls: type[T],
|
18
|
+
obj: str,
|
19
|
+
/,
|
20
|
+
*,
|
21
|
+
many: bool = False,
|
22
|
+
**kwargs,
|
23
|
+
) -> dict | list[dict]:
|
24
|
+
"""
|
25
|
+
kwargs for json.loads(s, **kwargs)
|
26
|
+
"""
|
27
|
+
result = json.loads(obj, **kwargs)
|
28
|
+
if many:
|
29
|
+
return result if isinstance(result, list) else [result]
|
30
|
+
return (
|
31
|
+
result[0]
|
32
|
+
if isinstance(result, list) and len(result) > 0
|
33
|
+
else result
|
34
|
+
)
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def to_obj(
|
38
|
+
cls,
|
39
|
+
subj: T,
|
40
|
+
*,
|
41
|
+
many: bool = False,
|
42
|
+
**kwargs,
|
43
|
+
):
|
44
|
+
"""
|
45
|
+
kwargs for json.dumps(obj, **kwargs)
|
46
|
+
"""
|
47
|
+
if many:
|
48
|
+
if isinstance(subj, Collective):
|
49
|
+
return json.dumps([i.to_dict() for i in subj], **kwargs)
|
50
|
+
return json.dumps([subj.to_dict()], **kwargs)
|
51
|
+
return json.dumps(subj.to_dict(), **kwargs)
|
52
|
+
|
53
|
+
|
54
|
+
class JsonFileAdapter(Adapter):
|
55
|
+
|
56
|
+
obj_key = ".json"
|
57
|
+
|
58
|
+
@classmethod
|
59
|
+
def from_obj(
|
60
|
+
cls,
|
61
|
+
subj_cls: type[T],
|
62
|
+
obj: str | Path,
|
63
|
+
/,
|
64
|
+
*,
|
65
|
+
many: bool = False,
|
66
|
+
**kwargs,
|
67
|
+
) -> dict | list[dict]:
|
68
|
+
"""
|
69
|
+
kwargs for json.load(fp, **kwargs)
|
70
|
+
"""
|
71
|
+
with open(obj) as f:
|
72
|
+
result = json.load(f, **kwargs)
|
73
|
+
if many:
|
74
|
+
return result if isinstance(result, list) else [result]
|
75
|
+
return (
|
76
|
+
result[0]
|
77
|
+
if isinstance(result, list) and len(result) > 0
|
78
|
+
else result
|
79
|
+
)
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
def to_obj(
|
83
|
+
cls,
|
84
|
+
subj: T,
|
85
|
+
/,
|
86
|
+
*,
|
87
|
+
fp: str | Path,
|
88
|
+
many: bool = False,
|
89
|
+
**kwargs,
|
90
|
+
):
|
91
|
+
"""
|
92
|
+
kwargs for json.dump(obj, fp, **kwargs)
|
93
|
+
"""
|
94
|
+
if many:
|
95
|
+
if isinstance(subj, Collective):
|
96
|
+
json.dump([i.to_dict() for i in subj], fp=fp, **kwargs)
|
97
|
+
return
|
98
|
+
json.dump([subj.to_dict()], fp=fp, **kwargs)
|
99
|
+
return
|
100
|
+
json.dump(subj.to_dict(), fp=fp, **kwargs)
|
101
|
+
logging.info(f"Successfully saved data to {fp}")
|
File without changes
|