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,290 +1,331 @@
1
- """Resource management primitives for structured concurrency.
1
+ """Core async primitives (thin wrappers over anyio)"""
2
2
 
3
- Pure async primitives focused on correctness and simplicity.
4
- """
3
+ from __future__ import annotations
5
4
 
6
- import math
7
- from types import TracebackType
5
+ from dataclasses import dataclass
6
+ from typing import Any, Generic, TypeVar
8
7
 
9
8
  import anyio
9
+ import anyio.abc
10
10
 
11
- from .resource_tracker import track_resource, untrack_resource
11
+ T = TypeVar("T")
12
+
13
+
14
+ __all__ = (
15
+ "Lock",
16
+ "Semaphore",
17
+ "CapacityLimiter",
18
+ "Queue",
19
+ "Event",
20
+ "Condition",
21
+ )
12
22
 
13
23
 
14
24
  class Lock:
15
- """A mutex lock for controlling access to a shared resource."""
25
+ """Async mutex lock (anyio.Lock wrapper).
26
+
27
+ Provides mutual exclusion for async code. Use with async context manager
28
+ for automatic acquisition/release.
29
+ """
16
30
 
17
- def __init__(self):
18
- """Initialize a new lock."""
31
+ __slots__ = ("_lock",)
32
+
33
+ def __init__(self) -> None:
19
34
  self._lock = anyio.Lock()
20
- self._acquired = False
21
- track_resource(self, f"Lock-{id(self)}", "Lock")
22
-
23
- def __del__(self):
24
- """Clean up resource tracking when lock is destroyed."""
25
- try:
26
- untrack_resource(self)
27
- except Exception:
28
- pass
29
-
30
- async def __aenter__(self) -> None:
31
- """Acquire the lock."""
32
- await self._lock.acquire()
33
- self._acquired = True
34
-
35
- async def __aexit__(
36
- self,
37
- exc_type: type[BaseException] | None,
38
- exc_val: BaseException | None,
39
- exc_tb: TracebackType | None,
40
- ) -> None:
41
- """Release the lock."""
42
- self._lock.release()
43
- self._acquired = False
44
35
 
45
36
  async def acquire(self) -> None:
46
- """Acquire the lock directly."""
37
+ """Acquire the lock, blocking if necessary."""
47
38
  await self._lock.acquire()
48
- self._acquired = True
49
39
 
50
40
  def release(self) -> None:
51
- """Release the lock directly."""
52
- if not self._acquired:
53
- raise RuntimeError(
54
- "Attempted to release lock that was not acquired by this task"
55
- )
41
+ """Release the lock."""
56
42
  self._lock.release()
57
- self._acquired = False
43
+
44
+ async def __aenter__(self) -> Lock:
45
+ await self.acquire()
46
+ return self
47
+
48
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
49
+ self.release()
58
50
 
59
51
 
60
52
  class Semaphore:
61
- """A semaphore preventing excessive releases."""
53
+ """Async semaphore (anyio.Semaphore wrapper).
54
+
55
+ Limits concurrent access to a resource. Initialized with a count,
56
+ decremented on acquire, incremented on release.
57
+ """
58
+
59
+ __slots__ = ("_sem",)
62
60
 
63
- def __init__(self, initial_value: int):
64
- """Initialize a new semaphore."""
61
+ def __init__(self, initial_value: int) -> None:
65
62
  if initial_value < 0:
66
- raise ValueError("The initial value must be >= 0")
67
- self._initial_value = initial_value
68
- self._current_acquisitions = 0
69
- self._semaphore = anyio.Semaphore(initial_value)
70
- track_resource(self, f"Semaphore-{id(self)}", "Semaphore")
71
-
72
- def __del__(self):
73
- """Clean up resource tracking when semaphore is destroyed."""
74
- try:
75
- untrack_resource(self)
76
- except Exception:
77
- pass
78
-
79
- async def __aenter__(self) -> None:
80
- """Acquire the semaphore."""
63
+ raise ValueError("initial_value must be >= 0")
64
+ self._sem = anyio.Semaphore(initial_value)
65
+
66
+ async def acquire(self) -> None:
67
+ """Acquire a semaphore slot, blocking if none available."""
68
+ await self._sem.acquire()
69
+
70
+ def release(self) -> None:
71
+ """Release a semaphore slot."""
72
+ self._sem.release()
73
+
74
+ async def __aenter__(self) -> Semaphore:
81
75
  await self.acquire()
