From 348b36bf8593f3fcbed84fa4540bc0977af0b2cd Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Thu, 25 Apr 2024 17:53:38 -0700
Subject: [PATCH] [FEAT] Confluence data connector (#1181)

* WIP Confluence data connector backend

* confluence data connector complete

* confluence citations

* fix citation for confluence

* Patch confulence integration

* fix Citation Icon for confluence

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 collector/extensions/index.js                 | 157 +++++++++++------
 collector/package.json                        |   2 +-
 .../utils/extensions/Confluence/index.js      | 110 ++++++++++++
 .../DataConnectorOption/media/confluence.jpeg | Bin 0 -> 5659 bytes
 .../DataConnectorOption/media/index.js        |   2 +
 .../Connectors/Confluence/index.jsx           | 164 ++++++++++++++++++
 .../MangeWorkspace/DataConnectors/index.jsx   |   7 +
 .../ChatHistory/Citation/index.jsx            |  39 +++--
 .../src/media/dataConnectors/confluence.png   | Bin 0 -> 9582 bytes
 frontend/src/models/dataConnector.js          |  23 +++
 server/endpoints/extensions/index.js          |  22 +++
 11 files changed, 458 insertions(+), 68 deletions(-)
 create mode 100644 collector/utils/extensions/Confluence/index.js
 create mode 100644 frontend/src/components/DataConnectorOption/media/confluence.jpeg
 create mode 100644 frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Confluence/index.jsx
 create mode 100644 frontend/src/media/dataConnectors/confluence.png

diff --git a/collector/extensions/index.js b/collector/extensions/index.js
index 0e91d1731..6a3f3393e 100644
--- a/collector/extensions/index.js
+++ b/collector/extensions/index.js
@@ -4,69 +4,112 @@ const { reqBody } = require("../utils/http");
 function extensions(app) {
   if (!app) return;
 
-  app.post("/ext/github-repo", [verifyPayloadIntegrity], async function (request, response) {
-    try {
-      const loadGithubRepo = require("../utils/extensions/GithubRepo");
-      const { success, reason, data } = await loadGithubRepo(reqBody(request));
-      response.status(200).json({
-        success,
-        reason,
-        data
-      });
-    } catch (e) {
-      console.error(e);
-      response.status(200).json({
-        success: false,
-        reason: e.message || "A processing error occurred.",
-        data: {},
-      });
+  app.post(
+    "/ext/github-repo",
+    [verifyPayloadIntegrity],
+    async function (request, response) {
+      try {
+        const loadGithubRepo = require("../utils/extensions/GithubRepo");
+        const { success, reason, data } = await loadGithubRepo(
+          reqBody(request)
+        );
+        response.status(200).json({
+          success,
+          reason,
+          data,
+        });
+      } catch (e) {
+        console.error(e);
+        response.status(200).json({
+          success: false,
+          reason: e.message || "A processing error occurred.",
+          data: {},
+        });
+      }
+      return;
     }
-    return;
-  });
+  );
 
   // gets all branches for a specific repo
-  app.post("/ext/github-repo/branches", [verifyPayloadIntegrity], async function (request, response) {
-    try {
-      const GithubRepoLoader = require("../utils/extensions/GithubRepo/RepoLoader");
-      const allBranches = await (new GithubRepoLoader(reqBody(request))).getRepoBranches()
-      response.status(200).json({
-        success: true,
-        reason: null,
-        data: {
-          branches: allBranches
-        }
-      });
-    } catch (e) {
-      console.error(e);
-      response.status(400).json({
-        success: false,
-        reason: e.message,
-        data: {
-          branches: []
-        }
-      });
+  app.post(
+    "/ext/github-repo/branches",
+    [verifyPayloadIntegrity],
+    async function (request, response) {
+      try {
+        const GithubRepoLoader = require("../utils/extensions/GithubRepo/RepoLoader");
+        const allBranches = await new GithubRepoLoader(
+          reqBody(request)
+        ).getRepoBranches();
+        response.status(200).json({
+          success: true,
+          reason: null,
+          data: {
+            branches: allBranches,
+          },
+        });
+      } catch (e) {
+        console.error(e);
+        response.status(400).json({
+          success: false,
+          reason: e.message,
+          data: {
+            branches: [],
+          },
+        });
+      }
+      return;
     }
-    return;
-  });
+  );
 
-  app.post("/ext/youtube-transcript", [verifyPayloadIntegrity], async function (request, response) {
-    try {
-      const loadYouTubeTranscript = require("../utils/extensions/YoutubeTranscript");
-      const { success, reason, data } = await loadYouTubeTranscript(reqBody(request));
-      response.status(200).json({ success, reason, data });
-    } catch (e) {
-      console.error(e);
-      response.status(400).json({
-        success: false,
-        reason: e.message,
-        data: {
-          title: null,
-          author: null
-        }
-      });
+  app.post(
+    "/ext/youtube-transcript",
+    [verifyPayloadIntegrity],
+    async function (request, response) {
+      try {
+        const loadYouTubeTranscript = require("../utils/extensions/YoutubeTranscript");
+        const { success, reason, data } = await loadYouTubeTranscript(
+          reqBody(request)
+        );
+        response.status(200).json({ success, reason, data });
+      } catch (e) {
+        console.error(e);
+        response.status(400).json({
+          success: false,
+          reason: e.message,
+          data: {
+            title: null,
+            author: null,
+          },
+        });
+      }
+      return;
     }
-    return;
-  });
+  );
+
+  app.post(
+    "/ext/confluence",
+    [verifyPayloadIntegrity],
+    async function (request, response) {
+      try {
+        const loadConfluence = require("../utils/extensions/Confluence");
+        const { success, reason, data } = await loadConfluence(
+          reqBody(request)
+        );
+        response.status(200).json({ success, reason, data });
+      } catch (e) {
+        console.error(e);
+        response.status(400).json({
+          success: false,
+          reason: e.message,
+          data: {
+            title: null,
+            author: null,
+          },
+        });
+      }
+      return;
+    }
+  );
 }
 
 module.exports = extensions;
diff --git a/collector/package.json b/collector/package.json
index 4a5a99fff..5d2e5f0f5 100644
--- a/collector/package.json
+++ b/collector/package.json
@@ -49,4 +49,4 @@
     "nodemon": "^2.0.22",
     "prettier": "^2.4.1"
   }
-}
\ No newline at end of file
+}
diff --git a/collector/utils/extensions/Confluence/index.js b/collector/utils/extensions/Confluence/index.js
new file mode 100644
index 000000000..1ea642e1a
--- /dev/null
+++ b/collector/utils/extensions/Confluence/index.js
@@ -0,0 +1,110 @@
+const fs = require("fs");
+const path = require("path");
+const { default: slugify } = require("slugify");
+const { v4 } = require("uuid");
+const { writeToServerDocuments } = require("../../files");
+const { tokenizeString } = require("../../tokenizer");
+const {
+  ConfluencePagesLoader,
+} = require("langchain/document_loaders/web/confluence");
+
+function validSpaceUrl(spaceUrl = "") {
+  const UrlPattern = require("url-pattern");
+  const pattern = new UrlPattern(
+    "https\\://(:subdomain).atlassian.net/wiki/spaces/(:spaceKey)*"
+  );
+  const match = pattern.match(spaceUrl);
+  if (!match) return { valid: false, result: null };
+  return { valid: true, result: match };
+}
+
+async function loadConfluence({ pageUrl, username, accessToken }) {
+  if (!pageUrl || !username || !accessToken) {
+    return {
+      success: false,
+      reason:
+        "You need either a username and access token, or a personal access token (PAT), to use the Confluence connector.",
+    };
+  }
+
+  const validSpace = validSpaceUrl(pageUrl);
+  if (!validSpace.result) {
+    return {
+      success: false,
+      reason:
+        "Confluence space URL is not in the expected format of https://domain.atlassian.net/wiki/space/~SPACEID/*",
+    };
+  }
+
+  const { subdomain, spaceKey } = validSpace.result;
+  console.log(`-- Working Confluence ${subdomain}.atlassian.net --`);
+  const loader = new ConfluencePagesLoader({
+    baseUrl: `https://${subdomain}.atlassian.net/wiki`,
+    spaceKey,
+    username,
+    accessToken,
+  });
+
+  const { docs, error } = await loader
+    .load()
+    .then((docs) => {
+      return { docs, error: null };
+    })
+    .catch((e) => {
+      return {
+        docs: [],
+        error: e.message?.split("Error:")?.[1] || e.message,
+      };
+    });
+
+  if (!docs.length || !!error) {
+    return {
+      success: false,
+      reason: error ?? "No pages found for that Confluence space.",
+    };
+  }
+  const outFolder = slugify(
+    `${subdomain}-confluence-${v4().slice(0, 4)}`
+  ).toLowerCase();
+  const outFolderPath = path.resolve(
+    __dirname,
+    `../../../../server/storage/documents/${outFolder}`
+  );
+  fs.mkdirSync(outFolderPath);
+
+  docs.forEach((doc) => {
+    const data = {
+      id: v4(),
+      url: doc.metadata.url + ".page",
+      title: doc.metadata.title || doc.metadata.source,
+      docAuthor: subdomain,
+      description: doc.metadata.title,
+      docSource: `${subdomain} Confluence`,
+      chunkSource: `confluence://${doc.metadata.url}`,
+      published: new Date().toLocaleString(),
+      wordCount: doc.pageContent.split(" ").length,
+      pageContent: doc.pageContent,
+      token_count_estimate: tokenizeString(doc.pageContent).length,
+    };
+
+    console.log(
+      `[Confluence Loader]: Saving ${doc.metadata.title} to ${outFolder}`
+    );
+    writeToServerDocuments(
+      data,
+      `${slugify(doc.metadata.title)}-${data.id}`,
+      outFolderPath
+    );
+  });
+
+  return {
+    success: true,
+    reason: null,
+    data: {
+      spaceKey,
+      destination: outFolder,
+    },
+  };
+}
+
+module.exports = loadConfluence;
diff --git a/frontend/src/components/DataConnectorOption/media/confluence.jpeg b/frontend/src/components/DataConnectorOption/media/confluence.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..7559663a68ac1dfdc3f5a64f4de891d3d449272c
GIT binary patch
literal 5659
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
DdbL6T

literal 0
HcmV?d00001

diff --git a/frontend/src/components/DataConnectorOption/media/index.js b/frontend/src/components/DataConnectorOption/media/index.js
index 543bed5f7..ac8105975 100644
--- a/frontend/src/components/DataConnectorOption/media/index.js
+++ b/frontend/src/components/DataConnectorOption/media/index.js
@@ -1,9 +1,11 @@
 import Github from "./github.svg";
 import YouTube from "./youtube.svg";
+import Confluence from "./confluence.jpeg";
 
 const ConnectorImages = {
   github: Github,
   youtube: YouTube,
+  confluence: Confluence,
 };
 
 export default ConnectorImages;
diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Confluence/index.jsx b/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Confluence/index.jsx
new file mode 100644
index 000000000..52ca7e63d
--- /dev/null
+++ b/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Confluence/index.jsx
@@ -0,0 +1,164 @@
+import { useState } from "react";
+import System from "@/models/system";
+import showToast from "@/utils/toast";
+import { Warning } from "@phosphor-icons/react";
+import { Tooltip } from "react-tooltip";
+
+export default function ConfluenceOptions() {
+  const [loading, setLoading] = useState(false);
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    const form = new FormData(e.target);
+
+    try {
+      setLoading(true);
+      showToast(
+        "Fetching all pages for Confluence space - this may take a while.",
+        "info",
+        {
+          clear: true,
+          autoClose: false,
+        }
+      );
+      const { data, error } = await System.dataConnectors.confluence.collect({
+        pageUrl: form.get("pageUrl"),
+        username: form.get("username"),
+        accessToken: form.get("accessToken"),
+      });
+
+      if (!!error) {
+        showToast(error, "error", { clear: true });
+        setLoading(false);
+        return;
+      }
+
+      showToast(
+        `Pages collected from Confluence space ${data.spaceKey}. Output folder is ${data.destination}.`,
+        "success",
+        { clear: true }
+      );
+      e.target.reset();
+      setLoading(false);
+    } catch (e) {
+      console.error(e);
+      showToast(e.message, "error", { clear: true });
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="flex w-full">
+      <div className="flex flex-col w-full px-1 md:pb-6 pb-16">
+        <form className="w-full" onSubmit={handleSubmit}>
+          <div className="w-full flex flex-col py-2">
+            <div className="w-full flex flex-col gap-4">
+              <div className="flex flex-col pr-10">
+                <div className="flex flex-col gap-y-1 mb-4">
+                  <label className="text-white text-sm font-bold flex gap-x-2 items-center">
+                    <p className="font-bold text-white">Confluence Page URL</p>
+                  </label>
+                  <p className="text-xs font-normal text-white/50">
+                    URL of a page in the Confluence space.
+                  </p>
+                </div>
+                <input
+                  type="url"
+                  name="pageUrl"
+                  className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
+                  placeholder="https://example.atlassian.net/wiki/spaces/~7120208c08555d52224113949698b933a3bb56/pages/851969/Test+anythingLLM+page"
+                  required={true}
+                  autoComplete="off"
+                  spellCheck={false}
+                />
+              </div>
+              <div className="flex flex-col pr-10">
+                <div className="flex flex-col gap-y-1 mb-4">
+                  <label className="text-white text-sm font-bold">
+                    Confluence Username
+                  </label>
+                  <p className="text-xs font-normal text-white/50">
+                    Your Confluence username.
+                  </p>
+                </div>
+                <input
+                  type="email"
+                  name="username"
+                  className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
+                  placeholder="jdoe@example.com"
+                  required={true}
+                  autoComplete="off"
+                  spellCheck={false}
+                />
+              </div>
+              <div className="flex flex-col pr-10">
+                <div className="flex flex-col gap-y-1 mb-4">
+                  <label className="text-white text-sm font-bold flex gap-x-2 items-center">
+                    <p className="font-bold text-white">
+                      Confluence Access Token
+                    </p>
+                    <Warning
+                      size={14}
+                      className="ml-1 text-orange-500 cursor-pointer"
+                      data-tooltip-id="access-token-tooltip"
+                      data-tooltip-place="right"
+                    />
+                    <Tooltip
+                      delayHide={300}
+                      id="access-token-tooltip"
+                      className="max-w-xs"
+                      clickable={true}
+                    >
+                      <p className="text-sm">
+                        You need to provide an access token for authentication.
+                        You can generate an access token{" "}
+                        <a
+                          href="https://id.atlassian.com/manage-profile/security/api-tokens"
+                          target="_blank"
+                          rel="noopener noreferrer"
+                          className="underline"
+                          onClick={(e) => e.stopPropagation()}
+                        >
+                          here
+                        </a>
+                        .
+                      </p>
+                    </Tooltip>
+                  </label>
+                  <p className="text-xs font-normal text-white/50">
+                    Access token for authentication.
+                  </p>
+                </div>
+                <input
+                  type="password"
+                  name="accessToken"
+                  className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
+                  placeholder="abcd1234"
+                  required={true}
+                  autoComplete="off"
+                  spellCheck={false}
+                />
+              </div>
+            </div>
+          </div>
+
+          <div className="flex flex-col gap-y-2 w-full pr-10">
+            <button
+              type="submit"
+              disabled={loading}
+              className="mt-2 w-full justify-center border border-slate-200 px-4 py-2 rounded-lg text-[#222628] text-sm font-bold items-center flex gap-x-2 bg-slate-200 hover:bg-slate-300 hover:text-slate-800 disabled:bg-slate-300 disabled:cursor-not-allowed"
+            >
+              {loading ? "Collecting pages..." : "Submit"}
+            </button>
+            {loading && (
+              <p className="text-xs text-white/50">
+                Once complete, all pages will be available for embedding into
+                workspaces.
+              </p>
+            )}
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}
diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/index.jsx b/frontend/src/components/Modals/MangeWorkspace/DataConnectors/index.jsx
index 419fc1fc9..69d30e281 100644
--- a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/DataConnectors/index.jsx
@@ -2,6 +2,7 @@ import ConnectorImages from "@/components/DataConnectorOption/media";
 import { MagnifyingGlass } from "@phosphor-icons/react";
 import GithubOptions from "./Connectors/Github";
 import YoutubeOptions from "./Connectors/Youtube";
+import ConfluenceOptions from "./Connectors/Confluence";
 import { useState } from "react";
 import ConnectorOption from "./ConnectorOption";
 
@@ -20,6 +21,12 @@ export const DATA_CONNECTORS = {
       "Import the transcription of an entire YouTube video from a link.",
     options: <YoutubeOptions />,
   },
+  confluence: {
+    name: "Confluence",
+    image: ConnectorImages.confluence,
+    description: "Import an entire Confluence page in a single click.",
+    options: <ConfluenceOptions />,
+  },
 };
 
 export default function DataConnectors() {
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
index 1dfeaaaf3..7105901d3 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
@@ -1,4 +1,4 @@
-import { memo, useState } from "react";
+import React, { memo, useState } from "react";
 import { v4 } from "uuid";
 import { decode as HTMLDecode } from "he";
 import truncate from "truncate";
@@ -14,6 +14,7 @@ import {
   X,
   YoutubeLogo,
 } from "@phosphor-icons/react";
+import ConfluenceLogo from "@/media/dataConnectors/confluence.png";
 import { Tooltip } from "react-tooltip";
 import { toPercentString } from "@/utils/numbers";
 
@@ -202,13 +203,6 @@ function CitationDetailModal({ source, onClose }) {
   );
 }
 
-const ICONS = {
-  file: FileText,
-  link: Link,
-  youtube: YoutubeLogo,
-  github: GithubLogo,
-};
-
 // Show the correct title and/or display text for citations
 // which contain valid outbound links that can be clicked by the
 // user when viewing a citation. Optionally allows various icons
@@ -221,10 +215,17 @@ function parseChunkSource({ title = "", chunks = [] }) {
     icon: "file",
   };
 
-  if (!chunks.length || !chunks[0].chunkSource.startsWith("link://"))
+  if (
+    !chunks.length ||
+    (!chunks[0].chunkSource.startsWith("link://") &&
+      !chunks[0].chunkSource.startsWith("confluence://"))
+  )
     return nullResponse;
   try {
-    const url = new URL(chunks[0].chunkSource.split("link://")[1]);
+    const url = new URL(
+      chunks[0].chunkSource.split("link://")[1] ||
+        chunks[0].chunkSource.split("confluence://")[1]
+    );
     let text = url.host + url.pathname;
     let icon = "link";
 
@@ -238,6 +239,11 @@ function parseChunkSource({ title = "", chunks = [] }) {
       icon = "github";
     }
 
+    if (url.host.includes("atlassian.net")) {
+      text = title;
+      icon = "confluence";
+    }
+
     return {
       isUrl: true,
       href: url.toString(),
@@ -247,3 +253,16 @@ function parseChunkSource({ title = "", chunks = [] }) {
   } catch {}
   return nullResponse;
 }
+
+// Patch to render Confluence icon as a element like we do with Phosphor
+const ConfluenceIcon = ({ ...props }) => (
+  <img src={ConfluenceLogo} {...props} />
+);
+
+const ICONS = {
+  file: FileText,
+  link: Link,
+  youtube: YoutubeLogo,
+  github: GithubLogo,
+  confluence: ConfluenceIcon,
+};
diff --git a/frontend/src/media/dataConnectors/confluence.png b/frontend/src/media/dataConnectors/confluence.png
new file mode 100644
index 0000000000000000000000000000000000000000..27a5da07bd1402f48dc9cc39d26225505dd85242
GIT binary patch
literal 9582
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
J)#~=K{|7a%wvPY+

literal 0
HcmV?d00001

diff --git a/frontend/src/models/dataConnector.js b/frontend/src/models/dataConnector.js
index e0b3c0c3e..19fa5f912 100644
--- a/frontend/src/models/dataConnector.js
+++ b/frontend/src/models/dataConnector.js
@@ -60,6 +60,29 @@ const DataConnector = {
         });
     },
   },
