Skip to content

feat: Add Zendesk connector integration #243

New issue

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

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

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
8 changes: 1 addition & 7 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,4 @@ services:
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
- LANGCHAIN_TRACING_V2=false
- LANGSMITH_TRACING=false
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Add ZENDESK_CONNECTOR to enums

Revision ID: 15
Revises: 14
"""

from collections.abc import Sequence

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "15"
down_revision: str | None = "14"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Safely add 'ZENDESK_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 = 'ZENDESK_CONNECTOR'
) THEN
ALTER TYPE searchsourceconnectortype ADD VALUE 'ZENDESK_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 = 'ZENDESK_CONNECTOR'
) THEN
ALTER TYPE documenttype ADD VALUE 'ZENDESK_CONNECTOR';
END IF;
END
$$;
"""
)


def downgrade() -> None:
"""
Downgrade logic not implemented since PostgreSQL
does not support removing enum values.
"""
pass
45 changes: 45 additions & 0 deletions surfsense_backend/app/connectors/zendesk_connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import datetime

import httpx


class ZendeskConnector:
def __init__(self, subdomain: str, email: str, api_token: str):
if not subdomain or not email or not api_token:
raise ValueError("Subdomain, email, and API token cannot be empty.")
self.base_url = f"https://{subdomain}.zendesk.com/api/v2"
self.auth = (f"{email}/token", api_token)

async def get_tickets(
self, start_date: str | None = None, end_date: str | None = None
):
tickets = []
async with httpx.AsyncClient() as client:
params = {}
if start_date:
# Zendesk API uses 'start_time' for filtering by updated_at
# Convert YYYY-MM-DD to Unix timestamp
start_timestamp = int(
datetime.strptime(start_date, "%Y-%m-%d").timestamp()
)
params["start_time"] = start_timestamp

url = f"{self.base_url}/tickets.json"
while url:
try:
response = await client.get(url, auth=self.auth, params=params)
response.raise_for_status()
data = response.json()
tickets.extend(data["tickets"])
url = data["next_page"]
# Clear params for subsequent paginated requests as they are part of the next_page URL
params = {}
except httpx.HTTPStatusError as e:
print(
f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
)
break
except httpx.RequestError as e:
print(f"An error occurred while requesting {e.request.url}: {e}")
break
return tickets
4 changes: 3 additions & 1 deletion surfsense_backend/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ class DocumentType(str, Enum):
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
JIRA_CONNECTOR = "JIRA_CONNECTOR"
CONFLUENCE_CONNECTOR = "CONFLUENCE_CONNECTOR"
ZENDESK_CONNECTOR = "ZENDESK_CONNECTOR"


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"
LINKUP_API = "LINKUP_API"
SLACK_CONNECTOR = "SLACK_CONNECTOR"
Expand All @@ -57,6 +58,7 @@ class SearchSourceConnectorType(str, Enum):
DISCORD_CONNECTOR = "DISCORD_CONNECTOR"
JIRA_CONNECTOR = "JIRA_CONNECTOR"
CONFLUENCE_CONNECTOR = "CONFLUENCE_CONNECTOR"
ZENDESK_CONNECTOR = "ZENDESK_CONNECTOR"


class ChatType(str, Enum):
Expand Down
73 changes: 73 additions & 0 deletions surfsense_backend/app/routes/search_source_connectors_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
index_linear_issues,
index_notion_pages,
index_slack_messages,
index_zendesk_tickets,
)
from app.users import current_active_user
from app.utils.check_ownership import check_ownership
Expand Down Expand Up @@ -488,6 +489,21 @@ async def index_connector_content(
)
response_message = "Discord indexing started in the background."

elif connector.connector_type == SearchSourceConnectorType.ZENDESK_CONNECTOR:
# Run indexing in background
logger.info(
f"Triggering Zendesk indexing for connector {connector_id} into search space {search_space_id} from {indexing_from} to {indexing_to}"
)
background_tasks.add_task(
run_zendesk_indexing_with_new_session,
connector_id,
search_space_id,
str(user.id),
indexing_from,
indexing_to,
)
response_message = "Zendesk indexing started in the background."

else:
raise HTTPException(
status_code=400,
Expand Down Expand Up @@ -960,3 +976,60 @@ async def run_confluence_indexing(
exc_info=True,
)
# Optionally update status in DB to indicate failure


async def run_zendesk_indexing_with_new_session(
connector_id: int,
search_space_id: int,
user_id: str,
start_date: str,
end_date: str,
):
"""Wrapper to run Zendesk indexing with its own database session."""
logger.info(
f"Background task started: Indexing Zendesk connector {connector_id} into space {search_space_id} from {start_date} to {end_date}"
)
async with async_session_maker() as session:
await run_zendesk_indexing(
session, connector_id, search_space_id, user_id, start_date, end_date
)
logger.info(f"Background task finished: Indexing Zendesk connector {connector_id}")


async def run_zendesk_indexing(
session: AsyncSession,
connector_id: int,
search_space_id: int,
user_id: str,
start_date: str,
end_date: str,
):
"""Runs the Zendesk indexing task and updates the timestamp."""
try:
indexed_count, error_message = await index_zendesk_tickets(
session,
connector_id,
search_space_id,
user_id,
start_date,
end_date,
update_last_indexed=False,
)
if error_message:
logger.error(
f"Zendesk indexing failed for connector {connector_id}: {error_message}"
)
# Optionally update status in DB to indicate failure
else:
logger.info(
f"Zendesk 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_zendesk_indexing for connector {connector_id}: {e}",
exc_info=True,
)
# Optionally update status in DB to indicate failure
20 changes: 20 additions & 0 deletions surfsense_backend/app/schemas/search_source_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ def validate_config_for_connector_type(
if not config.get("CONFLUENCE_BASE_URL"):
raise ValueError("CONFLUENCE_BASE_URL cannot be empty")

elif connector_type == SearchSourceConnectorType.ZENDESK_CONNECTOR:
# For ZENDESK_CONNECTOR, require ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, and ZENDESK_API_TOKEN
allowed_keys = ["ZENDESK_SUBDOMAIN", "ZENDESK_EMAIL", "ZENDESK_API_TOKEN"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(
f"For ZENDESK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
)

# Ensure the subdomain is not empty
if not config.get("ZENDESK_SUBDOMAIN"):
raise ValueError("ZENDESK_SUBDOMAIN cannot be empty")

# Ensure the email is not empty
if not config.get("ZENDESK_EMAIL"):
raise ValueError("ZENDESK_EMAIL cannot be empty")

# Ensure the API token is not empty
if not config.get("ZENDESK_API_TOKEN"):
raise ValueError("ZENDESK_API_TOKEN cannot be empty")

return config


Expand Down
Loading