-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlambda_function.py
More file actions
145 lines (118 loc) · 5.14 KB
/
Copy pathlambda_function.py
File metadata and controls
145 lines (118 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""AWS Lambda entry point for Welt, the Slack frontend for AgentCore agents.
Serves the same conversation flow as `main.py` over the Slack Events API
(HTTP request URL) instead of Socket Mode, for deployments on the Lambda
Python runtime. Bolt acks each event within Slack's 3-second window and
re-invokes this same function asynchronously to run the lazy listener, so
the function's IAM role must allow `lambda:InvokeFunction` on the function
itself. Configure the handler as `lambda_function.lambda_handler` (the
Python runtime default) and point the Slack app's Events API request URL at
the function.
"""
from __future__ import annotations
import asyncio
import functools
import logging
import os
from slack_bolt import Ack, App, BoltContext
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from slack_sdk.http_retry.builtin_async_handlers import AsyncRateLimitErrorRetryHandler
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
from slack_sdk.web.async_client import AsyncWebClient
from app import bolt_listeners
from app.agent_service import init_client, log_local_mode
from app.bolt_logic import INTERRUPT_ACTION_PATTERN
from app.bolt_middlewares import before_authorize_http
from app.env import Env, load_env, require_env
logger = logging.getLogger(__name__)
def lambda_handler(event: dict, context: object) -> dict:
"""
Handle one Lambda invocation: an Events API request or a lazy run.
Args:
event (dict): The Lambda event (an HTTP request from a Function URL
or API Gateway, or Bolt's lazy-listener re-invocation).
context (object): The Lambda context object.
Returns:
dict: The HTTP response for the Lambda runtime.
"""
return _get_handler().handle(event, context)
@functools.cache
def _get_handler() -> SlackRequestHandler:
"""
Build the request handler once per Lambda execution environment.
Deferring the build to the first invocation (instead of import time)
keeps the module importable without configuration; a misconfigured
function still fails on its cold start with a clear error.
Returns:
SlackRequestHandler: The handler wrapping the configured Bolt app.
"""
env = load_env(os.environ)
signing_secret = require_env(os.environ, "SLACK_SIGNING_SECRET")
SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(level=env.deps_log_level)
# LOG_LEVEL applies to Welt's own loggers only; the root level above
# (DEPS_LOG_LEVEL) covers the dependencies. See Env.deps_log_level.
logging.getLogger("app").setLevel(env.log_level)
logger.setLevel(env.log_level)
for warning in env.boot_warnings:
logger.warning(warning)
# No region means no ARN: local mode (see Env.agent_region). On Lambda
# that can only be a forgotten AGENT_ARN; the log line names it, and
# the first invocation fails on the unreachable localhost.
if env.agent_region is None:
log_local_mode()
else:
init_client(region_name=env.agent_region)
return SlackRequestHandler(app=create_bolt_app(env, signing_secret))
def create_bolt_app(env: Env, signing_secret: str) -> App:
"""
Create and configure the sync Slack Bolt app instance for HTTP serving.
The listener body is the same async flow Socket Mode runs; each lazy
invocation carries one message, so it bridges with `asyncio.run` and an
`AsyncWebClient` of its own.
Args:
env (Env): The validated configuration.
signing_secret (str): The Slack signing secret for request
verification.
Returns:
App: The configured Slack Bolt app instance.
"""
app = App(
token=env.slack_bot_token,
signing_secret=signing_secret,
before_authorize=before_authorize_http,
process_before_response=True,
)
app.client.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))
def respond_to_new_post(context: BoltContext, payload: dict) -> None:
client = AsyncWebClient(token=env.slack_bot_token)
client.retry_handlers.append(AsyncRateLimitErrorRetryHandler(max_retry_count=2))
asyncio.run(
bolt_listeners.respond_to_new_post(
env=env, context=context, payload=payload, client=client
)
)
def respond_to_interrupt_action(
context: BoltContext, body: dict, payload: dict
) -> None:
client = AsyncWebClient(token=env.slack_bot_token)
client.retry_handlers.append(AsyncRateLimitErrorRetryHandler(max_retry_count=2))
asyncio.run(
bolt_listeners.respond_to_interrupt_action(
env=env, context=context, body=body, payload=payload, client=client
)
)
app.event("message")(ack=just_ack, lazy=[respond_to_new_post])
app.action(INTERRUPT_ACTION_PATTERN)(
ack=just_ack, lazy=[respond_to_interrupt_action]
)
return app
def just_ack(ack: Ack) -> None:
"""
Acknowledge the incoming request immediately.
The real work happens in the lazy listener, keeping the 3-second ack.
Args:
ack (Ack): The acknowledgment function provided by Slack Bolt.
Returns:
None
"""
ack()