taskiq-llm-queue 0.1.0a1__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 (65) hide show
  1. taskiq_llm_queue/__init__.py +138 -0
  2. taskiq_llm_queue/_logging.py +40 -0
  3. taskiq_llm_queue/_version.py +5 -0
  4. taskiq_llm_queue/broker/__init__.py +86 -0
  5. taskiq_llm_queue/broker/acker.py +239 -0
  6. taskiq_llm_queue/broker/batching.py +188 -0
  7. taskiq_llm_queue/broker/broker.py +937 -0
  8. taskiq_llm_queue/broker/delivery.py +50 -0
  9. taskiq_llm_queue/broker/errors.py +178 -0
  10. taskiq_llm_queue/broker/events.py +51 -0
  11. taskiq_llm_queue/broker/filters.py +32 -0
  12. taskiq_llm_queue/broker/heartbeat.py +157 -0
  13. taskiq_llm_queue/broker/queue.py +141 -0
  14. taskiq_llm_queue/broker/s3_offload.py +346 -0
  15. taskiq_llm_queue/broker/scheduler.py +113 -0
  16. taskiq_llm_queue/broker/serializer.py +32 -0
  17. taskiq_llm_queue/cli.py +488 -0
  18. taskiq_llm_queue/client.py +1409 -0
  19. taskiq_llm_queue/config.py +592 -0
  20. taskiq_llm_queue/context.py +62 -0
  21. taskiq_llm_queue/core.py +832 -0
  22. taskiq_llm_queue/envelope.py +136 -0
  23. taskiq_llm_queue/errors.py +71 -0
  24. taskiq_llm_queue/failover.py +286 -0
  25. taskiq_llm_queue/http_transport.py +220 -0
  26. taskiq_llm_queue/keys.py +87 -0
  27. taskiq_llm_queue/langchain/__init__.py +64 -0
  28. taskiq_llm_queue/langchain/_client.py +142 -0
  29. taskiq_llm_queue/langchain/bedrock.py +291 -0
  30. taskiq_llm_queue/langchain/openai.py +103 -0
  31. taskiq_llm_queue/lifecycle.py +512 -0
  32. taskiq_llm_queue/metrics/__init__.py +45 -0
  33. taskiq_llm_queue/metrics/emf.py +100 -0
  34. taskiq_llm_queue/metrics/prometheus.py +66 -0
  35. taskiq_llm_queue/metrics_names.py +19 -0
  36. taskiq_llm_queue/protocols.py +140 -0
  37. taskiq_llm_queue/providers/__init__.py +62 -0
  38. taskiq_llm_queue/providers/bedrock.py +188 -0
  39. taskiq_llm_queue/providers/classify.py +251 -0
  40. taskiq_llm_queue/providers/embeddings.py +87 -0
  41. taskiq_llm_queue/providers/hosts.py +34 -0
  42. taskiq_llm_queue/providers/openai_compat.py +142 -0
  43. taskiq_llm_queue/providers/registry.py +173 -0
  44. taskiq_llm_queue/py.typed +0 -0
  45. taskiq_llm_queue/reliability/__init__.py +59 -0
  46. taskiq_llm_queue/reliability/_lua.py +461 -0
  47. taskiq_llm_queue/reliability/breaker.py +86 -0
  48. taskiq_llm_queue/reliability/claims.py +226 -0
  49. taskiq_llm_queue/reliability/config_store.py +148 -0
  50. taskiq_llm_queue/reliability/fallback.py +111 -0
  51. taskiq_llm_queue/reliability/limiter.py +482 -0
  52. taskiq_llm_queue/reliability/local_limiter.py +153 -0
  53. taskiq_llm_queue/results/__init__.py +25 -0
  54. taskiq_llm_queue/results/backend.py +226 -0
  55. taskiq_llm_queue/results/dispatcher.py +366 -0
  56. taskiq_llm_queue/results/s3_backend.py +105 -0
  57. taskiq_llm_queue/routing.py +173 -0
  58. taskiq_llm_queue/routing_hooks.py +90 -0
  59. taskiq_llm_queue/tasks.py +453 -0
  60. taskiq_llm_queue-0.1.0a1.dist-info/METADATA +296 -0
  61. taskiq_llm_queue-0.1.0a1.dist-info/RECORD +65 -0
  62. taskiq_llm_queue-0.1.0a1.dist-info/WHEEL +4 -0
  63. taskiq_llm_queue-0.1.0a1.dist-info/entry_points.txt +2 -0
  64. taskiq_llm_queue-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  65. taskiq_llm_queue-0.1.0a1.dist-info/licenses/NOTICE +15 -0
