lionagi 0.16.2__py3-none-any.whl → 0.17.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/adapters/_utils.py +10 -23
- lionagi/adapters/async_postgres_adapter.py +83 -79
- lionagi/ln/__init__.py +4 -4
- lionagi/ln/_json_dump.py +0 -6
- lionagi/ln/fuzzy/__init__.py +4 -1
- lionagi/ln/fuzzy/_fuzzy_validate.py +109 -0
- lionagi/ln/fuzzy/_to_dict.py +388 -0
- lionagi/models/__init__.py +0 -2
- lionagi/operations/__init__.py +0 -6
- lionagi/operations/_visualize_graph.py +285 -0
- lionagi/operations/brainstorm/brainstorm.py +14 -12
- lionagi/operations/builder.py +23 -302
- lionagi/operations/communicate/communicate.py +1 -1
- lionagi/operations/flow.py +14 -11
- lionagi/operations/node.py +14 -3
- lionagi/operations/operate/operate.py +5 -11
- lionagi/operations/parse/parse.py +2 -3
- lionagi/operations/types.py +0 -2
- lionagi/operations/utils.py +11 -5
- lionagi/protocols/generic/pile.py +3 -7
- lionagi/protocols/graph/graph.py +23 -6
- lionagi/protocols/graph/node.py +0 -2
- lionagi/protocols/messages/message.py +0 -1
- lionagi/protocols/operatives/operative.py +2 -2
- lionagi/protocols/types.py +0 -15
- lionagi/service/connections/endpoint.py +11 -5
- lionagi/service/connections/match_endpoint.py +2 -10
- lionagi/service/connections/providers/types.py +1 -3
- lionagi/service/hooks/hook_event.py +1 -1
- lionagi/service/hooks/hook_registry.py +1 -1
- lionagi/service/rate_limited_processor.py +1 -1
- lionagi/session/branch.py +24 -18
- lionagi/session/session.py +2 -18
- lionagi/utils.py +3 -335
- lionagi/version.py +1 -1
- {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/METADATA +4 -13
- {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/RECORD +39 -61
- lionagi/adapters/postgres_model_adapter.py +0 -131
- lionagi/libs/concurrency.py +0 -1
- lionagi/libs/nested/__init__.py +0 -3
- lionagi/libs/nested/flatten.py +0 -172
- lionagi/libs/nested/nfilter.py +0 -59
- lionagi/libs/nested/nget.py +0 -45
- lionagi/libs/nested/ninsert.py +0 -104
- lionagi/libs/nested/nmerge.py +0 -158
- lionagi/libs/nested/npop.py +0 -69
- lionagi/libs/nested/nset.py +0 -94
- lionagi/libs/nested/unflatten.py +0 -83
- lionagi/libs/nested/utils.py +0 -189
- lionagi/libs/parse.py +0 -31
- lionagi/libs/schema/json_schema.py +0 -231
- lionagi/libs/unstructured/__init__.py +0 -0
- lionagi/libs/unstructured/pdf_to_image.py +0 -45
- lionagi/libs/unstructured/read_image_to_base64.py +0 -33
- lionagi/libs/validate/fuzzy_match_keys.py +0 -7
- lionagi/libs/validate/fuzzy_validate_mapping.py +0 -144
- lionagi/libs/validate/string_similarity.py +0 -7
- lionagi/libs/validate/xml_parser.py +0 -203
- lionagi/models/note.py +0 -387
- lionagi/protocols/graph/_utils.py +0 -22
- lionagi/service/connections/providers/claude_code_.py +0 -299
- {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/WHEEL +0 -0
- {lionagi-0.16.2.dist-info → lionagi-0.17.0.dist-info}/licenses/LICENSE +0 -0
lionagi/libs/nested/ninsert.py
DELETED
@@ -1,104 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from typing import Any
|
6
|
-
|
7
|
-
from lionagi.utils import to_list
|
8
|
-
|
9
|
-
|
10
|
-
def ninsert(
|
11
|
-
nested_structure: dict[Any, Any] | list[Any],
|
12
|
-
/,
|
13
|
-
indices: list[str | int],
|
14
|
-
value: Any,
|
15
|
-
*,
|
16
|
-
current_depth: int = 0,
|
17
|
-
) -> None:
|
18
|
-
"""
|
19
|
-
Inserts a value into a nested structure at a specified path.
|
20
|
-
|
21
|
-
Navigates a nested dictionary or list based on a sequence of indices or
|
22
|
-
keys and inserts `value` at the final location. This method can create
|
23
|
-
intermediate dictionaries or lists as needed.
|
24
|
-
|
25
|
-
Args:
|
26
|
-
nested_structure: The nested structure to modify.
|
27
|
-
indices: The sequence of keys or indices defining the insertion path.
|
28
|
-
value: The value to insert at the specified location.
|
29
|
-
current_depth: Internal use only; tracks the current depth during
|
30
|
-
recursive calls.
|
31
|
-
|
32
|
-
Raises:
|
33
|
-
ValueError: If the indices list is empty.
|
34
|
-
TypeError: If an invalid key or container type is encountered.
|
35
|
-
|
36
|
-
Examples:
|
37
|
-
>>> subject_ = {'a': {'b': [1, 2]}}
|
38
|
-
>>> ninsert(subject_, ['a', 'b', 2], 3)
|
39
|
-
>>> assert subject_ == {'a': {'b': [1, 2, 3]}}
|
40
|
-
|
41
|
-
>>> subject_ = []
|
42
|
-
>>> ninsert(subject_, [0, 'a'], 1)
|
43
|
-
>>> assert subject_ == [{'a': 1}]
|
44
|
-
"""
|
45
|
-
if not indices:
|
46
|
-
raise ValueError("Indices list cannot be empty")
|
47
|
-
|
48
|
-
indices = to_list(indices)
|
49
|
-
for i, part in enumerate(indices[:-1]):
|
50
|
-
if isinstance(part, int):
|
51
|
-
if isinstance(nested_structure, dict):
|
52
|
-
raise TypeError(
|
53
|
-
f"Unsupported key type: {type(part).__name__}.Only string keys are acceptable.",
|
54
|
-
)
|
55
|
-
while len(nested_structure) <= part:
|
56
|
-
nested_structure.append(None)
|
57
|
-
if nested_structure[part] is None or not isinstance(
|
58
|
-
nested_structure[part], (dict, list)
|
59
|
-
):
|
60
|
-
next_part = indices[i + 1]
|
61
|
-
nested_structure[part] = (
|
62
|
-
[] if isinstance(next_part, int) else {}
|
63
|
-
)
|
64
|
-
elif isinstance(nested_structure, dict):
|
65
|
-
if part is None:
|
66
|
-
raise TypeError("Cannot use NoneType as a key in a dictionary")
|
67
|
-
if isinstance(part, (float, complex)):
|
68
|
-
raise TypeError(
|
69
|
-
f"Unsupported key type: {type(part).__name__}.Only string keys are acceptable.",
|
70
|
-
)
|
71
|
-
if part not in nested_structure:
|
72
|
-
next_part = indices[i + 1]
|
73
|
-
nested_structure[part] = (
|
74
|
-
[] if isinstance(next_part, int) else {}
|
75
|
-
)
|
76
|
-
else:
|
77
|
-
raise TypeError(
|
78
|
-
f"Invalid container type: {type(nested_structure)} encountered during insertion"
|
79
|
-
)
|
80
|
-
|
81
|
-
nested_structure = nested_structure[part]
|
82
|
-
current_depth += 1
|
83
|
-
|
84
|
-
last_part = indices[-1]
|
85
|
-
if isinstance(last_part, int):
|
86
|
-
if isinstance(nested_structure, dict):
|
87
|
-
raise TypeError(
|
88
|
-
f"Unsupported key type: {type(last_part).__name__}."
|
89
|
-
"Only string keys are acceptable.",
|
90
|
-
)
|
91
|
-
while len(nested_structure) <= last_part:
|
92
|
-
nested_structure.append(None)
|
93
|
-
nested_structure[last_part] = value
|
94
|
-
elif isinstance(nested_structure, list):
|
95
|
-
raise TypeError("Cannot use non-integer index on a list")
|
96
|
-
else:
|
97
|
-
if last_part is None:
|
98
|
-
raise TypeError("Cannot use NoneType as a key in a dictionary")
|
99
|
-
if isinstance(last_part, (float, complex)):
|
100
|
-
raise TypeError(
|
101
|
-
f"Unsupported key type: {type(last_part).__name__}."
|
102
|
-
"Only string keys are acceptable.",
|
103
|
-
)
|
104
|
-
nested_structure[last_part] = value
|
lionagi/libs/nested/nmerge.py
DELETED
@@ -1,158 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from collections import defaultdict
|
6
|
-
from collections.abc import Callable, Sequence
|
7
|
-
from itertools import chain
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from .utils import is_homogeneous
|
11
|
-
|
12
|
-
|
13
|
-
def nmerge(
|
14
|
-
nested_structure: Sequence[dict[str, Any] | list[Any]],
|
15
|
-
/,
|
16
|
-
*,
|
17
|
-
overwrite: bool = False,
|
18
|
-
dict_sequence: bool = False,
|
19
|
-
sort_list: bool = False,
|
20
|
-
custom_sort: Callable[[Any], Any] | None = None,
|
21
|
-
) -> dict[str, Any] | list[Any]:
|
22
|
-
"""
|
23
|
-
Merge multiple dictionaries, lists, or sequences into a unified structure.
|
24
|
-
|
25
|
-
Args:
|
26
|
-
nested_structure: A sequence containing dictionaries, lists, or other
|
27
|
-
iterable objects to merge.
|
28
|
-
overwrite: If True, overwrite existing keys in dictionaries with
|
29
|
-
those from subsequent dictionaries.
|
30
|
-
dict_sequence: Enables unique key generation for duplicate keys by
|
31
|
-
appending a sequence number. Applicable only if `overwrite` is
|
32
|
-
False.
|
33
|
-
sort_list: When True, sort the resulting list after merging. It does
|
34
|
-
not affect dictionaries.
|
35
|
-
custom_sort: An optional callable that defines custom sorting logic
|
36
|
-
for the merged list.
|
37
|
-
|
38
|
-
Returns:
|
39
|
-
A merged dictionary or list, depending on the types present in
|
40
|
-
`nested_structure`.
|
41
|
-
|
42
|
-
Raises:
|
43
|
-
TypeError: If `nested_structure` contains objects of incompatible
|
44
|
-
types that cannot be merged.
|
45
|
-
"""
|
46
|
-
if not isinstance(nested_structure, list):
|
47
|
-
raise TypeError("Please input a list")
|
48
|
-
if is_homogeneous(nested_structure, dict):
|
49
|
-
return _merge_dicts(nested_structure, overwrite, dict_sequence)
|
50
|
-
elif is_homogeneous(nested_structure, list):
|
51
|
-
return _merge_sequences(nested_structure, sort_list, custom_sort)
|
52
|
-
else:
|
53
|
-
raise TypeError(
|
54
|
-
"All items in the input list must be of the same type, either dict, list, or Iterable."
|
55
|
-
)
|
56
|
-
|
57
|
-
|
58
|
-
def _deep_merge_dicts(
|
59
|
-
dict1: dict[str, Any], dict2: dict[str, Any]
|
60
|
-
) -> dict[str, Any]:
|
61
|
-
"""
|
62
|
-
Recursively merges two dictionaries, combining values where keys overlap.
|
63
|
-
|
64
|
-
Args:
|
65
|
-
dict1: The first dictionary.
|
66
|
-
dict2: The second dictionary.
|
67
|
-
|
68
|
-
Returns:
|
69
|
-
The merged dictionary.
|
70
|
-
"""
|
71
|
-
for key in dict2:
|
72
|
-
if key in dict1:
|
73
|
-
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
74
|
-
_deep_merge_dicts(dict1[key], dict2[key])
|
75
|
-
else:
|
76
|
-
if not isinstance(dict1[key], list):
|
77
|
-
dict1[key] = [dict1[key]]
|
78
|
-
dict1[key].append(dict2[key])
|
79
|
-
else:
|
80
|
-
dict1[key] = dict2[key]
|
81
|
-
return dict1
|
82
|
-
|
83
|
-
|
84
|
-
def _merge_dicts(
|
85
|
-
iterables: list[dict[str, Any]],
|
86
|
-
dict_update: bool,
|
87
|
-
dict_sequence: bool,
|
88
|
-
) -> dict[str, Any]:
|
89
|
-
"""
|
90
|
-
Merges a list of dictionaries into a single dictionary, with options for
|
91
|
-
handling duplicate keys and sequences.
|
92
|
-
|
93
|
-
Args:
|
94
|
-
iterables: A list of dictionaries to merge.
|
95
|
-
dict_update: If True, overwrite existing keys in dictionaries
|
96
|
-
with those from subsequent dictionaries.
|
97
|
-
dict_sequence: Enables unique key generation for duplicate keys
|
98
|
-
by appending a sequence number
|
99
|
-
|
100
|
-
Returns:
|
101
|
-
The merged dictionary.
|
102
|
-
"""
|
103
|
-
merged_dict = {} # {'a': [1, 2]}
|
104
|
-
sequence_counters = defaultdict(int)
|
105
|
-
list_values = {}
|
106
|
-
|
107
|
-
for d in iterables: # [{'a': [1, 2]}, {'a': [3, 4]}]
|
108
|
-
for key, value in d.items(): # {'a': [3, 4]}
|
109
|
-
if key not in merged_dict or dict_update:
|
110
|
-
if (
|
111
|
-
key in merged_dict
|
112
|
-
and isinstance(merged_dict[key], dict)
|
113
|
-
and isinstance(value, dict)
|
114
|
-
):
|
115
|
-
_deep_merge_dicts(merged_dict[key], value)
|
116
|
-
else:
|
117
|
-
merged_dict[key] = value # {'a': [1, 2]}
|
118
|
-
if isinstance(value, list):
|
119
|
-
list_values[key] = True
|
120
|
-
elif dict_sequence:
|
121
|
-
sequence_counters[key] += 1
|
122
|
-
new_key = f"{key}{sequence_counters[key]}"
|
123
|
-
merged_dict[new_key] = value
|
124
|
-
else:
|
125
|
-
if not isinstance(merged_dict[key], list) or list_values.get(
|
126
|
-
key, False
|
127
|
-
):
|
128
|
-
merged_dict[key] = [merged_dict[key]]
|
129
|
-
merged_dict[key].append(value)
|
130
|
-
|
131
|
-
return merged_dict
|
132
|
-
|
133
|
-
|
134
|
-
def _merge_sequences(
|
135
|
-
iterables: list[list[Any]],
|
136
|
-
sort_list: bool,
|
137
|
-
custom_sort: Callable[[Any], Any] | None = None,
|
138
|
-
) -> list[Any]:
|
139
|
-
"""
|
140
|
-
Merges a list of lists into a single list, with options for sorting and
|
141
|
-
custom sorting logic.
|
142
|
-
|
143
|
-
Args:
|
144
|
-
iterables: A list of lists to merge.
|
145
|
-
sort_list: When True, sort the resulting list after merging.
|
146
|
-
custom_sort: An optional callable that defines custom sorting logic
|
147
|
-
for the merged list.
|
148
|
-
|
149
|
-
Returns:
|
150
|
-
The merged list.
|
151
|
-
"""
|
152
|
-
merged_list = list(chain(*iterables))
|
153
|
-
if sort_list:
|
154
|
-
if custom_sort:
|
155
|
-
return sorted(merged_list, key=custom_sort)
|
156
|
-
else:
|
157
|
-
return sorted(merged_list, key=lambda x: (isinstance(x, str), x))
|
158
|
-
return merged_list
|
lionagi/libs/nested/npop.py
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from collections.abc import Sequence
|
6
|
-
from typing import Any
|
7
|
-
|
8
|
-
from lionagi.utils import UNDEFINED, to_list
|
9
|
-
|
10
|
-
|
11
|
-
def npop(
|
12
|
-
input_: dict[str, Any] | list[Any],
|
13
|
-
/,
|
14
|
-
indices: str | int | Sequence[str | int],
|
15
|
-
default: Any = UNDEFINED,
|
16
|
-
) -> Any:
|
17
|
-
"""
|
18
|
-
Perform a nested pop operation on the input structure.
|
19
|
-
|
20
|
-
This function navigates through the nested structure using the provided
|
21
|
-
indices and removes and returns the value at the final location.
|
22
|
-
|
23
|
-
Args:
|
24
|
-
input_: The input nested structure (dict or list) to pop from.
|
25
|
-
indices: A single index or a sequence of indices to navigate the
|
26
|
-
nested structure.
|
27
|
-
default: The value to return if the key is not found. If not
|
28
|
-
provided, a KeyError will be raised.
|
29
|
-
|
30
|
-
Returns:
|
31
|
-
The value at the specified nested location.
|
32
|
-
|
33
|
-
Raises:
|
34
|
-
ValueError: If the indices list is empty.
|
35
|
-
KeyError: If a key is not found in a dictionary.
|
36
|
-
IndexError: If an index is out of range for a list.
|
37
|
-
TypeError: If an operation is not supported on the current data type.
|
38
|
-
"""
|
39
|
-
if not indices:
|
40
|
-
raise ValueError("Indices list cannot be empty")
|
41
|
-
|
42
|
-
indices = to_list(indices)
|
43
|
-
|
44
|
-
current = input_
|
45
|
-
for key in indices[:-1]:
|
46
|
-
if isinstance(current, dict):
|
47
|
-
if current.get(key):
|
48
|
-
current = current[key]
|
49
|
-
else:
|
50
|
-
raise KeyError(f"{key} is not found in {current}")
|
51
|
-
elif isinstance(current, list) and isinstance(key, int):
|
52
|
-
if key >= len(current):
|
53
|
-
raise KeyError(
|
54
|
-
f"{key} exceeds the length of the list {current}"
|
55
|
-
)
|
56
|
-
elif key < 0:
|
57
|
-
raise ValueError("list index cannot be negative")
|
58
|
-
current = current[key]
|
59
|
-
|
60
|
-
last_key = indices[-1]
|
61
|
-
try:
|
62
|
-
return current.pop(
|
63
|
-
last_key,
|
64
|
-
)
|
65
|
-
except Exception as e:
|
66
|
-
if default is not UNDEFINED:
|
67
|
-
return default
|
68
|
-
else:
|
69
|
-
raise KeyError(f"Invalid npop. Error: {e}")
|
lionagi/libs/nested/nset.py
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from collections.abc import Sequence
|
6
|
-
from typing import Any
|
7
|
-
|
8
|
-
from lionagi.utils import to_list
|
9
|
-
|
10
|
-
from .utils import ensure_list_index
|
11
|
-
|
12
|
-
|
13
|
-
def nset(
|
14
|
-
nested_structure: dict[str, Any] | list[Any],
|
15
|
-
/,
|
16
|
-
indices: str | int | Sequence[str | int],
|
17
|
-
value: Any,
|
18
|
-
) -> None:
|
19
|
-
"""Set a value within a nested structure at the specified path.
|
20
|
-
|
21
|
-
This method allows setting a value deep within a nested dictionary or list
|
22
|
-
by specifying a path to the target location using a sequence of indices.
|
23
|
-
Each index in the sequence represents a level in the nested structure,
|
24
|
-
with integers used for list indices and strings for dictionary keys.
|
25
|
-
|
26
|
-
Args:
|
27
|
-
nested_structure: The nested structure to modify.
|
28
|
-
indices: The path of indices leading to the target location.
|
29
|
-
value: The value to set at the specified location.
|
30
|
-
|
31
|
-
Raises:
|
32
|
-
ValueError: If the indices sequence is empty.
|
33
|
-
TypeError: If the target container is not a list or dictionary,
|
34
|
-
or if the index type is incorrect.
|
35
|
-
|
36
|
-
Examples:
|
37
|
-
>>> data = {'a': {'b': [10, 20]}}
|
38
|
-
>>> nset(data, ['a', 'b', 1], 99)
|
39
|
-
>>> assert data == {'a': {'b': [10, 99]}}
|
40
|
-
|
41
|
-
>>> data = [0, [1, 2], 3]
|
42
|
-
>>> nset(data, [1, 1], 99)
|
43
|
-
>>> assert data == [0, [1, 99], 3]
|
44
|
-
"""
|
45
|
-
|
46
|
-
if not indices:
|
47
|
-
raise ValueError(
|
48
|
-
"Indices list is empty, cannot determine target container"
|
49
|
-
)
|
50
|
-
|
51
|
-
_indices = to_list(indices)
|
52
|
-
target_container = nested_structure
|
53
|
-
|
54
|
-
for i, index in enumerate(_indices[:-1]):
|
55
|
-
if isinstance(target_container, list):
|
56
|
-
if not isinstance(index, int):
|
57
|
-
raise TypeError("Cannot use non-integer index on a list")
|
58
|
-
ensure_list_index(target_container, index)
|
59
|
-
if target_container[index] is None:
|
60
|
-
next_index = _indices[i + 1]
|
61
|
-
target_container[index] = (
|
62
|
-
[] if isinstance(next_index, int) else {}
|
63
|
-
)
|
64
|
-
elif isinstance(target_container, dict):
|
65
|
-
if isinstance(index, int):
|
66
|
-
raise TypeError(
|
67
|
-
f"Unsupported key type: {type(index).__name__}. "
|
68
|
-
"Only string keys are acceptable."
|
69
|
-
)
|
70
|
-
if index not in target_container:
|
71
|
-
next_index = _indices[i + 1]
|
72
|
-
target_container[index] = (
|
73
|
-
[] if isinstance(next_index, int) else {}
|
74
|
-
)
|
75
|
-
else:
|
76
|
-
raise TypeError("Target container is not a list or dictionary")
|
77
|
-
|
78
|
-
target_container = target_container[index]
|
79
|
-
|
80
|
-
last_index = _indices[-1]
|
81
|
-
if isinstance(target_container, list):
|
82
|
-
if not isinstance(last_index, int):
|
83
|
-
raise TypeError("Cannot use non-integer index on a list")
|
84
|
-
ensure_list_index(target_container, last_index)
|
85
|
-
target_container[last_index] = value
|
86
|
-
elif isinstance(target_container, dict):
|
87
|
-
if not isinstance(last_index, str):
|
88
|
-
raise TypeError(
|
89
|
-
f"Unsupported key type: {type(last_index).__name__}. "
|
90
|
-
"Only string keys are acceptable."
|
91
|
-
)
|
92
|
-
target_container[last_index] = value
|
93
|
-
else:
|
94
|
-
raise TypeError("Cannot set value on non-list/dict element")
|
lionagi/libs/nested/unflatten.py
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from typing import Any
|
6
|
-
|
7
|
-
|
8
|
-
def unflatten(
|
9
|
-
flat_dict: dict[str, Any], sep: str = "|", inplace: bool = False
|
10
|
-
) -> dict[str, Any] | list[Any]:
|
11
|
-
"""
|
12
|
-
Unflatten a single-level dictionary into a nested dictionary or list.
|
13
|
-
|
14
|
-
Args:
|
15
|
-
flat_dict: The flattened dictionary to unflatten.
|
16
|
-
sep: The separator used for joining keys.
|
17
|
-
inplace: Whether to modify the input dictionary in place.
|
18
|
-
|
19
|
-
Returns:
|
20
|
-
The unflattened nested dictionary or list.
|
21
|
-
|
22
|
-
Examples:
|
23
|
-
>>> unflatten({"a|b|c": 1, "a|b|d": 2})
|
24
|
-
{'a': {'b': {'c': 1, 'd': 2}}}
|
25
|
-
|
26
|
-
>>> unflatten({"0": "a", "1": "b", "2": "c"})
|
27
|
-
['a', 'b', 'c']
|
28
|
-
"""
|
29
|
-
|
30
|
-
def _unflatten(data: dict) -> dict | list:
|
31
|
-
result = {}
|
32
|
-
for key, value in data.items():
|
33
|
-
parts = key.split(sep)
|
34
|
-
current = result
|
35
|
-
for part in parts[:-1]:
|
36
|
-
if part not in current:
|
37
|
-
current[part] = {}
|
38
|
-
current = current[part]
|
39
|
-
if isinstance(value, dict):
|
40
|
-
current[parts[-1]] = _unflatten(value)
|
41
|
-
else:
|
42
|
-
current[parts[-1]] = value
|
43
|
-
|
44
|
-
# Convert dictionary to list if keys are consecutive integers
|
45
|
-
if result and all(
|
46
|
-
isinstance(key, str) and key.isdigit() for key in result
|
47
|
-
):
|
48
|
-
return [result[str(i)] for i in range(len(result))]
|
49
|
-
return result
|
50
|
-
|
51
|
-
if inplace:
|
52
|
-
unflattened_dict = {}
|
53
|
-
for key, value in flat_dict.items():
|
54
|
-
parts = key.split(sep)
|
55
|
-
current = unflattened_dict
|
56
|
-
for part in parts[:-1]:
|
57
|
-
if part not in current:
|
58
|
-
current[part] = {}
|
59
|
-
current = current[part]
|
60
|
-
current[parts[-1]] = value
|
61
|
-
|
62
|
-
unflattened_result = _unflatten(unflattened_dict)
|
63
|
-
flat_dict.clear()
|
64
|
-
if isinstance(unflattened_result, list):
|
65
|
-
flat_dict.update(
|
66
|
-
{str(i): v for i, v in enumerate(unflattened_result)}
|
67
|
-
)
|
68
|
-
else:
|
69
|
-
flat_dict.update(unflattened_result)
|
70
|
-
return flat_dict
|
71
|
-
|
72
|
-
else:
|
73
|
-
unflattened_dict = {}
|
74
|
-
for key, value in flat_dict.items():
|
75
|
-
parts = key.split(sep)
|
76
|
-
current = unflattened_dict
|
77
|
-
for part in parts[:-1]:
|
78
|
-
if part not in current:
|
79
|
-
current[part] = {}
|
80
|
-
current = current[part]
|
81
|
-
current[parts[-1]] = value
|
82
|
-
|
83
|
-
return _unflatten(unflattened_dict)
|
lionagi/libs/nested/utils.py
DELETED
@@ -1,189 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from typing import Any
|
6
|
-
|
7
|
-
from lionagi.utils import UNDEFINED
|
8
|
-
|
9
|
-
|
10
|
-
def is_homogeneous(
|
11
|
-
iterables: list[Any] | dict[Any, Any], type_check: type | tuple[type, ...]
|
12
|
-
) -> bool:
|
13
|
-
"""
|
14
|
-
Check if all elements in a list or all values in a dict are of same type.
|
15
|
-
|
16
|
-
Args:
|
17
|
-
iterables: The list or dictionary to check.
|
18
|
-
type_check: The type to check against.
|
19
|
-
|
20
|
-
Returns:
|
21
|
-
True if all elements/values are of the same type, False otherwise.
|
22
|
-
"""
|
23
|
-
if isinstance(iterables, list):
|
24
|
-
return all(isinstance(it, type_check) for it in iterables)
|
25
|
-
|
26
|
-
elif isinstance(iterables, dict):
|
27
|
-
return all(isinstance(val, type_check) for val in iterables.values())
|
28
|
-
|
29
|
-
else:
|
30
|
-
return isinstance(iterables, type_check)
|
31
|
-
|
32
|
-
|
33
|
-
def is_same_dtype(
|
34
|
-
input_: list[Any] | dict[Any, Any],
|
35
|
-
dtype: type | None = None,
|
36
|
-
return_dtype: bool = False,
|
37
|
-
) -> bool | tuple[bool, type | None]:
|
38
|
-
"""
|
39
|
-
Check if all elements in a list or dict values are of the same data type.
|
40
|
-
|
41
|
-
Args:
|
42
|
-
input_: The input list or dictionary to check.
|
43
|
-
dtype: The data type to check against. If None, uses the type of the
|
44
|
-
first element.
|
45
|
-
return_dtype: If True, return the data type with the check result.
|
46
|
-
|
47
|
-
Returns:
|
48
|
-
If return_dtype is False, returns True if all elements are of the
|
49
|
-
same type (or if the input is empty), False otherwise.
|
50
|
-
If return_dtype is True, returns a tuple (bool, type | None).
|
51
|
-
"""
|
52
|
-
if not input_:
|
53
|
-
return True
|
54
|
-
|
55
|
-
iterable = input_.values() if isinstance(input_, dict) else input_
|
56
|
-
first_element_type = type(next(iter(iterable), None))
|
57
|
-
|
58
|
-
dtype = dtype or first_element_type
|
59
|
-
|
60
|
-
result = all(isinstance(element, dtype) for element in iterable)
|
61
|
-
return (result, dtype) if return_dtype else result
|
62
|
-
|
63
|
-
|
64
|
-
def is_structure_homogeneous(
|
65
|
-
structure: Any, return_structure_type: bool = False
|
66
|
-
) -> bool | tuple[bool, type | None]:
|
67
|
-
"""
|
68
|
-
Check if a nested structure is homogeneous (no mix of lists and dicts).
|
69
|
-
|
70
|
-
Args:
|
71
|
-
structure: The nested structure to check.
|
72
|
-
return_structure_type: If True, return the type of the homogeneous
|
73
|
-
structure.
|
74
|
-
|
75
|
-
Returns:
|
76
|
-
If return_structure_type is False, returns True if the structure is
|
77
|
-
homogeneous, False otherwise.
|
78
|
-
If True, returns a tuple (bool, type | None).
|
79
|
-
|
80
|
-
Examples:
|
81
|
-
>>> is_structure_homogeneous({'a': {'b': 1}, 'c': {'d': 2}})
|
82
|
-
True
|
83
|
-
>>> is_structure_homogeneous({'a': {'b': 1}, 'c': [1, 2]})
|
84
|
-
False
|
85
|
-
"""
|
86
|
-
|
87
|
-
def _check_structure(substructure):
|
88
|
-
structure_type = None
|
89
|
-
if isinstance(substructure, list):
|
90
|
-
structure_type = list
|
91
|
-
for item in substructure:
|
92
|
-
if not isinstance(item, structure_type) and isinstance(
|
93
|
-
item, list | dict
|
94
|
-
):
|
95
|
-
return False, None
|
96
|
-
result, _ = _check_structure(item)
|
97
|
-
if not result:
|
98
|
-
return False, None
|
99
|
-
elif isinstance(substructure, dict):
|
100
|
-
structure_type = dict
|
101
|
-
for item in substructure.values():
|
102
|
-
if not isinstance(item, structure_type) and isinstance(
|
103
|
-
item, list | dict
|
104
|
-
):
|
105
|
-
return False, None
|
106
|
-
result, _ = _check_structure(item)
|
107
|
-
if not result:
|
108
|
-
return False, None
|
109
|
-
return True, structure_type
|
110
|
-
|
111
|
-
is_homogeneous, structure_type = _check_structure(structure)
|
112
|
-
return (
|
113
|
-
(is_homogeneous, structure_type)
|
114
|
-
if return_structure_type
|
115
|
-
else is_homogeneous
|
116
|
-
)
|
117
|
-
|
118
|
-
|
119
|
-
def deep_update(
|
120
|
-
original: dict[Any, Any], update: dict[Any, Any]
|
121
|
-
) -> dict[Any, Any]:
|
122
|
-
"""
|
123
|
-
Recursively merge two dicts, updating nested dicts instead of overwriting.
|
124
|
-
|
125
|
-
Args:
|
126
|
-
original: The dictionary to update.
|
127
|
-
update: The dictionary containing updates to apply to `original`.
|
128
|
-
|
129
|
-
Returns:
|
130
|
-
The `original` dictionary after applying updates from `update`.
|
131
|
-
|
132
|
-
Note:
|
133
|
-
This method modifies the `original` dictionary in place.
|
134
|
-
"""
|
135
|
-
for key, value in update.items():
|
136
|
-
if isinstance(value, dict) and key in original:
|
137
|
-
original[key] = deep_update(original.get(key, {}), value)
|
138
|
-
else:
|
139
|
-
original[key] = value
|
140
|
-
return original
|
141
|
-
|
142
|
-
|
143
|
-
def get_target_container(
|
144
|
-
nested: list[Any] | dict[Any, Any], indices: list[int | str]
|
145
|
-
) -> list[Any] | dict[Any, Any]:
|
146
|
-
"""
|
147
|
-
Retrieve the target container in a nested structure using indices.
|
148
|
-
|
149
|
-
Args:
|
150
|
-
nested: The nested structure to navigate.
|
151
|
-
indices: A list of indices to navigate through the nested structure.
|
152
|
-
|
153
|
-
Returns:
|
154
|
-
The target container at the specified path.
|
155
|
-
|
156
|
-
Raises:
|
157
|
-
IndexError: If a list index is out of range.
|
158
|
-
KeyError: If a dictionary key is not found.
|
159
|
-
TypeError: If the current element is neither a list nor a dictionary.
|
160
|
-
"""
|
161
|
-
current_element = nested
|
162
|
-
for index in indices:
|
163
|
-
if isinstance(current_element, list):
|
164
|
-
if isinstance(index, str) and index.isdigit():
|
165
|
-
index = int(index)
|
166
|
-
|
167
|
-
if isinstance(index, int) and 0 <= index < len(current_element):
|
168
|
-
current_element = current_element[index]
|
169
|
-
|
170
|
-
else:
|
171
|
-
raise IndexError("List index is invalid or out of range")
|
172
|
-
|
173
|
-
elif isinstance(current_element, dict):
|
174
|
-
if index in current_element:
|
175
|
-
current_element = current_element.get(index, None)
|
176
|
-
else:
|
177
|
-
raise KeyError("Key not found in dictionary")
|
178
|
-
else:
|
179
|
-
raise TypeError(
|
180
|
-
"Current element is neither a list nor a dictionary"
|
181
|
-
)
|
182
|
-
return current_element
|
183
|
-
|
184
|
-
|
185
|
-
def ensure_list_index(
|
186
|
-
lst: list[Any], index: int, default: Any = UNDEFINED
|
187
|
-
) -> None:
|
188
|
-
while len(lst) <= index:
|
189
|
-
lst.append(default if default is not UNDEFINED else None)
|