From f28b8c60febeabc4d478757f17c8d4cbe1748ee0 Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Fri, 30 Jan 2026 17:22:18 -0500 Subject: [PATCH 1/3] Slack Bot template app --- extensions/slack-bot/.env.example | 8 ++ extensions/slack-bot/.gitignore | 4 + extensions/slack-bot/README.md | 61 +++++++++++++ extensions/slack-bot/app/__init__.py | 1 + extensions/slack-bot/app/api.py | 60 +++++++++++++ extensions/slack-bot/app/chat_ui.py | 121 ++++++++++++++++++++++++++ extensions/slack-bot/app/config.py | 13 +++ extensions/slack-bot/app/main.py | 33 +++++++ extensions/slack-bot/app/responder.py | 22 +++++ extensions/slack-bot/pyproject.toml | 27 ++++++ 10 files changed, 350 insertions(+) create mode 100644 extensions/slack-bot/.env.example create mode 100644 extensions/slack-bot/.gitignore create mode 100644 extensions/slack-bot/README.md create mode 100644 extensions/slack-bot/app/__init__.py create mode 100644 extensions/slack-bot/app/api.py create mode 100644 extensions/slack-bot/app/chat_ui.py create mode 100644 extensions/slack-bot/app/config.py create mode 100644 extensions/slack-bot/app/main.py create mode 100644 extensions/slack-bot/app/responder.py create mode 100644 extensions/slack-bot/pyproject.toml diff --git a/extensions/slack-bot/.env.example b/extensions/slack-bot/.env.example new file mode 100644 index 00000000..b19aee5f --- /dev/null +++ b/extensions/slack-bot/.env.example @@ -0,0 +1,8 @@ +# Slack Bot Token (xoxb-...) +# Get from: OAuth & Permissions > Bot User OAuth Token +SLACK_BOT_TOKEN=xoxb-your-token-here + +# Slack App Token for Socket Mode (xapp-...) +# Get from: Settings > Basic Information > App-Level Tokens +# Needs connections:write scope +SLACK_APP_TOKEN=xapp-your-token-here diff --git a/extensions/slack-bot/.gitignore b/extensions/slack-bot/.gitignore new file mode 100644 index 00000000..793e4176 --- /dev/null +++ b/extensions/slack-bot/.gitignore @@ -0,0 +1,4 @@ +.env +.venv +__pycache__/ +*.pyc diff --git a/extensions/slack-bot/README.md b/extensions/slack-bot/README.md new file mode 100644 index 00000000..20ca42f1 --- /dev/null +++ b/extensions/slack-bot/README.md @@ -0,0 +1,61 @@ +# Slack Bot + +A basic Slack bot using Socket Mode with a web chat interface. + +The bot here just mirrors back your last word as a question, to demonstrate that it's a good listener. Customize the behavior in `app/responder.py` to do whatever you'd like. + +## Routes + +- `/` - Web chat interface (Shiny) +- `/api/health` - Health check endpoint + +## Slack App Setup + +1. Create a new app at https://api.slack.com/apps (From scratch) + +2. **Enable Socket Mode** + - Settings → Socket Mode → Enable + - Create an App-Level Token with `connections:write` scope + - Save this token as `SLACK_APP_TOKEN` (starts with `xapp-`) + +3. **Add Bot Token Scopes** (OAuth & Permissions → Scopes → Bot Token Scopes) + - `app_mentions:read` - receive @-mention events + - `chat:write` - send messages + - `im:history` - receive DM messages + - `im:read` - access DM channel info + - `im:write` - send DMs + +4. **Subscribe to Events** (Event Subscriptions → Subscribe to bot events) + - `app_mention` - when someone @-mentions the bot + - `message.im` - DMs to the bot + +5. **Install to Workspace** + - OAuth & Permissions → Install to Workspace + - Copy the Bot User OAuth Token as `SLACK_BOT_TOKEN` (starts with `xoxb-`) + +## Running Locally + +```bash +# Install dependencies +uv sync + +# Configure environment +cp .env.example .env +# Edit .env with your tokens + +# Run +uv run uvicorn app.main:app --reload +``` + +## Deploying to Posit Connect + +Set `min_processes` to 1 in the runtime settings so the app stays running and listens for Slack messages. Otherwise, the app will go idle and stop responding to Slack. + +## Usage + +**Web chat:** +- Open http://localhost:8000 in your browser + +**Slack:** +- Mention the bot in a channel: `@bot I feel sad` → `Sad?` +- DM the bot: `My dog is sick` → `Sick?` diff --git a/extensions/slack-bot/app/__init__.py b/extensions/slack-bot/app/__init__.py new file mode 100644 index 00000000..0ac5a3ba --- /dev/null +++ b/extensions/slack-bot/app/__init__.py @@ -0,0 +1 @@ +# Slack bot application diff --git a/extensions/slack-bot/app/api.py b/extensions/slack-bot/app/api.py new file mode 100644 index 00000000..d98c552e --- /dev/null +++ b/extensions/slack-bot/app/api.py @@ -0,0 +1,60 @@ +import re + +from fastapi import FastAPI +from slack_bolt.async_app import AsyncApp +from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler + +from app.config import settings +from app.responder import generate_response + +# Initialize Slack app (started by main.py lifespan) +slack_app = AsyncApp(token=settings.slack_bot_token) + + +def create_socket_handler() -> AsyncSocketModeHandler: + """Create the Socket Mode handler for Slack.""" + return AsyncSocketModeHandler(slack_app, settings.slack_app_token) + + +def strip_bot_mention(text: str) -> str: + """Remove bot @-mention from message text.""" + return re.sub(r"<@[A-Z0-9]+>\s*", "", text).strip() + + +# Slack event handlers + +@slack_app.event("app_mention") +async def handle_mention(event, say, client): + """Handle @-mentions in channels and threads.""" + thread_ts = event.get("thread_ts") or event["ts"] + user_message = strip_bot_mention(event["text"]) + + response = generate_response(user_message) + await say(text=response, thread_ts=thread_ts) + + +@slack_app.event("message") +async def handle_dm(event, say, client): + """Handle DMs to the bot.""" + # Only handle DMs + if event.get("channel_type") != "im": + return + + # Ignore bot messages and subtypes (edits, etc.) + if event.get("bot_id") or event.get("subtype"): + return + + user_message = event["text"] + + response = generate_response(user_message) + await say(text=response) + + +# FastAPI app (just health check) +fastapi_app = FastAPI() + + +@fastapi_app.get("/health") +async def health(): + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/extensions/slack-bot/app/chat_ui.py b/extensions/slack-bot/app/chat_ui.py new file mode 100644 index 00000000..a7ab7387 --- /dev/null +++ b/extensions/slack-bot/app/chat_ui.py @@ -0,0 +1,121 @@ +import os + +from shiny.express import ui +from shinychat.express import Chat +from slack_sdk import WebClient + +from app.responder import generate_response + +# Check for Slack tokens +slack_bot_token = os.environ.get("SLACK_BOT_TOKEN") +slack_app_token = os.environ.get("SLACK_APP_TOKEN") +slack_configured = bool(slack_bot_token and slack_app_token) + +# Get bot name if configured +bot_name = None +if slack_configured: + try: + client = WebClient(token=slack_bot_token) + auth_response = client.auth_test() + bot_name = auth_response.get("user") + except Exception: + pass + +ui.page_opts(fillable=True, title="Slack Bot") + +with ui.sidebar(width=350): + ui.h3("Slack Bot") + + ui.markdown( + """ +This is a basic Slack bot with a web chat interface. + +The bot mirrors back your last word as a question, demonstrating +that it's a good listener. Customize the behavior in +`app/responder.py` to do whatever you'd like. + +**Usage:** +- Chat here in the web interface +- @mention the bot in Slack +- DM the bot directly +""" + ) + + if slack_configured: + status_text = f"Slack connected as @{bot_name}" if bot_name else "Slack connected" + ui.div( + ui.tags.span(status_text, style="color: green; font-weight: bold;"), + style="margin: 1em 0; padding: 0.5em; background: #e8f5e9; border-radius: 4px;", + ) + else: + ui.div( + ui.markdown( + """ +**Slack not configured** + +Set these environment variables: +- `SLACK_BOT_TOKEN` (xoxb-...) +- `SLACK_APP_TOKEN` (xapp-...) +""" + ), + style="margin: 1em 0; padding: 0.5em; background: #ffebee; border-radius: 4px;", + ) + + ui.HTML( + """ +
+Posit Connect +

