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
lionagi/utils.py CHANGED
@@ -3,20 +3,13 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import asyncio
6
- import contextlib
7
6
  import copy as _copy
8
7
  import functools
9
- import importlib.metadata
10
8
  import importlib.util
11
9
  import json
12
10
  import logging
13
- import re
14
- import shutil
15
11
  import subprocess
16
- import sys
17
12
  import time as t_
18
- import uuid
19
- import xml.etree.ElementTree as ET
20
13
  from abc import ABC
21
14
  from collections.abc import (
22
15
  AsyncGenerator,
@@ -27,20 +20,10 @@ from collections.abc import (
27
20
  )
28
21
  from concurrent.futures import ThreadPoolExecutor
29
22
  from datetime import datetime, timezone
30
- from decimal import Decimal
31
23
  from enum import Enum
32
- from functools import lru_cache, partial
33
- from inspect import isclass
24
+ from functools import lru_cache
34
25
  from pathlib import Path
35
- from typing import (
36
- Any,
37
- Literal,
38
- TypedDict,
39
- TypeVar,
40
- get_args,
41
- get_origin,
42
- overload,
43
- )
26
+ from typing import Any, Literal, TypedDict, TypeVar, overload
44
27
 
45
28
  from pydantic import BaseModel, model_validator
46
29
  from pydantic_core import PydanticUndefinedType
@@ -136,7 +119,7 @@ def hash_dict(data) -> int:
136
119
  class Params(BaseModel):
137
120
 
138
121
  def keys(self):
139
- return self.model_fields.keys()
122
+ return self.__class__.model_fields.keys()
140
123
 
141
124
  def __call__(self, *args, **kwargs):
142
125
  raise NotImplementedError(
@@ -1005,45 +988,19 @@ def create_path(
1005
988
  ValueError: If filename is invalid.
1006
989
  FileExistsError: If file exists and file_exist_ok=False.
1007
990
  """
1008
- if "/" in filename:
1009
- sub_dir, filename = filename.split("/")[:-1], filename.split("/")[-1]
1010
- directory = Path(directory) / "/".join(sub_dir)
1011
-
1012
- if "\\" in filename:
1013
- raise ValueError("Filename cannot contain directory separators.")
1014
-
1015
- directory = Path(directory)
1016
-
1017
- # Extract name and extension from filename if present
1018
- if "." in filename:
1019
- name, ext = filename.rsplit(".", 1)
1020
- else:
1021
- name, ext = filename, extension
1022
-
1023
- # Ensure extension has a single leading dot
1024
- ext = f".{ext.lstrip('.')}" if ext else ""
1025
-
1026
- # Add timestamp if requested
1027
- if timestamp:
1028
- ts_str = datetime.now().strftime(timestamp_format or "%Y%m%d%H%M%S")
1029
- name = f"{ts_str}_{name}" if time_prefix else f"{name}_{ts_str}"
1030
-
1031
- # Add random suffix if requested
1032
- if random_hash_digits > 0:
1033
- # Use UUID4 and truncate its hex for random suffix
1034
- random_suffix = uuid.uuid4().hex[:random_hash_digits]
1035
- name = f"{name}-{random_suffix}"
1036
-
1037
- full_path = directory / f"{name}{ext}"
1038
-
1039
- # Check if file or directory existence
1040
- full_path.parent.mkdir(parents=True, exist_ok=dir_exist_ok)
1041
- if full_path.exists() and not file_exist_ok:
1042
- raise FileExistsError(
1043
- f"File {full_path} already exists and file_exist_ok is False."
1044
- )
1045
-
1046
- return full_path
991
+ from lionagi.libs.file.create_path import create_path
992
+
993
+ return create_path(
994
+ directory,
995
+ filename,
996
+ extension=extension,
997
+ timestamp=timestamp,
998
+ dir_exist_ok=dir_exist_ok,
999
+ file_exist_ok=file_exist_ok,
1000
+ time_prefix=time_prefix,
1001
+ timestamp_format=timestamp_format,
1002
+ random_hash_digits=random_hash_digits,
1003
+ )
1047
1004
 
1048
1005
 
1049
1006
  class CreatePathParams(Params):
@@ -1102,34 +1059,9 @@ def to_xml(
1102
1059
  >>> to_xml({"a": 1, "b": {"c": "hello", "d": [10, 20]}}, root_name="data")
1103
1060
  '<data><a>1</a><b><c>hello</c><d>10</d><d>20</d></b></data>'
1104
1061
  """
1062
+ from lionagi.libs.parse.to_xml import to_xml
1105
1063
 
1106
- def _convert(value: Any, tag_name: str) -> str:
1107
- # If value is a dict, recursively convert its keys
1108
- if isinstance(value, dict):
1109
- inner = "".join(_convert(v, k) for k, v in value.items())
1110
- return f"<{tag_name}>{inner}</{tag_name}>"
1111
- # If value is a list, repeat the same tag for each element
1112
- elif isinstance(value, list):
1113
- return "".join(_convert(item, tag_name) for item in value)
1114
- # If value is a primitive, convert to string and place inside tag
1115
- else:
1116
- text = "" if value is None else str(value)
1117
- # Escape special XML characters if needed (minimal)
1118
- text = (
1119
- text.replace("&", "&amp;")
1120
- .replace("<", "&lt;")
1121
- .replace(">", "&gt;")
1122
- .replace('"', "&quot;")
1123
- .replace("'", "&apos;")
1124
- )
1125
- return f"<{tag_name}>{text}</{tag_name}>"
1126
-
1127
- # If top-level obj is not a dict, wrap it in one
1128
- if not isinstance(obj, dict):
1129
- obj = {root_name: obj}
1130
-
1131
- inner_xml = "".join(_convert(v, k) for k, v in obj.items())
1132
- return f"<{root_name}>{inner_xml}</{root_name}>"
1064
+ return to_xml(obj, root_name=root_name)
1133
1065
 
1134
1066
 
1135
1067
  def fuzzy_parse_json(
@@ -1154,186 +1086,9 @@ def fuzzy_parse_json(
1154
1086
  ValueError: If the string cannot be parsed as valid JSON
1155
1087
  TypeError: If the input is not a string
1156
1088
  """
1157
- _check_valid_str(str_to_parse)
1158
-
1159
- # 1. Direct attempt
1160
- with contextlib.suppress(Exception):
1161
- return json.loads(str_to_parse)
1162
-
1163
- # 2. Try cleaning: replace single quotes with double and normalize
1164
- cleaned = _clean_json_string(str_to_parse.replace("'", '"'))
1165
- with contextlib.suppress(Exception):
1166
- return json.loads(cleaned)
1167
-
1168
- # 3. Try fixing brackets
1169
- fixed = fix_json_string(cleaned)
1170
- with contextlib.suppress(Exception):
1171
- return json.loads(fixed)
1172
-
1173
- # If all attempts fail
1174
- raise ValueError("Invalid JSON string")
1175
-
1176
-
1177
- def _check_valid_str(str_to_parse: str, /):
1178
- if not isinstance(str_to_parse, str):
1179
- raise TypeError("Input must be a string")
1180
- if not str_to_parse.strip():
1181
- raise ValueError("Input string is empty")
1182
-
1183
-
1184
- def _clean_json_string(s: str) -> str:
1185
- """Basic normalization: replace unescaped single quotes, trim spaces, ensure keys are quoted."""
1186
- # Replace unescaped single quotes with double quotes
1187
- # '(?<!\\)'" means a single quote not preceded by a backslash
1188
- s = re.sub(r"(?<!\\)'", '"', s)
1189
- # Collapse multiple whitespaces
1190
- s = re.sub(r"\s+", " ", s)
1191
- # Ensure keys are quoted
1192
- # This attempts to find patterns like { key: value } and turn them into {"key": value}
1193
- s = re.sub(r'([{,])\s*([^"\s]+)\s*:', r'\1"\2":', s)
1194
- return s.strip()
1195
-
1196
-
1197
- def fix_json_string(str_to_parse: str, /) -> str:
1198
- """Try to fix JSON string by ensuring brackets are matched properly."""
1199
- if not str_to_parse:
1200
- raise ValueError("Input string is empty")
1201
-
1202
- brackets = {"{": "}", "[": "]"}
1203
- open_brackets = []
1204
- pos = 0
1205
- length = len(str_to_parse)
1206
-
1207
- while pos < length:
1208
- char = str_to_parse[pos]
1209
-
1210
- if char == "\\":
1211
- pos += 2 # Skip escaped chars
1212
- continue
1213
-
1214
- if char == '"':
1215
- pos += 1
1216
- # skip string content
1217
- while pos < length:
1218
- if str_to_parse[pos] == "\\":
1219
- pos += 2
1220
- continue
1221
- if str_to_parse[pos] == '"':
1222
- pos += 1
1223
- break
1224
- pos += 1
1225
- continue
1226
-
1227
- if char in brackets:
1228
- open_brackets.append(brackets[char])
1229
- elif char in brackets.values():
1230
- if not open_brackets:
1231
- # Extra closing bracket
1232
- # Better to raise error than guess
1233
- raise ValueError("Extra closing bracket found.")
1234
- if open_brackets[-1] != char:
1235
- # Mismatched bracket
1236
- raise ValueError("Mismatched brackets.")
1237
- open_brackets.pop()
1238
-
1239
- pos += 1
1240
-
1241
- # Add missing closing brackets if any
1242
- if open_brackets:
1243
- str_to_parse += "".join(reversed(open_brackets))
1244
-
1245
- return str_to_parse
1246
-
1247
-
1248
- class XMLParser:
1249
- def __init__(self, xml_string: str):
1250
- self.xml_string = xml_string.strip()
1251
- self.index = 0
1252
-
1253
- def parse(self) -> dict[str, Any]:
1254
- """Parse the XML string and return the root element as a dictionary."""
1255
- return self._parse_element()
1256
-
1257
- def _parse_element(self) -> dict[str, Any]:
1258
- """Parse a single XML element and its children."""
1259
- self._skip_whitespace()
1260
- if self.xml_string[self.index] != "<":
1261
- raise ValueError(
1262
- f"Expected '<', found '{self.xml_string[self.index]}'"
1263
- )
1089
+ from lionagi.libs.parse.fuzzy_parse_json import fuzzy_parse_json
1264
1090
 
1265
- tag, attributes = self._parse_opening_tag()
1266
- children: dict[str, str | list | dict] = {}
1267
- text = ""
1268
-
1269
- while self.index < len(self.xml_string):
1270
- self._skip_whitespace()
1271
- if self.xml_string.startswith("</", self.index):
1272
- closing_tag = self._parse_closing_tag()
1273
- if closing_tag != tag:
1274
- raise ValueError(
1275
- f"Mismatched tags: '{tag}' and '{closing_tag}'"
1276
- )
1277
- break
1278
- elif self.xml_string.startswith("<", self.index):
1279
- child = self._parse_element()
1280
- child_tag, child_data = next(iter(child.items()))
1281
- if child_tag in children:
1282
- if not isinstance(children[child_tag], list):
1283
- children[child_tag] = [children[child_tag]]
1284
- children[child_tag].append(child_data)
1285
- else:
1286
- children[child_tag] = child_data
1287
- else:
1288
- text += self._parse_text()
1289
-
1290
- result: dict[str, Any] = {}
1291
- if attributes:
1292
- result["@attributes"] = attributes
1293
- if children:
1294
- result.update(children)
1295
- elif text.strip():
1296
- result = text.strip()
1297
-
1298
- return {tag: result}
1299
-
1300
- def _parse_opening_tag(self) -> tuple[str, dict[str, str]]:
1301
- """Parse an opening XML tag and its attributes."""
1302
- match = re.match(
1303
- r'<(\w+)((?:\s+\w+="[^"]*")*)\s*/?>',
1304
- self.xml_string[self.index :], # noqa
1305
- )
1306
- if not match:
1307
- raise ValueError("Invalid opening tag")
1308
- self.index += match.end()
1309
- tag = match.group(1)
1310
- attributes = dict(re.findall(r'(\w+)="([^"]*)"', match.group(2)))
1311
- return tag, attributes
1312
-
1313
- def _parse_closing_tag(self) -> str:
1314
- """Parse a closing XML tag."""
1315
- match = re.match(r"</(\w+)>", self.xml_string[self.index :]) # noqa
1316
- if not match:
1317
- raise ValueError("Invalid closing tag")
1318
- self.index += match.end()
1319
- return match.group(1)
1320
-
1321
- def _parse_text(self) -> str:
1322
- """Parse text content between XML tags."""
1323
- start = self.index
1324
- while (
1325
- self.index < len(self.xml_string)
1326
- and self.xml_string[self.index] != "<"
1327
- ):
1328
- self.index += 1
1329
- return self.xml_string[start : self.index] # noqa
1330
-
1331
- def _skip_whitespace(self) -> None:
1332
- """Skip any whitespace characters at the current parsing position."""
1333
- p_ = len(self.xml_string[self.index :]) # noqa
1334
- m_ = len(self.xml_string[self.index :].lstrip()) # noqa
1335
-
1336
- self.index += p_ - m_
1091
+ return fuzzy_parse_json(str_to_parse)
1337
1092
 
1338
1093
 
1339
1094
  def xml_to_dict(
@@ -1361,31 +1116,20 @@ def xml_to_dict(
1361
1116
  Raises:
1362
1117
  ValueError: If the XML is malformed or parsing fails.
1363
1118
  """
1364
- try:
1365
- a = XMLParser(xml_string).parse()
1366
- if remove_root and (root_tag or "root") in a:
1367
- a = a[root_tag or "root"]
1368
- return a
1369
- except ValueError as e:
1370
- if not suppress:
1371
- raise e
1372
-
1119
+ from lionagi.libs.parse.xml_parser import xml_to_dict
1373
1120
 
1374
- def dict_to_xml(data: dict, /, root_tag: str = "root") -> str:
1121
+ return xml_to_dict(
1122
+ xml_string,
1123
+ suppress=suppress,
1124
+ remove_root=remove_root,
1125
+ root_tag=root_tag,
1126
+ )
1375
1127
 
1376
- root = ET.Element(root_tag)
1377
1128
 
1378
- def convert(dict_obj: dict, parent: Any) -> None:
1379
- for key, val in dict_obj.items():
1380
- if isinstance(val, dict):
1381
- element = ET.SubElement(parent, key)
1382
- convert(dict_obj=val, parent=element)
1383
- else:
1384
- element = ET.SubElement(parent, key)
1385
- element.text = str(object=val)
1129
+ def dict_to_xml(data: dict, /, root_tag: str = "root") -> str:
1130
+ from lionagi.libs.parse.xml_parser import dict_to_xml
1386
1131
 
1387
- convert(dict_obj=data, parent=root)
1388
- return ET.tostring(root, encoding="unicode")
1132
+ return dict_to_xml(data, root_tag=root_tag)
1389
1133
 
1390
1134
 
1391
1135
  def to_dict(
@@ -1433,285 +1177,23 @@ def to_dict(
1433
1177
  >>> to_dict({"a": {"b": {"c": 1}}}, recursive=True, max_recursive_depth=2)
1434
1178
  {'a': {'b': {'c': 1}}}
1435
1179
  """
1180
+ from lionagi.libs.parse.to_dict import to_dict
1436
1181
 
1437
- try:
1438
- if recursive:
1439
- input_ = recursive_to_dict(
1440
- input_,
1441
- use_model_dump=use_model_dump,
1442
- fuzzy_parse=fuzzy_parse,
1443
- str_type=str_type,
1444
- parser=parser,
1445
- max_recursive_depth=max_recursive_depth,
1446
- recursive_custom_types=not recursive_python_only,
1447
- use_enum_values=use_enum_values,
1448
- **kwargs,
1449
- )
1450
-
1451
- return _to_dict(
1452
- input_,
1453
- fuzzy_parse=fuzzy_parse,
1454
- parser=parser,
1455
- str_type=str_type,
1456
- use_model_dump=use_model_dump,
1457
- use_enum_values=use_enum_values,
1458
- **kwargs,
1459
- )
1460
- except Exception as e:
1461
- if suppress or input_ == "":
1462
- return {}
1463
- raise e
1464
-
1465
-
1466
- def recursive_to_dict(
1467
- input_: Any,
1468
- /,
1469
- *,
1470
- max_recursive_depth: int = None,
1471
- recursive_custom_types: bool = False,
1472
- **kwargs: Any,
1473
- ) -> Any:
1474
-
1475
- if not isinstance(max_recursive_depth, int):
1476
- max_recursive_depth = 5
1477
- else:
1478
- if max_recursive_depth < 0:
1479
- raise ValueError(
1480
- "max_recursive_depth must be a non-negative integer"
1481
- )
1482
- if max_recursive_depth == 0:
1483
- return input_
1484
- if max_recursive_depth > 10:
1485
- raise ValueError(
1486
- "max_recursive_depth must be less than or equal to 10"
1487
- )
1488
-
1489
- return _recur_to_dict(
1182
+ return to_dict(
1490
1183
  input_,
1184
+ use_model_dump=use_model_dump,
1185
+ fuzzy_parse=fuzzy_parse,
1186
+ suppress=suppress,
1187
+ str_type=str_type,
1188
+ parser=parser,
1189
+ recursive=recursive,
1491
1190
  max_recursive_depth=max_recursive_depth,
1492
- current_depth=0,
1493
- recursive_custom_types=recursive_custom_types,
1191
+ recursive_python_only=recursive_python_only,
1192
+ use_enum_values=use_enum_values,
1494
1193
  **kwargs,
1495
1194
  )
1496
1195
 
1497
1196
 
1498
- def _recur_to_dict(
1499
- input_: Any,
1500
- /,
1501
- *,
1502
- max_recursive_depth: int,
1503
- current_depth: int = 0,
1504
- recursive_custom_types: bool = False,
1505
- **kwargs: Any,
1506
- ) -> Any:
1507
-
1508
- if current_depth >= max_recursive_depth:
1509
- return input_
1510
-
1511
- if isinstance(input_, str):
1512
- try:
1513
- # Attempt to parse the string
1514
- parsed = _to_dict(input_, **kwargs)
1515
- # Recursively process the parsed result
1516
- return _recur_to_dict(
1517
- parsed,
1518
- max_recursive_depth=max_recursive_depth,
1519
- current_depth=current_depth + 1,
1520
- recursive_custom_types=recursive_custom_types,
1521
- **kwargs,
1522
- )
1523
- except Exception:
1524
- # Return the original string if parsing fails
1525
- return input_
1526
-
1527
- elif isinstance(input_, dict):
1528
- # Recursively process dictionary values
1529
- return {
1530
- key: _recur_to_dict(
1531
- value,
1532
- max_recursive_depth=max_recursive_depth,
1533
- current_depth=current_depth + 1,
1534
- recursive_custom_types=recursive_custom_types,
1535
- **kwargs,
1536
- )
1537
- for key, value in input_.items()
1538
- }
1539
-
1540
- elif isinstance(input_, (list, tuple, set)):
1541
- # Recursively process list or tuple elements
1542
- processed = [
1543
- _recur_to_dict(
1544
- element,
1545
- max_recursive_depth=max_recursive_depth,
1546
- current_depth=current_depth + 1,
1547
- recursive_custom_types=recursive_custom_types,
1548
- **kwargs,
1549
- )
1550
- for element in input_
1551
- ]
1552
- return type(input_)(processed)
1553
-
1554
- elif isinstance(input_, type) and issubclass(input_, Enum):
1555
- try:
1556
- obj_dict = _to_dict(input_, **kwargs)
1557
- return _recur_to_dict(
1558
- obj_dict,
1559
- max_recursive_depth=max_recursive_depth,
1560
- current_depth=current_depth + 1,
1561
- **kwargs,
1562
- )
1563
- except Exception:
1564
- return input_
1565
-
1566
- elif recursive_custom_types:
1567
- # Process custom classes if enabled
1568
- try:
1569
- obj_dict = _to_dict(input_, **kwargs)
1570
- return _recur_to_dict(
1571
- obj_dict,
1572
- max_recursive_depth=max_recursive_depth,
1573
- current_depth=current_depth + 1,
1574
- recursive_custom_types=recursive_custom_types,
1575
- **kwargs,
1576
- )
1577
- except Exception:
1578
- return input_
1579
-
1580
- else:
1581
- # Return the input as is for other data types
1582
- return input_
1583
-
1584
-
1585
- def _enum_to_dict(input_, /, use_enum_values: bool = True):
1586
- dict_ = dict(input_.__members__).copy()
1587
- if use_enum_values:
1588
- return {key: value.value for key, value in dict_.items()}
1589
- return dict_
1590
-
1591
-
1592
- def _str_to_dict(
1593
- input_: str,
1594
- /,
1595
- fuzzy_parse: bool = False,
1596
- str_type: Literal["json", "xml"] | None = "json",
1597
- parser: Callable[[str], Any] | None = None,
1598
- remove_root: bool = False,
1599
- root_tag: str = "root",
1600
- **kwargs: Any,
1601
- ):
1602
- """
1603
- kwargs for parser
1604
- """
1605
- if not parser:
1606
- if str_type == "xml" and not parser:
1607
- parser = partial(
1608
- xml_to_dict, remove_root=remove_root, root_tag=root_tag
1609
- )
1610
-
1611
- elif fuzzy_parse:
1612
- parser = fuzzy_parse_json
1613
- else:
1614
- parser = json.loads
1615
-
1616
- return parser(input_, **kwargs)
1617
-
1618
-
1619
- def _na_to_dict(input_: type[None] | UndefinedType | PydanticUndefinedType, /):
1620
- return {}
1621
-
1622
-
1623
- def _model_to_dict(input_: Any, /, use_model_dump=True, **kwargs):
1624
- """
1625
- kwargs: built-in serialization methods kwargs
1626
- accepted built-in serialization methods:
1627
- - mdoel_dump
1628
- - to_dict
1629
- - to_json
1630
- - dict
1631
- - json
1632
- """
1633
-
1634
- if use_model_dump and hasattr(input_, "model_dump"):
1635
- return input_.model_dump(**kwargs)
1636
-
1637
- methods = (
1638
- "to_dict",
1639
- "to_json",
1640
- "json",
1641
- "dict",
1642
- )
1643
- for method in methods:
1644
- if hasattr(input_, method):
1645
- result = getattr(input_, method)(**kwargs)
1646
- return json.loads(result) if isinstance(result, str) else result
1647
-
1648
- if hasattr(input_, "__dict__"):
1649
- return input_.__dict__
1650
-
1651
- try:
1652
- return dict(input_)
1653
- except Exception as e:
1654
- raise ValueError(f"Unable to convert input to dictionary: {e}")
1655
-
1656
-
1657
- def _set_to_dict(input_: set, /) -> dict:
1658
- return {v: v for v in input_}
1659
-
1660
-
1661
- def _iterable_to_dict(input_: Iterable, /) -> dict:
1662
- return {idx: v for idx, v in enumerate(input_)}
1663
-
1664
-
1665
- def _to_dict(
1666
- input_: Any,
1667
- /,
1668
- *,
1669
- fuzzy_parse: bool = False,
1670
- str_type: Literal["json", "xml"] | None = "json",
1671
- parser: Callable[[str], Any] | None = None,
1672
- remove_root: bool = False,
1673
- root_tag: str = "root",
1674
- use_model_dump: bool = True,
1675
- use_enum_values: bool = True,
1676
- **kwargs: Any,
1677
- ) -> dict[str, Any]:
1678
-
1679
- if isinstance(input_, set):
1680
- return _set_to_dict(input_)
1681
-
1682
- if isinstance(input_, type) and issubclass(input_, Enum):
1683
- return _enum_to_dict(input_, use_enum_values=use_enum_values)
1684
-
1685
- if isinstance(input_, Mapping):
1686
- return dict(input_)
1687
-
1688
- if isinstance(input_, type(None) | UndefinedType | PydanticUndefinedType):
1689
- return _na_to_dict(input_)
1690
-
1691
- if isinstance(input_, str):
1692
- return _str_to_dict(
1693
- input_,
1694
- fuzzy_parse=fuzzy_parse,
1695
- str_type=str_type,
1696
- parser=parser,
1697
- remove_root=remove_root,
1698
- root_tag=root_tag,
1699
- **kwargs,
1700
- )
1701
-
1702
- if isinstance(input_, BaseModel) or not isinstance(input_, Sequence):
1703
- return _model_to_dict(input_, use_model_dump=use_model_dump, **kwargs)
1704
-
1705
- if isinstance(input_, Iterable):
1706
- return _iterable_to_dict(input_)
1707
-
1708
- return dict(input_)
1709
-
1710
-
1711
- # Precompile the regex for extracting JSON code blocks
1712
- _JSON_BLOCK_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
1713
-
1714
-
1715
1197
  def to_json(
1716
1198
  input_data: str | list[str], /, *, fuzzy_parse: bool = False
1717
1199
  ) -> dict[str, Any] | list[dict[str, Any]]:
@@ -1731,38 +1213,12 @@ def to_json(
1731
1213
  - If multiple JSON objects are found: returns a list of dicts.
1732
1214
  - If no valid JSON found: returns an empty list.
1733
1215
  """
1216
+ from lionagi.libs.parse.to_json import to_json
1734
1217
 
1735
- # If input_data is a list, join into a single string
1736
- if isinstance(input_data, list):
1737
- input_str = "\n".join(input_data)
1738
- else:
1739
- input_str = input_data
1740
-
1741
- # 1. Try direct parsing
1742
- try:
1743
- if fuzzy_parse:
1744
- return fuzzy_parse_json(input_str)
1745
- return json.loads(input_str)
1746
- except Exception:
1747
- pass
1748
-
1749
- # 2. Attempt extracting JSON blocks from markdown
1750
- matches = _JSON_BLOCK_PATTERN.findall(input_str)
1751
- if not matches:
1752
- return []
1753
-
1754
- # If only one match, return single dict; if multiple, return list of dicts
1755
- if len(matches) == 1:
1756
- data_str = matches[0]
1757
- return (
1758
- fuzzy_parse_json(data_str) if fuzzy_parse else json.loads(data_str)
1759
- )
1760
-
1761
- # Multiple matches
1762
- if fuzzy_parse:
1763
- return [fuzzy_parse_json(m) for m in matches]
1764
- else:
1765
- return [json.loads(m) for m in matches]
1218
+ return to_json(
1219
+ input_data,
1220
+ fuzzy_parse=fuzzy_parse,
1221
+ )
1766
1222
 
1767
1223
 
1768
1224
  def get_bins(input_: list[str], upper: int) -> list[list[int]]:
@@ -1978,71 +1434,17 @@ def to_num(
1978
1434
  ValueError: For invalid input or out of bounds values.
1979
1435
  TypeError: For invalid input types or invalid type conversions.
1980
1436
  """
1981
- # Validate input
1982
- if isinstance(input_, (list, tuple)):
1983
- raise TypeError("Input cannot be a sequence")
1984
-
1985
- # Handle boolean input
1986
- if isinstance(input_, bool):
1987
- return validate_num_type(num_type)(input_)
1988
-
1989
- # Handle direct numeric input
1990
- if isinstance(input_, (int, float, complex, Decimal)):
1991
- inferred_type = type(input_)
1992
- if isinstance(input_, Decimal):
1993
- inferred_type = float
1994
- value = float(input_) if not isinstance(input_, complex) else input_
1995
- value = apply_bounds(value, upper_bound, lower_bound)
1996
- value = apply_precision(value, precision)
1997
- return convert_type(value, validate_num_type(num_type), inferred_type)
1998
-
1999
- # Convert input to string and extract numbers
2000
- input_str = str(input_)
2001
- number_matches = extract_numbers(input_str)
2002
-
2003
- if not number_matches:
2004
- raise ValueError(f"No valid numbers found in: {input_str}")
2005
-
2006
- # Process numbers
2007
- results = []
2008
- target_type = validate_num_type(num_type)
1437
+ from lionagi.libs.parse.to_num import to_num
2009
1438
 
2010
- number_matches = (
2011
- number_matches[:num_count]
2012
- if num_count < len(number_matches)
2013
- else number_matches
1439
+ return to_num(
1440
+ input_,
1441
+ upper_bound=upper_bound,
1442
+ lower_bound=lower_bound,
1443
+ num_type=num_type,
1444
+ precision=precision,
1445
+ num_count=num_count,
2014
1446
  )
2015
1447
 
2016
- for type_and_value in number_matches:
2017
- try:
2018
- # Infer appropriate type
2019
- inferred_type = infer_type(type_and_value)
2020
-
2021
- # Parse to numeric value
2022
- value = parse_number(type_and_value)
2023
-
2024
- # Apply bounds if not complex
2025
- value = apply_bounds(value, upper_bound, lower_bound)
2026
-
2027
- # Apply precision
2028
- value = apply_precision(value, precision)
2029
-
2030
- # Convert to target type if different from inferred
2031
- value = convert_type(value, target_type, inferred_type)
2032
-
2033
- results.append(value)
2034
-
2035
- except Exception as e:
2036
- if len(type_and_value) == 2:
2037
- raise type(e)(
2038
- f"Error processing {type_and_value[1]}: {str(e)}"
2039
- )
2040
- raise type(e)(f"Error processing {type_and_value}: {str(e)}")
2041
-
2042
- if results and num_count == 1:
2043
- return results[0]
2044
- return results
2045
-
2046
1448
 
2047
1449
  def extract_numbers(text: str) -> list[tuple[str, str]]:
2048
1450
  """Extract numeric values from text using ordered regex patterns.
@@ -2053,19 +1455,9 @@ def extract_numbers(text: str) -> list[tuple[str, str]]:
2053
1455
  Returns:
2054
1456
  List of tuples containing (pattern_type, matched_value).
2055
1457
  """
2056
- combined_pattern = "|".join(PATTERNS.values())
2057
- matches = re.finditer(combined_pattern, text, re.IGNORECASE)
2058
- numbers = []
1458
+ from lionagi.libs.parse.to_num import extract_numbers
2059
1459
 
2060
- for match in matches:
2061
- value = match.group()
2062
- # Check which pattern matched
2063
- for pattern_name, pattern in PATTERNS.items():
2064
- if re.fullmatch(pattern, value, re.IGNORECASE):
2065
- numbers.append((pattern_name, value))
2066
- break
2067
-
2068
- return numbers
1460
+ return extract_numbers(text=text)
2069
1461
 
2070
1462
 
2071
1463
  def validate_num_type(num_type: NUM_TYPES) -> type:
@@ -2080,14 +1472,9 @@ def validate_num_type(num_type: NUM_TYPES) -> type:
2080
1472
  Raises:
2081
1473
  ValueError: If the type specification is invalid.
2082
1474
  """
2083
- if isinstance(num_type, str):
2084
- if num_type not in TYPE_MAP:
2085
- raise ValueError(f"Invalid number type: {num_type}")
2086
- return TYPE_MAP[num_type]
1475
+ from lionagi.libs.parse.to_num import validate_num_type
2087
1476
 
2088
- if num_type not in (int, float, complex):
2089
- raise ValueError(f"Invalid number type: {num_type}")
2090
- return num_type
1477
+ return validate_num_type(num_type=num_type)
2091
1478
 
2092
1479
 
2093
1480
  def infer_type(value: tuple[str, str]) -> type:
@@ -2099,10 +1486,9 @@ def infer_type(value: tuple[str, str]) -> type:
2099
1486
  Returns:
2100
1487
  The inferred Python type.
2101
1488
  """
2102
- pattern_type, _ = value
2103
- if pattern_type in ("complex", "complex_sci", "pure_imaginary"):
2104
- return complex
2105
- return float
1489
+ from lionagi.libs.parse.to_num import infer_type
1490
+
1491
+ return infer_type(value=value)
2106
1492
 
2107
1493
 
2108
1494
  def convert_special(value: str) -> float:
@@ -2114,10 +1500,9 @@ def convert_special(value: str) -> float:
2114
1500
  Returns:
2115
1501
  The converted float value.
2116
1502
  """
2117
- value = value.lower()
2118
- if "infinity" in value or "inf" in value:
2119
- return float("-inf") if value.startswith("-") else float("inf")
2120
- return float("nan")
1503
+ from lionagi.libs.parse.to_num import convert_special
1504
+
1505
+ return convert_special(value=value)
2121
1506
 
2122
1507
 
2123
1508
  def convert_percentage(value: str) -> float:
@@ -2132,10 +1517,9 @@ def convert_percentage(value: str) -> float:
2132
1517
  Raises:
2133
1518
  ValueError: If the percentage value is invalid.
2134
1519
  """
2135
- try:
2136
- return float(value.rstrip("%")) / 100
2137
- except ValueError as e:
2138
- raise ValueError(f"Invalid percentage value: {value}") from e
1520
+ from lionagi.libs.parse.to_num import convert_percentage
1521
+
1522
+ return convert_percentage(value=value)
2139
1523
 
2140
1524
 
2141
1525
  def convert_complex(value: str) -> complex:
@@ -2150,23 +1534,9 @@ def convert_complex(value: str) -> complex:
2150
1534
  Raises:
2151
1535
  ValueError: If the complex number is invalid.
2152
1536
  """
2153
- try:
2154
- # Handle pure imaginary numbers
2155
- if value.endswith("j") or value.endswith("J"):
2156
- if value in ("j", "J"):
2157
- return complex(0, 1)
2158
- if value in ("+j", "+J"):
2159
- return complex(0, 1)
2160
- if value in ("-j", "-J"):
2161
- return complex(0, -1)
2162
- if "+" not in value and "-" not in value[1:]:
2163
- # Pure imaginary number
2164
- imag = float(value[:-1] or "1")
2165
- return complex(0, imag)
2166
-
2167
- return complex(value.replace(" ", ""))
2168
- except ValueError as e:
2169
- raise ValueError(f"Invalid complex number: {value}") from e
1537
+ from lionagi.libs.parse.to_num import convert_complex
1538
+
1539
+ return convert_complex(value=value)
2170
1540
 
2171
1541
 
2172
1542
  def convert_type(
@@ -2187,19 +1557,13 @@ def convert_type(
2187
1557
  Raises:
2188
1558
  TypeError: If the conversion is not possible.
2189
1559
  """
2190
- try:
2191
- # If no specific type requested, use inferred type
2192
- if target_type is float and inferred_type is complex:
2193
- return value
1560
+ from lionagi.libs.parse.to_num import convert_type
2194
1561
 
2195
- # Handle explicit type conversions
2196
- if target_type is int and isinstance(value, complex):
2197
- raise TypeError("Cannot convert complex number to int")
2198
- return target_type(value)
2199
- except (ValueError, TypeError) as e:
2200
- raise TypeError(
2201
- f"Cannot convert {value} to {target_type.__name__}"
2202
- ) from e
1562
+ return convert_type(
1563
+ value=value,
1564
+ target_type=target_type,
1565
+ inferred_type=inferred_type,
1566
+ )
2203
1567
 
2204
1568
 
2205
1569
  def apply_bounds(
@@ -2220,14 +1584,13 @@ def apply_bounds(
2220
1584
  Raises:
2221
1585
  ValueError: If the value is outside bounds.
2222
1586
  """
2223
- if isinstance(value, complex):
2224
- return value
1587
+ from lionagi.libs.parse.to_num import apply_bounds
2225
1588
 
2226
- if upper_bound is not None and value > upper_bound:
2227
- raise ValueError(f"Value {value} exceeds upper bound {upper_bound}")
2228
- if lower_bound is not None and value < lower_bound:
2229
- raise ValueError(f"Value {value} below lower bound {lower_bound}")
2230
- return value
1589
+ return apply_bounds(
1590
+ value=value,
1591
+ upper_bound=upper_bound,
1592
+ lower_bound=lower_bound,
1593
+ )
2231
1594
 
2232
1595
 
2233
1596
  def apply_precision(
@@ -2243,11 +1606,9 @@ def apply_precision(
2243
1606
  Returns:
2244
1607
  The rounded value.
2245
1608
  """
2246
- if precision is None or isinstance(value, complex):
2247
- return value
2248
- if isinstance(value, float):
2249
- return round(value, precision)
2250
- return value
1609
+ from lionagi.libs.parse.to_num import apply_precision
1610
+
1611
+ return apply_precision(value=value, precision=precision)
2251
1612
 
2252
1613
 
2253
1614
  def parse_number(type_and_value: tuple[str, str]) -> float | complex:
@@ -2262,113 +1623,32 @@ def parse_number(type_and_value: tuple[str, str]) -> float | complex:
2262
1623
  Raises:
2263
1624
  ValueError: If parsing fails.
2264
1625
  """
2265
- num_type, value = type_and_value
2266
- value = value.strip()
2267
-
2268
- try:
2269
- if num_type == "special":
2270
- return convert_special(value)
2271
-
2272
- if num_type == "percentage":
2273
- return convert_percentage(value)
2274
-
2275
- if num_type == "fraction":
2276
- if "/" not in value:
2277
- raise ValueError(f"Invalid fraction: {value}")
2278
- if value.count("/") > 1:
2279
- raise ValueError(f"Invalid fraction: {value}")
2280
- num, denom = value.split("/")
2281
- if not (num.strip("-").isdigit() and denom.isdigit()):
2282
- raise ValueError(f"Invalid fraction: {value}")
2283
- denom_val = float(denom)
2284
- if denom_val == 0:
2285
- raise ValueError("Division by zero")
2286
- return float(num) / denom_val
2287
- if num_type in ("complex", "complex_sci", "pure_imaginary"):
2288
- return convert_complex(value)
2289
- if num_type == "scientific":
2290
- if "e" not in value.lower():
2291
- raise ValueError(f"Invalid scientific notation: {value}")
2292
- parts = value.lower().split("e")
2293
- if len(parts) != 2:
2294
- raise ValueError(f"Invalid scientific notation: {value}")
2295
- if not (parts[1].lstrip("+-").isdigit()):
2296
- raise ValueError(f"Invalid scientific notation: {value}")
2297
- return float(value)
2298
- if num_type == "decimal":
2299
- return float(value)
2300
-
2301
- raise ValueError(f"Unknown number type: {num_type}")
2302
- except Exception as e:
2303
- # Preserve the specific error type but wrap with more context
2304
- raise type(e)(f"Failed to parse {value} as {num_type}: {str(e)}")
1626
+ from lionagi.libs.parse.to_num import parse_number
1627
+
1628
+ return parse_number(type_and_value=type_and_value)
2305
1629
 
2306
1630
 
2307
1631
  def breakdown_pydantic_annotation(
2308
1632
  model: type[B], max_depth: int | None = None, current_depth: int = 0
2309
1633
  ) -> dict[str, Any]:
1634
+ from lionagi.libs.schema.breakdown_pydantic_annotation import (
1635
+ breakdown_pydantic_annotation,
1636
+ )
2310
1637
 
2311
- if not _is_pydantic_model(model):
2312
- raise TypeError("Input must be a Pydantic model")
2313
-
2314
- if max_depth is not None and current_depth >= max_depth:
2315
- raise RecursionError("Maximum recursion depth reached")
2316
-
2317
- out: dict[str, Any] = {}
2318
- for k, v in model.__annotations__.items():
2319
- origin = get_origin(v)
2320
- if _is_pydantic_model(v):
2321
- out[k] = breakdown_pydantic_annotation(
2322
- v, max_depth, current_depth + 1
2323
- )
2324
- elif origin is list:
2325
- args = get_args(v)
2326
- if args and _is_pydantic_model(args[0]):
2327
- out[k] = [
2328
- breakdown_pydantic_annotation(
2329
- args[0], max_depth, current_depth + 1
2330
- )
2331
- ]
2332
- else:
2333
- out[k] = [args[0] if args else Any]
2334
- else:
2335
- out[k] = v
2336
-
2337
- return out
2338
-
2339
-
2340
- def _is_pydantic_model(x: Any) -> bool:
2341
- try:
2342
- return isclass(x) and issubclass(x, BaseModel)
2343
- except TypeError:
2344
- return False
1638
+ return breakdown_pydantic_annotation(
1639
+ model=model,
1640
+ max_depth=max_depth,
1641
+ current_depth=current_depth,
1642
+ )
2345
1643
 
2346
1644
 
2347
1645
  def run_package_manager_command(
2348
1646
  args: Sequence[str],
2349
1647
  ) -> subprocess.CompletedProcess[bytes]:
2350
1648
  """Run a package manager command, using uv if available, otherwise falling back to pip."""
2351
- # Check if uv is available in PATH
2352
- uv_path = shutil.which("uv")
1649
+ from lionagi.libs.package.imports import run_package_manager_command
2353
1650
 
2354
- if uv_path:
2355
- # Use uv if available
2356
- try:
2357
- return subprocess.run(
2358
- [uv_path] + list(args),
2359
- check=True,
2360
- capture_output=True,
2361
- )
2362
- except subprocess.CalledProcessError:
2363
- # If uv fails, fall back to pip
2364
- print("uv command failed, falling back to pip...")
2365
-
2366
- # Fall back to pip
2367
- return subprocess.run(
2368
- [sys.executable, "-m", "pip"] + list(args),
2369
- check=True,
2370
- capture_output=True,
2371
- )
1651
+ return run_package_manager_command(args=args)
2372
1652
 
2373
1653
 
2374
1654
  def check_import(
@@ -2394,34 +1674,15 @@ def check_import(
2394
1674
  ImportError: If the package is not found and not installed.
2395
1675
  ValueError: If the import fails after installation attempt.
2396
1676
  """
2397
- if not is_import_installed(package_name):
2398
- if attempt_install:
2399
- logging.info(
2400
- f"Package {package_name} not found. Attempting " "to install.",
2401
- )
2402
- try:
2403
- return install_import(
2404
- package_name=package_name,
2405
- module_name=module_name,
2406
- import_name=import_name,
2407
- pip_name=pip_name,
2408
- )
2409
- except ImportError as e:
2410
- raise ValueError(
2411
- f"Failed to install {package_name}: {e}"
2412
- ) from e
2413
- else:
2414
- logging.info(
2415
- f"Package {package_name} not found. {error_message}",
2416
- )
2417
- raise ImportError(
2418
- f"Package {package_name} not found. {error_message}",
2419
- )
1677
+ from lionagi.libs.package.imports import check_import
2420
1678
 
2421
- return import_module(
1679
+ return check_import(
2422
1680
  package_name=package_name,
2423
1681
  module_name=module_name,
2424
1682
  import_name=import_name,
1683
+ pip_name=pip_name,
1684
+ attempt_install=attempt_install,
1685
+ error_message=error_message,
2425
1686
  )
2426
1687
 
2427
1688
 
@@ -2442,31 +1703,13 @@ def import_module(
2442
1703
  Raises:
2443
1704
  ImportError: If the module cannot be imported.
2444
1705
  """
2445
- try:
2446
- full_import_path = (
2447
- f"{package_name}.{module_name}" if module_name else package_name
2448
- )
1706
+ from lionagi.libs.package.imports import import_module
2449
1707
 
2450
- if import_name:
2451
- import_name = (
2452
- [import_name]
2453
- if not isinstance(import_name, list)
2454
- else import_name
2455
- )
2456
- a = __import__(
2457
- full_import_path,
2458
- fromlist=import_name,
2459
- )
2460
- if len(import_name) == 1:
2461
- return getattr(a, import_name[0])
2462
- return [getattr(a, name) for name in import_name]
2463
- else:
2464
- return __import__(full_import_path)
2465
-
2466
- except ImportError as e:
2467
- raise ImportError(
2468
- f"Failed to import module {full_import_path}: {e}"
2469
- ) from e
1708
+ return import_module(
1709
+ package_name=package_name,
1710
+ module_name=module_name,
1711
+ import_name=import_name,
1712
+ )
2470
1713
 
2471
1714
 
2472
1715
  def install_import(
@@ -2488,29 +1731,14 @@ def install_import(
2488
1731
  ImportError: If the package cannot be imported or installed.
2489
1732
  subprocess.CalledProcessError: If pip installation fails.
2490
1733
  """
2491
- pip_name = pip_name or package_name
1734
+ from lionagi.libs.package.imports import install_import
2492
1735
 
2493
- try:
2494
- return import_module(
2495
- package_name=package_name,
2496
- module_name=module_name,
2497
- import_name=import_name,
2498
- )
2499
- except ImportError:
2500
- logging.info(f"Installing {pip_name}...")
2501
- try:
2502
- run_package_manager_command(["install", pip_name])
2503
- return import_module(
2504
- package_name=package_name,
2505
- module_name=module_name,
2506
- import_name=import_name,
2507
- )
2508
- except subprocess.CalledProcessError as e:
2509
- raise ImportError(f"Failed to install {pip_name}: {e}") from e
2510
- except ImportError as e:
2511
- raise ImportError(
2512
- f"Failed to import {pip_name} after installation: {e}"
2513
- ) from e
1736
+ install_import(
1737
+ package_name=package_name,
1738
+ module_name=module_name,
1739
+ import_name=import_name,
1740
+ pip_name=pip_name,
1741
+ )
2514
1742
 
2515
1743
 
2516
1744
  def is_import_installed(package_name: str) -> bool:
@@ -2527,23 +1755,9 @@ def is_import_installed(package_name: str) -> bool:
2527
1755
 
2528
1756
 
2529
1757
  def read_image_to_base64(image_path: str | Path) -> str:
2530
- import base64
2531
-
2532
- import cv2
2533
-
2534
- image_path = str(image_path)
2535
- image = cv2.imread(image_path, cv2.COLOR_BGR2RGB)
1758
+ from lionagi.libs.file.file_util import FileUtil
2536
1759
 
2537
- if image is None:
2538
- raise ValueError(f"Could not read image from path: {image_path}")
2539
-
2540
- file_extension = "." + image_path.split(".")[-1]
2541
-
2542
- success, buffer = cv2.imencode(file_extension, image)
2543
- if not success:
2544
- raise ValueError(f"Could not encode image to {file_extension} format.")
2545
- encoded_image = base64.b64encode(buffer).decode("utf-8")
2546
- return encoded_image
1760
+ return FileUtil.read_image_to_base64(image_path=image_path)
2547
1761
 
2548
1762
 
2549
1763
  def pdf_to_images(
@@ -2561,23 +1775,11 @@ def pdf_to_images(
2561
1775
  Returns:
2562
1776
  list: A list of file paths for the saved images.
2563
1777
  """
2564
- import os
1778
+ from lionagi.libs.file.file_util import FileUtil
2565
1779
 
2566
- convert_from_path = check_import(
2567
- "pdf2image", import_name="convert_from_path"
1780
+ return FileUtil.pdf_to_images(
1781
+ pdf_path=pdf_path,
1782
+ output_folder=output_folder,
1783
+ dpi=dpi,
1784
+ fmt=fmt,
2568
1785
  )
2569
-
2570
- # Ensure the output folder exists
2571
- os.makedirs(output_folder, exist_ok=True)
2572
-
2573
- # Convert PDF to a list of PIL Image objects
2574
- images = convert_from_path(pdf_path, dpi=dpi)
2575
-
2576
- saved_paths = []
2577
- for i, image in enumerate(images):
2578
- # Construct the output file name
2579
- image_file = os.path.join(output_folder, f"page_{i+1}.{fmt}")
2580
- image.save(image_file, fmt.upper())
2581
- saved_paths.append(image_file)
2582
-
2583
- return saved_paths