+
+  confluence: {
+    collect: async function ({ pageUrl, username, accessToken }) {
+      return await fetch(`${API_BASE}/ext/confluence`, {
+        method: "POST",
+        headers: baseHeaders(),
+        body: JSON.stringify({
+          pageUrl,
+          username,
+          accessToken,
+        }),
+      })
+        .then((res) => res.json())
+        .then((res) => {
+          if (!res.success) throw new Error(res.reason);
+          return { data: res.data, error: null };
+        })
+        .catch((e) => {
+          console.error(e);
+          return { data: null, error: e.message };
+        });
+    },
+  },
 };
 
 export default DataConnector;
diff --git a/server/endpoints/extensions/index.js b/server/endpoints/extensions/index.js
index bf07ec56c..07eb7130d 100644
--- a/server/endpoints/extensions/index.js
+++ b/server/endpoints/extensions/index.js
@@ -71,6 +71,28 @@ function extensionEndpoints(app) {
       }
     }
   );
+
+  app.post(
+    "/ext/confluence",
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
+    async (request, response) => {
+      try {
+        const responseFromProcessor =
+          await new CollectorApi().forwardExtensionRequest({
+            endpoint: "/ext/confluence",
+            method: "POST",
+            body: request.body,
+          });
+        await Telemetry.sendTelemetry("extension_invoked", {
+          type: "confluence",
+        });
+        response.status(200).json(responseFromProcessor);
+      } catch (e) {
+        console.error(e);
+        response.sendStatus(500).end();
+      }
+    }
+  );
 }
 
 module.exports = { extensionEndpoints };
-- 
GitLab