76
+ return self
82
77
 
83
- async def __aexit__(
84
- self,
85
- exc_type: type[BaseException] | None,
86
- exc_val: BaseException | None,
87
- exc_tb: TracebackType | None,
88
- ) -> None:
89
- """Release the semaphore."""
78
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
90
79
  self.release()
91
80
 
81
+
82
+ class CapacityLimiter:
83
+ """Capacity limiter for controlling resource usage (anyio.CapacityLimiter wrapper).
84
+
85
+ Controls concurrent access to limited resources like threads or connections.
86
+ Key advantages over Semaphore:
87
+ - Supports fractional tokens for fine-grained control
88
+ - Allows dynamic capacity adjustment at runtime
89
+ - Provides delegation methods for resource pooling
90
+ """
91
+
92
+ __slots__ = ("_lim",)
93
+
94
+ def __init__(self, total_tokens: float) -> None:
95
+ """Initialize with given capacity.
96
+
97
+ Args:
98
+ total_tokens: Maximum capacity (must be > 0).
99
+ Can be fractional for fine-grained control.
100
+ """
101
+ if total_tokens <= 0:
102
+ raise ValueError("total_tokens must be > 0")
103
+ self._lim = anyio.CapacityLimiter(total_tokens)
104
+
92
105
  async def acquire(self) -> None:
93
- """Acquire the semaphore."""
94
- await self._semaphore.acquire()
95
- self._current_acquisitions += 1
106
+ """Acquire capacity, blocking if none available."""
107
+ await self._lim.acquire()
96
108
 
97
109
  def release(self) -> None:
98
- """Release the semaphore."""
99
- if self._current_acquisitions <= 0:
100
- raise RuntimeError(
101
- "Cannot release semaphore: no outstanding acquisitions"
102
- )
103
- self._semaphore.release()
104
- self._current_acquisitions -= 1
110
+ """Release capacity."""
111
+ self._lim.release()
105
112
 
106
113
  @property
107
- def current_acquisitions(self) -> int:
108
- """Get the current number of outstanding acquisitions."""
109
- return self._current_acquisitions
114
+ def remaining_tokens(self) -> float:
115
+ """Current available capacity (deprecated, use available_tokens)."""
116
+ return self._lim.available_tokens
110
117
 
111
118
  @property
112
- def initial_value(self) -> int:
113
- """Get the initial semaphore value."""
114
- return self._initial_value
119
+ def total_tokens(self) -> float:
120
+ """Get the current capacity limit."""
121
+ return self._lim.total_tokens
115
122
 
