lionagi 0.10.7__py3-none-any.whl → 0.12.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/__init__.py +1 -0
- lionagi/fields/file.py +1 -1
- lionagi/fields/reason.py +1 -1
- lionagi/libs/file/concat.py +6 -1
- lionagi/libs/file/concat_files.py +5 -1
- lionagi/libs/file/create_path.py +80 -0
- lionagi/libs/file/file_util.py +358 -0
- lionagi/libs/file/save.py +1 -1
- lionagi/libs/package/imports.py +177 -8
- lionagi/libs/parse/fuzzy_parse_json.py +117 -0
- lionagi/libs/parse/to_dict.py +336 -0
- lionagi/libs/parse/to_json.py +61 -0
- lionagi/libs/parse/to_num.py +378 -0
- lionagi/libs/parse/to_xml.py +57 -0
- lionagi/libs/parse/xml_parser.py +148 -0
- lionagi/libs/schema/breakdown_pydantic_annotation.py +48 -0
- lionagi/protocols/generic/log.py +2 -1
- lionagi/utils.py +123 -921
- lionagi/version.py +1 -1
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/METADATA +8 -11
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/RECORD +24 -30
- lionagi/libs/parse.py +0 -30
- lionagi/tools/browser/__init__.py +0 -0
- lionagi/tools/browser/providers/browser_use_.py +0 -3
- lionagi/tools/code/__init__.py +0 -3
- lionagi/tools/code/coder.py +0 -3
- lionagi/tools/code/manager.py +0 -3
- lionagi/tools/code/providers/__init__.py +0 -3
- lionagi/tools/code/providers/aider_.py +0 -3
- lionagi/tools/code/providers/e2b_.py +0 -3
- lionagi/tools/code/sandbox.py +0 -3
- lionagi/tools/file/manager.py +0 -3
- lionagi/tools/file/providers/__init__.py +0 -3
- lionagi/tools/file/providers/docling_.py +0 -3
- lionagi/tools/file/writer.py +0 -3
- lionagi/tools/query/__init__.py +0 -3
- /lionagi/{tools/browser/providers → libs/parse}/__init__.py +0 -0
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/WHEEL +0 -0
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,378 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from decimal import Decimal
|
5
|
+
from typing import Any, Literal, TypeVar
|
6
|
+
|
7
|
+
# Type definitions
|
8
|
+
NUM_TYPE_LITERAL = Literal["int", "float", "complex"]
|
9
|
+
NUM_TYPES = type[int] | type[float] | type[complex] | NUM_TYPE_LITERAL
|
10
|
+
NumericType = TypeVar("NumericType", int, float, complex)
|
11
|
+
|
12
|
+
# Type mapping
|
13
|
+
TYPE_MAP = {"int": int, "float": float, "complex": complex}
|
14
|
+
|
15
|
+
# Regex patterns for different numeric formats
|
16
|
+
PATTERNS = {
|
17
|
+
"scientific": r"[-+]?(?:\d*\.)?\d+[eE][-+]?\d+",
|
18
|
+
"complex_sci": r"[-+]?(?:\d*\.)?\d+(?:[eE][-+]?\d+)?[-+](?:\d*\.)?\d+(?:[eE][-+]?\d+)?[jJ]",
|
19
|
+
"complex": r"[-+]?(?:\d*\.)?\d+[-+](?:\d*\.)?\d+[jJ]",
|
20
|
+
"pure_imaginary": r"[-+]?(?:\d*\.)?\d*[jJ]",
|
21
|
+
"percentage": r"[-+]?(?:\d*\.)?\d+%",
|
22
|
+
"fraction": r"[-+]?\d+/\d+",
|
23
|
+
"decimal": r"[-+]?(?:\d*\.)?\d+",
|
24
|
+
"special": r"[-+]?(?:inf|infinity|nan)",
|
25
|
+
}
|
26
|
+
|
27
|
+
|
28
|
+
def to_num(
|
29
|
+
input_: Any,
|
30
|
+
/,
|
31
|
+
*,
|
32
|
+
upper_bound: int | float | None = None,
|
33
|
+
lower_bound: int | float | None = None,
|
34
|
+
num_type: NUM_TYPES = float,
|
35
|
+
precision: int | None = None,
|
36
|
+
num_count: int = 1,
|
37
|
+
) -> int | float | complex | list[int | float | complex]:
|
38
|
+
"""Convert input to numeric type(s) with validation and bounds checking.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
input_value: The input to convert to number(s).
|
42
|
+
upper_bound: Maximum allowed value (inclusive).
|
43
|
+
lower_bound: Minimum allowed value (inclusive).
|
44
|
+
num_type: Target numeric type ('int', 'float', 'complex' or type objects).
|
45
|
+
precision: Number of decimal places for rounding (float only).
|
46
|
+
num_count: Number of numeric values to extract.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Converted number(s). Single value if num_count=1, else list.
|
50
|
+
|
51
|
+
Raises:
|
52
|
+
ValueError: For invalid input or out of bounds values.
|
53
|
+
TypeError: For invalid input types or invalid type conversions.
|
54
|
+
"""
|
55
|
+
# Validate input
|
56
|
+
if isinstance(input_, (list, tuple)):
|
57
|
+
raise TypeError("Input cannot be a sequence")
|
58
|
+
|
59
|
+
# Handle boolean input
|
60
|
+
if isinstance(input_, bool):
|
61
|
+
return validate_num_type(num_type)(input_)
|
62
|
+
|
63
|
+
# Handle direct numeric input
|
64
|
+
if isinstance(input_, (int, float, complex, Decimal)):
|
65
|
+
inferred_type = type(input_)
|
66
|
+
if isinstance(input_, Decimal):
|
67
|
+
inferred_type = float
|
68
|
+
value = float(input_) if not isinstance(input_, complex) else input_
|
69
|
+
value = apply_bounds(value, upper_bound, lower_bound)
|
70
|
+
value = apply_precision(value, precision)
|
71
|
+
return convert_type(value, validate_num_type(num_type), inferred_type)
|
72
|
+
|
73
|
+
# Convert input to string and extract numbers
|
74
|
+
input_str = str(input_)
|
75
|
+
number_matches = extract_numbers(input_str)
|
76
|
+
|
77
|
+
if not number_matches:
|
78
|
+
raise ValueError(f"No valid numbers found in: {input_str}")
|
79
|
+
|
80
|
+
# Process numbers
|
81
|
+
results = []
|
82
|
+
target_type = validate_num_type(num_type)
|
83
|
+
|
84
|
+
number_matches = (
|
85
|
+
number_matches[:num_count]
|
86
|
+
if num_count < len(number_matches)
|
87
|
+
else number_matches
|
88
|
+
)
|
89
|
+
|
90
|
+
for type_and_value in number_matches:
|
91
|
+
try:
|
92
|
+
# Infer appropriate type
|
93
|
+
inferred_type = infer_type(type_and_value)
|
94
|
+
|
95
|
+
# Parse to numeric value
|
96
|
+
value = parse_number(type_and_value)
|
97
|
+
|
98
|
+
# Apply bounds if not complex
|
99
|
+
value = apply_bounds(value, upper_bound, lower_bound)
|
100
|
+
|
101
|
+
# Apply precision
|
102
|
+
value = apply_precision(value, precision)
|
103
|
+
|
104
|
+
# Convert to target type if different from inferred
|
105
|
+
value = convert_type(value, target_type, inferred_type)
|
106
|
+
|
107
|
+
results.append(value)
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
if len(type_and_value) == 2:
|
111
|
+
raise type(e)(
|
112
|
+
f"Error processing {type_and_value[1]}: {str(e)}"
|
113
|
+
)
|
114
|
+
raise type(e)(f"Error processing {type_and_value}: {str(e)}")
|
115
|
+
|
116
|
+
if results and num_count == 1:
|
117
|
+
return results[0]
|
118
|
+
return results
|
119
|
+
|
120
|
+
|
121
|
+
def extract_numbers(text: str) -> list[tuple[str, str]]:
|
122
|
+
"""Extract numeric values from text using ordered regex patterns.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
text: The text to extract numbers from.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
List of tuples containing (pattern_type, matched_value).
|
129
|
+
"""
|
130
|
+
combined_pattern = "|".join(PATTERNS.values())
|
131
|
+
matches = re.finditer(combined_pattern, text, re.IGNORECASE)
|
132
|
+
numbers = []
|
133
|
+
|
134
|
+
for match in matches:
|
135
|
+
value = match.group()
|
136
|
+
# Check which pattern matched
|
137
|
+
for pattern_name, pattern in PATTERNS.items():
|
138
|
+
if re.fullmatch(pattern, value, re.IGNORECASE):
|
139
|
+
numbers.append((pattern_name, value))
|
140
|
+
break
|
141
|
+
|
142
|
+
return numbers
|
143
|
+
|
144
|
+
|
145
|
+
def validate_num_type(num_type: NUM_TYPES) -> type:
|
146
|
+
"""Validate and normalize numeric type specification.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
num_type: The numeric type to validate.
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
The normalized Python type object.
|
153
|
+
|
154
|
+
Raises:
|
155
|
+
ValueError: If the type specification is invalid.
|
156
|
+
"""
|
157
|
+
if isinstance(num_type, str):
|
158
|
+
if num_type not in TYPE_MAP:
|
159
|
+
raise ValueError(f"Invalid number type: {num_type}")
|
160
|
+
return TYPE_MAP[num_type]
|
161
|
+
|
162
|
+
if num_type not in (int, float, complex):
|
163
|
+
raise ValueError(f"Invalid number type: {num_type}")
|
164
|
+
return num_type
|
165
|
+
|
166
|
+
|
167
|
+
def infer_type(value: tuple[str, str]) -> type:
|
168
|
+
"""Infer appropriate numeric type from value.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
value: Tuple of (pattern_type, matched_value).
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
The inferred Python type.
|
175
|
+
"""
|
176
|
+
pattern_type, _ = value
|
177
|
+
if pattern_type in ("complex", "complex_sci", "pure_imaginary"):
|
178
|
+
return complex
|
179
|
+
return float
|
180
|
+
|
181
|
+
|
182
|
+
def convert_special(value: str) -> float:
|
183
|
+
"""Convert special float values (inf, -inf, nan).
|
184
|
+
|
185
|
+
Args:
|
186
|
+
value: The string value to convert.
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
The converted float value.
|
190
|
+
"""
|
191
|
+
value = value.lower()
|
192
|
+
if "infinity" in value or "inf" in value:
|
193
|
+
return float("-inf") if value.startswith("-") else float("inf")
|
194
|
+
return float("nan")
|
195
|
+
|
196
|
+
|
197
|
+
def convert_percentage(value: str) -> float:
|
198
|
+
"""Convert percentage string to float.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
value: The percentage string to convert.
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
The converted float value.
|
205
|
+
|
206
|
+
Raises:
|
207
|
+
ValueError: If the percentage value is invalid.
|
208
|
+
"""
|
209
|
+
try:
|
210
|
+
return float(value.rstrip("%")) / 100
|
211
|
+
except ValueError as e:
|
212
|
+
raise ValueError(f"Invalid percentage value: {value}") from e
|
213
|
+
|
214
|
+
|
215
|
+
def convert_complex(value: str) -> complex:
|
216
|
+
"""Convert complex number string to complex.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
value: The complex number string to convert.
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
The converted complex value.
|
223
|
+
|
224
|
+
Raises:
|
225
|
+
ValueError: If the complex number is invalid.
|
226
|
+
"""
|
227
|
+
try:
|
228
|
+
# Handle pure imaginary numbers
|
229
|
+
if value.endswith("j") or value.endswith("J"):
|
230
|
+
if value in ("j", "J"):
|
231
|
+
return complex(0, 1)
|
232
|
+
if value in ("+j", "+J"):
|
233
|
+
return complex(0, 1)
|
234
|
+
if value in ("-j", "-J"):
|
235
|
+
return complex(0, -1)
|
236
|
+
if "+" not in value and "-" not in value[1:]:
|
237
|
+
# Pure imaginary number
|
238
|
+
imag = float(value[:-1] or "1")
|
239
|
+
return complex(0, imag)
|
240
|
+
|
241
|
+
return complex(value.replace(" ", ""))
|
242
|
+
except ValueError as e:
|
243
|
+
raise ValueError(f"Invalid complex number: {value}") from e
|
244
|
+
|
245
|
+
|
246
|
+
def convert_type(
|
247
|
+
value: float | complex,
|
248
|
+
target_type: type,
|
249
|
+
inferred_type: type,
|
250
|
+
) -> int | float | complex:
|
251
|
+
"""Convert value to target type if specified, otherwise use inferred type.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
value: The value to convert.
|
255
|
+
target_type: The requested target type.
|
256
|
+
inferred_type: The inferred type from the value.
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
The converted value.
|
260
|
+
|
261
|
+
Raises:
|
262
|
+
TypeError: If the conversion is not possible.
|
263
|
+
"""
|
264
|
+
try:
|
265
|
+
# If no specific type requested, use inferred type
|
266
|
+
if target_type is float and inferred_type is complex:
|
267
|
+
return value
|
268
|
+
|
269
|
+
# Handle explicit type conversions
|
270
|
+
if target_type is int and isinstance(value, complex):
|
271
|
+
raise TypeError("Cannot convert complex number to int")
|
272
|
+
return target_type(value)
|
273
|
+
except (ValueError, TypeError) as e:
|
274
|
+
raise TypeError(
|
275
|
+
f"Cannot convert {value} to {target_type.__name__}"
|
276
|
+
) from e
|
277
|
+
|
278
|
+
|
279
|
+
def apply_bounds(
|
280
|
+
value: float | complex,
|
281
|
+
upper_bound: float | None = None,
|
282
|
+
lower_bound: float | None = None,
|
283
|
+
) -> float | complex:
|
284
|
+
"""Apply bounds checking to numeric value.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
value: The value to check.
|
288
|
+
upper_bound: Maximum allowed value (inclusive).
|
289
|
+
lower_bound: Minimum allowed value (inclusive).
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
The validated value.
|
293
|
+
|
294
|
+
Raises:
|
295
|
+
ValueError: If the value is outside bounds.
|
296
|
+
"""
|
297
|
+
if isinstance(value, complex):
|
298
|
+
return value
|
299
|
+
|
300
|
+
if upper_bound is not None and value > upper_bound:
|
301
|
+
raise ValueError(f"Value {value} exceeds upper bound {upper_bound}")
|
302
|
+
if lower_bound is not None and value < lower_bound:
|
303
|
+
raise ValueError(f"Value {value} below lower bound {lower_bound}")
|
304
|
+
return value
|
305
|
+
|
306
|
+
|
307
|
+
def apply_precision(
|
308
|
+
value: float | complex,
|
309
|
+
precision: int | None,
|
310
|
+
) -> float | complex:
|
311
|
+
"""Apply precision rounding to numeric value.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
value: The value to round.
|
315
|
+
precision: Number of decimal places.
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
The rounded value.
|
319
|
+
"""
|
320
|
+
if precision is None or isinstance(value, complex):
|
321
|
+
return value
|
322
|
+
if isinstance(value, float):
|
323
|
+
return round(value, precision)
|
324
|
+
return value
|
325
|
+
|
326
|
+
|
327
|
+
def parse_number(type_and_value: tuple[str, str]) -> float | complex:
|
328
|
+
"""Parse string to numeric value based on pattern type.
|
329
|
+
|
330
|
+
Args:
|
331
|
+
type_and_value: Tuple of (pattern_type, matched_value).
|
332
|
+
|
333
|
+
Returns:
|
334
|
+
The parsed numeric value.
|
335
|
+
|
336
|
+
Raises:
|
337
|
+
ValueError: If parsing fails.
|
338
|
+
"""
|
339
|
+
num_type, value = type_and_value
|
340
|
+
value = value.strip()
|
341
|
+
|
342
|
+
try:
|
343
|
+
if num_type == "special":
|
344
|
+
return convert_special(value)
|
345
|
+
|
346
|
+
if num_type == "percentage":
|
347
|
+
return convert_percentage(value)
|
348
|
+
|
349
|
+
if num_type == "fraction":
|
350
|
+
if "/" not in value:
|
351
|
+
raise ValueError(f"Invalid fraction: {value}")
|
352
|
+
if value.count("/") > 1:
|
353
|
+
raise ValueError(f"Invalid fraction: {value}")
|
354
|
+
num, denom = value.split("/")
|
355
|
+
if not (num.strip("-").isdigit() and denom.isdigit()):
|
356
|
+
raise ValueError(f"Invalid fraction: {value}")
|
357
|
+
denom_val = float(denom)
|
358
|
+
if denom_val == 0:
|
359
|
+
raise ValueError("Division by zero")
|
360
|
+
return float(num) / denom_val
|
361
|
+
if num_type in ("complex", "complex_sci", "pure_imaginary"):
|
362
|
+
return convert_complex(value)
|
363
|
+
if num_type == "scientific":
|
364
|
+
if "e" not in value.lower():
|
365
|
+
raise ValueError(f"Invalid scientific notation: {value}")
|
366
|
+
parts = value.lower().split("e")
|
367
|
+
if len(parts) != 2:
|
368
|
+
raise ValueError(f"Invalid scientific notation: {value}")
|
369
|
+
if not (parts[1].lstrip("+-").isdigit()):
|
370
|
+
raise ValueError(f"Invalid scientific notation: {value}")
|
371
|
+
return float(value)
|
372
|
+
if num_type == "decimal":
|
373
|
+
return float(value)
|
374
|
+
|
375
|
+
raise ValueError(f"Unknown number type: {num_type}")
|
376
|
+
except Exception as e:
|
377
|
+
# Preserve the specific error type but wrap with more context
|
378
|
+
raise type(e)(f"Failed to parse {value} as {num_type}: {str(e)}")
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
|
4
|
+
def to_xml(
|
5
|
+
obj: dict | list | str | int | float | bool | None,
|
6
|
+
root_name: str = "root",
|
7
|
+
) -> str:
|
8
|
+
"""
|
9
|
+
Convert a dictionary into an XML formatted string.
|
10
|
+
|
11
|
+
Rules:
|
12
|
+
- A dictionary key becomes an XML tag.
|
13
|
+
- If the dictionary value is:
|
14
|
+
- A primitive type (str, int, float, bool, None): it becomes the text content of the tag.
|
15
|
+
- A list: each element of the list will repeat the same tag.
|
16
|
+
- Another dictionary: it is recursively converted to nested XML.
|
17
|
+
- root_name sets the top-level XML element name.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
obj: The Python object to convert (typically a dictionary).
|
21
|
+
root_name: The name of the root XML element.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
A string representing the XML.
|
25
|
+
|
26
|
+
Examples:
|
27
|
+
>>> to_xml({"a": 1, "b": {"c": "hello", "d": [10, 20]}}, root_name="data")
|
28
|
+
'<data><a>1</a><b><c>hello</c><d>10</d><d>20</d></b></data>'
|
29
|
+
"""
|
30
|
+
|
31
|
+
def _convert(value: Any, tag_name: str) -> str:
|
32
|
+
# If value is a dict, recursively convert its keys
|
33
|
+
if isinstance(value, dict):
|
34
|
+
inner = "".join(_convert(v, k) for k, v in value.items())
|
35
|
+
return f"<{tag_name}>{inner}</{tag_name}>"
|
36
|
+
# If value is a list, repeat the same tag for each element
|
37
|
+
elif isinstance(value, list):
|
38
|
+
return "".join(_convert(item, tag_name) for item in value)
|
39
|
+
# If value is a primitive, convert to string and place inside tag
|
40
|
+
else:
|
41
|
+
text = "" if value is None else str(value)
|
42
|
+
# Escape special XML characters if needed (minimal)
|
43
|
+
text = (
|
44
|
+
text.replace("&", "&")
|
45
|
+
.replace("<", "<")
|
46
|
+
.replace(">", ">")
|
47
|
+
.replace('"', """)
|
48
|
+
.replace("'", "'")
|
49
|
+
)
|
50
|
+
return f"<{tag_name}>{text}</{tag_name}>"
|
51
|
+
|
52
|
+
# If top-level obj is not a dict, wrap it in one
|
53
|
+
if not isinstance(obj, dict):
|
54
|
+
obj = {root_name: obj}
|
55
|
+
|
56
|
+
inner_xml = "".join(_convert(v, k) for k, v in obj.items())
|
57
|
+
return f"<{root_name}>{inner_xml}</{root_name}>"
|
@@ -0,0 +1,148 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
|
7
|
+
class XMLParser:
|
8
|
+
def __init__(self, xml_string: str):
|
9
|
+
self.xml_string = xml_string.strip()
|
10
|
+
self.index = 0
|
11
|
+
|
12
|
+
def parse(self) -> dict[str, Any]:
|
13
|
+
"""Parse the XML string and return the root element as a dictionary."""
|
14
|
+
return self._parse_element()
|
15
|
+
|
16
|
+
def _parse_element(self) -> dict[str, Any]:
|
17
|
+
"""Parse a single XML element and its children."""
|
18
|
+
self._skip_whitespace()
|
19
|
+
if self.xml_string[self.index] != "<":
|
20
|
+
raise ValueError(
|
21
|
+
f"Expected '<', found '{self.xml_string[self.index]}'"
|
22
|
+
)
|
23
|
+
|
24
|
+
tag, attributes = self._parse_opening_tag()
|
25
|
+
children: dict[str, str | list | dict] = {}
|
26
|
+
text = ""
|
27
|
+
|
28
|
+
while self.index < len(self.xml_string):
|
29
|
+
self._skip_whitespace()
|
30
|
+
if self.xml_string.startswith("</", self.index):
|
31
|
+
closing_tag = self._parse_closing_tag()
|
32
|
+
if closing_tag != tag:
|
33
|
+
raise ValueError(
|
34
|
+
f"Mismatched tags: '{tag}' and '{closing_tag}'"
|
35
|
+
)
|
36
|
+
break
|
37
|
+
elif self.xml_string.startswith("<", self.index):
|
38
|
+
child = self._parse_element()
|
39
|
+
child_tag, child_data = next(iter(child.items()))
|
40
|
+
if child_tag in children:
|
41
|
+
if not isinstance(children[child_tag], list):
|
42
|
+
children[child_tag] = [children[child_tag]]
|
43
|
+
children[child_tag].append(child_data)
|
44
|
+
else:
|
45
|
+
children[child_tag] = child_data
|
46
|
+
else:
|
47
|
+
text += self._parse_text()
|
48
|
+
|
49
|
+
result: dict[str, Any] = {}
|
50
|
+
if attributes:
|
51
|
+
result["@attributes"] = attributes
|
52
|
+
if children:
|
53
|
+
result.update(children)
|
54
|
+
elif text.strip():
|
55
|
+
result = text.strip()
|
56
|
+
|
57
|
+
return {tag: result}
|
58
|
+
|
59
|
+
def _parse_opening_tag(self) -> tuple[str, dict[str, str]]:
|
60
|
+
"""Parse an opening XML tag and its attributes."""
|
61
|
+
match = re.match(
|
62
|
+
r'<(\w+)((?:\s+\w+="[^"]*")*)\s*/?>',
|
63
|
+
self.xml_string[self.index :], # noqa
|
64
|
+
)
|
65
|
+
if not match:
|
66
|
+
raise ValueError("Invalid opening tag")
|
67
|
+
self.index += match.end()
|
68
|
+
tag = match.group(1)
|
69
|
+
attributes = dict(re.findall(r'(\w+)="([^"]*)"', match.group(2)))
|
70
|
+
return tag, attributes
|
71
|
+
|
72
|
+
def _parse_closing_tag(self) -> str:
|
73
|
+
"""Parse a closing XML tag."""
|
74
|
+
match = re.match(r"</(\w+)>", self.xml_string[self.index :]) # noqa
|
75
|
+
if not match:
|
76
|
+
raise ValueError("Invalid closing tag")
|
77
|
+
self.index += match.end()
|
78
|
+
return match.group(1)
|
79
|
+
|
80
|
+
def _parse_text(self) -> str:
|
81
|
+
"""Parse text content between XML tags."""
|
82
|
+
start = self.index
|
83
|
+
while (
|
84
|
+
self.index < len(self.xml_string)
|
85
|
+
and self.xml_string[self.index] != "<"
|
86
|
+
):
|
87
|
+
self.index += 1
|
88
|
+
return self.xml_string[start : self.index] # noqa
|
89
|
+
|
90
|
+
def _skip_whitespace(self) -> None:
|
91
|
+
"""Skip any whitespace characters at the current parsing position."""
|
92
|
+
p_ = len(self.xml_string[self.index :]) # noqa
|
93
|
+
m_ = len(self.xml_string[self.index :].lstrip()) # noqa
|
94
|
+
|
95
|
+
self.index += p_ - m_
|
96
|
+
|
97
|
+
|
98
|
+
def xml_to_dict(
|
99
|
+
xml_string: str,
|
100
|
+
/,
|
101
|
+
suppress=False,
|
102
|
+
remove_root: bool = True,
|
103
|
+
root_tag: str = None,
|
104
|
+
) -> dict[str, Any]:
|
105
|
+
"""
|
106
|
+
Parse an XML string into a nested dictionary structure.
|
107
|
+
|
108
|
+
This function converts an XML string into a dictionary where:
|
109
|
+
- Element tags become dictionary keys
|
110
|
+
- Text content is assigned directly to the tag key if there are no children
|
111
|
+
- Attributes are stored in a '@attributes' key
|
112
|
+
- Multiple child elements with the same tag are stored as lists
|
113
|
+
|
114
|
+
Args:
|
115
|
+
xml_string: The XML string to parse.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
A dictionary representation of the XML structure.
|
119
|
+
|
120
|
+
Raises:
|
121
|
+
ValueError: If the XML is malformed or parsing fails.
|
122
|
+
"""
|
123
|
+
try:
|
124
|
+
a = XMLParser(xml_string).parse()
|
125
|
+
if remove_root and (root_tag or "root") in a:
|
126
|
+
a = a[root_tag or "root"]
|
127
|
+
return a
|
128
|
+
except ValueError as e:
|
129
|
+
if not suppress:
|
130
|
+
raise e
|
131
|
+
|
132
|
+
|
133
|
+
def dict_to_xml(data: dict, /, root_tag: str = "root") -> str:
|
134
|
+
import xml.etree.ElementTree as ET
|
135
|
+
|
136
|
+
root = ET.Element(root_tag)
|
137
|
+
|
138
|
+
def convert(dict_obj: dict, parent: Any) -> None:
|
139
|
+
for key, val in dict_obj.items():
|
140
|
+
if isinstance(val, dict):
|
141
|
+
element = ET.SubElement(parent, key)
|
142
|
+
convert(dict_obj=val, parent=element)
|
143
|
+
else:
|
144
|
+
element = ET.SubElement(parent, key)
|
145
|
+
element.text = str(object=val)
|
146
|
+
|
147
|
+
convert(dict_obj=data, parent=root)
|
148
|
+
return ET.tostring(root, encoding="unicode")
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from inspect import isclass
|
4
|
+
from typing import Any, get_args, get_origin
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
def breakdown_pydantic_annotation(
|
10
|
+
model: type[BaseModel],
|
11
|
+
max_depth: int | None = None,
|
12
|
+
current_depth: int = 0,
|
13
|
+
) -> dict[str, Any]:
|
14
|
+
|
15
|
+
if not _is_pydantic_model(model):
|
16
|
+
raise TypeError("Input must be a Pydantic model")
|
17
|
+
|
18
|
+
if max_depth is not None and current_depth >= max_depth:
|
19
|
+
raise RecursionError("Maximum recursion depth reached")
|
20
|
+
|
21
|
+
out: dict[str, Any] = {}
|
22
|
+
for k, v in model.__annotations__.items():
|
23
|
+
origin = get_origin(v)
|
24
|
+
if _is_pydantic_model(v):
|
25
|
+
out[k] = breakdown_pydantic_annotation(
|
26
|
+
v, max_depth, current_depth + 1
|
27
|
+
)
|
28
|
+
elif origin is list:
|
29
|
+
args = get_args(v)
|
30
|
+
if args and _is_pydantic_model(args[0]):
|
31
|
+
out[k] = [
|
32
|
+
breakdown_pydantic_annotation(
|
33
|
+
args[0], max_depth, current_depth + 1
|
34
|
+
)
|
35
|
+
]
|
36
|
+
else:
|
37
|
+
out[k] = [args[0] if args else Any]
|
38
|
+
else:
|
39
|
+
out[k] = v
|
40
|
+
|
41
|
+
return out
|
42
|
+
|
43
|
+
|
44
|
+
def _is_pydantic_model(x: Any) -> bool:
|
45
|
+
try:
|
46
|
+
return isclass(x) and issubclass(x, BaseModel)
|
47
|
+
except TypeError:
|
48
|
+
return False
|
lionagi/protocols/generic/log.py
CHANGED
@@ -11,7 +11,8 @@ from typing import Any
|
|
11
11
|
|
12
12
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
13
13
|
|
14
|
-
from lionagi.
|
14
|
+
from lionagi.libs.file.create_path import create_path
|
15
|
+
from lionagi.utils import to_dict
|
15
16
|
|
16
17
|
from .._concepts import Manager
|
17
18
|
from .element import Element
|