@@ -0,0 +1,188 @@
1
+ """Micro-batching for SQS batch APIs: send, delete (ack) and change-visibility (heartbeat).
2
+
3
+ Items are grouped until `max_items` or `max_bytes` is reached or the oldest item has waited
4
+ `max_delay_secs`, then handed to one flush call. Batches of one batcher are flushed one at a
5
+ time and in order, which keeps FIFO group order for sends (P8, A6, DS7, DS15).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import contextlib
12
+ from collections import deque
13
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
14
+ from dataclasses import dataclass
15
+ from typing import Any, Generic, TypeVar
16
+
17
+ from taskiq_llm_queue._logging import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+ T = TypeVar("T")
22
+ R = TypeVar("R")
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class _Pending(Generic[T, R]):
27
+ item: T
28
+ size: int
29
+ future: asyncio.Future[R]
30
+ queued_at: float
31
+
32
+
33
+ class MicroBatcher(Generic[T, R]):
34
+ """Collects items and flushes them in bounded batches through one async callable.
35
+
36
+ `flush` receives the items of one batch and returns one result per item, in order; an
37
+ item whose result is a `BaseException` fails its future with it. Results are always
38
+ retrieved, so fire-and-forget callers (acks) never log "exception never retrieved".
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ flush: Callable[[Sequence[T]], Awaitable[Sequence[R | BaseException]]],
44
+ *,
45
+ max_items: int = 10,
46
+ max_bytes: int | None = None,
47
+ size_of: Callable[[T], int] | None = None,
48
+ max_delay_secs: float = 0.5,
49
+ name: str = "batcher",
50
+ ) -> None:
51
+ """Configure limits; the background flush task starts on the first `submit`."""
52
+ self._flush = flush
53
+ self._max_items = max_items
54
+ self._max_bytes = max_bytes
55
+ self._size_of = size_of
56
+ self._max_delay_secs = max_delay_secs
57
+ self._name = name
58
+ self._pending: deque[_Pending[T, R]] = deque()
59
+ self._pending_bytes = 0
60
+ self._wake = asyncio.Event()
61
+ self._task: asyncio.Task[None] | None = None
62
+ self._closing = False
63
+ self._force = False
64
+ self._in_flight = 0
65
+ self._idle = asyncio.Event()
66
+ self._idle.set()
67
+
68
+ def __len__(self) -> int:
69
+ return len(self._pending) + self._in_flight
70
+
71
+ def submit(self, item: T) -> asyncio.Future[R]:
72
+ """Queue one item and return a future for its result; never blocks."""
73
+ if self._closing:
74
+ raise RuntimeError(f"{self._name} is closed")
75
+ loop = asyncio.get_running_loop()
76
+ future: asyncio.Future[R] = loop.create_future()
77
+ future.add_done_callback(_retrieve)
78
+ size = self._size_of(item) if self._size_of else 0
79
+ self._pending.append(_Pending(item, size, future, loop.time()))
80
+ self._pending_bytes += size
81
+ self._idle.clear()
82
+ if self._task is None or self._task.done():
83
+ self._task = asyncio.create_task(self._run(), name=f"llmq-{self._name}")
84
+ self._wake.set()
85
+ return future
86
+
87
+ async def flush_now(self) -> None:
88
+ """Send everything queued so far and wait until those batches are done."""
89
+ if self._idle.is_set():
90
+ return
91
+ self._force = True
92
+ self._wake.set()
93
+ try:
94
+ await self._idle.wait()
95
+ finally:
96
+ self._force = False
97
+
98
+ async def close(self) -> None:
99
+ """Flush what is queued, then stop the background task; idempotent."""
100
+ self._closing = True
101
+ await self.flush_now()
102
+ if self._task is not None:
103
+ self._task.cancel()
104
+ with contextlib.suppress(asyncio.CancelledError):
105
+ await self._task
106
+ self._task = None
107
+
108
+ def _full(self) -> bool:
109
+ return len(self._pending) >= self._max_items or (
110
+ self._max_bytes is not None and self._pending_bytes >= self._max_bytes
111
+ )
112
+
113
+ def _take(self) -> list[_Pending[T, R]]:
114
+ batch: list[_Pending[T, R]] = []
115
+ total = 0
116
+ while self._pending and len(batch) < self._max_items:
117
+ nxt = self._pending[0]
118
+ if batch and self._max_bytes is not None and total + nxt.size > self._max_bytes:
119
+ break
120
+ batch.append(self._pending.popleft())
121
+ total += nxt.size
122
+ self._pending_bytes -= nxt.size
123
+ return batch
124
+
125
+ async def _run(self) -> None:
126
+ loop = asyncio.get_running_loop()
127
+ while True:
128
+ if not self._pending:
129
+ self._wake.clear()
130
+ await self._wake.wait()
131
+ continue
132
+ deadline = self._pending[0].queued_at + self._max_delay_secs
133
+ while not (self._full() or self._force or self._closing):
134
+ remaining = deadline - loop.time()
135
+ if remaining <= 0:
136
+ break
137
+ self._wake.clear()
138
+ with contextlib.suppress(TimeoutError):
139
+ await asyncio.wait_for(self._wake.wait(), remaining)
140
+ batch = self._take()
141
+ if batch:
142
+ await self._send(batch)
143
+ if not self._pending:
144
+ self._idle.set()
145
+
146
+ async def _send(self, batch: list[_Pending[T, R]]) -> None:
147
+ self._in_flight += len(batch)
148
+ try:
149
+ results: Sequence[R | BaseException] = await self._flush([p.item for p in batch])
150
+ except Exception as exc:
151
+ logger.warning(
152
+ "batch_flush_failed", batcher=self._name, size=len(batch), error=str(exc)
153
+ )
154
+ results = [exc] * len(batch)
155
+ finally:
156
+ self._in_flight -= len(batch)
157
+ if len(results) != len(batch):
158
+ mismatch = RuntimeError(f"{self._name}: flush returned {len(results)} results")
159
+ results = [mismatch] * len(batch)
160
+ for pending, result in zip(batch, results, strict=True):
161
+ if pending.future.done():
162
+ continue
163
+ if isinstance(result, BaseException):
164
+ pending.future.set_exception(result)
165
+ else:
166
+ pending.future.set_result(result)
167
+
168
+
169
+ def _retrieve(future: asyncio.Future[Any]) -> None:
170
+ if not future.cancelled():
171
+ future.exception()
172
+
173
+
174
+ def message_bytes(body: str, attributes: Mapping[str, Mapping[str, Any]] | None = None) -> int:
175
+ """Size SQS counts for one message: UTF-8 body plus attribute names, types and values.
176
+
177
+ Used to keep `SendMessageBatch` under the 1 MiB request cap (DS15).
178
+ """
179
+ size = len(body.encode("utf-8"))
180
+ for name, value in (attributes or {}).items():
181
+ size += len(name.encode("utf-8")) + len(str(value.get("DataType", "")).encode("utf-8"))
182
+ string_value = value.get("StringValue")
183
+ if string_value is not None:
184
+ size += len(str(string_value).encode("utf-8"))
185
+ binary_value = value.get("BinaryValue")
186
+ if isinstance(binary_value, bytes | bytearray):
187
+ size += len(binary_value)
188
+ return size