Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

wip #3325

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft

wip #3325

Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion backend/api/quivr_api/modules/rag_service/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async def generate_source(

# Get source documents from the result, default to an empty list if not found
# If source documents exist
logger.info(f"Source documents: {source_documents}")
if source_documents:
logger.debug(f"Citations {citations}")
for index, doc in enumerate(source_documents):
Expand All @@ -48,6 +49,7 @@ async def generate_source(
"original_file_name" in doc.metadata
and doc.metadata["original_file_name"] is not None
and doc.metadata["original_file_name"].startswith("http")
and doc.metadata["integration"] == ""
)

# Determine the name based on whether it's a URL or a file
Expand All @@ -63,6 +65,8 @@ async def generate_source(
# Determine the source URL based on whether it's a URL or a file
if is_url:
source_url = doc.metadata["original_file_name"]
elif doc.metadata["integration"] != "":
logger.info(f"Integration: {doc.metadata['integration']}")
else:
# Check if the URL has already been generated
try:
Expand All @@ -87,8 +91,12 @@ async def generate_source(
except Exception as e:
logger.error(f"Error generating file signed URL: {e}")
continue


logger.info(f"Metadata: {doc.metadata}")
# Append a new Sources object to the list
if doc.metadata["integration"] == "Zendesk":
logger.error(f"Zendesk integration: {doc.metadata['integration']}")
source_url = doc.metadata["integration_link"]
sources_list.append(
Sources(
name=name,
Expand Down
10 changes: 9 additions & 1 deletion backend/api/quivr_api/modules/sync/controller/sync_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from quivr_api.modules.sync.controller.github_sync_routes import github_sync_router
from quivr_api.modules.sync.controller.google_sync_routes import google_sync_router
from quivr_api.modules.sync.controller.notion_sync_routes import notion_sync_router
from quivr_api.modules.sync.controller.zendesk_sync_routes import zendesk_sync_router
from quivr_api.modules.sync.dto import SyncsDescription
from quivr_api.modules.sync.dto.inputs import SyncsActiveInput, SyncsActiveUpdateInput
from quivr_api.modules.sync.dto.outputs import AuthMethodEnum
Expand Down Expand Up @@ -49,6 +50,7 @@
sync_router.include_router(github_sync_router)
sync_router.include_router(dropbox_sync_router)
sync_router.include_router(notion_sync_router)
sync_router.include_router(zendesk_sync_router)


# Google sync description
Expand Down Expand Up @@ -82,6 +84,12 @@
auth_method=AuthMethodEnum.URI_WITH_CALLBACK,
)

zendesk_sync = SyncsDescription(
name="Zendesk",
description="Sync your Zendesk with Quivr",
auth_method=AuthMethodEnum.URI_WITH_CALLBACK,
)


@sync_router.get(
"/sync/all",
Expand All @@ -100,7 +108,7 @@ async def get_syncs(current_user: UserIdentity = Depends(get_current_user)):
List[SyncsDescription]: A list of available sync descriptions.
"""
logger.debug(f"Fetching all sync descriptions for user: {current_user.id}")
return [google_sync, azure_sync, dropbox_sync, notion_sync]
return [google_sync, azure_sync, dropbox_sync, notion_sync, zendesk_sync]


@sync_router.get(
Expand Down
52 changes: 52 additions & 0 deletions backend/api/quivr_api/modules/sync/controller/zendesk_ask_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
zendeskAskTokenPage = """
<!DOCTYPE html>
<html>
<head>
<title>Enter Zendesk API Token</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f8f9fa;
font-family: Arial, sans-serif;
}
.container {
text-align: center;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.input-field {
margin-bottom: 20px;
}
.submit-button {
padding: 10px 20px;
font-size: 1em;
color: #fff;
background-color: #6142d4;
border: none;
border-radius: 5px;
cursor: pointer;
}
.submit-button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Enter Your Zendesk API Token</h2>
<form action="/sync/zendesk/submit-token" method="post">
<div class="input-field">
<input type="text" name="api_token" placeholder="API Token" required>
</div>
<button type="submit" class="submit-button">Submit</button>
</form>
</div>
</body>
</html>
"""
206 changes: 206 additions & 0 deletions backend/api/quivr_api/modules/sync/controller/zendesk_sync_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import random

from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse

from quivr_api.logger import get_logger
from quivr_api.middlewares.auth import AuthBearer, get_current_user
from quivr_api.modules.sync.dto.inputs import (
SyncsUserInput,
SyncsUserStatus,
SyncUserUpdateInput,
)
from quivr_api.modules.sync.service.sync_service import SyncService, SyncUserService
from quivr_api.modules.user.entity.user_identity import UserIdentity

from .successfull_connection import successfullConnectionPage

# Initialize logger
logger = get_logger(__name__)

# Initialize sync service
sync_service = SyncService()
sync_user_service = SyncUserService()

# Initialize API router
zendesk_sync_router = APIRouter()

#


@zendesk_sync_router.post(
"/sync/zendesk/authorize",
dependencies=[Depends(AuthBearer())],
tags=["Sync"],
)
def authorize_zendesk(
request: Request, name: str, current_user: UserIdentity = Depends(get_current_user)
):
"""
Authorize Zendesk sync for the current user.

Args:
request (Request): The request object.
current_user (UserIdentity): The current authenticated user.

Returns:
dict: A dictionary containing the authorization URL.
"""

state = str(current_user.email) + "," + str(random.randint(100000, 999999))
sync_user_input = SyncsUserInput(
user_id=str(current_user.id),
name=name,
provider="Zendesk",
credentials={},
state={"state": state.split(",")[1]},
additional_data={},
status=str(SyncsUserStatus.SYNCING),
)
sync_user_service.create_sync_user(sync_user_input)
return {
"authorization_url": f"http://localhost:5050/sync/zendesk/enter-token?state={state}"
}


@zendesk_sync_router.get("/sync/zendesk/enter-token", tags=["Sync"])
def enter_zendesk_token_page(request: Request):
"""
Serve the HTML page to enter the Zendesk API token and domain name.
"""
state = request.query_params.get("state", "")
zendeskAskTokenPage = f"""
<!DOCTYPE html>
<html>
<head>
<title>Enter Zendesk API Token and Domain</title>
<style>
body {{
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f2f5;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.container {{
text-align: center;
background-color: #ffffff;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
width: 400px;
}}
.input-field {{
margin-bottom: 24px;
text-align: left;
}}
.input-field input {{
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}}
.input-field input:focus {{
outline: none;
border-color: #6142d4;
}}
.submit-button {{
width: 100%;
padding: 12px;
font-size: 16px;
font-weight: bold;
color: #ffffff;
background-color: #6142d4;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease;
}}
.submit-button:hover {{
background-color: #4e35a8;
}}
.domain-suffix {{
display: block;
margin-top: 8px;
font-size: 14px;
color: #6b7280;
}}
</style>
</head>
<body>
<div class="container">
<h2>Enter Your Zendesk API Token and Domain</h2>
<form action="/sync/zendesk/submit-token" method="post">
<div class="input-field">
<input type="text" name="email" placeholder="Zendesk Email" required>
</div>
<div class="input-field">
<input type="text" name="sub_domain_name" placeholder="Sub Domain Name" required>
<span class="domain-suffix">.zendesk.com</span>
</div>
<div class="input-field">
<input type="text" name="api_token" placeholder="API Token" required>
</div>
<input type="hidden" name="state" value="{state}">
<button type="submit" class="submit-button">Submit</button>
</form>
</div>
</body>
</html>
"""
return HTMLResponse(content=zendeskAskTokenPage, status_code=200)


@zendesk_sync_router.post("/sync/zendesk/submit-token", tags=["Sync"])
def submit_zendesk_token(
api_token: str = Form(...),
sub_domain_name: str = Form(...),
email: str = Form(...),
state: str = Form(...),
):
"""
Handle the submission of the Zendesk API token.

Args:
api_token (str): The API token provided by the user.
current_user (UserIdentity): The current authenticated user.

Returns:
HTMLResponse: A success page.
"""
user_email, sync_state = state.split(",")
state_dict = {"state": sync_state}
logger.debug(f"Handling OAuth2 callback for user with state: {state}")
sync_user_state = sync_user_service.get_sync_user_by_state(state_dict)
logger.info(f"Retrieved sync user state: {sync_user_state}")
if not sync_user_state or state_dict != sync_user_state.state:
logger.error("Invalid state parameter")
raise HTTPException(status_code=400, detail="Invalid state parameter")

logger.debug(
f"Received Zendesk API token and sub domain name for user: {sync_user_state.user_id}"
)
assert email is not None, "User email is None"

# Update the sync user with the provided Zendesk API token
sync_user_input = SyncUserUpdateInput(
email=email,
credentials={
"api_token": api_token,
"sub_domain_name": sub_domain_name,
"email": email,
},
status=str(SyncsUserStatus.SYNCED),
)
sync_user_service.update_sync_user(
sync_user_state.user_id, state_dict, sync_user_input
)
logger.info(
f"Zendesk API token updated successfully for user: {sync_user_state.user_id}"
)

return HTMLResponse(successfullConnectionPage)
9 changes: 9 additions & 0 deletions backend/api/quivr_api/modules/sync/repository/sync_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
GitHubSync,
GoogleDriveSync,
NotionSync,
ZendeskSync,
)

logger = get_logger(__name__)
Expand Down Expand Up @@ -287,6 +288,14 @@ async def get_files_folder_user_sync(
sync_user["credentials"], folder_id if folder_id else "", recursive
)
}
elif provider == "zendesk":
logger.info("Getting files for Zendesk sync")
sync = ZendeskSync()
return {
"files": sync.get_files(
sync_user["credentials"], folder_id if folder_id else "", recursive
)
}

else:
logger.warning(
Expand Down
Loading
Loading