Last updated Sep 30, 2023

Build a Confluence keyword extractor with Forge and OpenAI

This tutorial uses UI Kit 1 and @forge/cli version 7.1.0. This tutorial won't work if you're using the latest version of @forge/cli.

If you frequently work with Confluence pages, you may find yourself spending a lot of time manually categorizing and organizing content. This can be a time-consuming process that takes away from other important tasks. With an app that extracts keywords from your Confluence pages, you can save time and streamline the organization process. Instead of sifting through large quantities of information, simply let the app do the work for you by identifying key themes and topics within your pages.

In this tutorial, we will build a Forge app that integrates with OpenAI APIs to extract keywords from the content of a Confluence page.

Before you begin

Make sure you have the following:

The tutorial uses these components to build the app: permissions, fragment, ContentAction, fetch API, api.asApp(), useProductContext, and useState.

  • This sample app makes use of the OpenAI npm package version 3.3.0. Version 4.x.x is currently incompatible with the Forge runtime environment.
  • This app also includes polyfilling tty.isatty due to a limitation in the Forge runtime.

Set up a cloud developer site

An Atlassian cloud developer site lets you install and test your app on Confluence and Jira products set up for you. If you don't have one yet, set it up now:

  1. Go to http://go.atlassian.com/cloud-dev and create a site using the email address associated with your Atlassian account.
  2. Once your site is ready, log in and complete the setup wizard.

You can install your app to multiple Atlassian sites. However, app data won't be shared between separate Atlassian sites, products, or Forge environments.

The limits on the numbers of users you can create are as follows:

  • Confluence: 5 users
  • Jira Service Management: 1 agent
  • Jira Software and Jira Work Management: 5 users

The Atlassian Marketplace doesn't currently support cross-product apps. If your app supports multiple products, you can publish two separate listings on the Marketplace, but your app won't be able to make API calls across different products and instances/installations.

About the app

Demo of the finished app

Animation showing the Confluence keyword extractor app in action

The GIF above is an example of how the app will work. When a user clicks Keyword extractor in the three dots menu, the extracted keywords will be added to the page as Confluence labels.

You can find the source code for this demo here.

How does the app work?

Confluence keyword extractor high level diagram

A high-level outline of how the app works is:

  1. First, the user requests the keywords. This is as simple as a clicking a button, as shown in the demo above.
  2. The Keyword extractor app gets all the content from the particular Confluence page using the Confluence API.
  3. The Keyword extractor app then passes the content to OpenAI along with a prompt to generate keywords.
  4. Once the app gets the keywords, they are added as labels to the page.

Let’s see how to build this app.

Step 1: Create the app

Assuming your development environment is set up, you can get right to it. Follow these steps:

  1. Create a new project by running forge create.
  2. You’ll be asked to give your app a name, such as keyword-extractor.
  3. Select a template to help you build the app. In this case, select the category UI kit, the Confluence product, and then the template confluence-content-action.
  4. Try to deploy the app and see how it looks. In your terminal, navigate to the app’s directory and run forge deploy.
  5. Run forge install to install the app on your Confluence instance. You’ll be asked to provide a destination site.
  6. Once the app is installed, you’ll be able to access it from the three dots menu on any Confluence page on your site. If you used the example name suggested here, it’ll appear as keyword-extractor(DEVELOPMENT). This is part of the behavior of the ContentAction component.

Confluence keyword extractor in three dots menu

Step 2: Get all the content of a Confluence page via REST API

Update the manifest to include the required permissions

manifest.yml

1
2
modules:
  jira:issuePanel:
    - key: summarizer-hello-world-panel
      function: main
      title: summarizer
      icon: https://developer.atlassian.com/platform/forge/images/icons/issue-panel-icon.svg
  function:
    - key: main
      handler: index.run
permissions:
  scopes:
    - 'read:jira-work'
  external:
    fetch:
      backend:
        - 'api.openai.com'
app:
  id: <your-app-id>

To call certain Confluence and external APIs, you need to give your app permission to do so. This is done by adding scopes and external permissions to the app’s manifest.yml file. This app will call three APIs:

  1. Get page API - The read:page:confluence permission is required to call this API.
  2. Add labels to content API - The write:confluence-content and write:label:confluence permissions are required to call this API.
  3. OpenAI API - The external.fetch.backend permission is used to define external domains your Forge functions can talk to.

The confluence:contentAction module entry was added by the confluence-content-action template. You can learn more about this module here.

Update index.jsx with the main top-level logic for your app

The top-level code calls other functions to interact with the Confluence and ChatGPT APIs, which you’ll add as you work through the tutorial.

index.jsx

1
2
// Import necessary libraries and modules
import ForgeUI, { render, ContentAction, useState } from '@forge/ui';
import { useProductContext } from "@forge/ui";
import api, { route } from "@forge/api";
import { Configuration, OpenAIApi } from 'openai';
import tty from 'tty';

// Define the main component of the app
const App = () => {
  // Get the current context (e.g., Confluence page) information
  const context = useProductContext();
  const pageId = context.contentId

  // Use state to fetch and store page data asynchronously
  const [pageData] = useState(async () => {
    return await getPageData(pageId);
  });

  // Define a prompt to be used for the OpenAI API
  const prompt = `Here is the data:"${pageData}"
  Give me the 5 most important keywords from the text. Return the results in the form of a JavaScript array. 
  The response shouldn't contain anything apart from the array. No extra text or JavaScript formatting.`

  // Use state to call the OpenAI API and store the result (keywords)
  const [keywords] = useState(async () => {
    return await callOpenAI(prompt);
  });

  // Use state to add the extracted keywords as labels to the current page
  const [response] = useState(async () => {
    return await addKeywordsToLabels(keywords, pageId);
  });

  // Log the response from adding keywords as labels
  console.log(response)

  // Render nothing, as the main purpose is API interactions and data processing
  return (null);
};

// Render the main component within a ContentAction
export const run = render(
  <ContentAction>
    <App/>
  </ContentAction>
);

This is the main part of the app, which contains the top-level logic to call APIs and render the UI.

  • The app imports the UI components and API methods it'll use.
  • The run function is executed. This is handled in manifest.yml by defining index.run.
  • App() is then triggered. This is where all the magic happens:
    • Try to get the current page ID using useProductContext.
    • Then, try to get all the content in the page using the getPageData() method, which is defined later in this tutorial.
    • After the content is retrieved, create the prompt to be used by ChatGPT.
    • Pass that prompt to OpenAI via an API call using the callOpenAI() method.
    • Add the retrieved keywords to the current page using the addKeywordsToLabels() method.

Update index.jsx to call a Confluence API to get the content of a page

index.jsx

1
2
// Function to fetch page data from Confluence
const getPageData = async (pageId) => {
  const response = await api.asApp().requestConfluence(route`/wiki/api/v2/pages/${pageId}?body-format=storage`, {
    headers: {
      'Accept': 'application/json'
    }
  });
  
  // Log the response status and text
  console.log(`Response: ${response.status} ${response.statusText}`);

  // Extract and return the content of the page
  const responseData = await response.json()
  const returnedData = responseData.body.storage.value
  
  return returnedData
}

This function calls the get page API using the api.asApp() method.

  • Once the app retrieves all the content using the Confluence API, try to extract only the text from the page and exclude other data like created at and author.
  • The app will use this data to construct the prompt variable it sends to ChatGPT.

Step 3: Integrate your app with the OpenAI API

Now that the app can retrieve all the content of a Confluence page via an API, the next step is to pass it to the OpenAI API to get the keywords.

Update index.jsx to call the ChatGPT API to retrieve the keywords

In the previous step, you added the variable prompt, then constructed a prompt using the page content and a command that tells OpenAI what to do with that data - in this case, extract keywords. The code passes the prompt variable to the callOpenAI function, which calls the OpenAI API and returns the results.

Here is the code for the callOpenAI function:

index.jsx

1
2
// Function to interact with the OpenAI API using a given prompt
const callOpenAI = async (prompt) => {

  // Polyfilling tty.isatty due to a limitation in the Forge runtime
  // This is done to prevent an error caused by a missing dependency
  tty.isatty = () => { return false };

  // Create a configuration object for the OpenAI API
  const configuration = new Configuration({
    apiKey: process.env.OPEN_API_KEY,          // Replace with your actual API key
    organisation: process.env.OPEN_ORG_ID     // Replace with your actual organisation ID
  });

  // Log the API configuration for debugging purposes
  console.log(configuration)

  // Create an instance of the OpenAIApi with the provided configuration
  const openai = new OpenAIApi(configuration);

  // Log the prompt that will be sent to the OpenAI API
  console.log(prompt)
  
  // Create a chat completion request using the OpenAI API
  const chatCompletion = await openai.createChatCompletion({
    model: "gpt-3.5-turbo",  // Specify the model to use (GPT-3.5 Turbo)
    messages: [{
      role: "user",         // Role of the user in the conversation
      content: prompt       // The user's input prompt
    }]
  });
  
  // Extract the response content from the API response
  const response = chatCompletion.data.choices[0].message.content;
  
  // Log the generated response for debugging purposes
  console.log("Prompt response - " + response);
  
  // Return the generated response from the OpenAI API
  return response;
}

Here, the app makes a basic API call to OpenAI. You can learn more about this through their documentation. The steps involved include:

  • Create a configuration object for the OpenAI API with information like your API key and organization ID.
  • Create an instance of the OpenAI API with the provided configuration.
  • Make an API call using the createChatCompletion()method and pass the prompt to it.
  • Extract the content from the API response.

The OPEN_API_KEY environment variable is where you set the OpenAI API key that is needed to interact with their APIs. You might also need to pass an organisation ID, here referred to as OPEN_ORG_ID.

To create an environment variable in Forge, enter the following command in your terminal:

1
2
forge variables set --encrypt OPEN_API_KEY your-api-key
forge variables set --encrypt OPEN_ORG_ID your-org-id

The --encrypt flag instructs Forge to store the variable in encrypted form.

Step 4: Add keywords to the page as labels

The last step is to add the keywords in the form of a JavaScript array as labels to the page. We’re going to do that using the add labels to content API.

index.jsx

1
2
// Function to add keywords as labels to the current page
const addKeywordsToLabels = async (keywords, pageId) => {
  // Parse the keywords and prepare them for adding as labels
  const bodyData = JSON.parse(keywords).map(label => ({
    prefix: "global",
    name: label.split(" ").join("-")
  }));

  // Log the formatted data to be added as labels
  console.log(`bodyData - ${JSON.stringify(bodyData)}`)

  // Make a request to the Confluence API to add labels to the page
  const response = await api.asApp().requestConfluence(route`/wiki/rest/api/content/${pageId}/label`, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(bodyData)
  });
  
  // Log the response status and text
  console.log(`Response: ${response.status} ${response.statusText}`);
  
  // Parse and return the response JSON
  const responseJson = await response.json()

  return responseJson
}

Step 5: Deploy your app

Now it’s time to:

  • Run forge deploy in the terminal again as the manifest.yml file was updated.
  • Run forge install --upgrade and select the installation to upgrade. If you have followed along with this tutorial, it should list the development environment for your Confluence instance.
  • Try out the app in your cloud instance. The first time you run it, Atlassian asks you for permission for that app to access Confluence content and your user information.

Next steps

You've shown incredible dedication and skill by finishing the Forge app tutorial. Well done! If you need help, reach out to our developer community. Keep up the excellent work and continue to explore new opportunities for your apps using Forge and OpenAI technology.

Rate this page: