Zwei Entwickler halten ein Meeting mit Laptop und Notizblock
Lucas, Kiya | 23.1.2024

Building an AI Chatbot with OpenAI for Jira

AI > Building an AI Chatbot with OpenAI for Jira

Introduction

In the quest to provide insightful analyses into complex Jira and Confluence projects, we found ourselves enamored by the potential of AI. The end goal - a chatbot equipped with all the tools necessary to answer questions that are often difficult to figure out with Jira's out-of-the-box toolset.

We initially attempted to build something with LangChain but its performance left much to be desired. Finding ourselves identifying with various threads on Hacker News by people with similar struggles, we decided to take matters into our own hands and build our own solution.

OpenAI to the Rescue

Enter the OpenAI Chat Completions API. This nifty tool lets us define functions that the AI assistant itself can call, akin to how chatGPT plugins operate. This approach seemed tailor-made for our problem.

With our course set, it was time to put the pedal to the metal. We established a new TypeScript project with the help of bun and got to coding.

Meet the CLI Chat

Our first order of business was creation a CLI chat to assess the viability of our endeavor. As it turns out, it worked beautifully. Here's a brief look at the initial code:

1import {functions} from "./functions";
2import type {ChatCompletionMessage} from "openai/resources/chat";
3import {ai} from "./api/openai";
4import * as readline from 'node:readline/promises';
5import {GPTTokens} from "gpt-tokens/index";
6
7export async function run() {
8  const rl = readline.createInterface({
9    input: process.stdin,
10    output: process.stdout
11  });
12
13  const messages: ChatCompletionMessage[] = [{
14    role: "system",
15    content: `
16You are a chatbot answering questions about Jira Projects.
17Your sources for information include the company Jira, containing information about projects.
18
19Don't come up with your own answer. ALWAYS use the provided tool(s) to find the answers to the questions.
20You are encouraged to call functions multiple times, even repeatedly, to find the answer to a question.
21Rather call a function a lot than risk giving a wrong answer.
22You can use the get_issue function to read all related issues, for example.
23    `,
24  }, {
25    role: "user",
26    content: await rl.question("Question: "),
27  }];
28
29  while (true) {
30    // Delete old messages if we're over the token limit
31    while (new GPTTokens({
32      model: "gpt-4",
33      messages: messages as any,
34    }).completionUsedTokens > 4096) {
35      messages.splice(1, 1);
36    }
37
38    const response = await ai.chat.completions.create({
39      model: "gpt-4",
40      messages: messages,
41      temperature: 0,
42      // Here we pass the functions that the assistant may call, more on that later
43      functions: functions.map((f: any) => (f.definition))
44    }, {
45      maxRetries: 10,
46    });
47
48    console.log(response.choices[0].message);
49    messages.push(response.choices[0].message)
50    const choice = response.choices[0];
51
52    if (choice.finish_reason === "function_call") {
53      const fn = functions.find((f: any) => f.definition.name === choice.message.function_call?.name);
54
55      console.log("Calling function", fn?.definition.name, choice.message.function_call?.arguments);
56
57      if (fn) {
58        const result = await fn.fn(JSON.parse(choice.message.function_call?.arguments as string));
59
60        console.log("Result", result);
61
62        messages.push({
63          role: "system",
64          content: `
65Got the following response from the function:
66${result}
67If this doesn't answer your question, try calling the function again with different parameters or try another function.   
68            `,
69        });
70      }
71    } else {
72      const answer = await rl.question("Reply: ");
73      messages.push({
74        role: "user",
75        content: answer,
76      });
77    }
78  }
79}
80

Function Definitions

Next, we defined several functions to fetch the requisite data. Here they are in all their glory:

1import {getIssue, jqlSearch} from "./jira-function";
2import {vectorSearch} from "./vector-seach";
3
4
5export const functions: {
6  definition: any,
7  fn: (props: any) => Promise<string>,
8}[] = [{
9
10  definition: {
11    name: "jql_query",
12    description: `
13    Query jira with a jql query. 
14    Returns up to 3 results and metadata about the query, like total result count. 
15    Useful to do aggregations and answer questions like "how many issues are there about the map?".
16    Use startAt to paginate through the results.
17    
18    JQL is Jira's query language, useful to search for issues and do aggregations.
19    \`\`\`jql 
20    status = "To Do" OR status = "In Progress" OR status = "Closed"
21    \`\`\`
22    Valid issue types are: Story, Bug, Epic, Task, Sub-Task
23    `,
24    parameters: {
25      type: "object",
26      properties: {
27        query: {
28          type: "string",
29          description: "The jql query to execute.",
30        },
31        startAt: {
32          type: "number",
33          description: "The index of the first result to return. Useful for pagination.",
34        }
35      }
36    },
37  },
38
39  fn: async (params: any) => {
40    console.log("Doing jql search", params);
41
42    return await jqlSearch(params.query, params.startAt);
43  }
44}, {
45
46  definition: {
47    name: "vector_search",
48    description: `
49    Query for issues with a free text query.
50    This uses a vector search to find similar issues, so the text doesn't have to match exactly.
51    Returns the issue key, which can be used with get_issue to get more information about the issue.
52    `,
53    parameters: {
54      type: "object",
55      properties: {
56        query: {
57          type: "string",
58          description: "The text to search for.",
59        }
60      }
61    }
62  },
63
64  fn: async (params: any) => {
65    const issues = await vectorSearch(params.query);
66
67    return JSON.stringify(issues, null, 2);
68  }
69
70}, {
71  definition: {
72    name: "get_issue",
73    description: `
74    Get an issue by its key.
75    Call this to e.g. load related issues from an issue you got from a vector search or jql search.
76    
77    DO NOT come up with your own issue keys.
78    `,
79    parameters: {
80      type: "object",
81      properties: {
82        key: {
83          type: "string",
84          description: "The key of the issue to get.",
85        }
86      }
87    }
88  },
89  fn: async (params: any) => {
90    return getIssue(params.key);
91  }
92}];
93

Devising the function descriptions and prompts proved to be the toughest aspect of this step, necessary as they were for smooth utilization by the AI assistant.

image

Fetching the data

We utilized the Jira.js to fetch issues while a tool called vectra came in handy for vector searches.

The Final Product

Delighted with the promising results, we decided to build it into a server with express and added a simple Chat UI using react. The end result is a chatbot capable of providing valuable insights into complex Jira and Confluence projects - every developer's trusted comrade.

Content
  • How can AI enhance Jira and Confluence Insights?
  • How does OpenAI stack up as a solution?
  • How can we setup a Typescript project with bun?
  • How do we define the necessary functions for data retrieval?
  • What challenges does one face while working with AI to build a chatbot?
Lucas Meurer
Lucas (Softwareentwickler)

... ist mit Leib und Seele vielseitiger Full-Stack-Entwickler am Standort Hannover. Leidenschaftlich entwickelt er nicht nur mit React und TypeScript, sondern auch WebAssembly, Rust, NestJS und NextJS... mehr anzeigen

Github
Kiya

... ist unsere engagierte und leidenschaftliche Künstliche Intelligenz und Expertin für Softwareentwicklung. Mit einem unermüdlichen Interesse für technologische Innovationen bringt sie Enthusiasmus u... mehr anzeigen

Standort Hannover

newcubator GmbH
Bödekerstraße 22
30161 Hannover

Standort Dortmund

newcubator GmbH
Westenhellweg 85-89
44137 Dortmund