Set min_processes to 1 in runtime settings so the app stays running and listens for Slack messages.

+
+ +
+Slack App Setup + +
    +
  1. Create a new app at api.slack.com/apps (From scratch)

  2. + +
  3. Enable Socket Mode

    +
      +
    • Settings → Socket Mode → Enable
    • +
    • Create an App-Level Token with connections:write scope
    • +
    • Save as SLACK_APP_TOKEN (starts with xapp-)
    • +
  4. + +
  5. Add Bot Token Scopes (OAuth & Permissions → Bot Token Scopes)

    +
      +
    • app_mentions:read
    • +
    • chat:write
    • +
    • im:history
    • +
    • im:read
    • +
    • im:write
    • +
  6. + +
  7. Subscribe to Events (Event Subscriptions → Subscribe to bot events)

    +
      +
    • app_mention
    • +
    • message.im
    • +
  8. + +
  9. Install to Workspace

    +
      +
    • OAuth & Permissions → Install to Workspace
    • +
    • Copy Bot User OAuth Token as SLACK_BOT_TOKEN (starts with xoxb-)
    • +
  10. +
+
+""" + ) + +chat = Chat(id="chat") + +chat.ui( + messages=[ + {"content": "Hello! How can I help you?", "role": "assistant"}, + ], +) + + +@chat.on_user_submit +async def handle_user_input(user_input: str): + response = generate_response(user_input) + await chat.append_message(response) diff --git a/extensions/slack-bot/app/config.py b/extensions/slack-bot/app/config.py new file mode 100644 index 00000000..fd30920a --- /dev/null +++ b/extensions/slack-bot/app/config.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # Slack tokens + slack_bot_token: str # xoxb-... + slack_app_token: str # xapp-... (for Socket Mode) + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/extensions/slack-bot/app/main.py b/extensions/slack-bot/app/main.py new file mode 100644 index 00000000..ab4ba9d0 --- /dev/null +++ b/extensions/slack-bot/app/main.py @@ -0,0 +1,33 @@ +import asyncio +from contextlib import asynccontextmanager +from pathlib import Path + +from starlette.applications import Starlette +from starlette.routing import Mount +from shiny.express import wrap_express_app + +from app.api import fastapi_app, create_socket_handler + +# Wrap the Shiny Express app for mounting +chat_ui_path = Path(__file__).parent / "chat_ui.py" +shiny_app = wrap_express_app(chat_ui_path) + + +@asynccontextmanager +async def lifespan(app): + """Start Slack Socket Mode handler on startup.""" + handler = create_socket_handler() + asyncio.create_task(handler.start_async()) + print("Slack bot started!") + + yield + + await handler.close_async() + + +routes = [ + Mount("/api", app=fastapi_app), + Mount("/", app=shiny_app), +] + +app = Starlette(routes=routes, lifespan=lifespan) diff --git a/extensions/slack-bot/app/responder.py b/extensions/slack-bot/app/responder.py new file mode 100644 index 00000000..13413c12 --- /dev/null +++ b/extensions/slack-bot/app/responder.py @@ -0,0 +1,22 @@ +"""Message response generation.""" + + +def generate_response(message: str, context: list[dict] | None = None) -> str: + """Generate a response to a user message. + + Args: + message: The user's message text. + context: Optional conversation history (for future use). + + Returns: + The response text. + """ + return _get_last_word_question(message) + + +def _get_last_word_question(text: str) -> str: + """Extract the last word and return it as a question.""" + words = text.strip().rstrip("?!.,").split() + if words: + return f"{words[-1].capitalize()}?" + return "Hmm?" diff --git a/extensions/slack-bot/pyproject.toml b/extensions/slack-bot/pyproject.toml new file mode 100644 index 00000000..f2ba9280 --- /dev/null +++ b/extensions/slack-bot/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "slack-bot" +version = "0.1.0" +description = "A Slack bot using Socket Mode" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.109.0", + "uvicorn>=0.27.0", + "slack-bolt>=1.18.0", + "aiohttp>=3.9.0", + "pydantic-settings>=2.1.0", + "shiny>=1.0.0", + "shinychat>=0.1.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["app"] From 01d539e08ea1e4c6d09e7cfbefa40efc55f724b5 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:12:27 -0500 Subject: [PATCH 2/3] flatten app directory structure to match other extensions --- extensions/slack-bot/api.py | 60 +++++++++++++++ extensions/slack-bot/chat_ui.py | 121 ++++++++++++++++++++++++++++++ extensions/slack-bot/config.py | 13 ++++ extensions/slack-bot/main.py | 33 ++++++++ extensions/slack-bot/responder.py | 22 ++++++ 5 files changed, 249 insertions(+) create mode 100644 extensions/slack-bot/api.py create mode 100644 extensions/slack-bot/chat_ui.py create mode 100644 extensions/slack-bot/config.py create mode 100644 extensions/slack-bot/main.py create mode 100644 extensions/slack-bot/responder.py diff --git a/extensions/slack-bot/api.py b/extensions/slack-bot/api.py new file mode 100644 index 00000000..24a73f79 --- /dev/null +++ b/extensions/slack-bot/api.py @@ -0,0 +1,60 @@ +import re + +from fastapi import FastAPI +from slack_bolt.async_app import AsyncApp +from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler + +from config import settings +from responder import generate_response + +# Initialize Slack app (started by main.py lifespan) +slack_app = AsyncApp(token=settings.slack_bot_token) + + +def create_socket_handler() -> AsyncSocketModeHandler: + """Create the Socket Mode handler for Slack.""" + return AsyncSocketModeHandler(slack_app, settings.slack_app_token) + + +def strip_bot_mention(text: str) -> str: + """Remove bot @-mention from message text.""" + return re.sub(r"<@[A-Z0-9]+>\s*", "", text).strip() + + +# Slack event handlers + +@slack_app.event("app_mention") +async def handle_mention(event, say, client): + """Handle @-mentions in channels and threads.""" + thread_ts = event.get("thread_ts") or event["ts"] + user_message = strip_bot_mention(event["text"]) + + response = generate_response(user_message) + await say(text=response, thread_ts=thread_ts) + + +@slack_app.event("message") +async def handle_dm(event, say, client): + """Handle DMs to the bot.""" + # Only handle DMs + if event.get("channel_type") != "im": + return + + # Ignore bot messages and subtypes (edits, etc.) + if event.get("bot_id") or event.get("subtype"): + return + + user_message = event["text"] + + response = generate_response(user_message) + await say(text=response) + + +# FastAPI app (just health check) +fastapi_app = FastAPI() + + +@fastapi_app.get("/health") +async def health(): + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/extensions/slack-bot/chat_ui.py b/extensions/slack-bot/chat_ui.py new file mode 100644 index 00000000..1e7e55e6 --- /dev/null +++ b/extensions/slack-bot/chat_ui.py @@ -0,0 +1,121 @@ +import os + +from shiny.express import ui +from shinychat.express import Chat +from slack_sdk import WebClient + +from responder import generate_response + +# Check for Slack tokens +slack_bot_token = os.environ.get("SLACK_BOT_TOKEN") +slack_app_token = os.environ.get("SLACK_APP_TOKEN") +slack_configured = bool(slack_bot_token and slack_app_token) + +# Get bot name if configured +bot_name = None +if slack_configured: + try: + client = WebClient(token=slack_bot_token) + auth_response = client.auth_test() + bot_name = auth_response.get("user") + except Exception: + pass + +ui.page_opts(fillable=True, title="Slack Bot") + +with ui.sidebar(width=350): + ui.h3("Slack Bot") + + ui.markdown( + """ +This is a basic Slack bot with a web chat interface. + +The bot mirrors back your last word as a question, demonstrating +that it's a good listener. Customize the behavior in +`responder.py` to do whatever you'd like. + +**Usage:** +- Chat here in the web interface +- @mention the bot in Slack +- DM the bot directly +""" + ) + + if slack_configured: + status_text = f"Slack connected as @{bot_name}" if bot_name else "Slack connected" + ui.div( + ui.tags.span(status_text, style="color: green; font-weight: bold;"), + style="margin: 1em 0; padding: 0.5em; background: #e8f5e9; border-radius: 4px;", + ) + else: + ui.div( + ui.markdown( + """ +**Slack not configured** + +Set these environment variables: +- `SLACK_BOT_TOKEN` (xoxb-...) +- `SLACK_APP_TOKEN` (xapp-...) +""" + ), + style="margin: 1em 0; padding: 0.5em; background: #ffebee; border-radius: 4px;", + ) + + ui.HTML( + """ +
+Posit Connect +

