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.
Files changed (39) hide show
  1. lionagi/adapters/__init__.py +1 -0
  2. lionagi/fields/file.py +1 -1
  3. lionagi/fields/reason.py +1 -1
  4. lionagi/libs/file/concat.py +6 -1
  5. lionagi/libs/file/concat_files.py +5 -1
  6. lionagi/libs/file/create_path.py +80 -0
  7. lionagi/libs/file/file_util.py +358 -0
  8. lionagi/libs/file/save.py +1 -1
  9. lionagi/libs/package/imports.py +177 -8
  10. lionagi/libs/parse/fuzzy_parse_json.py +117 -0
  11. lionagi/libs/parse/to_dict.py +336 -0
  12. lionagi/libs/parse/to_json.py +61 -0
  13. lionagi/libs/parse/to_num.py +378 -0
  14. lionagi/libs/parse/to_xml.py +57 -0
  15. lionagi/libs/parse/xml_parser.py +148 -0
  16. lionagi/libs/schema/breakdown_pydantic_annotation.py +48 -0
  17. lionagi/protocols/generic/log.py +2 -1
  18. lionagi/utils.py +123 -921
  19. lionagi/version.py +1 -1
  20. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/METADATA +8 -11
  21. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/RECORD +24 -30
  22. lionagi/libs/parse.py +0 -30
  23. lionagi/tools/browser/__init__.py +0 -0
  24. lionagi/tools/browser/providers/browser_use_.py +0 -3
  25. lionagi/tools/code/__init__.py +0 -3
  26. lionagi/tools/code/coder.py +0 -3
  27. lionagi/tools/code/manager.py +0 -3
  28. lionagi/tools/code/providers/__init__.py +0 -3
  29. lionagi/tools/code/providers/aider_.py +0 -3
  30. lionagi/tools/code/providers/e2b_.py +0 -3
  31. lionagi/tools/code/sandbox.py +0 -3
  32. lionagi/tools/file/manager.py +0 -3
  33. lionagi/tools/file/providers/__init__.py +0 -3
  34. lionagi/tools/file/providers/docling_.py +0 -3
  35. lionagi/tools/file/writer.py +0 -3
  36. lionagi/tools/query/__init__.py +0 -3
  37. /lionagi/{tools/browser/providers → libs/parse}/__init__.py +0 -0
  38. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/WHEEL +0 -0
  39. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,117 @@
