agent-wait-aws 0.2.0__tar.gz
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.
- agent_wait_aws-0.2.0/.gitignore +19 -0
- agent_wait_aws-0.2.0/LICENSE +21 -0
- agent_wait_aws-0.2.0/PKG-INFO +52 -0
- agent_wait_aws-0.2.0/README.md +27 -0
- agent_wait_aws-0.2.0/pyproject.toml +40 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/__init__.py +17 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/announce/__init__.py +6 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/announce/dynamodb.py +98 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/announce/eventbridge.py +53 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/announce/sns.py +47 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/announce/sqs.py +42 -0
- agent_wait_aws-0.2.0/src/agent_wait_aws/py.typed +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
*.egg-info/
|
|
2
|
+
*.pyc
|
|
3
|
+
.coverage
|
|
4
|
+
.env
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.scratch/
|
|
8
|
+
.venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
build/
|
|
11
|
+
cdk.out/
|
|
12
|
+
reports/*.log
|
|
13
|
+
|
|
14
|
+
# generated by the docs workflow from README.md / CHANGELOG.md
|
|
15
|
+
docs/index.md
|
|
16
|
+
docs/changelog.md
|
|
17
|
+
# build output
|
|
18
|
+
dist/
|
|
19
|
+
site/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamaljeet Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-wait-aws
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AWS announce adapters for agent-wait: SNS, SQS, EventBridge, and DynamoDB (the question as a row), plus a CDK stack for the example.
|
|
5
|
+
Project-URL: Homepage, https://skamalj.github.io/agent-wait/
|
|
6
|
+
Project-URL: Documentation, https://skamalj.github.io/agent-wait/
|
|
7
|
+
Project-URL: Source, https://github.com/skamalj/agent-wait
|
|
8
|
+
Project-URL: Issues, https://github.com/skamalj/agent-wait/issues
|
|
9
|
+
Author-email: Kamaljeet Singh <skamalj@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,aws,dynamodb,eventbridge,human-in-the-loop,langgraph,sns,sqs
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: agent-wait<0.3,>=0.2
|
|
23
|
+
Requires-Dist: boto3>=1.35
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# agent-wait-aws
|
|
27
|
+
|
|
28
|
+
AWS announce adapters for [agent-wait](https://pypi.org/project/agent-wait/).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install agent-wait-aws # pulls in agent-wait and boto3
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from agent_wait_aws import DynamoDbAnnounce, EventBridgeAnnounce, SnsAnnounce, SqsAnnounce
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Adapter | Where the question lands |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `SnsAnnounce(topic_arn)` | A topic. Policy `tags` become message attributes, so subscription filter policies can route on them. |
|
|
41
|
+
| `SqsAnnounce(queue_url)` | A queue. On FIFO: `MessageGroupId = thread_id`, and `MessageDeduplicationId` is the stable dedupe key, so a republished question is swallowed. |
|
|
42
|
+
| `EventBridgeAnnounce(bus)` | A bus, with the transition as detail-type. Notices partial failures, which `PutEvents` reports inside an HTTP 200. |
|
|
43
|
+
| `DynamoDbAnnounce(table)` | A row. `created` writes it `open`, `resumed` marks it `closed`; a GSI on `status` gives an approvals UI its query with no broker involved. |
|
|
44
|
+
|
|
45
|
+
Every one is a `BaseAnnounce` subclass with a single `deliver()` method, so a failure is a
|
|
46
|
+
log line rather than a failed run. They are also the reference for writing your own:
|
|
47
|
+
`sqs.py` is twenty lines.
|
|
48
|
+
|
|
49
|
+
The package also carries a CDK stack for the example agent — a FIFO queue, a topic, one
|
|
50
|
+
Lambda, the checkpointer's table and an approvals table.
|
|
51
|
+
|
|
52
|
+
Full documentation: [skamalj.github.io/agent-wait](https://skamalj.github.io/agent-wait/).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# agent-wait-aws
|
|
2
|
+
|
|
3
|
+
AWS announce adapters for [agent-wait](https://pypi.org/project/agent-wait/).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install agent-wait-aws # pulls in agent-wait and boto3
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from agent_wait_aws import DynamoDbAnnounce, EventBridgeAnnounce, SnsAnnounce, SqsAnnounce
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Adapter | Where the question lands |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `SnsAnnounce(topic_arn)` | A topic. Policy `tags` become message attributes, so subscription filter policies can route on them. |
|
|
16
|
+
| `SqsAnnounce(queue_url)` | A queue. On FIFO: `MessageGroupId = thread_id`, and `MessageDeduplicationId` is the stable dedupe key, so a republished question is swallowed. |
|
|
17
|
+
| `EventBridgeAnnounce(bus)` | A bus, with the transition as detail-type. Notices partial failures, which `PutEvents` reports inside an HTTP 200. |
|
|
18
|
+
| `DynamoDbAnnounce(table)` | A row. `created` writes it `open`, `resumed` marks it `closed`; a GSI on `status` gives an approvals UI its query with no broker involved. |
|
|
19
|
+
|
|
20
|
+
Every one is a `BaseAnnounce` subclass with a single `deliver()` method, so a failure is a
|
|
21
|
+
log line rather than a failed run. They are also the reference for writing your own:
|
|
22
|
+
`sqs.py` is twenty lines.
|
|
23
|
+
|
|
24
|
+
The package also carries a CDK stack for the example agent — a FIFO queue, a topic, one
|
|
25
|
+
Lambda, the checkpointer's table and an approvals table.
|
|
26
|
+
|
|
27
|
+
Full documentation: [skamalj.github.io/agent-wait](https://skamalj.github.io/agent-wait/).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-wait-aws"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "AWS announce adapters for agent-wait: SNS, SQS, EventBridge, and DynamoDB (the question as a row), plus a CDK stack for the example."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Kamaljeet Singh", email = "skamalj@gmail.com" }]
|
|
10
|
+
keywords = ["agents", "langgraph", "human-in-the-loop", "aws", "sns", "sqs", "eventbridge", "dynamodb"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["agent-wait>=0.2,<0.3", "boto3>=1.35"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://skamalj.github.io/agent-wait/"
|
|
25
|
+
Documentation = "https://skamalj.github.io/agent-wait/"
|
|
26
|
+
Source = "https://github.com/skamalj/agent-wait"
|
|
27
|
+
Issues = "https://github.com/skamalj/agent-wait/issues"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/agent_wait_aws"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["src/agent_wait_aws", "README.md", "LICENSE"]
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources]
|
|
40
|
+
agent-wait = { workspace = true }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""agent-wait-aws: announce adapters for AWS.
|
|
2
|
+
|
|
3
|
+
Four places a question can land:
|
|
4
|
+
|
|
5
|
+
SnsAnnounce(topic_arn) # fan out; policy tags become filterable attributes
|
|
6
|
+
SqsAnnounce(queue_url) # one consumer, ordered per thread on FIFO
|
|
7
|
+
EventBridgeAnnounce(bus) # rules and targets decide who cares
|
|
8
|
+
DynamoDbAnnounce(table) # the question *is* a row; query it directly
|
|
9
|
+
|
|
10
|
+
This package contains nothing but adapters and the CDK stack for the example. There
|
|
11
|
+
is no store, no key provider and no run handler here, because the library keeps no state
|
|
12
|
+
and receives no answers.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .announce import DynamoDbAnnounce, EventBridgeAnnounce, SnsAnnounce, SqsAnnounce
|
|
16
|
+
|
|
17
|
+
__all__ = ["DynamoDbAnnounce", "EventBridgeAnnounce", "SnsAnnounce", "SqsAnnounce"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""`DynamoDbAnnounce` -- the question *is* the row.
|
|
2
|
+
|
|
3
|
+
Here to make a point: since nothing reads state back through this library, an
|
|
4
|
+
"announce adapter" does not have to be a message broker. It is
|
|
5
|
+
just somewhere the question lands where whoever answers it will find it. A table is a
|
|
6
|
+
perfectly good somewhere -- and for an approval queue it is a better one, because a UI
|
|
7
|
+
can `Query` it for "everything still open" without anybody having to build a projection
|
|
8
|
+
off an event stream first.
|
|
9
|
+
|
|
10
|
+
announce=[DynamoDbAnnounce("approvals")]
|
|
11
|
+
|
|
12
|
+
pk = "THREAD#order-4471" sk = "WAIT#<interrupt_id>"
|
|
13
|
+
status = "open" | "closed"
|
|
14
|
+
|
|
15
|
+
`created` writes the row; `resumed` marks it closed rather than deleting it, so the row
|
|
16
|
+
is still there when someone asks why the button stopped working. Both are idempotent:
|
|
17
|
+
the same interrupt republished after a crash overwrites its own row with identical
|
|
18
|
+
content, which is the whole reason `dedupe_key` is stable.
|
|
19
|
+
|
|
20
|
+
A GSI on `status` gives you "every open approval", which is the query an approvals UI
|
|
21
|
+
actually wants. The CDK stack in this package creates it.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import boto3
|
|
29
|
+
from agent_wait.announce import BaseAnnounce
|
|
30
|
+
from agent_wait.model import Transition, WaitEnvelope
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DynamoDbAnnounce(BaseAnnounce):
|
|
34
|
+
name = "dynamodb"
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
table_name: str,
|
|
39
|
+
*,
|
|
40
|
+
table: Any = None,
|
|
41
|
+
region_name: str | None = None,
|
|
42
|
+
ttl_seconds: int | None = None,
|
|
43
|
+
only: tuple[Transition, ...] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(only=only)
|
|
46
|
+
self.table_name = table_name
|
|
47
|
+
self._table = table or boto3.resource("dynamodb", region_name=region_name).Table(table_name)
|
|
48
|
+
self._ttl_seconds = ttl_seconds
|
|
49
|
+
|
|
50
|
+
def deliver(self, envelope: WaitEnvelope, transition: Transition) -> None:
|
|
51
|
+
if transition == "resumed":
|
|
52
|
+
self._close(envelope)
|
|
53
|
+
else:
|
|
54
|
+
self._open(envelope)
|
|
55
|
+
|
|
56
|
+
def _open(self, envelope: WaitEnvelope) -> None:
|
|
57
|
+
item: dict[str, Any] = {
|
|
58
|
+
"pk": f"THREAD#{envelope.thread_id}",
|
|
59
|
+
"sk": f"WAIT#{envelope.interrupt_id}",
|
|
60
|
+
"status": "open",
|
|
61
|
+
"thread_id": envelope.thread_id,
|
|
62
|
+
"interrupt_id": envelope.interrupt_id,
|
|
63
|
+
"question": envelope.question,
|
|
64
|
+
"allowed_actions": list(envelope.allowed_actions),
|
|
65
|
+
"expires_at": envelope.expires_at,
|
|
66
|
+
"reply_with": dict(envelope.reply_with),
|
|
67
|
+
"tags": dict(envelope.tags),
|
|
68
|
+
"event_id": envelope.event_id,
|
|
69
|
+
}
|
|
70
|
+
if envelope.default is not None:
|
|
71
|
+
item["default"] = envelope.default
|
|
72
|
+
if envelope.reply_to:
|
|
73
|
+
item["reply_to"] = dict(envelope.reply_to)
|
|
74
|
+
if self._ttl_seconds:
|
|
75
|
+
item["ttl"] = int(_now()) + self._ttl_seconds
|
|
76
|
+
self._table.put_item(Item=item)
|
|
77
|
+
|
|
78
|
+
def _close(self, envelope: WaitEnvelope) -> None:
|
|
79
|
+
"""Mark closed, and only if the row is there.
|
|
80
|
+
|
|
81
|
+
The condition matters: a `resumed` for an interrupt this table never saw
|
|
82
|
+
(announced by a different adapter, or written before this adapter was added)
|
|
83
|
+
should leave nothing behind. Creating a closed row for a question nobody was
|
|
84
|
+
ever asked would put a phantom in the approvals UI's history.
|
|
85
|
+
"""
|
|
86
|
+
self._table.update_item(
|
|
87
|
+
Key={"pk": f"THREAD#{envelope.thread_id}", "sk": f"WAIT#{envelope.interrupt_id}"},
|
|
88
|
+
UpdateExpression="SET #s = :closed",
|
|
89
|
+
ExpressionAttributeNames={"#s": "status"},
|
|
90
|
+
ExpressionAttributeValues={":closed": "closed"},
|
|
91
|
+
ConditionExpression="attribute_exists(pk)",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _now() -> float:
|
|
96
|
+
import time
|
|
97
|
+
|
|
98
|
+
return time.time()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""`EventBridgeAnnounce` -- `PutEvents` onto a bus, `DetailType = wait.<transition>`.
|
|
2
|
+
|
|
3
|
+
The most useful of the four for a large organisation: rules on the bus can fan a
|
|
4
|
+
`wait.created` out to Slack, a ticketing system and an audit log without the agent, or
|
|
5
|
+
this library, knowing that any of them exist.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import boto3
|
|
14
|
+
from agent_wait.announce import BaseAnnounce
|
|
15
|
+
from agent_wait.model import Transition, WaitEnvelope
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventBridgeAnnounce(BaseAnnounce):
|
|
19
|
+
name = "eventbridge"
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
bus_name: str,
|
|
24
|
+
*,
|
|
25
|
+
source: str = "agent-wait",
|
|
26
|
+
client: Any = None,
|
|
27
|
+
region_name: str | None = None,
|
|
28
|
+
only: tuple[Transition, ...] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(only=only)
|
|
31
|
+
self.bus_name = bus_name
|
|
32
|
+
self.source = source
|
|
33
|
+
self._client = client or boto3.client("events", region_name=region_name)
|
|
34
|
+
|
|
35
|
+
def deliver(self, envelope: WaitEnvelope, transition: Transition) -> None:
|
|
36
|
+
response = self._client.put_events(
|
|
37
|
+
Entries=[
|
|
38
|
+
{
|
|
39
|
+
"EventBusName": self.bus_name,
|
|
40
|
+
"Source": self.source,
|
|
41
|
+
"DetailType": envelope.type,
|
|
42
|
+
"Detail": envelope.to_json(),
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
# PutEvents reports per-entry failures in the body with a 200 status, so a naive
|
|
47
|
+
# call looks successful even when nothing was delivered.
|
|
48
|
+
if response.get("FailedEntryCount"):
|
|
49
|
+
self._log.error(
|
|
50
|
+
"EventBridge rejected the envelope for interrupt %s: %s",
|
|
51
|
+
envelope.interrupt_id,
|
|
52
|
+
json.dumps(response.get("Entries", []), default=str),
|
|
53
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""`SnsAnnounce` -- publish the envelope, with the policy's tags as message attributes.
|
|
2
|
+
|
|
3
|
+
The tags are the useful part. `WaitPolicy(tags={"approver_group": "finance"})` becomes an
|
|
4
|
+
SNS message attribute, so a subscription filter policy can route finance approvals to one
|
|
5
|
+
place and everything else somewhere else -- without the agent knowing any of those places
|
|
6
|
+
exist.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import boto3
|
|
14
|
+
from agent_wait.announce import BaseAnnounce
|
|
15
|
+
from agent_wait.model import Transition, WaitEnvelope
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SnsAnnounce(BaseAnnounce):
|
|
19
|
+
name = "sns"
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
topic_arn: str,
|
|
24
|
+
*,
|
|
25
|
+
client: Any = None,
|
|
26
|
+
region_name: str | None = None,
|
|
27
|
+
only: tuple[Transition, ...] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(only=only)
|
|
30
|
+
self.topic_arn = topic_arn
|
|
31
|
+
self._client = client or boto3.client("sns", region_name=region_name)
|
|
32
|
+
|
|
33
|
+
def deliver(self, envelope: WaitEnvelope, transition: Transition) -> None:
|
|
34
|
+
attributes: dict[str, dict[str, str]] = {
|
|
35
|
+
"transition": {"DataType": "String", "StringValue": transition},
|
|
36
|
+
"thread_id": {"DataType": "String", "StringValue": envelope.thread_id},
|
|
37
|
+
}
|
|
38
|
+
for key, value in envelope.tags.items():
|
|
39
|
+
# SNS rejects an empty StringValue, and a filter policy cannot use one.
|
|
40
|
+
if value:
|
|
41
|
+
attributes[key] = {"DataType": "String", "StringValue": str(value)}
|
|
42
|
+
self._client.publish(
|
|
43
|
+
TopicArn=self.topic_arn,
|
|
44
|
+
Subject=envelope.type,
|
|
45
|
+
Message=envelope.to_json(),
|
|
46
|
+
MessageAttributes=attributes,
|
|
47
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""`SqsAnnounce` -- put the envelope on a queue.
|
|
2
|
+
|
|
3
|
+
On a FIFO queue, `MessageGroupId` is the thread id (one in-flight question per
|
|
4
|
+
conversation, so a consumer never sees two from the same graph out of order) and
|
|
5
|
+
`MessageDeduplicationId` is the envelope's `dedupe_key` -- not its `event_id`, which is
|
|
6
|
+
fresh on every publish and would let a republished question through as new.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import boto3
|
|
15
|
+
from agent_wait.announce import BaseAnnounce
|
|
16
|
+
from agent_wait.model import Transition, WaitEnvelope
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SqsAnnounce(BaseAnnounce):
|
|
20
|
+
name = "sqs"
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
queue_url: str,
|
|
25
|
+
*,
|
|
26
|
+
group_id: Callable[[WaitEnvelope], str] | None = None,
|
|
27
|
+
client: Any = None,
|
|
28
|
+
region_name: str | None = None,
|
|
29
|
+
only: tuple[Transition, ...] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(only=only)
|
|
32
|
+
self.queue_url = queue_url
|
|
33
|
+
self.group_id = group_id or (lambda envelope: envelope.thread_id)
|
|
34
|
+
self._client = client or boto3.client("sqs", region_name=region_name)
|
|
35
|
+
self.is_fifo = queue_url.endswith(".fifo")
|
|
36
|
+
|
|
37
|
+
def deliver(self, envelope: WaitEnvelope, transition: Transition) -> None:
|
|
38
|
+
kwargs: dict[str, Any] = {"QueueUrl": self.queue_url, "MessageBody": envelope.to_json()}
|
|
39
|
+
if self.is_fifo:
|
|
40
|
+
kwargs["MessageGroupId"] = self.group_id(envelope)
|
|
41
|
+
kwargs["MessageDeduplicationId"] = envelope.dedupe_key
|
|
42
|
+
self._client.send_message(**kwargs)
|
|
File without changes
|