Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added slack chat integration. #236

Merged
merged 47 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8e87e61
feat: Added slack chat integration.
osala-eng Mar 8, 2024
e679932
refactor!: Move slack application under api.
osala-eng Mar 12, 2024
906408c
feat: Add installable slack application.
osala-eng Mar 15, 2024
3ef4a5a
chore: Setup integrations tracking and linking to spaces.
osala-eng Mar 15, 2024
c4c23af
chore: Rename slack installations.
osala-eng Mar 15, 2024
0d7606d
chore: Update slack integration configs.
osala-eng Mar 19, 2024
bde246a
chore: Update docq slack integration models.
osala-eng Mar 19, 2024
6e4bfa1
chore: Add a custom slack oauth flow.
osala-eng Mar 19, 2024
198cc3b
chore: Update slack oauth flow handler.
osala-eng Mar 19, 2024
c54617f
chore: Handle app state config.
osala-eng Mar 19, 2024
4deb500
chore: Cleanup extra comments.
osala-eng Mar 19, 2024
c9a79c5
chore: Setup installation flow.
osala-eng Mar 19, 2024
88029e6
chore: Update integrations UI.
osala-eng Mar 19, 2024
2971d71
chore: Handle list slack channels for the selected team.
osala-eng Mar 20, 2024
0459711
chore: Setup RAG end to end for slack application.
osala-eng Mar 20, 2024
04921e8
chore: Acknowledge app home opened events.
osala-eng Mar 20, 2024
15fc783
chore: Setup slack default message.
osala-eng Mar 20, 2024
cfaca10
refactor: Refactor integrations.
osala-eng Mar 20, 2024
3c28c5b
chore: Acknowledge slack chats and mentions.
osala-eng Mar 20, 2024
7f43ab1
refactor: Refactor integrations data layer.
osala-eng Mar 22, 2024
1413f73
chore: Create a separate file for external systems.
osala-eng Mar 22, 2024
72ccaf4
chore: Update cookie handler.
osala-eng Mar 22, 2024
44f183a
refactor(Admin UI): consolidate Chat Integrations section in to Admin
janaka Apr 3, 2024
7fe1e11
refactor(ui): move Integrations to Admin section.
janaka Apr 4, 2024
c40320c
refactor(data layer): data layer for Slack integration
janaka Apr 4, 2024
ed7e732
chore: clean up commented code.
janaka Apr 4, 2024
1f419cf
fix: syntax error
janaka Apr 4, 2024
c5ad97d
chore: Uncomment docq slack installation code.
osala-eng Apr 5, 2024
aa3b315
chore: Remove duplicate col name in messages table.
osala-eng Apr 5, 2024
780169f
chore: Fix typo in website url.
osala-eng Apr 5, 2024
89d78bd
chore: Remove print statement in layout init.
osala-eng Apr 5, 2024
9f23c8a
chore: Add slack oauth flow comment.
osala-eng Apr 5, 2024
d01970c
bump version v0.9.7 -> v0.10.0
janaka Apr 5, 2024
d48c44c
make the redirect relative to the current domain rather than based on…
janaka Apr 6, 2024
f281c4b
adjust `Chat Intergrations` admin Ux
janaka Apr 6, 2024
a09c559
chore: add tracing to admin section index page
janaka Apr 6, 2024
1008a0f
chore: add tracing to Slack integration code.
janaka Apr 6, 2024
4e73765
refactor(slack): remove defaulting to ENV VARS
janaka Apr 6, 2024
8d235d9
refactor(slack): switch to named env var pattern
janaka Apr 6, 2024
f65c567
refactor(slack): switch to explicitly enabling request signing verifi…
janaka Apr 6, 2024
d494fac
docs: add comment note
janaka Apr 6, 2024
5d70f30
chore: add more handling code and logging for failure troubleshooting
janaka Apr 6, 2024
edea18b
chore: improve tracing more to troubleshoot Slack integration config …
janaka Apr 6, 2024
152b8fa
chore: fix slack env var name
janaka Apr 6, 2024
7f2a0c5
chore: improve tracing
janaka Apr 6, 2024
8e91898
fix: incorrect import
janaka Apr 6, 2024
9dfdbea
fix: type issues and improve None handling
janaka Apr 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading