lionagi 0.15.9__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.
@@ -392,79 +392,75 @@ class MessageManager(Manager):
392
392
 
393
393
  @property
394
394
  def last_response(self) -> AssistantResponse | None:
395
- """
396
- Retrieve the most recent `AssistantResponse`.
397
- """
398
- for mid in reversed(self.messages.progression):
399
- if isinstance(self.messages[mid], AssistantResponse):
400
- return self.messages[mid]
395
+ """Retrieve the most recent `AssistantResponse`."""
396
+ res = self.messages.filter_by_type(
397
+ item_type=AssistantResponse,
398
+ strict_type=True,
399
+ as_pile=False,
400
+ reverse=True,
401
+ num_items=1,
402
+ )
403
+ if len(res) == 1:
404
+ return res[0]
401
405
  return None
402
406
 
403
407
  @property
404
408
  def last_instruction(self) -> Instruction | None:
405
- """
406
- Retrieve the most recent `Instruction`.
407
- """
408
- for mid in reversed(self.messages.progression):
409
- if isinstance(self.messages[mid], Instruction):
410
- return self.messages[mid]
409
+ """Retrieve the most recent `Instruction`."""
410
+ res = self.messages.filter_by_type(
411
+ item_type=Instruction,
412
+ strict_type=True,
413
+ as_pile=False,
414
+ reverse=True,
415
+ num_items=1,
416
+ )
417
+ if len(res) == 1:
418
+ return res[0]
411
419
  return None
412
420
 
413
421
  @property
414
422
  def assistant_responses(self) -> Pile[AssistantResponse]:
415
423
  """All `AssistantResponse` messages in the manager."""
