lionagi 0.15.8__py3-none-any.whl → 0.15.11__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 (42) hide show
  1. lionagi/__init__.py +4 -6
  2. lionagi/adapters/async_postgres_adapter.py +55 -319
  3. lionagi/libs/file/_utils.py +10 -0
  4. lionagi/libs/file/process.py +16 -13
  5. lionagi/libs/file/save.py +3 -2
  6. lionagi/libs/schema/load_pydantic_model_from_schema.py +2 -1
  7. lionagi/libs/unstructured/pdf_to_image.py +2 -2
  8. lionagi/libs/validate/string_similarity.py +4 -4
  9. lionagi/ln/__init__.py +38 -0
  10. lionagi/ln/_extract_json.py +60 -0
  11. lionagi/ln/_fuzzy_json.py +116 -0
  12. lionagi/ln/_json_dump.py +75 -0
  13. lionagi/ln/_models.py +0 -1
  14. lionagi/models/field_model.py +8 -6
  15. lionagi/operations/__init__.py +3 -0
  16. lionagi/operations/builder.py +10 -0
  17. lionagi/protocols/generic/element.py +56 -53
  18. lionagi/protocols/generic/event.py +46 -67
  19. lionagi/protocols/generic/pile.py +56 -1
  20. lionagi/protocols/generic/progression.py +11 -11
  21. lionagi/protocols/graph/_utils.py +22 -0
  22. lionagi/protocols/graph/graph.py +17 -21
  23. lionagi/protocols/graph/node.py +23 -5
  24. lionagi/protocols/messages/manager.py +41 -45
  25. lionagi/protocols/messages/message.py +3 -1
  26. lionagi/protocols/operatives/step.py +2 -19
  27. lionagi/protocols/types.py +1 -2
  28. lionagi/service/connections/providers/claude_code_.py +9 -7
  29. lionagi/service/third_party/claude_code.py +3 -2
  30. lionagi/session/session.py +14 -2
  31. lionagi/tools/file/reader.py +5 -6
  32. lionagi/utils.py +8 -385
  33. lionagi/version.py +1 -1
  34. {lionagi-0.15.8.dist-info → lionagi-0.15.11.dist-info}/METADATA +2 -2
  35. {lionagi-0.15.8.dist-info → lionagi-0.15.11.dist-info}/RECORD +37 -37
  36. lionagi/libs/package/__init__.py +0 -3
  37. lionagi/libs/package/imports.py +0 -21
  38. lionagi/libs/package/management.py +0 -62
  39. lionagi/libs/package/params.py +0 -30
  40. lionagi/libs/package/system.py +0 -22
  41. {lionagi-0.15.8.dist-info → lionagi-0.15.11.dist-info}/WHEEL +0 -0
  42. {lionagi-0.15.8.dist-info → lionagi-0.15.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,60 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ import orjson
5
+
6
+ from ._fuzzy_json import fuzzy_json
7
+
8
+ # Precompile the regex for extracting JSON code blocks
9
+ _JSON_BLOCK_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
10
+
11
+
12
+ def extract_json(
13
+ input_data: str | list[str],
14
+ /,
15
+ *,
16
+ fuzzy_parse: bool = False,
17
+ return_one_if_single: bool = True,
18
+ ) -> dict[str, Any] | list[dict[str, Any]]:
19
+ """Extract and parse JSON content from a string or markdown code blocks.
20
+ Attempts direct JSON parsing first. If that fails, looks for JSON content
21
+ within markdown code blocks denoted by ```json.
22
+
23
+ Args:
24
+ input_data (str | list[str]): The input string or list of strings to parse.
25
+ fuzzy_parse (bool): If True, attempts fuzzy JSON parsing on failed attempts.
26
+ return_one_if_single (bool): If True and only one JSON object is found,
27
+ returns a dict instead of a list with one dict.
28
+ """
29
+
30
+ # If input_data is a list, join into a single string
31
+ if isinstance(input_data, list):
32
+ input_str = "\n".join(input_data)
33
+ else:
34
+ input_str = input_data
35
+
36
+ # 1. Try direct parsing
37
+ try:
38
+ if fuzzy_parse:
39
+ return fuzzy_json(input_str)
40
+ return orjson.loads(input_str)
41
+ except Exception:
42
+ pass
43
+
44
+ # 2. Attempt extracting JSON blocks from markdown
45
+ matches = _JSON_BLOCK_PATTERN.findall(input_str)
46
+ if not matches:
47
+ return []
48
+
49
+ # If only one match, return single dict; if multiple, return list of dicts
50
+ if return_one_if_single and len(matches) == 1:
51
+ data_str = matches[0]
52
+ if fuzzy_parse:
53
+ return fuzzy_json(data_str)
54
+ return orjson.loads(data_str)
55
+
56
+ # Multiple matches
57
+ if fuzzy_parse:
58
+ return [fuzzy_json(m) for m in matches]
59
+ else:
60
+ return [orjson.loads(m) for m in matches]
@@ -0,0 +1,116 @@
1
+ import contextlib
2
+ import re
3
+ from typing import Any
4
+
5
+ import orjson
6
+
7
+
8
+ def fuzzy_json(str_to_parse: str, /) -> dict[str, Any] | list[dict[str, Any]]:
9
+ """
10
+ Attempt to parse a JSON string, trying a few minimal "fuzzy" fixes if needed.
11
+
12
+ Steps:
13
+ 1. Parse directly with json.loads.
14
+ 2. Replace single quotes with double quotes, normalize spacing, and try again.
15
+ 3. Attempt to fix unmatched brackets using fix_json_string.
16
+ 4. If all fail, raise ValueError.
17
+
18
+ Args:
19
+ str_to_parse: The JSON string to parse
20
+
21
+ Returns:
22
+ Parsed JSON (dict or list of dicts)
23
+
24
+ Raises:
25
+ ValueError: If the string cannot be parsed as valid JSON
26
+ TypeError: If the input is not a string
27
+ """
28
+ _check_valid_str(str_to_parse)
29
+
30
+ # 1. Direct attempt
31
+ with contextlib.suppress(Exception):
32
+ return orjson.loads(str_to_parse)
33
+
34
+ # 2. Try cleaning: replace single quotes with double and normalize
35
+ cleaned = _clean_json_string(str_to_parse.replace("'", '"'))
36
+ with contextlib.suppress(Exception):
37
+ return orjson.loads(cleaned)
38
+
39
+ # 3. Try fixing brackets
40
+ fixed = fix_json_string(cleaned)
41
+ with contextlib.suppress(Exception):
42
+ return orjson.loads(fixed)
43
+
44
+ # If all attempts fail
45
+ raise ValueError("Invalid JSON string")
46
+
47
+
48
+ def _check_valid_str(str_to_parse: str, /):
49
+ if not isinstance(str_to_parse, str):
50
+ raise TypeError("Input must be a string")
51
+ if not str_to_parse.strip():
52
+ raise ValueError("Input string is empty")
53
+
54
+
55
+ def _clean_json_string(s: str) -> str:
56
+ """Basic normalization: replace unescaped single quotes, trim spaces, ensure keys are quoted."""
57
+ # Replace unescaped single quotes with double quotes
58
+ # '(?<!\\)'" means a single quote not preceded by a backslash
59
+ s = re.sub(r"(?<!\\)'", '"', s)
60
+ # Collapse multiple whitespaces
61
+ s = re.sub(r"\s+", " ", s)
62
+ # Ensure keys are quoted
63
+ # This attempts to find patterns like { key: value } and turn them into {"key": value}
64
+ s = re.sub(r'([{,])\s*([^"\s]+)\s*:', r'\1"\2":', s)
65
+ return s.strip()
66
+
67
+
68
+ def fix_json_string(str_to_parse: str, /) -> str:
69
+ """Try to fix JSON string by ensuring brackets are matched properly."""
70
+ if not str_to_parse:
71
+ raise ValueError("Input string is empty")
72
+
73
+ brackets = {"{": "}", "[": "]"}
74
+ open_brackets = []
75
+ pos = 0
76
+ length = len(str_to_parse)
77
+
78
+ while pos < length:
79
+ char = str_to_parse[pos]
80
+
81
+ if char == "\\":
82
+ pos += 2 # Skip escaped chars
83
+ continue
84
+
85
+ if char == '"':
86
+ pos += 1
87
+ # skip string content
88
+ while pos < length:
89
+ if str_to_parse[pos] == "\\":
90
+ pos += 2
91
+ continue
92
+ if str_to_parse[pos] == '"':
93
+ pos += 1
94
+ break
95
+ pos += 1
96
+ continue
97
+
98
+ if char in brackets:
99
+ open_brackets.append(brackets[char])
100
+ elif char in brackets.values():
101
+ if not open_brackets:
102
+ # Extra closing bracket
103
+ # Better to raise error than guess
104
+ raise ValueError("Extra closing bracket found.")
105
+ if open_brackets[-1] != char:
106
+ # Mismatched bracket
107
+ raise ValueError("Mismatched brackets.")
108
+ open_brackets.pop()
109
+
110
+ pos += 1
111
+
112
+ # Add missing closing brackets if any
113
+ if open_brackets:
114
+ str_to_parse += "".join(reversed(open_brackets))
115
+
116
+ return str_to_parse
@@ -0,0 +1,75 @@
1
+ import datetime as dt
2
+ import decimal
3
+ from collections.abc import Callable
4
+ from pathlib import Path
5
+ from uuid import UUID
6
+
7
+ import orjson
8
+
9
+
10
+ def _get_default_serializers():
11
+ return {
12
+ dt.datetime: lambda o: o.isoformat(),
13
+ Path: lambda o: str(o),
14
+ UUID: lambda o: str(o),
15
+ decimal.Decimal: lambda o: str(o),
16
+ set: lambda o: list(o),
17
+ frozenset: lambda o: list(o),
18
+ }
19
+
20
+
21
+ def _get_default_serializer_order():
22
+ return [dt.datetime, Path, UUID, decimal.Decimal, set, frozenset]
23
+
24
+
25
+ def get_orjson_default(
26
+ order: list[type] = None,
27
+ additional: dict[type, Callable] = None,
28
+ extend_default: bool = True,
29
+ ) -> Callable:
30
+ """get the default function for orjson.dumps
31
+ Args:
32
+ order: order of types to check. Defaults to None.
33
+ additional: additional serializers
34
+ extend_default: when order is provided, whether to extend the default order or replace it.
35
+ """
36
+ dict_ = _get_default_serializers()
37
+ dict_.update(additional or {})
38
+ order_ = _get_default_serializer_order()
39
+
40
+ if order:
41
+ if len(additional or {}) > 0 and extend_default:
42
+ order_.extend([k for k in order if k not in order_])
43
+ else:
44
+ order_ = list(order)
45
+ else:
46
+ if len(additional or {}) > 0:
47
+ order_.extend([k for k in additional.keys() if k not in order_])
48
+
49
+ def default(obj):
50
+ for t in order_:
51
+ if isinstance(obj, t) and t in dict_:
52
+ return dict_[t](obj)
53
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
54
+
55
+ return default
56
+
57
+
58
+ DEFAULT_SERIALIZER = get_orjson_default()
59
+ DEFAULT_SERIALIZER_OPTION = (
60
+ orjson.OPT_INDENT_2
61
+ | orjson.OPT_SORT_KEYS
62
+ | orjson.OPT_APPEND_NEWLINE
63
+ | orjson.OPT_SERIALIZE_DATACLASS
64
+ )
65
+
66
+
67
+ def json_dumps(d_, decode=True, /) -> str:
68
+ by_ = orjson.dumps(
69
+ d_,
70
+ default=DEFAULT_SERIALIZER,
71
+ option=DEFAULT_SERIALIZER_OPTION,
72
+ )
73
+ if decode:
74
+ return by_.decode("utf-8")
75
+ return by_
lionagi/ln/_models.py CHANGED
@@ -70,7 +70,6 @@ class Params:
70
70
  _validate_strict(k)
71
71
 
72
72
  def default_kw(self) -> Any:
73
-
74
73
  # create a partial function with the current parameters
75
74
  dict_ = self.to_dict()
76
75
 
@@ -16,7 +16,6 @@ from typing import Annotated, Any
16
16
  from typing_extensions import Self, override
17
17
 
18
18
  from .._errors import ValidationError
19
- from ..utils import UNDEFINED
20
19
 
21
20
  # Cache of valid Pydantic Field parameters
22
21
  _PYDANTIC_FIELD_PARAMS: set[str] | None = None
@@ -660,13 +659,16 @@ def to_dict(self) -> dict[str, Any]:
660
659
 
661
660
  # Convert metadata to dictionary
662
661
  for meta in self.metadata:
663
- if meta.key not in ("nullable", "listable", "validator"):
662
+ if meta.key not in (
663
+ "nullable",
664
+ "listable",
665
+ "validator",
666
+ "name",
667
+ "validator_kwargs",
668
+ "annotation",
669
+ ):
664
670
  result[meta.key] = meta.value
665
671
 
666
- # Add annotation if available
667
- if hasattr(self, "annotation"):
668
- result["annotation"] = self.base_type
669
-
670
672
  return result
671
673
 
672
674
 
@@ -8,6 +8,8 @@ from .flow import flow
8
8
  from .node import BranchOperations, Operation
9
9
  from .plan.plan import PlanOperation, plan
10
10
 
11
+ Builder = OperationGraphBuilder
12
+
11
13
  __all__ = (
12
14
  "ExpansionStrategy",
13
15
  "OperationGraphBuilder",
@@ -19,4 +21,5 @@ __all__ = (
19
21
  "PlanOperation",
20
22
  "brainstorm",
21
23
  "BrainstormOperation",
24
+ "Builder",
22
25
  )
@@ -453,6 +453,16 @@ def visualize_graph(
453
453
  figsize=(14, 10),
454
454
  ):
455
455
  """Visualization with improved layout for complex graphs."""
456
+ from lionagi.protocols.graph.graph import (
457
+ _MATPLIB_AVAILABLE,
458
+ _NETWORKX_AVAILABLE,
459
+ )
460
+
461
+ if _MATPLIB_AVAILABLE is not True:
462
+ raise _MATPLIB_AVAILABLE
463
+ if _NETWORKX_AVAILABLE is not True:
464
+ raise _NETWORKX_AVAILABLE
465
+
456
466
  import matplotlib.pyplot as plt
457
467
  import networkx as nx
458
468
  import numpy as np
@@ -4,11 +4,12 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import datetime as dt
7
8
  from collections.abc import Mapping, Sequence
8
- from datetime import datetime
9
- from typing import Any, Generic, TypeAlias, TypeVar
9
+ from typing import Any, Generic, Literal, TypeAlias, TypeVar
10
10
  from uuid import UUID, uuid4
11
11
 
12
+ import orjson
12
13
  from pydantic import (
13
14
  BaseModel,
14
15
  ConfigDict,
@@ -17,10 +18,11 @@ from pydantic import (
17
18
  field_validator,
18
19
  )
19
20
 
21
+ from lionagi import ln
20
22
  from lionagi._class_registry import get_class
21
23
  from lionagi._errors import IDError
22
24
  from lionagi.settings import Settings
23
- from lionagi.utils import UNDEFINED, time, to_dict
25
+ from lionagi.utils import import_module, time, to_dict
24
26
 
25
27
  from .._concepts import Collective, Observable, Ordering
26
28
 
@@ -29,6 +31,7 @@ __all__ = (
29
31
  "Element",
30
32
  "ID",
31
33
  "validate_order",
34
+ "DEFAULT_ELEMENT_SERIALIZER",
32
35
  )
33
36
 
34
37
 
@@ -173,15 +176,6 @@ class Element(BaseModel, Observable):
173
176
  If a `lion_class` field is present in `metadata`, it must match the
174
177
  fully qualified name of this class. Converts `metadata` to a dict
175
178
  if needed.
176
-
177
- Args:
178
- val (dict): The initial metadata value.
179
-
180
- Returns:
181
- dict: A valid dictionary of metadata.
182
-
183
- Raises:
184
- ValueError: If the metadata is invalid or has a class mismatch.
185
179
  """
186
180
  if not val:
187
181
  return {}
@@ -196,7 +190,7 @@ class Element(BaseModel, Observable):
196
190
  return val
197
191
 
198
192
  @field_validator("created_at", mode="before")
199
- def _coerce_created_at(cls, val: float | datetime | None) -> float:
193
+ def _coerce_created_at(cls, val: float | dt.datetime | None) -> float:
200
194
  """Coerces `created_at` to a float-based timestamp.
201
195
 
202
196
  Args:
@@ -212,7 +206,7 @@ class Element(BaseModel, Observable):
212
206
  return time(tz=Settings.Config.TIMEZONE, type_="timestamp")
213
207
  if isinstance(val, float):
214
208
  return val
215
- if isinstance(val, datetime):
209
+ if isinstance(val, dt.datetime):
216
210
  return val.timestamp()
217
211
  try:
218
212
  return float(val) # type: ignore
@@ -245,33 +239,24 @@ class Element(BaseModel, Observable):
245
239
  return str(val)
246
240
 
247
241
  @property
248
- def created_datetime(self) -> datetime:
242
+ def created_datetime(self) -> dt.datetime:
249
243
  """Returns the creation time as a datetime object.
250
244
 
251
245
  Returns:
252
246
  datetime: The creation time in UTC.
253
247
  """
254
- return datetime.fromtimestamp(self.created_at)
248
+ return dt.datetime.fromtimestamp(self.created_at)
255
249
 
256
250
  def __eq__(self, other: Any) -> bool:
257
- """Compares two Element instances by their ID.
258
-
259
- Args:
260
- other (Any): Another object for comparison.
261
-
262
- Returns:
263
- bool: True if both share the same ID, False otherwise.
264
- """
251
+ """Compares two Element instances by their ID."""
265
252
  if not isinstance(other, Element):
266
- return NotImplemented
253
+ raise NotImplementedError(
254
+ f"Cannot compare Element with {type(other)}"
255
+ )
267
256
  return self.id == other.id
268
257
 
269
258
  def __hash__(self) -> int:
270
- """Returns a hash of this element's ID.
271
-
272
- Returns:
273
- int: The hash of the ID, making elements usable as dictionary keys.
274
- """
259
+ """Returns a hash of this element's ID."""
275
260
  return hash(self.id)
276
261
 
277
262
  def __bool__(self) -> bool:
@@ -282,42 +267,37 @@ class Element(BaseModel, Observable):
282
267
  def class_name(cls, full: bool = False) -> str:
283
268
  """Returns this class's name.
284
269
 
285
- Args:
286
- full (bool):
287
- If True, returns the fully qualified class name; otherwise,
288
- returns only the class name.
289
-
290
- Returns:
291
- str: The class name or fully qualified name.
270
+ full (bool): If True, returns the fully qualified class name; otherwise,
271
+ returns only the class name.
292
272
  """
293
273
  if full:
294
274
  return str(cls).split("'")[1]
295
275
  return cls.__name__
296
276
 
297
- def to_dict(self) -> dict:
298
- """Converts this Element to a dictionary.
299
-
300
- All fields are included except those set to `UNDEFINED`.
301
-
302
- Returns:
303
- dict: The dictionary representation of this Element.
304
- """
277
+ def _to_dict(self) -> dict:
305
278
  dict_ = self.model_dump()
306
279
  dict_["metadata"].update({"lion_class": self.class_name(full=True)})
307
- return {k: v for k, v in dict_.items() if v is not UNDEFINED}
280
+ return {k: v for k, v in dict_.items() if ln.not_sentinel(v)}
281
+
282
+ def to_dict(self, mode: Literal["python", "json"] = "python") -> dict:
283
+ """Converts this Element to a dictionary."""
284
+ if mode == "python":
285
+ return self._to_dict()
286
+ return orjson.loads(self.to_json(decode=False))
287
+
288
+ def as_jsonable(self) -> dict:
289
+ """Converts this Element to a JSON-serializable dictionary."""
290
+ return self.to_dict(mode="json")
308
291
 
309
292
  @classmethod
310
293
  def from_dict(cls, data: dict, /) -> Element:
311
294
  """Deserializes a dictionary into an Element or subclass of Element.
312
295
 
313
296
  If `lion_class` in `metadata` refers to a subclass, this method
314
- attempts to create an instance of that subclass.
297
+ is polymorphic, it will attempt to create an instance of that subclass.
315
298
 
316
299
  Args:
317
300
  data (dict): A dictionary of field data.
318
-
319
- Returns:
320
- Element: An Element or a subclass instance loaded from `data`.
321
301
  """
322
302
  metadata = data.pop("metadata", {})
323
303
  if "lion_class" in metadata:
@@ -337,9 +317,6 @@ class Element(BaseModel, Observable):
337
317
  return subcls_type.from_dict(data)
338
318
 
339
319
  except Exception:
340
- # Fallback attempt: direct import if not in registry
341
- from lionagi.libs.package.imports import import_module
342
-
343
320
  mod, imp = subcls.rsplit(".", 1)
344
321
  subcls_type = import_module(mod, import_name=imp)
345
322
  data["metadata"] = metadata
@@ -350,6 +327,32 @@ class Element(BaseModel, Observable):
350
327
  data["metadata"] = metadata
351
328
  return cls.model_validate(data)
352
329
 
330
+ def to_json(self, decode: bool = True) -> str:
331
+ """Converts this Element to a JSON string."""
332
+ dict_ = self._to_dict()
333
+ if decode:
334
+ return orjson.dumps(
335
+ dict_,
336
+ default=DEFAULT_ELEMENT_SERIALIZER,
337
+ option=ln.DEFAULT_SERIALIZER_OPTION,
338
+ ).decode()
339
+ return orjson.dumps(dict_, default=DEFAULT_ELEMENT_SERIALIZER)
340
+
341
+ def from_json(cls, json_str: str) -> Element:
342
+ """Deserializes a JSON string into an Element or subclass of Element."""
343
+ data = orjson.loads(json_str)
344
+ return cls.from_dict(data)
345
+
346
+
347
+ DEFAULT_ELEMENT_SERIALIZER = ln.get_orjson_default(
348
+ order=[IDType, Element, BaseModel],
349
+ additional={
350
+ IDType: lambda o: str(o),
351
+ Element: lambda o: o.to_dict(),
352
+ BaseModel: lambda o: o.model_dump(mode="json"),
353
+ },
354
+ )
355
+
353
356
 
354
357
  def validate_order(order: Any) -> list[IDType]:
355
358
  """Validates and flattens an ordering into a list of IDType objects.