Skip to content

Commit 82314fd

Browse files
improvements to the sample
1 parent a39fd0a commit 82314fd

17 files changed

Lines changed: 742 additions & 229 deletions

README.md

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,127 @@
1-
# bolt-python-search-template
2-
A template for implementing enterprise search in Bolt for Python
1+
# Bolt for Python Search Template
2+
3+
> ⚠️ **Beta Notice**: This template demonstrates search functionality that is currently in beta and not yet widely available to all developers. The features shown here are being tested and may change before general availability.
4+
5+
This is a Slack app template for building search functionality using Bolt for Python. It demonstrates how to create custom functions for search and filtering capabilities.
6+
7+
Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create).
8+
9+
## Installation
10+
11+
### Using Slack CLI
12+
13+
Install the latest version of the Slack CLI for your operating system:
14+
15+
- [Slack CLI for macOS & Linux](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux/)
16+
- [Slack CLI for Windows](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-windows/)
17+
18+
You'll also need to log in if this is your first time using the Slack CLI.
19+
20+
```sh
21+
slack login
22+
```
23+
24+
#### Initializing the project
25+
26+
```sh
27+
slack create bolt-python-search --template slack-samples/bolt-python-search-template -branch init
28+
cd bolt-python-search
29+
30+
# Setup your python virtual environment
31+
python3 -m venv .venv
32+
source .venv/bin/activate
33+
34+
# Install the dependencies
35+
pip install .
36+
```
37+
38+
#### Creating the Slack app
39+
40+
```sh
41+
slack install
42+
```
43+
44+
#### Running the app
45+
46+
```sh
47+
slack run
48+
```
49+
50+
<details>
51+
<summary><h3>Using Terminal</h3></summary>
52+
53+
1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest"
54+
2. Choose the workspace you want to install the application to
55+
3. Copy the contents of [manifest.json](./manifest.json) into the text box that says `*Paste your manifest code here*` (within the JSON tab) and click _Next_
56+
4. Review the configuration and click _Create_
57+
5. Click _Install to Workspace_ and _Allow_ on the screen that follows. You'll then be redirected to the App Configuration dashboard.
58+
59+
#### Environment Variables
60+
61+
Before you can run the app, you'll need to store some environment variables.
62+
63+
1. Open your apps configuration page from this list, click **OAuth & Permissions** in the left hand menu, then copy the Bot User OAuth Token. You will store this in your environment as `SLACK_BOT_TOKEN`.
64+
2. Click ***Basic Information** from the left hand menu and follow the steps in the App-Level Tokens section to create an app-level token with the `connections:write` scope. Copy this token. You will store this in your environment as `SLACK_APP_TOKEN`.
65+
66+
```zsh
67+
# Replace with your app token and bot token
68+
export SLACK_BOT_TOKEN=<your-bot-token>
69+
export SLACK_APP_TOKEN=<your-app-token>
70+
```
71+
72+
### Setup Your Local Project
73+
74+
```sh
75+
# Clone this project onto your machine
76+
git clone https://github.com/slack-samples/bolt-python-search-template.git
77+
78+
# Change into this project directory
79+
cd bolt-python-search-template
80+
81+
# Setup your python virtual environment
82+
python3 -m venv .venv
83+
source .venv/bin/activate
84+
85+
# Install the dependencies
86+
pip install .
87+
88+
# Start your local server
89+
python3 app.py
90+
```
91+
92+
</details>
93+
94+
## Linting
95+
96+
```sh
97+
# Run ruff from root directory for linting
98+
ruff check
99+
100+
# Run ruff from root directory for formatting
101+
ruff format
102+
```
103+
104+
## Testing
105+
106+
```sh
107+
# Run pytest from root directory for unit testing
108+
pytest .
109+
```
110+
111+
## Project Structure
112+
113+
### `manifest.json`
114+
115+
`manifest.json` is a configuration for Slack apps. With a manifest, you can create an app with a pre-defined configuration, or adjust the configuration of an existing app.
116+
117+
### `app.py`
118+
119+
`app.py` is the entry point for the application and is the file you'll run to start the server. This project aims to keep this file as thin as possible, primarily using it as a way to route inbound requests.
120+
121+
### `/listeners`
122+
123+
Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming [Events](https://docs.slack.dev/reference/events) requests, `/listeners/functions` handles [custom steps](https://docs.slack.dev/tools/bolt-js/concepts/custom-steps) and so on.
124+
125+
### `/test`
126+
127+
The `/test` directory contains the test suite for this project. It mirrors the structure of the source code, with test files corresponding to their implementation counterparts. For example, tests for files in `/listeners/functions` are located in `/test/listeners/functions`.

app.py

Lines changed: 5 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,16 @@
1-
import json
21
import logging
32
import os
4-
from enum import Enum
5-
from typing import TypedDict, List, Optional, NotRequired
63

7-
from slack_bolt import Ack, App, Complete, Fail
4+
from slack_bolt import App
85
from slack_bolt.adapter.socket_mode import SocketModeHandler
9-
from slack_sdk import WebClient
106

11-
logging.basicConfig(level=logging.INFO)
12-
13-
#----------------------
14-
# MODELS
15-
#----------------------
16-
17-
class FilterType(Enum):
18-
MULTI_SELECT = "multi_select"
19-
TOGGLE = "toggle"
20-
21-
class EntityReference(TypedDict):
22-
id: str
23-
type: Optional[str]
24-
25-
class SearchResult(TypedDict):
26-
title: str
27-
description: str
28-
link: str
29-
date_updated: str
30-
external_ref: EntityReference
31-
content: NotRequired[str]
32-
33-
34-
class FilterOptions(TypedDict):
35-
name: str
36-
value: str
37-
38-
39-
class SearchFilter(TypedDict):
40-
name: str
41-
display_name: str
42-
filter_type: FilterType
43-
options: Optional[List[FilterOptions]]
44-
45-
46-
client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN"), base_url="https://slack.com/api/")
47-
app = App(client=client)
48-
49-
50-
@app.function("search", auto_acknowledge=False)
51-
def handle_search_step_event(
52-
ack: Ack, inputs: dict, body: dict, fail: Fail, complete: Complete, logger: logging.Logger
53-
):
54-
55-
try:
56-
query = inputs.get("query")
57-
languages_filter = inputs.get("filters", {}).get("languages", [])
58-
type_filter = inputs.get("filters", {}).get("type", [])
59-
60-
filters_payload = {}
61-
if languages_filter:
62-
filters_payload["languages"] = languages_filter
63-
if type_filter:
64-
if len(type_filter) == 1:
65-
filters_payload["type"] = type_filter[0]
66-
67-
params = {
68-
"filters": json.dumps(filters_payload)
69-
}
70-
if query:
71-
params["query"] = query
72-
73-
logger.info(f"Calling developer.sampleData.get with filters: {filters_payload}")
74-
75-
response = client.api_call(
76-
api_method="developer.sampleData.get",
77-
params=params
78-
)
7+
from listeners import register_listeners
798

80-
if not response.get("ok"):
81-
logger.error(f"API error: {response}")
82-
fail(error="Failed to fetch sample data")
83-
return
84-
85-
samples = response.get("samples", [])
86-
87-
results: List[SearchResult] = [
88-
{
89-
"title": sample["title"],
90-
"description": sample["description"],
91-
"link": sample["link"],
92-
"date_updated": sample["date_updated"],
93-
"external_ref": sample["external_ref"],
94-
**({"content": sample["content"]} if "content" in sample else {}),
95-
}
96-
for sample in samples
97-
]
98-
99-
complete(outputs={"search_result": results})
100-
101-
except Exception as e:
102-
logger.error(f"Error in search step: {str(e)}")
103-
fail(error=str(e))
104-
finally:
105-
ack()
106-
107-
108-
@app.function("filters", auto_acknowledge=False)
109-
def handle_filters_step_event(
110-
ack: Ack, inputs: dict, fail: Fail, complete: Complete, logger: logging.Logger
111-
):
112-
try:
113-
filters: List[SearchFilter] = [
114-
{
115-
"name": "languages",
116-
"display_name": "Languages",
117-
"type": FilterType.MULTI_SELECT.value,
118-
"options": [
119-
{"name": "Python", "value": "python"},
120-
{"name": "Java", "value": "java"},
121-
{"name": "JavaScript", "value": "javascript"},
122-
{"name": "TypeScript", "value": "typescript"},
123-
],
124-
},
125-
{
126-
"name": "type",
127-
"display_name": "Type",
128-
"type": FilterType.MULTI_SELECT.value,
129-
"options": [
130-
{"name": "Template", "value": "template"},
131-
{"name": "Sample", "value": "sample"},
132-
],
133-
},
134-
]
135-
136-
complete(outputs={"filters": filters})
137-
finally:
138-
ack()
139-
140-
@app.event("entity_details_requested")
141-
def handle_flexpane_event(event, body, client, logger):
142-
sample_id = event["external_ref"]["id"]
143-
144-
response = client.api_call(api_method="developer.sampleData.get")
145-
146-
if not response.get("ok"):
147-
logger.error(f"Failed to fetch samples: {response}")
148-
return
149-
150-
samples = response.get("samples", [])
151-
152-
sample = next((s for s in samples if s["external_ref"]["id"] == sample_id), None)
153-
154-
if not sample:
155-
logger.warning(f"No matching sample found for id={sample_id}")
156-
return
157-
158-
159-
custom_fields = [
160-
{
161-
"key": "description",
162-
"label": "Description of sample",
163-
"type": "string",
164-
"value": sample["description"],
165-
},
166-
{
167-
"key": "date_updated",
168-
"label": "Last updated",
169-
"type": "string",
170-
"value": sample["date_updated"],
171-
},
172-
]
173-
174-
if "content" in sample:
175-
custom_fields.append({
176-
"key": "content",
177-
"label": "Details of sample",
178-
"type": "string",
179-
"value": sample["content"],
180-
})
9+
logging.basicConfig(level=logging.INFO)
18110

182-
payload = {
183-
"trigger_id": event["trigger_id"],
184-
"metadata": {
185-
"entity_type": "slack#/entities/item",
186-
"url": event["link"]["url"],
187-
"external_ref": {"id": sample_id},
188-
"entity_payload": {
189-
"attributes": {
190-
"title": {
191-
"text": sample["title"],
192-
"edit": {
193-
"enabled": False,
194-
"text": {
195-
"max_length": 50
196-
}
197-
}
198-
},
199-
},
200-
"custom_fields": custom_fields,
201-
},
202-
},
203-
}
11+
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
20412

205-
try:
206-
client.api_call(
207-
api_method="entity.presentDetails",
208-
json=payload,
209-
)
210-
except Exception as e:
211-
logger.error(f"Error calling entity.presentDetails: {str(e)}")
13+
register_listeners(app)
21214

21315
if __name__ == "__main__":
21416
SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).start()

listeners/__init__.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
from slack_bolt import App
1+
from listeners import events, functions
22

3-
from .functions import handle_search_event, handle_filters_event
43

5-
6-
def register_listeners(app: App):
7-
app.function("search", auto_acknowledge=False, ack_timeout=10)(handle_search_event)
8-
app.function("filters", auto_acknowledge=False)(handle_filters_event)
4+
def register_listeners(app):
5+
functions.register(app)
6+
events.register(app)

listeners/events/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from slack_bolt import App
2+
3+
from .entity_details_requested import entity_details_requested_callback
4+
5+
6+
def register(app: App):
7+
app.event("entity_details_requested")(entity_details_requested_callback)

0 commit comments

Comments
 (0)