lionagi 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|