lionagi 0.14.9__py3-none-any.whl → 0.14.10__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/fields/reason.py +1 -1
- lionagi/libs/concurrency/throttle.py +79 -0
- lionagi/libs/parse.py +2 -1
- lionagi/libs/unstructured/__init__.py +0 -0
- lionagi/libs/unstructured/pdf_to_image.py +45 -0
- lionagi/libs/unstructured/read_image_to_base64.py +33 -0
- lionagi/libs/validate/to_num.py +378 -0
- lionagi/libs/validate/xml_parser.py +203 -0
- lionagi/service/hooks/_utils.py +2 -2
- lionagi/service/hooks/hook_registry.py +8 -8
- lionagi/tools/file/reader.py +1 -1
- lionagi/tools/memory/tools.py +2 -2
- lionagi/utils.py +4 -773
- lionagi/version.py +1 -1
- {lionagi-0.14.9.dist-info → lionagi-0.14.10.dist-info}/METADATA +5 -1
- {lionagi-0.14.9.dist-info → lionagi-0.14.10.dist-info}/RECORD +18 -12
- {lionagi-0.14.9.dist-info → lionagi-0.14.10.dist-info}/WHEEL +0 -0
- {lionagi-0.14.9.dist-info → lionagi-0.14.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
import xml.etree.ElementTree as ET
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
|
8
|
+
def to_xml(
|
9
|
+
obj: dict | list | str | int | float | bool | None,
|
10
|
+
root_name: str = "root",
|
11
|
+
) -> str:
|
12
|
+
"""
|
13
|
+
Convert a dictionary into an XML formatted string.
|
14
|
+
|
15
|
+
Rules:
|
16
|
+
- A dictionary key becomes an XML tag.
|
17
|
+
- If the dictionary value is:
|
18
|
+
- A primitive type (str, int, float, bool, None): it becomes the text content of the tag.
|
19
|
+
- A list: each element of the list will repeat the same tag.
|
20
|
+
- Another dictionary: it is recursively converted to nested XML.
|
21
|
+
- root_name sets the top-level XML element name.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
obj: The Python object to convert (typically a dictionary).
|
25
|
+
root_name: The name of the root XML element.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
A string representing the XML.
|
29
|
+
|
30
|
+
Examples:
|
31
|
+
>>> to_xml({"a": 1, "b": {"c": "hello", "d": [10, 20]}}, root_name="data")
|
32
|
+
'<data><a>1</a><b><c>hello</c><d>10</d><d>20</d></b></data>'
|
33
|
+
"""
|
34
|
+
|
35
|
+
def _convert(value: Any, tag_name: str) -> str:
|
36
|
+
# If value is a dict, recursively convert its keys
|
37
|
+
if isinstance(value, dict):
|
38
|
+
inner = "".join(_convert(v, k) for k, v in value.items())
|
39
|
+
return f"<{tag_name}>{inner}</{tag_name}>"
|
40
|
+
# If value is a list, repeat the same tag for each element
|
41
|
+
elif isinstance(value, list):
|
42
|
+
return "".join(_convert(item, tag_name) for item in value)
|
43
|
+
# If value is a primitive, convert to string and place inside tag
|
44
|
+
else:
|
45
|
+
text = "" if value is None else str(value)
|
46
|
+
# Escape special XML characters if needed (minimal)
|
47
|
+
text = (
|
48
|
+
text.replace("&", "&")
|
49
|
+
.replace("<", "<")
|
50
|
+
.replace(">", ">")
|
51
|
+
.replace('"', """)
|
52
|
+
.replace("'", "'")
|
53
|
+
)
|
54
|
+
return f"<{tag_name}>{text}</{tag_name}>"
|
55
|
+
|
56
|
+
# If top-level obj is not a dict, wrap it in one
|
57
|
+
if not isinstance(obj, dict):
|
58
|
+
obj = {root_name: obj}
|
59
|
+
|
60
|
+
inner_xml = "".join(_convert(v, k) for k, v in obj.items())
|
61
|
+
return f"<{root_name}>{inner_xml}</{root_name}>"
|
62
|
+
|
63
|
+
|
64
|
+
class XMLParser:
|
65
|
+
def __init__(self, xml_string: str):
|
66
|
+
self.xml_string = xml_string.strip()
|
67
|
+
self.index = 0
|
68
|
+
|
69
|
+
def parse(self) -> dict[str, Any]:
|
70
|
+
"""Parse the XML string and return the root element as a dictionary."""
|
71
|
+
return self._parse_element()
|
72
|
+
|
73
|
+
def _parse_element(self) -> dict[str, Any]:
|
74
|
+
"""Parse a single XML element and its children."""
|
75
|
+
self._skip_whitespace()
|
76
|
+
if self.xml_string[self.index] != "<":
|
77
|
+
raise ValueError(
|
78
|
+
f"Expected '<', found '{self.xml_string[self.index]}'"
|
79
|
+
)
|
80
|
+
|
81
|
+
tag, attributes = self._parse_opening_tag()
|
82
|
+
children: dict[str, str | list | dict] = {}
|
83
|
+
text = ""
|
84
|
+
|
85
|
+
while self.index < len(self.xml_string):
|
86
|
+
self._skip_whitespace()
|
87
|
+
if self.xml_string.startswith("</", self.index):
|
88
|
+
closing_tag = self._parse_closing_tag()
|
89
|
+
if closing_tag != tag:
|
90
|
+
raise ValueError(
|
91
|
+
f"Mismatched tags: '{tag}' and '{closing_tag}'"
|
92
|
+
)
|
93
|
+
break
|
94
|
+
elif self.xml_string.startswith("<", self.index):
|
95
|
+
child = self._parse_element()
|
96
|
+
child_tag, child_data = next(iter(child.items()))
|
97
|
+
if child_tag in children:
|
98
|
+
if not isinstance(children[child_tag], list):
|
99
|
+
children[child_tag] = [children[child_tag]]
|
100
|
+
children[child_tag].append(child_data)
|
101
|
+
else:
|
102
|
+
children[child_tag] = child_data
|
103
|
+
else:
|
104
|
+
text += self._parse_text()
|
105
|
+
|
106
|
+
result: dict[str, Any] = {}
|
107
|
+
if attributes:
|
108
|
+
result["@attributes"] = attributes
|
109
|
+
if children:
|
110
|
+
result.update(children)
|
111
|
+
elif text.strip():
|
112
|
+
result = text.strip()
|
113
|
+
|
114
|
+
return {tag: result}
|
115
|
+
|
116
|
+
def _parse_opening_tag(self) -> tuple[str, dict[str, str]]:
|
117
|
+
"""Parse an opening XML tag and its attributes."""
|
118
|
+
match = re.match(
|
119
|
+
r'<(\w+)((?:\s+\w+="[^"]*")*)\s*/?>',
|
120
|
+
self.xml_string[self.index :], # noqa
|
121
|
+
)
|
122
|
+
if not match:
|
123
|
+
raise ValueError("Invalid opening tag")
|
124
|
+
self.index += match.end()
|
125
|
+
tag = match.group(1)
|
126
|
+
attributes = dict(re.findall(r'(\w+)="([^"]*)"', match.group(2)))
|
127
|
+
return tag, attributes
|
128
|
+
|
129
|
+
def _parse_closing_tag(self) -> str:
|
130
|
+
"""Parse a closing XML tag."""
|
131
|
+
match = re.match(r"</(\w+)>", self.xml_string[self.index :]) # noqa
|
132
|
+
if not match:
|
133
|
+
raise ValueError("Invalid closing tag")
|
134
|
+
self.index += match.end()
|
135
|
+
return match.group(1)
|
136
|
+
|
137
|
+
def _parse_text(self) -> str:
|
138
|
+
"""Parse text content between XML tags."""
|
139
|
+
start = self.index
|
140
|
+
while (
|
141
|
+
self.index < len(self.xml_string)
|
142
|
+
and self.xml_string[self.index] != "<"
|
143
|
+
):
|
144
|
+
self.index += 1
|
145
|
+
return self.xml_string[start : self.index] # noqa
|
146
|
+
|
147
|
+
def _skip_whitespace(self) -> None:
|
148
|
+
"""Skip any whitespace characters at the current parsing position."""
|
149
|
+
p_ = len(self.xml_string[self.index :]) # noqa
|
150
|
+
m_ = len(self.xml_string[self.index :].lstrip()) # noqa
|
151
|
+
|
152
|
+
self.index += p_ - m_
|
153
|
+
|
154
|
+
|
155
|
+
def xml_to_dict(
|
156
|
+
xml_string: str,
|
157
|
+
/,
|
158
|
+
suppress=False,
|
159
|
+
remove_root: bool = True,
|
160
|
+
root_tag: str = None,
|
161
|
+
) -> dict[str, Any]:
|
162
|
+
"""
|
163
|
+
Parse an XML string into a nested dictionary structure.
|
164
|
+
|
165
|
+
This function converts an XML string into a dictionary where:
|
166
|
+
- Element tags become dictionary keys
|
167
|
+
- Text content is assigned directly to the tag key if there are no children
|
168
|
+
- Attributes are stored in a '@attributes' key
|
169
|
+
- Multiple child elements with the same tag are stored as lists
|
170
|
+
|
171
|
+
Args:
|
172
|
+
xml_string: The XML string to parse.
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
A dictionary representation of the XML structure.
|
176
|
+
|
177
|
+
Raises:
|
178
|
+
ValueError: If the XML is malformed or parsing fails.
|
179
|
+
"""
|
180
|
+
try:
|
181
|
+
a = XMLParser(xml_string).parse()
|
182
|
+
if remove_root and (root_tag or "root") in a:
|
183
|
+
a = a[root_tag or "root"]
|
184
|
+
return a
|
185
|
+
except ValueError as e:
|
186
|
+
if not suppress:
|
187
|
+
raise e
|
188
|
+
|
189
|
+
|
190
|
+
def dict_to_xml(data: dict, /, root_tag: str = "root") -> str:
|
191
|
+
root = ET.Element(root_tag)
|
192
|
+
|
193
|
+
def convert(dict_obj: dict, parent: Any) -> None:
|
194
|
+
for key, val in dict_obj.items():
|
195
|
+
if isinstance(val, dict):
|
196
|
+
element = ET.SubElement(parent, key)
|
197
|
+
convert(dict_obj=val, parent=element)
|
198
|
+
else:
|
199
|
+
element = ET.SubElement(parent, key)
|
200
|
+
element.text = str(object=val)
|
201
|
+
|
202
|
+
convert(dict_obj=data, parent=root)
|
203
|
+
return ET.tostring(root, encoding="unicode")
|
lionagi/service/hooks/_utils.py
CHANGED
@@ -26,14 +26,14 @@ def get_handler(d_: dict, k: str | type, get: bool = False, /):
|
|
26
26
|
if not is_coro_func(handler):
|
27
27
|
|
28
28
|
async def _func(x):
|
29
|
-
sleep(0)
|
29
|
+
await sleep(0)
|
30
30
|
return handler(x)
|
31
31
|
|
32
32
|
return _func
|
33
33
|
return handler
|
34
34
|
|
35
35
|
async def _func(x):
|
36
|
-
sleep(0)
|
36
|
+
await sleep(0)
|
37
37
|
return x
|
38
38
|
|
39
39
|
return _func
|
@@ -97,8 +97,8 @@ class HookRegistry:
|
|
97
97
|
**kw,
|
98
98
|
)
|
99
99
|
return (res, False, EventStatus.COMPLETED)
|
100
|
-
except get_cancelled_exc_class():
|
101
|
-
return (UNDEFINED, True, EventStatus.CANCELLED)
|
100
|
+
except get_cancelled_exc_class() as e:
|
101
|
+
return ((UNDEFINED, e), True, EventStatus.CANCELLED)
|
102
102
|
except Exception as e:
|
103
103
|
return (e, exit, EventStatus.CANCELLED)
|
104
104
|
|
@@ -143,14 +143,14 @@ class HookRegistry:
|
|
143
143
|
**kw,
|
144
144
|
)
|
145
145
|
return (res, False, EventStatus.COMPLETED)
|
146
|
-
except get_cancelled_exc_class():
|
147
|
-
return (UNDEFINED, True, EventStatus.CANCELLED)
|
146
|
+
except get_cancelled_exc_class() as e:
|
147
|
+
return ((UNDEFINED, e), True, EventStatus.CANCELLED)
|
148
148
|
except Exception as e:
|
149
149
|
return (e, exit, EventStatus.ABORTED)
|
150
150
|
|
151
151
|
async def handle_streaming_chunk(
|
152
152
|
self, chunk_type: str | type, chunk: Any, /, exit: bool = False, **kw
|
153
|
-
) -> tuple[
|
153
|
+
) -> tuple[Any, bool, EventStatus | None]:
|
154
154
|
"""Hook to be called to consume streaming chunks.
|
155
155
|
|
156
156
|
Typically used for logging or stream event abortion.
|
@@ -165,11 +165,11 @@ class HookRegistry:
|
|
165
165
|
None,
|
166
166
|
**kw,
|
167
167
|
)
|
168
|
-
return (res, False)
|
168
|
+
return (res, False, None)
|
169
169
|
except get_cancelled_exc_class() as e:
|
170
|
-
return (e, True)
|
170
|
+
return ((UNDEFINED, e), True, EventStatus.CANCELLED)
|
171
171
|
except Exception as e:
|
172
|
-
return (e, exit)
|
172
|
+
return (e, exit, EventStatus.ABORTED)
|
173
173
|
|
174
174
|
async def call(
|
175
175
|
self,
|
lionagi/tools/file/reader.py
CHANGED
@@ -7,9 +7,9 @@ from enum import Enum
|
|
7
7
|
|
8
8
|
from pydantic import BaseModel, Field, model_validator
|
9
9
|
|
10
|
+
from lionagi.libs.validate.to_num import to_num
|
10
11
|
from lionagi.protocols.action.tool import Tool
|
11
12
|
from lionagi.service.token_calculator import TokenCalculator
|
12
|
-
from lionagi.utils import to_num
|
13
13
|
|
14
14
|
from ..base import LionTool
|
15
15
|
|
lionagi/tools/memory/tools.py
CHANGED
@@ -4,13 +4,13 @@ Memory Tools - Proper lionagi tool implementation following reader pattern
|
|
4
4
|
|
5
5
|
from datetime import datetime, timezone
|
6
6
|
from enum import Enum
|
7
|
-
from typing import Any
|
7
|
+
from typing import Any
|
8
8
|
|
9
9
|
from pydantic import BaseModel, Field, model_validator
|
10
10
|
|
11
|
+
from lionagi.libs.validate.to_num import to_num
|
11
12
|
from lionagi.protocols.action.tool import Tool
|
12
13
|
from lionagi.tools.base import LionTool
|
13
|
-
from lionagi.utils import to_num
|
14
14
|
|
15
15
|
|
16
16
|
class MemoryAction(str, Enum):
|