123
+ @total_tokens.setter
124
+ def total_tokens(self, value: float) -> None:
125
+ """Dynamically adjust the capacity limit.
116
126
 
117
- class CapacityLimiter:
118
- """A context manager for limiting the number of concurrent operations."""
119
-
120
- def __init__(self, total_tokens: int | float):
121
- """Initialize a new capacity limiter."""
122
- if total_tokens == math.inf:
123
- processed_tokens = math.inf
124
- elif isinstance(total_tokens, (int, float)) and total_tokens >= 1:
125
- processed_tokens = (
126
- int(total_tokens) if total_tokens != math.inf else math.inf
127
- )
128
- else:
129
- raise ValueError(
130
- "The total number of tokens must be >= 1 (int or math.inf)"
131
- )
132
-
133
- self._limiter = anyio.CapacityLimiter(processed_tokens)
134
- self._borrower_counter = 0
135
- self._active_borrowers = {}
136
- track_resource(self, f"CapacityLimiter-{id(self)}", "CapacityLimiter")
137
-
138
- def __del__(self):
139
- """Clean up resource tracking when limiter is destroyed."""
140
- try:
141
- untrack_resource(self)
142
- except Exception:
143
- pass
144
-
145
- async def __aenter__(self) -> None:
146
- """Acquire a token."""
127
+ Args:
128
+ value: New capacity (must be > 0).
129
+ Can be adjusted at runtime to adapt to load.
130
+ """
131
+ if value <= 0:
132
+ raise ValueError("total_tokens must be > 0")
133
+ self._lim.total_tokens = value
134
+
135
+ @property
136
+ def borrowed_tokens(self) -> float:
137
+ """Get the number of currently borrowed tokens."""
138
+ return self._lim.borrowed_tokens
139
+
140
+ @property
141
+ def available_tokens(self) -> float:
142
+ """Get the number of currently available tokens."""
143
+ return self._lim.available_tokens
144
+
145
+ def acquire_on_behalf_of(self, borrower: object) -> None:
146
+ """Synchronously acquire capacity on behalf of another object.
147
+
148
+ For resource pooling where the acquirer differs from the releaser.
149
+
150
+ Args:
151
+ borrower: Object that will be responsible for releasing.
152
+ """
153
+ self._lim.acquire_on_behalf_of(borrower)
154
+
155
+ def release_on_behalf_of(self, borrower: object) -> None:
156
+ """Release capacity that was acquired on behalf of an object.
157
+
158
+ Args:
159
+ borrower: Object that previously acquired the capacity.
160
+ """
161
+ self._lim.release_on_behalf_of(borrower)
162
+
163
+ # Support idiomatic AnyIO usage: `async with limiter: ...`
164
+ async def __aenter__(self) -> CapacityLimiter:
147
165
  await self.acquire()
166
+ return self
148
167
 
149
- async def __aexit__(
150
- self,
151
- exc_type: type[BaseException] | None,
152
- exc_val: BaseException | None,
153
- exc_tb: TracebackType | None,
154
- ) -> None:
155
- """Release the token."""
168
+ async def __aexit__(self, exc_type, exc, tb) -> None:
156
169
  self.release()
157
170
 
158
- async def acquire(self) -> None:
159
- """Acquire a token."""
160
- # Create a unique borrower identity for each acquisition
161
- self._borrower_counter += 1
162
- borrower = f"borrower-{self._borrower_counter}"
163
- await self._limiter.acquire_on_behalf_of(borrower)
164
- self._active_borrowers[borrower] = True
165
171
 
166
- def release(self) -> None:
167
- """Release a token."""
168
- # Find and release the first active borrower
169
- if not self._active_borrowers:
170
- raise RuntimeError("No tokens to release")
172
+ @dataclass(slots=True)
173
+ class Queue(Generic[T]):
174
+ """Async queue using anyio memory object streams.
171
175
 
172
- borrower = next(iter(self._active_borrowers))
173
- self._limiter.release_on_behalf_of(borrower)
174
- del self._active_borrowers[borrower]
176
+ Provides FIFO queue semantics with optional maxsize for backpressure.
177
+ Must call close() or use async context manager for proper cleanup.
175
178
 
176
- @property
177
- def total_tokens(self) -> int | float:
178
- """The total number of tokens."""
179
- return float(self._limiter.total_tokens)
179
+ Usage:
180
+ queue = Queue.with_maxsize(100)
181
+ await queue.put(item)
182
+ item = await queue.get()
183
+ await queue.close()
184
+ """
180
185
 
