Skip to content

Commit

Permalink
feat: Slack chat integration. (#236)
Browse files Browse the repository at this point in the history
This feature is a Docq Slack bot that can be added to a Slack workspace. When invited into a channel, users can [at] mention the bot and ask questions. Docq org admins can configure a channel the bot is in to be associated with a space group.

*adds Slack auth and event handling endpoints
* refactor(UI)(admin section): Rename tab names, drop the "admin prefix"
* bump version v0.9.7 -> v0.10.0
* make the redirect relative to the current domain rather than based on the configured server address.
* refactor(slack): remove defaulting to ENV VARS. set explicitly 

---------

Co-authored-by: Janaka Abeywardhana <[email protected]>
  • Loading branch information
osala-eng and janaka authored Apr 6, 2024
1 parent 7807c82 commit de24797
Show file tree
Hide file tree
Showing 26 changed files with 1,298 additions and 89 deletions.
57 changes: 40 additions & 17 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "docq"
version = "0.9.7"
version = "0.10.0"
description = "Docq.AI - Your private ChatGPT alternative. Securely unlock knowledge from confidential documents."
authors = ["Docq.AI Team <[email protected]>"]
maintainers = ["Docq.AI Team <[email protected]>"]
Expand Down Expand Up @@ -61,6 +61,7 @@ jwt = "^1.3.1"
llama-index-embeddings-huggingface-optimum = "^0.1.4"
llama-index-core = "^0.10.21.post1"
llama-index-readers-file = "^0.1.12"
slack-bolt = "^1.18.1"

[tool.poetry.group.dev.dependencies]
pre-commit = "^2.18.1"
Expand Down
4 changes: 3 additions & 1 deletion source/docq/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
SESSION_COOKIE_NAME = "docqai/_docq"
ENV_VAR_DOCQ_GROQ_API_KEY = "DOCQ_GROQ_API_KEY"


ENV_VAR_DOCQ_SLACK_CLIENT_ID = "DOCQ_SLACK_CLIENT_ID"
ENV_VAR_DOCQ_SLACK_CLIENT_SECRET = "DOCQ_SLACK_CLIENT_SECRET" # noqa: S105
ENV_VAR_DOCQ_SLACK_SIGNING_SECRET = "DOCQ_SLACK_SIGNING_SECRET" # noqa: S105


class SpaceType(Enum):
Expand Down
8 changes: 8 additions & 0 deletions source/docq/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Initialize integrations."""

from .slack import manage_slack, manage_slack_messages


def _init() -> None:
"""Initialize integrations."""
manage_slack._init()
226 changes: 226 additions & 0 deletions source/docq/integrations/slack/manage_slack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"""Manage integrations with third-party services."""

import logging
import sqlite3
from contextlib import closing
from typing import Optional

from docq.config import SpaceType
from docq.domain import SpaceKey
from docq.support.store import get_sqlite_shared_system_file
from slack_sdk.oauth.installation_store import Installation

from .models import SlackChannel, SlackInstallation

SQL_CREATE_DOCQ_SLACK_APPLICATIONS_TABLE = """
CREATE TABLE IF NOT EXISTS docq_slack_installations (
id INTEGER PRIMARY KEY,
app_id TEXT NOT NULL,
team_id TEXT NOT NULL,
team_name TEXT NOT NULL, -- References a slack workspace
org_id INTEGER NOT NULL,
space_group_id INTEGER, -- TODO: Implement globally available content for the entire slack workspace
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (org_id) REFERENCES orgs(id),
FOREIGN KEY (space_group_id) REFERENCES space_groups(id),
UNIQUE (app_id, team_id, org_id)
);
"""

SQL_CREATE_DOCQ_SLACK_CHANNELS_TABLE = """
CREATE TABLE IF NOT EXISTS docq_slack_channels (
id INTEGER PRIMARY KEY,
channel_id TEXT NOT NULL,
channel_name TEXT NOT NULL,
org_id INTEGER NOT NULL,
space_group_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (org_id) REFERENCES orgs(id),
FOREIGN KEY (space_group_id) REFERENCES space_groups(id),
UNIQUE (channel_id, org_id)
);
"""



def _init() -> None:
"""Initialize the Slack integration."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
connection.execute(SQL_CREATE_DOCQ_SLACK_APPLICATIONS_TABLE)
connection.execute(SQL_CREATE_DOCQ_SLACK_CHANNELS_TABLE)
connection.commit()


def create_docq_slack_installation(installation: Installation, org_id: int) -> None:
"""Create a Docq installation."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
connection.execute(
"INSERT OR REPLACE INTO docq_slack_installations (app_id, team_id, team_name, org_id) VALUES (?, ?, ?, ?)",
(installation.app_id, installation.team_id, installation.team_name, org_id),
)
connection.commit()


def update_docq_slack_installation(app_id: str, team_name: str, org_id: int, space_group_id: int) -> None:
"""Update a Docq installation."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
connection.execute(
"UPDATE docq_slack_installations SET space_group_id = ? WHERE app_id = ? AND team_name = ? AND org_id = ?",
(space_group_id, app_id, team_name, org_id),
)
connection.commit()


def list_docq_slack_installations(org_id: Optional[int], team_id: Optional[str]) -> list[SlackInstallation]:
"""List Docq installations."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
if org_id:
criteria = " WHERE org_id = ?"
params = (org_id,)
elif team_id:
criteria = " WHERE team_id = ?"
params = (team_id,)

cursor.execute(
f"SELECT app_id, team_id, team_name, org_id, space_group_id, created_at FROM docq_slack_installations{criteria}",
params,
)
rows = cursor.fetchall()
return [
SlackInstallation(
app_id=row[0], team_id=row[1], team_name=row[2], org_id=row[3], space_group_id=row[4], created_at=row[5]
)
for row in rows
]


# def get_docq_slack_installation(app_id: str, team_id: str, org_id: int) -> SlackInstallation:
# """Get a Docq installation."""
# with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
# cursor = connection.cursor()
# cursor.execute(
# "SELECT app_id, team_id, team_name, org_id, space_group_id, created_at FROM docq_slack_installations WHERE app_id = ? AND team_id = ? AND org_id = ?",
# (app_id, team_id, org_id),
# )
# row = cursor.fetchone()
# return SlackInstallation(
# app_id=row[0], team_id=row[1], team_name=row[2], org_id=row[3], space_group_id=row[4], created_at=row[5]
# )


def integration_exists(app_id: str, team_id: str, selected_org_id: int) -> bool:
"""Check if an integration exists."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT id FROM docq_slack_installations WHERE app_id = ? AND team_id = ? AND org_id = ?",
(app_id, team_id, selected_org_id),
)
return cursor.fetchone() is not None