Set min_processes to 1 in runtime settings so the app stays running and listens for Slack messages.

+
+ +
+Slack App Setup + +
    +
  1. Create a new app at api.slack.com/apps (From scratch)

  2. + +
  3. Enable Socket Mode

    +
      +
    • Settings → Socket Mode → Enable
    • +
    • Create an App-Level Token with connections:write scope
    • +
    • Save as SLACK_APP_TOKEN (starts with xapp-)
    • +
  4. + +
  5. Add Bot Token Scopes (OAuth & Permissions → Bot Token Scopes)

    +
      +
    • app_mentions:read
    • +
    • chat:write
    • +
    • im:history
    • +
    • im:read
    • +
    • im:write
    • +
  6. + +
  7. Subscribe to Events (Event Subscriptions → Subscribe to bot events)

    +
      +
    • app_mention
    • +
    • message.im
    • +
  8. + +
  9. Install to Workspace

    +
      +
    • OAuth & Permissions → Install to Workspace
    • +
    • Copy Bot User OAuth Token as SLACK_BOT_TOKEN (starts with xoxb-)
    • +
  10. +
+
+""" + ) + +chat = Chat(id="chat") + +chat.ui( + messages=[ + {"content": "Hello! How can I help you?", "role": "assistant"}, + ], +) + + +@chat.on_user_submit +async def handle_user_input(user_input: str): + response = generate_response(user_input) + await chat.append_message(response) diff --git a/extensions/slack-bot/config.py b/extensions/slack-bot/config.py new file mode 100644 index 00000000..fd30920a --- /dev/null +++ b/extensions/slack-bot/config.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # Slack tokens + slack_bot_token: str # xoxb-... + slack_app_token: str # xapp-... (for Socket Mode) + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/extensions/slack-bot/main.py b/extensions/slack-bot/main.py new file mode 100644 index 00000000..f6ebb433 --- /dev/null +++ b/extensions/slack-bot/main.py @@ -0,0 +1,33 @@ +import asyncio +from contextlib import asynccontextmanager +from pathlib import Path + +from starlette.applications import Starlette +from starlette.routing import Mount +from shiny.express import wrap_express_app + +from api import fastapi_app, create_socket_handler + +# Wrap the Shiny Express app for mounting +chat_ui_path = Path(__file__).parent / "chat_ui.py" +shiny_app = wrap_express_app(chat_ui_path) + + +@asynccontextmanager +async def lifespan(app): + """Start Slack Socket Mode handler on startup.""" + handler = create_socket_handler() + asyncio.create_task(handler.start_async()) + print("Slack bot started!") + + yield + + await handler.close_async() + + +routes = [ + Mount("/api", app=fastapi_app), + Mount("/", app=shiny_app), +] + +app = Starlette(routes=routes, lifespan=lifespan) diff --git a/extensions/slack-bot/responder.py b/extensions/slack-bot/responder.py new file mode 100644 index 00000000..13413c12 --- /dev/null +++ b/extensions/slack-bot/responder.py @@ -0,0 +1,22 @@ +"""Message response generation.""" + + +def generate_response(message: str, context: list[dict] | None = None) -> str: + """Generate a response to a user message. + + Args: + message: The user's message text. + context: Optional conversation history (for future use). + + Returns: + The response text. + """ + return _get_last_word_question(message) + + +def _get_last_word_question(text: str) -> str: + """Extract the last word and return it as a question.""" + words = text.strip().rstrip("?!.,").split() + if words: + return f"{words[-1].capitalize()}?" + return "Hmm?" From 93fafae5c1e0a95873eb8638f2a5e29b2bc4f3c2 Mon Sep 17 00:00:00 2001 From: Jules Walzer-Goldfeld <54960783+juleswg23@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:13:16 -0500 Subject: [PATCH 3/3] remove extensions/slack-bot/app subdirectory --- extensions/slack-bot/app/__init__.py | 1 - extensions/slack-bot/app/api.py | 60 ------------- extensions/slack-bot/app/chat_ui.py | 121 -------------------------- extensions/slack-bot/app/config.py | 13 --- extensions/slack-bot/app/main.py | 33 ------- extensions/slack-bot/app/responder.py | 22 ----- 6 files changed, 250 deletions(-) delete mode 100644 extensions/slack-bot/app/__init__.py delete mode 100644 extensions/slack-bot/app/api.py delete mode 100644 extensions/slack-bot/app/chat_ui.py delete mode 100644 extensions/slack-bot/app/config.py delete mode 100644 extensions/slack-bot/app/main.py delete mode 100644 extensions/slack-bot/app/responder.py diff --git a/extensions/slack-bot/app/__init__.py b/extensions/slack-bot/app/__init__.py deleted file mode 100644 index 0ac5a3ba..00000000 --- a/extensions/slack-bot/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Slack bot application diff --git a/extensions/slack-bot/app/api.py b/extensions/slack-bot/app/api.py deleted file mode 100644 index d98c552e..00000000 --- a/extensions/slack-bot/app/api.py +++ /dev/null @@ -1,60 +0,0 @@ -import re - -from fastapi import FastAPI -from slack_bolt.async_app import AsyncApp -from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler - -from app.config import settings -from app.responder import generate_response - -# Initialize Slack app (started by main.py lifespan) -slack_app = AsyncApp(token=settings.slack_bot_token) - - -def create_socket_handler() -> AsyncSocketModeHandler: - """Create the Socket Mode handler for Slack.""" - return AsyncSocketModeHandler(slack_app, settings.slack_app_token) - - -def strip_bot_mention(text: str) -> str: - """Remove bot @-mention from message text.""" - return re.sub(r"<@[A-Z0-9]+>\s*", "", text).strip() - - -# Slack event handlers - -@slack_app.event("app_mention") -async def handle_mention(event, say, client): - """Handle @-mentions in channels and threads.""" - thread_ts = event.get("thread_ts") or event["ts"] - user_message = strip_bot_mention(event["text"]) - - response = generate_response(user_message) - await say(text=response, thread_ts=thread_ts) - - -@slack_app.event("message") -async def handle_dm(event, say, client): - """Handle DMs to the bot.""" - # Only handle DMs - if event.get("channel_type") != "im": - return - - # Ignore bot messages and subtypes (edits, etc.) - if event.get("bot_id") or event.get("subtype"): - return - - user_message = event["text"] - - response = generate_response(user_message) - await say(text=response) - - -# FastAPI app (just health check) -fastapi_app = FastAPI() - - -@fastapi_app.get("/health") -async def health(): - """Health check endpoint.""" - return {"status": "healthy"} diff --git a/extensions/slack-bot/app/chat_ui.py b/extensions/slack-bot/app/chat_ui.py deleted file mode 100644 index a7ab7387..00000000 --- a/extensions/slack-bot/app/chat_ui.py +++ /dev/null @@ -1,121 +0,0 @@ -import os - -from shiny.express import ui -from shinychat.express import Chat -from slack_sdk import WebClient - -from app.responder import generate_response - -# Check for Slack tokens -slack_bot_token = os.environ.get("SLACK_BOT_TOKEN") -slack_app_token = os.environ.get("SLACK_APP_TOKEN") -slack_configured = bool(slack_bot_token and slack_app_token) - -# Get bot name if configured -bot_name = None -if slack_configured: - try: - client = WebClient(token=slack_bot_token) - auth_response = client.auth_test() - bot_name = auth_response.get("user") - except Exception: - pass - -ui.page_opts(fillable=True, title="Slack Bot") - -with ui.sidebar(width=350): - ui.h3("Slack Bot") - - ui.markdown( - """ -This is a basic Slack bot with a web chat interface. - -The bot mirrors back your last word as a question, demonstrating -that it's a good listener. Customize the behavior in -`app/responder.py` to do whatever you'd like. - -**Usage:** -- Chat here in the web interface -- @mention the bot in Slack -- DM the bot directly -""" - ) - - if slack_configured: - status_text = f"Slack connected as @{bot_name}" if bot_name else "Slack connected" - ui.div( - ui.tags.span(status_text, style="color: green; font-weight: bold;"), - style="margin: 1em 0; padding: 0.5em; background: #e8f5e9; border-radius: 4px;", - ) - else: - ui.div( - ui.markdown( - """ -**Slack not configured** - -Set these environment variables: -- `SLACK_BOT_TOKEN` (xoxb-...) -- `SLACK_APP_TOKEN` (xapp-...) -""" - ), - style="margin: 1em 0; padding: 0.5em; background: #ffebee; border-radius: 4px;", - ) - - ui.HTML( - """ -
-Posit Connect -