181
- @total_tokens.setter
182
- def total_tokens(self, value: int | float) -> None:
183
- """Set the total number of tokens."""
184
- if value == math.inf:
185
- processed_value = math.inf
186
- elif isinstance(value, (int, float)) and value >= 1:
187
- processed_value = int(value) if value != math.inf else math.inf
188
- else:
189
- raise ValueError(
190
- "The total number of tokens must be >= 1 (int or math.inf)"
191
- )
192
-
193
- current_borrowed = self._limiter.borrowed_tokens
194
- if processed_value != math.inf and processed_value < current_borrowed:
195
- raise ValueError(
196
- f"Cannot set total_tokens to {processed_value}: {current_borrowed} tokens "
197
- f"are currently borrowed. Wait for tokens to be released or "
198
- f"set total_tokens to at least {current_borrowed}."
199
- )
200
-
201
- self._limiter.total_tokens = processed_value
186
+ _send: anyio.abc.ObjectSendStream[T]
187
+ _recv: anyio.abc.ObjectReceiveStream[T]
188
+
189
+ @classmethod
190
+ def with_maxsize(cls, maxsize: int) -> Queue[T]:
191
+ """Create queue with maximum buffer size."""
192
+ send, recv = anyio.create_memory_object_stream(maxsize)
193
+ return cls(send, recv)
194
+
195
+ async def put(self, item: T) -> None:
196
+ """Put item into queue. May block if queue is full."""
197
+ await self._send.send(item)
198
+
199
+ def put_nowait(self, item: T) -> None:
200
+ """Put item into queue without blocking.
201
+
202
+ Args:
203
+ item: Item to put in the queue.
204
+
205
+ Raises:
206
+ anyio.WouldBlock: If the queue is full.
207
+ """
208
+ self._send.send_nowait(item)
209
+
210
+ async def get(self) -> T:
211
+ """Get item from queue. Blocks until item available."""
212
+ return await self._recv.receive()
213
+
214
+ def get_nowait(self) -> T:
215
+ """Get item from queue without blocking.
216
+
217
+ Returns:
218
+ Next item from the queue.
219
+
220
+ Raises:
221
+ anyio.WouldBlock: If the queue is empty.
222
+ """
223
+ return self._recv.receive_nowait()
224
+
225
+ async def close(self) -> None:
226
+ """Close both send and receive streams. Call this for cleanup."""
227
+ await self._send.aclose()
228
+ await self._recv.aclose()
229
+
230
+ async def __aenter__(self) -> Queue[T]:
231
+ return self
232
+
233
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
234
+ await self.close()
202
235
 
203
236
  @property
204
- def borrowed_tokens(self) -> int:
205
- """The number of tokens currently borrowed."""
206
- return self._limiter.borrowed_tokens
237
+ def sender(self) -> anyio.abc.ObjectSendStream[T]:
238
+ """Direct access to send stream for advanced usage."""
239
+ return self._send
207
240
 
208
241
  @property
209
- def available_tokens(self) -> int | float:
210
- """The number of tokens currently available."""
211
- return self._limiter.available_tokens
242
+ def receiver(self) -> anyio.abc.ObjectReceiveStream[T]:
243
+ """Direct access to receive stream for advanced usage."""
244
+ return self._recv
212
245
 
213
246
 
214
247
  class Event:
215
- """An event object for task synchronization."""
248
+ """Async event for signaling between tasks (anyio.Event wrapper).
216
249
 
217
- def __init__(self):
218
- """Initialize a new event in the unset state."""
219
- self._event = anyio.Event()
220
- track_resource(self, f"Event-{id(self)}", "Event")
250
+ An event object manages an internal flag that can be set to true
251
+ with set() and reset to false with clear(). The wait() method blocks
252
+ until the flag is true.
253
+ """
221
254
 
222
- def __del__(self):
223
- """Clean up resource tracking when event is destroyed."""
224
- try:
225
- untrack_resource(self)
226
- except Exception:
227
- pass
255
+ __slots__ = ("_event",)
228
256
 
229
- def is_set(self) -> bool:
230
- """Check if the event is set."""
231
- return self._event.is_set()
257
+ def __init__(self) -> None:
258
+ self._event = anyio.Event()
232
259
 