def insert_or_update_slack_channel(channel_id: str, channel_name: str, org_id: int) -> None:
"""Insert or update a channel."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
connection.execute(
"INSERT OR REPLACE INTO docq_slack_channels (channel_id, channel_name, org_id) VALUES (?, ?, ?)",
(channel_id, channel_name, org_id),
)
connection.commit()


def link_space_group_to_slack_channel(org_id: int, channel_id: str, channel_name: str, space_group_id: int,) -> None:
"""Add a space group to a channel."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
connection.execute(
"INSERT OR REPLACE INTO docq_slack_channels (space_group_id, channel_id, channel_name, org_id) VALUES (?, ?, ?, ?)",
(space_group_id, channel_id, channel_name, org_id),
)
connection.commit()


def get_slack_channel_linked_space_group_id(org_id: int, channel_id: str) -> Optional[int]:
"""Get a channel space group id."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
try:
cursor.execute(
"SELECT space_group_id FROM docq_slack_channels WHERE channel_id = ? AND org_id = ?",
(channel_id, org_id),
)
result = cursor.fetchone()
return result[0] if result is not None else None
except sqlite3.OperationalError:
logging.error("No installations found.")
return None


def list_slack_channels(org_id: int) -> list[SlackChannel]:
"""List Slack channels."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT channel_id, channel_name, org_id, space_group_id, created_at FROM docq_slack_channels WHERE org_id = ?",
(org_id,),
)
rows = cursor.fetchall()
return [
SlackChannel(
channel_id=row[0], channel_name=row[1], org_id=row[2], space_group_id=row[3], created_at=row[4]
)
for row in rows
]


def get_slack_channel(channel_id: str) -> SlackChannel:
"""Get a channel."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT channel_id, channel_name, org_id, space_group_id, created_at FROM docq_slack_channels WHERE channel_id = ?",
(channel_id,),
)
row = cursor.fetchone()
return SlackChannel(
channel_id=row[0], channel_name=row[1], org_id=row[2], space_group_id=row[3], created_at=row[4]
)


def get_slack_bot_token(app_id: str, team_id: str) -> str:
"""Get a bot token."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT bot_token FROM slack_bots WHERE app_id = ? AND team_id = ?", (app_id, team_id)
)
return cursor.fetchone()[0]

def get_rag_spaces(channel_id: str) -> Optional[list[SpaceKey]]:
"""Get a list of spaces configured for the given channel."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
cursor.execute(
"""
SELECT s.id, s.org_id, s.name, s.summary, s.archived, s.datasource_type, s.datasource_configs, s.space_type, s.created_at, s.updated_at
FROM spaces s
JOIN space_group_members ON s.id = space_group_members.space_id
JOIN docq_slack_channels ON space_group_members.group_id = docq_slack_channels.space_group_id
WHERE docq_slack_channels.channel_id = ?
""",
(channel_id,),
)
spaces = cursor.fetchall()

return [ SpaceKey(SpaceType[row[7]], row[0], row[1], row[3]) for row in spaces ] if spaces else None


def get_org_id_from_channel_id(channel_id: str) -> Optional[int]:
"""Get the org id from a channel id."""
with closing(sqlite3.connect(get_sqlite_shared_system_file())) as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT org_id FROM docq_slack_channels WHERE channel_id = ?", (channel_id,)
)
result = cursor.fetchone()
return result[0] if result is not None else None
Loading

0 comments on commit de24797

Please sign in to comment.