lionagi 0.15.13__py3-none-any.whl → 0.16.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 (42) hide show
  1. lionagi/config.py +1 -0
  2. lionagi/libs/validate/fuzzy_match_keys.py +5 -182
  3. lionagi/libs/validate/string_similarity.py +6 -331
  4. lionagi/ln/__init__.py +56 -66
  5. lionagi/ln/_async_call.py +13 -10
  6. lionagi/ln/_hash.py +33 -8
  7. lionagi/ln/_list_call.py +2 -35
  8. lionagi/ln/_to_list.py +51 -28
  9. lionagi/ln/_utils.py +156 -0
  10. lionagi/ln/concurrency/__init__.py +39 -31
  11. lionagi/ln/concurrency/_compat.py +65 -0
  12. lionagi/ln/concurrency/cancel.py +92 -109
  13. lionagi/ln/concurrency/errors.py +17 -17
  14. lionagi/ln/concurrency/patterns.py +249 -206
  15. lionagi/ln/concurrency/primitives.py +257 -216
  16. lionagi/ln/concurrency/resource_tracker.py +42 -155
  17. lionagi/ln/concurrency/task.py +55 -73
  18. lionagi/ln/concurrency/throttle.py +3 -0
  19. lionagi/ln/concurrency/utils.py +1 -0
  20. lionagi/ln/fuzzy/__init__.py +15 -0
  21. lionagi/ln/{_extract_json.py → fuzzy/_extract_json.py} +22 -9
  22. lionagi/ln/{_fuzzy_json.py → fuzzy/_fuzzy_json.py} +14 -8
  23. lionagi/ln/fuzzy/_fuzzy_match.py +172 -0
  24. lionagi/ln/fuzzy/_fuzzy_validate.py +46 -0
  25. lionagi/ln/fuzzy/_string_similarity.py +332 -0
  26. lionagi/ln/{_models.py → types.py} +153 -4
  27. lionagi/operations/flow.py +2 -1
  28. lionagi/operations/operate/operate.py +26 -16
  29. lionagi/protocols/contracts.py +46 -0
  30. lionagi/protocols/generic/event.py +6 -6
  31. lionagi/protocols/generic/processor.py +9 -5
  32. lionagi/protocols/ids.py +82 -0
  33. lionagi/protocols/types.py +10 -12
  34. lionagi/service/connections/match_endpoint.py +9 -0
  35. lionagi/service/connections/providers/nvidia_nim_.py +100 -0
  36. lionagi/utils.py +34 -64
  37. lionagi/version.py +1 -1
  38. {lionagi-0.15.13.dist-info → lionagi-0.16.0.dist-info}/METADATA +4 -2
  39. {lionagi-0.15.13.dist-info → lionagi-0.16.0.dist-info}/RECORD +41 -33
  40. lionagi/ln/_types.py +0 -146
  41. {lionagi-0.15.13.dist-info → lionagi-0.16.0.dist-info}/WHEEL +0 -0
  42. {lionagi-0.15.13.dist-info → lionagi-0.16.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,182 +1,69 @@
1
- """Resource tracking utilities for concurrency primitives.
1
+ """Lightweight resource tracking (debug aid)."""
2
2
 
3
- This module provides lightweight resource leak detection and lifecycle tracking
4
- to address the security vulnerabilities identified in the hardening tests.
5
- """
3
+ from __future__ import annotations
6
4
 
5
+ import threading
6
+ import time
7
7
  import weakref
8
8
  from dataclasses import dataclass
9
- from datetime import datetime
10
- from typing import Any
11
9
 
10
+ __all__ = (
11
+ "track_resource",
12
+ "untrack_resource",
13
+ "LeakInfo",
14
+ "LeakTracker",
15
+ )
12
16
 
13
- @dataclass
14
- class ResourceInfo:
15
- """Information about a tracked resource."""
16
17
 
18
+ @dataclass(frozen=True, slots=True)
19
+ class LeakInfo:
17
20
  name: str
18
- creation_time: datetime
19
- resource_type: str
21
+ kind: str | None
22
+ created_at: float
20
23
 
21
24
 
22
- class ResourceTracker:
23
- """Lightweight resource lifecycle tracking for leak detection.
24
-
25
- This addresses the over-engineering concerns by providing simple,
26
- practical resource management without complex abstraction layers.
27
- """
28
-
29
- def __init__(self):
30
- """Initialize a new resource tracker."""
31
- self._active_resources: dict[int, ResourceInfo] = {}
32
- self._weak_refs: weakref.WeakKeyDictionary = (
33
- weakref.WeakKeyDictionary()
34
- )
25
+ class LeakTracker:
26
+ def __init__(self) -> None:
27
+ self._live: dict[int, LeakInfo] = {}
28
+ self._lock = threading.Lock()
35
29
 
36
30
  def track(
37
- self, resource: Any, name: str, resource_type: str | None = None
31
+ self, obj: object, *, name: str | None, kind: str | None
38
32
  ) -> None:
39
- """Track a resource for leak detection.
40
-
41
- Args:
42
- resource: The resource to track
43
- name: Human-readable name for the resource
44
- resource_type: Optional type classification
45
- """
46
- if resource_type is None:
47
- resource_type = type(resource).__name__
48
-
49
- resource_info = ResourceInfo(
50
- name=name,
51
- creation_time=datetime.now(),
52
- resource_type=resource_type,
33
+ info = LeakInfo(
34
+ name=name or f"obj-{id(obj)}", kind=kind, created_at=time.time()
53
35
  )
36
+ key = id(obj)
54
37
 
55
- # Use weak reference to avoid interfering with garbage collection
56
- self._weak_refs[resource] = resource_info
57
- self._active_resources[id(resource)] = resource_info
58
-
59
- def untrack(self, resource: Any) -> None:
60
- """Manually untrack a resource.
61
-
62
- Args:
63
- resource: The resource to stop tracking
64
- """
65
- resource_id = id(resource)
66
- self._active_resources.pop(resource_id, None)
67
- self._weak_refs.pop(resource, None)
68
-
69
- def cleanup_check(self) -> list[ResourceInfo]:
70
- """Check for potentially leaked resources.
38
+ def _finalizer(_key: int = key) -> None:
39
+ with self._lock:
40
+ self._live.pop(_key, None)
71
41
 
72
- Returns:
73
- List of resource info for resources that may have leaked
74
- """
75
- # Clean up references to garbage collected objects
76
- current_resources = []
77
- for resource, info in list(self._weak_refs.items()):
78
- current_resources.append(info)
42
+ with self._lock:
43
+ self._live[key] = info
44
+ weakref.finalize(obj, _finalizer)
79
45
 
80
- return current_resources
46
+ def untrack(self, obj: object) -> None:
47
+ with self._lock:
48
+ self._live.pop(id(obj), None)
81
49
 
82
- def get_active_count(self) -> int:
83
- """Get the number of currently tracked resources.
50
+ def live(self) -> list[LeakInfo]:
51
+ with self._lock:
52
+ return list(self._live.values())
84
53
 
85
- Returns:
86
- Number of active tracked resources
87
- """
88
- return len(self._weak_refs)
54
+ def clear(self) -> None:
55
+ with self._lock:
56
+ self._live.clear()
89
57
 
90
- def get_resource_summary(self) -> dict[str, int]:
91
- """Get a summary of tracked resources by type.
92
58
 
93
- Returns:
94
- Dictionary mapping resource types to counts
95
- """
96
- summary = {}
97
- for info in self._weak_refs.values():
98
- resource_type = info.resource_type
99
- summary[resource_type] = summary.get(resource_type, 0) + 1
100
- return summary
101
-
102
-
103
- # Global tracker instance for convenience
104
- _global_tracker = ResourceTracker()
59
+ _TRACKER = LeakTracker()
105
60
 
106
61
 
107
62
  def track_resource(
108
- resource: Any, name: str, resource_type: str | None = None
63
+ obj: object, name: str | None = None, kind: str | None = None
109
64
  ) -> None:
110
- """Track a resource using the global tracker.
111
-
112
- Args:
113
- resource: The resource to track
114
- name: Human-readable name for the resource
115
- resource_type: Optional type classification
116
- """
117
- _global_tracker.track(resource, name, resource_type)
118
-
119
-
120
- def untrack_resource(resource: Any) -> None:
121
- """Untrack a resource using the global tracker.
122
-
123
- Args:
124
- resource: The resource to stop tracking
125
- """
126
- _global_tracker.untrack(resource)
127
-
128
-
129
- def get_global_tracker() -> ResourceTracker:
130
- """Get the global resource tracker instance.
131
-
132
- Returns:
133
- The global ResourceTracker instance
134
- """
135
- return _global_tracker
136
-
137
-
138
- def cleanup_check() -> list[ResourceInfo]:
139
- """Check for potentially leaked resources using global tracker.
140
-
141
- Returns:
142
- List of resource info for resources that may have leaked
143
- """
144
- return _global_tracker.cleanup_check()
145
-
146
-
147
- class resource_leak_detector:
148
- """Context manager for resource leak detection in tests and production.
149
-
150
- Example:
151
- async with resource_leak_detector() as tracker:
152
- lock = Lock()
153
- tracker.track(lock, "test_lock")
154
- # ... use lock
155
- # Automatically checks for leaks on exit
156
- """
157
-
158
- def __init__(self, raise_on_leak: bool = False):
159
- """Initialize the leak detector.
160
-
161
- Args:
162
- raise_on_leak: Whether to raise an exception if leaks are detected
163
- """
164
- self.raise_on_leak = raise_on_leak
165
- self.tracker = ResourceTracker()
166
- self._initial_count = 0
167
-
168
- async def __aenter__(self) -> ResourceTracker:
169
- """Enter the context and return the tracker."""
170
- self._initial_count = self.tracker.get_active_count()
171
- return self.tracker
65
+ _TRACKER.track(obj, name=name, kind=kind)
172
66
 
173
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
174
- """Exit the context and check for leaks."""
175
- leaked_resources = self.tracker.cleanup_check()
176
67
 
177
- if leaked_resources and self.raise_on_leak:
178
- resource_summary = self.tracker.get_resource_summary()
179
- raise RuntimeError(
180
- f"Resource leak detected: {len(leaked_resources)} resources "
181
- f"still active. Summary: {resource_summary}"
182
- )
68
+ def untrack_resource(obj: object) -> None:
69
+ _TRACKER.untrack(obj)
@@ -1,9 +1,9 @@
1
- """Task group implementation for structured concurrency."""
1
+ """Task group wrapper (thin facade over anyio.create_task_group)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Awaitable, Callable
6
- from types import TracebackType
5
+ from collections.abc import AsyncIterator, Awaitable, Callable
6
+ from contextlib import asynccontextmanager
7
7
  from typing import Any, TypeVar
8
8
 
9
9
  import anyio
@@ -11,33 +11,55 @@ import anyio
11
11
  T = TypeVar("T")
12
12
  R = TypeVar("R")
13
13
 
14
+ __all__ = (
15
+ "TaskGroup",
16
+ "create_task_group",
17
+ )
18
+
14
19
 
15
20
  class TaskGroup:
16
- """A group of tasks that are treated as a unit."""
21
+ """Structured concurrency task group (anyio.abc.TaskGroup wrapper).
22
+
23
+ Manages a group of concurrent tasks with structured lifecycle.
24
+ If any task fails, all other tasks in the group are cancelled.
25
+
26
+ Note: Lifecycle is managed by the create_task_group() context manager.
27
+ Do not instantiate directly.
28
+
29
+ Usage:
30
+ async with create_task_group() as tg:
31
+ tg.start_soon(worker_task, arg1)
32
+ tg.start_soon(worker_task, arg2)
33
+ # All tasks complete before exiting context
34
+ """
35
+
36
+ __slots__ = ("_tg",)
17
37
 
18
- def __init__(self):
19
- """Initialize a new task group."""
20
- self._task_group = None
38
+ def __init__(self, tg: anyio.abc.TaskGroup) -> None:
39
+ self._tg = tg
21
40
 
22
- async def start_soon(
41
+ @property
42
+ def cancel_scope(self) -> anyio.CancelScope:
43
+ """Cancel scope controlling this task group's lifetime.
44
+
45
+ Use this to cancel all tasks: tg.cancel_scope.cancel()
46
+ """
47
+ return self._tg.cancel_scope
48
+
49
+ def start_soon(
23
50
  self,
24
51
  func: Callable[..., Awaitable[Any]],
25
52
  *args: Any,
26
53
  name: str | None = None,
27
54
  ) -> None:
28
- """Start a new task in this task group.
55
+ """Start a task without waiting for it to initialize.
29
56
 
30
57
  Args:
31
- func: The coroutine function to call
32
- *args: Positional arguments to pass to the function
33
- name: Optional name for the task
34
-
35
- Note:
36
- This method does not wait for the task to initialize.
58
+ func: Async function to run as a task
59
+ *args: Arguments to pass to the function
60
+ name: Optional name for the task (for debugging)
37
61
  """
38
- if self._task_group is None:
39
- raise RuntimeError("Task group is not active")
40
- self._task_group.start_soon(func, *args, name=name)
62
+ self._tg.start_soon(func, *args, name=name)
41
63
 
42
64
  async def start(
43
65
  self,
@@ -45,67 +67,27 @@ class TaskGroup:
45
67
  *args: Any,
46
68
  name: str | None = None,
47
69
  ) -> R:
48
- """Start a new task and wait for it to initialize.
49
-
50
- Args:
51
- func: The coroutine function to call
52
- *args: Positional arguments to pass to the function
53
- name: Optional name for the task
70
+ """Start a task and wait for it to initialize.
54
71
 
55
- Returns:
56
- The value passed to task_status.started()
57
-
58
- Note:
59
- The function must accept a task_status keyword argument and call
60
- task_status.started() once initialization is complete.
61
- """
62
- if self._task_group is None:
63
- raise RuntimeError("Task group is not active")
64
- return await self._task_group.start(func, *args, name=name)
65
-
66
- async def __aenter__(self) -> TaskGroup:
67
- """Enter the task group context.
68
-
69
- Returns:
70
- The task group instance.
71
- """
72
- task_group = anyio.create_task_group()
73
- self._task_group = await task_group.__aenter__()
74
- return self
72
+ The task function should use task_status.started() to signal initialization.
75
73
 
76
- async def __aexit__(
77
- self,
78
- exc_type: type[BaseException] | None,
79
- exc_val: BaseException | None,
80
- exc_tb: TracebackType | None,
81
- ) -> bool:
82
- """Exit the task group context.
83
-
84
- This will wait for all tasks in the group to complete.
85
- If any task raised an exception, it will be propagated.
86
- If multiple tasks raised exceptions, they will be combined into an ExceptionGroup.
74
+ Args:
75
+ func: Async function to run as a task
76
+ *args: Arguments to pass to the function
77
+ name: Optional name for the task (for debugging)
87
78
 
88
79
  Returns:
89
- True if the exception was handled, False otherwise.
80
+ Value passed to task_status.started() by the task
90
81
  """
91
- if self._task_group is None:
92
- return False
82
+ return await self._tg.start(func, *args, name=name)
93
83
 
94
- try:
95
- return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
96
- finally:
97
- self._task_group = None
98
84
 
85
+ @asynccontextmanager
86
+ async def create_task_group() -> AsyncIterator[TaskGroup]:
87
+ """Create a new task group for structured concurrency.
99
88
 
100
- def create_task_group() -> TaskGroup:
101
- """Create a new task group.
102
-
103
- Returns:
104
- A new task group instance.
105
-
106
- Example:
107
- async with create_task_group() as tg:
108
- await tg.start_soon(task1)
109
- await tg.start_soon(task2)
89
+ Returns an async context manager that yields a TaskGroup.
90
+ All tasks started within the group complete before the context exits.
110
91
  """
111
- return TaskGroup()
92
+ async with anyio.create_task_group() as tg:
93
+ yield TaskGroup(tg)
@@ -6,12 +6,15 @@ from collections.abc import Callable
6
6
  from time import sleep, time
7
7
  from typing import Any, TypeVar
8
8
 
9
+ from typing_extensions import deprecated
10
+
9
11
  T = TypeVar("T")
10
12
 
11
13
 
12
14
  __all__ = ("Throttle",)
13
15
 
14
16
 
17
+ @deprecated("Throttle is deprecated and will be removed in a future release.")
15
18
  class Throttle:
16
19
  """
17
20
  Provide a throttling mechanism for function calls.
@@ -12,4 +12,5 @@ def _is_coro_func(func: Callable[..., Any]) -> bool:
12
12
 
13
13
 
14
14
  def is_coro_func(func: Callable[..., Any]) -> bool:
15
+ """Check if a function is a coroutine function, with caching for performance."""
15
16
  return _is_coro_func(func)
@@ -0,0 +1,15 @@
1
+ from ._extract_json import extract_json
2
+ from ._fuzzy_json import fuzzy_json
3
+ from ._fuzzy_match import FuzzyMatchKeysParams, fuzzy_match_keys
4
+ from ._fuzzy_validate import fuzzy_validate_pydantic
5
+ from ._string_similarity import SIMILARITY_TYPE, string_similarity
6
+
7
+ __all__ = (
8
+ "fuzzy_json",
9
+ "fuzzy_match_keys",
10
+ "extract_json",
11
+ "string_similarity",
12
+ "SIMILARITY_TYPE",
13
+ "fuzzy_validate_pydantic",
14
+ "FuzzyMatchKeysParams",
15
+ )
@@ -1,13 +1,16 @@
1
1
  import re
2
2
  from typing import Any
3
3
 
4
- import orjson
4
+ import msgspec
5
5
 
6
6
  from ._fuzzy_json import fuzzy_json
7
7
 
8
8
  # Precompile the regex for extracting JSON code blocks
9
9
  _JSON_BLOCK_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
10
10
 
11
+ # Initialize a decoder for efficiency
12
+ _decoder = msgspec.json.Decoder(type=Any)
13
+
11
14
 
12
15
  def extract_json(
13
16
  input_data: str | list[str],
@@ -37,7 +40,7 @@ def extract_json(
37
40
  try:
38
41
  if fuzzy_parse:
39
42
  return fuzzy_json(input_str)
40
- return orjson.loads(input_str)
43
+ return _decoder.decode(input_str.encode("utf-8"))
41
44
  except Exception:
42
45
  pass
43
46
 
@@ -49,12 +52,22 @@ def extract_json(
49
52
  # If only one match, return single dict; if multiple, return list of dicts
50
53
  if return_one_if_single and len(matches) == 1:
51
54
  data_str = matches[0]
52
- if fuzzy_parse:
53
- return fuzzy_json(data_str)
54
- return orjson.loads(data_str)
55
+ try:
56
+ if fuzzy_parse:
57
+ return fuzzy_json(data_str)
58
+ return _decoder.decode(data_str.encode("utf-8"))
59
+ except Exception:
60
+ return []
55
61
 
56
62
  # 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]
63
+ results = []
64
+ for m in matches:
65
+ try:
66
+ if fuzzy_parse:
67
+ results.append(fuzzy_json(m))
68
+ else:
69
+ results.append(_decoder.decode(m.encode("utf-8")))
70
+ except Exception:
71
+ # Skip invalid JSON blocks
72
+ continue
73
+ return results
@@ -2,7 +2,9 @@ import contextlib
2
2
  import re
3
3
  from typing import Any
4
4
 
5
- import orjson
5
+ import msgspec
6
+
7
+ _decoder = msgspec.json.Decoder(type=Any)
6
8
 
7
9
 
8
10
  def fuzzy_json(str_to_parse: str, /) -> dict[str, Any] | list[dict[str, Any]]:
@@ -10,7 +12,7 @@ def fuzzy_json(str_to_parse: str, /) -> dict[str, Any] | list[dict[str, Any]]:
10
12
  Attempt to parse a JSON string, trying a few minimal "fuzzy" fixes if needed.
11
13
 
12
14
  Steps:
13
- 1. Parse directly with json.loads.
15
+ 1. Parse directly with msgspec.
14
16
  2. Replace single quotes with double quotes, normalize spacing, and try again.
15
17
  3. Attempt to fix unmatched brackets using fix_json_string.
16
18
  4. If all fail, raise ValueError.
@@ -27,19 +29,21 @@ def fuzzy_json(str_to_parse: str, /) -> dict[str, Any] | list[dict[str, Any]]:
27
29
  """
28
30
  _check_valid_str(str_to_parse)
29
31
 
32
+ # Use .encode("utf-8") as msgspec typically works with bytes for optimal performance
33
+
30
34
  # 1. Direct attempt
31
- with contextlib.suppress(Exception):
32
- return orjson.loads(str_to_parse)
35
+ with contextlib.suppress(msgspec.DecodeError):
36
+ return _decoder.decode(str_to_parse.encode("utf-8"))
33
37
 
34
38
  # 2. Try cleaning: replace single quotes with double and normalize
35
39
  cleaned = _clean_json_string(str_to_parse.replace("'", '"'))
36
- with contextlib.suppress(Exception):
37
- return orjson.loads(cleaned)
40
+ with contextlib.suppress(msgspec.DecodeError):
41
+ return _decoder.decode(cleaned.encode("utf-8"))
38
42
 
39
43
  # 3. Try fixing brackets
40
44
  fixed = fix_json_string(cleaned)
41
- with contextlib.suppress(Exception):
42
- return orjson.loads(fixed)
45
+ with contextlib.suppress(msgspec.DecodeError):
46
+ return _decoder.decode(fixed.encode("utf-8"))
43
47
 
44
48
  # If all attempts fail
45
49
  raise ValueError("Invalid JSON string")
@@ -59,6 +63,8 @@ def _clean_json_string(s: str) -> str:
59
63
  s = re.sub(r"(?<!\\)'", '"', s)
60
64
  # Collapse multiple whitespaces
61
65
  s = re.sub(r"\s+", " ", s)
66
+ # Remove trailing commas before closing brackets/braces
67
+ s = re.sub(r",\s*([}\]])", r"\1", s)
62
68
  # Ensure keys are quoted
63
69
  # This attempts to find patterns like { key: value } and turn them into {"key": value}
64
70
  s = re.sub(r'([{,])\s*([^"\s]+)\s*:', r'\1"\2":', s)