mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-19 18:45:15 +02:00
Merge pull request #222 from CREDO23/feature/add-jira-connector
[Feat]: Add JIRA connector
This commit is contained in:
commit
665074e6e9
31 changed files with 7710 additions and 4614 deletions
|
|
@ -67,7 +67,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^surfsense_backend/
|
files: ^surfsense_backend/
|
||||||
additional_dependencies: []
|
additional_dependencies: ['types-requests']
|
||||||
args: [--ignore-missing-imports, --disallow-untyped-defs]
|
args: [--ignore-missing-imports, --disallow-untyped-defs]
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
|
|
@ -75,7 +75,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
files: ^surfsense_backend/
|
files: ^surfsense_backend/
|
||||||
args: ['-r', '.', '-f', 'json']
|
args: ['-r', '-f', 'json']
|
||||||
exclude: ^surfsense_backend/(tests/|alembic/)
|
exclude: ^surfsense_backend/(tests/|alembic/)
|
||||||
|
|
||||||
# Frontend/Extension Hooks (TypeScript/JavaScript)
|
# Frontend/Extension Hooks (TypeScript/JavaScript)
|
||||||
|
|
|
||||||
1
node_modules/.cache/prettier/.prettier-caches/a2ecb2962bf19c1099cfe708e42daa0097f94976.json
generated
vendored
Normal file
1
node_modules/.cache/prettier/.prettier-caches/a2ecb2962bf19c1099cfe708e42daa0097f94976.json
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"2d0ec64d93969318101ee479b664221b32241665":{"files":{"surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx":["EHKKvlOK0vfy0GgHwlG/J2Bx5rw=",true]},"modified":1753426633288}}
|
||||||
|
|
@ -6,10 +6,8 @@ Revises: 10
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects.postgresql import UUID, JSON
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "11"
|
revision: str = "11"
|
||||||
|
|
@ -21,66 +19,144 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema - add LiteLLMProvider enum, LLMConfig table and user LLM preferences."""
|
"""Upgrade schema - add LiteLLMProvider enum, LLMConfig table and user LLM preferences."""
|
||||||
|
|
||||||
# Check if enum type exists and create if it doesn't
|
# Create enum only if not exists
|
||||||
op.execute("""
|
op.execute(
|
||||||
|
"""
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'litellmprovider') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'litellmprovider') THEN
|
||||||
CREATE TYPE litellmprovider AS ENUM ('OPENAI', 'ANTHROPIC', 'GROQ', 'COHERE', 'HUGGINGFACE', 'AZURE_OPENAI', 'GOOGLE', 'AWS_BEDROCK', 'OLLAMA', 'MISTRAL', 'TOGETHER_AI', 'REPLICATE', 'PALM', 'VERTEX_AI', 'ANYSCALE', 'PERPLEXITY', 'DEEPINFRA', 'AI21', 'NLPCLOUD', 'ALEPH_ALPHA', 'PETALS', 'CUSTOM');
|
CREATE TYPE litellmprovider AS ENUM (
|
||||||
|
'OPENAI', 'ANTHROPIC', 'GROQ', 'COHERE', 'HUGGINGFACE',
|
||||||
|
'AZURE_OPENAI', 'GOOGLE', 'AWS_BEDROCK', 'OLLAMA', 'MISTRAL',
|
||||||
|
'TOGETHER_AI', 'REPLICATE', 'PALM', 'VERTEX_AI', 'ANYSCALE',
|
||||||
|
'PERPLEXITY', 'DEEPINFRA', 'AI21', 'NLPCLOUD', 'ALEPH_ALPHA',
|
||||||
|
'PETALS', 'CUSTOM'
|
||||||
|
);
|
||||||
END IF;
|
END IF;
|
||||||
END$$;
|
END$$;
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Create llm_configs table using raw SQL to avoid enum creation conflicts
|
# Create llm_configs table only if it doesn't already exist
|
||||||
op.execute("""
|
op.execute(
|
||||||
CREATE TABLE llm_configs (
|
"""
|
||||||
id SERIAL PRIMARY KEY,
|
DO $$
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
BEGIN
|
||||||
name VARCHAR(100) NOT NULL,
|
IF NOT EXISTS (
|
||||||
provider litellmprovider NOT NULL,
|
SELECT FROM information_schema.tables
|
||||||
custom_provider VARCHAR(100),
|
WHERE table_name = 'llm_configs'
|
||||||
model_name VARCHAR(100) NOT NULL,
|
) THEN
|
||||||
api_key TEXT NOT NULL,
|
CREATE TABLE llm_configs (
|
||||||
api_base VARCHAR(500),
|
id SERIAL PRIMARY KEY,
|
||||||
litellm_params JSONB,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE
|
name VARCHAR(100) NOT NULL,
|
||||||
)
|
provider litellmprovider NOT NULL,
|
||||||
""")
|
custom_provider VARCHAR(100),
|
||||||
|
model_name VARCHAR(100) NOT NULL,
|
||||||
|
api_key TEXT NOT NULL,
|
||||||
|
api_base VARCHAR(500),
|
||||||
|
litellm_params JSONB,
|
||||||
|
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Create indexes
|
# Create indexes if they don't exist
|
||||||
op.create_index(op.f('ix_llm_configs_id'), 'llm_configs', ['id'], unique=False)
|
op.execute(
|
||||||
op.create_index(op.f('ix_llm_configs_created_at'), 'llm_configs', ['created_at'], unique=False)
|
"""
|
||||||
op.create_index(op.f('ix_llm_configs_name'), 'llm_configs', ['name'], unique=False)
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'llm_configs' AND indexname = 'ix_llm_configs_id'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX ix_llm_configs_id ON llm_configs(id);
|
||||||
|
END IF;
|
||||||
|
|
||||||
# Add LLM preference columns to user table
|
IF NOT EXISTS (
|
||||||
op.add_column('user', sa.Column('long_context_llm_id', sa.Integer(), nullable=True))
|
SELECT 1 FROM pg_indexes
|
||||||
op.add_column('user', sa.Column('fast_llm_id', sa.Integer(), nullable=True))
|
WHERE tablename = 'llm_configs' AND indexname = 'ix_llm_configs_created_at'
|
||||||
op.add_column('user', sa.Column('strategic_llm_id', sa.Integer(), nullable=True))
|
) THEN
|
||||||
|
CREATE INDEX ix_llm_configs_created_at ON llm_configs(created_at);
|
||||||
|
END IF;
|
||||||
|
|
||||||
# Create foreign key constraints for LLM preferences
|
IF NOT EXISTS (
|
||||||
op.create_foreign_key(op.f('fk_user_long_context_llm_id_llm_configs'), 'user', 'llm_configs', ['long_context_llm_id'], ['id'], ondelete='SET NULL')
|
SELECT 1 FROM pg_indexes
|
||||||
op.create_foreign_key(op.f('fk_user_fast_llm_id_llm_configs'), 'user', 'llm_configs', ['fast_llm_id'], ['id'], ondelete='SET NULL')
|
WHERE tablename = 'llm_configs' AND indexname = 'ix_llm_configs_name'
|
||||||
op.create_foreign_key(op.f('fk_user_strategic_llm_id_llm_configs'), 'user', 'llm_configs', ['strategic_llm_id'], ['id'], ondelete='SET NULL')
|
) THEN
|
||||||
|
CREATE INDEX ix_llm_configs_name ON llm_configs(name);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Safely add columns to user table
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
existing_columns = [col["name"] for col in inspector.get_columns("user")]
|
||||||
|
|
||||||
|
with op.batch_alter_table("user") as batch_op:
|
||||||
|
if "long_context_llm_id" not in existing_columns:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column("long_context_llm_id", sa.Integer(), nullable=True)
|
||||||
|
)
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
op.f("fk_user_long_context_llm_id_llm_configs"),
|
||||||
|
"llm_configs",
|
||||||
|
["long_context_llm_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
|
||||||
|
if "fast_llm_id" not in existing_columns:
|
||||||
|
batch_op.add_column(sa.Column("fast_llm_id", sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
op.f("fk_user_fast_llm_id_llm_configs"),
|
||||||
|
"llm_configs",
|
||||||
|
["fast_llm_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
|
||||||
|
if "strategic_llm_id" not in existing_columns:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column("strategic_llm_id", sa.Integer(), nullable=True)
|
||||||
|
)
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
op.f("fk_user_strategic_llm_id_llm_configs"),
|
||||||
|
"llm_configs",
|
||||||
|
["strategic_llm_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema - remove LLMConfig table and user LLM preferences."""
|
"""Downgrade schema - remove LLMConfig table and user LLM preferences."""
|
||||||
|
|
||||||
# Drop foreign key constraints
|
# Drop foreign key constraints
|
||||||
op.drop_constraint(op.f('fk_user_strategic_llm_id_llm_configs'), 'user', type_='foreignkey')
|
op.drop_constraint(
|
||||||
op.drop_constraint(op.f('fk_user_fast_llm_id_llm_configs'), 'user', type_='foreignkey')
|
op.f("fk_user_strategic_llm_id_llm_configs"), "user", type_="foreignkey"
|
||||||
op.drop_constraint(op.f('fk_user_long_context_llm_id_llm_configs'), 'user', type_='foreignkey')
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
op.f("fk_user_fast_llm_id_llm_configs"), "user", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
op.f("fk_user_long_context_llm_id_llm_configs"), "user", type_="foreignkey"
|
||||||
|
)
|
||||||
|
|
||||||
# Drop LLM preference columns from user table
|
# Drop LLM preference columns from user table
|
||||||
op.drop_column('user', 'strategic_llm_id')
|
op.drop_column("user", "strategic_llm_id")
|
||||||
op.drop_column('user', 'fast_llm_id')
|
op.drop_column("user", "fast_llm_id")
|
||||||
op.drop_column('user', 'long_context_llm_id')
|
op.drop_column("user", "long_context_llm_id")
|
||||||
|
|
||||||
# Drop indexes and table
|
# Drop indexes and table
|
||||||
op.drop_index(op.f('ix_llm_configs_name'), table_name='llm_configs')
|
op.drop_index(op.f("ix_llm_configs_name"), table_name="llm_configs")
|
||||||
op.drop_index(op.f('ix_llm_configs_created_at'), table_name='llm_configs')
|
op.drop_index(op.f("ix_llm_configs_created_at"), table_name="llm_configs")
|
||||||
op.drop_index(op.f('ix_llm_configs_id'), table_name='llm_configs')
|
op.drop_index(op.f("ix_llm_configs_id"), table_name="llm_configs")
|
||||||
op.drop_table('llm_configs')
|
op.drop_table("llm_configs")
|
||||||
|
|
||||||
# Drop LiteLLMProvider enum
|
# Drop LiteLLMProvider enum
|
||||||
op.execute("DROP TYPE IF EXISTS litellmprovider")
|
op.execute("DROP TYPE IF EXISTS litellmprovider")
|
||||||
|
|
@ -7,9 +7,7 @@ Revises: 11
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.dialects.postgresql import JSON
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "12"
|
revision: str = "12"
|
||||||
|
|
@ -21,50 +19,76 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema - add LogLevel and LogStatus enums and logs table."""
|
"""Upgrade schema - add LogLevel and LogStatus enums and logs table."""
|
||||||
|
|
||||||
# Create LogLevel enum
|
# Create LogLevel enum if it doesn't exist
|
||||||
op.execute("""
|
op.execute(
|
||||||
CREATE TYPE loglevel AS ENUM ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
|
"""
|
||||||
""")
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'loglevel') THEN
|
||||||
|
CREATE TYPE loglevel AS ENUM ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL');
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Create LogStatus enum
|
# Create LogStatus enum if it doesn't exist
|
||||||
op.execute("""
|
op.execute(
|
||||||
CREATE TYPE logstatus AS ENUM ('IN_PROGRESS', 'SUCCESS', 'FAILED')
|
"""
|
||||||
""")
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'logstatus') THEN
|
||||||
|
CREATE TYPE logstatus AS ENUM ('IN_PROGRESS', 'SUCCESS', 'FAILED');
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Create logs table
|
# Create logs table if it doesn't exist
|
||||||
op.execute("""
|
op.execute(
|
||||||
CREATE TABLE logs (
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
level loglevel NOT NULL,
|
level loglevel NOT NULL,
|
||||||
status logstatus NOT NULL,
|
status logstatus NOT NULL,
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
source VARCHAR(200),
|
source VARCHAR(200),
|
||||||
log_metadata JSONB DEFAULT '{}',
|
log_metadata JSONB DEFAULT '{}',
|
||||||
search_space_id INTEGER NOT NULL REFERENCES searchspaces(id) ON DELETE CASCADE
|
search_space_id INTEGER NOT NULL REFERENCES searchspaces(id) ON DELETE CASCADE
|
||||||
)
|
);
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Create indexes
|
# Get existing indexes
|
||||||
op.create_index(op.f('ix_logs_id'), 'logs', ['id'], unique=False)
|
conn = op.get_bind()
|
||||||
op.create_index(op.f('ix_logs_created_at'), 'logs', ['created_at'], unique=False)
|
inspector = inspect(conn)
|
||||||
op.create_index(op.f('ix_logs_level'), 'logs', ['level'], unique=False)
|
existing_indexes = [idx["name"] for idx in inspector.get_indexes("logs")]
|
||||||
op.create_index(op.f('ix_logs_status'), 'logs', ['status'], unique=False)
|
|
||||||
op.create_index(op.f('ix_logs_source'), 'logs', ['source'], unique=False)
|
# Create indexes only if they don't already exist
|
||||||
|
if "ix_logs_id" not in existing_indexes:
|
||||||
|
op.create_index("ix_logs_id", "logs", ["id"])
|
||||||
|
if "ix_logs_created_at" not in existing_indexes:
|
||||||
|
op.create_index("ix_logs_created_at", "logs", ["created_at"])
|
||||||
|
if "ix_logs_level" not in existing_indexes:
|
||||||
|
op.create_index("ix_logs_level", "logs", ["level"])
|
||||||
|
if "ix_logs_status" not in existing_indexes:
|
||||||
|
op.create_index("ix_logs_status", "logs", ["status"])
|
||||||
|
if "ix_logs_source" not in existing_indexes:
|
||||||
|
op.create_index("ix_logs_source", "logs", ["source"])
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema - remove logs table and enums."""
|
"""Downgrade schema - remove logs table and enums."""
|
||||||
|
|
||||||
# Drop indexes
|
# Drop indexes
|
||||||
op.drop_index(op.f('ix_logs_source'), table_name='logs')
|
op.drop_index("ix_logs_source", table_name="logs")
|
||||||
op.drop_index(op.f('ix_logs_status'), table_name='logs')
|
op.drop_index("ix_logs_status", table_name="logs")
|
||||||
op.drop_index(op.f('ix_logs_level'), table_name='logs')
|
op.drop_index("ix_logs_level", table_name="logs")
|
||||||
op.drop_index(op.f('ix_logs_created_at'), table_name='logs')
|
op.drop_index("ix_logs_created_at", table_name="logs")
|
||||||
op.drop_index(op.f('ix_logs_id'), table_name='logs')
|
op.drop_index("ix_logs_id", table_name="logs")
|
||||||
|
|
||||||
# Drop logs table
|
# Drop logs table
|
||||||
op.drop_table('logs')
|
op.drop_table("logs")
|
||||||
|
|
||||||
# Drop enums
|
# Drop enums
|
||||||
op.execute("DROP TYPE IF EXISTS logstatus")
|
op.execute("DROP TYPE IF EXISTS logstatus")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
"""Add JIRA_CONNECTOR to enums
|
||||||
|
|
||||||
|
Revision ID: 13
|
||||||
|
Revises: 12
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "13"
|
||||||
|
down_revision: Union[str, None] = "12"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Safely add 'JIRA_CONNECTOR' to enum types if missing."""
|
||||||
|
|
||||||
|
# Add to searchsourceconnectortype enum
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type t
|
||||||
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||||
|
WHERE t.typname = 'searchsourceconnectortype' AND e.enumlabel = 'JIRA_CONNECTOR'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE searchsourceconnectortype ADD VALUE 'JIRA_CONNECTOR';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to documenttype enum
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type t
|
||||||
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||||
|
WHERE t.typname = 'documenttype' AND e.enumlabel = 'JIRA_CONNECTOR'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE documenttype ADD VALUE 'JIRA_CONNECTOR';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""
|
||||||
|
Downgrade logic not implemented since PostgreSQL
|
||||||
|
does not support removing enum values.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
@ -4,16 +4,17 @@ Revision ID: 1
|
||||||
Revises:
|
Revises:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
# Import pgvector if needed for other types, though not for this ENUM change
|
# Import pgvector if needed for other types, though not for this ENUM change
|
||||||
# import pgvector
|
# import pgvector
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '1'
|
revision: str = "1"
|
||||||
down_revision: Union[str, None] = None
|
down_revision: Union[str, None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
@ -24,7 +25,23 @@ def upgrade() -> None:
|
||||||
|
|
||||||
# Manually add the command to add the enum value
|
# Manually add the command to add the enum value
|
||||||
# Note: It's generally better to let autogenerate handle this, but we're bypassing it
|
# Note: It's generally better to let autogenerate handle this, but we're bypassing it
|
||||||
op.execute("ALTER TYPE searchsourceconnectortype ADD VALUE 'GITHUB_CONNECTOR'")
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_enum
|
||||||
|
WHERE enumlabel = 'GITHUB_CONNECTOR'
|
||||||
|
AND enumtypid = (
|
||||||
|
SELECT oid FROM pg_type WHERE typname = 'searchsourceconnectortype'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE searchsourceconnectortype ADD VALUE 'GITHUB_CONNECTOR';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Pass for the rest, as autogenerate didn't run to add other schema details
|
# Pass for the rest, as autogenerate didn't run to add other schema details
|
||||||
pass
|
pass
|
||||||
|
|
@ -39,14 +56,19 @@ def downgrade() -> None:
|
||||||
# For now, we'll just pass. If you needed to reverse this, you'd likely
|
# For now, we'll just pass. If you needed to reverse this, you'd likely
|
||||||
# have to manually check if 'GITHUB_CONNECTOR' is used in the table
|
# have to manually check if 'GITHUB_CONNECTOR' is used in the table
|
||||||
# and then potentially recreate the type without it.
|
# and then potentially recreate the type without it.
|
||||||
op.execute("ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old")
|
op.execute(
|
||||||
op.execute("CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR')")
|
"ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old"
|
||||||
op.execute((
|
)
|
||||||
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
|
op.execute(
|
||||||
"connector_type::text::searchsourceconnectortype"
|
"CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR')"
|
||||||
))
|
)
|
||||||
|
op.execute(
|
||||||
|
(
|
||||||
|
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
|
||||||
|
"connector_type::text::searchsourceconnectortype"
|
||||||
|
)
|
||||||
|
)
|
||||||
op.execute("DROP TYPE searchsourceconnectortype_old")
|
op.execute("DROP TYPE searchsourceconnectortype_old")
|
||||||
|
|
||||||
|
|
||||||
pass
|
pass
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,56 @@ Revision ID: 2
|
||||||
Revises: e55302644c51
|
Revises: e55302644c51
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '2'
|
revision: str = "2"
|
||||||
down_revision: Union[str, None] = 'e55302644c51'
|
down_revision: Union[str, None] = "e55302644c51"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = 'LINEAR_CONNECTOR'
|
||||||
|
AND enumtypid = (
|
||||||
|
SELECT oid FROM pg_type WHERE typname = 'searchsourceconnectortype'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE searchsourceconnectortype ADD VALUE 'LINEAR_CONNECTOR';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Manually add the command to add the enum value
|
|
||||||
op.execute("ALTER TYPE searchsourceconnectortype ADD VALUE 'LINEAR_CONNECTOR'")
|
|
||||||
|
|
||||||
# Pass for the rest, as autogenerate didn't run to add other schema details
|
#
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
# Downgrading removal of an enum value requires recreating the type
|
# Downgrading removal of an enum value requires recreating the type
|
||||||
op.execute("ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old")
|
op.execute(
|
||||||
op.execute("CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR', 'GITHUB_CONNECTOR')")
|
"ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old"
|
||||||
op.execute((
|
)
|
||||||
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
|
op.execute(
|
||||||
"connector_type::text::searchsourceconnectortype"
|
"CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR', 'GITHUB_CONNECTOR')"
|
||||||
))
|
)
|
||||||
|
op.execute(
|
||||||
|
(
|
||||||
|
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
|
||||||
|
"connector_type::text::searchsourceconnectortype"
|
||||||
|
)
|
||||||
|
)
|
||||||
op.execute("DROP TYPE searchsourceconnectortype_old")
|
op.execute("DROP TYPE searchsourceconnectortype_old")
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,40 @@ Revision ID: 3
|
||||||
Revises: 2
|
Revises: 2
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '3'
|
revision: str = "3"
|
||||||
down_revision: Union[str, None] = '2'
|
down_revision: Union[str, None] = "2"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
# Define the ENUM type name and the new value
|
# Define the ENUM type name and the new value
|
||||||
ENUM_NAME = 'documenttype' # Make sure this matches the name in your DB (usually lowercase class name)
|
ENUM_NAME = "documenttype" # Make sure this matches the name in your DB (usually lowercase class name)
|
||||||
NEW_VALUE = 'LINEAR_CONNECTOR'
|
NEW_VALUE = "LINEAR_CONNECTOR"
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
op.execute(f"ALTER TYPE {ENUM_NAME} ADD VALUE '{NEW_VALUE}'")
|
op.execute(
|
||||||
|
f"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = '{NEW_VALUE}'
|
||||||
|
AND enumtypid = (
|
||||||
|
SELECT oid FROM pg_type WHERE typname = '{ENUM_NAME}'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE {ENUM_NAME} ADD VALUE '{NEW_VALUE}';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Warning: This will delete all rows with the new value
|
# Warning: This will delete all rows with the new value
|
||||||
|
|
@ -34,19 +49,19 @@ def downgrade() -> None:
|
||||||
|
|
||||||
# Enum values *before* LINEAR_CONNECTOR was added
|
# Enum values *before* LINEAR_CONNECTOR was added
|
||||||
old_values = (
|
old_values = (
|
||||||
'EXTENSION',
|
"EXTENSION",
|
||||||
'CRAWLED_URL',
|
"CRAWLED_URL",
|
||||||
'FILE',
|
"FILE",
|
||||||
'SLACK_CONNECTOR',
|
"SLACK_CONNECTOR",
|
||||||
'NOTION_CONNECTOR',
|
"NOTION_CONNECTOR",
|
||||||
'YOUTUBE_VIDEO',
|
"YOUTUBE_VIDEO",
|
||||||
'GITHUB_CONNECTOR'
|
"GITHUB_CONNECTOR",
|
||||||
)
|
)
|
||||||
old_values_sql = ", ".join([f"'{v}'" for v in old_values])
|
old_values_sql = ", ".join([f"'{v}'" for v in old_values])
|
||||||
|
|
||||||
# Table and column names (adjust if different)
|
# Table and column names (adjust if different)
|
||||||
table_name = 'documents'
|
table_name = "documents"
|
||||||
column_name = 'document_type'
|
column_name = "document_type"
|
||||||
|
|
||||||
# 1. Rename the current enum type
|
# 1. Rename the current enum type
|
||||||
op.execute(f"ALTER TYPE {ENUM_NAME} RENAME TO {old_enum_name}")
|
op.execute(f"ALTER TYPE {ENUM_NAME} RENAME TO {old_enum_name}")
|
||||||
|
|
@ -55,9 +70,7 @@ def downgrade() -> None:
|
||||||
op.execute(f"CREATE TYPE {ENUM_NAME} AS ENUM({old_values_sql})")
|
op.execute(f"CREATE TYPE {ENUM_NAME} AS ENUM({old_values_sql})")
|
||||||
|
|
||||||
# 3. Update the table:
|
# 3. Update the table:
|
||||||
op.execute(
|
op.execute(f"DELETE FROM {table_name} WHERE {column_name}::text = '{NEW_VALUE}'")
|
||||||
f"DELETE FROM {table_name} WHERE {column_name}::text = '{NEW_VALUE}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. Alter the column to use the new enum type (casting old values)
|
# 4. Alter the column to use the new enum type (casting old values)
|
||||||
op.execute(
|
op.execute(
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,56 @@ Revision ID: 4
|
||||||
Revises: 3
|
Revises: 3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '4'
|
revision: str = "4"
|
||||||
down_revision: Union[str, None] = '3'
|
down_revision: Union[str, None] = "3"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
ENUM_NAME = "searchsourceconnectortype"
|
||||||
|
NEW_VALUE = "LINKUP_API"
|
||||||
|
|
||||||
# Manually add the command to add the enum value
|
op.execute(
|
||||||
op.execute("ALTER TYPE searchsourceconnectortype ADD VALUE 'LINKUP_API'")
|
f"""
|
||||||
|
DO $$
|
||||||
# Pass for the rest, as autogenerate didn't run to add other schema details
|
BEGIN
|
||||||
pass
|
IF NOT EXISTS (
|
||||||
# ### end Alembic commands ###
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = '{NEW_VALUE}'
|
||||||
|
AND enumtypid = (
|
||||||
|
SELECT oid FROM pg_type WHERE typname = '{ENUM_NAME}'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE {ENUM_NAME} ADD VALUE '{NEW_VALUE}';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
# Downgrading removal of an enum value requires recreating the type
|
# Downgrading removal of an enum value requires recreating the type
|
||||||
op.execute("ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old")
|
op.execute(
|
||||||
op.execute("CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR', 'GITHUB_CONNECTOR', 'LINEAR_CONNECTOR')")
|
"ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old"
|
||||||
op.execute((
|
)
|
||||||
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
|
op.execute(
|
||||||
"connector_type::text::searchsourceconnectortype"
|
"CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR', 'GITHUB_CONNECTOR', 'LINEAR_CONNECTOR')"
|
||||||
))
|
)
|
||||||
|
op.execute(
|
||||||
|
(
|
||||||
|
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
|
||||||
|
"connector_type::text::searchsourceconnectortype"
|
||||||
|
)
|
||||||
|
)
|
||||||
op.execute("DROP TYPE searchsourceconnectortype_old")
|
op.execute("DROP TYPE searchsourceconnectortype_old")
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,58 @@ Revision ID: 6
|
||||||
Revises: 5
|
Revises: 5
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.dialects.postgresql import JSON
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '6'
|
revision: str = "6"
|
||||||
down_revision: Union[str, None] = '5'
|
down_revision: Union[str, None] = "5"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Drop the old column and create a new one with the new name and type
|
bind = op.get_bind()
|
||||||
# We need to do this because PostgreSQL doesn't support direct column renames with type changes
|
inspector = inspect(bind)
|
||||||
op.add_column('podcasts', sa.Column('podcast_transcript', JSON, nullable=False, server_default='{}'))
|
|
||||||
|
|
||||||
# Copy data from old column to new column
|
columns = [col["name"] for col in inspector.get_columns("podcasts")]
|
||||||
# Convert text to JSON by storing it as a JSON string value
|
if "podcast_transcript" not in columns:
|
||||||
op.execute("UPDATE podcasts SET podcast_transcript = jsonb_build_object('text', podcast_content) WHERE podcast_content != ''")
|
op.add_column(
|
||||||
|
"podcasts",
|
||||||
|
sa.Column("podcast_transcript", JSON, nullable=False, server_default="{}"),
|
||||||
|
)
|
||||||
|
|
||||||
# Drop the old column
|
# Copy data from old column to new column
|
||||||
op.drop_column('podcasts', 'podcast_content')
|
op.execute(
|
||||||
|
"""
|
||||||
|
UPDATE podcasts
|
||||||
|
SET podcast_transcript = jsonb_build_object('text', podcast_content)
|
||||||
|
WHERE podcast_content != ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Drop the old column only if it exists
|
||||||
|
if "podcast_content" in columns:
|
||||||
|
op.drop_column("podcasts", "podcast_content")
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Add back the original column
|
# Add back the original column
|
||||||
op.add_column('podcasts', sa.Column('podcast_content', sa.Text(), nullable=False, server_default=''))
|
op.add_column(
|
||||||
|
"podcasts",
|
||||||
|
sa.Column("podcast_content", sa.Text(), nullable=False, server_default=""),
|
||||||
|
)
|
||||||
|
|
||||||
# Copy data from JSON column back to text column
|
# Copy data from JSON column back to text column
|
||||||
# Extract the 'text' field if it exists, otherwise use empty string
|
# Extract the 'text' field if it exists, otherwise use empty string
|
||||||
op.execute("UPDATE podcasts SET podcast_content = COALESCE((podcast_transcript->>'text'), '')")
|
op.execute(
|
||||||
|
"UPDATE podcasts SET podcast_content = COALESCE((podcast_transcript->>'text'), '')"
|
||||||
|
)
|
||||||
|
|
||||||
# Drop the new column
|
# Drop the new column
|
||||||
op.drop_column('podcasts', 'podcast_transcript')
|
op.drop_column("podcasts", "podcast_transcript")
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,34 @@ Revision ID: 7
|
||||||
Revises: 6
|
Revises: 6
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '7'
|
revision: str = "7"
|
||||||
down_revision: Union[str, None] = '6'
|
down_revision: Union[str, None] = "6"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Drop the is_generated column
|
# Get the current database connection
|
||||||
op.drop_column('podcasts', 'is_generated')
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
|
||||||
|
# Check if the column exists before attempting to drop it
|
||||||
|
columns = [col["name"] for col in inspector.get_columns("podcasts")]
|
||||||
|
if "is_generated" in columns:
|
||||||
|
op.drop_column("podcasts", "is_generated")
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Add back the is_generated column with its original constraints
|
# Add back the is_generated column with its original constraints
|
||||||
op.add_column('podcasts', sa.Column('is_generated', sa.Boolean(), nullable=False, server_default='false'))
|
op.add_column(
|
||||||
|
"podcasts",
|
||||||
|
sa.Column("is_generated", sa.Boolean(), nullable=False, server_default="false"),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,54 +3,68 @@
|
||||||
Revision ID: 8
|
Revision ID: 8
|
||||||
Revises: 7
|
Revises: 7
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '8'
|
revision: str = "8"
|
||||||
down_revision: Union[str, None] = '7'
|
down_revision: Union[str, None] = "7"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add content_hash column as nullable first to handle existing data
|
bind = op.get_bind()
|
||||||
op.add_column('documents', sa.Column('content_hash', sa.String(), nullable=True))
|
inspector = inspect(bind)
|
||||||
|
columns = [col["name"] for col in inspector.get_columns("documents")]
|
||||||
|
|
||||||
# Update existing documents to generate content hashes
|
# Only add the column if it doesn't already exist
|
||||||
# Using SHA-256 hash of the content column with proper UTF-8 encoding
|
if "content_hash" not in columns:
|
||||||
op.execute("""
|
op.add_column(
|
||||||
UPDATE documents
|
"documents", sa.Column("content_hash", sa.String(), nullable=True)
|
||||||
SET content_hash = encode(sha256(convert_to(content, 'UTF8')), 'hex')
|
|
||||||
WHERE content_hash IS NULL
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Handle duplicate content hashes by keeping only the oldest document for each hash
|
|
||||||
# Delete newer documents with duplicate content hashes
|
|
||||||
op.execute("""
|
|
||||||
DELETE FROM documents
|
|
||||||
WHERE id NOT IN (
|
|
||||||
SELECT MIN(id)
|
|
||||||
FROM documents
|
|
||||||
GROUP BY content_hash
|
|
||||||
)
|
)
|
||||||
""")
|
|
||||||
|
|
||||||
# Now alter the column to match the model: nullable=False, index=True, unique=True
|
# Populate the content_hash column
|
||||||
op.alter_column('documents', 'content_hash',
|
op.execute(
|
||||||
existing_type=sa.String(),
|
"""
|
||||||
nullable=False)
|
UPDATE documents
|
||||||
op.create_index(op.f('ix_documents_content_hash'), 'documents', ['content_hash'], unique=False)
|
SET content_hash = encode(sha256(convert_to(content, 'UTF8')), 'hex')
|
||||||
op.create_unique_constraint(op.f('uq_documents_content_hash'), 'documents', ['content_hash'])
|
WHERE content_hash IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM documents
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM documents
|
||||||
|
GROUP BY content_hash
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
op.alter_column(
|
||||||
|
"documents", "content_hash", existing_type=sa.String(), nullable=False
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_documents_content_hash"),
|
||||||
|
"documents",
|
||||||
|
["content_hash"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_unique_constraint(
|
||||||
|
op.f("uq_documents_content_hash"), "documents", ["content_hash"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Column 'content_hash' already exists. Skipping column creation.")
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove constraints and index first
|
op.drop_constraint(op.f("uq_documents_content_hash"), "documents", type_="unique")
|
||||||
op.drop_constraint(op.f('uq_documents_content_hash'), 'documents', type_='unique')
|
op.drop_index(op.f("ix_documents_content_hash"), table_name="documents")
|
||||||
op.drop_index(op.f('ix_documents_content_hash'), table_name='documents')
|
op.drop_column("documents", "content_hash")
|
||||||
|
|
||||||
# Remove content_hash column from documents table
|
|
||||||
op.drop_column('documents', 'content_hash')
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ Revises: 8
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "9"
|
revision: str = "9"
|
||||||
|
|
@ -24,11 +22,38 @@ DOCUMENT_NEW_VALUE = "DISCORD_CONNECTOR"
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema - add DISCORD_CONNECTOR to connector and document enum."""
|
"""Upgrade schema - add DISCORD_CONNECTOR to connector and document enum safely."""
|
||||||
# Add DISCORD_CONNECTOR to searchsourceconnectortype
|
# Add DISCORD_CONNECTOR to searchsourceconnectortype only if not exists
|
||||||
op.execute(f"ALTER TYPE {CONNECTOR_ENUM} ADD VALUE '{CONNECTOR_NEW_VALUE}'")
|
op.execute(
|
||||||
# Add DISCORD_CONNECTOR to documenttype
|
f"""
|
||||||
op.execute(f"ALTER TYPE {DOCUMENT_ENUM} ADD VALUE '{DOCUMENT_NEW_VALUE}'")
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = '{CONNECTOR_NEW_VALUE}'
|
||||||
|
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = '{CONNECTOR_ENUM}')
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE {CONNECTOR_ENUM} ADD VALUE '{CONNECTOR_NEW_VALUE}';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add DISCORD_CONNECTOR to documenttype only if not exists
|
||||||
|
op.execute(
|
||||||
|
f"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = '{DOCUMENT_NEW_VALUE}'
|
||||||
|
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = '{DOCUMENT_ENUM}')
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE {DOCUMENT_ENUM} ADD VALUE '{DOCUMENT_NEW_VALUE}';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
|
@ -85,7 +110,6 @@ def downgrade() -> None:
|
||||||
# 4. Drop the old connector enum type
|
# 4. Drop the old connector enum type
|
||||||
op.execute(f"DROP TYPE {old_connector_enum_name}")
|
op.execute(f"DROP TYPE {old_connector_enum_name}")
|
||||||
|
|
||||||
|
|
||||||
# Document Enum Downgrade Steps
|
# Document Enum Downgrade Steps
|
||||||
# 1. Rename the current document enum type
|
# 1. Rename the current document enum type
|
||||||
op.execute(f"ALTER TYPE {DOCUMENT_ENUM} RENAME TO {old_document_enum_name}")
|
op.execute(f"ALTER TYPE {DOCUMENT_ENUM} RENAME TO {old_document_enum_name}")
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,67 @@
|
||||||
"""Add GITHUB_CONNECTOR to DocumentType enum
|
|
||||||
|
|
||||||
Revision ID: e55302644c51
|
|
||||||
Revises: 1
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = 'e55302644c51'
|
revision: str = "e55302644c51"
|
||||||
down_revision: Union[str, None] = '1'
|
down_revision: Union[str, None] = "1"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
# Define the ENUM type name and the new value
|
# Define the ENUM type name and the new value
|
||||||
ENUM_NAME = 'documenttype' # Make sure this matches the name in your DB (usually lowercase class name)
|
ENUM_NAME = "documenttype"
|
||||||
NEW_VALUE = 'GITHUB_CONNECTOR'
|
NEW_VALUE = "GITHUB_CONNECTOR"
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
op.execute(f"ALTER TYPE {ENUM_NAME} ADD VALUE '{NEW_VALUE}'")
|
op.execute(
|
||||||
|
f"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = '{NEW_VALUE}'
|
||||||
|
AND enumtypid = (
|
||||||
|
SELECT oid FROM pg_type WHERE typname = '{ENUM_NAME}'
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE {ENUM_NAME} ADD VALUE '{NEW_VALUE}';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Warning: This will delete all rows with the new value
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema - remove GITHUB_CONNECTOR from enum."""
|
"""Downgrade schema - remove GITHUB_CONNECTOR from enum."""
|
||||||
|
|
||||||
# The old type name
|
|
||||||
old_enum_name = f"{ENUM_NAME}_old"
|
old_enum_name = f"{ENUM_NAME}_old"
|
||||||
|
|
||||||
# Enum values *before* GITHUB_CONNECTOR was added
|
|
||||||
old_values = (
|
old_values = (
|
||||||
'EXTENSION',
|
"EXTENSION",
|
||||||
'CRAWLED_URL',
|
"CRAWLED_URL",
|
||||||
'FILE',
|
"FILE",
|
||||||
'SLACK_CONNECTOR',
|
"SLACK_CONNECTOR",
|
||||||
'NOTION_CONNECTOR',
|
"NOTION_CONNECTOR",
|
||||||
'YOUTUBE_VIDEO'
|
"YOUTUBE_VIDEO",
|
||||||
)
|
)
|
||||||
old_values_sql = ", ".join([f"'{v}'" for v in old_values])
|
old_values_sql = ", ".join([f"'{v}'" for v in old_values])
|
||||||
|
|
||||||
# Table and column names (adjust if different)
|
table_name = "documents"
|
||||||
table_name = 'documents'
|
column_name = "document_type"
|
||||||
column_name = 'document_type'
|
|
||||||
|
|
||||||
# 1. Rename the current enum type
|
# 1. Create the new enum type with the old values
|
||||||
op.execute(f"ALTER TYPE {ENUM_NAME} RENAME TO {old_enum_name}")
|
op.execute(f"CREATE TYPE {old_enum_name} AS ENUM({old_values_sql})")
|
||||||
|
|
||||||
# 2. Create the new enum type with the old values
|
# 2. Delete rows using the new value
|
||||||
op.execute(f"CREATE TYPE {ENUM_NAME} AS ENUM({old_values_sql})")
|
op.execute(f"DELETE FROM {table_name} WHERE {column_name}::text = '{NEW_VALUE}'")
|
||||||
|
|
||||||
# 3. Update the table:
|
# 3. Alter the column to use the old enum type
|
||||||
op.execute(
|
|
||||||
f"DELETE FROM {table_name} WHERE {column_name}::text = '{NEW_VALUE}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. Alter the column to use the new enum type (casting old values)
|
|
||||||
op.execute(
|
op.execute(
|
||||||
f"ALTER TABLE {table_name} ALTER COLUMN {column_name} "
|
f"ALTER TABLE {table_name} ALTER COLUMN {column_name} "
|
||||||
f"TYPE {ENUM_NAME} USING {column_name}::text::{ENUM_NAME}"
|
f"TYPE {old_enum_name} USING {column_name}::text::{old_enum_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5. Drop the old enum type
|
# 4. Drop the current enum type and rename the old one
|
||||||
op.execute(f"DROP TYPE {old_enum_name}")
|
op.execute(f"DROP TYPE {ENUM_NAME}")
|
||||||
# ### end Alembic commands ###
|
op.execute(f"ALTER TYPE {old_enum_name} RENAME TO {ENUM_NAME}")
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,8 @@ You are SurfSense, an advanced AI research assistant that provides detailed, wel
|
||||||
- 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)
|
- JIRA_CONNECTOR: "Jira project issues, tickets, and comments" (personal project tracking)
|
||||||
|
- DISCORD_CONNECTOR: "Discord server conversations and shared content" (personal community communications)
|
||||||
- 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>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ def get_connector_emoji(connector_name: str) -> str:
|
||||||
"NOTION_CONNECTOR": "📘",
|
"NOTION_CONNECTOR": "📘",
|
||||||
"GITHUB_CONNECTOR": "🐙",
|
"GITHUB_CONNECTOR": "🐙",
|
||||||
"LINEAR_CONNECTOR": "📊",
|
"LINEAR_CONNECTOR": "📊",
|
||||||
|
"JIRA_CONNECTOR": "🎫",
|
||||||
|
"DISCORD_CONNECTOR": "🗨️",
|
||||||
"TAVILY_API": "🔍",
|
"TAVILY_API": "🔍",
|
||||||
"LINKUP_API": "🔗"
|
"LINKUP_API": "🔗"
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +52,8 @@ def get_connector_friendly_name(connector_name: str) -> str:
|
||||||
"NOTION_CONNECTOR": "Notion",
|
"NOTION_CONNECTOR": "Notion",
|
||||||
"GITHUB_CONNECTOR": "GitHub",
|
"GITHUB_CONNECTOR": "GitHub",
|
||||||
"LINEAR_CONNECTOR": "Linear",
|
"LINEAR_CONNECTOR": "Linear",
|
||||||
|
"JIRA_CONNECTOR": "Jira",
|
||||||
|
"DISCORD_CONNECTOR": "Discord",
|
||||||
"TAVILY_API": "Tavily Search",
|
"TAVILY_API": "Tavily Search",
|
||||||
"LINKUP_API": "Linkup Search"
|
"LINKUP_API": "Linkup Search"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
487
surfsense_backend/app/connectors/jira_connector.py
Normal file
487
surfsense_backend/app/connectors/jira_connector.py
Normal file
|
|
@ -0,0 +1,487 @@
|
||||||
|
"""
|
||||||
|
Jira Connector Module
|
||||||
|
|
||||||
|
A module for retrieving data from Jira.
|
||||||
|
Allows fetching issue lists and their comments, projects and more.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class JiraConnector:
|
||||||
|
"""Class for retrieving data from Jira."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
api_token: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the JiraConnector class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') (optional)
|
||||||
|
email: Jira account email address (optional)
|
||||||
|
api_token: Jira API token (optional)
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip("/") if base_url else None
|
||||||
|
self.email = email
|
||||||
|
self.api_token = api_token
|
||||||
|
self.api_version = "3" # Jira Cloud API version
|
||||||
|
|
||||||
|
def set_credentials(self, base_url: str, email: str, api_token: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the Jira credentials.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: Jira instance base URL
|
||||||
|
email: Jira account email address
|
||||||
|
api_token: Jira API token
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.email = email
|
||||||
|
self.api_token = api_token
|
||||||
|
|
||||||
|
def set_email(self, email: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the Jira account email.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: Jira account email address
|
||||||
|
"""
|
||||||
|
self.email = email
|
||||||
|
|
||||||
|
def set_api_token(self, api_token: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the Jira API token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_token: Jira API token
|
||||||
|
"""
|
||||||
|
self.api_token = api_token
|
||||||
|
|
||||||
|
def get_headers(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get headers for Jira API requests using Basic Authentication.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of headers
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If email, api_token, or base_url have not been set
|
||||||
|
"""
|
||||||
|
if not all([self.base_url, self.email, self.api_token]):
|
||||||
|
raise ValueError(
|
||||||
|
"Jira credentials not initialized. Call set_credentials() first."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Basic Auth header using email:api_token
|
||||||
|
auth_str = f"{self.email}:{self.api_token}"
|
||||||
|
auth_bytes = auth_str.encode("utf-8")
|
||||||
|
auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": auth_header,
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_api_request(
|
||||||
|
self, endpoint: str, params: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Make a request to the Jira API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint: API endpoint (without base URL)
|
||||||
|
params: Query parameters for the request (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response data from the API
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If email, api_token, or base_url have not been set
|
||||||
|
Exception: If the API request fails
|
||||||
|
"""
|
||||||
|
if not all([self.base_url, self.email, self.api_token]):
|
||||||
|
raise ValueError(
|
||||||
|
"Jira credentials not initialized. Call set_credentials() first."
|
||||||
|
)
|
||||||
|
|
||||||
|
url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}"
|
||||||
|
headers = self.get_headers()
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers, params=params, timeout=500)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"API request failed with status code {response.status_code}: {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_all_projects(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Fetch all projects from Jira.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of project objects
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If credentials have not been set
|
||||||
|
Exception: If the API request fails
|
||||||
|
"""
|
||||||
|
return self.make_api_request("project/search")
|
||||||
|
|
||||||
|
def get_all_issues(self, project_key: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Fetch all issues from Jira.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_key: Optional project key to filter issues (e.g., 'PROJ')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of issue objects
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If credentials have not been set
|
||||||
|
Exception: If the API request fails
|
||||||
|
"""
|
||||||
|
jql = "ORDER BY created DESC"
|
||||||
|
if project_key:
|
||||||
|
jql = f'project = "{project_key}" ' + jql
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
"summary",
|
||||||
|
"description",
|
||||||
|
"status",
|
||||||
|
"assignee",
|
||||||
|
"reporter",
|
||||||
|
"created",
|
||||||
|
"updated",
|
||||||
|
"priority",
|
||||||
|
"issuetype",
|
||||||
|
"project",
|
||||||
|
]
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"jql": jql,
|
||||||
|
"fields": ",".join(fields),
|
||||||
|
"maxResults": 100,
|
||||||
|
"startAt": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
all_issues = []
|
||||||
|
start_at = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
params["startAt"] = start_at
|
||||||
|
result = self.make_api_request("search", params)
|
||||||
|
|
||||||
|
if not isinstance(result, dict) or "issues" not in result:
|
||||||
|
raise Exception("Invalid response from Jira API")
|
||||||
|
|
||||||
|
issues = result["issues"]
|
||||||
|
all_issues.extend(issues)
|
||||||
|
|
||||||
|
print(f"Fetched {len(issues)} issues (startAt={start_at})")
|
||||||
|
|
||||||
|
total = result.get("total", 0)
|
||||||
|
if start_at + len(issues) >= total:
|
||||||
|
break
|
||||||
|
|
||||||
|
start_at += len(issues)
|
||||||
|
|
||||||
|
return all_issues
|
||||||
|
|
||||||
|
def get_issues_by_date_range(
|
||||||
|
self,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
include_comments: bool = True,
|
||||||
|
project_key: Optional[str] = None,
|
||||||
|
) -> tuple[List[Dict[str, Any]], Optional[str]]:
|
||||||
|
"""
|
||||||
|
Fetch issues within a date range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start_date: Start date in YYYY-MM-DD format
|
||||||
|
end_date: End date in YYYY-MM-DD format (inclusive)
|
||||||
|
include_comments: Whether to include comments in the response
|
||||||
|
project_key: Optional project key to filter issues
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing (issues list, error message or None)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Build JQL query for date range
|
||||||
|
# Query issues that were either created OR updated within the date range
|
||||||
|
date_filter = (
|
||||||
|
f"(createdDate >= '{start_date}' AND createdDate <= '{end_date}')"
|
||||||
|
)
|
||||||
|
# TODO : This JQL needs some improvement to work as expected
|
||||||
|
|
||||||
|
jql = f"{date_filter}"
|
||||||
|
if project_key:
|
||||||
|
jql = (
|
||||||
|
f'project = "{project_key}" AND {date_filter} ORDER BY created DESC'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define fields to retrieve
|
||||||
|
fields = [
|
||||||
|
"summary",
|
||||||
|
"description",
|
||||||
|
"status",
|
||||||
|
"assignee",
|
||||||
|
"reporter",
|
||||||
|
"created",
|
||||||
|
"updated",
|
||||||
|
"priority",
|
||||||
|
"issuetype",
|
||||||
|
"project",
|
||||||
|
]
|
||||||
|
|
||||||
|
if include_comments:
|
||||||
|
fields.append("comment")
|
||||||
|
|
||||||
|
params = {
|
||||||
|
# "jql": "", TODO : Add a JQL query to filter from a date range
|
||||||
|
"fields": ",".join(fields),
|
||||||
|
"maxResults": 100,
|
||||||
|
"startAt": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
all_issues = []
|
||||||
|
start_at = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
params["startAt"] = start_at
|
||||||
|
|
||||||
|
result = self.make_api_request("search", params)
|
||||||
|
|
||||||
|
if not isinstance(result, dict) or "issues" not in result:
|
||||||
|
return [], "Invalid response from Jira API"
|
||||||
|
|
||||||
|
issues = result["issues"]
|
||||||
|
all_issues.extend(issues)
|
||||||
|
|
||||||
|
# Check if there are more issues to fetch
|
||||||
|
total = result.get("total", 0)
|
||||||
|
if start_at + len(issues) >= total:
|
||||||
|
break
|
||||||
|
|
||||||
|
start_at += len(issues)
|
||||||
|
|
||||||
|
if not all_issues:
|
||||||
|
return [], "No issues found in the specified date range."
|
||||||
|
|
||||||
|
return all_issues, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return [], f"Error fetching issues: {str(e)}"
|
||||||
|
|
||||||
|
def format_issue(self, issue: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Format an issue for easier consumption.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
issue: The issue object from Jira API
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted issue dictionary
|
||||||
|
"""
|
||||||
|
fields = issue.get("fields", {})
|
||||||
|
|
||||||
|
# Extract basic issue details
|
||||||
|
formatted = {
|
||||||
|
"id": issue.get("id", ""),
|
||||||
|
"key": issue.get("key", ""),
|
||||||
|
"title": fields.get("summary", ""),
|
||||||
|
"description": fields.get("description", ""),
|
||||||
|
"status": (
|
||||||
|
fields.get("status", {}).get("name", "Unknown")
|
||||||
|
if fields.get("status")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"status_category": (
|
||||||
|
fields.get("status", {})
|
||||||
|
.get("statusCategory", {})
|
||||||
|
.get("name", "Unknown")
|
||||||
|
if fields.get("status")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"priority": (
|
||||||
|
fields.get("priority", {}).get("name", "Unknown")
|
||||||
|
if fields.get("priority")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"issue_type": (
|
||||||
|
fields.get("issuetype", {}).get("name", "Unknown")
|
||||||
|
if fields.get("issuetype")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"project": (
|
||||||
|
fields.get("project", {}).get("key", "Unknown")
|
||||||
|
if fields.get("project")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"created_at": fields.get("created", ""),
|
||||||
|
"updated_at": fields.get("updated", ""),
|
||||||
|
"reporter": (
|
||||||
|
{
|
||||||
|
"account_id": (
|
||||||
|
fields.get("reporter", {}).get("accountId", "")
|
||||||
|
if fields.get("reporter")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"display_name": (
|
||||||
|
fields.get("reporter", {}).get("displayName", "Unknown")
|
||||||
|
if fields.get("reporter")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"email": (
|
||||||
|
fields.get("reporter", {}).get("emailAddress", "")
|
||||||
|
if fields.get("reporter")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if fields.get("reporter")
|
||||||
|
else {"account_id": "", "display_name": "Unknown", "email": ""}
|
||||||
|
),
|
||||||
|
"assignee": (
|
||||||
|
{
|
||||||
|
"account_id": fields.get("assignee", {}).get("accountId", ""),
|
||||||
|
"display_name": fields.get("assignee", {}).get(
|
||||||
|
"displayName", "Unknown"
|
||||||
|
),
|
||||||
|
"email": fields.get("assignee", {}).get("emailAddress", ""),
|
||||||
|
}
|
||||||
|
if fields.get("assignee")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"comments": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract comments if available
|
||||||
|
if "comment" in fields and "comments" in fields["comment"]:
|
||||||
|
for comment in fields["comment"]["comments"]:
|
||||||
|
formatted_comment = {
|
||||||
|
"id": comment.get("id", ""),
|
||||||
|
"body": comment.get("body", ""),
|
||||||
|
"created_at": comment.get("created", ""),
|
||||||
|
"updated_at": comment.get("updated", ""),
|
||||||
|
"author": (
|
||||||
|
{
|
||||||
|
"account_id": (
|
||||||
|
comment.get("author", {}).get("accountId", "")
|
||||||
|
if comment.get("author")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"display_name": (
|
||||||
|
comment.get("author", {}).get("displayName", "Unknown")
|
||||||
|
if comment.get("author")
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
"email": (
|
||||||
|
comment.get("author", {}).get("emailAddress", "")
|
||||||
|
if comment.get("author")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if comment.get("author")
|
||||||
|
else {"account_id": "", "display_name": "Unknown", "email": ""}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
formatted["comments"].append(formatted_comment)
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
def format_issue_to_markdown(self, issue: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Convert an issue to markdown format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
issue: The issue object (either raw or formatted)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown string representation of the issue
|
||||||
|
"""
|
||||||
|
# Format the issue if it's not already formatted
|
||||||
|
if "key" not in issue:
|
||||||
|
issue = self.format_issue(issue)
|
||||||
|
|
||||||
|
# Build the markdown content
|
||||||
|
markdown = (
|
||||||
|
f"# {issue.get('key', 'No Key')}: {issue.get('title', 'No Title')}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if issue.get("status"):
|
||||||
|
markdown += f"**Status:** {issue['status']}\n"
|
||||||
|
|
||||||
|
if issue.get("priority"):
|
||||||
|
markdown += f"**Priority:** {issue['priority']}\n"
|
||||||
|
|
||||||
|
if issue.get("issue_type"):
|
||||||
|
markdown += f"**Type:** {issue['issue_type']}\n"
|
||||||
|
|
||||||
|
if issue.get("project"):
|
||||||
|
markdown += f"**Project:** {issue['project']}\n\n"
|
||||||
|
|
||||||
|
if issue.get("assignee") and issue["assignee"].get("display_name"):
|
||||||
|
markdown += f"**Assignee:** {issue['assignee']['display_name']}\n"
|
||||||
|
|
||||||
|
if issue.get("reporter") and issue["reporter"].get("display_name"):
|
||||||
|
markdown += f"**Reporter:** {issue['reporter']['display_name']}\n"
|
||||||
|
|
||||||
|
if issue.get("created_at"):
|
||||||
|
created_date = self.format_date(issue["created_at"])
|
||||||
|
markdown += f"**Created:** {created_date}\n"
|
||||||
|
|
||||||
|
if issue.get("updated_at"):
|
||||||
|
updated_date = self.format_date(issue["updated_at"])
|
||||||
|
markdown += f"**Updated:** {updated_date}\n\n"
|
||||||
|
|
||||||
|
if issue.get("description"):
|
||||||
|
markdown += f"## Description\n\n{issue['description']}\n\n"
|
||||||
|
|
||||||
|
if issue.get("comments"):
|
||||||
|
markdown += f"## Comments ({len(issue['comments'])})\n\n"
|
||||||
|
|
||||||
|
for comment in issue["comments"]:
|
||||||
|
author_name = "Unknown"
|
||||||
|
if comment.get("author") and comment["author"].get("display_name"):
|
||||||
|
author_name = comment["author"]["display_name"]
|
||||||
|
|
||||||
|
comment_date = "Unknown date"
|
||||||
|
if comment.get("created_at"):
|
||||||
|
comment_date = self.format_date(comment["created_at"])
|
||||||
|
|
||||||
|
markdown += f"### {author_name} ({comment_date})\n\n{comment.get('body', '')}\n\n---\n\n"
|
||||||
|
|
||||||
|
return markdown
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_date(iso_date: str) -> str:
|
||||||
|
"""
|
||||||
|
Format an ISO date string to a more readable format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
iso_date: ISO format date string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted date string
|
||||||
|
"""
|
||||||
|
if not iso_date or not isinstance(iso_date, str):
|
||||||
|
return "Unknown date"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Jira dates are typically in format: 2023-01-01T12:00:00.000+0000
|
||||||
|
dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
|
||||||
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except ValueError:
|
||||||
|
return iso_date
|
||||||
|
|
@ -2,30 +2,30 @@ from collections.abc import AsyncGenerator
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from app.config import config
|
||||||
|
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
||||||
|
from app.retriver.documents_hybrid_search import DocumentHybridSearchRetriever
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
from pgvector.sqlalchemy import Vector
|
from pgvector.sqlalchemy import Vector
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
ARRAY,
|
ARRAY,
|
||||||
|
JSON,
|
||||||
|
TIMESTAMP,
|
||||||
Boolean,
|
Boolean,
|
||||||
Column,
|
Column,
|
||||||
Enum as SQLAlchemyEnum,
|
)
|
||||||
|
from sqlalchemy import Enum as SQLAlchemyEnum
|
||||||
|
from sqlalchemy import (
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
Integer,
|
Integer,
|
||||||
JSON,
|
|
||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
text,
|
text,
|
||||||
TIMESTAMP
|
|
||||||
)
|
)
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, relationship
|
||||||
|
|
||||||
from app.config import config
|
|
||||||
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
|
||||||
from app.retriver.documents_hybrid_search import DocumentHybridSearchRetriever
|
|
||||||
|
|
||||||
if config.AUTH_TYPE == "GOOGLE":
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
from fastapi_users.db import (
|
from fastapi_users.db import (
|
||||||
SQLAlchemyBaseOAuthAccountTableUUID,
|
SQLAlchemyBaseOAuthAccountTableUUID,
|
||||||
|
|
@ -51,9 +51,11 @@ class DocumentType(str, Enum):
|
||||||
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
||||||
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
||||||
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
|
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
|
||||||
|
JIRA_CONNECTOR = "JIRA_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
|
||||||
TAVILY_API = "TAVILY_API"
|
TAVILY_API = "TAVILY_API"
|
||||||
LINKUP_API = "LINKUP_API"
|
LINKUP_API = "LINKUP_API"
|
||||||
SLACK_CONNECTOR = "SLACK_CONNECTOR"
|
SLACK_CONNECTOR = "SLACK_CONNECTOR"
|
||||||
|
|
@ -61,6 +63,8 @@ class SearchSourceConnectorType(str, Enum):
|
||||||
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
GITHUB_CONNECTOR = "GITHUB_CONNECTOR"
|
||||||
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
|
||||||
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
|
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
|
||||||
|
JIRA_CONNECTOR = "JIRA_CONNECTOR"
|
||||||
|
|
||||||
|
|
||||||
class ChatType(str, Enum):
|
class ChatType(str, Enum):
|
||||||
QNA = "QNA"
|
QNA = "QNA"
|
||||||
|
|
@ -68,6 +72,7 @@ class ChatType(str, Enum):
|
||||||
REPORT_DEEP = "REPORT_DEEP"
|
REPORT_DEEP = "REPORT_DEEP"
|
||||||
REPORT_DEEPER = "REPORT_DEEPER"
|
REPORT_DEEPER = "REPORT_DEEPER"
|
||||||
|
|
||||||
|
|
||||||
class LiteLLMProvider(str, Enum):
|
class LiteLLMProvider(str, Enum):
|
||||||
OPENAI = "OPENAI"
|
OPENAI = "OPENAI"
|
||||||
ANTHROPIC = "ANTHROPIC"
|
ANTHROPIC = "ANTHROPIC"
|
||||||
|
|
@ -92,6 +97,7 @@ class LiteLLMProvider(str, Enum):
|
||||||
PETALS = "PETALS"
|
PETALS = "PETALS"
|
||||||
CUSTOM = "CUSTOM"
|
CUSTOM = "CUSTOM"
|
||||||
|
|
||||||
|
|
||||||
class LogLevel(str, Enum):
|
class LogLevel(str, Enum):
|
||||||
DEBUG = "DEBUG"
|
DEBUG = "DEBUG"
|
||||||
INFO = "INFO"
|
INFO = "INFO"
|
||||||
|
|
@ -99,18 +105,27 @@ class LogLevel(str, Enum):
|
||||||
ERROR = "ERROR"
|
ERROR = "ERROR"
|
||||||
CRITICAL = "CRITICAL"
|
CRITICAL = "CRITICAL"
|
||||||
|
|
||||||
|
|
||||||
class LogStatus(str, Enum):
|
class LogStatus(str, Enum):
|
||||||
IN_PROGRESS = "IN_PROGRESS"
|
IN_PROGRESS = "IN_PROGRESS"
|
||||||
SUCCESS = "SUCCESS"
|
SUCCESS = "SUCCESS"
|
||||||
FAILED = "FAILED"
|
FAILED = "FAILED"
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TimestampMixin:
|
class TimestampMixin:
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def created_at(cls):
|
def created_at(cls):
|
||||||
return Column(TIMESTAMP(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
|
return Column(
|
||||||
|
TIMESTAMP(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
default=lambda: datetime.now(timezone.utc),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(Base):
|
class BaseModel(Base):
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|
@ -118,6 +133,7 @@ class BaseModel(Base):
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
|
||||||
class Chat(BaseModel, TimestampMixin):
|
class Chat(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "chats"
|
__tablename__ = "chats"
|
||||||
|
|
||||||
|
|
@ -126,8 +142,11 @@ class Chat(BaseModel, TimestampMixin):
|
||||||
initial_connectors = Column(ARRAY(String), nullable=True)
|
initial_connectors = Column(ARRAY(String), nullable=True)
|
||||||
messages = Column(JSON, nullable=False)
|
messages = Column(JSON, nullable=False)
|
||||||
|
|
||||||
search_space_id = Column(Integer, ForeignKey('searchspaces.id', ondelete='CASCADE'), nullable=False)
|
search_space_id = Column(
|
||||||
search_space = relationship('SearchSpace', back_populates='chats')
|
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
search_space = relationship("SearchSpace", back_populates="chats")
|
||||||
|
|
||||||
|
|
||||||
class Document(BaseModel, TimestampMixin):
|
class Document(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "documents"
|
__tablename__ = "documents"
|
||||||
|
|
@ -140,9 +159,14 @@ class Document(BaseModel, TimestampMixin):
|
||||||
content_hash = Column(String, nullable=False, index=True, unique=True)
|
content_hash = Column(String, nullable=False, index=True, unique=True)
|
||||||
embedding = Column(Vector(config.embedding_model_instance.dimension))
|
embedding = Column(Vector(config.embedding_model_instance.dimension))
|
||||||
|
|
||||||
search_space_id = Column(Integer, ForeignKey("searchspaces.id", ondelete='CASCADE'), nullable=False)
|
search_space_id = Column(
|
||||||
|
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
search_space = relationship("SearchSpace", back_populates="documents")
|
search_space = relationship("SearchSpace", back_populates="documents")
|
||||||
chunks = relationship("Chunk", back_populates="document", cascade="all, delete-orphan")
|
chunks = relationship(
|
||||||
|
"Chunk", back_populates="document", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Chunk(BaseModel, TimestampMixin):
|
class Chunk(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "chunks"
|
__tablename__ = "chunks"
|
||||||
|
|
@ -150,9 +174,12 @@ class Chunk(BaseModel, TimestampMixin):
|
||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
embedding = Column(Vector(config.embedding_model_instance.dimension))
|
embedding = Column(Vector(config.embedding_model_instance.dimension))
|
||||||
|
|
||||||
document_id = Column(Integer, ForeignKey("documents.id", ondelete='CASCADE'), nullable=False)
|
document_id = Column(
|
||||||
|
Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
document = relationship("Document", back_populates="chunks")
|
document = relationship("Document", back_populates="chunks")
|
||||||
|
|
||||||
|
|
||||||
class Podcast(BaseModel, TimestampMixin):
|
class Podcast(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "podcasts"
|
__tablename__ = "podcasts"
|
||||||
|
|
||||||
|
|
@ -160,35 +187,66 @@ class Podcast(BaseModel, TimestampMixin):
|
||||||
podcast_transcript = Column(JSON, nullable=False, default={})
|
podcast_transcript = Column(JSON, nullable=False, default={})
|
||||||
file_location = Column(String(500), nullable=False, default="")
|
file_location = Column(String(500), nullable=False, default="")
|
||||||
|
|
||||||
search_space_id = Column(Integer, ForeignKey("searchspaces.id", ondelete='CASCADE'), nullable=False)
|
search_space_id = Column(
|
||||||
|
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
search_space = relationship("SearchSpace", back_populates="podcasts")
|
search_space = relationship("SearchSpace", back_populates="podcasts")
|
||||||
|
|
||||||
|
|
||||||
class SearchSpace(BaseModel, TimestampMixin):
|
class SearchSpace(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "searchspaces"
|
__tablename__ = "searchspaces"
|
||||||
|
|
||||||
name = Column(String(100), nullable=False, index=True)
|
name = Column(String(100), nullable=False, index=True)
|
||||||
description = Column(String(500), nullable=True)
|
description = Column(String(500), nullable=True)
|
||||||
|
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete='CASCADE'), nullable=False)
|
user_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
user = relationship("User", back_populates="search_spaces")
|
user = relationship("User", back_populates="search_spaces")
|
||||||
|
|
||||||
documents = relationship("Document", back_populates="search_space", order_by="Document.id", cascade="all, delete-orphan")
|
documents = relationship(
|
||||||
podcasts = relationship("Podcast", back_populates="search_space", order_by="Podcast.id", cascade="all, delete-orphan")
|
"Document",
|
||||||
chats = relationship('Chat', back_populates='search_space', order_by='Chat.id', cascade="all, delete-orphan")
|
back_populates="search_space",
|
||||||
logs = relationship("Log", back_populates="search_space", order_by="Log.id", cascade="all, delete-orphan")
|
order_by="Document.id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
podcasts = relationship(
|
||||||
|
"Podcast",
|
||||||
|
back_populates="search_space",
|
||||||
|
order_by="Podcast.id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
chats = relationship(
|
||||||
|
"Chat",
|
||||||
|
back_populates="search_space",
|
||||||
|
order_by="Chat.id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
logs = relationship(
|
||||||
|
"Log",
|
||||||
|
back_populates="search_space",
|
||||||
|
order_by="Log.id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SearchSourceConnector(BaseModel, TimestampMixin):
|
class SearchSourceConnector(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "search_source_connectors"
|
__tablename__ = "search_source_connectors"
|
||||||
|
|
||||||
name = Column(String(100), nullable=False, index=True)
|
name = Column(String(100), nullable=False, index=True)
|
||||||
connector_type = Column(SQLAlchemyEnum(SearchSourceConnectorType), nullable=False, unique=True)
|
connector_type = Column(
|
||||||
|
SQLAlchemyEnum(SearchSourceConnectorType), nullable=False, unique=True
|
||||||
|
)
|
||||||
is_indexable = Column(Boolean, nullable=False, default=False)
|
is_indexable = Column(Boolean, nullable=False, default=False)
|
||||||
last_indexed_at = Column(TIMESTAMP(timezone=True), nullable=True)
|
last_indexed_at = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||||
config = Column(JSON, nullable=False)
|
config = Column(JSON, nullable=False)
|
||||||
|
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete='CASCADE'), nullable=False)
|
user_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
user = relationship("User", back_populates="search_source_connectors")
|
user = relationship("User", back_populates="search_source_connectors")
|
||||||
|
|
||||||
|
|
||||||
class LLMConfig(BaseModel, TimestampMixin):
|
class LLMConfig(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "llm_configs"
|
__tablename__ = "llm_configs"
|
||||||
|
|
||||||
|
|
@ -206,55 +264,102 @@ class LLMConfig(BaseModel, TimestampMixin):
|
||||||
# For any other parameters that litellm supports
|
# For any other parameters that litellm supports
|
||||||
litellm_params = Column(JSON, nullable=True, default={})
|
litellm_params = Column(JSON, nullable=True, default={})
|
||||||
|
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete='CASCADE'), nullable=False)
|
user_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
user = relationship("User", back_populates="llm_configs", foreign_keys=[user_id])
|
user = relationship("User", back_populates="llm_configs", foreign_keys=[user_id])
|
||||||
|
|
||||||
|
|
||||||
class Log(BaseModel, TimestampMixin):
|
class Log(BaseModel, TimestampMixin):
|
||||||
__tablename__ = "logs"
|
__tablename__ = "logs"
|
||||||
|
|
||||||
level = Column(SQLAlchemyEnum(LogLevel), nullable=False, index=True)
|
level = Column(SQLAlchemyEnum(LogLevel), nullable=False, index=True)
|
||||||
status = Column(SQLAlchemyEnum(LogStatus), nullable=False, index=True)
|
status = Column(SQLAlchemyEnum(LogStatus), nullable=False, index=True)
|
||||||
message = Column(Text, nullable=False)
|
message = Column(Text, nullable=False)
|
||||||
source = Column(String(200), nullable=True, index=True) # Service/component that generated the log
|
source = Column(
|
||||||
|
String(200), nullable=True, index=True
|
||||||
|
) # Service/component that generated the log
|
||||||
log_metadata = Column(JSON, nullable=True, default={}) # Additional context data
|
log_metadata = Column(JSON, nullable=True, default={}) # Additional context data
|
||||||
|
|
||||||
search_space_id = Column(Integer, ForeignKey("searchspaces.id", ondelete='CASCADE'), nullable=False)
|
search_space_id = Column(
|
||||||
|
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
search_space = relationship("SearchSpace", back_populates="logs")
|
search_space = relationship("SearchSpace", back_populates="logs")
|
||||||
|
|
||||||
|
|
||||||
if config.AUTH_TYPE == "GOOGLE":
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
|
|
||||||
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
|
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class User(SQLAlchemyBaseUserTableUUID, Base):
|
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||||
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
|
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
|
||||||
"OAuthAccount", lazy="joined"
|
"OAuthAccount", lazy="joined"
|
||||||
)
|
)
|
||||||
search_spaces = relationship("SearchSpace", back_populates="user")
|
search_spaces = relationship("SearchSpace", back_populates="user")
|
||||||
search_source_connectors = relationship("SearchSourceConnector", back_populates="user")
|
search_source_connectors = relationship(
|
||||||
llm_configs = relationship("LLMConfig", back_populates="user", foreign_keys="LLMConfig.user_id", cascade="all, delete-orphan")
|
"SearchSourceConnector", back_populates="user"
|
||||||
|
)
|
||||||
|
llm_configs = relationship(
|
||||||
|
"LLMConfig",
|
||||||
|
back_populates="user",
|
||||||
|
foreign_keys="LLMConfig.user_id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
long_context_llm_id = Column(Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True)
|
long_context_llm_id = Column(
|
||||||
fast_llm_id = Column(Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True)
|
Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True
|
||||||
strategic_llm_id = Column(Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True)
|
)
|
||||||
|
fast_llm_id = Column(
|
||||||
|
Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True
|
||||||
|
)
|
||||||
|
strategic_llm_id = Column(
|
||||||
|
Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
long_context_llm = relationship(
|
||||||
|
"LLMConfig", foreign_keys=[long_context_llm_id], post_update=True
|
||||||
|
)
|
||||||
|
fast_llm = relationship(
|
||||||
|
"LLMConfig", foreign_keys=[fast_llm_id], post_update=True
|
||||||
|
)
|
||||||
|
strategic_llm = relationship(
|
||||||
|
"LLMConfig", foreign_keys=[strategic_llm_id], post_update=True
|
||||||
|
)
|
||||||
|
|
||||||
long_context_llm = relationship("LLMConfig", foreign_keys=[long_context_llm_id], post_update=True)
|
|
||||||
fast_llm = relationship("LLMConfig", foreign_keys=[fast_llm_id], post_update=True)
|
|
||||||
strategic_llm = relationship("LLMConfig", foreign_keys=[strategic_llm_id], post_update=True)
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
class User(SQLAlchemyBaseUserTableUUID, Base):
|
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||||
|
|
||||||
search_spaces = relationship("SearchSpace", back_populates="user")
|
search_spaces = relationship("SearchSpace", back_populates="user")
|
||||||
search_source_connectors = relationship("SearchSourceConnector", back_populates="user")
|
search_source_connectors = relationship(
|
||||||
llm_configs = relationship("LLMConfig", back_populates="user", foreign_keys="LLMConfig.user_id", cascade="all, delete-orphan")
|
"SearchSourceConnector", back_populates="user"
|
||||||
|
)
|
||||||
|
llm_configs = relationship(
|
||||||
|
"LLMConfig",
|
||||||
|
back_populates="user",
|
||||||
|
foreign_keys="LLMConfig.user_id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
long_context_llm_id = Column(Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True)
|
long_context_llm_id = Column(
|
||||||
fast_llm_id = Column(Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True)
|
Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True
|
||||||
strategic_llm_id = Column(Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True)
|
)
|
||||||
|
fast_llm_id = Column(
|
||||||
|
Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True
|
||||||
|
)
|
||||||
|
strategic_llm_id = Column(
|
||||||
|
Integer, ForeignKey("llm_configs.id", ondelete="SET NULL"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
long_context_llm = relationship("LLMConfig", foreign_keys=[long_context_llm_id], post_update=True)
|
long_context_llm = relationship(
|
||||||
fast_llm = relationship("LLMConfig", foreign_keys=[fast_llm_id], post_update=True)
|
"LLMConfig", foreign_keys=[long_context_llm_id], post_update=True
|
||||||
strategic_llm = relationship("LLMConfig", foreign_keys=[strategic_llm_id], post_update=True)
|
)
|
||||||
|
fast_llm = relationship(
|
||||||
|
"LLMConfig", foreign_keys=[fast_llm_id], post_update=True
|
||||||
|
)
|
||||||
|
strategic_llm = relationship(
|
||||||
|
"LLMConfig", foreign_keys=[strategic_llm_id], post_update=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
engine = create_async_engine(DATABASE_URL)
|
engine = create_async_engine(DATABASE_URL)
|
||||||
|
|
@ -265,15 +370,32 @@ async def setup_indexes():
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
# Create indexes
|
# Create indexes
|
||||||
# Document Summary Indexes
|
# Document Summary Indexes
|
||||||
await conn.execute(text('CREATE INDEX IF NOT EXISTS document_vector_index ON documents USING hnsw (embedding public.vector_cosine_ops)'))
|
await conn.execute(
|
||||||
await conn.execute(text('CREATE INDEX IF NOT EXISTS document_search_index ON documents USING gin (to_tsvector(\'english\', content))'))
|
text(
|
||||||
|
"CREATE INDEX IF NOT EXISTS document_vector_index ON documents USING hnsw (embedding public.vector_cosine_ops)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"CREATE INDEX IF NOT EXISTS document_search_index ON documents USING gin (to_tsvector('english', content))"
|
||||||
|
)
|
||||||
|
)
|
||||||
# Document Chuck Indexes
|
# Document Chuck Indexes
|
||||||
await conn.execute(text('CREATE INDEX IF NOT EXISTS chucks_vector_index ON chunks USING hnsw (embedding public.vector_cosine_ops)'))
|
await conn.execute(
|
||||||
await conn.execute(text('CREATE INDEX IF NOT EXISTS chucks_search_index ON chunks USING gin (to_tsvector(\'english\', content))'))
|
text(
|
||||||
|
"CREATE INDEX IF NOT EXISTS chucks_vector_index ON chunks USING hnsw (embedding public.vector_cosine_ops)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"CREATE INDEX IF NOT EXISTS chucks_search_index ON chunks USING gin (to_tsvector('english', content))"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_db_and_tables():
|
async def create_db_and_tables():
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.execute(text('CREATE EXTENSION IF NOT EXISTS vector'))
|
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
await setup_indexes()
|
await setup_indexes()
|
||||||
|
|
||||||
|
|
@ -284,14 +406,23 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
|
||||||
|
|
||||||
if config.AUTH_TYPE == "GOOGLE":
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
|
|
||||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||||
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
|
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||||
yield SQLAlchemyUserDatabase(session, User)
|
yield SQLAlchemyUserDatabase(session, User)
|
||||||
|
|
||||||
async def get_chucks_hybrid_search_retriever(session: AsyncSession = Depends(get_async_session)):
|
|
||||||
|
async def get_chucks_hybrid_search_retriever(
|
||||||
|
session: AsyncSession = Depends(get_async_session),
|
||||||
|
):
|
||||||
return ChucksHybridSearchRetriever(session)
|
return ChucksHybridSearchRetriever(session)
|
||||||
|
|
||||||
async def get_documents_hybrid_search_retriever(session: AsyncSession = Depends(get_async_session)):
|
|
||||||
|
async def get_documents_hybrid_search_retriever(
|
||||||
|
session: AsyncSession = Depends(get_async_session),
|
||||||
|
):
|
||||||
return DocumentHybridSearchRetriever(session)
|
return DocumentHybridSearchRetriever(session)
|
||||||
|
|
|
||||||
|
|
@ -9,35 +9,58 @@ POST /search-source-connectors/{connector_id}/index - Index content from a conne
|
||||||
|
|
||||||
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).
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
import logging
|
||||||
from sqlalchemy.future import select
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy.exc import IntegrityError
|
from typing import Any, Dict, List
|
||||||
from typing import List, Dict, Any
|
|
||||||
from app.db import get_async_session, User, SearchSourceConnector, SearchSourceConnectorType, SearchSpace, async_session_maker
|
from app.connectors.github_connector import GitHubConnector
|
||||||
from app.schemas import SearchSourceConnectorCreate, SearchSourceConnectorUpdate, SearchSourceConnectorRead, SearchSourceConnectorBase
|
from app.db import (
|
||||||
|
SearchSourceConnector,
|
||||||
|
SearchSourceConnectorType,
|
||||||
|
SearchSpace,
|
||||||
|
User,
|
||||||
|
async_session_maker,
|
||||||
|
get_async_session,
|
||||||
|
)
|
||||||
|
from app.schemas import (
|
||||||
|
SearchSourceConnectorBase,
|
||||||
|
SearchSourceConnectorCreate,
|
||||||
|
SearchSourceConnectorRead,
|
||||||
|
SearchSourceConnectorUpdate,
|
||||||
|
)
|
||||||
|
from app.tasks.connectors_indexing_tasks import (
|
||||||
|
index_discord_messages,
|
||||||
|
index_github_repos,
|
||||||
|
index_jira_issues,
|
||||||
|
index_linear_issues,
|
||||||
|
index_notion_pages,
|
||||||
|
index_slack_messages,
|
||||||
|
)
|
||||||
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 fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
|
||||||
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, index_discord_messages
|
from sqlalchemy.exc import IntegrityError
|
||||||
from app.connectors.github_connector import GitHubConnector
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from datetime import datetime, timedelta
|
from sqlalchemy.future import select
|
||||||
import logging
|
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
# Use Pydantic's BaseModel here
|
# Use Pydantic's BaseModel here
|
||||||
class GitHubPATRequest(BaseModel):
|
class GitHubPATRequest(BaseModel):
|
||||||
github_pat: str = Field(..., description="GitHub Personal Access Token")
|
github_pat: str = Field(..., description="GitHub Personal Access Token")
|
||||||
|
|
||||||
|
|
||||||
# --- New Endpoint to list GitHub Repositories ---
|
# --- New Endpoint to list GitHub Repositories ---
|
||||||
@router.post("/github/repositories/", response_model=List[Dict[str, Any]])
|
@router.post("/github/repositories/", response_model=List[Dict[str, Any]])
|
||||||
async def list_github_repositories(
|
async def list_github_repositories(
|
||||||
pat_request: GitHubPATRequest,
|
pat_request: GitHubPATRequest,
|
||||||
user: User = Depends(current_active_user) # Ensure the user is logged in
|
user: User = Depends(current_active_user), # Ensure the user is logged in
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fetches a list of repositories accessible by the provided GitHub PAT.
|
Fetches a list of repositories accessible by the provided GitHub PAT.
|
||||||
|
|
@ -54,14 +77,19 @@ async def list_github_repositories(
|
||||||
logger.error(f"GitHub PAT validation failed for user {user.id}: {str(e)}")
|
logger.error(f"GitHub PAT validation failed for user {user.id}: {str(e)}")
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid GitHub PAT: {str(e)}")
|
raise HTTPException(status_code=400, detail=f"Invalid GitHub PAT: {str(e)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to fetch GitHub repositories for user {user.id}: {str(e)}")
|
logger.error(
|
||||||
raise HTTPException(status_code=500, detail="Failed to fetch GitHub repositories.")
|
f"Failed to fetch GitHub repositories for user {user.id}: {str(e)}"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail="Failed to fetch GitHub repositories."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/search-source-connectors/", response_model=SearchSourceConnectorRead)
|
@router.post("/search-source-connectors/", response_model=SearchSourceConnectorRead)
|
||||||
async def create_search_source_connector(
|
async def create_search_source_connector(
|
||||||
connector: SearchSourceConnectorCreate,
|
connector: SearchSourceConnectorCreate,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user)
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new search source connector.
|
Create a new search source connector.
|
||||||
|
|
@ -72,17 +100,16 @@ async def create_search_source_connector(
|
||||||
try:
|
try:
|
||||||
# Check if a connector with the same type already exists for this user
|
# Check if a connector with the same type already exists for this user
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(SearchSourceConnector)
|
select(SearchSourceConnector).filter(
|
||||||
.filter(
|
|
||||||
SearchSourceConnector.user_id == user.id,
|
SearchSourceConnector.user_id == user.id,
|
||||||
SearchSourceConnector.connector_type == connector.connector_type
|
SearchSourceConnector.connector_type == connector.connector_type,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
existing_connector = result.scalars().first()
|
existing_connector = result.scalars().first()
|
||||||
if existing_connector:
|
if existing_connector:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
detail=f"A connector with type {connector.connector_type} already exists. Each user can have only one connector of each type."
|
detail=f"A connector with type {connector.connector_type} already exists. Each user can have only one connector of each type.",
|
||||||
)
|
)
|
||||||
db_connector = SearchSourceConnector(**connector.model_dump(), user_id=user.id)
|
db_connector = SearchSourceConnector(**connector.model_dump(), user_id=user.id)
|
||||||
session.add(db_connector)
|
session.add(db_connector)
|
||||||
|
|
@ -91,15 +118,12 @@ async def create_search_source_connector(
|
||||||
return db_connector
|
return db_connector
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=422, detail=f"Validation error: {str(e)}")
|
||||||
status_code=422,
|
|
||||||
detail=f"Validation error: {str(e)}"
|
|
||||||
)
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
detail=f"Integrity error: A connector with this type already exists. {str(e)}"
|
detail=f"Integrity error: A connector with this type already exists. {str(e)}",
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
|
|
@ -109,38 +133,44 @@ async def create_search_source_connector(
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Failed to create search source connector: {str(e)}"
|
detail=f"Failed to create search source connector: {str(e)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/search-source-connectors/", response_model=List[SearchSourceConnectorRead])
|
|
||||||
|
@router.get(
|
||||||
|
"/search-source-connectors/", response_model=List[SearchSourceConnectorRead]
|
||||||
|
)
|
||||||
async def read_search_source_connectors(
|
async def read_search_source_connectors(
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
search_space_id: int = None,
|
search_space_id: int = None,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user)
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""List all search source connectors for the current user."""
|
"""List all search source connectors for the current user."""
|
||||||
try:
|
try:
|
||||||
query = select(SearchSourceConnector).filter(SearchSourceConnector.user_id == user.id)
|
query = select(SearchSourceConnector).filter(
|
||||||
|
SearchSourceConnector.user_id == user.id
|
||||||
|
)
|
||||||
|
|
||||||
# No need to filter by search_space_id as connectors are user-owned, not search space specific
|
# No need to filter by search_space_id as connectors are user-owned, not search space specific
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(query.offset(skip).limit(limit))
|
||||||
query.offset(skip).limit(limit)
|
|
||||||
)
|
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Failed to fetch search source connectors: {str(e)}"
|
detail=f"Failed to fetch search source connectors: {str(e)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/search-source-connectors/{connector_id}", response_model=SearchSourceConnectorRead)
|
|
||||||
|
@router.get(
|
||||||
|
"/search-source-connectors/{connector_id}", response_model=SearchSourceConnectorRead
|
||||||
|
)
|
||||||
async def read_search_source_connector(
|
async def read_search_source_connector(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user)
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""Get a specific search source connector by ID."""
|
"""Get a specific search source connector by ID."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -149,30 +179,36 @@ async def read_search_source_connector(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500, detail=f"Failed to fetch search source connector: {str(e)}"
|
||||||
detail=f"Failed to fetch search source connector: {str(e)}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.put("/search-source-connectors/{connector_id}", response_model=SearchSourceConnectorRead)
|
|
||||||
|
@router.put(
|
||||||
|
"/search-source-connectors/{connector_id}", response_model=SearchSourceConnectorRead
|
||||||
|
)
|
||||||
async def update_search_source_connector(
|
async def update_search_source_connector(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
connector_update: SearchSourceConnectorUpdate,
|
connector_update: SearchSourceConnectorUpdate,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user)
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update a search source connector.
|
Update a search source connector.
|
||||||
Handles partial updates, including merging changes into the 'config' field.
|
Handles partial updates, including merging changes into the 'config' field.
|
||||||
"""
|
"""
|
||||||
db_connector = await check_ownership(session, SearchSourceConnector, connector_id, user)
|
db_connector = await check_ownership(
|
||||||
|
session, SearchSourceConnector, connector_id, user
|
||||||
|
)
|
||||||
|
|
||||||
# Convert the sparse update data (only fields present in request) to a dict
|
# Convert the sparse update data (only fields present in request) to a dict
|
||||||
update_data = connector_update.model_dump(exclude_unset=True)
|
update_data = connector_update.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
# Special handling for 'config' field
|
# Special handling for 'config' field
|
||||||
if "config" in update_data:
|
if "config" in update_data:
|
||||||
incoming_config = update_data["config"] # Config data from the request
|
incoming_config = update_data["config"] # Config data from the request
|
||||||
existing_config = db_connector.config if db_connector.config else {} # Current config from DB
|
existing_config = (
|
||||||
|
db_connector.config if db_connector.config else {}
|
||||||
|
) # Current config from DB
|
||||||
|
|
||||||
# Merge incoming config into existing config
|
# Merge incoming config into existing config
|
||||||
# This preserves existing keys (like GITHUB_PAT) if they are not in the incoming data
|
# This preserves existing keys (like GITHUB_PAT) if they are not in the incoming data
|
||||||
|
|
@ -182,24 +218,27 @@ async def update_search_source_connector(
|
||||||
# -- Validation after merging --
|
# -- Validation after merging --
|
||||||
# Validate the *merged* config based on the connector type
|
# Validate the *merged* config based on the connector type
|
||||||
# We need the connector type - use the one from the update if provided, else the existing one
|
# We need the connector type - use the one from the update if provided, else the existing one
|
||||||
current_connector_type = connector_update.connector_type if connector_update.connector_type is not None else db_connector.connector_type
|
current_connector_type = (
|
||||||
|
connector_update.connector_type
|
||||||
|
if connector_update.connector_type is not None
|
||||||
|
else db_connector.connector_type
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We can reuse the base validator by creating a temporary base model instance
|
# We can reuse the base validator by creating a temporary base model instance
|
||||||
# Note: This assumes 'name' and 'is_indexable' are not crucial for config validation itself
|
# Note: This assumes 'name' and 'is_indexable' are not crucial for config validation itself
|
||||||
temp_data_for_validation = {
|
temp_data_for_validation = {
|
||||||
"name": db_connector.name, # Use existing name
|
"name": db_connector.name, # Use existing name
|
||||||
"connector_type": current_connector_type,
|
"connector_type": current_connector_type,
|
||||||
"is_indexable": db_connector.is_indexable, # Use existing value
|
"is_indexable": db_connector.is_indexable, # Use existing value
|
||||||
"last_indexed_at": db_connector.last_indexed_at, # Not used by validator
|
"last_indexed_at": db_connector.last_indexed_at, # Not used by validator
|
||||||
"config": merged_config
|
"config": merged_config,
|
||||||
}
|
}
|
||||||
SearchSourceConnectorBase.model_validate(temp_data_for_validation)
|
SearchSourceConnectorBase.model_validate(temp_data_for_validation)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
# Raise specific validation error for the merged config
|
# Raise specific validation error for the merged config
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=422,
|
status_code=422, detail=f"Validation error for merged config: {str(e)}"
|
||||||
detail=f"Validation error for merged config: {str(e)}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# If validation passes, update the main update_data dict with the merged config
|
# If validation passes, update the main update_data dict with the merged config
|
||||||
|
|
@ -210,18 +249,17 @@ async def update_search_source_connector(
|
||||||
# Prevent changing connector_type if it causes a duplicate (check moved here)
|
# Prevent changing connector_type if it causes a duplicate (check moved here)
|
||||||
if key == "connector_type" and value != db_connector.connector_type:
|
if key == "connector_type" and value != db_connector.connector_type:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(SearchSourceConnector)
|
select(SearchSourceConnector).filter(
|
||||||
.filter(
|
|
||||||
SearchSourceConnector.user_id == user.id,
|
SearchSourceConnector.user_id == user.id,
|
||||||
SearchSourceConnector.connector_type == value,
|
SearchSourceConnector.connector_type == value,
|
||||||
SearchSourceConnector.id != connector_id
|
SearchSourceConnector.id != connector_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
existing_connector = result.scalars().first()
|
existing_connector = result.scalars().first()
|
||||||
if existing_connector:
|
if existing_connector:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
detail=f"A connector with type {value} already exists. Each user can have only one connector of each type."
|
detail=f"A connector with type {value} already exists. Each user can have only one connector of each type.",
|
||||||
)
|
)
|
||||||
|
|
||||||
setattr(db_connector, key, value)
|
setattr(db_connector, key, value)
|
||||||
|
|
@ -234,26 +272,31 @@ async def update_search_source_connector(
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
# This might occur if connector_type constraint is violated somehow after the check
|
# This might occur if connector_type constraint is violated somehow after the check
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409, detail=f"Database integrity error during update: {str(e)}"
|
||||||
detail=f"Database integrity error during update: {str(e)}"
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(f"Failed to update search source connector {connector_id}: {e}", exc_info=True)
|
logger.error(
|
||||||
|
f"Failed to update search source connector {connector_id}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Failed to update search source connector: {str(e)}"
|
detail=f"Failed to update search source connector: {str(e)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/search-source-connectors/{connector_id}", response_model=dict)
|
@router.delete("/search-source-connectors/{connector_id}", response_model=dict)
|
||||||
async def delete_search_source_connector(
|
async def delete_search_source_connector(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user)
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""Delete a search source connector."""
|
"""Delete a search source connector."""
|
||||||
try:
|
try:
|
||||||
db_connector = await check_ownership(session, SearchSourceConnector, connector_id, user)
|
db_connector = await check_ownership(
|
||||||
|
session, SearchSourceConnector, connector_id, user
|
||||||
|
)
|
||||||
await session.delete(db_connector)
|
await session.delete(db_connector)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return {"message": "Search source connector deleted successfully"}
|
return {"message": "Search source connector deleted successfully"}
|
||||||
|
|
@ -263,18 +306,29 @@ async def delete_search_source_connector(
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Failed to delete search source connector: {str(e)}"
|
detail=f"Failed to delete search source connector: {str(e)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/search-source-connectors/{connector_id}/index", response_model=Dict[str, Any])
|
|
||||||
|
@router.post(
|
||||||
|
"/search-source-connectors/{connector_id}/index", response_model=Dict[str, Any]
|
||||||
|
)
|
||||||
async def index_connector_content(
|
async def index_connector_content(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int = Query(..., description="ID of the search space to store indexed content"),
|
search_space_id: int = Query(
|
||||||
start_date: str = Query(None, description="Start date for indexing (YYYY-MM-DD format). If not provided, uses last_indexed_at or defaults to 365 days ago"),
|
..., description="ID of the search space to store indexed content"
|
||||||
end_date: str = Query(None, description="End date for indexing (YYYY-MM-DD format). If not provided, uses today's date"),
|
),
|
||||||
|
start_date: str = Query(
|
||||||
|
None,
|
||||||
|
description="Start date for indexing (YYYY-MM-DD format). If not provided, uses last_indexed_at or defaults to 365 days ago",
|
||||||
|
),
|
||||||
|
end_date: str = Query(
|
||||||
|
None,
|
||||||
|
description="End date for indexing (YYYY-MM-DD format). If not provided, uses today's date",
|
||||||
|
),
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
background_tasks: BackgroundTasks = None
|
background_tasks: BackgroundTasks = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Index content from a connector to a search space.
|
Index content from a connector to a search space.
|
||||||
|
|
@ -284,6 +338,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
|
||||||
|
- JIRA_CONNECTOR: Indexes issues and comments from Jira
|
||||||
- DISCORD_CONNECTOR: Indexes messages from all accessible Discord channels
|
- DISCORD_CONNECTOR: Indexes messages from all accessible Discord channels
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -296,10 +351,14 @@ async def index_connector_content(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Check if the connector belongs to the user
|
# Check if the connector belongs to the user
|
||||||
connector = await check_ownership(session, SearchSourceConnector, connector_id, user)
|
connector = await check_ownership(
|
||||||
|
session, SearchSourceConnector, connector_id, user
|
||||||
|
)
|
||||||
|
|
||||||
# Check if the search space belongs to the user
|
# Check if the search space belongs to the user
|
||||||
search_space = await check_ownership(session, SearchSpace, search_space_id, user)
|
search_space = await check_ownership(
|
||||||
|
session, SearchSpace, search_space_id, user
|
||||||
|
)
|
||||||
|
|
||||||
# Handle different connector types
|
# Handle different connector types
|
||||||
response_message = ""
|
response_message = ""
|
||||||
|
|
@ -316,7 +375,9 @@ async def index_connector_content(
|
||||||
else:
|
else:
|
||||||
indexing_from = connector.last_indexed_at.strftime("%Y-%m-%d")
|
indexing_from = connector.last_indexed_at.strftime("%Y-%m-%d")
|
||||||
else:
|
else:
|
||||||
indexing_from = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")
|
indexing_from = (datetime.now() - timedelta(days=365)).strftime(
|
||||||
|
"%Y-%m-%d"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
indexing_from = start_date
|
indexing_from = start_date
|
||||||
|
|
||||||
|
|
@ -327,42 +388,98 @@ async def index_connector_content(
|
||||||
|
|
||||||
if connector.connector_type == SearchSourceConnectorType.SLACK_CONNECTOR:
|
if connector.connector_type == SearchSourceConnectorType.SLACK_CONNECTOR:
|
||||||
# Run indexing in background
|
# Run indexing in background
|
||||||
logger.info(f"Triggering Slack indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}")
|
logger.info(
|
||||||
background_tasks.add_task(run_slack_indexing_with_new_session, connector_id, search_space_id, str(user.id), indexing_from, indexing_to)
|
f"Triggering Slack indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
run_slack_indexing_with_new_session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
str(user.id),
|
||||||
|
indexing_from,
|
||||||
|
indexing_to,
|
||||||
|
)
|
||||||
response_message = "Slack indexing started in the background."
|
response_message = "Slack indexing started in the background."
|
||||||
|
|
||||||
elif connector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR:
|
elif connector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR:
|
||||||
# Run indexing in background
|
# Run indexing in background
|
||||||
logger.info(f"Triggering Notion indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}")
|
logger.info(
|
||||||
background_tasks.add_task(run_notion_indexing_with_new_session, connector_id, search_space_id, str(user.id), indexing_from, indexing_to)
|
f"Triggering Notion indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
run_notion_indexing_with_new_session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
str(user.id),
|
||||||
|
indexing_from,
|
||||||
|
indexing_to,
|
||||||
|
)
|
||||||
response_message = "Notion indexing started in the background."
|
response_message = "Notion indexing started in the background."
|
||||||
|
|
||||||
elif connector.connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR:
|
elif connector.connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR:
|
||||||
# Run indexing in background
|
# Run indexing in background
|
||||||
logger.info(f"Triggering GitHub indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}")
|
logger.info(
|
||||||
background_tasks.add_task(run_github_indexing_with_new_session, connector_id, search_space_id, str(user.id), indexing_from, indexing_to)
|
f"Triggering GitHub indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
run_github_indexing_with_new_session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
str(user.id),
|
||||||
|
indexing_from,
|
||||||
|
indexing_to,
|
||||||
|
)
|
||||||
response_message = "GitHub indexing started in the background."
|
response_message = "GitHub indexing started in the background."
|
||||||
|
|
||||||
elif connector.connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR:
|
elif connector.connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR:
|
||||||
# Run indexing in background
|
# Run indexing in background
|
||||||
logger.info(f"Triggering Linear indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}")
|
logger.info(
|
||||||
background_tasks.add_task(run_linear_indexing_with_new_session, connector_id, search_space_id, str(user.id), indexing_from, indexing_to)
|
f"Triggering Linear indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
run_linear_indexing_with_new_session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
str(user.id),
|
||||||
|
indexing_from,
|
||||||
|
indexing_to,
|
||||||
|
)
|
||||||
response_message = "Linear indexing started in the background."
|
response_message = "Linear indexing started in the background."
|
||||||
|
|
||||||
|
elif connector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR:
|
||||||
|
# Run indexing in background
|
||||||
|
logger.info(
|
||||||
|
f"Triggering Jira indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
||||||
|
)
|
||||||
|
background_tasks.add_task(
|
||||||
|
run_jira_indexing_with_new_session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
str(user.id),
|
||||||
|
indexing_from,
|
||||||
|
indexing_to,
|
||||||
|
)
|
||||||
|
response_message = "Jira indexing started in the background."
|
||||||
|
|
||||||
elif connector.connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR:
|
elif connector.connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR:
|
||||||
# Run indexing in background
|
# Run indexing in background
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Triggering Discord indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
f"Triggering Discord indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
|
||||||
)
|
)
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
run_discord_indexing_with_new_session, connector_id, search_space_id, str(user.id), indexing_from, indexing_to
|
run_discord_indexing_with_new_session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
str(user.id),
|
||||||
|
indexing_from,
|
||||||
|
indexing_to,
|
||||||
)
|
)
|
||||||
response_message = "Discord indexing started in the background."
|
response_message = "Discord indexing started in the background."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Indexing not supported for connector type: {connector.connector_type}"
|
detail=f"Indexing not supported for connector type: {connector.connector_type}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -370,21 +487,21 @@ async def index_connector_content(
|
||||||
"connector_id": connector_id,
|
"connector_id": connector_id,
|
||||||
"search_space_id": search_space_id,
|
"search_space_id": search_space_id,
|
||||||
"indexing_from": indexing_from,
|
"indexing_from": indexing_from,
|
||||||
"indexing_to": indexing_to
|
"indexing_to": indexing_to,
|
||||||
}
|
}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to initiate indexing for connector {connector_id}: {e}", exc_info=True)
|
logger.error(
|
||||||
|
f"Failed to initiate indexing for connector {connector_id}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500, detail=f"Failed to initiate indexing: {str(e)}"
|
||||||
detail=f"Failed to initiate indexing: {str(e)}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update_connector_last_indexed(
|
|
||||||
session: AsyncSession,
|
async def update_connector_last_indexed(session: AsyncSession, connector_id: int):
|
||||||
connector_id: int
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Update the last_indexed_at timestamp for a connector.
|
Update the last_indexed_at timestamp for a connector.
|
||||||
|
|
||||||
|
|
@ -394,8 +511,9 @@ async def update_connector_last_indexed(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(SearchSourceConnector)
|
select(SearchSourceConnector).filter(
|
||||||
.filter(SearchSourceConnector.id == connector_id)
|
SearchSourceConnector.id == connector_id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
|
|
||||||
|
|
@ -404,22 +522,28 @@ async def update_connector_last_indexed(
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(f"Updated last_indexed_at for connector {connector_id}")
|
logger.info(f"Updated last_indexed_at for connector {connector_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to update last_indexed_at for connector {connector_id}: {str(e)}")
|
logger.error(
|
||||||
|
f"Failed to update last_indexed_at for connector {connector_id}: {str(e)}"
|
||||||
|
)
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
|
|
||||||
|
|
||||||
async def run_slack_indexing_with_new_session(
|
async def run_slack_indexing_with_new_session(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new session and run the Slack indexing task.
|
Create a new session and run the Slack indexing task.
|
||||||
This prevents session leaks by creating a dedicated session for the background task.
|
This prevents session leaks by creating a dedicated session for the background task.
|
||||||
"""
|
"""
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
await run_slack_indexing(session, connector_id, search_space_id, user_id, start_date, end_date)
|
await run_slack_indexing(
|
||||||
|
session, connector_id, search_space_id, user_id, start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_slack_indexing(
|
async def run_slack_indexing(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|
@ -427,7 +551,7 @@ async def run_slack_indexing(
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Background task to run Slack indexing.
|
Background task to run Slack indexing.
|
||||||
|
|
@ -449,31 +573,39 @@ async def run_slack_indexing(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
update_last_indexed=False # Don't update timestamp in the indexing function
|
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)
|
# Only update last_indexed_at if indexing was successful (either new docs or updated docs)
|
||||||
if documents_processed > 0:
|
if documents_processed > 0:
|
||||||
await update_connector_last_indexed(session, connector_id)
|
await update_connector_last_indexed(session, connector_id)
|
||||||
logger.info(f"Slack indexing completed successfully: {documents_processed} documents processed")
|
logger.info(
|
||||||
|
f"Slack indexing completed successfully: {documents_processed} documents processed"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Slack indexing failed or no documents processed: {error_or_warning}")
|
logger.error(
|
||||||
|
f"Slack indexing failed or no documents processed: {error_or_warning}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in background Slack indexing task: {str(e)}")
|
logger.error(f"Error in background Slack indexing task: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
async def run_notion_indexing_with_new_session(
|
async def run_notion_indexing_with_new_session(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new session and run the Notion indexing task.
|
Create a new session and run the Notion indexing task.
|
||||||
This prevents session leaks by creating a dedicated session for the background task.
|
This prevents session leaks by creating a dedicated session for the background task.
|
||||||
"""
|
"""
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
await run_notion_indexing(session, connector_id, search_space_id, user_id, start_date, end_date)
|
await run_notion_indexing(
|
||||||
|
session, connector_id, search_space_id, user_id, start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_notion_indexing(
|
async def run_notion_indexing(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|
@ -481,7 +613,7 @@ async def run_notion_indexing(
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Background task to run Notion indexing.
|
Background task to run Notion indexing.
|
||||||
|
|
@ -503,112 +635,158 @@ async def run_notion_indexing(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
update_last_indexed=False # Don't update timestamp in the indexing function
|
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)
|
# Only update last_indexed_at if indexing was successful (either new docs or updated docs)
|
||||||
if documents_processed > 0:
|
if documents_processed > 0:
|
||||||
await update_connector_last_indexed(session, connector_id)
|
await update_connector_last_indexed(session, connector_id)
|
||||||
logger.info(f"Notion indexing completed successfully: {documents_processed} documents processed")
|
logger.info(
|
||||||
|
f"Notion indexing completed successfully: {documents_processed} documents processed"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Notion indexing failed or no documents processed: {error_or_warning}")
|
logger.error(
|
||||||
|
f"Notion indexing failed or no documents processed: {error_or_warning}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in background Notion indexing task: {str(e)}")
|
logger.error(f"Error in background Notion indexing task: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# Add new helper functions for GitHub indexing
|
# Add new helper functions for GitHub indexing
|
||||||
async def run_github_indexing_with_new_session(
|
async def run_github_indexing_with_new_session(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""Wrapper to run GitHub indexing with its own database session."""
|
"""Wrapper to run GitHub indexing with its own database session."""
|
||||||
logger.info(f"Background task started: Indexing GitHub connector {connector_id} into space {search_space_id} from {start_date} to {end_date}")
|
logger.info(
|
||||||
|
f"Background task started: Indexing GitHub connector {connector_id} into space {search_space_id} from {start_date} to {end_date}"
|
||||||
|
)
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
await run_github_indexing(session, connector_id, search_space_id, user_id, start_date, end_date)
|
await run_github_indexing(
|
||||||
|
session, connector_id, search_space_id, user_id, start_date, end_date
|
||||||
|
)
|
||||||
logger.info(f"Background task finished: Indexing GitHub connector {connector_id}")
|
logger.info(f"Background task finished: Indexing GitHub connector {connector_id}")
|
||||||
|
|
||||||
|
|
||||||
async def run_github_indexing(
|
async def run_github_indexing(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""Runs the GitHub indexing task and updates the timestamp."""
|
"""Runs the GitHub indexing task and updates the timestamp."""
|
||||||
try:
|
try:
|
||||||
indexed_count, error_message = await index_github_repos(
|
indexed_count, error_message = await index_github_repos(
|
||||||
session, connector_id, search_space_id, user_id, start_date, end_date, update_last_indexed=False
|
session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
user_id,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
update_last_indexed=False,
|
||||||
)
|
)
|
||||||
if error_message:
|
if error_message:
|
||||||
logger.error(f"GitHub indexing failed for connector {connector_id}: {error_message}")
|
logger.error(
|
||||||
|
f"GitHub indexing failed for connector {connector_id}: {error_message}"
|
||||||
|
)
|
||||||
# Optionally update status in DB to indicate failure
|
# Optionally update status in DB to indicate failure
|
||||||
else:
|
else:
|
||||||
logger.info(f"GitHub indexing successful for connector {connector_id}. Indexed {indexed_count} documents.")
|
logger.info(
|
||||||
|
f"GitHub indexing successful for connector {connector_id}. Indexed {indexed_count} documents."
|
||||||
|
)
|
||||||
# Update the last indexed timestamp only on success
|
# Update the last indexed timestamp only on success
|
||||||
await update_connector_last_indexed(session, connector_id)
|
await update_connector_last_indexed(session, connector_id)
|
||||||
await session.commit() # Commit timestamp update
|
await session.commit() # Commit timestamp update
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(f"Critical error in run_github_indexing for connector {connector_id}: {e}", exc_info=True)
|
logger.error(
|
||||||
|
f"Critical error in run_github_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 Linear indexing
|
# Add new helper functions for Linear indexing
|
||||||
async def run_linear_indexing_with_new_session(
|
async def run_linear_indexing_with_new_session(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""Wrapper to run Linear indexing with its own database session."""
|
"""Wrapper to run Linear indexing with its own database session."""
|
||||||
logger.info(f"Background task started: Indexing Linear connector {connector_id} into space {search_space_id} from {start_date} to {end_date}")
|
logger.info(
|
||||||
|
f"Background task started: Indexing Linear connector {connector_id} into space {search_space_id} from {start_date} to {end_date}"
|
||||||
|
)
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
await run_linear_indexing(session, connector_id, search_space_id, user_id, start_date, end_date)
|
await run_linear_indexing(
|
||||||
|
session, connector_id, search_space_id, user_id, start_date, end_date
|
||||||
|
)
|
||||||
logger.info(f"Background task finished: Indexing Linear connector {connector_id}")
|
logger.info(f"Background task finished: Indexing Linear connector {connector_id}")
|
||||||
|
|
||||||
|
|
||||||
async def run_linear_indexing(
|
async def run_linear_indexing(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""Runs the Linear indexing task and updates the timestamp."""
|
"""Runs the Linear indexing task and updates the timestamp."""
|
||||||
try:
|
try:
|
||||||
indexed_count, error_message = await index_linear_issues(
|
indexed_count, error_message = await index_linear_issues(
|
||||||
session, connector_id, search_space_id, user_id, start_date, end_date, update_last_indexed=False
|
session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
user_id,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
update_last_indexed=False,
|
||||||
)
|
)
|
||||||
if error_message:
|
if error_message:
|
||||||
logger.error(f"Linear indexing failed for connector {connector_id}: {error_message}")
|
logger.error(
|
||||||
|
f"Linear indexing failed for connector {connector_id}: {error_message}"
|
||||||
|
)
|
||||||
# Optionally update status in DB to indicate failure
|
# Optionally update status in DB to indicate failure
|
||||||
else:
|
else:
|
||||||
logger.info(f"Linear indexing successful for connector {connector_id}. Indexed {indexed_count} documents.")
|
logger.info(
|
||||||
|
f"Linear indexing successful for connector {connector_id}. Indexed {indexed_count} documents."
|
||||||
|
)
|
||||||
# Update the last indexed timestamp only on success
|
# Update the last indexed timestamp only on success
|
||||||
await update_connector_last_indexed(session, connector_id)
|
await update_connector_last_indexed(session, connector_id)
|
||||||
await session.commit() # Commit timestamp update
|
await session.commit() # Commit timestamp update
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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
|
# Add new helper functions for discord indexing
|
||||||
async def run_discord_indexing_with_new_session(
|
async def run_discord_indexing_with_new_session(
|
||||||
connector_id: int,
|
connector_id: int,
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new session and run the Discord indexing task.
|
Create a new session and run the Discord indexing task.
|
||||||
This prevents session leaks by creating a dedicated session for the background task.
|
This prevents session leaks by creating a dedicated session for the background task.
|
||||||
"""
|
"""
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
await run_discord_indexing(session, connector_id, search_space_id, user_id, start_date, end_date)
|
await run_discord_indexing(
|
||||||
|
session, connector_id, search_space_id, user_id, start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_discord_indexing(
|
async def run_discord_indexing(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|
@ -616,7 +794,7 @@ async def run_discord_indexing(
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str
|
end_date: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Background task to run Discord indexing.
|
Background task to run Discord indexing.
|
||||||
|
|
@ -637,14 +815,76 @@ async def run_discord_indexing(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
update_last_indexed=False # Don't update timestamp in the indexing function
|
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)
|
# Only update last_indexed_at if indexing was successful (either new docs or updated docs)
|
||||||
if documents_processed > 0:
|
if documents_processed > 0:
|
||||||
await update_connector_last_indexed(session, connector_id)
|
await update_connector_last_indexed(session, connector_id)
|
||||||
logger.info(f"Discord indexing completed successfully: {documents_processed} documents processed")
|
logger.info(
|
||||||
|
f"Discord indexing completed successfully: {documents_processed} documents processed"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Discord indexing failed or no documents processed: {error_or_warning}")
|
logger.error(
|
||||||
|
f"Discord indexing failed or no documents processed: {error_or_warning}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in background Discord indexing task: {str(e)}")
|
logger.error(f"Error in background Discord indexing task: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# Add new helper functions for Jira indexing
|
||||||
|
async def run_jira_indexing_with_new_session(
|
||||||
|
connector_id: int,
|
||||||
|
search_space_id: int,
|
||||||
|
user_id: str,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
):
|
||||||
|
"""Wrapper to run Jira indexing with its own database session."""
|
||||||
|
logger.info(
|
||||||
|
f"Background task started: Indexing Jira connector {connector_id} into space {search_space_id} from {start_date} to {end_date}"
|
||||||
|
)
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
await run_jira_indexing(
|
||||||
|
session, connector_id, search_space_id, user_id, start_date, end_date
|
||||||
|
)
|
||||||
|
logger.info(f"Background task finished: Indexing Jira connector {connector_id}")
|
||||||
|
|
||||||
|
|
||||||
|
async def run_jira_indexing(
|
||||||
|
session: AsyncSession,
|
||||||
|
connector_id: int,
|
||||||
|
search_space_id: int,
|
||||||
|
user_id: str,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
):
|
||||||
|
"""Runs the Jira indexing task and updates the timestamp."""
|
||||||
|
try:
|
||||||
|
indexed_count, error_message = await index_jira_issues(
|
||||||
|
session,
|
||||||
|
connector_id,
|
||||||
|
search_space_id,
|
||||||
|
user_id,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
update_last_indexed=False,
|
||||||
|
)
|
||||||
|
if error_message:
|
||||||
|
logger.error(
|
||||||
|
f"Jira indexing failed for connector {connector_id}: {error_message}"
|
||||||
|
)
|
||||||
|
# Optionally update status in DB to indicate failure
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Jira indexing successful for connector {connector_id}. Indexed {indexed_count} documents."
|
||||||
|
)
|
||||||
|
# Update the last indexed timestamp only on success
|
||||||
|
await update_connector_last_indexed(session, connector_id)
|
||||||
|
await session.commit() # Commit timestamp update
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Critical error in run_jira_indexing for connector {connector_id}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
# Optionally update status in DB to indicate failure
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
from datetime import datetime
|
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, Any, Optional
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, field_validator, ConfigDict
|
from typing import Any, Dict, Optional
|
||||||
from .base import IDModel, TimestampModel
|
|
||||||
from app.db import SearchSourceConnectorType
|
from app.db import SearchSourceConnectorType
|
||||||
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
|
|
||||||
|
from .base import IDModel, TimestampModel
|
||||||
|
|
||||||
|
|
||||||
class SearchSourceConnectorBase(BaseModel):
|
class SearchSourceConnectorBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -12,16 +15,20 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
last_indexed_at: Optional[datetime] = None
|
last_indexed_at: Optional[datetime] = None
|
||||||
config: Dict[str, Any]
|
config: Dict[str, Any]
|
||||||
|
|
||||||
@field_validator('config')
|
@field_validator("config")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_config_for_connector_type(cls, config: Dict[str, Any], values: Dict[str, Any]) -> Dict[str, Any]:
|
def validate_config_for_connector_type(
|
||||||
connector_type = values.data.get('connector_type')
|
cls, config: Dict[str, Any], values: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
connector_type = values.data.get("connector_type")
|
||||||
|
|
||||||
if connector_type == SearchSourceConnectorType.SERPER_API:
|
if connector_type == SearchSourceConnectorType.SERPER_API:
|
||||||
# For SERPER_API, only allow SERPER_API_KEY
|
# For SERPER_API, only allow SERPER_API_KEY
|
||||||
allowed_keys = ["SERPER_API_KEY"]
|
allowed_keys = ["SERPER_API_KEY"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For SERPER_API connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For SERPER_API connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the API key is not empty
|
# Ensure the API key is not empty
|
||||||
if not config.get("SERPER_API_KEY"):
|
if not config.get("SERPER_API_KEY"):
|
||||||
|
|
@ -31,7 +38,9 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# For TAVILY_API, only allow TAVILY_API_KEY
|
# For TAVILY_API, only allow TAVILY_API_KEY
|
||||||
allowed_keys = ["TAVILY_API_KEY"]
|
allowed_keys = ["TAVILY_API_KEY"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For TAVILY_API connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For TAVILY_API connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the API key is not empty
|
# Ensure the API key is not empty
|
||||||
if not config.get("TAVILY_API_KEY"):
|
if not config.get("TAVILY_API_KEY"):
|
||||||
|
|
@ -41,7 +50,9 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# For LINKUP_API, only allow LINKUP_API_KEY
|
# For LINKUP_API, only allow LINKUP_API_KEY
|
||||||
allowed_keys = ["LINKUP_API_KEY"]
|
allowed_keys = ["LINKUP_API_KEY"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the API key is not empty
|
# Ensure the API key is not empty
|
||||||
if not config.get("LINKUP_API_KEY"):
|
if not config.get("LINKUP_API_KEY"):
|
||||||
|
|
@ -51,7 +62,9 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# For SLACK_CONNECTOR, only allow SLACK_BOT_TOKEN
|
# For SLACK_CONNECTOR, only allow SLACK_BOT_TOKEN
|
||||||
allowed_keys = ["SLACK_BOT_TOKEN"]
|
allowed_keys = ["SLACK_BOT_TOKEN"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For SLACK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For SLACK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the bot token is not empty
|
# Ensure the bot token is not empty
|
||||||
if not config.get("SLACK_BOT_TOKEN"):
|
if not config.get("SLACK_BOT_TOKEN"):
|
||||||
|
|
@ -61,7 +74,9 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# For NOTION_CONNECTOR, only allow NOTION_INTEGRATION_TOKEN
|
# For NOTION_CONNECTOR, only allow NOTION_INTEGRATION_TOKEN
|
||||||
allowed_keys = ["NOTION_INTEGRATION_TOKEN"]
|
allowed_keys = ["NOTION_INTEGRATION_TOKEN"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For NOTION_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For NOTION_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the integration token is not empty
|
# Ensure the integration token is not empty
|
||||||
if not config.get("NOTION_INTEGRATION_TOKEN"):
|
if not config.get("NOTION_INTEGRATION_TOKEN"):
|
||||||
|
|
@ -71,7 +86,9 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# For GITHUB_CONNECTOR, only allow GITHUB_PAT and repo_full_names
|
# For GITHUB_CONNECTOR, only allow GITHUB_PAT and repo_full_names
|
||||||
allowed_keys = ["GITHUB_PAT", "repo_full_names"]
|
allowed_keys = ["GITHUB_PAT", "repo_full_names"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the token is not empty
|
# Ensure the token is not empty
|
||||||
if not config.get("GITHUB_PAT"):
|
if not config.get("GITHUB_PAT"):
|
||||||
|
|
@ -86,7 +103,9 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# 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"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For LINEAR_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(
|
||||||
|
f"For LINEAR_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure the token is not empty
|
# Ensure the token is not empty
|
||||||
if not config.get("LINEAR_API_KEY"):
|
if not config.get("LINEAR_API_KEY"):
|
||||||
|
|
@ -96,17 +115,40 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
# For DISCORD_CONNECTOR, only allow DISCORD_BOT_TOKEN
|
# For DISCORD_CONNECTOR, only allow DISCORD_BOT_TOKEN
|
||||||
allowed_keys = ["DISCORD_BOT_TOKEN"]
|
allowed_keys = ["DISCORD_BOT_TOKEN"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For DISCORD_CONNECTOR connector type, config must only contain these keys: {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
|
# Ensure the bot token is not empty
|
||||||
if not config.get("DISCORD_BOT_TOKEN"):
|
if not config.get("DISCORD_BOT_TOKEN"):
|
||||||
raise ValueError("DISCORD_BOT_TOKEN cannot be empty")
|
raise ValueError("DISCORD_BOT_TOKEN cannot be empty")
|
||||||
|
elif connector_type == SearchSourceConnectorType.JIRA_CONNECTOR:
|
||||||
|
# For JIRA_CONNECTOR, require JIRA_EMAIL, JIRA_API_TOKEN and JIRA_BASE_URL
|
||||||
|
allowed_keys = ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"]
|
||||||
|
if set(config.keys()) != set(allowed_keys):
|
||||||
|
raise ValueError(
|
||||||
|
f"For JIRA_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the email is not empty
|
||||||
|
if not config.get("JIRA_EMAIL"):
|
||||||
|
raise ValueError("JIRA_EMAIL cannot be empty")
|
||||||
|
|
||||||
|
# Ensure the API token is not empty
|
||||||
|
if not config.get("JIRA_API_TOKEN"):
|
||||||
|
raise ValueError("JIRA_API_TOKEN cannot be empty")
|
||||||
|
|
||||||
|
# Ensure the base URL is not empty
|
||||||
|
if not config.get("JIRA_BASE_URL"):
|
||||||
|
raise ValueError("JIRA_BASE_URL cannot be empty")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SearchSourceConnectorUpdate(BaseModel):
|
class SearchSourceConnectorUpdate(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
connector_type: Optional[SearchSourceConnectorType] = None
|
connector_type: Optional[SearchSourceConnectorType] = None
|
||||||
|
|
@ -114,6 +156,7 @@ class SearchSourceConnectorUpdate(BaseModel):
|
||||||
last_indexed_at: Optional[datetime] = None
|
last_indexed_at: Optional[datetime] = None
|
||||||
config: Optional[Dict[str, Any]] = None
|
config: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
class SearchSourceConnectorRead(SearchSourceConnectorBase, IDModel, TimestampModel):
|
class SearchSourceConnectorRead(SearchSourceConnectorBase, IDModel, TimestampModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
from typing import List, Dict, Optional
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from typing import Dict, List, Optional
|
||||||
from sqlalchemy.future import select
|
|
||||||
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
|
||||||
from app.retriver.documents_hybrid_search import DocumentHybridSearchRetriever
|
|
||||||
from app.db import SearchSourceConnector, SearchSourceConnectorType, Chunk, Document, SearchSpace
|
|
||||||
from tavily import TavilyClient
|
|
||||||
from linkup import LinkupClient
|
|
||||||
from sqlalchemy import func
|
|
||||||
|
|
||||||
from app.agents.researcher.configuration import SearchMode
|
from app.agents.researcher.configuration import SearchMode
|
||||||
|
from app.db import (
|
||||||
|
Chunk,
|
||||||
|
Document,
|
||||||
|
SearchSourceConnector,
|
||||||
|
SearchSourceConnectorType,
|
||||||
|
SearchSpace,
|
||||||
|
)
|
||||||
|
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
||||||
|
from app.retriver.documents_hybrid_search import DocumentHybridSearchRetriever
|
||||||
|
from linkup import LinkupClient
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from tavily import TavilyClient
|
||||||
|
|
||||||
|
|
||||||
class ConnectorService:
|
class ConnectorService:
|
||||||
|
|
@ -18,8 +24,12 @@ class ConnectorService:
|
||||||
self.chunk_retriever = ChucksHybridSearchRetriever(session)
|
self.chunk_retriever = ChucksHybridSearchRetriever(session)
|
||||||
self.document_retriever = DocumentHybridSearchRetriever(session)
|
self.document_retriever = DocumentHybridSearchRetriever(session)
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.source_id_counter = 100000 # High starting value to avoid collisions with existing IDs
|
self.source_id_counter = (
|
||||||
self.counter_lock = asyncio.Lock() # Lock to protect counter in multithreaded environments
|
100000 # High starting value to avoid collisions with existing IDs
|
||||||
|
)
|
||||||
|
self.counter_lock = (
|
||||||
|
asyncio.Lock()
|
||||||
|
) # Lock to protect counter in multithreaded environments
|
||||||
|
|
||||||
async def initialize_counter(self):
|
async def initialize_counter(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -38,13 +48,22 @@ class ConnectorService:
|
||||||
)
|
)
|
||||||
chunk_count = result.scalar() or 0
|
chunk_count = result.scalar() or 0
|
||||||
self.source_id_counter = chunk_count + 1
|
self.source_id_counter = chunk_count + 1
|
||||||
print(f"Initialized source_id_counter to {self.source_id_counter} for user {self.user_id}")
|
print(
|
||||||
|
f"Initialized source_id_counter to {self.source_id_counter} for user {self.user_id}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error initializing source_id_counter: {str(e)}")
|
print(f"Error initializing source_id_counter: {str(e)}")
|
||||||
# Fallback to default value
|
# Fallback to default value
|
||||||
self.source_id_counter = 1
|
self.source_id_counter = 1
|
||||||
|
|
||||||
async def search_crawled_urls(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_crawled_urls(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for crawled URLs and return both the source information and langchain documents
|
Search for crawled URLs and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -57,7 +76,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="CRAWLED_URL"
|
document_type="CRAWLED_URL",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
crawled_urls_chunks = await self.document_retriever.hybrid_search(
|
crawled_urls_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -65,7 +84,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="CRAWLED_URL"
|
document_type="CRAWLED_URL",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
crawled_urls_chunks = self._transform_document_results(crawled_urls_chunks)
|
crawled_urls_chunks = self._transform_document_results(crawled_urls_chunks)
|
||||||
|
|
@ -84,15 +103,18 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(crawled_urls_chunks):
|
for _i, chunk in enumerate(crawled_urls_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Create a source entry
|
# Create a source entry
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": document.get('title', 'Untitled Document'),
|
"title": document.get("title", "Untitled Document"),
|
||||||
"description": metadata.get('og:description', metadata.get('ogDescription', chunk.get('content', '')[:100])),
|
"description": metadata.get(
|
||||||
"url": metadata.get('url', '')
|
"og:description",
|
||||||
|
metadata.get("ogDescription", chunk.get("content", "")[:100]),
|
||||||
|
),
|
||||||
|
"url": metadata.get("url", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -108,7 +130,14 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, crawled_urls_chunks
|
return result_object, crawled_urls_chunks
|
||||||
|
|
||||||
async def search_files(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_files(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for files and return both the source information and langchain documents
|
Search for files and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -121,7 +150,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="FILE"
|
document_type="FILE",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
files_chunks = await self.document_retriever.hybrid_search(
|
files_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -129,7 +158,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="FILE"
|
document_type="FILE",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
files_chunks = self._transform_document_results(files_chunks)
|
files_chunks = self._transform_document_results(files_chunks)
|
||||||
|
|
@ -148,15 +177,18 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(files_chunks):
|
for _i, chunk in enumerate(files_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Create a source entry
|
# Create a source entry
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": document.get('title', 'Untitled Document'),
|
"title": document.get("title", "Untitled Document"),
|
||||||
"description": metadata.get('og:description', metadata.get('ogDescription', chunk.get('content', '')[:100])),
|
"description": metadata.get(
|
||||||
"url": metadata.get('url', '')
|
"og:description",
|
||||||
|
metadata.get("ogDescription", chunk.get("content", "")[:100]),
|
||||||
|
),
|
||||||
|
"url": metadata.get("url", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -185,19 +217,23 @@ class ConnectorService:
|
||||||
"""
|
"""
|
||||||
transformed_results = []
|
transformed_results = []
|
||||||
for doc in document_results:
|
for doc in document_results:
|
||||||
transformed_results.append({
|
transformed_results.append(
|
||||||
'document': {
|
{
|
||||||
'id': doc.get('document_id'),
|
"document": {
|
||||||
'title': doc.get('title', 'Untitled Document'),
|
"id": doc.get("document_id"),
|
||||||
'document_type': doc.get('document_type'),
|
"title": doc.get("title", "Untitled Document"),
|
||||||
'metadata': doc.get('metadata', {}),
|
"document_type": doc.get("document_type"),
|
||||||
},
|
"metadata": doc.get("metadata", {}),
|
||||||
'content': doc.get('chunks_content', doc.get('content', '')),
|
},
|
||||||
'score': doc.get('score', 0.0)
|
"content": doc.get("chunks_content", doc.get("content", "")),
|
||||||
})
|
"score": doc.get("score", 0.0),
|
||||||
|
}
|
||||||
|
)
|
||||||
return transformed_results
|
return transformed_results
|
||||||
|
|
||||||
async def get_connector_by_type(self, user_id: str, connector_type: SearchSourceConnectorType) -> Optional[SearchSourceConnector]:
|
async def get_connector_by_type(
|
||||||
|
self, user_id: str, connector_type: SearchSourceConnectorType
|
||||||
|
) -> Optional[SearchSourceConnector]:
|
||||||
"""
|
"""
|
||||||
Get a connector by type for a specific user
|
Get a connector by type for a specific user
|
||||||
|
|
||||||
|
|
@ -209,15 +245,16 @@ class ConnectorService:
|
||||||
Optional[SearchSourceConnector]: The connector if found, None otherwise
|
Optional[SearchSourceConnector]: The connector if found, None otherwise
|
||||||
"""
|
"""
|
||||||
result = await self.session.execute(
|
result = await self.session.execute(
|
||||||
select(SearchSourceConnector)
|
select(SearchSourceConnector).filter(
|
||||||
.filter(
|
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == connector_type
|
SearchSourceConnector.connector_type == connector_type,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result.scalars().first()
|
return result.scalars().first()
|
||||||
|
|
||||||
async def search_tavily(self, user_query: str, user_id: str, top_k: int = 20) -> tuple:
|
async def search_tavily(
|
||||||
|
self, user_query: str, user_id: str, top_k: int = 20
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search using Tavily API and return both the source information and documents
|
Search using Tavily API and return both the source information and documents
|
||||||
|
|
||||||
|
|
@ -230,7 +267,9 @@ class ConnectorService:
|
||||||
tuple: (sources_info, documents)
|
tuple: (sources_info, documents)
|
||||||
"""
|
"""
|
||||||
# Get Tavily connector configuration
|
# Get Tavily connector configuration
|
||||||
tavily_connector = await self.get_connector_by_type(user_id, SearchSourceConnectorType.TAVILY_API)
|
tavily_connector = await self.get_connector_by_type(
|
||||||
|
user_id, SearchSourceConnectorType.TAVILY_API
|
||||||
|
)
|
||||||
|
|
||||||
if not tavily_connector:
|
if not tavily_connector:
|
||||||
# Return empty results if no Tavily connector is configured
|
# Return empty results if no Tavily connector is configured
|
||||||
|
|
@ -250,7 +289,7 @@ class ConnectorService:
|
||||||
response = tavily_client.search(
|
response = tavily_client.search(
|
||||||
query=user_query,
|
query=user_query,
|
||||||
max_results=top_k,
|
max_results=top_k,
|
||||||
search_depth="advanced" # Use advanced search for better results
|
search_depth="advanced", # Use advanced search for better results
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract results from Tavily response
|
# Extract results from Tavily response
|
||||||
|
|
@ -271,13 +310,12 @@ class ConnectorService:
|
||||||
|
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for i, result in enumerate(tavily_results):
|
for i, result in enumerate(tavily_results):
|
||||||
|
|
||||||
# Create a source entry
|
# Create a source entry
|
||||||
source = {
|
source = {
|
||||||
"id": self.source_id_counter,
|
"id": self.source_id_counter,
|
||||||
"title": result.get("title", "Tavily Result"),
|
"title": result.get("title", "Tavily Result"),
|
||||||
"description": result.get("content", "")[:100],
|
"description": result.get("content", "")[:100],
|
||||||
"url": result.get("url", "")
|
"url": result.get("url", ""),
|
||||||
}
|
}
|
||||||
sources_list.append(source)
|
sources_list.append(source)
|
||||||
|
|
||||||
|
|
@ -293,9 +331,9 @@ class ConnectorService:
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"url": result.get("url", ""),
|
"url": result.get("url", ""),
|
||||||
"published_date": result.get("published_date", ""),
|
"published_date": result.get("published_date", ""),
|
||||||
"source": "TAVILY_API"
|
"source": "TAVILY_API",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
documents.append(document)
|
documents.append(document)
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -320,7 +358,14 @@ class ConnectorService:
|
||||||
"sources": [],
|
"sources": [],
|
||||||
}, []
|
}, []
|
||||||
|
|
||||||
async def search_slack(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_slack(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for slack and return both the source information and langchain documents
|
Search for slack and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -333,7 +378,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="SLACK_CONNECTOR"
|
document_type="SLACK_CONNECTOR",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
slack_chunks = await self.document_retriever.hybrid_search(
|
slack_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -341,7 +386,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="SLACK_CONNECTOR"
|
document_type="SLACK_CONNECTOR",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
slack_chunks = self._transform_document_results(slack_chunks)
|
slack_chunks = self._transform_document_results(slack_chunks)
|
||||||
|
|
@ -360,13 +405,13 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(slack_chunks):
|
for _i, chunk in enumerate(slack_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Create a mapped source entry with Slack-specific metadata
|
# Create a mapped source entry with Slack-specific metadata
|
||||||
channel_name = metadata.get('channel_name', 'Unknown Channel')
|
channel_name = metadata.get("channel_name", "Unknown Channel")
|
||||||
channel_id = metadata.get('channel_id', '')
|
channel_id = metadata.get("channel_id", "")
|
||||||
message_date = metadata.get('start_date', '')
|
message_date = metadata.get("start_date", "")
|
||||||
|
|
||||||
# Create a more descriptive title for Slack messages
|
# Create a more descriptive title for Slack messages
|
||||||
title = f"Slack: {channel_name}"
|
title = f"Slack: {channel_name}"
|
||||||
|
|
@ -374,7 +419,7 @@ class ConnectorService:
|
||||||
title += f" ({message_date})"
|
title += f" ({message_date})"
|
||||||
|
|
||||||
# Create a more descriptive description for Slack messages
|
# Create a more descriptive description for Slack messages
|
||||||
description = chunk.get('content', '')[:100]
|
description = chunk.get("content", "")[:100]
|
||||||
if len(description) == 100:
|
if len(description) == 100:
|
||||||
description += "..."
|
description += "..."
|
||||||
|
|
||||||
|
|
@ -384,7 +429,7 @@ class ConnectorService:
|
||||||
url = f"https://slack.com/app_redirect?channel={channel_id}"
|
url = f"https://slack.com/app_redirect?channel={channel_id}"
|
||||||
|
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
|
@ -403,7 +448,14 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, slack_chunks
|
return result_object, slack_chunks
|
||||||
|
|
||||||
async def search_notion(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_notion(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for Notion pages and return both the source information and langchain documents
|
Search for Notion pages and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -422,7 +474,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="NOTION_CONNECTOR"
|
document_type="NOTION_CONNECTOR",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
notion_chunks = await self.document_retriever.hybrid_search(
|
notion_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -430,7 +482,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="NOTION_CONNECTOR"
|
document_type="NOTION_CONNECTOR",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
notion_chunks = self._transform_document_results(notion_chunks)
|
notion_chunks = self._transform_document_results(notion_chunks)
|
||||||
|
|
@ -449,13 +501,13 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(notion_chunks):
|
for _i, chunk in enumerate(notion_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Create a mapped source entry with Notion-specific metadata
|
# Create a mapped source entry with Notion-specific metadata
|
||||||
page_title = metadata.get('page_title', 'Untitled Page')
|
page_title = metadata.get("page_title", "Untitled Page")
|
||||||
page_id = metadata.get('page_id', '')
|
page_id = metadata.get("page_id", "")
|
||||||
indexed_at = metadata.get('indexed_at', '')
|
indexed_at = metadata.get("indexed_at", "")
|
||||||
|
|
||||||
# Create a more descriptive title for Notion pages
|
# Create a more descriptive title for Notion pages
|
||||||
title = f"Notion: {page_title}"
|
title = f"Notion: {page_title}"
|
||||||
|
|
@ -463,7 +515,7 @@ class ConnectorService:
|
||||||
title += f" (indexed: {indexed_at})"
|
title += f" (indexed: {indexed_at})"
|
||||||
|
|
||||||
# Create a more descriptive description for Notion pages
|
# Create a more descriptive description for Notion pages
|
||||||
description = chunk.get('content', '')[:100]
|
description = chunk.get("content", "")[:100]
|
||||||
if len(description) == 100:
|
if len(description) == 100:
|
||||||
description += "..."
|
description += "..."
|
||||||
|
|
||||||
|
|
@ -474,7 +526,7 @@ class ConnectorService:
|
||||||
url = f"https://notion.so/{page_id.replace('-', '')}"
|
url = f"https://notion.so/{page_id.replace('-', '')}"
|
||||||
|
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
|
@ -493,7 +545,14 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, notion_chunks
|
return result_object, notion_chunks
|
||||||
|
|
||||||
async def search_extension(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_extension(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for extension data and return both the source information and langchain documents
|
Search for extension data and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -512,7 +571,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="EXTENSION"
|
document_type="EXTENSION",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
extension_chunks = await self.document_retriever.hybrid_search(
|
extension_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -520,7 +579,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="EXTENSION"
|
document_type="EXTENSION",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
extension_chunks = self._transform_document_results(extension_chunks)
|
extension_chunks = self._transform_document_results(extension_chunks)
|
||||||
|
|
@ -539,15 +598,17 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for i, chunk in enumerate(extension_chunks):
|
for i, chunk in enumerate(extension_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Extract extension-specific metadata
|
# Extract extension-specific metadata
|
||||||
webpage_title = metadata.get('VisitedWebPageTitle', 'Untitled Page')
|
webpage_title = metadata.get("VisitedWebPageTitle", "Untitled Page")
|
||||||
webpage_url = metadata.get('VisitedWebPageURL', '')
|
webpage_url = metadata.get("VisitedWebPageURL", "")
|
||||||
visit_date = metadata.get('VisitedWebPageDateWithTimeInISOString', '')
|
visit_date = metadata.get("VisitedWebPageDateWithTimeInISOString", "")
|
||||||
visit_duration = metadata.get('VisitedWebPageVisitDurationInMilliseconds', '')
|
visit_duration = metadata.get(
|
||||||
browsing_session_id = metadata.get('BrowsingSessionId', '')
|
"VisitedWebPageVisitDurationInMilliseconds", ""
|
||||||
|
)
|
||||||
|
browsing_session_id = metadata.get("BrowsingSessionId", "")
|
||||||
|
|
||||||
# Create a more descriptive title for extension data
|
# Create a more descriptive title for extension data
|
||||||
title = webpage_title
|
title = webpage_title
|
||||||
|
|
@ -555,14 +616,18 @@ class ConnectorService:
|
||||||
# Format the date for display (simplified)
|
# Format the date for display (simplified)
|
||||||
try:
|
try:
|
||||||
# Just extract the date part for display
|
# Just extract the date part for display
|
||||||
formatted_date = visit_date.split('T')[0] if 'T' in visit_date else visit_date
|
formatted_date = (
|
||||||
|
visit_date.split("T")[0]
|
||||||
|
if "T" in visit_date
|
||||||
|
else visit_date
|
||||||
|
)
|
||||||
title += f" (visited: {formatted_date})"
|
title += f" (visited: {formatted_date})"
|
||||||
except:
|
except:
|
||||||
# Fallback if date parsing fails
|
# Fallback if date parsing fails
|
||||||
title += f" (visited: {visit_date})"
|
title += f" (visited: {visit_date})"
|
||||||
|
|
||||||
# Create a more descriptive description for extension data
|
# Create a more descriptive description for extension data
|
||||||
description = chunk.get('content', '')[:100]
|
description = chunk.get("content", "")[:100]
|
||||||
if len(description) == 100:
|
if len(description) == 100:
|
||||||
description += "..."
|
description += "..."
|
||||||
|
|
||||||
|
|
@ -573,7 +638,7 @@ class ConnectorService:
|
||||||
if duration_seconds < 60:
|
if duration_seconds < 60:
|
||||||
duration_text = f"{duration_seconds:.1f} seconds"
|
duration_text = f"{duration_seconds:.1f} seconds"
|
||||||
else:
|
else:
|
||||||
duration_text = f"{duration_seconds/60:.1f} minutes"
|
duration_text = f"{duration_seconds / 60:.1f} minutes"
|
||||||
|
|
||||||
if description:
|
if description:
|
||||||
description += f" | Duration: {duration_text}"
|
description += f" | Duration: {duration_text}"
|
||||||
|
|
@ -582,10 +647,10 @@ class ConnectorService:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": webpage_url
|
"url": webpage_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -601,7 +666,14 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, extension_chunks
|
return result_object, extension_chunks
|
||||||
|
|
||||||
async def search_youtube(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_youtube(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for YouTube videos and return both the source information and langchain documents
|
Search for YouTube videos and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -620,7 +692,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="YOUTUBE_VIDEO"
|
document_type="YOUTUBE_VIDEO",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
youtube_chunks = await self.document_retriever.hybrid_search(
|
youtube_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -628,7 +700,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="YOUTUBE_VIDEO"
|
document_type="YOUTUBE_VIDEO",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
youtube_chunks = self._transform_document_results(youtube_chunks)
|
youtube_chunks = self._transform_document_results(youtube_chunks)
|
||||||
|
|
@ -647,13 +719,13 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(youtube_chunks):
|
for _i, chunk in enumerate(youtube_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Extract YouTube-specific metadata
|
# Extract YouTube-specific metadata
|
||||||
video_title = metadata.get('video_title', 'Untitled Video')
|
video_title = metadata.get("video_title", "Untitled Video")
|
||||||
video_id = metadata.get('video_id', '')
|
video_id = metadata.get("video_id", "")
|
||||||
channel_name = metadata.get('channel_name', '')
|
channel_name = metadata.get("channel_name", "")
|
||||||
# published_date = metadata.get('published_date', '')
|
# published_date = metadata.get('published_date', '')
|
||||||
|
|
||||||
# Create a more descriptive title for YouTube videos
|
# Create a more descriptive title for YouTube videos
|
||||||
|
|
@ -662,7 +734,9 @@ class ConnectorService:
|
||||||
title += f" - {channel_name}"
|
title += f" - {channel_name}"
|
||||||
|
|
||||||
# Create a more descriptive description for YouTube videos
|
# Create a more descriptive description for YouTube videos
|
||||||
description = metadata.get('description', chunk.get('content', '')[:100])
|
description = metadata.get(
|
||||||
|
"description", chunk.get("content", "")[:100]
|
||||||
|
)
|
||||||
if len(description) == 100:
|
if len(description) == 100:
|
||||||
description += "..."
|
description += "..."
|
||||||
|
|
||||||
|
|
@ -670,12 +744,12 @@ class ConnectorService:
|
||||||
url = f"https://www.youtube.com/watch?v={video_id}" if video_id else ""
|
url = f"https://www.youtube.com/watch?v={video_id}" if video_id else ""
|
||||||
|
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": url,
|
"url": url,
|
||||||
"video_id": video_id, # Additional field for YouTube videos
|
"video_id": video_id, # Additional field for YouTube videos
|
||||||
"channel_name": channel_name # Additional field for YouTube videos
|
"channel_name": channel_name, # Additional field for YouTube videos
|
||||||
}
|
}
|
||||||
|
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -691,7 +765,14 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, youtube_chunks
|
return result_object, youtube_chunks
|
||||||
|
|
||||||
async def search_github(self, user_query: str, user_id: int, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_github(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: int,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for GitHub documents and return both the source information and langchain documents
|
Search for GitHub documents and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -704,7 +785,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="GITHUB_CONNECTOR"
|
document_type="GITHUB_CONNECTOR",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
github_chunks = await self.document_retriever.hybrid_search(
|
github_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -712,7 +793,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="GITHUB_CONNECTOR"
|
document_type="GITHUB_CONNECTOR",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
github_chunks = self._transform_document_results(github_chunks)
|
github_chunks = self._transform_document_results(github_chunks)
|
||||||
|
|
@ -731,15 +812,19 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(github_chunks):
|
for _i, chunk in enumerate(github_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Create a source entry
|
# Create a source entry
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": document.get('title', 'GitHub Document'), # Use specific title if available
|
"title": document.get(
|
||||||
"description": metadata.get('description', chunk.get('content', '')[:100]), # Use description or content preview
|
"title", "GitHub Document"
|
||||||
"url": metadata.get('url', '') # Use URL if available in metadata
|
), # Use specific title if available
|
||||||
|
"description": metadata.get(
|
||||||
|
"description", chunk.get("content", "")[:100]
|
||||||
|
), # Use description or content preview
|
||||||
|
"url": metadata.get("url", ""), # Use URL if available in metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -755,7 +840,14 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, github_chunks
|
return result_object, github_chunks
|
||||||
|
|
||||||
async def search_linear(self, user_query: str, user_id: str, search_space_id: int, top_k: int = 20, search_mode: SearchMode = SearchMode.CHUNKS) -> tuple:
|
async def search_linear(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search for Linear issues and comments and return both the source information and langchain documents
|
Search for Linear issues and comments and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -774,7 +866,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="LINEAR_CONNECTOR"
|
document_type="LINEAR_CONNECTOR",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
linear_chunks = await self.document_retriever.hybrid_search(
|
linear_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -782,7 +874,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="LINEAR_CONNECTOR"
|
document_type="LINEAR_CONNECTOR",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
linear_chunks = self._transform_document_results(linear_chunks)
|
linear_chunks = self._transform_document_results(linear_chunks)
|
||||||
|
|
@ -801,14 +893,14 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for _i, chunk in enumerate(linear_chunks):
|
for _i, chunk in enumerate(linear_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Extract Linear-specific metadata
|
# Extract Linear-specific metadata
|
||||||
issue_identifier = metadata.get('issue_identifier', '')
|
issue_identifier = metadata.get("issue_identifier", "")
|
||||||
issue_title = metadata.get('issue_title', 'Untitled Issue')
|
issue_title = metadata.get("issue_title", "Untitled Issue")
|
||||||
issue_state = metadata.get('state', '')
|
issue_state = metadata.get("state", "")
|
||||||
comment_count = metadata.get('comment_count', 0)
|
comment_count = metadata.get("comment_count", 0)
|
||||||
|
|
||||||
# Create a more descriptive title for Linear issues
|
# Create a more descriptive title for Linear issues
|
||||||
title = f"Linear: {issue_identifier} - {issue_title}"
|
title = f"Linear: {issue_identifier} - {issue_title}"
|
||||||
|
|
@ -816,7 +908,7 @@ class ConnectorService:
|
||||||
title += f" ({issue_state})"
|
title += f" ({issue_state})"
|
||||||
|
|
||||||
# Create a more descriptive description for Linear issues
|
# Create a more descriptive description for Linear issues
|
||||||
description = chunk.get('content', '')[:100]
|
description = chunk.get("content", "")[:100]
|
||||||
if len(description) == 100:
|
if len(description) == 100:
|
||||||
description += "..."
|
description += "..."
|
||||||
|
|
||||||
|
|
@ -835,13 +927,13 @@ class ConnectorService:
|
||||||
url = f"https://linear.app/issue/{issue_identifier}"
|
url = f"https://linear.app/issue/{issue_identifier}"
|
||||||
|
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": url,
|
"url": url,
|
||||||
"issue_identifier": issue_identifier,
|
"issue_identifier": issue_identifier,
|
||||||
"state": issue_state,
|
"state": issue_state,
|
||||||
"comment_count": comment_count
|
"comment_count": comment_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -857,7 +949,130 @@ class ConnectorService:
|
||||||
|
|
||||||
return result_object, linear_chunks
|
return result_object, linear_chunks
|
||||||
|
|
||||||
async def search_linkup(self, user_query: str, user_id: str, mode: str = "standard") -> tuple:
|
async def search_jira(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
search_mode: SearchMode = SearchMode.CHUNKS,
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Search for Jira issues and comments 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
|
||||||
|
search_mode: Search mode (CHUNKS or DOCUMENTS)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (sources_info, langchain_documents)
|
||||||
|
"""
|
||||||
|
if search_mode == SearchMode.CHUNKS:
|
||||||
|
jira_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="JIRA_CONNECTOR",
|
||||||
|
)
|
||||||
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
|
jira_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="JIRA_CONNECTOR",
|
||||||
|
)
|
||||||
|
# Transform document retriever results to match expected format
|
||||||
|
jira_chunks = self._transform_document_results(jira_chunks)
|
||||||
|
|
||||||
|
# Early return if no results
|
||||||
|
if not jira_chunks:
|
||||||
|
return {
|
||||||
|
"id": 30,
|
||||||
|
"name": "Jira Issues",
|
||||||
|
"type": "JIRA_CONNECTOR",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
# Process each chunk and create sources directly without deduplication
|
||||||
|
sources_list = []
|
||||||
|
async with self.counter_lock:
|
||||||
|
for _i, chunk in enumerate(jira_chunks):
|
||||||
|
# Extract document metadata
|
||||||
|
document = chunk.get("document", {})
|
||||||
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
|
# Extract Jira-specific metadata
|
||||||
|
issue_key = metadata.get("issue_key", "")
|
||||||
|
issue_title = metadata.get("issue_title", "Untitled Issue")
|
||||||
|
status = metadata.get("status", "")
|
||||||
|
priority = metadata.get("priority", "")
|
||||||
|
issue_type = metadata.get("issue_type", "")
|
||||||
|
comment_count = metadata.get("comment_count", 0)
|
||||||
|
|
||||||
|
# Create a more descriptive title for Jira issues
|
||||||
|
title = f"Jira: {issue_key} - {issue_title}"
|
||||||
|
if status:
|
||||||
|
title += f" ({status})"
|
||||||
|
|
||||||
|
# Create a more descriptive description for Jira issues
|
||||||
|
description = chunk.get("content", "")[:100]
|
||||||
|
if len(description) == 100:
|
||||||
|
description += "..."
|
||||||
|
|
||||||
|
# Add priority and type info to description
|
||||||
|
info_parts = []
|
||||||
|
if priority:
|
||||||
|
info_parts.append(f"Priority: {priority}")
|
||||||
|
if issue_type:
|
||||||
|
info_parts.append(f"Type: {issue_type}")
|
||||||
|
if comment_count:
|
||||||
|
info_parts.append(f"Comments: {comment_count}")
|
||||||
|
|
||||||
|
if info_parts:
|
||||||
|
if description:
|
||||||
|
description += f" | {' | '.join(info_parts)}"
|
||||||
|
else:
|
||||||
|
description = " | ".join(info_parts)
|
||||||
|
|
||||||
|
# For URL, we could construct a URL to the Jira issue if we have the base URL
|
||||||
|
# For now, use a generic placeholder
|
||||||
|
url = ""
|
||||||
|
if issue_key and metadata.get("base_url"):
|
||||||
|
url = f"{metadata.get('base_url')}/browse/{issue_key}"
|
||||||
|
|
||||||
|
source = {
|
||||||
|
"id": document.get("id", self.source_id_counter),
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"url": url,
|
||||||
|
"issue_key": issue_key,
|
||||||
|
"status": status,
|
||||||
|
"priority": priority,
|
||||||
|
"issue_type": issue_type,
|
||||||
|
"comment_count": comment_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.source_id_counter += 1
|
||||||
|
sources_list.append(source)
|
||||||
|
|
||||||
|
# Create result object
|
||||||
|
result_object = {
|
||||||
|
"id": 10, # Assign a unique ID for the Jira connector
|
||||||
|
"name": "Jira Issues",
|
||||||
|
"type": "JIRA_CONNECTOR",
|
||||||
|
"sources": sources_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result_object, jira_chunks
|
||||||
|
|
||||||
|
async def search_linkup(
|
||||||
|
self, user_query: str, user_id: str, mode: str = "standard"
|
||||||
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Search using Linkup API and return both the source information and documents
|
Search using Linkup API and return both the source information and documents
|
||||||
|
|
||||||
|
|
@ -870,7 +1085,9 @@ class ConnectorService:
|
||||||
tuple: (sources_info, documents)
|
tuple: (sources_info, documents)
|
||||||
"""
|
"""
|
||||||
# Get Linkup connector configuration
|
# Get Linkup connector configuration
|
||||||
linkup_connector = await self.get_connector_by_type(user_id, SearchSourceConnectorType.LINKUP_API)
|
linkup_connector = await self.get_connector_by_type(
|
||||||
|
user_id, SearchSourceConnectorType.LINKUP_API
|
||||||
|
)
|
||||||
|
|
||||||
if not linkup_connector:
|
if not linkup_connector:
|
||||||
# Return empty results if no Linkup connector is configured
|
# Return empty results if no Linkup connector is configured
|
||||||
|
|
@ -894,7 +1111,7 @@ class ConnectorService:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract results from Linkup response - access as attribute instead of using .get()
|
# Extract results from Linkup response - access as attribute instead of using .get()
|
||||||
linkup_results = response.results if hasattr(response, 'results') else []
|
linkup_results = response.results if hasattr(response, "results") else []
|
||||||
|
|
||||||
# Only proceed if we have results
|
# Only proceed if we have results
|
||||||
if not linkup_results:
|
if not linkup_results:
|
||||||
|
|
@ -912,33 +1129,41 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for i, result in enumerate(linkup_results):
|
for i, result in enumerate(linkup_results):
|
||||||
# Only process results that have content
|
# Only process results that have content
|
||||||
if not hasattr(result, 'content') or not result.content:
|
if not hasattr(result, "content") or not result.content:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Create a source entry
|
# Create a source entry
|
||||||
source = {
|
source = {
|
||||||
"id": self.source_id_counter,
|
"id": self.source_id_counter,
|
||||||
"title": result.name if hasattr(result, 'name') else "Linkup Result",
|
"title": (
|
||||||
"description": result.content[:100] if hasattr(result, 'content') else "",
|
result.name if hasattr(result, "name") else "Linkup Result"
|
||||||
"url": result.url if hasattr(result, 'url') else ""
|
),
|
||||||
|
"description": (
|
||||||
|
result.content[:100] if hasattr(result, "content") else ""
|
||||||
|
),
|
||||||
|
"url": result.url if hasattr(result, "url") else "",
|
||||||
}
|
}
|
||||||
sources_list.append(source)
|
sources_list.append(source)
|
||||||
|
|
||||||
# Create a document entry
|
# Create a document entry
|
||||||
document = {
|
document = {
|
||||||
"chunk_id": f"linkup_chunk_{i}",
|
"chunk_id": f"linkup_chunk_{i}",
|
||||||
"content": result.content if hasattr(result, 'content') else "",
|
"content": result.content if hasattr(result, "content") else "",
|
||||||
"score": 1.0, # Default score since not provided by Linkup
|
"score": 1.0, # Default score since not provided by Linkup
|
||||||
"document": {
|
"document": {
|
||||||
"id": self.source_id_counter,
|
"id": self.source_id_counter,
|
||||||
"title": result.name if hasattr(result, 'name') else "Linkup Result",
|
"title": (
|
||||||
|
result.name
|
||||||
|
if hasattr(result, "name")
|
||||||
|
else "Linkup Result"
|
||||||
|
),
|
||||||
"document_type": "LINKUP_API",
|
"document_type": "LINKUP_API",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"url": result.url if hasattr(result, 'url') else "",
|
"url": result.url if hasattr(result, "url") else "",
|
||||||
"type": result.type if hasattr(result, 'type') else "",
|
"type": result.type if hasattr(result, "type") else "",
|
||||||
"source": "LINKUP_API"
|
"source": "LINKUP_API",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
documents.append(document)
|
documents.append(document)
|
||||||
self.source_id_counter += 1
|
self.source_id_counter += 1
|
||||||
|
|
@ -963,7 +1188,14 @@ class ConnectorService:
|
||||||
"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:
|
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
|
Search for Discord messages and return both the source information and langchain documents
|
||||||
|
|
||||||
|
|
@ -982,7 +1214,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="DISCORD_CONNECTOR"
|
document_type="DISCORD_CONNECTOR",
|
||||||
)
|
)
|
||||||
elif search_mode == SearchMode.DOCUMENTS:
|
elif search_mode == SearchMode.DOCUMENTS:
|
||||||
discord_chunks = await self.document_retriever.hybrid_search(
|
discord_chunks = await self.document_retriever.hybrid_search(
|
||||||
|
|
@ -990,7 +1222,7 @@ class ConnectorService:
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
document_type="DISCORD_CONNECTOR"
|
document_type="DISCORD_CONNECTOR",
|
||||||
)
|
)
|
||||||
# Transform document retriever results to match expected format
|
# Transform document retriever results to match expected format
|
||||||
discord_chunks = self._transform_document_results(discord_chunks)
|
discord_chunks = self._transform_document_results(discord_chunks)
|
||||||
|
|
@ -1009,13 +1241,13 @@ class ConnectorService:
|
||||||
async with self.counter_lock:
|
async with self.counter_lock:
|
||||||
for i, chunk in enumerate(discord_chunks):
|
for i, chunk in enumerate(discord_chunks):
|
||||||
# Extract document metadata
|
# Extract document metadata
|
||||||
document = chunk.get('document', {})
|
document = chunk.get("document", {})
|
||||||
metadata = document.get('metadata', {})
|
metadata = document.get("metadata", {})
|
||||||
|
|
||||||
# Create a mapped source entry with Discord-specific metadata
|
# Create a mapped source entry with Discord-specific metadata
|
||||||
channel_name = metadata.get('channel_name', 'Unknown Channel')
|
channel_name = metadata.get("channel_name", "Unknown Channel")
|
||||||
channel_id = metadata.get('channel_id', '')
|
channel_id = metadata.get("channel_id", "")
|
||||||
message_date = metadata.get('start_date', '')
|
message_date = metadata.get("start_date", "")
|
||||||
|
|
||||||
# Create a more descriptive title for Discord messages
|
# Create a more descriptive title for Discord messages
|
||||||
title = f"Discord: {channel_name}"
|
title = f"Discord: {channel_name}"
|
||||||
|
|
@ -1023,12 +1255,12 @@ class ConnectorService:
|
||||||
title += f" ({message_date})"
|
title += f" ({message_date})"
|
||||||
|
|
||||||
# Create a more descriptive description for Discord messages
|
# Create a more descriptive description for Discord messages
|
||||||
description = chunk.get('content', '')[:100]
|
description = chunk.get("content", "")[:100]
|
||||||
if len(description) == 100:
|
if len(description) == 100:
|
||||||
description += "..."
|
description += "..."
|
||||||
|
|
||||||
url = ""
|
url = ""
|
||||||
guild_id = metadata.get('guild_id', '')
|
guild_id = metadata.get("guild_id", "")
|
||||||
if guild_id and channel_id:
|
if guild_id and channel_id:
|
||||||
url = f"https://discord.com/channels/{guild_id}/{channel_id}"
|
url = f"https://discord.com/channels/{guild_id}/{channel_id}"
|
||||||
elif channel_id:
|
elif channel_id:
|
||||||
|
|
@ -1036,7 +1268,7 @@ class ConnectorService:
|
||||||
url = f"https://discord.com/channels/@me/{channel_id}"
|
url = f"https://discord.com/channels/@me/{channel_id}"
|
||||||
|
|
||||||
source = {
|
source = {
|
||||||
"id": document.get('id', self.source_id_counter),
|
"id": document.get("id", self.source_id_counter),
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
|
@ -1054,5 +1286,3 @@ class ConnectorService:
|
||||||
}
|
}
|
||||||
|
|
||||||
return result_object, discord_chunks
|
return result_object, discord_chunks
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,12 +9,12 @@ import { ArrowLeft, Check, Loader2, Github } from "lucide-react";
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
// Import Utils, Types, Hook, and Components
|
// Import Utils, Types, Hook, and Components
|
||||||
|
|
@ -27,201 +27,227 @@ import { EditSimpleTokenForm } from "@/components/editConnector/EditSimpleTokenF
|
||||||
import { getConnectorIcon } from "@/components/chat";
|
import { getConnectorIcon } from "@/components/chat";
|
||||||
|
|
||||||
export default function EditConnectorPage() {
|
export default function EditConnectorPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const searchSpaceId = params.search_space_id as string;
|
const searchSpaceId = params.search_space_id as string;
|
||||||
// Ensure connectorId is parsed safely
|
// Ensure connectorId is parsed safely
|
||||||
const connectorIdParam = params.connector_id as string;
|
const connectorIdParam = params.connector_id as string;
|
||||||
const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN;
|
const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN;
|
||||||
|
|
||||||
// Use the custom hook to manage state and logic
|
// Use the custom hook to manage state and logic
|
||||||
const {
|
const {
|
||||||
connectorsLoading,
|
connectorsLoading,
|
||||||
connector,
|
connector,
|
||||||
isSaving,
|
isSaving,
|
||||||
editForm,
|
editForm,
|
||||||
patForm, // Needed for GitHub child component
|
patForm, // Needed for GitHub child component
|
||||||
handleSaveChanges,
|
handleSaveChanges,
|
||||||
// GitHub specific props for the child component
|
// GitHub specific props for the child component
|
||||||
editMode,
|
editMode,
|
||||||
setEditMode, // Pass down if needed by GitHub component
|
setEditMode, // Pass down if needed by GitHub component
|
||||||
originalPat,
|
originalPat,
|
||||||
currentSelectedRepos,
|
currentSelectedRepos,
|
||||||
fetchedRepos,
|
fetchedRepos,
|
||||||
setFetchedRepos,
|
setFetchedRepos,
|
||||||
newSelectedRepos,
|
newSelectedRepos,
|
||||||
setNewSelectedRepos,
|
setNewSelectedRepos,
|
||||||
isFetchingRepos,
|
isFetchingRepos,
|
||||||
handleFetchRepositories,
|
handleFetchRepositories,
|
||||||
handleRepoSelectionChange,
|
handleRepoSelectionChange,
|
||||||
} = useConnectorEditPage(connectorId, searchSpaceId);
|
} = useConnectorEditPage(connectorId, searchSpaceId);
|
||||||
|
|
||||||
// Redirect if connectorId is not a valid number after parsing
|
// Redirect if connectorId is not a valid number after parsing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNaN(connectorId)) {
|
if (isNaN(connectorId)) {
|
||||||
toast.error("Invalid Connector ID.");
|
toast.error("Invalid Connector ID.");
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
}
|
}
|
||||||
}, [connectorId, router, searchSpaceId]);
|
}, [connectorId, router, searchSpaceId]);
|
||||||
|
|
||||||
// Loading State
|
// Loading State
|
||||||
if (connectorsLoading || !connector) {
|
if (connectorsLoading || !connector) {
|
||||||
// Handle NaN case before showing skeleton
|
// Handle NaN case before showing skeleton
|
||||||
if (isNaN(connectorId)) return null;
|
if (isNaN(connectorId)) return null;
|
||||||
return <EditConnectorLoadingSkeleton />;
|
return <EditConnectorLoadingSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main Render using data/handlers from the hook
|
// Main Render using data/handlers from the hook
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
<div className="container mx-auto py-8 max-w-3xl">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="mb-6"
|
className="mb-6"
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Connectors
|
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Connectors
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<Card className="border-2 border-border">
|
<Card className="border-2 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
{getConnectorIcon(connector.connector_type)}
|
{getConnectorIcon(connector.connector_type)}
|
||||||
Edit {getConnectorTypeDisplay(connector.connector_type)} Connector
|
Edit {getConnectorTypeDisplay(connector.connector_type)} Connector
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Modify connector name and configuration.
|
Modify connector name and configuration.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<Form {...editForm}>
|
<Form {...editForm}>
|
||||||
{/* Pass hook's handleSaveChanges */}
|
{/* Pass hook's handleSaveChanges */}
|
||||||
<form
|
<form
|
||||||
onSubmit={editForm.handleSubmit(handleSaveChanges)}
|
onSubmit={editForm.handleSubmit(handleSaveChanges)}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Pass form control from hook */}
|
{/* Pass form control from hook */}
|
||||||
<EditConnectorNameForm control={editForm.control} />
|
<EditConnectorNameForm control={editForm.control} />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold">Configuration</h3>
|
<h3 className="text-lg font-semibold">Configuration</h3>
|
||||||
|
|
||||||
{/* == GitHub == */}
|
{/* == GitHub == */}
|
||||||
{connector.connector_type === "GITHUB_CONNECTOR" && (
|
{connector.connector_type === "GITHUB_CONNECTOR" && (
|
||||||
<EditGitHubConnectorConfig
|
<EditGitHubConnectorConfig
|
||||||
// Pass relevant state and handlers from hook
|
// Pass relevant state and handlers from hook
|
||||||
editMode={editMode}
|
editMode={editMode}
|
||||||
setEditMode={setEditMode} // Pass setter if child manages mode
|
setEditMode={setEditMode} // Pass setter if child manages mode
|
||||||
originalPat={originalPat}
|
originalPat={originalPat}
|
||||||
currentSelectedRepos={currentSelectedRepos}
|
currentSelectedRepos={currentSelectedRepos}
|
||||||
fetchedRepos={fetchedRepos}
|
fetchedRepos={fetchedRepos}
|
||||||
newSelectedRepos={newSelectedRepos}
|
newSelectedRepos={newSelectedRepos}
|
||||||
isFetchingRepos={isFetchingRepos}
|
isFetchingRepos={isFetchingRepos}
|
||||||
patForm={patForm}
|
patForm={patForm}
|
||||||
handleFetchRepositories={handleFetchRepositories}
|
handleFetchRepositories={handleFetchRepositories}
|
||||||
handleRepoSelectionChange={handleRepoSelectionChange}
|
handleRepoSelectionChange={handleRepoSelectionChange}
|
||||||
setNewSelectedRepos={setNewSelectedRepos}
|
setNewSelectedRepos={setNewSelectedRepos}
|
||||||
setFetchedRepos={setFetchedRepos}
|
setFetchedRepos={setFetchedRepos}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* == Slack == */}
|
{/* == Slack == */}
|
||||||
{connector.connector_type === "SLACK_CONNECTOR" && (
|
{connector.connector_type === "SLACK_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="SLACK_BOT_TOKEN"
|
fieldName="SLACK_BOT_TOKEN"
|
||||||
fieldLabel="Slack Bot Token"
|
fieldLabel="Slack Bot Token"
|
||||||
fieldDescription="Update the Slack Bot Token if needed."
|
fieldDescription="Update the Slack Bot Token if needed."
|
||||||
placeholder="Begins with xoxb-..."
|
placeholder="Begins with xoxb-..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* == Notion == */}
|
{/* == Notion == */}
|
||||||
{connector.connector_type === "NOTION_CONNECTOR" && (
|
{connector.connector_type === "NOTION_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="NOTION_INTEGRATION_TOKEN"
|
fieldName="NOTION_INTEGRATION_TOKEN"
|
||||||
fieldLabel="Notion Integration Token"
|
fieldLabel="Notion Integration Token"
|
||||||
fieldDescription="Update the Notion Integration Token if needed."
|
fieldDescription="Update the Notion Integration Token if needed."
|
||||||
placeholder="Begins with secret_..."
|
placeholder="Begins with secret_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* == Serper == */}
|
{/* == Serper == */}
|
||||||
{connector.connector_type === "SERPER_API" && (
|
{connector.connector_type === "SERPER_API" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="SERPER_API_KEY"
|
fieldName="SERPER_API_KEY"
|
||||||
fieldLabel="Serper API Key"
|
fieldLabel="Serper API Key"
|
||||||
fieldDescription="Update the Serper API Key if needed."
|
fieldDescription="Update the Serper API Key if needed."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* == Tavily == */}
|
{/* == Tavily == */}
|
||||||
{connector.connector_type === "TAVILY_API" && (
|
{connector.connector_type === "TAVILY_API" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="TAVILY_API_KEY"
|
fieldName="TAVILY_API_KEY"
|
||||||
fieldLabel="Tavily API Key"
|
fieldLabel="Tavily API Key"
|
||||||
fieldDescription="Update the Tavily API Key if needed."
|
fieldDescription="Update the Tavily API Key if needed."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* == Linear == */}
|
{/* == Linear == */}
|
||||||
{connector.connector_type === "LINEAR_CONNECTOR" && (
|
{connector.connector_type === "LINEAR_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="LINEAR_API_KEY"
|
fieldName="LINEAR_API_KEY"
|
||||||
fieldLabel="Linear API Key"
|
fieldLabel="Linear API Key"
|
||||||
fieldDescription="Update your Linear API Key if needed."
|
fieldDescription="Update your Linear API Key if needed."
|
||||||
placeholder="Begins with lin_api_..."
|
placeholder="Begins with lin_api_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* == Linkup == */}
|
{/* == Jira == */}
|
||||||
{connector.connector_type === "LINKUP_API" && (
|
{connector.connector_type === "JIRA_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<div className="space-y-4">
|
||||||
control={editForm.control}
|
<EditSimpleTokenForm
|
||||||
fieldName="LINKUP_API_KEY"
|
control={editForm.control}
|
||||||
fieldLabel="Linkup API Key"
|
fieldName="JIRA_BASE_URL"
|
||||||
fieldDescription="Update your Linkup API Key if needed."
|
fieldLabel="Jira Base URL"
|
||||||
placeholder="Begins with linkup_..."
|
fieldDescription="Update your Jira instance URL if needed."
|
||||||
/>
|
placeholder="https://yourcompany.atlassian.net"
|
||||||
)}
|
/>
|
||||||
|
<EditSimpleTokenForm
|
||||||
|
control={editForm.control}
|
||||||
|
fieldName="JIRA_EMAIL"
|
||||||
|
fieldLabel="Jira Email"
|
||||||
|
fieldDescription="Update your Atlassian account email if needed."
|
||||||
|
placeholder="your.email@company.com"
|
||||||
|
/>
|
||||||
|
<EditSimpleTokenForm
|
||||||
|
control={editForm.control}
|
||||||
|
fieldName="JIRA_API_TOKEN"
|
||||||
|
fieldLabel="Jira API Token"
|
||||||
|
fieldDescription="Update your Jira API Token if needed."
|
||||||
|
placeholder="Your Jira API Token"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* == Discord == */}
|
{/* == Linkup == */}
|
||||||
{connector.connector_type === "DISCORD_CONNECTOR" && (
|
{connector.connector_type === "LINKUP_API" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="DISCORD_BOT_TOKEN"
|
fieldName="LINKUP_API_KEY"
|
||||||
fieldLabel="Discord Bot Token"
|
fieldLabel="Linkup API Key"
|
||||||
fieldDescription="Update the Discord Bot Token if needed."
|
fieldDescription="Update your Linkup API Key if needed."
|
||||||
placeholder="Bot token..."
|
placeholder="Begins with linkup_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</CardContent>
|
{/* == Discord == */}
|
||||||
<CardFooter className="border-t pt-6">
|
{connector.connector_type === "DISCORD_CONNECTOR" && (
|
||||||
<Button
|
<EditSimpleTokenForm
|
||||||
type="submit"
|
control={editForm.control}
|
||||||
disabled={isSaving}
|
fieldName="DISCORD_BOT_TOKEN"
|
||||||
className="w-full sm:w-auto"
|
fieldLabel="Discord Bot Token"
|
||||||
>
|
fieldDescription="Update the Discord Bot Token if needed."
|
||||||
{isSaving ? (
|
placeholder="Bot token..."
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
/>
|
||||||
) : (
|
)}
|
||||||
<Check className="mr-2 h-4 w-4" />
|
</CardContent>
|
||||||
)}
|
<CardFooter className="border-t pt-6">
|
||||||
Save Changes
|
<Button
|
||||||
</Button>
|
type="submit"
|
||||||
</CardFooter>
|
disabled={isSaving}
|
||||||
</form>
|
className="w-full sm:w-auto"
|
||||||
</Form>
|
>
|
||||||
</Card>
|
{isSaving ? (
|
||||||
</motion.div>
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
</div>
|
) : (
|
||||||
);
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ import * as z from "zod";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
import { useSearchSourceConnectors, SearchSourceConnector } from "@/hooks/useSearchSourceConnectors";
|
import {
|
||||||
|
useSearchSourceConnectors,
|
||||||
|
SearchSourceConnector,
|
||||||
|
} from "@/hooks/useSearchSourceConnectors";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
|
@ -28,11 +31,7 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertTitle,
|
|
||||||
} from "@/components/ui/alert";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
// Define the form schema with Zod
|
||||||
const apiConnectorFormSchema = z.object({
|
const apiConnectorFormSchema = z.object({
|
||||||
|
|
@ -47,13 +46,15 @@ const apiConnectorFormSchema = z.object({
|
||||||
// Helper function to get connector type display name
|
// Helper function to get connector type display name
|
||||||
const getConnectorTypeDisplay = (type: string): string => {
|
const getConnectorTypeDisplay = (type: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
"SERPER_API": "Serper API",
|
SERPER_API: "Serper API",
|
||||||
"TAVILY_API": "Tavily API",
|
TAVILY_API: "Tavily API",
|
||||||
"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",
|
LINEAR_CONNECTOR: "Linear Connector",
|
||||||
"LINKUP_API": "Linkup",
|
JIRA_CONNECTOR: "Jira Connector",
|
||||||
|
DISCORD_CONNECTOR: "Discord Connector",
|
||||||
|
LINKUP_API: "Linkup",
|
||||||
// Add other connector types here as needed
|
// Add other connector types here as needed
|
||||||
};
|
};
|
||||||
return typeMap[type] || type;
|
return typeMap[type] || type;
|
||||||
|
|
@ -69,7 +70,9 @@ export default function EditConnectorPage() {
|
||||||
const connectorId = parseInt(params.connector_id as string, 10);
|
const connectorId = parseInt(params.connector_id as string, 10);
|
||||||
|
|
||||||
const { connectors, updateConnector } = useSearchSourceConnectors();
|
const { connectors, updateConnector } = useSearchSourceConnectors();
|
||||||
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
const [connector, setConnector] = useState<SearchSourceConnector | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
// console.log("connector", connector);
|
// console.log("connector", connector);
|
||||||
|
|
@ -85,20 +88,20 @@ export default function EditConnectorPage() {
|
||||||
// Get API key field name based on connector type
|
// Get API key field name based on connector type
|
||||||
const getApiKeyFieldName = (connectorType: string): string => {
|
const getApiKeyFieldName = (connectorType: string): string => {
|
||||||
const fieldMap: Record<string, string> = {
|
const fieldMap: Record<string, string> = {
|
||||||
"SERPER_API": "SERPER_API_KEY",
|
SERPER_API: "SERPER_API_KEY",
|
||||||
"TAVILY_API": "TAVILY_API_KEY",
|
TAVILY_API: "TAVILY_API_KEY",
|
||||||
"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",
|
DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN",
|
||||||
"LINKUP_API": "LINKUP_API_KEY"
|
LINKUP_API: "LINKUP_API_KEY",
|
||||||
};
|
};
|
||||||
return fieldMap[connectorType] || "";
|
return fieldMap[connectorType] || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find connector in the list
|
// Find connector in the list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentConnector = connectors.find(c => c.id === connectorId);
|
const currentConnector = connectors.find((c) => c.id === connectorId);
|
||||||
|
|
||||||
if (currentConnector) {
|
if (currentConnector) {
|
||||||
setConnector(currentConnector);
|
setConnector(currentConnector);
|
||||||
|
|
@ -150,7 +153,9 @@ export default function EditConnectorPage() {
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating connector:", error);
|
console.error("Error updating connector:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to update connector");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Failed to update connector",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -186,24 +191,30 @@ export default function EditConnectorPage() {
|
||||||
<Card className="border-2 border-border">
|
<Card className="border-2 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold">
|
<CardTitle className="text-2xl font-bold">
|
||||||
Edit {connector ? getConnectorTypeDisplay(connector.connector_type) : ""} Connector
|
Edit{" "}
|
||||||
|
{connector
|
||||||
|
? getConnectorTypeDisplay(connector.connector_type)
|
||||||
|
: ""}{" "}
|
||||||
|
Connector
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Update your connector settings.</CardDescription>
|
||||||
Update your connector settings.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Alert className="mb-6 bg-muted">
|
<Alert className="mb-6 bg-muted">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
<AlertTitle>API Key Security</AlertTitle>
|
<AlertTitle>API Key Security</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Your API key is stored securely. For security reasons, we don't display your existing API key.
|
Your API key is stored securely. For security reasons, we don't
|
||||||
If you don't update the API key field, your existing key will be preserved.
|
display your existing API key. If you don't update the API key
|
||||||
|
field, your existing key will be preserved.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
|
|
@ -245,7 +256,8 @@ export default function EditConnectorPage() {
|
||||||
? "Enter new Slack Bot Token (optional)"
|
? "Enter new Slack Bot Token (optional)"
|
||||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
: connector?.connector_type === "NOTION_CONNECTOR"
|
||||||
? "Enter new Notion Token (optional)"
|
? "Enter new Notion Token (optional)"
|
||||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
: connector?.connector_type ===
|
||||||
|
"GITHUB_CONNECTOR"
|
||||||
? "Enter new GitHub PAT (optional)"
|
? "Enter new GitHub PAT (optional)"
|
||||||
: connector?.connector_type === "LINKUP_API"
|
: connector?.connector_type === "LINKUP_API"
|
||||||
? "Enter new Linkup API Key (optional)"
|
? "Enter new Linkup API Key (optional)"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,472 @@
|
||||||
|
"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 jiraConnectorFormSchema = z.object({
|
||||||
|
name: z.string().min(3, {
|
||||||
|
message: "Connector name must be at least 3 characters.",
|
||||||
|
}),
|
||||||
|
base_url: z
|
||||||
|
.string()
|
||||||
|
.url({
|
||||||
|
message:
|
||||||
|
"Please enter a valid Jira URL (e.g., https://yourcompany.atlassian.net)",
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(url) => {
|
||||||
|
return url.includes("atlassian.net") || url.includes("jira");
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Please enter a valid Jira instance URL",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
email: z.string().email({
|
||||||
|
message: "Please enter a valid email address.",
|
||||||
|
}),
|
||||||
|
api_token: z.string().min(10, {
|
||||||
|
message: "Jira API Token is required and must be valid.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the type for the form values
|
||||||
|
type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>;
|
||||||
|
|
||||||
|
export default function JiraConnectorPage() {
|
||||||
|
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<JiraConnectorFormValues>({
|
||||||
|
resolver: zodResolver(jiraConnectorFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "Jira Connector",
|
||||||
|
base_url: "",
|
||||||
|
email: "",
|
||||||
|
api_token: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const onSubmit = async (values: JiraConnectorFormValues) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await createConnector({
|
||||||
|
name: values.name,
|
||||||
|
connector_type: "JIRA_CONNECTOR",
|
||||||
|
config: {
|
||||||
|
JIRA_BASE_URL: values.base_url,
|
||||||
|
JIRA_EMAIL: values.email,
|
||||||
|
JIRA_API_TOKEN: values.api_token,
|
||||||
|
},
|
||||||
|
is_indexable: true,
|
||||||
|
last_indexed_at: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Jira connector created successfully!");
|
||||||
|
|
||||||
|
// Navigate back to connectors page
|
||||||
|
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 Jira Instance
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Integrate with Jira to search and retrieve information from
|
||||||
|
your issues, tickets, and comments. This connector can index
|
||||||
|
your Jira content for search.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Alert className="mb-6 bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>Jira Personal Access Token Required</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
You'll need a Jira Personal Access Token to use this
|
||||||
|
connector. You can create one from{" "}
|
||||||
|
<a
|
||||||
|
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Atlassian Account Settings
|
||||||
|
</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 Jira Connector" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
A friendly name to identify this connector.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="base_url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Jira Instance URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://yourcompany.atlassian.net"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Jira instance URL. For Atlassian Cloud, this is
|
||||||
|
typically https://yourcompany.atlassian.net
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="your.email@company.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Atlassian account email address.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Your Jira API Token"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your Jira API Token will be encrypted and stored securely.
|
||||||
|
</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 Jira
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</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 Jira integration:
|
||||||
|
</h4>
|
||||||
|
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||||
|
<li>Search through all your Jira issues and tickets</li>
|
||||||
|
<li>
|
||||||
|
Access issue descriptions, comments, and full discussion
|
||||||
|
threads
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Connect your team's project management directly to your
|
||||||
|
search space
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Keep your search results up-to-date with latest Jira content
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Index your Jira issues for enhanced search capabilities
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Search by issue keys, status, priority, and assignee
|
||||||
|
information
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="documentation">
|
||||||
|
<Card className="border-2 border-border">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold">
|
||||||
|
Jira Connector Documentation
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Learn how to set up and use the Jira connector to index your
|
||||||
|
project management 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 Jira connector uses the Jira REST API with Basic Authentication
|
||||||
|
to fetch all issues and comments that your account has
|
||||||
|
access to within your Jira instance.
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
For follow up indexing runs, the connector retrieves
|
||||||
|
issues and comments that have been updated since the last
|
||||||
|
indexing attempt.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Indexing is configured to run periodically, so updates
|
||||||
|
should appear in your search results within minutes.
|
||||||
|
</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>Read-Only Access is Sufficient</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
You only need read access for this connector to work.
|
||||||
|
The API Token will only be used to read your Jira data.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">
|
||||||
|
Step 1: Create an API Token
|
||||||
|
</h4>
|
||||||
|
<ol className="list-decimal pl-5 space-y-3">
|
||||||
|
<li>Log in to your Atlassian account</li>
|
||||||
|
<li>
|
||||||
|
Navigate to{" "}
|
||||||
|
<a
|
||||||
|
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
https://id.atlassian.com/manage-profile/security/api-tokens
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click <strong>Create API token</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Enter a label for your token (like "SurfSense
|
||||||
|
Connector")
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click <strong>Create</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Copy the generated token as it will only be shown
|
||||||
|
once
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">
|
||||||
|
Step 2: Grant necessary access
|
||||||
|
</h4>
|
||||||
|
<p className="text-muted-foreground mb-3">
|
||||||
|
The API Token will have access to all projects and
|
||||||
|
issues that your user account can see. Make sure your
|
||||||
|
account has appropriate permissions for the projects
|
||||||
|
you want to index.
|
||||||
|
</p>
|
||||||
|
<Alert className="bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>Data Privacy</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Only issues, comments, and basic metadata will be
|
||||||
|
indexed. Jira attachments and linked files are not
|
||||||
|
indexed by this connector.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>Jira</strong> Connector.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Enter your <strong>Jira Instance URL</strong> (e.g.,
|
||||||
|
https://yourcompany.atlassian.net)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Place your <strong>Personal Access Token</strong> in
|
||||||
|
the form field.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click <strong>Connect</strong> to establish the
|
||||||
|
connection.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Once connected, your Jira issues will be indexed
|
||||||
|
automatically.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<Alert className="bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>What Gets Indexed</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<p className="mb-2">
|
||||||
|
The Jira connector indexes the following data:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
<li>Issue keys and summaries (e.g., PROJ-123)</li>
|
||||||
|
<li>Issue descriptions</li>
|
||||||
|
<li>Issue comments and discussion threads</li>
|
||||||
|
<li>
|
||||||
|
Issue status, priority, and type information
|
||||||
|
</li>
|
||||||
|
<li>Assignee and reporter information</li>
|
||||||
|
<li>Project information</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
import {
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
IconBrandDiscord,
|
IconBrandDiscord,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
|
|
@ -67,23 +76,26 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
{
|
{
|
||||||
id: "slack-connector",
|
id: "slack-connector",
|
||||||
title: "Slack",
|
title: "Slack",
|
||||||
description: "Connect to your Slack workspace to access messages and channels.",
|
description:
|
||||||
|
"Connect to your Slack workspace to access messages and channels.",
|
||||||
icon: <IconBrandSlack className="h-6 w-6" />,
|
icon: <IconBrandSlack className="h-6 w-6" />,
|
||||||
status: "available",
|
status: "available",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ms-teams",
|
id: "ms-teams",
|
||||||
title: "Microsoft Teams",
|
title: "Microsoft Teams",
|
||||||
description: "Connect to Microsoft Teams to access your team's conversations.",
|
description:
|
||||||
|
"Connect to Microsoft Teams to access your team's conversations.",
|
||||||
icon: <IconBrandWindows className="h-6 w-6" />,
|
icon: <IconBrandWindows className="h-6 w-6" />,
|
||||||
status: "coming-soon",
|
status: "coming-soon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "discord-connector",
|
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: "available"
|
status: "available",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -94,16 +106,18 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
{
|
{
|
||||||
id: "linear-connector",
|
id: "linear-connector",
|
||||||
title: "Linear",
|
title: "Linear",
|
||||||
description: "Connect to Linear to search issues, comments and project data.",
|
description:
|
||||||
|
"Connect to Linear to search issues, comments and project data.",
|
||||||
icon: <IconLayoutKanban className="h-6 w-6" />,
|
icon: <IconLayoutKanban className="h-6 w-6" />,
|
||||||
status: "available",
|
status: "available",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "jira-connector",
|
id: "jira-connector",
|
||||||
title: "Jira",
|
title: "Jira",
|
||||||
description: "Connect to Jira to search issues, tickets and project data.",
|
description:
|
||||||
|
"Connect to Jira to search issues, tickets and project data.",
|
||||||
icon: <IconTicket className="h-6 w-6" />,
|
icon: <IconTicket className="h-6 w-6" />,
|
||||||
status: "coming-soon",
|
status: "available",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -114,14 +128,16 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
{
|
{
|
||||||
id: "notion-connector",
|
id: "notion-connector",
|
||||||
title: "Notion",
|
title: "Notion",
|
||||||
description: "Connect to your Notion workspace to access pages and databases.",
|
description:
|
||||||
|
"Connect to your Notion workspace to access pages and databases.",
|
||||||
icon: <IconBrandNotion className="h-6 w-6" />,
|
icon: <IconBrandNotion className="h-6 w-6" />,
|
||||||
status: "available",
|
status: "available",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "github-connector",
|
id: "github-connector",
|
||||||
title: "GitHub",
|
title: "GitHub",
|
||||||
description: "Connect a GitHub PAT to index code and docs from accessible repositories.",
|
description:
|
||||||
|
"Connect a GitHub PAT to index code and docs from accessible repositories.",
|
||||||
icon: <IconBrandGithub className="h-6 w-6" />,
|
icon: <IconBrandGithub className="h-6 w-6" />,
|
||||||
status: "available",
|
status: "available",
|
||||||
},
|
},
|
||||||
|
|
@ -141,7 +157,8 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
{
|
{
|
||||||
id: "zoom",
|
id: "zoom",
|
||||||
title: "Zoom",
|
title: "Zoom",
|
||||||
description: "Connect to Zoom to access meeting recordings and transcripts.",
|
description:
|
||||||
|
"Connect to Zoom to access meeting recordings and transcripts.",
|
||||||
icon: <IconBrandZoom className="h-6 w-6" />,
|
icon: <IconBrandZoom className="h-6 w-6" />,
|
||||||
status: "coming-soon",
|
status: "coming-soon",
|
||||||
},
|
},
|
||||||
|
|
@ -152,7 +169,7 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const fadeIn = {
|
const fadeIn = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: { opacity: 1, transition: { duration: 0.4 } }
|
visible: { opacity: 1, transition: { duration: 0.4 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const staggerContainer = {
|
const staggerContainer = {
|
||||||
|
|
@ -160,9 +177,9 @@ const staggerContainer = {
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
staggerChildren: 0.1
|
staggerChildren: 0.1,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardVariants = {
|
const cardVariants = {
|
||||||
|
|
@ -173,30 +190,36 @@ const cardVariants = {
|
||||||
transition: {
|
transition: {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 20
|
damping: 20,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
hover: {
|
hover: {
|
||||||
scale: 1.02,
|
scale: 1.02,
|
||||||
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
boxShadow:
|
||||||
|
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
||||||
transition: {
|
transition: {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 400,
|
stiffness: 400,
|
||||||
damping: 10
|
damping: 10,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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", "team-chats"]);
|
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) =>
|
||||||
prev.includes(categoryId)
|
prev.includes(categoryId)
|
||||||
? prev.filter(id => id !== categoryId)
|
? prev.filter((id) => id !== categoryId)
|
||||||
: [...prev, categoryId]
|
: [...prev, categoryId],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -207,7 +230,7 @@ export default function ConnectorsPage() {
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
ease: [0.22, 1, 0.36, 1]
|
ease: [0.22, 1, 0.36, 1],
|
||||||
}}
|
}}
|
||||||
className="mb-12 text-center"
|
className="mb-12 text-center"
|
||||||
>
|
>
|
||||||
|
|
@ -215,7 +238,8 @@ export default function ConnectorsPage() {
|
||||||
Connect Your Tools
|
Connect Your Tools
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-3 text-lg max-w-2xl mx-auto">
|
<p className="text-muted-foreground mt-3 text-lg max-w-2xl mx-auto">
|
||||||
Integrate with your favorite services to enhance your research capabilities.
|
Integrate with your favorite services to enhance your research
|
||||||
|
capabilities.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
@ -239,9 +263,17 @@ export default function ConnectorsPage() {
|
||||||
<div className="flex items-center justify-between space-x-4 p-4">
|
<div className="flex items-center justify-between space-x-4 p-4">
|
||||||
<h3 className="text-xl font-semibold">{category.title}</h3>
|
<h3 className="text-xl font-semibold">{category.title}</h3>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-9 p-0 hover:bg-muted"
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: expandedCategories.includes(category.id) ? 180 : 0 }}
|
animate={{
|
||||||
|
rotate: expandedCategories.includes(category.id)
|
||||||
|
? 180
|
||||||
|
: 0,
|
||||||
|
}}
|
||||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
>
|
>
|
||||||
<IconChevronDown className="h-5 w-5" />
|
<IconChevronDown className="h-5 w-5" />
|
||||||
|
|
@ -279,14 +311,22 @@ export default function ConnectorsPage() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-medium">{connector.title}</h3>
|
<h3 className="font-medium">
|
||||||
|
{connector.title}
|
||||||
|
</h3>
|
||||||
{connector.status === "coming-soon" && (
|
{connector.status === "coming-soon" && (
|
||||||
<Badge variant="outline" className="text-xs bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border-amber-200 dark:border-amber-800">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border-amber-200 dark:border-amber-800"
|
||||||
|
>
|
||||||
Coming soon
|
Coming soon
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{connector.status === "connected" && (
|
{connector.status === "connected" && (
|
||||||
<Badge variant="outline" className="text-xs bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800"
|
||||||
|
>
|
||||||
Connected
|
Connected
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -301,28 +341,45 @@ export default function ConnectorsPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="mt-auto pt-2">
|
<CardFooter className="mt-auto pt-2">
|
||||||
{connector.status === 'available' && (
|
{connector.status === "available" && (
|
||||||
<Link href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`} className="w-full">
|
<Link
|
||||||
<Button variant="default" className="w-full group">
|
href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="w-full group"
|
||||||
|
>
|
||||||
<span>Connect</span>
|
<span>Connect</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
initial={{ x: 0 }}
|
initial={{ x: 0 }}
|
||||||
whileHover={{ x: 3 }}
|
whileHover={{ x: 3 }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 10,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconChevronRight className="h-4 w-4" />
|
<IconChevronRight className="h-4 w-4" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{connector.status === 'coming-soon' && (
|
{connector.status === "coming-soon" && (
|
||||||
<Button variant="outline" disabled className="w-full opacity-70">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled
|
||||||
|
className="w-full opacity-70"
|
||||||
|
>
|
||||||
Coming Soon
|
Coming Soon
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{connector.status === 'connected' && (
|
{connector.status === "connected" && (
|
||||||
<Button variant="outline" className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950"
|
||||||
|
>
|
||||||
Manage
|
Manage
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Plus,
|
Plus,
|
||||||
|
|
@ -12,77 +12,98 @@ import {
|
||||||
Webhook,
|
Webhook,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
FileText,
|
FileText,
|
||||||
} from 'lucide-react';
|
} from "lucide-react";
|
||||||
import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban, IconLinkPlus, IconBrandDiscord } from "@tabler/icons-react";
|
import {
|
||||||
import { Button } from '@/components/ui/button';
|
IconBrandNotion,
|
||||||
import { Connector, ResearchMode } from './types';
|
IconBrandSlack,
|
||||||
|
IconBrandYoutube,
|
||||||
|
IconBrandGithub,
|
||||||
|
IconLayoutKanban,
|
||||||
|
IconLinkPlus,
|
||||||
|
IconBrandDiscord,
|
||||||
|
IconTicket,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Connector, ResearchMode } from "./types";
|
||||||
|
|
||||||
// Helper function to get connector icon
|
// Helper function to get connector icon
|
||||||
export const getConnectorIcon = (connectorType: string) => {
|
export const getConnectorIcon = (connectorType: string) => {
|
||||||
const iconProps = { className: "h-4 w-4" };
|
const iconProps = { className: "h-4 w-4" };
|
||||||
|
|
||||||
switch(connectorType) {
|
switch (connectorType) {
|
||||||
case 'LINKUP_API':
|
case "LINKUP_API":
|
||||||
return <IconLinkPlus {...iconProps} />;
|
return <IconLinkPlus {...iconProps} />;
|
||||||
case 'LINEAR_CONNECTOR':
|
case "LINEAR_CONNECTOR":
|
||||||
return <IconLayoutKanban {...iconProps} />;
|
return <IconLayoutKanban {...iconProps} />;
|
||||||
case 'GITHUB_CONNECTOR':
|
case "GITHUB_CONNECTOR":
|
||||||
return <IconBrandGithub {...iconProps} />;
|
return <IconBrandGithub {...iconProps} />;
|
||||||
case 'YOUTUBE_VIDEO':
|
case "YOUTUBE_VIDEO":
|
||||||
return <IconBrandYoutube {...iconProps} />;
|
return <IconBrandYoutube {...iconProps} />;
|
||||||
case 'CRAWLED_URL':
|
case "CRAWLED_URL":
|
||||||
return <Globe {...iconProps} />;
|
return <Globe {...iconProps} />;
|
||||||
case 'FILE':
|
case "FILE":
|
||||||
return <File {...iconProps} />;
|
return <File {...iconProps} />;
|
||||||
case 'EXTENSION':
|
case "EXTENSION":
|
||||||
return <Webhook {...iconProps} />;
|
return <Webhook {...iconProps} />;
|
||||||
case 'SERPER_API':
|
case "SERPER_API":
|
||||||
case 'TAVILY_API':
|
case "TAVILY_API":
|
||||||
return <Link {...iconProps} />;
|
return <Link {...iconProps} />;
|
||||||
case 'SLACK_CONNECTOR':
|
case "SLACK_CONNECTOR":
|
||||||
return <IconBrandSlack {...iconProps} />;
|
return <IconBrandSlack {...iconProps} />;
|
||||||
case 'NOTION_CONNECTOR':
|
case "NOTION_CONNECTOR":
|
||||||
return <IconBrandNotion {...iconProps} />;
|
return <IconBrandNotion {...iconProps} />;
|
||||||
case 'DISCORD_CONNECTOR':
|
case "DISCORD_CONNECTOR":
|
||||||
return <IconBrandDiscord {...iconProps} />;
|
return <IconBrandDiscord {...iconProps} />;
|
||||||
case 'DEEP':
|
case "JIRA_CONNECTOR":
|
||||||
|
return <IconTicket {...iconProps} />;
|
||||||
|
case "DEEP":
|
||||||
return <Sparkles {...iconProps} />;
|
return <Sparkles {...iconProps} />;
|
||||||
case 'DEEPER':
|
case "DEEPER":
|
||||||
return <Microscope {...iconProps} />;
|
return <Microscope {...iconProps} />;
|
||||||
case 'DEEPEST':
|
case "DEEPEST":
|
||||||
return <Telescope {...iconProps} />;
|
return <Telescope {...iconProps} />;
|
||||||
default:
|
default:
|
||||||
return <Search {...iconProps} />;
|
return <Search {...iconProps} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const researcherOptions: { value: ResearchMode; label: string; icon: React.ReactNode }[] = [
|
export const researcherOptions: {
|
||||||
|
value: ResearchMode;
|
||||||
|
label: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
value: 'QNA',
|
value: "QNA",
|
||||||
label: 'Q/A',
|
label: "Q/A",
|
||||||
icon: getConnectorIcon('GENERAL')
|
icon: getConnectorIcon("GENERAL"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'REPORT_GENERAL',
|
value: "REPORT_GENERAL",
|
||||||
label: 'General',
|
label: "General",
|
||||||
icon: getConnectorIcon('GENERAL')
|
icon: getConnectorIcon("GENERAL"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'REPORT_DEEP',
|
value: "REPORT_DEEP",
|
||||||
label: 'Deep',
|
label: "Deep",
|
||||||
icon: getConnectorIcon('DEEP')
|
icon: getConnectorIcon("DEEP"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'REPORT_DEEPER',
|
value: "REPORT_DEEPER",
|
||||||
label: 'Deeper',
|
label: "Deeper",
|
||||||
icon: getConnectorIcon('DEEPER')
|
icon: getConnectorIcon("DEEPER"),
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a small icon for a connector type
|
* Displays a small icon for a connector type
|
||||||
*/
|
*/
|
||||||
export const ConnectorIcon = ({ type, index = 0 }: { type: string; index?: number }) => (
|
export const ConnectorIcon = ({
|
||||||
|
type,
|
||||||
|
index = 0,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
index?: number;
|
||||||
|
}) => (
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full flex items-center justify-center bg-muted border border-background"
|
className="w-4 h-4 rounded-full flex items-center justify-center bg-muted border border-background"
|
||||||
style={{ zIndex: 10 - index }}
|
style={{ zIndex: 10 - index }}
|
||||||
|
|
@ -109,15 +130,21 @@ type ConnectorButtonProps = {
|
||||||
/**
|
/**
|
||||||
* Button that displays selected connectors and opens connector selection dialog
|
* Button that displays selected connectors and opens connector selection dialog
|
||||||
*/
|
*/
|
||||||
export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources }: ConnectorButtonProps) => {
|
export const ConnectorButton = ({
|
||||||
|
selectedConnectors,
|
||||||
|
onClick,
|
||||||
|
connectorSources,
|
||||||
|
}: ConnectorButtonProps) => {
|
||||||
const totalConnectors = connectorSources.length;
|
const totalConnectors = connectorSources.length;
|
||||||
const selectedCount = selectedConnectors.length;
|
const selectedCount = selectedConnectors.length;
|
||||||
const progressPercentage = (selectedCount / totalConnectors) * 100;
|
const progressPercentage = (selectedCount / totalConnectors) * 100;
|
||||||
|
|
||||||
// Get the name of a single selected connector
|
// Get the name of a single selected connector
|
||||||
const getSingleConnectorName = () => {
|
const getSingleConnectorName = () => {
|
||||||
const connector = connectorSources.find(c => c.type === selectedConnectors[0]);
|
const connector = connectorSources.find(
|
||||||
return connector?.name || '';
|
(c) => c.type === selectedConnectors[0],
|
||||||
|
);
|
||||||
|
return connector?.name || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get display text based on selection count
|
// Get display text based on selection count
|
||||||
|
|
@ -158,14 +185,18 @@ export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 px-2 text-xs font-medium rounded-md border-border relative overflow-hidden group"
|
className="h-8 px-2 text-xs font-medium rounded-md border-border relative overflow-hidden group"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label={selectedCount === 0 ? "Select Connectors" : `${selectedCount} connectors selected`}
|
aria-label={
|
||||||
|
selectedCount === 0
|
||||||
|
? "Select Connectors"
|
||||||
|
: `${selectedCount} connectors selected`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Progress indicator */}
|
{/* Progress indicator */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 h-1 bg-primary"
|
className="absolute bottom-0 left-0 h-1 bg-primary"
|
||||||
style={{
|
style={{
|
||||||
width: `${progressPercentage}%`,
|
width: `${progressPercentage}%`,
|
||||||
transition: 'width 0.3s ease'
|
transition: "width 0.3s ease",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -183,29 +214,32 @@ type ResearchModeControlProps = {
|
||||||
onChange: (value: ResearchMode) => void;
|
onChange: (value: ResearchMode) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProps) => {
|
export const ResearchModeControl = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: ResearchModeControlProps) => {
|
||||||
// Determine if we're in Q/A mode or Report mode
|
// Determine if we're in Q/A mode or Report mode
|
||||||
const isQnaMode = value === 'QNA';
|
const isQnaMode = value === "QNA";
|
||||||
const isReportMode = value.startsWith('REPORT_');
|
const isReportMode = value.startsWith("REPORT_");
|
||||||
|
|
||||||
// Get the current report sub-mode
|
// Get the current report sub-mode
|
||||||
const getCurrentReportMode = () => {
|
const getCurrentReportMode = () => {
|
||||||
if (!isReportMode) return 'GENERAL';
|
if (!isReportMode) return "GENERAL";
|
||||||
return value.replace('REPORT_', '') as 'GENERAL' | 'DEEP' | 'DEEPER';
|
return value.replace("REPORT_", "") as "GENERAL" | "DEEP" | "DEEPER";
|
||||||
};
|
};
|
||||||
|
|
||||||
const reportSubOptions = [
|
const reportSubOptions = [
|
||||||
{ value: 'GENERAL', label: 'General', icon: getConnectorIcon('GENERAL') },
|
{ value: "GENERAL", label: "General", icon: getConnectorIcon("GENERAL") },
|
||||||
{ value: 'DEEP', label: 'Deep', icon: getConnectorIcon('DEEP') },
|
{ value: "DEEP", label: "Deep", icon: getConnectorIcon("DEEP") },
|
||||||
{ value: 'DEEPER', label: 'Deeper', icon: getConnectorIcon('DEEPER') },
|
{ value: "DEEPER", label: "Deeper", icon: getConnectorIcon("DEEPER") },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleModeToggle = (mode: 'QNA' | 'REPORT') => {
|
const handleModeToggle = (mode: "QNA" | "REPORT") => {
|
||||||
if (mode === 'QNA') {
|
if (mode === "QNA") {
|
||||||
onChange('QNA');
|
onChange("QNA");
|
||||||
} else {
|
} else {
|
||||||
// Default to GENERAL for Report mode
|
// Default to GENERAL for Report mode
|
||||||
onChange('REPORT_GENERAL');
|
onChange("REPORT_GENERAL");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -220,10 +254,10 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
<button
|
<button
|
||||||
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
|
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||||
isQnaMode
|
isQnaMode
|
||||||
? 'bg-primary text-primary-foreground'
|
? "bg-primary text-primary-foreground"
|
||||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleModeToggle('QNA')}
|
onClick={() => handleModeToggle("QNA")}
|
||||||
aria-pressed={isQnaMode}
|
aria-pressed={isQnaMode}
|
||||||
>
|
>
|
||||||
<MessageCircle className="h-3 w-3" />
|
<MessageCircle className="h-3 w-3" />
|
||||||
|
|
@ -232,10 +266,10 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
<button
|
<button
|
||||||
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
|
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||||
isReportMode
|
isReportMode
|
||||||
? 'bg-primary text-primary-foreground'
|
? "bg-primary text-primary-foreground"
|
||||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleModeToggle('REPORT')}
|
onClick={() => handleModeToggle("REPORT")}
|
||||||
aria-pressed={isReportMode}
|
aria-pressed={isReportMode}
|
||||||
>
|
>
|
||||||
<FileText className="h-3 w-3" />
|
<FileText className="h-3 w-3" />
|
||||||
|
|
@ -251,8 +285,8 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className={`flex h-full items-center gap-1 px-2 text-xs font-medium transition-colors whitespace-nowrap ${
|
className={`flex h-full items-center gap-1 px-2 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||||
getCurrentReportMode() === option.value
|
getCurrentReportMode() === option.value
|
||||||
? 'bg-primary text-primary-foreground'
|
? "bg-primary text-primary-foreground"
|
||||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleReportSubModeChange(option.value)}
|
onClick={() => handleReportSubModeChange(option.value)}
|
||||||
aria-pressed={getCurrentReportMode() === option.value}
|
aria-pressed={getCurrentReportMode() === option.value}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
// Helper function to get connector type display name
|
// Helper function to get connector type display name
|
||||||
export const getConnectorTypeDisplay = (type: string): string => {
|
export const getConnectorTypeDisplay = (type: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
"SERPER_API": "Serper API",
|
SERPER_API: "Serper API",
|
||||||
"TAVILY_API": "Tavily API",
|
TAVILY_API: "Tavily API",
|
||||||
"SLACK_CONNECTOR": "Slack",
|
SLACK_CONNECTOR: "Slack",
|
||||||
"NOTION_CONNECTOR": "Notion",
|
NOTION_CONNECTOR: "Notion",
|
||||||
"GITHUB_CONNECTOR": "GitHub",
|
GITHUB_CONNECTOR: "GitHub",
|
||||||
"LINEAR_CONNECTOR": "Linear",
|
LINEAR_CONNECTOR: "Linear",
|
||||||
"DISCORD_CONNECTOR": "Discord",
|
JIRA_CONNECTOR: "Jira",
|
||||||
"LINKUP_API": "Linkup",
|
DISCORD_CONNECTOR: "Discord",
|
||||||
};
|
LINKUP_API: "Linkup",
|
||||||
return typeMap[type] || type;
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue