You can launch triggers directly from Slack using slash commands, providing responses to the conversation from within the executions running through Valohai.
In this guide, we will be getting an image from a Slack conversation and passing it on to an execution that modifies it, then posting the result back to the Slack conversation.
While we will use a basic image manipulation script as the middle step as a visually obvious modification, you can easily plug in any Valohai execution that takes an input image and produces an output image in its place by changing the input names in the pipeline layout.
Slack API setup
Slack API access
While normal members generally can create app integrations on Slack workspaces, you may need to ask your organization admin or tech support for more access to do this, or ask them to do this on your behalf.
You will need the message verification secret and Slack API token first.
Create a new Slack App.
- When you are logged in to Slack chat in the app or web, navigate to the drop-down menu by your workspace name near the top left corner of the window. Under “Tools and Settings” you should find “Manage Apps.” Click it to navigate to the Slack management portal.
- On the Slack management portal, click the “Build” link on the top navigation bar. This will navigate you to the Slack developer portal.
- Click on “Create new app”
- Select “From app manifest”.
- Select the workspace to add the app to
- Enter the app manifest (example manifest below). You should change the app name to your liking, or you can do it later.
- Review the app’s summary and finish creating the app. It should have these permissions:
channels:history
,channels:join
,channels:read
,chat:write
,commands
,files:read
,files:write
- Install the app to your Slack workspace.
Example app manifest:
{
"display_information": {
"name": "My Slack Integration"
},
"features": {
"bot_user": {
"display_name": "My Slack Integration",
"always_online": false
}
},
"oauth_config": {
"scopes": {
"bot": [
"channels:history",
"channels:join",
"channels:read",
"chat:write",
"commands",
"files:read",
"files:write"
]
}
},
"settings": {
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}
The integration requires two authentication tokens from your Slack app to interact with Slack.
Signing Secret: From your app summary page (where you installed it), browse down to find the Signing Secret. Click “Show” and copy the token into your notes.
Bot User OAuth Token: From the app summary page, browse the sidebar to “OAuth and Permissions.” Click it to navigate to the OAuth configuration page. Copy the Bot User OAuth Token into your notes.
Valohai project setup
Add the following steps and pipeline to your valohai.yaml
configuration file, commit the changes, push them to Git
and fetch the new commit to Valohai.
Create a file called slack_input.py
.
import sys
import os
from urllib.parse import parse_qs
import valohai
import requests
valohai.prepare(
step="slack-slash",
default_inputs={
'slack-webhook-input': '',
}
)
with open(valohai.inputs('slack-webhook-input').path()) as f:
slack_info = {k: v[0] for k, v in parse_qs(f.read()).items()}
print("Slack called us with:")
for key, value in slack_info.items():
print(f"{key}: {value}")
print("Replying to the slash command...")
parameters = slack_info.get("text")
if parameters:
reply = f"Hello from the execution! You told me to: {parameters}"
else:
reply = "Hello from the execution! I didn't get any parameters."
requests.post(slack_info["response_url"], json={
"text": reply
})
image_paths = [
# You can download multiple images at once
valohai.outputs().path("image1.png"),
]
convo_api = "https://slack.com/api/conversations.history"
slack_token = os.getenv("SLACK_TOKEN")
if slack_token:
print("Got slack token")
else:
requests.post(slack_info["response_url"], json={
"replace_original": True,
"text": "We don't have a SLACK_TOKEN! Please configure it in project settings."
})
sys.exit(3)
def download_image(url, destination):
with requests.get(url, stream=True, headers={"Authorization": f"Bearer {slack_token}"}) as r:
r.raise_for_status()
with open(destination, 'wb') as img:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
img.write(chunk)
image_filenames = []
def previous_image():
"""Browses Slack API and downloads two last posted images in the channel"""
resp = requests.get(convo_api, headers={"Authorization": f"Bearer {slack_token}"}, params={
"channel": slack_info["channel_id"]
})
resp.raise_for_status()
resp_json = resp.json()
print("Response:", resp_json)
for msg in resp_json["messages"]:
if not image_paths:
return True
if "files" in msg:
for att in msg["files"]:
if att["mimetype"] not in ["image/png", "image/jpeg"]:
continue
if "url_private_download" in att:
print("Found an image in files!")
path = image_paths.pop()
download_image(att["url_private_download"], path)
image_filenames.append(att["name"])
return not image_paths
search = previous_image
print(f"Querying Slack for images... params: {parameters}")
success = search()
if not success:
requests.post(slack_info["response_url"], json={
"text": ":pensive: Sorry, I could not find enough images with your command."
})
sys.exit(2)
user_id = slack_info['user_id']
print("Found images! They have been saved as outputs.")
requests.post(slack_info["response_url"], json={
"response_type": "in_channel",
"text": f"<@{user_id}> :rocket: Found image(s): {' ,'.join(image_filenames)}! Launching execution..."
})
Create a file called slack_output.py
.
import os
from urllib.parse import parse_qs
import valohai
import requests
valohai.prepare(
step="slack-upload",
default_inputs={
'slack-webhook-input': '',
'message-attachment': '',
}
)
with open(valohai.inputs('slack-webhook-input').path()) as f:
slack_info = {k: v[0] for k, v in parse_qs(f.read()).items()}
def check_ok(response_data):
if not response_data['ok']:
print("Slack problem: ", response_data)
slack_token = os.getenv("SLACK_TOKEN")
upload_context_api = "https://slack.com/api/files.getUploadURLExternal"
upload_finalize_api = "https://slack.com/api/files.completeUploadExternal"
print("Posting file")
file_path = valohai.inputs("message-attachment").path()
user_id = slack_info['user_id']
file_size = os.stat(file_path).st_size
upload_context_resp = requests.post(
upload_context_api,
data={
'filename': os.path.basename(file_path),
'length': file_size,
},
headers={"Authorization": f"Bearer {slack_token}"}
)
upload_context = upload_context_resp.json()
check_ok(upload_context)
print(f"Starting upload")
file_resp = requests.post(
upload_context["upload_url"],
headers={"Authorization": f"Bearer {slack_token}"},
files={'file': open(file_path, "rb")},
)
if not file_resp.content.startswith(b"OK"):
print("Upload problem:", file_resp.status_code, file_resp.content)
print("Wrapping up...")
finalize_resp = requests.post(
upload_finalize_api,
json={
'files': [{
'id': upload_context['file_id'],
'title': os.path.basename(file_path),
}],
'initial_comment': f"Hey <@{user_id}>, it's done! :shark:",
'channel_id': slack_info['channel_id'],
},
headers={"Authorization": f"Bearer {slack_token}"},
)
check_ok(finalize_resp.json())
print("Done!")
Create a file called deepfry.py
.
import valohai
from PIL import Image, ImageFilter
tmpfile = "intermediate.jpg"
def process_image(source_path):
img = Image.open(source_path)
xsize, ysize = img.size
delta = 1
# Make the image smaller
if xsize > 1000:
new_x = 1000
new_y = int(ysize * new_x/xsize)
else:
new_x, new_y = xsize, ysize
if new_y > 1000:
new_new_y = 1000
new_x = int(new_x * new_new_y/new_y)
new_y = new_new_y
else:
new_y, new_x = new_y, new_x
img = img.resize((new_x, new_y))
# Apply some filtering
for iteration in range(5):
img.filter(ImageFilter.EDGE_ENHANCE_MORE)
# Improve the color balance
img = img.convert("RGB")
r, g, b = img.split()
r.point(lambda i: min(i*2, 255))
g.point(lambda i: i*0.3)
b.point(lambda i: i*0.9)
img = Image.merge("RGB", (r, g, b))
# Repeatedly adjust image quality
for iteration in range(50):
left = img.crop((0, 0, delta, new_y))
right = img.crop((delta, 0, new_x, new_y))
img.paste(left, (new_x - delta, 0, new_x, new_y))
img.paste(right, (0, 0, new_x - delta, new_y))
img.save(tmpfile, 'jpeg', quality=10)
img = Image.open(tmpfile)
return img
def main():
processed_image = process_image(valohai.inputs("source_image").path())
# Save final file as an output
processed_image.save(valohai.outputs().path("fried_image.png"))
if __name__ == "__main__":
main()
Add these steps and pipeline definition to your valohai.yaml
:
- step:
name: deepfry-image
image: python:3.11
command:
- pip install valohai-utils pillow
- python ./deepfry.py
inputs:
- name: source_image
- step:
name: slack-slash
image: python:3.11
command:
- pip install valohai-utils requests
- python ./slack_input.py {parameters}
inputs:
- name: slack-webhook-input
optional: true
- step:
name: slack-upload
image: python:3.11
command:
- pip install valohai-utils requests
- python ./slack_output.py {parameters}
inputs:
- name: slack-webhook-input
optional: true
- name: message-attachment
optional: true
- pipeline:
name: Business Deepfrier 2024
edges:
- configuration: {}
source: slack-slash.output.image1.png
target: deepfry.input.source_image
- configuration: {}
source: slack-slash.input.slack-webhook-input
target: slack-upload.input.slack-webhook-input
- configuration: {}
source: deepfry.output.fried_image.png
target: slack-upload.input.message-attachment
nodes:
- name: slack-slash
on-error: stop-all
step: slack-slash
type: execution
- name: deepfry
on-error: stop-all
step: deepfry-image
type: execution
- name: slack-upload
on-error: stop-all
step: slack-upload
type: execution
We can now define the webhook trigger. Go to Project settings -> open Triggers tab -> create New Trigger
- For type, select Webhook
-
Add a Web Request Authentication Condition
- For Auth Type, select “HMAC”
- For Token Lookup Namespace, select “HTTP Header”
- For Token Lookup Key, type
X-Slack-Signature
- For Secret Key, type the Slack Signing Secret from your notes
- For Value Prefix, type
v0=
- For HMAC Basestring Format, enter
v0:{header:X-Slack-Request-Timestamp}:{body}
- For Algorithm, select “SHA256”
-
Add a Web Request Timestamp condition
- For Token Lookup Namespace, select “HTTP Header”
- For Token Lookup Key, type
X-Slack-Request-Timestamp
- Tolerance of 60s is ok.
-
Add a Rate Limit condition
- A Period of 60 seconds with a quota of 2-3 is fine.
- As the slash command can be triggered on Slack any time, a rate limit can help limit excess usage.
- Exceeding the rate limit will cause the webhook endpoint to return a 400 status response, and too many of these during a short time will cause Slack to disable the command. You may need to enable it again in the Slack developer portal.
-
Add a Run Pipeline action
- For Source Commit Reference, type the primary branch of your Valohai project in Git. This is usually either
main
ormaster
- For Pipeline name, type “Business Deepfrier 2024”
- For Payload Input Name, type “slack-slash.slack-webhook-input”
- For Source Commit Reference, type the primary branch of your Valohai project in Git. This is usually either
-
Add a Web Text Response action
- For Response Text, type “Deep frying…”
- This text displayed by Slack as the immediate response to the Slack slash command before the pipeline boots up. Feel free to customize this to add your own touch
-
Save trigger
- You are now on the Triggers list again. Select the … menu of the trigger we just created, and select “Edit” to return to editing the trigger. Get the trigger URL and save it to your notes.
Navigate to your Valohai project’s Environment Variables settings, and add the Bot User OAuth Token from your notes
as SLACK_TOKEN
. You should mark it as a secret.
Slack slash command
Return to the Slack app portal. Select your integration app and navigate to “Slash Commands” on the sidebar.
- Click “Create new command.”
- For Command, type
/deepfry
- For Request URL, enter the Valohai trigger URL from the previous step
- The short description and usage hint are optional.
- Save the slash command.
Invite your bot user to a Slack conversation (DM or channel) in your workspace: /invite @My Slack Integration
Demo: Deep-fry an image
Try it! Post an image you want to deep-fry on a Slack channel you’ve added your integration onto and type /deepfry
.
Request timeout
Slack has a very short timeout for slash command responses, which can sometimes fail the slash command.
Debugging tips
Sometimes triggers fail to launch. You can check the trigger logs (Project settings -> Triggers -> … menu for the trigger -> View logs) to find out more information.
Triggers that repeatedly fail to launch will disable the trigger. To try again, enable the trigger in the trigger edit page. You can also set a notification for failed trigger launches in project notification settings.