Set min_processes to 1 in runtime settings so the app stays running and listens for Slack messages.

-
- -
-Slack App Setup - -
    -
  1. Create a new app at api.slack.com/apps (From scratch)

  2. - -
  3. Enable Socket Mode

    -
      -
    • Settings → Socket Mode → Enable
    • -
    • Create an App-Level Token with connections:write scope
    • -
    • Save as SLACK_APP_TOKEN (starts with xapp-)
    • -
  4. - -
  5. Add Bot Token Scopes (OAuth & Permissions → Bot Token Scopes)

    -
      -
    • app_mentions:read
    • -
    • chat:write
    • -
    • im:history
    • -
    • im:read
    • -
    • im:write
    • -
  6. - -
  7. Subscribe to Events (Event Subscriptions → Subscribe to bot events)

    -
      -
    • app_mention
    • -
    • message.im
    • -
  8. - -
  9. Install to Workspace

    -
      -
    • OAuth & Permissions → Install to Workspace
    • -
    • Copy Bot User OAuth Token as SLACK_BOT_TOKEN (starts with xoxb-)
    • -
  10. -
-
-""" - ) - -chat = Chat(id="chat") - -chat.ui( - messages=[ - {"content": "Hello! How can I help you?", "role": "assistant"}, - ], -) - - -@chat.on_user_submit -async def handle_user_input(user_input: str): - response = generate_response(user_input) - await chat.append_message(response) diff --git a/extensions/slack-bot/app/config.py b/extensions/slack-bot/app/config.py deleted file mode 100644 index fd30920a..00000000 --- a/extensions/slack-bot/app/config.py +++ /dev/null @@ -1,13 +0,0 @@ -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - # Slack tokens - slack_bot_token: str # xoxb-... - slack_app_token: str # xapp-... (for Socket Mode) - - class Config: - env_file = ".env" - - -settings = Settings() diff --git a/extensions/slack-bot/app/main.py b/extensions/slack-bot/app/main.py deleted file mode 100644 index ab4ba9d0..00000000 --- a/extensions/slack-bot/app/main.py +++ /dev/null @@ -1,33 +0,0 @@ -import asyncio -from contextlib import asynccontextmanager -from pathlib import Path - -from starlette.applications import Starlette -from starlette.routing import Mount -from shiny.express import wrap_express_app - -from app.api import fastapi_app, create_socket_handler - -# Wrap the Shiny Express app for mounting -chat_ui_path = Path(__file__).parent / "chat_ui.py" -shiny_app = wrap_express_app(chat_ui_path) - - -@asynccontextmanager -async def lifespan(app): - """Start Slack Socket Mode handler on startup.""" - handler = create_socket_handler() - asyncio.create_task(handler.start_async()) - print("Slack bot started!") - - yield - - await handler.close_async() - - -routes = [ - Mount("/api", app=fastapi_app), - Mount("/", app=shiny_app), -] - -app = Starlette(routes=routes, lifespan=lifespan) diff --git a/extensions/slack-bot/app/responder.py b/extensions/slack-bot/app/responder.py deleted file mode 100644 index 13413c12..00000000 --- a/extensions/slack-bot/app/responder.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Message response generation.""" - - -def generate_response(message: str, context: list[dict] | None = None) -> str: - """Generate a response to a user message. - - Args: - message: The user's message text. - context: Optional conversation history (for future use). - - Returns: - The response text. - """ - return _get_last_word_question(message) - - -def _get_last_word_question(text: str) -> str: - """Extract the last word and return it as a question.""" - words = text.strip().rstrip("?!.,").split() - if words: - return f"{words[-1].capitalize()}?" - return "Hmm?"