233
260
  def set(self) -> None:
234
- """Set the event, allowing all waiting tasks to proceed."""
261
+ """Set the internal flag to true, waking up all waiting tasks."""
235
262
  self._event.set()
236
263
 
264
+ def is_set(self) -> bool:
265
+ """Return True if the internal flag is set."""
266
+ return self._event.is_set()
267
+
237
268
  async def wait(self) -> None:
238
- """Wait until the event is set."""
269
+ """Block until the internal flag becomes true."""
239
270
  await self._event.wait()
240
271
 
272
+ def statistics(self) -> anyio.EventStatistics:
273
+ """Return statistics about waiting tasks."""
274
+ return self._event.statistics()
275
+
241
276
 
242
277
  class Condition:
243
- """A condition variable for task synchronization."""
244
-
245
- def __init__(self, lock: Lock | None = None):
246
- """Initialize a new condition."""
247
- self._lock = lock or Lock()
248
- self._condition = anyio.Condition(self._lock._lock)
249
- track_resource(self, f"Condition-{id(self)}", "Condition")
250
-
251
- def __del__(self):
252
- """Clean up resource tracking when condition is destroyed."""
253
- try:
254
- untrack_resource(self)
255
- except Exception:
256
- pass
257
-
258
- async def __aenter__(self) -> "Condition":
278
+ """Async condition variable (anyio.Condition wrapper).
279
+
280
+ A condition variable allows one or more coroutines to wait until
281
+ they are notified by another coroutine. Must be used with a Lock.
282
+ """
283
+
284
+ __slots__ = ("_condition",)
285
+
286
+ def __init__(self, lock: Lock | None = None) -> None:
287
+ """Initialize with an optional lock.
288
+
289
+ Args:
290
+ lock: Lock to use. If None, creates a new Lock.
291
+ """
292
+ _lock = lock._lock if lock else None
293
+ self._condition = anyio.Condition(_lock)
294
+
295
+ async def acquire(self) -> None:
259
296
  """Acquire the underlying lock."""
260
- await self._lock.__aenter__()
261
- return self
297
+ await self._condition.acquire()
262
298
 
263
- async def __aexit__(
264
- self,
265
- exc_type: type[BaseException] | None,
266
- exc_val: BaseException | None,
267
- exc_tb: TracebackType | None,
268
- ) -> None:
299
+ def release(self) -> None:
269
300
  """Release the underlying lock."""
270
- await self._lock.__aexit__(exc_type, exc_val, exc_tb)
301
+ self._condition.release()
302
+
303
+ async def __aenter__(self) -> Condition:
304
+ await self.acquire()
305
+ return self
306
+
307
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
308
+ self.release()
271
309
 
272
310
  async def wait(self) -> None:
273
- """Wait for a notification.
311
+ """Wait until notified.
274
312
 
275
- This releases the underlying lock, waits for a notification, and then
276
- reacquires the lock.
313
+ Releases the lock, blocks until notified, then re-acquires the lock.
277
314
  """
278
315
  await self._condition.wait()
279
316
 
280
- async def notify(self, n: int = 1) -> None:
281
- """Notify waiting tasks.
317
+ def notify(self, n: int = 1) -> None:
318
+ """Wake up at most n tasks waiting on this condition.
282
319
 
283
320
  Args:
284
- n: The number of tasks to notify
321
+ n: Maximum number of tasks to wake (default: 1)
285
322
  """
286
- await self._condition.notify(n)
323
+ self._condition.notify(n)
324
+
325
+ def notify_all(self) -> None:
326
+ """Wake up all tasks waiting on this condition."""
327
+ self._condition.notify_all()
287
328
 
288
- async def notify_all(self) -> None:
289
- """Notify all waiting tasks."""
290
- await self._condition.notify_all()
329
+ def statistics(self) -> anyio.abc.ConditionStatistics:
330
+ """Return statistics about waiting tasks."""
331
+ return self._condition.statistics()