Add slack example

This commit is contained in:
mudler
2023-12-16 18:54:06 +01:00
parent 0f953d8ad9
commit 7e3f2cbffb
19 changed files with 1854 additions and 1 deletions

306
examples/slack/main_prod.py Normal file
View File

@@ -0,0 +1,306 @@
# Unzip the dependencies managed by serverless-python-requirements
try:
import unzip_requirements # type:ignore
except ImportError:
pass
#
# Imports
#
import json
import logging
import os
import openai
from slack_sdk.web import WebClient
from slack_sdk.errors import SlackApiError
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
from slack_bolt import App, Ack, BoltContext
from app.bolt_listeners import register_listeners, before_authorize
from app.env import USE_SLACK_LANGUAGE, SLACK_APP_LOG_LEVEL, DEFAULT_OPENAI_MODEL
from app.slack_ops import (
build_home_tab,
DEFAULT_HOME_TAB_MESSAGE,
DEFAULT_HOME_TAB_CONFIGURE_LABEL,
)
from app.i18n import translate
#
# Product deployment (AWS Lambda)
#
# export SLACK_CLIENT_ID=
# export SLACK_CLIENT_SECRET=
# export SLACK_SIGNING_SECRET=
# export SLACK_SCOPES=app_mentions:read,channels:history,groups:history,im:history,mpim:history,chat:write.public,chat:write,users:read
# export SLACK_INSTALLATION_S3_BUCKET_NAME=
# export SLACK_STATE_S3_BUCKET_NAME=
# export OPENAI_S3_BUCKET_NAME=
# npm install -g serverless
# serverless plugin install -n serverless-python-requirements
# serverless deploy
#
import boto3
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from slack_bolt.adapter.aws_lambda.lambda_s3_oauth_flow import LambdaS3OAuthFlow
SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(format="%(asctime)s %(message)s", level=SLACK_APP_LOG_LEVEL)
s3_client = boto3.client("s3")
openai_bucket_name = os.environ["OPENAI_S3_BUCKET_NAME"]
client_template = WebClient()
client_template.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))
def register_revocation_handlers(app: App):
# Handle uninstall events and token revocations
@app.event("tokens_revoked")
def handle_tokens_revoked_events(
event: dict,
context: BoltContext,
logger: logging.Logger,
):
user_ids = event.get("tokens", {}).get("oauth", [])
if len(user_ids) > 0:
for user_id in user_ids:
app.installation_store.delete_installation(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
user_id=user_id,
)
bots = event.get("tokens", {}).get("bot", [])
if len(bots) > 0:
app.installation_store.delete_bot(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)
try:
s3_client.delete_object(Bucket=openai_bucket_name, Key=context.team_id)
except Exception as e:
logger.error(
f"Failed to delete an OpenAI auth key: (team_id: {context.team_id}, error: {e})"
)
@app.event("app_uninstalled")
def handle_app_uninstalled_events(
context: BoltContext,
logger: logging.Logger,
):
app.installation_store.delete_all(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)
try:
s3_client.delete_object(Bucket=openai_bucket_name, Key=context.team_id)
except Exception as e:
logger.error(
f"Failed to delete an OpenAI auth key: (team_id: {context.team_id}, error: {e})"
)
def handler(event, context_):
app = App(
process_before_response=True,
before_authorize=before_authorize,
oauth_flow=LambdaS3OAuthFlow(),
client=client_template,
)
app.oauth_flow.settings.install_page_rendering_enabled = False
register_listeners(app)
register_revocation_handlers(app)
if USE_SLACK_LANGUAGE is True:
@app.middleware
def set_locale(
context: BoltContext,
client: WebClient,
logger: logging.Logger,
next_,
):
bot_scopes = context.authorize_result.bot_scopes
if bot_scopes is not None and "users:read" in bot_scopes:
user_id = context.actor_user_id or context.user_id
try:
user_info = client.users_info(user=user_id, include_locale=True)
context["locale"] = user_info.get("user", {}).get("locale")
except SlackApiError as e:
logger.debug(f"Failed to fetch user info due to {e}")
pass
next_()
@app.middleware
def set_s3_openai_api_key(context: BoltContext, next_):
try:
s3_response = s3_client.get_object(
Bucket=openai_bucket_name, Key=context.team_id
)
config_str: str = s3_response["Body"].read().decode("utf-8")
if config_str.startswith("{"):
config = json.loads(config_str)
context["OPENAI_API_KEY"] = config.get("api_key")
context["OPENAI_MODEL"] = config.get("model")
else:
# The legacy data format
context["OPENAI_API_KEY"] = config_str
context["OPENAI_MODEL"] = DEFAULT_OPENAI_MODEL
except: # noqa: E722
context["OPENAI_API_KEY"] = None
next_()
@app.event("app_home_opened")
def render_home_tab(client: WebClient, context: BoltContext):
message = DEFAULT_HOME_TAB_MESSAGE
configure_label = DEFAULT_HOME_TAB_CONFIGURE_LABEL
try:
s3_client.get_object(Bucket=openai_bucket_name, Key=context.team_id)
message = "This app is ready to use in this workspace :raised_hands:"
except: # noqa: E722
pass
openai_api_key = context.get("OPENAI_API_KEY")
if openai_api_key is not None:
message = translate(
openai_api_key=openai_api_key, context=context, text=message
)
configure_label = translate(
openai_api_key=openai_api_key,
context=context,
text=DEFAULT_HOME_TAB_CONFIGURE_LABEL,
)
client.views_publish(
user_id=context.user_id,
view=build_home_tab(message, configure_label),
)
@app.action("configure")
def handle_some_action(ack, body: dict, client: WebClient, context: BoltContext):
ack()
already_set_api_key = context.get("OPENAI_API_KEY")
api_key_text = "Save your OpenAI API key:"
submit = "Submit"
cancel = "Cancel"
if already_set_api_key is not None:
api_key_text = translate(
openai_api_key=already_set_api_key, context=context, text=api_key_text
)
submit = translate(
openai_api_key=already_set_api_key, context=context, text=submit
)
cancel = translate(
openai_api_key=already_set_api_key, context=context, text=cancel
)
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "configure",
"title": {"type": "plain_text", "text": "OpenAI API Key"},
"submit": {"type": "plain_text", "text": submit},
"close": {"type": "plain_text", "text": cancel},
"blocks": [
{
"type": "input",
"block_id": "api_key",
"label": {"type": "plain_text", "text": api_key_text},
"element": {"type": "plain_text_input", "action_id": "input"},
},
{
"type": "input",
"block_id": "model",
"label": {"type": "plain_text", "text": "OpenAI Model"},
"element": {
"type": "static_select",
"action_id": "input",
"options": [
{
"text": {
"type": "plain_text",
"text": "GPT-3.5 Turbo",
},
"value": "gpt-3.5-turbo",
},
{
"text": {"type": "plain_text", "text": "GPT-4"},
"value": "gpt-4",
},
],
"initial_option": {
"text": {
"type": "plain_text",
"text": "GPT-3.5 Turbo",
},
"value": "gpt-3.5-turbo",
},
},
},
],
},
)
def validate_api_key_registration(ack: Ack, view: dict, context: BoltContext):
already_set_api_key = context.get("OPENAI_API_KEY")
inputs = view["state"]["values"]
api_key = inputs["api_key"]["input"]["value"]
model = inputs["model"]["input"]["selected_option"]["value"]
try:
# Verify if the API key is valid
openai.Model.retrieve(api_key=api_key, id="gpt-3.5-turbo")
try:
# Verify if the given model works with the API key
openai.Model.retrieve(api_key=api_key, id=model)
except Exception:
text = "This model is not yet available for this API key"
if already_set_api_key is not None:
text = translate(
openai_api_key=already_set_api_key, context=context, text=text
)
ack(
response_action="errors",
errors={"model": text},
)
return
ack()
except Exception:
text = "This API key seems to be invalid"
if already_set_api_key is not None:
text = translate(
openai_api_key=already_set_api_key, context=context, text=text
)
ack(
response_action="errors",
errors={"api_key": text},
)
def save_api_key_registration(
view: dict,
logger: logging.Logger,
context: BoltContext,
):
inputs = view["state"]["values"]
api_key = inputs["api_key"]["input"]["value"]
model = inputs["model"]["input"]["selected_option"]["value"]
try:
openai.Model.retrieve(api_key=api_key, id=model)
s3_client.put_object(
Bucket=openai_bucket_name,
Key=context.team_id,
Body=json.dumps({"api_key": api_key, "model": model}),
)
except Exception as e:
logger.exception(e)
app.view("configure")(
ack=validate_api_key_registration,
lazy=[save_api_key_registration],
)
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context_)