1
+ import contextlib
2
+ import json
3
+ import re
4
+ from typing import Any
5
+
6
+
7
+ def fuzzy_parse_json(
8
+ str_to_parse: str, /
9
+ ) -> dict[str, Any] | list[dict[str, Any]]:
10
+ """
11
+ Attempt to parse a JSON string, trying a few minimal "fuzzy" fixes if needed.
12
+
13
+ Steps:
14
+ 1. Parse directly with json.loads.
15
+ 2. Replace single quotes with double quotes, normalize spacing, and try again.
16
+ 3. Attempt to fix unmatched brackets using fix_json_string.
17
+ 4. If all fail, raise ValueError.
18
+
19
+ Args:
20
+ str_to_parse: The JSON string to parse
21
+
22
+ Returns:
23
+ Parsed JSON (dict or list of dicts)
24
+
25
+ Raises:
26
+ ValueError: If the string cannot be parsed as valid JSON
27
+ TypeError: If the input is not a string
28
+ """
29
+ _check_valid_str(str_to_parse)
30
+
31
+ # 1. Direct attempt
32
+ with contextlib.suppress(Exception):
33
+ return json.loads(str_to_parse)
34
+
35
+ # 2. Try cleaning: replace single quotes with double and normalize
36
+ cleaned = _clean_json_string(str_to_parse.replace("'", '"'))
37
+ with contextlib.suppress(Exception):
38
+ return json.loads(cleaned)
39
+
40
+ # 3. Try fixing brackets
41
+ fixed = fix_json_string(cleaned)
42
+ with contextlib.suppress(Exception):
43
+ return json.loads(fixed)
44
+
45
+ # If all attempts fail
46
+ raise ValueError("Invalid JSON string")
47
+
48
+
49
+ def _check_valid_str(str_to_parse: str, /):
50
+ if not isinstance(str_to_parse, str):
51
+ raise TypeError("Input must be a string")
52
+ if not str_to_parse.strip():
53
+ raise ValueError("Input string is empty")
54
+
55
+
56
+ def _clean_json_string(s: str) -> str:
57
+ """Basic normalization: replace unescaped single quotes, trim spaces, ensure keys are quoted."""
58
+ # Replace unescaped single quotes with double quotes
59
+ # '(?<!\\)'" means a single quote not preceded by a backslash
60
+ s = re.sub(r"(?<!\\)'", '"', s)
61
+ # Collapse multiple whitespaces
62
+ s = re.sub(r"\s+", " ", s)
63
+ # Ensure keys are quoted
64
+ # This attempts to find patterns like { key: value } and turn them into {"key": value}
65
+ s = re.sub(r'([{,])\s*([^"\s]+)\s*:', r'\1"\2":', s)
66
+ return s.strip()
67
+
68
+
69
+ def fix_json_string(str_to_parse: str, /) -> str:
70
+ """Try to fix JSON string by ensuring brackets are matched properly."""
71
+ if not str_to_parse:
72
+ raise ValueError("Input string is empty")
73
+
74
+ brackets = {"{": "}", "[": "]"}
75
+ open_brackets = []
76
+ pos = 0
77
+ length = len(str_to_parse)
78
+
79
+ while pos < length:
80
+ char = str_to_parse[pos]
81
+
82
+ if char == "\\":
83
+ pos += 2 # Skip escaped chars
84
+ continue
85
+
86
+ if char == '"':
87
+ pos += 1
88
+ # skip string content
89
+ while pos < length:
90
+ if str_to_parse[pos] == "\\":
91
+ pos += 2
92
+ continue
93
+ if str_to_parse[pos] == '"':
94
+ pos += 1
95
+ break
96
+ pos += 1
97
+ continue
98
+
99
+ if char in brackets:
100
+ open_brackets.append(brackets[char])
101
+ elif char in brackets.values():
102
+ if not open_brackets:
103
+ # Extra closing bracket
104
+ # Better to raise error than guess
105
+ raise ValueError("Extra closing bracket found.")
106
+ if open_brackets[-1] != char:
107
+ # Mismatched bracket
108
+ raise ValueError("Mismatched brackets.")
109
+ open_brackets.pop()
110
+
111
+ pos += 1
112
+
113
+ # Add missing closing brackets if any
114
+ if open_brackets:
115
+ str_to_parse += "".join(reversed(open_brackets))
116
+
117
+ return str_to_parse
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable, Iterable, Mapping, Sequence
5
+ from enum import Enum
6
+ from functools import partial
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from lionagi.utils import PydanticUndefinedType, UndefinedType
12
+
13
+
14
+ def to_dict(
15
+ input_: Any,
16
+ /,
17
+ *,
18
+ use_model_dump: bool = True,
19
+ fuzzy_parse: bool = False,
20
+ suppress: bool = False,
21
+ str_type: Literal["json", "xml"] | None = "json",
22
+ parser: Callable[[str], Any] | None = None,
23
+ recursive: bool = False,
24
+ max_recursive_depth: int = None,
25
+ recursive_python_only: bool = True,
26
+ use_enum_values: bool = False,
27
+ **kwargs: Any,
28
+ ) -> dict[str, Any]:
29
+ """
30
+ Convert various input types to a dictionary, with optional recursive processing.
31
+
32
+ Args:
33
+ input_: The input to convert.
34
+ use_model_dump: Use model_dump() for Pydantic models if available.
35
+ fuzzy_parse: Use fuzzy parsing for string inputs.
36
+ suppress: Return empty dict on errors if True.
37
+ str_type: Input string type ("json" or "xml").
38
+ parser: Custom parser function for string inputs.
39
+ recursive: Enable recursive conversion of nested structures.
40
+ max_recursive_depth: Maximum recursion depth (default 5, max 10).
41
+ recursive_python_only: If False, attempts to convert custom types recursively.
42
+ use_enum_values: Use enum values instead of names.
43
+ **kwargs: Additional arguments for parsing functions.
44
+
45
+ Returns:
46
+ dict[str, Any]: A dictionary derived from the input.
47
+
48
+ Raises:
49
+ ValueError: If parsing fails and suppress is False.
50
+
51
+ Examples:
52
+ >>> to_dict({"a": 1, "b": [2, 3]})
53
+ {'a': 1, 'b': [2, 3]}
54
+ >>> to_dict('{"x": 10}', str_type="json")
55
+ {'x': 10}
56
+ >>> to_dict({"a": {"b": {"c": 1}}}, recursive=True, max_recursive_depth=2)
57
+ {'a': {'b': {'c': 1}}}
58
+ """
59
+
60
+ try:
61
+ if recursive:
62
+ input_ = recursive_to_dict(
63
+ input_,
64
+ use_model_dump=use_model_dump,
65
+ fuzzy_parse=fuzzy_parse,
66
+ str_type=str_type,
67
+ parser=parser,
68
+ max_recursive_depth=max_recursive_depth,
69
+ recursive_custom_types=not recursive_python_only,
70
+ use_enum_values=use_enum_values,
71
+ **kwargs,
72
+ )
73
+
74
+ return _to_dict(
75
+ input_,
76
+ fuzzy_parse=fuzzy_parse,
77
+ parser=parser,
78
+ str_type=str_type,
79
+ use_model_dump=use_model_dump,
80
+ use_enum_values=use_enum_values,
81
+ **kwargs,
82
+ )
83
+ except Exception as e:
84
+ if suppress or input_ == "":
85
+ return {}
86
+ raise e
87
+
88
+
89
+ def recursive_to_dict(
90
+ input_: Any,
91
+ /,
92
+ *,
93
+ max_recursive_depth: int = None,
94
+ recursive_custom_types: bool = False,
95
+ **kwargs: Any,
96
+ ) -> Any:
97
+
98
+ if not isinstance(max_recursive_depth, int):
99
+ max_recursive_depth = 5
100
+ else:
101
+ if max_recursive_depth < 0:
102
+ raise ValueError(
103
+ "max_recursive_depth must be a non-negative integer"
104
+ )
105
+ if max_recursive_depth == 0:
106
+ return input_
107
+ if max_recursive_depth > 10:
108
+ raise ValueError(
109
+ "max_recursive_depth must be less than or equal to 10"
110
+ )
111
+
112
+ return _recur_to_dict(
113
+ input_,
114
+ max_recursive_depth=max_recursive_depth,
115
+ current_depth=0,
116
+ recursive_custom_types=recursive_custom_types,
117
+ **kwargs,
118
+ )
119
+
120
+
121
+ def _recur_to_dict(
122
+ input_: Any,
123
+ /,
124
+ *,
125
+ max_recursive_depth: int,
126
+ current_depth: int = 0,
127
+ recursive_custom_types: bool = False,
128
+ **kwargs: Any,
129
+ ) -> Any:
130
+
131
+ if current_depth >= max_recursive_depth:
132
+ return input_
133
+
134
+ if isinstance(input_, str):
135
+ try:
136
+ # Attempt to parse the string
137
+ parsed = _to_dict(input_, **kwargs)
138
+ # Recursively process the parsed result
139
+ return _recur_to_dict(
140
+ parsed,
141
+ max_recursive_depth=max_recursive_depth,
142
+ current_depth=current_depth + 1,
143
+ recursive_custom_types=recursive_custom_types,
144
+ **kwargs,
145
+ )
146
+ except Exception:
147
+ # Return the original string if parsing fails
148
+ return input_
149
+
150
+ elif isinstance(input_, dict):
151
+ # Recursively process dictionary values
152
+ return {
153
+ key: _recur_to_dict(
154
+ value,
155
+ max_recursive_depth=max_recursive_depth,
156
+ current_depth=current_depth + 1,
157
+ recursive_custom_types=recursive_custom_types,
158
+ **kwargs,
159
+ )
160
+ for key, value in input_.items()
161
+ }
162
+
163
+ elif isinstance(input_, (list, tuple, set)):
164
+ # Recursively process list or tuple elements
165
+ processed = [
166
+ _recur_to_dict(
167
+ element,
168
+ max_recursive_depth=max_recursive_depth,
169
+ current_depth=current_depth + 1,
170
+ recursive_custom_types=recursive_custom_types,
171
+ **kwargs,
172
+ )
173
+ for element in input_
174
+ ]
175
+ return type(input_)(processed)
176
+
177
+ elif isinstance(input_, type) and issubclass(input_, Enum):
178
+ try:
179
+ obj_dict = _to_dict(input_, **kwargs)
180
+ return _recur_to_dict(
181
+ obj_dict,
182
+ max_recursive_depth=max_recursive_depth,
183
+ current_depth=current_depth + 1,
184
+ **kwargs,
185
+ )
186
+ except Exception:
187
+ return input_
188
+
189
+ elif recursive_custom_types:
190
+ # Process custom classes if enabled
191
+ try:
192
+ obj_dict = _to_dict(input_, **kwargs)
193
+ return _recur_to_dict(
194
+ obj_dict,
195
+ max_recursive_depth=max_recursive_depth,
196
+ current_depth=current_depth + 1,
197
+ recursive_custom_types=recursive_custom_types,
198
+ **kwargs,
199
+ )
200
+ except Exception:
201
+ return input_
202
+
203
+ else:
204
+ # Return the input as is for other data types
205
+ return input_
206
+
207
+
208
+ def _enum_to_dict(input_, /, use_enum_values: bool = True):
209
+ dict_ = dict(input_.__members__).copy()
210
+ if use_enum_values:
211
+ return {key: value.value for key, value in dict_.items()}
212
+ return dict_
213
+
214
+
215
+ def _str_to_dict(
216
+ input_: str,
217
+ /,
218
+ fuzzy_parse: bool = False,
219
+ str_type: Literal["json", "xml"] | None = "json",
220
+ parser: Callable[[str], Any] | None = None,
221
+ remove_root: bool = False,
222
+ root_tag: str = "root",
223
+ **kwargs: Any,
224
+ ):
225
+ """
226
+ kwargs for parser
227
+ """
228
+ if not parser:
229
+ if str_type == "xml" and not parser:
230
+ from lionagi.libs.parse.xml_parser import xml_to_dict
231
+
232
+ parser = partial(
233
+ xml_to_dict, remove_root=remove_root, root_tag=root_tag
234
+ )
235
+
236
+ elif fuzzy_parse:
237
+
238
+ from lionagi.libs.parse.fuzzy_parse_json import fuzzy_parse_json
239
+
240
+ parser = fuzzy_parse_json
241
+ else:
242
+ parser = json.loads
243
+
244
+ return parser(input_, **kwargs)
245
+
246
+
247
+ def _na_to_dict(input_: type[None] | UndefinedType | PydanticUndefinedType, /):
248
+ return {}
249
+
250
+
251
+ def _model_to_dict(input_: Any, /, use_model_dump=True, **kwargs):
252
+ """
253
+ kwargs: built-in serialization methods kwargs
254
+ accepted built-in serialization methods:
255
+ - mdoel_dump
256
+ - to_dict
257
+ - to_json
258
+ - dict
259
+ - json
260
+ """
261
+
262
+ if use_model_dump and hasattr(input_, "model_dump"):
263
+ return input_.model_dump(**kwargs)
264
+
265
+ methods = (
266
+ "to_dict",
267
+ "to_json",
268
+ "json",
269
+ "dict",
270
+ )
271
+ for method in methods:
272
+ if hasattr(input_, method):
273
+ result = getattr(input_, method)(**kwargs)
274
+ return json.loads(result) if isinstance(result, str) else result
275
+
276
+ if hasattr(input_, "__dict__"):
277
+ return input_.__dict__
278
+
279
+ try:
280
+ return dict(input_)
281
+ except Exception as e:
282
+ raise ValueError(f"Unable to convert input to dictionary: {e}")
283
+
284
+
285
+ def _set_to_dict(input_: set, /) -> dict:
286
+ return {v: v for v in input_}
287
+
288
+
289
+ def _iterable_to_dict(input_: Iterable, /) -> dict:
290
+ return {idx: v for idx, v in enumerate(input_)}
291
+
292
+
293
+ def _to_dict(
294
+ input_: Any,
295
+ /,
296
+ *,
297
+ fuzzy_parse: bool = False,
298
+ str_type: Literal["json", "xml"] | None = "json",
299
+ parser: Callable[[str], Any] | None = None,
300
+ remove_root: bool = False,
301
+ root_tag: str = "root",
302
+ use_model_dump: bool = True,
303
+ use_enum_values: bool = True,
304
+ **kwargs: Any,
305
+ ) -> dict[str, Any]:
306
+
307
+ if isinstance(input_, set):
308
+ return _set_to_dict(input_)
309
+
310
+ if isinstance(input_, type) and issubclass(input_, Enum):
311
+ return _enum_to_dict(input_, use_enum_values=use_enum_values)
312
+
313
+ if isinstance(input_, Mapping):
314
+ return dict(input_)
315
+
316
+ if isinstance(input_, type(None) | UndefinedType | PydanticUndefinedType):
317
+ return _na_to_dict(input_)
318
+
319
+ if isinstance(input_, str):
320
+ return _str_to_dict(
321
+ input_,
322
+ fuzzy_parse=fuzzy_parse,
323
+ str_type=str_type,
324
+ parser=parser,
325
+ remove_root=remove_root,
326
+ root_tag=root_tag,
327
+ **kwargs,
328
+ )
329
+
330
+ if isinstance(input_, BaseModel) or not isinstance(input_, Sequence):
331
+ return _model_to_dict(input_, use_model_dump=use_model_dump, **kwargs)
332
+
333
+ if isinstance(input_, Iterable):
334
+ return _iterable_to_dict(input_)
335
+
336
+ return dict(input_)
@@ -0,0 +1,61 @@
1
+ import json
2
+ import re
3
+ from typing import Any
4
+
5
+ from .fuzzy_parse_json import fuzzy_parse_json
6
+
7
+ # Precompile the regex for extracting JSON code blocks
8
+ _JSON_BLOCK_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
9
+
10
+
11
+ def to_json(
12
+ input_data: str | list[str], /, *, fuzzy_parse: bool = False
13
+ ) -> dict[str, Any] | list[dict[str, Any]]:
14
+ """
15
+ Extract and parse JSON content from a string or markdown code blocks.
16
+
17
+ Attempts direct JSON parsing first. If that fails, looks for JSON content
18
+ within markdown code blocks denoted by ```json.
19
+
20
+ Args:
21
+ input_data (str | list[str]): The input string or list of strings to parse.
22
+ fuzzy_parse (bool): If True, attempts fuzzy JSON parsing on failed attempts.
23
+
24
+ Returns:
25
+ dict or list of dicts:
26
+ - If a single JSON object is found: returns a dict.
27
+ - If multiple JSON objects are found: returns a list of dicts.
28
+ - If no valid JSON found: returns an empty list.
29
+ """
30
+
31
+ # If input_data is a list, join into a single string
32
+ if isinstance(input_data, list):
33
+ input_str = "\n".join(input_data)
34
+ else:
35
+ input_str = input_data
36
+
37
+ # 1. Try direct parsing
38
+ try:
39
+ if fuzzy_parse:
40
+ return fuzzy_parse_json(input_str)
41
+ return json.loads(input_str)
42
+ except Exception:
43
+ pass
44
+
45
+ # 2. Attempt extracting JSON blocks from markdown
46
+ matches = _JSON_BLOCK_PATTERN.findall(input_str)
47
+ if not matches:
48
+ return []
49
+
50
+ # If only one match, return single dict; if multiple, return list of dicts
51
+ if len(matches) == 1:
52
+ data_str = matches[0]
53
+ return (
54
+ fuzzy_parse_json(data_str) if fuzzy_parse else json.loads(data_str)
55
+ )
56
+
57
+ # Multiple matches
58
+ if fuzzy_parse:
59
+ return [fuzzy_parse_json(m) for m in matches]
60
+ else:
61
+ return [json.loads(m) for m in matches]