416
- return Pile(
417
- collections=[
418
- self.messages[mid]
419
- for mid in self.messages.progression
420
- if isinstance(self.messages[mid], AssistantResponse)
421
- ]
424
+ return self.messages.filter_by_type(
425
+ item_type=AssistantResponse,
426
+ strict_type=True,
427
+ as_pile=True,
422
428
  )
423
429
 
424
430
  @property
425
431
  def actions(self) -> Pile[ActionRequest | ActionResponse]:
426
432
  """All action messages in the manager."""
427
- return Pile(
428
- collections=[
429
- self.messages[mid]
430
- for mid in self.messages.progression
431
- if isinstance(
432
- self.messages[mid], (ActionRequest, ActionResponse)
433
- )
434
- ]
433
+ return self.messages.filter_by_type(
434
+ item_type={ActionRequest, ActionResponse},
435
+ strict_type=True,
436
+ as_pile=True,
435
437
  )
436
438
 
437
439
  @property
438
440
  def action_requests(self) -> Pile[ActionRequest]:
439
441
  """All `ActionRequest` messages in the manager."""
440
- return Pile(
441
- collections=[
442
- self.messages[mid]
443
- for mid in self.messages.progression
444
- if isinstance(self.messages[mid], ActionRequest)
445
- ]
442
+ return self.messages.filter_by_type(
443
+ item_type=ActionRequest,
444
+ strict_type=True,
445
+ as_pile=True,
446
446
  )
447
447
 
448
448
  @property
449
449
  def action_responses(self) -> Pile[ActionResponse]:
450
450
  """All `ActionResponse` messages in the manager."""
451
- return Pile(
452
- collections=[
453
- self.messages[mid]
454
- for mid in self.messages.progression
455
- if isinstance(self.messages[mid], ActionResponse)
456
- ]
451
+ return self.messages.filter_by_type(
452
+ item_type=ActionResponse,
453
+ strict_type=True,
454
+ as_pile=True,
457
455
  )
458
456
 
459
457
  @property
460
458
  def instructions(self) -> Pile[Instruction]:
461
459
  """All `Instruction` messages in the manager."""
462
- return Pile(
463
- collections=[
464
- self.messages[mid]
465
- for mid in self.messages.progression
466
- if isinstance(self.messages[mid], Instruction)
467
- ]
460
+ return self.messages.filter_by_type(
461
+ item_type=Instruction,
462
+ strict_type=True,
463
+ as_pile=True,
468
464
  )
469
465
 
470
466
  def remove_last_instruction_tool_schemas(self) -> None:
@@ -481,7 +477,7 @@ class MessageManager(Manager):
481
477
  Example method to merge the content of recent ActionResponses
482
478
  into an instruction's context.
483
479
  """
484
- for i in reversed(self.messages.progression):
480
+ for i in reversed(list(self.messages.progression)):
485
481
  if isinstance(self.messages[i], ActionResponse):
486
482
  instruction.context.append(self.messages[i].content)
487
483
  else:
@@ -2,36 +2,19 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel
6
6
  from pydantic.fields import FieldInfo
7
7
 
8
8
  from lionagi.fields.action import (
9
9
  ACTION_REQUESTS_FIELD,
10
10
  ACTION_REQUIRED_FIELD,
11
11
  ACTION_RESPONSES_FIELD,
12
- ActionRequestModel,
13
- ActionResponseModel,
14
12
  )
15
- from lionagi.fields.reason import REASON_FIELD, Reason
13
+ from lionagi.fields.reason import REASON_FIELD
16
14
  from lionagi.models import FieldModel, ModelParams
17
15
  from lionagi.protocols.operatives.operative import Operative
18
16
 
19
17
 
20
- class StepModel(BaseModel):
21
- """Model representing a single operational step with optional reasoning and actions."""
22
-
23
- title: str
24
- description: str
25
- reason: Reason | None = Field(**REASON_FIELD.to_dict())
26
- action_requests: list[ActionRequestModel] = Field(
27
- **ACTION_REQUESTS_FIELD.to_dict()
28
- )
29
- action_required: bool = Field(**ACTION_REQUIRED_FIELD.to_dict())
30
- action_responses: list[ActionResponseModel] = Field(
31
- **ACTION_RESPONSES_FIELD.to_dict()
32
- )
33
-
34
-
35
18
  class Step:
36
19
  """Utility class providing methods to create and manage Operative instances for steps."""
37
20
 
@@ -49,7 +49,7 @@ from .messages.manager import (
49
49
  SenderRecipient,
50
50
  System,
51
51
  )
52
- from .operatives.step import Operative, Step, StepModel
52
+ from .operatives.step import Operative, Step
53
53
 
54
54
  __all__ = (
55
55
  "Collective",
@@ -107,7 +107,6 @@ __all__ = (
107
107
  "Report",
108
108
  "Operative",
109
109
  "Step",
110
- "StepModel",
111
110
  "ActionManager",
112
111
  "Tool",
113
112
  "FunctionCalling",
@@ -173,13 +173,12 @@ class ReaderTool(LionTool):
173
173
  system_tool_name = "reader_tool"
174
174
 
175
175
  def __init__(self):
176
- from lionagi.libs.package.imports import check_import
176
+ from lionagi.libs.file.process import _HAS_DOCLING
177
177
 
178
- DocumentConverter = check_import(
179
- "docling",
180
- module_name="document_converter",
181
- import_name="DocumentConverter",
182
- )
178
+ if _HAS_DOCLING is not True:
179
+ raise _HAS_DOCLING
180
+
181
+ from docling.document_converter import DocumentConverter
183
182
 
184
183
  super().__init__()
185
184
  self.converter = DocumentConverter()
lionagi/utils.py CHANGED
@@ -2,18 +2,12 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import asyncio
6
5
  import contextlib
7
6
  import copy as _copy
8
7
  import dataclasses
9
- import functools
10
8
  import importlib.util
11
9
  import json
12
10
  import logging
13
- import re
14
- import shutil
15
- import subprocess
16
- import sys
17
11
  import types
18
12
  import uuid
19
13
  from collections.abc import (
@@ -23,7 +17,6 @@ from collections.abc import (
23
17
  Mapping,
24
18
  Sequence,
25
19
  )
26
- from concurrent.futures import ThreadPoolExecutor
27
20
  from datetime import datetime, timezone
28
21
  from enum import Enum as _Enum
29
22
  from functools import partial
@@ -44,16 +37,10 @@ from pydantic_core import PydanticUndefinedType
44
37
  from typing_extensions import deprecated
45
38
 
46
39
  from .libs.validate.xml_parser import xml_to_dict
47
- from .ln import (
48
- DataClass,
49
- Enum,
50
- KeysDict,
51
- Params,
52
- Undefined,
53
- UndefinedType,
54
- hash_dict,
55
- to_list,
56
- )
40
+ from .ln import DataClass, Enum, KeysDict, Params, Undefined, UndefinedType
41
+ from .ln import extract_json as to_json
42
+ from .ln import fuzzy_json as fuzzy_parse_json
43
+ from .ln import hash_dict, to_list
57
44
  from .ln.concurrency import is_coro_func
58
45
  from .settings import Settings
59
46
 
@@ -99,6 +86,7 @@ __all__ = (
99
86
  "hash_dict",
100
87
  "is_union_type",
101
88
  "union_members",
89
+ "to_json",
102
90
  )
103
91
 
104
92
 
@@ -313,6 +301,9 @@ def lcall(
313
301
  )
314
302
 
315
303
 
304
+ @deprecated(
305
+ "Use `lionagi.ln.alcall` instead, function signature has changed, this will be removed in future versions."
306
+ )
316
307
  async def alcall(
317
308
  input_: list[Any],
318
309
  func: Callable[..., T],
@@ -514,119 +505,6 @@ def create_path(
514
505
  return full_path
515
506
 
516
507
 
517
- def fuzzy_parse_json(
518
- str_to_parse: str, /
519
- ) -> dict[str, Any] | list[dict[str, Any]]:
520
- """
521
- Attempt to parse a JSON string, trying a few minimal "fuzzy" fixes if needed.
522
-
523
- Steps:
524
- 1. Parse directly with json.loads.
525
- 2. Replace single quotes with double quotes, normalize spacing, and try again.
526
- 3. Attempt to fix unmatched brackets using fix_json_string.
527
- 4. If all fail, raise ValueError.
528
-
529
- Args:
530
- str_to_parse: The JSON string to parse
531
-
532
- Returns:
533
- Parsed JSON (dict or list of dicts)
534
-
535
- Raises:
536
- ValueError: If the string cannot be parsed as valid JSON
537
- TypeError: If the input is not a string
538
- """
539
- _check_valid_str(str_to_parse)
540
-
541
- # 1. Direct attempt
542
- with contextlib.suppress(Exception):
543
- return json.loads(str_to_parse)
544
-
545
- # 2. Try cleaning: replace single quotes with double and normalize
546
- cleaned = _clean_json_string(str_to_parse.replace("'", '"'))
547
- with contextlib.suppress(Exception):
548
- return json.loads(cleaned)
549
-
550
- # 3. Try fixing brackets
551
- fixed = fix_json_string(cleaned)
552
- with contextlib.suppress(Exception):
553
- return json.loads(fixed)
554
-
555
- # If all attempts fail
556
- raise ValueError("Invalid JSON string")
557
-
558
-
559
- def _check_valid_str(str_to_parse: str, /):
560
- if not isinstance(str_to_parse, str):
561
- raise TypeError("Input must be a string")
562
- if not str_to_parse.strip():
563
- raise ValueError("Input string is empty")
564
-
565
-
566
- def _clean_json_string(s: str) -> str:
567
- """Basic normalization: replace unescaped single quotes, trim spaces, ensure keys are quoted."""
568
- # Replace unescaped single quotes with double quotes
569
- # '(?<!\\)'" means a single quote not preceded by a backslash
570
- s = re.sub(r"(?<!\\)'", '"', s)
571
- # Collapse multiple whitespaces
572
- s = re.sub(r"\s+", " ", s)
573
- # Ensure keys are quoted
574
- # This attempts to find patterns like { key: value } and turn them into {"key": value}
575
- s = re.sub(r'([{,])\s*([^"\s]+)\s*:', r'\1"\2":', s)
576
- return s.strip()
577
-
578
-
579
- def fix_json_string(str_to_parse: str, /) -> str:
580
- """Try to fix JSON string by ensuring brackets are matched properly."""
581
- if not str_to_parse:
582
- raise ValueError("Input string is empty")
583
-
584
- brackets = {"{": "}", "[": "]"}
585
- open_brackets = []
586
- pos = 0
587
- length = len(str_to_parse)
588
-
589
- while pos < length:
590
- char = str_to_parse[pos]
591
-
592
- if char == "\\":
593
- pos += 2 # Skip escaped chars
594
- continue
595
-
596
- if char == '"':
597
- pos += 1
598
- # skip string content
599
- while pos < length:
600
- if str_to_parse[pos] == "\\":
601
- pos += 2
602
- continue
603
- if str_to_parse[pos] == '"':
604
- pos += 1
605
- break
606
- pos += 1
607
- continue
608
-
609
- if char in brackets:
610
- open_brackets.append(brackets[char])
611
- elif char in brackets.values():
612
- if not open_brackets:
613
- # Extra closing bracket
614
- # Better to raise error than guess
615
- raise ValueError("Extra closing bracket found.")
616
- if open_brackets[-1] != char:
617
- # Mismatched bracket
618
- raise ValueError("Mismatched brackets.")
619
- open_brackets.pop()
620
-
621
- pos += 1
622
-
623
- # Add missing closing brackets if any
624
- if open_brackets:
625
- str_to_parse += "".join(reversed(open_brackets))
626
-
627
- return str_to_parse
628
-
629
-
630
508
  def to_dict(
631
509
  input_: Any,
632
510
  /,
@@ -947,63 +825,6 @@ def _to_dict(
947
825
  return dict(input_)
948
826
 
949
827
 
950
- # Precompile the regex for extracting JSON code blocks
951
- _JSON_BLOCK_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
952
-
953
-
954
- def to_json(
955
- input_data: str | list[str], /, *, fuzzy_parse: bool = False
956
- ) -> dict[str, Any] | list[dict[str, Any]]:
957
- """
958
- Extract and parse JSON content from a string or markdown code blocks.
959
-
960
- Attempts direct JSON parsing first. If that fails, looks for JSON content
961
- within markdown code blocks denoted by ```json.
962
-
963
- Args:
964
- input_data (str | list[str]): The input string or list of strings to parse.
965
- fuzzy_parse (bool): If True, attempts fuzzy JSON parsing on failed attempts.
966
-
967
- Returns:
968
- dict or list of dicts:
969
- - If a single JSON object is found: returns a dict.
970
- - If multiple JSON objects are found: returns a list of dicts.
971
- - If no valid JSON found: returns an empty list.
972
- """
973
-
974
- # If input_data is a list, join into a single string
975
- if isinstance(input_data, list):
976
- input_str = "\n".join(input_data)
977
- else:
978
- input_str = input_data
979
-
980
- # 1. Try direct parsing
981
- try:
982
- if fuzzy_parse:
983
- return fuzzy_parse_json(input_str)
984
- return json.loads(input_str)
985
- except Exception:
986
- pass
987
-
988
- # 2. Attempt extracting JSON blocks from markdown
989
- matches = _JSON_BLOCK_PATTERN.findall(input_str)
990
- if not matches:
991
- return []
992
-
993
- # If only one match, return single dict; if multiple, return list of dicts
994
- if len(matches) == 1:
995
- data_str = matches[0]
996
- return (
997
- fuzzy_parse_json(data_str) if fuzzy_parse else json.loads(data_str)
998
- )
999
-
1000
- # Multiple matches
1001
- if fuzzy_parse:
1002
- return [fuzzy_parse_json(m) for m in matches]
1003
- else:
1004
- return [json.loads(m) for m in matches]
1005
-
1006
-
1007
828
  def get_bins(input_: list[str], upper: int) -> list[list[int]]:
1008
829
  """Organizes indices of strings into bins based on a cumulative upper limit.
1009
830
 
@@ -1030,79 +851,6 @@ def get_bins(input_: list[str], upper: int) -> list[list[int]]:
1030
851
  return bins
1031
852
 
1032
853
 
1033
- def force_async(fn: Callable[..., T]) -> Callable[..., Callable[..., T]]:
1034
- """
1035
- Convert a synchronous function to an asynchronous function
1036
- using a thread pool.
1037
-
1038
- Args:
1039
- fn: The synchronous function to convert.
1040
-
1041
- Returns:
1042
- The asynchronous version of the function.
1043
- """
1044
- pool = ThreadPoolExecutor()
1045
-
1046
- @functools.wraps(fn)
1047
- def wrapper(*args, **kwargs):
1048
- future = pool.submit(fn, *args, **kwargs)
1049
- return asyncio.wrap_future(future) # Make it awaitable
1050
-
1051
- return wrapper
1052
-
1053
-
1054
- def throttle(
1055
- func: Callable[..., T], period: float
1056
- ) -> Callable[..., Callable[..., T]]:
1057
- """
1058
- Throttle function execution to limit the rate of calls.
1059
-
1060
- Args:
1061
- func: The function to throttle.
1062
- period: The minimum time interval between consecutive calls.
1063
-
1064
- Returns:
1065
- The throttled function.
1066
- """
1067
- from .ln.concurrency.throttle import Throttle
1068
-
1069
- if not is_coro_func(func):
1070
- func = force_async(func)
1071
- throttle_instance = Throttle(period)
1072
-
1073
- @functools.wraps(func)
1074
- async def wrapper(*args, **kwargs):
1075
- await throttle_instance(func)(*args, **kwargs)
1076
- return await func(*args, **kwargs)
1077
-
1078
- return wrapper
1079
-
1080
-
1081
- def max_concurrent(
1082
- func: Callable[..., T], limit: int
1083
- ) -> Callable[..., Callable[..., T]]:
1084
- """
1085
- Limit the concurrency of function execution using a semaphore.
1086
-
1087
- Args:
1088
- func: The function to limit concurrency for.
1089
- limit: The maximum number of concurrent executions.
1090
-
1091
- Returns:
1092
- The function wrapped with concurrency control.
1093
- """
1094
- if not is_coro_func(func):
1095
- func = force_async(func)
1096
- semaphore = asyncio.Semaphore(limit)
1097
-
1098
- @functools.wraps(func)
1099
- async def wrapper(*args, **kwargs):
1100
- async with semaphore:
1101
- return await func(*args, **kwargs)
1102
-
1103
- return wrapper
1104
-
1105
-
1106
854
  def breakdown_pydantic_annotation(
1107
855
  model: type[B], max_depth: int | None = None, current_depth: int = 0
1108
856
  ) -> dict[str, Any]:
@@ -1142,87 +890,6 @@ def _is_pydantic_model(x: Any) -> bool:
1142
890
  return False
1143
891
 
1144
892
 
1145
- def run_package_manager_command(
1146
- args: Sequence[str],
1147
- ) -> subprocess.CompletedProcess[bytes]:
1148
- """Run a package manager command, using uv if available, otherwise falling back to pip."""
1149
- # Check if uv is available in PATH
1150
- uv_path = shutil.which("uv")
1151
-
1152
- if uv_path:
1153
- # Use uv if available
1154
- try:
1155
- return subprocess.run(
1156
- [uv_path] + list(args),
1157
- check=True,
1158
- capture_output=True,
1159
- )
1160
- except subprocess.CalledProcessError:
1161
- # If uv fails, fall back to pip
1162
- print("uv command failed, falling back to pip...")
1163
-
1164
- # Fall back to pip
1165
- return subprocess.run(
1166
- [sys.executable, "-m", "pip"] + list(args),
1167
- check=True,
1168
- capture_output=True,
1169
- )
1170
-
1171
-
1172
- def check_import(
1173
- package_name: str,
1174
- module_name: str | None = None,
1175
- import_name: str | None = None,
1176
- pip_name: str | None = None,
1177
- attempt_install: bool = True,
1178
- error_message: str = "",
1179
- ):
1180
- """
1181
- Check if a package is installed, attempt to install if not.
1182
-
1183
- Args:
1184
- package_name: The name of the package to check.
1185
- module_name: The specific module to import (if any).
1186
- import_name: The specific name to import from the module (if any).
1187
- pip_name: The name to use for pip installation (if different).
1188
- attempt_install: Whether to attempt installation if not found.
1189
- error_message: Custom error message to use if package not found.
1190
-
1191
- Raises:
1192
- ImportError: If the package is not found and not installed.
1193
- ValueError: If the import fails after installation attempt.
1194
- """
1195
- if not is_import_installed(package_name):
1196
- if attempt_install:
1197
- logging.info(
1198
- f"Package {package_name} not found. Attempting to install.",
1199
- )
1200
- try:
1201
- return install_import(
1202
- package_name=package_name,
1203
- module_name=module_name,
1204
- import_name=import_name,
1205
- pip_name=pip_name,
1206
- )
1207
- except ImportError as e:
1208
- raise ValueError(
1209
- f"Failed to install {package_name}: {e}"
1210
- ) from e
1211
- else:
1212
- logging.info(
1213
- f"Package {package_name} not found. {error_message}",
1214
- )
1215
- raise ImportError(
1216
- f"Package {package_name} not found. {error_message}",
1217
- )
1218
-
1219
- return import_module(
1220
- package_name=package_name,
1221
- module_name=module_name,
1222
- import_name=import_name,
1223
- )
1224
-
1225
-
1226
893
  def import_module(
1227
894
  package_name: str,
1228
895
  module_name: str = None,
@@ -1267,50 +934,6 @@ def import_module(
1267
934
  ) from e
1268
935
 
1269
936
 
1270
- def install_import(
1271
- package_name: str,
1272
- module_name: str | None = None,
1273
- import_name: str | None = None,
1274
- pip_name: str | None = None,
1275
- ):
1276
- """
1277
- Attempt to import a package, installing it if not found.
1278
-
1279
- Args:
1280
- package_name: The name of the package to import.
1281
- module_name: The specific module to import (if any).
1282
- import_name: The specific name to import from the module (if any).
1283
- pip_name: The name to use for pip installation (if different).
1284
-
1285
- Raises:
1286
- ImportError: If the package cannot be imported or installed.
1287
- subprocess.CalledProcessError: If pip installation fails.
1288
- """
1289
- pip_name = pip_name or package_name
1290
-
1291
- try:
1292
- return import_module(
1293
- package_name=package_name,
1294
- module_name=module_name,
1295
- import_name=import_name,
1296
- )
1297
- except ImportError:
1298
- logging.info(f"Installing {pip_name}...")
1299
- try:
1300
- run_package_manager_command(["install", pip_name])
1301
- return import_module(
1302
- package_name=package_name,
1303
- module_name=module_name,
1304
- import_name=import_name,
1305
- )
1306
- except subprocess.CalledProcessError as e:
1307
- raise ImportError(f"Failed to install {pip_name}: {e}") from e
1308
- except ImportError as e:
1309
- raise ImportError(
1310
- f"Failed to import {pip_name} after installation: {e}"
1311
- ) from e
1312
-
1313
-
1314
937
  def is_import_installed(package_name: str) -> bool:
1315
938
  """
1316
939
  Check if a package is installed.
lionagi/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.15.9"
1
+ __version__ = "0.15.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lionagi
3
- Version: 0.15.9
3
+ Version: 0.15.11
4
4
  Summary: An Intelligence Operating System.
5
5
  Author-email: HaiyangLi <quantocean.li@gmail.com>
6
6
  License: Apache License
@@ -228,7 +228,7 @@ Requires-Dist: json-repair>=0.40.0
228
228
  Requires-Dist: pillow>=11.0.0
229
229
  Requires-Dist: psutil>=6.0.0
230
230
  Requires-Dist: pydantic-settings>=2.8.0
231
- Requires-Dist: pydapter[pandas]>=1.0.0
231
+ Requires-Dist: pydapter[pandas]>=1.0.2
232
232
  Requires-Dist: python-dotenv>=1.1.0
233
233
  Requires-Dist: tiktoken>=0.9.0
234
234
  Requires-Dist: toml>=0.8.0