mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-02 18:49:09 +00:00
Merge branch 'MODSetter:main' into main
This commit is contained in:
commit
5315406e36
23 changed files with 2963 additions and 1684 deletions
10
README.md
10
README.md
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
|
|
||||||
# SurfSense
|
# SurfSense
|
||||||
While tools like NotebookLM and Perplexity are impressive and highly effective for conducting research on any topic/query, SurfSense elevates this capability by integrating with your personal knowledge base. It is a highly customizable AI research agent, connected to external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, YouTube, GitHub and more to come.
|
While tools like NotebookLM and Perplexity are impressive and highly effective for conducting research on any topic/query, SurfSense elevates this capability by integrating with your personal knowledge base. It is a highly customizable AI research agent, connected to external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, YouTube, GitHub, Discord and more to come.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://trendshift.io/repositories/13606" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13606" alt="MODSetter%2FSurfSense | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/13606" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13606" alt="MODSetter%2FSurfSense | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
@ -62,6 +62,7 @@ Open source and easy to deploy locally.
|
||||||
- Notion
|
- Notion
|
||||||
- Youtube Videos
|
- Youtube Videos
|
||||||
- GitHub
|
- GitHub
|
||||||
|
- Discord
|
||||||
- and more to come.....
|
- and more to come.....
|
||||||
|
|
||||||
## 📄 **Supported File Extensions**
|
## 📄 **Supported File Extensions**
|
||||||
|
@ -106,7 +107,12 @@ Open source and easy to deploy locally.
|
||||||
|
|
||||||
Join the [SurfSense Discord](https://discord.gg/ejRNvftDp9) and help shape the future of SurfSense!
|
Join the [SurfSense Discord](https://discord.gg/ejRNvftDp9) and help shape the future of SurfSense!
|
||||||
|
|
||||||
|
## 🚀 Roadmap
|
||||||
|
|
||||||
|
Stay up to date with our development progress and upcoming features!
|
||||||
|
Check out our public roadmap and contribute your ideas or feedback:
|
||||||
|
|
||||||
|
**View the Roadmap:** [SurfSense Roadmap on GitHub Projects](https://github.com/users/MODSetter/projects/2)
|
||||||
|
|
||||||
## How to get started?
|
## How to get started?
|
||||||
|
|
||||||
|
@ -150,7 +156,7 @@ Before installation, make sure to complete the [prerequisite setup steps](https:
|
||||||
|
|
||||||
**Agent Chat**
|
**Agent Chat**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**Browser Extension**
|
**Browser Extension**
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
"""Add DISCORD_CONNECTOR to SearchSourceConnectorType and DocumentType enums
|
||||||
|
|
||||||
|
Revision ID: 9
|
||||||
|
Revises: 8
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "9"
|
||||||
|
down_revision: Union[str, None] = "8"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
# Define the ENUM type name and the new value
|
||||||
|
CONNECTOR_ENUM = "searchsourceconnectortype"
|
||||||
|
CONNECTOR_NEW_VALUE = "DISCORD_CONNECTOR"
|
||||||
|
DOCUMENT_ENUM = "documenttype"
|
||||||
|
DOCUMENT_NEW_VALUE = "DISCORD_CONNECTOR"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema - add DISCORD_CONNECTOR to connector and document enum."""
|
||||||
|
# Add DISCORD_CONNECTOR to searchsourceconnectortype
|
||||||
|
op.execute(f"ALTER TYPE {CONNECTOR_ENUM} ADD VALUE '{CONNECTOR_NEW_VALUE}'")
|
||||||
|
# Add DISCORD_CONNECTOR to documenttype
|
||||||
|
op.execute(f"ALTER TYPE {DOCUMENT_ENUM} ADD VALUE '{DOCUMENT_NEW_VALUE}'")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema - remove DISCORD_CONNECTOR from connector and document enum."""
|
||||||
|
|
||||||
|
# Old enum name
|
||||||
|
old_connector_enum_name = f"{CONNECTOR_ENUM}_old"
|
||||||
|
old_document_enum_name = f"{DOCUMENT_ENUM}_old"
|
||||||
|
|
||||||
|
old_connector_values = (
|
||||||
|
"SERPER_API",
|
||||||
|
"TAVILY_API",
|
||||||
|
"LINKUP_API",
|
||||||
|
"SLACK_CONNECTOR",
|
||||||
|
"NOTION_CONNECTOR",
|
||||||
|
"GITHUB_CONNECTOR",
|
||||||
|
"LINEAR_CONNECTOR",
|
||||||
|
)
|
||||||
|
old_document_values = (
|
||||||
|
"EXTENSION",
|
||||||
|
"CRAWLED_URL",
|
||||||
|
"FILE",
|
||||||
|
"SLACK_CONNECTOR",
|
||||||
|
"NOTION_CONNECTOR",
|
||||||
|
"YOUTUBE_VIDEO",
|
||||||
|
"GITHUB_CONNECTOR",
|
||||||
|
"LINEAR_CONNECTOR",
|
||||||
|
)
|
||||||
|
|
||||||
|
old_connector_values_sql = ", ".join([f"'{v}'" for v in old_connector_values])
|
||||||
|
old_document_values_sql = ", ".join([f"'{v}'" for v in old_document_values])
|
||||||
|
|
||||||
|
# Table and column names (adjust if different)
|
||||||
|
connector_table_name = "search_source_connectors"
|
||||||
|
connector_column_name = "connector_type"
|
||||||
|
document_table_name = "documents"
|
||||||
|
document_column_name = "document_type"
|
||||||
|
|
||||||
|
# Connector Enum Downgrade Steps
|
||||||
|
# 1. Rename the current connector enum type
|
||||||
|
op.execute(f"ALTER TYPE {CONNECTOR_ENUM} RENAME TO {old_connector_enum_name}")
|
||||||
|
|
||||||
|
# 2. Create the new connector enum type with the old values
|
||||||
|
op.execute(f"CREATE TYPE {CONNECTOR_ENUM} AS ENUM({old_connector_values_sql})")
|
||||||
|
|
||||||
|
# 3. Update the connector table:
|
||||||
|
op.execute(
|
||||||
|
f"ALTER TABLE {connector_table_name} "
|
||||||
|
f"ALTER COLUMN {connector_column_name} "
|
||||||
|
f"TYPE {CONNECTOR_ENUM} "
|
||||||
|
f"USING {connector_column_name}::text::{CONNECTOR_ENUM}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Drop the old connector enum type
|
||||||
|
op.execute(f"DROP TYPE {old_connector_enum_name}")
|
||||||
|
|
||||||
|
|
||||||
|
# Document Enum Downgrade Steps
|
||||||
|
# 1. Rename the current document enum type
|
||||||
|
op.execute(f"ALTER TYPE {DOCUMENT_ENUM} RENAME TO {old_document_enum_name}")
|
||||||
|
|
||||||
|
# 2. Create the new document enum type with the old values
|
||||||
|
op.execute(f"CREATE TYPE {DOCUMENT_ENUM} AS ENUM({old_document_values_sql})")
|
||||||
|
|
||||||
|
# 3. Delete rows with the new value from the documents table
|
||||||
|
op.execute(
|
||||||
|
f"DELETE FROM {document_table_name} WHERE {document_column_name}::text = '{DOCUMENT_NEW_VALUE}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Alter the document table to use the new enum type (casting old values)
|
||||||
|
op.execute(
|
||||||
|
f"ALTER TABLE {document_table_name} "
|
||||||
|
f"ALTER COLUMN {document_column_name} "
|
||||||
|
f"TYPE {DOCUMENT_ENUM} "
|
||||||
|
f"USING {document_column_name}::text::{DOCUMENT_ENUM}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Drop the old enum types
|
||||||
|
op.execute(f"DROP TYPE {old_document_enum_name}")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
|
@ -401,6 +401,23 @@ async def fetch_relevant_documents(
|
||||||
streaming_service.only_update_terminal(f"🔗 Found {len(linkup_chunks)} Linkup results related to your query")
|
streaming_service.only_update_terminal(f"🔗 Found {len(linkup_chunks)} Linkup results related to your query")
|
||||||
writer({"yeild_value": streaming_service._format_annotations()})
|
writer({"yeild_value": streaming_service._format_annotations()})
|
||||||
|
|
||||||
|
elif connector == "DISCORD_CONNECTOR":
|
||||||
|
source_object, discord_chunks = await connector_service.search_discord(
|
||||||
|
user_query=reformulated_query,
|
||||||
|
user_id=user_id,
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
top_k=top_k,
|
||||||
|
search_mode=search_mode
|
||||||
|
)
|
||||||
|
# Add to sources and raw documents
|
||||||
|
if source_object:
|
||||||
|
all_sources.append(source_object)
|
||||||
|
all_raw_documents.extend(discord_chunks)
|
||||||
|
# Stream found document count
|
||||||
|
if streaming_service and writer:
|
||||||
|
streaming_service.only_update_terminal(f"🗨️ Found {len(discord_chunks)} Discord messages related to your query")
|
||||||
|
writer({"yeild_value": streaming_service._format_annotations()})
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"Error searching connector {connector}: {str(e)}"
|
error_message = f"Error searching connector {connector}: {str(e)}"
|
||||||
|
|
|
@ -15,6 +15,7 @@ You are SurfSense, an advanced AI research assistant that synthesizes informatio
|
||||||
- YOUTUBE_VIDEO: "YouTube video transcripts and metadata" (personally saved videos)
|
- YOUTUBE_VIDEO: "YouTube video transcripts and metadata" (personally saved videos)
|
||||||
- GITHUB_CONNECTOR: "GitHub repository content and issues" (personal repositories and interactions)
|
- GITHUB_CONNECTOR: "GitHub repository content and issues" (personal repositories and interactions)
|
||||||
- LINEAR_CONNECTOR: "Linear project issues and discussions" (personal project management)
|
- LINEAR_CONNECTOR: "Linear project issues and discussions" (personal project management)
|
||||||
|
- DISCORD_CONNECTOR: "Discord server messages and channels" (personal community interactions)
|
||||||
- TAVILY_API: "Tavily search API results" (personalized search results)
|
- TAVILY_API: "Tavily search API results" (personalized search results)
|
||||||
- LINKUP_API: "Linkup search API results" (personalized search results)
|
- LINKUP_API: "Linkup search API results" (personalized search results)
|
||||||
</knowledge_sources>
|
</knowledge_sources>
|
||||||
|
|
310
surfsense_backend/app/connectors/discord_connector.py
Normal file
310
surfsense_backend/app/connectors/discord_connector.py
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
"""
|
||||||
|
Discord Connector
|
||||||
|
|
||||||
|
A module for interacting with Discord's HTTP API to retrieve guilds, channels, and message history.
|
||||||
|
|
||||||
|
Requires a Discord bot token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import datetime
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordConnector(commands.Bot):
|
||||||
|
"""Class for retrieving guild, channel, and message history from Discord."""
|
||||||
|
|
||||||
|
def __init__(self, token: str = None):
|
||||||
|
"""
|
||||||
|
Initialize the DiscordConnector with a bot token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token (str): The Discord bot token.
|
||||||
|
"""
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.guilds = True # Required to fetch guilds and channels
|
||||||
|
intents.messages = True # Required to fetch messages
|
||||||
|
intents.message_content = True # Required to read message content
|
||||||
|
intents.members = True # Required to fetch member information
|
||||||
|
super().__init__(command_prefix="!", intents=intents) # command_prefix is required but not strictly used here
|
||||||
|
self.token = token
|
||||||
|
self._bot_task = None # Holds the async bot task
|
||||||
|
self._is_running = False # Flag to track if the bot is running
|
||||||
|
|
||||||
|
# Event to confirm bot is ready
|
||||||
|
@self.event
|
||||||
|
async def on_ready():
|
||||||
|
logger.info(f"Logged in as {self.user} (ID: {self.user.id})")
|
||||||
|
self._is_running = True
|
||||||
|
|
||||||
|
@self.event
|
||||||
|
async def on_connect():
|
||||||
|
logger.debug("Bot connected to Discord gateway.")
|
||||||
|
|
||||||
|
@self.event
|
||||||
|
async def on_disconnect():
|
||||||
|
logger.debug("Bot disconnected from Discord gateway.")
|
||||||
|
self._is_running = False # Reset flag on disconnect
|
||||||
|
|
||||||
|
@self.event
|
||||||
|
async def on_resumed():
|
||||||
|
logger.debug("Bot resumed connection to Discord gateway.")
|
||||||
|
|
||||||
|
async def start_bot(self):
|
||||||
|
"""Starts the bot to connect to Discord."""
|
||||||
|
logger.info("Starting Discord bot...")
|
||||||
|
|
||||||
|
if not self.token:
|
||||||
|
raise ValueError("Discord bot token not set. Call set_token(token) first.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._is_running:
|
||||||
|
logger.warning("Bot is already running. Use close_bot() to stop it before starting again.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.start(self.token)
|
||||||
|
logger.info("Discord bot started successfully.")
|
||||||
|
except discord.LoginFailure:
|
||||||
|
logger.error("Failed to log in: Invalid token was provided. Please check your bot token.")
|
||||||
|
self._is_running = False
|
||||||
|
raise
|
||||||
|
except discord.PrivilegedIntentsRequired as e:
|
||||||
|
logger.error(f"Privileged Intents Required: {e}. Make sure all required intents are enabled in your bot's application page.")
|
||||||
|
self._is_running = False
|
||||||
|
raise
|
||||||
|
except discord.ConnectionClosed as e:
|
||||||
|
logger.error(f"Discord connection closed unexpectedly: {e}")
|
||||||
|
self._is_running = False
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred while starting the bot: {e}")
|
||||||
|
self._is_running = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def close_bot(self):
|
||||||
|
"""Closes the bot's connection to Discord."""
|
||||||
|
|
||||||
|
if self._is_running:
|
||||||
|
logger.info("Closing Discord bot connection...")
|
||||||
|
await self.close()
|
||||||
|
logger.info("Discord bot connection closed.")
|
||||||
|
self._is_running = False
|
||||||
|
else:
|
||||||
|
logger.info("Bot is not running or already disconnected.")
|
||||||
|
|
||||||
|
|
||||||
|
def set_token(self, token: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the discord bot token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token (str): The Discord bot token.
|
||||||
|
"""
|
||||||
|
logger.info("Setting Discord bot token.")
|
||||||
|
self.token = token
|
||||||
|
logger.info("Token set successfully. You can now start the bot with start_bot().")
|
||||||
|
|
||||||
|
async def _wait_until_ready(self):
|
||||||
|
"""Helper to wait until the bot is connected and ready."""
|
||||||
|
logger.info("Waiting for the bot to be ready...")
|
||||||
|
|
||||||
|
# Give the event loop a chance to switch to the bot's startup task.
|
||||||
|
# This allows self.start() to begin initializing the client.
|
||||||
|
# Terrible solution, but necessary to avoid blocking the event loop.
|
||||||
|
await asyncio.sleep(1) # Yield control to the event loop
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.wait_until_ready(), timeout=60.0)
|
||||||
|
logger.info("Bot is ready.")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error(f"Bot did not become ready within 60 seconds. Connection may have failed.")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred while waiting for the bot to be ready: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_guilds(self) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Fetch all guilds (servers) the bot is in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: A list of guilds with their ID, name, and member count.
|
||||||
|
Each guild is represented as a dictionary.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the token is not set.
|
||||||
|
"""
|
||||||
|
await self._wait_until_ready()
|
||||||
|
logger.info("Fetching guilds...")
|
||||||
|
|
||||||
|
guilds_data = []
|
||||||
|
for guild in self.guilds:
|
||||||
|
member_count = guild.member_count if guild.member_count is not None else "N/A"
|
||||||
|
guilds_data.append(
|
||||||
|
{
|
||||||
|
"id": str(guild.id),
|
||||||
|
"name": guild.name,
|
||||||
|
"member_count": member_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Fetched {len(guilds_data)} guilds.")
|
||||||
|
return guilds_data
|
||||||
|
|
||||||
|
async def get_text_channels(self, guild_id: str) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Fetch all text channels in a guild.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild_id (str): The ID of the guild to fetch channels from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: A list of text channels with their ID, name, and type.
|
||||||
|
Each channel is represented as a dictionary.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
discord.NotFound: If the guild is not found.
|
||||||
|
"""
|
||||||
|
await self._wait_until_ready()
|
||||||
|
logger.info(f"Fetching text channels for guild ID: {guild_id}")
|
||||||
|
|
||||||
|
guild = self.get_guild(int(guild_id))
|
||||||
|
if not guild:
|
||||||
|
logger.warning(f"Guild with ID {guild_id} not found.")
|
||||||
|
raise discord.NotFound(f"Guild with ID {guild_id} not found.")
|
||||||
|
|
||||||
|
channels_data = []
|
||||||
|
for channel in guild.channels:
|
||||||
|
if isinstance(channel, discord.TextChannel):
|
||||||
|
channels_data.append(
|
||||||
|
{"id": str(channel.id), "name": channel.name, "type": "text"}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Fetched {len(channels_data)} text channels from guild {guild_id}.")
|
||||||
|
return channels_data
|
||||||
|
|
||||||
|
async def get_channel_history(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
start_date: str = None,
|
||||||
|
end_date: str = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Fetch message history from a text channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel_id (str): The ID of the channel to fetch messages from.
|
||||||
|
start_date (str): Optional start date in ISO format (YYYY-MM-DD).
|
||||||
|
end_date (str): Optional end date in ISO format (YYYY-MM-DD).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: A list of messages with their ID, author ID, author name,
|
||||||
|
content, and creation timestamp.
|
||||||
|
Each message is represented as a dictionary.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
discord.NotFound: If the channel is not found.
|
||||||
|
discord.Forbidden: If the bot does not have permissions to read history in the channel.
|
||||||
|
"""
|
||||||
|
await self._wait_until_ready()
|
||||||
|
logger.info(f"Fetching message history for channel ID: {channel_id}")
|
||||||
|
|
||||||
|
channel = self.get_channel(int(channel_id))
|
||||||
|
if not channel:
|
||||||
|
logger.warning(f"Channel with ID {channel_id} not found.")
|
||||||
|
raise discord.NotFound(f"Channel with ID {channel_id} not found.")
|
||||||
|
if not isinstance(channel, discord.TextChannel):
|
||||||
|
logger.warning(f"Channel {channel_id} is not a text channel.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
messages_data = []
|
||||||
|
after = None
|
||||||
|
before = None
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
try:
|
||||||
|
start_datetime = datetime.datetime.fromisoformat(start_date).replace(tzinfo=datetime.timezone.utc)
|
||||||
|
after = start_datetime
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Invalid start_date format: {start_date}. Ignoring.")
|
||||||
|
|
||||||
|
if end_date:
|
||||||
|
try:
|
||||||
|
end_datetime = datetime.datetime.fromisoformat(f"{end_date}").replace(tzinfo=datetime.timezone.utc)
|
||||||
|
before = end_datetime
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Invalid end_date format: {end_date}. Ignoring.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for message in channel.history(limit=None, before=before, after=after):
|
||||||
|
messages_data.append(
|
||||||
|
{
|
||||||
|
"id": str(message.id),
|
||||||
|
"author_id": str(message.author.id),
|
||||||
|
"author_name": message.author.name,
|
||||||
|
"content": message.content,
|
||||||
|
"created_at": message.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.error(f"Bot does not have permissions to read message history in channel {channel_id}.")
|
||||||
|
raise
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(f"Failed to fetch messages from channel {channel_id}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
logger.info(f"Fetched {len(messages_data)} messages from channel {channel_id}.")
|
||||||
|
return messages_data
|
||||||
|
|
||||||
|
async def get_user_info(self, guild_id: str, user_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Get information about a user in a guild.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild_id (str): The ID of the guild.
|
||||||
|
user_id (str): The ID of the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict | None: A dictionary with user information (ID, name, joined_at, roles)
|
||||||
|
or None if the user is not found.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
discord.NotFound: If the guild or user is not found.
|
||||||
|
discord.Forbidden: If the bot does not have the GUILD_MEMBERS intent or
|
||||||
|
permissions to view members.
|
||||||
|
"""
|
||||||
|
await self._wait_until_ready()
|
||||||
|
logger.info(f"Fetching user info for user ID: {user_id} in guild ID: {guild_id}")
|
||||||
|
|
||||||
|
guild = self.get_guild(int(guild_id))
|
||||||
|
if not guild:
|
||||||
|
logger.warning(f"Guild with ID {guild_id} not found.")
|
||||||
|
raise discord.NotFound(f"Guild with ID {guild_id} not found.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
member = await guild.fetch_member(int(user_id))
|
||||||
|
if member:
|
||||||
|
roles = [role.name for role in member.roles if role.name != "@everyone"]
|
||||||
|
logger.info(f"User {user_id} found in guild {guild_id}.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(member.id),
|
||||||
|
"name": member.name,
|
||||||
|
"joined_at": member.joined_at.isoformat() if member.joined_at else None,
|
||||||
|
"roles": roles,
|
||||||
|
}
|
||||||
|
logger.warning(f"User {user_id} not found in guild {guild_id}.")
|
||||||
|
return None
|
||||||
|
except discord.NotFound:
|
||||||
|
logger.warning(f"User {user_id} not found in guild {guild_id}.")
|
||||||
|
return None
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.error(f"Bot does not have permissions to fetch members in guild {guild_id}. Ensure GUILD_MEMBERS intent is enabled.")
|
||||||
|
raise
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(f"Failed to fetch user info for {user_id} in guild {guild_id}: {e}")
|
||||||
|
return None
|
|
@ -50,6 +50,7 @@ class DocumentType(str, Enum):
|
||||||
YOUTUBE_VIDEO = "YOUTUBE_VIDEO"
|
YOUTUBE_VIDEO = "YOUTUBE_VIDEO"
|
||||||
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
||||||
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
||||||
|
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
|
||||||
|
|
||||||
class SearchSourceConnectorType(str, Enum):
|
class SearchSourceConnectorType(str, Enum):
|
||||||
SERPER_API = "SERPER_API" # NOT IMPLEMENTED YET : DON'T REMEMBER WHY : MOST PROBABLY BECAUSE WE NEED TO CRAWL THE RESULTS RETURNED BY IT
|
SERPER_API = "SERPER_API" # NOT IMPLEMENTED YET : DON'T REMEMBER WHY : MOST PROBABLY BECAUSE WE NEED TO CRAWL THE RESULTS RETURNED BY IT
|
||||||
|
@ -59,6 +60,7 @@ class SearchSourceConnectorType(str, Enum):
|
||||||
NOTION_CONNECTOR = "NOTION_CONNECTOR"
|
NOTION_CONNECTOR = "NOTION_CONNECTOR"
|
||||||
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
||||||
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
||||||
|
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
|
||||||
|
|
||||||
class ChatType(str, Enum):
|
class ChatType(str, Enum):
|
||||||
GENERAL = "GENERAL"
|
GENERAL = "GENERAL"
|
||||||
|
|
|
@ -7,7 +7,7 @@ PUT /search-source-connectors/{connector_id} - Update a specific connector
|
||||||
DELETE /search-source-connectors/{connector_id} - Delete a specific connector
|
DELETE /search-source-connectors/{connector_id} - Delete a specific connector
|
||||||
POST /search-source-connectors/{connector_id}/index - Index content from a connector to a search space
|
POST /search-source-connectors/{connector_id}/index - Index content from a connector to a search space
|
||||||
|
|
||||||
Note: Each user can have only one connector of each type (SERPER_API, TAVILY_API, SLACK_CONNECTOR, NOTION_CONNECTOR, GITHUB_CONNECTOR, LINEAR_CONNECTOR).
|
Note: Each user can have only one connector of each type (SERPER_API, TAVILY_API, SLACK_CONNECTOR, NOTION_CONNECTOR, GITHUB_CONNECTOR, LINEAR_CONNECTOR, DISCORD_CONNECTOR).
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Body
|
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Body
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
@ -19,7 +19,7 @@ from app.schemas import SearchSourceConnectorCreate, SearchSourceConnectorUpdate
|
||||||
from app.users import current_active_user
|
from app.users import current_active_user
|
||||||
from app.utils.check_ownership import check_ownership
|
from app.utils.check_ownership import check_ownership
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
from app.tasks.connectors_indexing_tasks import index_slack_messages, index_notion_pages, index_github_repos, index_linear_issues
|
from app.tasks.connectors_indexing_tasks import index_slack_messages, index_notion_pages, index_github_repos, index_linear_issues, index_discord_messages
|
||||||
from app.connectors.github_connector import GitHubConnector
|
from app.connectors.github_connector import GitHubConnector
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
@ -282,6 +282,7 @@ async def index_connector_content(
|
||||||
- NOTION_CONNECTOR: Indexes pages from all accessible Notion pages
|
- NOTION_CONNECTOR: Indexes pages from all accessible Notion pages
|
||||||
- GITHUB_CONNECTOR: Indexes code and documentation from GitHub repositories
|
- GITHUB_CONNECTOR: Indexes code and documentation from GitHub repositories
|
||||||
- LINEAR_CONNECTOR: Indexes issues and comments from Linear
|
- LINEAR_CONNECTOR: Indexes issues and comments from Linear
|
||||||
|
- DISCORD_CONNECTOR: Indexes messages from all accessible Discord channels
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
connector_id: ID of the connector to use
|
connector_id: ID of the connector to use
|
||||||
|
@ -378,6 +379,30 @@ async def index_connector_content(
|
||||||
background_tasks.add_task(run_linear_indexing_with_new_session, connector_id, search_space_id)
|
background_tasks.add_task(run_linear_indexing_with_new_session, connector_id, search_space_id)
|
||||||
response_message = "Linear indexing started in the background."
|
response_message = "Linear indexing started in the background."
|
||||||
|
|
||||||
|
elif connector.connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR:
|
||||||
|
# Determine the time range that will be indexed
|
||||||
|
if not connector.last_indexed_at:
|
||||||
|
start_date = "365 days ago"
|
||||||
|
else:
|
||||||
|
today = datetime.now().date()
|
||||||
|
if connector.last_indexed_at.date() == today:
|
||||||
|
# If last indexed today, go back 1 day to ensure we don't miss anything
|
||||||
|
start_date = (today - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
else:
|
||||||
|
start_date = connector.last_indexed_at.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
indexing_from = start_date
|
||||||
|
indexing_to = today_str
|
||||||
|
|
||||||
|
# Run indexing in background
|
||||||
|
logger.info(
|
||||||
|
f"Triggering Discord indexing for connector {connector_id} into search space {search_space_id}"
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
run_discord_indexing_with_new_session, connector_id, search_space_id
|
||||||
|
)
|
||||||
|
response_message = "Discord indexing started in the background."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
@ -577,3 +602,45 @@ async def run_linear_indexing(
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(f"Critical error in run_linear_indexing for connector {connector_id}: {e}", exc_info=True)
|
logger.error(f"Critical error in run_linear_indexing for connector {connector_id}: {e}", exc_info=True)
|
||||||
# Optionally update status in DB to indicate failure
|
# Optionally update status in DB to indicate failure
|
||||||
|
|
||||||
|
# Add new helper functions for discord indexing
|
||||||
|
async def run_discord_indexing_with_new_session(
|
||||||
|
connector_id: int,
|
||||||
|
search_space_id: int
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new session and run the Discord indexing task.
|
||||||
|
This prevents session leaks by creating a dedicated session for the background task.
|
||||||
|
"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
await run_discord_indexing(session, connector_id, search_space_id)
|
||||||
|
|
||||||
|
async def run_discord_indexing(
|
||||||
|
session: AsyncSession,
|
||||||
|
connector_id: int,
|
||||||
|
search_space_id: int
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Background task to run Discord indexing.
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
connector_id: ID of the Discord connector
|
||||||
|
search_space_id: ID of the search space
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Index Discord messages without updating last_indexed_at (we'll do it separately)
|
||||||
|
documents_processed, error_or_warning = await index_discord_messages(
|
||||||
|
session=session,
|
||||||
|
connector_id=connector_id,
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
update_last_indexed=False # Don't update timestamp in the indexing function
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only update last_indexed_at if indexing was successful (either new docs or updated docs)
|
||||||
|
if documents_processed > 0:
|
||||||
|
await update_connector_last_indexed(session, connector_id)
|
||||||
|
logger.info(f"Discord indexing completed successfully: {documents_processed} documents processed")
|
||||||
|
else:
|
||||||
|
logger.error(f"Discord indexing failed or no documents processed: {error_or_warning}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in background Discord indexing task: {str(e)}")
|
|
@ -81,6 +81,7 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
repo_full_names = config.get("repo_full_names")
|
repo_full_names = config.get("repo_full_names")
|
||||||
if not isinstance(repo_full_names, list) or not repo_full_names:
|
if not isinstance(repo_full_names, list) or not repo_full_names:
|
||||||
raise ValueError("repo_full_names must be a non-empty list of strings")
|
raise ValueError("repo_full_names must be a non-empty list of strings")
|
||||||
|
|
||||||
elif connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR:
|
elif connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR:
|
||||||
# For LINEAR_CONNECTOR, only allow LINEAR_API_KEY
|
# For LINEAR_CONNECTOR, only allow LINEAR_API_KEY
|
||||||
allowed_keys = ["LINEAR_API_KEY"]
|
allowed_keys = ["LINEAR_API_KEY"]
|
||||||
|
@ -91,6 +92,16 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
if not config.get("LINEAR_API_KEY"):
|
if not config.get("LINEAR_API_KEY"):
|
||||||
raise ValueError("LINEAR_API_KEY cannot be empty")
|
raise ValueError("LINEAR_API_KEY cannot be empty")
|
||||||
|
|
||||||
|
elif connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR:
|
||||||
|
# For DISCORD_CONNECTOR, only allow DISCORD_BOT_TOKEN
|
||||||
|
allowed_keys = ["DISCORD_BOT_TOKEN"]
|
||||||
|
if set(config.keys()) != set(allowed_keys):
|
||||||
|
raise ValueError(f"For DISCORD_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||||
|
|
||||||
|
# Ensure the bot token is not empty
|
||||||
|
if not config.get("DISCORD_BOT_TOKEN"):
|
||||||
|
raise ValueError("DISCORD_BOT_TOKEN cannot be empty")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
||||||
|
|
|
@ -11,8 +11,11 @@ from app.connectors.slack_history import SlackHistory
|
||||||
from app.connectors.notion_history import NotionHistoryConnector
|
from app.connectors.notion_history import NotionHistoryConnector
|
||||||
from app.connectors.github_connector import GitHubConnector
|
from app.connectors.github_connector import GitHubConnector
|
||||||
from app.connectors.linear_connector import LinearConnector
|
from app.connectors.linear_connector import LinearConnector
|
||||||
|
from app.connectors.discord_connector import DiscordConnector
|
||||||
from slack_sdk.errors import SlackApiError
|
from slack_sdk.errors import SlackApiError
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from app.utils.document_converters import generate_content_hash
|
from app.utils.document_converters import generate_content_hash
|
||||||
|
|
||||||
|
@ -912,3 +915,257 @@ async def index_linear_issues(
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(f"Failed to index Linear issues: {str(e)}", exc_info=True)
|
logger.error(f"Failed to index Linear issues: {str(e)}", exc_info=True)
|
||||||
return 0, f"Failed to index Linear issues: {str(e)}"
|
return 0, f"Failed to index Linear issues: {str(e)}"
|
||||||
|
|
||||||
|
async def index_discord_messages(
|
||||||
|
session: AsyncSession,
|
||||||
|
connector_id: int,
|
||||||
|
search_space_id: int,
|
||||||
|
update_last_indexed: bool = True
|
||||||
|
) -> Tuple[int, Optional[str]]:
|
||||||
|
"""
|
||||||
|
Index Discord messages from all accessible channels.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
connector_id: ID of the Discord connector
|
||||||
|
search_space_id: ID of the search space to store documents in
|
||||||
|
update_last_indexed: Whether to update the last_indexed_at timestamp (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing (number of documents indexed, error message or None)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the connector
|
||||||
|
result = await session.execute(
|
||||||
|
select(SearchSourceConnector)
|
||||||
|
.filter(
|
||||||
|
SearchSourceConnector.id == connector_id,
|
||||||
|
SearchSourceConnector.connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connector = result.scalars().first()
|
||||||
|
|
||||||
|
if not connector:
|
||||||
|
return 0, f"Connector with ID {connector_id} not found or is not a Discord connector"
|
||||||
|
|
||||||
|
# Get the Discord token from the connector config
|
||||||
|
discord_token = connector.config.get("DISCORD_BOT_TOKEN")
|
||||||
|
if not discord_token:
|
||||||
|
return 0, "Discord token not found in connector config"
|
||||||
|
|
||||||
|
logger.info(f"Starting Discord indexing for connector {connector_id}")
|
||||||
|
|
||||||
|
# Initialize Discord client
|
||||||
|
discord_client = DiscordConnector(token=discord_token)
|
||||||
|
|
||||||
|
# Calculate date range
|
||||||
|
end_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# Use last_indexed_at as start date if available, otherwise use 365 days ago
|
||||||
|
if connector.last_indexed_at:
|
||||||
|
start_date = connector.last_indexed_at.replace(tzinfo=timezone.utc)
|
||||||
|
logger.info(f"Using last_indexed_at ({start_date.strftime('%Y-%m-%d')}) as start date")
|
||||||
|
else:
|
||||||
|
start_date = end_date - timedelta(days=365)
|
||||||
|
logger.info(f"No last_indexed_at found, using {start_date.strftime('%Y-%m-%d')} (365 days ago) as start date")
|
||||||
|
|
||||||
|
# Format dates for Discord API
|
||||||
|
start_date_str = start_date.isoformat()
|
||||||
|
end_date_str = end_date.isoformat()
|
||||||
|
|
||||||
|
documents_indexed = 0
|
||||||
|
documents_skipped = 0
|
||||||
|
skipped_channels = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Starting Discord bot to fetch guilds")
|
||||||
|
discord_client._bot_task = asyncio.create_task(discord_client.start_bot())
|
||||||
|
await discord_client._wait_until_ready()
|
||||||
|
|
||||||
|
logger.info("Fetching Discord guilds")
|
||||||
|
guilds = await discord_client.get_guilds()
|
||||||
|
logger.info(f"Found {len(guilds)} guilds")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get Discord guilds: {str(e)}", exc_info=True)
|
||||||
|
await discord_client.close_bot()
|
||||||
|
return 0, f"Failed to get Discord guilds: {str(e)}"
|
||||||
|
if not guilds:
|
||||||
|
logger.info("No Discord guilds found to index")
|
||||||
|
await discord_client.close_bot()
|
||||||
|
return 0, "No Discord guilds found"
|
||||||
|
|
||||||
|
# Process each guild and channel
|
||||||
|
for guild in guilds:
|
||||||
|
guild_id = guild["id"]
|
||||||
|
guild_name = guild["name"]
|
||||||
|
logger.info(f"Processing guild: {guild_name} ({guild_id})")
|
||||||
|
try:
|
||||||
|
channels = await discord_client.get_text_channels(guild_id)
|
||||||
|
if not channels:
|
||||||
|
logger.info(f"No channels found in guild {guild_name}. Skipping.")
|
||||||
|
skipped_channels.append(f"{guild_name} (no channels)")
|
||||||
|
documents_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
for channel in channels:
|
||||||
|
channel_id = channel["id"]
|
||||||
|
channel_name = channel["name"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
messages = await discord_client.get_channel_history(
|
||||||
|
channel_id=channel_id,
|
||||||
|
start_date=start_date_str,
|
||||||
|
end_date=end_date_str,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get messages for channel {channel_name}: {str(e)}")
|
||||||
|
skipped_channels.append(f"{guild_name}#{channel_name} (fetch error)")
|
||||||
|
documents_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
logger.info(f"No messages found in channel {channel_name} for the specified date range.")
|
||||||
|
documents_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Format messages
|
||||||
|
formatted_messages = []
|
||||||
|
for msg in messages:
|
||||||
|
# Skip system messages if needed (Discord has some types)
|
||||||
|
if msg.get("type") in ["system"]:
|
||||||
|
continue
|
||||||
|
formatted_messages.append(msg)
|
||||||
|
|
||||||
|
if not formatted_messages:
|
||||||
|
logger.info(f"No valid messages found in channel {channel_name} after filtering.")
|
||||||
|
documents_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Convert messages to markdown format
|
||||||
|
channel_content = f"# Discord Channel: {guild_name} / {channel_name}\n\n"
|
||||||
|
for msg in formatted_messages:
|
||||||
|
user_name = msg.get("author_name", "Unknown User")
|
||||||
|
timestamp = msg.get("created_at", "Unknown Time")
|
||||||
|
text = msg.get("content", "")
|
||||||
|
channel_content += f"## {user_name} ({timestamp})\n\n{text}\n\n---\n\n"
|
||||||
|
|
||||||
|
# Format document metadata
|
||||||
|
metadata_sections = [
|
||||||
|
("METADATA", [
|
||||||
|
f"GUILD_NAME: {guild_name}",
|
||||||
|
f"GUILD_ID: {guild_id}",
|
||||||
|
f"CHANNEL_NAME: {channel_name}",
|
||||||
|
f"CHANNEL_ID: {channel_id}",
|
||||||
|
f"MESSAGE_COUNT: {len(formatted_messages)}"
|
||||||
|
]),
|
||||||
|
("CONTENT", [
|
||||||
|
"FORMAT: markdown",
|
||||||
|
"TEXT_START",
|
||||||
|
channel_content,
|
||||||
|
"TEXT_END"
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
||||||
|
# Build the document string
|
||||||
|
document_parts = []
|
||||||
|
document_parts.append("<DOCUMENT>")
|
||||||
|
for section_title, section_content in metadata_sections:
|
||||||
|
document_parts.append(f"<{section_title}>")
|
||||||
|
document_parts.extend(section_content)
|
||||||
|
document_parts.append(f"</{section_title}>")
|
||||||
|
document_parts.append("</DOCUMENT>")
|
||||||
|
combined_document_string = '\n'.join(document_parts)
|
||||||
|
content_hash = generate_content_hash(combined_document_string)
|
||||||
|
|
||||||
|
# Check if document with this content hash already exists
|
||||||
|
existing_doc_by_hash_result = await session.execute(
|
||||||
|
select(Document).where(Document.content_hash == content_hash)
|
||||||
|
)
|
||||||
|
existing_document_by_hash = existing_doc_by_hash_result.scalars().first()
|
||||||
|
|
||||||
|
if existing_document_by_hash:
|
||||||
|
logger.info(f"Document with content hash {content_hash} already exists for channel {guild_name}#{channel_name}. Skipping processing.")
|
||||||
|
documents_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate summary using summary_chain
|
||||||
|
summary_chain = SUMMARY_PROMPT_TEMPLATE | config.long_context_llm_instance
|
||||||
|
summary_result = await summary_chain.ainvoke({"document": combined_document_string})
|
||||||
|
summary_content = summary_result.content
|
||||||
|
summary_embedding = await asyncio.to_thread(
|
||||||
|
config.embedding_model_instance.embed, summary_content
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process chunks
|
||||||
|
raw_chunks = await asyncio.to_thread(
|
||||||
|
config.chunker_instance.chunk,
|
||||||
|
channel_content
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_texts = [chunk.text for chunk in raw_chunks if chunk.text.strip()]
|
||||||
|
chunk_embeddings = await asyncio.to_thread(
|
||||||
|
lambda texts: [config.embedding_model_instance.embed(t) for t in texts],
|
||||||
|
chunk_texts
|
||||||
|
)
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
Chunk(content=raw_chunk.text, embedding=embedding)
|
||||||
|
for raw_chunk, embedding in zip(raw_chunks, chunk_embeddings)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create and store new document
|
||||||
|
document = Document(
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
title=f"Discord - {guild_name}#{channel_name}",
|
||||||
|
document_type=DocumentType.DISCORD_CONNECTOR,
|
||||||
|
document_metadata={
|
||||||
|
"guild_name": guild_name,
|
||||||
|
"guild_id": guild_id,
|
||||||
|
"channel_name": channel_name,
|
||||||
|
"channel_id": channel_id,
|
||||||
|
"message_count": len(formatted_messages),
|
||||||
|
"start_date": start_date_str,
|
||||||
|
"end_date": end_date_str,
|
||||||
|
"indexed_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
},
|
||||||
|
content=summary_content,
|
||||||
|
content_hash=content_hash,
|
||||||
|
embedding=summary_embedding,
|
||||||
|
chunks=chunks
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(document)
|
||||||
|
documents_indexed += 1
|
||||||
|
logger.info(f"Successfully indexed new channel {guild_name}#{channel_name} with {len(formatted_messages)} messages")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing guild {guild_name}: {str(e)}", exc_info=True)
|
||||||
|
skipped_channels.append(f"{guild_name} (processing error)")
|
||||||
|
documents_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if update_last_indexed and documents_indexed > 0:
|
||||||
|
connector.last_indexed_at = datetime.now(timezone.utc)
|
||||||
|
logger.info(f"Updated last_indexed_at to {connector.last_indexed_at}")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
await discord_client.close_bot()
|
||||||
|
|
||||||
|
# Prepare result message
|
||||||
|
result_message = None
|
||||||
|
if skipped_channels:
|
||||||
|
result_message = f"Processed {documents_indexed} channels. Skipped {len(skipped_channels)} channels: {', '.join(skipped_channels)}"
|
||||||
|
else:
|
||||||
|
result_message = f"Processed {documents_indexed} channels."
|
||||||
|
|
||||||
|
logger.info(f"Discord indexing completed: {documents_indexed} new channels, {documents_skipped} skipped")
|
||||||
|
return documents_indexed, result_message
|
||||||
|
|
||||||
|
except SQLAlchemyError as db_error:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Database error during Discord indexing: {str(db_error)}", exc_info=True)
|
||||||
|
return 0, f"Database error: {str(db_error)}"
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Failed to index Discord messages: {str(e)}", exc_info=True)
|
||||||
|
return 0, f"Failed to index Discord messages: {str(e)}"
|
||||||
|
|
|
@ -959,3 +959,99 @@ class ConnectorService:
|
||||||
"type": "LINKUP_API",
|
"type": "LINKUP_API",
|
||||||
"sources": [],
|
"sources": [],
|
||||||
}, []
|
}, []
|
||||||
|
|
||||||
|
async def search_discord(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
||||||
|
"""
|
||||||
|
Search for Discord messages and return both the source information and langchain documents
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_query: The user's query
|
||||||
|
user_id: The user's ID
|
||||||
|
search_space_id: The search space ID to search in
|
||||||
|
top_k: Maximum number of results to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (sources_info, langchain_documents)
|
||||||
|
"""
|
||||||
|
if search_mode == SearchMode.CHUNKS:
|
||||||
|
discord_chunks = await self.chunk_retriever.hybrid_search(
|
||||||
|
query_text=user_query,
|
||||||
|
top_k=top_k,
|
||||||
|
user_id=user_id,
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
document_type="DISCORD_CONNECTOR"
|
||||||
|
)
|
||||||
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
|
discord_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
query_text=user_query,
|
||||||
|
top_k=top_k,
|
||||||
|
user_id=user_id,
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
document_type="DISCORD_CONNECTOR"
|
||||||
|
)
|
||||||
|
# Transform document retriever results to match expected format
|
||||||
|
discord_chunks = self._transform_document_results(discord_chunks)
|
||||||
|
|
||||||
|
# Early return if no results
|
||||||
|
if not discord_chunks:
|
||||||
|
return {
|
||||||
|
"id": 11,
|
||||||
|
"name": "Discord",
|
||||||
|
"type": "DISCORD_CONNECTOR",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
# Process each chunk and create sources directly without deduplication
|
||||||
|
sources_list = []
|
||||||
|
async with self.counter_lock:
|
||||||
|
for i, chunk in enumerate(discord_chunks):
|
||||||
|
# Fix for UI
|
||||||
|
discord_chunks[i]['document']['id'] = self.source_id_counter
|
||||||
|
# Extract document metadata
|
||||||
|
document = chunk.get('document', {})
|
||||||
|
metadata = document.get('metadata', {})
|
||||||
|
|
||||||
|
# Create a mapped source entry with Discord-specific metadata
|
||||||
|
channel_name = metadata.get('channel_name', 'Unknown Channel')
|
||||||
|
channel_id = metadata.get('channel_id', '')
|
||||||
|
message_date = metadata.get('start_date', '')
|
||||||
|
|
||||||
|
# Create a more descriptive title for Discord messages
|
||||||
|
title = f"Discord: {channel_name}"
|
||||||
|
if message_date:
|
||||||
|
title += f" ({message_date})"
|
||||||
|
|
||||||
|
# Create a more descriptive description for Discord messages
|
||||||
|
description = chunk.get('content', '')[:100]
|
||||||
|
if len(description) == 100:
|
||||||
|
description += "..."
|
||||||
|
|
||||||
|
url = ""
|
||||||
|
guild_id = metadata.get('guild_id', '')
|
||||||
|
if guild_id and channel_id:
|
||||||
|
url = f"https://discord.com/channels/{guild_id}/{channel_id}"
|
||||||
|
elif channel_id:
|
||||||
|
# Fallback for DM channels or when guild_id is not available
|
||||||
|
url = f"https://discord.com/channels/@me/{channel_id}"
|
||||||
|
|
||||||
|
source = {
|
||||||
|
"id": self.source_id_counter,
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.source_id_counter += 1
|
||||||
|
sources_list.append(source)
|
||||||
|
|
||||||
|
# Create result object
|
||||||
|
result_object = {
|
||||||
|
"id": 11,
|
||||||
|
"name": "Discord",
|
||||||
|
"type": "DISCORD_CONNECTOR",
|
||||||
|
"sources": sources_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result_object, discord_chunks
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ dependencies = [
|
||||||
"alembic>=1.13.0",
|
"alembic>=1.13.0",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"chonkie[all]>=1.0.6",
|
"chonkie[all]>=1.0.6",
|
||||||
|
"discord-py>=2.5.2",
|
||||||
"fastapi>=0.115.8",
|
"fastapi>=0.115.8",
|
||||||
"fastapi-users[oauth,sqlalchemy]>=14.0.1",
|
"fastapi-users[oauth,sqlalchemy]>=14.0.1",
|
||||||
"firecrawl-py>=1.12.0",
|
"firecrawl-py>=1.12.0",
|
||||||
|
|
3395
surfsense_backend/uv.lock
generated
3395
surfsense_backend/uv.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -191,6 +191,18 @@ export default function EditConnectorPage() {
|
||||||
placeholder="Begins with linkup_..."
|
placeholder="Begins with linkup_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* == Discord == */}
|
||||||
|
{connector.connector_type === "DISCORD_CONNECTOR" && (
|
||||||
|
<EditSimpleTokenForm
|
||||||
|
control={editForm.control}
|
||||||
|
fieldName="DISCORD_BOT_TOKEN"
|
||||||
|
fieldLabel="Discord Bot Token"
|
||||||
|
fieldDescription="Update the Discord Bot Token if needed."
|
||||||
|
placeholder="Bot token..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="border-t pt-6">
|
<CardFooter className="border-t pt-6">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -52,6 +52,7 @@ const getConnectorTypeDisplay = (type: string): string => {
|
||||||
"SLACK_CONNECTOR": "Slack Connector",
|
"SLACK_CONNECTOR": "Slack Connector",
|
||||||
"NOTION_CONNECTOR": "Notion Connector",
|
"NOTION_CONNECTOR": "Notion Connector",
|
||||||
"GITHUB_CONNECTOR": "GitHub Connector",
|
"GITHUB_CONNECTOR": "GitHub Connector",
|
||||||
|
"DISCORD_CONNECTOR": "Discord Connector",
|
||||||
"LINKUP_API": "Linkup",
|
"LINKUP_API": "Linkup",
|
||||||
// Add other connector types here as needed
|
// Add other connector types here as needed
|
||||||
};
|
};
|
||||||
|
@ -89,6 +90,7 @@ export default function EditConnectorPage() {
|
||||||
"SLACK_CONNECTOR": "SLACK_BOT_TOKEN",
|
"SLACK_CONNECTOR": "SLACK_BOT_TOKEN",
|
||||||
"NOTION_CONNECTOR": "NOTION_INTEGRATION_TOKEN",
|
"NOTION_CONNECTOR": "NOTION_INTEGRATION_TOKEN",
|
||||||
"GITHUB_CONNECTOR": "GITHUB_PAT",
|
"GITHUB_CONNECTOR": "GITHUB_PAT",
|
||||||
|
"DISCORD_CONNECTOR": "DISCORD_BOT_TOKEN",
|
||||||
"LINKUP_API": "LINKUP_API_KEY"
|
"LINKUP_API": "LINKUP_API_KEY"
|
||||||
};
|
};
|
||||||
return fieldMap[connectorType] || "";
|
return fieldMap[connectorType] || "";
|
||||||
|
|
|
@ -0,0 +1,315 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter, useParams } from "next/navigation";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertTitle,
|
||||||
|
} from "@/components/ui/alert";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
// Define the form schema with Zod
|
||||||
|
const discordConnectorFormSchema = z.object({
|
||||||
|
name: z.string().min(3, {
|
||||||
|
message: "Connector name must be at least 3 characters.",
|
||||||
|
}),
|
||||||
|
bot_token: z.string()
|
||||||
|
.min(50, { message: "Discord Bot Token appears to be too short." })
|
||||||
|
.regex(/^[A-Za-z0-9._-]+$/, { message: "Discord Bot Token contains invalid characters." }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the type for the form values
|
||||||
|
type DiscordConnectorFormValues = z.infer<typeof discordConnectorFormSchema>;
|
||||||
|
|
||||||
|
export default function DiscordConnectorPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const searchSpaceId = params.search_space_id as string;
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const { createConnector } = useSearchSourceConnectors();
|
||||||
|
|
||||||
|
// Initialize the form
|
||||||
|
const form = useForm<DiscordConnectorFormValues>({
|
||||||
|
resolver: zodResolver(discordConnectorFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "Discord Connector",
|
||||||
|
bot_token: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const onSubmit = async (values: DiscordConnectorFormValues) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await createConnector({
|
||||||
|
name: values.name,
|
||||||
|
connector_type: "DISCORD_CONNECTOR",
|
||||||
|
config: {
|
||||||
|
DISCORD_BOT_TOKEN: values.bot_token,
|
||||||
|
},
|
||||||
|
is_indexable: true,
|
||||||
|
last_indexed_at: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Discord connector created successfully!");
|
||||||
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating connector:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 max-w-3xl">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="mb-6"
|
||||||
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Connectors
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<Tabs defaultValue="connect" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||||
|
<TabsTrigger value="connect">Connect</TabsTrigger>
|
||||||
|
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="connect">
|
||||||
|
<Card className="border-2 border-border">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold">Connect Discord Server</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Integrate with Discord to search and retrieve information from your servers and channels. This connector can index your Discord messages for search.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Alert className="mb-6 bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>Bot Token Required</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
You'll need a Discord Bot Token to use this connector. You can create a Discord bot and get the token from the{" "}
|
||||||
|
<a
|
||||||
|
href="https://discord.com/developers/applications"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Discord Developer Portal
|
||||||
|
</a>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Connector Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My Discord Connector" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
A friendly name to identify this connector.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="bot_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Discord Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Bot Token..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Discord Bot Token will be encrypted and stored securely. You can find it in the Bot section of your application in the Discord Developer Portal.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
Connect Discord
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex flex-col items-start border-t bg-muted/50 px-6 py-4">
|
||||||
|
<h4 className="text-sm font-medium">What you get with Discord integration:</h4>
|
||||||
|
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||||
|
<li>Search through your Discord servers and channels</li>
|
||||||
|
<li>Access historical messages and shared files</li>
|
||||||
|
<li>Connect your team's knowledge directly to your search space</li>
|
||||||
|
<li>Keep your search results up-to-date with latest communications</li>
|
||||||
|
<li>Index your Discord messages for enhanced search capabilities</li>
|
||||||
|
</ul>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="documentation">
|
||||||
|
<Card className="border-2 border-border">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold">Discord Connector Documentation</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Learn how to set up and use the Discord connector to index your server data.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The Discord connector indexes all accessible channels for a given bot in your servers.
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
||||||
|
<li>Upcoming: Support for private channels by granting the bot access.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="authorization">
|
||||||
|
<AccordionTrigger className="text-lg font-medium">Authorization</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4">
|
||||||
|
<Alert className="bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>Bot Setup Required</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
You must create a Discord bot and add it to your server with the correct permissions.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<ol className="list-decimal pl-5 space-y-3">
|
||||||
|
<li>Go to <a href="https://discord.com/developers/applications" target="_blank" rel="noopener noreferrer" className="font-medium underline underline-offset-4">https://discord.com/developers/applications</a>.</li>
|
||||||
|
<li>Create a new application and add a bot to it.</li>
|
||||||
|
<li>Copy the Bot Token from the Bot section.</li>
|
||||||
|
<li>Invite the bot to your server with the following OAuth2 scopes and permissions:
|
||||||
|
<ul className="list-disc pl-5 mt-1">
|
||||||
|
<li>Scopes: <code>bot</code></li>
|
||||||
|
<li>Bot Permissions: <code>Read Messages/View Channels</code>, <code>Read Message History</code>, <code>Send Messages</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Paste the Bot Token above to connect.</li>
|
||||||
|
</ol>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="indexing">
|
||||||
|
<AccordionTrigger className="text-lg font-medium">Indexing</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4">
|
||||||
|
<ol className="list-decimal pl-5 space-y-3">
|
||||||
|
<li>Navigate to the Connector Dashboard and select the <strong>Discord</strong> Connector.</li>
|
||||||
|
<li>Place the <strong>Bot Token</strong> under <strong>Step 1 Provide Credentials</strong>.</li>
|
||||||
|
<li>Click <strong>Connect</strong> to establish the connection.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<Alert className="bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>Important: Bot Channel Access</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
After connecting, ensure the bot has access to all channels you want to index. You may need to adjust channel permissions in Discord.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Alert className="bg-muted mt-4">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>First Indexing</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
The first indexing pulls all accessible channels and may take longer than future updates. Only channels where the bot has access will be indexed.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4 className="font-medium mb-2">Troubleshooting:</h4>
|
||||||
|
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<strong>Missing messages:</strong> If you don't see messages from a channel, check the bot's permissions for that channel.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Bot not responding:</strong> Make sure the bot is online and the token is correct.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Private channels:</strong> The bot must be explicitly granted access to private channels.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -79,11 +79,11 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
status: "coming-soon",
|
status: "coming-soon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "discord",
|
id: "discord-connector",
|
||||||
title: "Discord",
|
title: "Discord",
|
||||||
description: "Connect to Discord servers to access messages and channels.",
|
description: "Connect to Discord servers to access messages and channels.",
|
||||||
icon: <IconBrandDiscord className="h-6 w-6" />,
|
icon: <IconBrandDiscord className="h-6 w-6" />,
|
||||||
status: "coming-soon",
|
status: "available"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -190,7 +190,7 @@ const cardVariants = {
|
||||||
export default function ConnectorsPage() {
|
export default function ConnectorsPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const searchSpaceId = params.search_space_id as string;
|
const searchSpaceId = params.search_space_id as string;
|
||||||
const [expandedCategories, setExpandedCategories] = useState<string[]>(["search-engines", "knowledge-bases", "project-management"]);
|
const [expandedCategories, setExpandedCategories] = useState<string[]>(["search-engines", "knowledge-bases", "project-management", "team-chats"]);
|
||||||
|
|
||||||
const toggleCategory = (categoryId: string) => {
|
const toggleCategory = (categoryId: string) => {
|
||||||
setExpandedCategories(prev =>
|
setExpandedCategories(prev =>
|
||||||
|
|
|
@ -45,7 +45,7 @@ import {
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useDocuments } from "@/hooks/use-documents";
|
import { useDocuments } from "@/hooks/use-documents";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { IconBrandGithub, IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconLayoutKanban } from "@tabler/icons-react";
|
import { IconBrandDiscord, IconBrandGithub, IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconLayoutKanban } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
|
@ -109,7 +109,7 @@ const fadeInScale = {
|
||||||
type Document = {
|
type Document = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
document_type: "EXTENSION" | "CRAWLED_URL" | "SLACK_CONNECTOR" | "NOTION_CONNECTOR" | "FILE" | "YOUTUBE_VIDEO" | "LINEAR_CONNECTOR";
|
document_type: "EXTENSION" | "CRAWLED_URL" | "SLACK_CONNECTOR" | "NOTION_CONNECTOR" | "FILE" | "YOUTUBE_VIDEO" | "LINEAR_CONNECTOR" | "DISCORD_CONNECTOR";
|
||||||
document_metadata: any;
|
document_metadata: any;
|
||||||
content: string;
|
content: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
@ -139,6 +139,7 @@ const documentTypeIcons = {
|
||||||
YOUTUBE_VIDEO: IconBrandYoutube,
|
YOUTUBE_VIDEO: IconBrandYoutube,
|
||||||
GITHUB_CONNECTOR: IconBrandGithub,
|
GITHUB_CONNECTOR: IconBrandGithub,
|
||||||
LINEAR_CONNECTOR: IconLayoutKanban,
|
LINEAR_CONNECTOR: IconLayoutKanban,
|
||||||
|
DISCORD_CONNECTOR: IconBrandDiscord,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const columns: ColumnDef<Document>[] = [
|
const columns: ColumnDef<Document>[] = [
|
||||||
|
|
|
@ -47,7 +47,7 @@ export function ModernHeroWithGradients() {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="mx-auto max-w-3xl py-6 text-center text-base text-gray-600 dark:text-neutral-300 md:text-lg lg:text-xl">
|
<p className="mx-auto max-w-3xl py-6 text-center text-base text-gray-600 dark:text-neutral-300 md:text-lg lg:text-xl">
|
||||||
A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, YouTube, GitHub and more.
|
A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, YouTube, GitHub, Discord, and more.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col items-center gap-6 py-6 sm:flex-row">
|
<div className="flex flex-col items-center gap-6 py-6 sm:flex-row">
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
Link,
|
Link,
|
||||||
Webhook,
|
Webhook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban, IconLinkPlus } from "@tabler/icons-react";
|
import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban, IconLinkPlus, IconBrandDiscord } from "@tabler/icons-react";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Connector, ResearchMode } from './types';
|
import { Connector, ResearchMode } from './types';
|
||||||
|
|
||||||
|
@ -41,6 +41,8 @@ export const getConnectorIcon = (connectorType: string) => {
|
||||||
return <IconBrandSlack {...iconProps} />;
|
return <IconBrandSlack {...iconProps} />;
|
||||||
case 'NOTION_CONNECTOR':
|
case 'NOTION_CONNECTOR':
|
||||||
return <IconBrandNotion {...iconProps} />;
|
return <IconBrandNotion {...iconProps} />;
|
||||||
|
case 'DISCORD_CONNECTOR':
|
||||||
|
return <IconBrandDiscord {...iconProps} />;
|
||||||
case 'DEEP':
|
case 'DEEP':
|
||||||
return <Sparkles {...iconProps} />;
|
return <Sparkles {...iconProps} />;
|
||||||
case 'DEEPER':
|
case 'DEEPER':
|
||||||
|
|
|
@ -31,5 +31,6 @@ export const editConnectorSchema = z.object({
|
||||||
TAVILY_API_KEY: z.string().optional(),
|
TAVILY_API_KEY: z.string().optional(),
|
||||||
LINEAR_API_KEY: z.string().optional(),
|
LINEAR_API_KEY: z.string().optional(),
|
||||||
LINKUP_API_KEY: z.string().optional(),
|
LINKUP_API_KEY: z.string().optional(),
|
||||||
|
DISCORD_BOT_TOKEN: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type EditConnectorFormValues = z.infer<typeof editConnectorSchema>;
|
export type EditConnectorFormValues = z.infer<typeof editConnectorSchema>;
|
||||||
|
|
|
@ -218,7 +218,7 @@ export function AppSidebar({
|
||||||
</div>
|
</div>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">SurfSense</span>
|
<span className="truncate font-medium">SurfSense</span>
|
||||||
<span className="truncate text-xs">beta v0.0.6</span>
|
<span className="truncate text-xs">beta v0.0.7</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
|
|
|
@ -41,7 +41,8 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
NOTION_INTEGRATION_TOKEN: "",
|
NOTION_INTEGRATION_TOKEN: "",
|
||||||
SERPER_API_KEY: "",
|
SERPER_API_KEY: "",
|
||||||
TAVILY_API_KEY: "",
|
TAVILY_API_KEY: "",
|
||||||
LINEAR_API_KEY: ""
|
LINEAR_API_KEY: "",
|
||||||
|
DISCORD_BOT_TOKEN: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,7 +61,8 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
SERPER_API_KEY: config.SERPER_API_KEY || "",
|
SERPER_API_KEY: config.SERPER_API_KEY || "",
|
||||||
TAVILY_API_KEY: config.TAVILY_API_KEY || "",
|
TAVILY_API_KEY: config.TAVILY_API_KEY || "",
|
||||||
LINEAR_API_KEY: config.LINEAR_API_KEY || "",
|
LINEAR_API_KEY: config.LINEAR_API_KEY || "",
|
||||||
LINKUP_API_KEY: config.LINKUP_API_KEY || ""
|
LINKUP_API_KEY: config.LINKUP_API_KEY || "",
|
||||||
|
DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "",
|
||||||
});
|
});
|
||||||
if (currentConnector.connector_type === 'GITHUB_CONNECTOR') {
|
if (currentConnector.connector_type === 'GITHUB_CONNECTOR') {
|
||||||
const savedRepos = config.repo_full_names || [];
|
const savedRepos = config.repo_full_names || [];
|
||||||
|
@ -171,6 +173,12 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
newConfig = { LINKUP_API_KEY: formData.LINKUP_API_KEY };
|
newConfig = { LINKUP_API_KEY: formData.LINKUP_API_KEY };
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'DISCORD_CONNECTOR':
|
||||||
|
if (formData.DISCORD_BOT_TOKEN !== originalConfig.DISCORD_BOT_TOKEN) {
|
||||||
|
if (!formData.DISCORD_BOT_TOKEN) { toast.error("Discord Bot Token cannot be empty."); setIsSaving(false); return; }
|
||||||
|
newConfig = { DISCORD_BOT_TOKEN: formData.DISCORD_BOT_TOKEN };
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newConfig !== null) {
|
if (newConfig !== null) {
|
||||||
|
@ -212,6 +220,8 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
editForm.setValue('LINEAR_API_KEY', newlySavedConfig.LINEAR_API_KEY || "");
|
editForm.setValue('LINEAR_API_KEY', newlySavedConfig.LINEAR_API_KEY || "");
|
||||||
} else if(connector.connector_type === 'LINKUP_API') {
|
} else if(connector.connector_type === 'LINKUP_API') {
|
||||||
editForm.setValue('LINKUP_API_KEY', newlySavedConfig.LINKUP_API_KEY || "");
|
editForm.setValue('LINKUP_API_KEY', newlySavedConfig.LINKUP_API_KEY || "");
|
||||||
|
} else if(connector.connector_type === 'DISCORD_CONNECTOR') {
|
||||||
|
editForm.setValue('DISCORD_BOT_TOKEN', newlySavedConfig.DISCORD_BOT_TOKEN || "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (connector.connector_type === 'GITHUB_CONNECTOR') {
|
if (connector.connector_type === 'GITHUB_CONNECTOR') {
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
|
||||||
"NOTION_CONNECTOR": "Notion",
|
"NOTION_CONNECTOR": "Notion",
|
||||||
"GITHUB_CONNECTOR": "GitHub",
|
"GITHUB_CONNECTOR": "GitHub",
|
||||||
"LINEAR_CONNECTOR": "Linear",
|
"LINEAR_CONNECTOR": "Linear",
|
||||||
|
"DISCORD_CONNECTOR": "Discord",
|
||||||
"LINKUP_API": "Linkup",
|
"LINKUP_API": "Linkup",
|
||||||
};
|
};
|
||||||
return typeMap[type] || type;
|
return typeMap[type] || type;
|
||||||
|
|
Loading…
Add table
Reference in a new issue