lionagi 0.15.9__py3-none-any.whl → 0.15.13__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/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.13"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lionagi
3
- Version: 0.15.9
3
+ Version: 0.15.13
4
4
  Summary: An Intelligence Operating System.
5
5
  Author-email: HaiyangLi <quantocean.li@gmail.com>
6
6
  License: Apache License
@@ -225,10 +225,10 @@ Requires-Dist: anyio>=4.7.0
225
225
  Requires-Dist: backoff>=2.0.0
226
226
  Requires-Dist: jinja2>=3.0.0
227
227
  Requires-Dist: json-repair>=0.40.0
228
- Requires-Dist: pillow>=11.0.0
228
+ Requires-Dist: pillow>=10.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.4
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
@@ -274,13 +274,13 @@ Description-Content-Type: text/markdown
274
274
  ![PyPI - Downloads](https://img.shields.io/pypi/dm/lionagi?color=blue)
275
275
  ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)
276
276
 
277
- [Documentation](https://lion-agi.github.io/lionagi/) |
277
+ [Documentation](https://khive-ai.github.io/lionagi/) |
278
278
  [Discord](https://discord.gg/JDj9ENhUE8) |
279
279
  [PyPI](https://pypi.org/project/lionagi/)
280
280
 
281
281
  # LION - Language InterOperable Network
282
282
 
283
- ## An Agentic Intelligence SDK
283
+ ## An AGentic Intelligence SDK
284
284
 
285
285
  LionAGI is a robust framework for orchestrating multi-step AI operations with
286
286
  precise control. Bring together multiple models, advanced ReAct reasoning, tool
@@ -288,13 +288,13 @@ integrations, and custom validations in a single coherent pipeline.
288
288
 
289
289
  ## Why LionAGI?
290
290
 
291
- - **Structured**: LLM interactions are validated and typed (via Pydantic).
291
+ - **Structured**: Validate and type all LLM interactions with Pydantic.
292
292
  - **Expandable**: Integrate multiple providers (OpenAI, Anthropic, Perplexity,
293
293
  custom) with minimal friction.
294
- - **Controlled**: Built-in safety checks, concurrency strategies, and advanced
295
- multi-step flowslike ReAct with verbose outputs.
296
- - **Transparent**: Real-time logging, message introspection, and easy debugging
297
- of tool usage.
294
+ - **Controlled**: Use built-in safety checks, concurrency strategies, and advanced
295
+ multi-step flows like ReAct.
296
+ - **Transparent**: Debug easily with real-time logging, message introspection, and
297
+ tool usage tracking.
298
298
 
299
299
  ## Installation
300
300
 
@@ -310,12 +310,12 @@ pip install lionagi # or install directly
310
310
  from lionagi import Branch, iModel
311
311
 
312
312
  # Pick a model
313
- gpt41 = iModel(provider="openai", model="gpt-4.1-mini")
313
+ gpt4o = iModel(provider="openai", model="gpt-4o-mini")
314
314
 
315
315
  # Create a Branch (conversation context)
316
316
  hunter = Branch(
317
317
  system="you are a hilarious dragon hunter who responds in 10 words rhymes.",
318
- chat_model=gpt41,
318
+ chat_model=gpt4o,
319
319
  )
320
320
 
321
321
  # Communicate asynchronously
@@ -341,8 +341,8 @@ res = await hunter.communicate(
341
341
  "Tell me a short dragon joke",
342
342
  response_format=Joke
343
343
  )
344
- print(type(response))
345
- print(response.joke)
344
+ print(type(res))
345
+ print(res.joke)
346
346
  ```
347
347
 
348
348
  ```
@@ -362,7 +362,10 @@ pip install "lionagi[reader]"
362
362
  ```python
363
363
  from lionagi.tools.types import ReaderTool
364
364
 
365
- branch = Branch(chat_model=gpt4o, tools=ReaderTool)
365
+ # Define model first
366
+ gpt4o = iModel(provider="openai", model="gpt-4o-mini")
367
+
368
+ branch = Branch(chat_model=gpt4o, tools=[ReaderTool])
366
369
  result = await branch.ReAct(
367
370
  instruct={
368
371
  "instruction": "Summarize my PDF and compare with relevant papers.",
@@ -396,13 +399,15 @@ print(df.tail())
396
399
  ```python
397
400
  from lionagi import Branch, iModel
398
401
 
402
+ # Define models for multi-model orchestration
403
+ gpt4o = iModel(provider="openai", model="gpt-4o-mini")
399
404
  sonnet = iModel(
400
405
  provider="anthropic",
401
406
  model="claude-3-5-sonnet-20241022",
402
407
  max_tokens=1000, # max_tokens is required for anthropic models
403
408
  )
404
409
 
405
- branch = Branch(chat_model=gpt41)
410
+ branch = Branch(chat_model=gpt4o)
406
411
  analysis = await branch.communicate("Analyze these stats", chat_model=sonnet) # Switch mid-flow
407
412
  ```
408
413