Einführung
Auf der Suche nach aufschlussreichen Analysen komplexer Jira- und Confluence-Projekte haben wir uns für das Potenzial von KI begeistert. Das Endziel - ein Chatbot, der mit allen notwendigen Tools ausgestattet ist, um Fragen zu beantworten, die mit den Standardtools von Jira oft schwer zu beantworten sind.
Wir haben zunächst versucht, etwas mit LangChain zu bauen, allerdings lässt die Leistung sehr zu wünschen übrig. Da wir uns mit verschiedenen Threads auf Hacker News von Leuten mit ähnlichen Problemen identifizierten, beschlossen wir, die Dinge selbst in die Hand zu nehmen und unsere eigene Lösung zu bauen.
OpenAI als Retter
Hier kommt die OpenAI Chat Completions API ins Spiel. Mit diesem raffinierten Tool können wir Funktionen definieren, die der KI-Assistent selbst aufrufen kann, ähnlich wie ChatGPT-Plugins funktionieren. Dieser Ansatz schien wie geschaffen für unser Problem.
Nachdem wir alle Vorbereitungen getroffen haben, konnte es losgehen. Wir erstellten ein neues TypeScript-Projekt mit Hilfe von Bun und starteten mit dem Projekt.
Triff den CLI Chat
Unsere erste Aufgabe war die Einrichtung eines CLI-Chats, um die Durchführbarkeit unseres Vorhabens zu prüfen. Wie sich herausstellte, hat es wunderbar funktioniert. Hier ist ein kurzer Blick auf den ursprünglichen 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: `
16Du bist ein Chatbot, der Fragen zu Jira-Projekten beantwortet.
17Zu den Informationsquellen gehört das Unternehmen Jira, das Informationen über Projekte enthält.
18
19Denke dir keine eigenen Antworten aus. Verwende IMMER das/die bereitgestellte(n) Tool(s), um die Antworten auf die Fragen zu finden.
20Ein Funktionen soll mehrfach aufgerufen werden, sogar wiederholt, um die Antwort auf eine Frage zu finden.
21Rufe lieber eine Funktion öfter auf, als eine falsche Antwort zu riskieren.
22Man könnte z. B. die Funktion get_issue verwenden, um alle zugehörigen Fragen zu lesen.
23 `,
24 }, {
25 role: "user",
26 content: await rl.question("Frage: "),
27 }];
28
29 while (true) {
30 // Alte Nachrichten löschen, wenn das Token-Limit überschritten ist
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 // Hier übergeben wir die Funktionen, die der Assistent aufrufen darf, mehr dazu später
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: `
65Ich habe folgende Antwort von der Funktion erhalten:
66${result}
67Wenn das die Frage nicht beantwortet, versuche, die Funktion erneut mit anderen Parametern aufzurufen oder eine andere Funktion zu verwenden.
68 `,
69 });
70 }
71 } else {
72 const answer = await rl.question("Antwort: ");
73 messages.push({
74 role: "user",
75 content: answer,
76 });
77 }
78 }
79}
80
Funktionsdefinition
Als Nächstes haben wir mehrere Funktionen definiert, um die erforderlichen Daten abzurufen:
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 Abfrage von jira mit einer jql-Abfrage.
14 Liefert bis zu 3 Ergebnisse und Metadaten über die Abfrage, wie die Gesamtzahl der Ergebnisse.
15 Nützlich für Aggregationen und zur Beantwortung von Fragen wie "Wie viele Probleme gibt es mit der Karte?".
16 Verwenden Sie startAt, um durch die Ergebnisse zu paginieren.
17
18 JQL ist die Abfragesprache von Jira, nützlich für die Suche nach Issues und für Aggregationen.
19 \jql
20 status = "Zu erledigen" OR status = "In Bearbeitung" OR status = "Abgeschlossen"
21 \`\`\`
22 Gültige Vorgangsarten sind: Story, Bug, Epic, Task, Sub-Task
23 `,
24 parameters: {
25 type: "object",
26 properties: {
27 query: {
28 type: "string",
29 description: "Die auszuführende jql-Abfrage.",
30 },
31 startAt: {
32 type: "number",
33 description: "Der Index des ersten Ergebnisses, das zurückgegeben wird. Nützlich für die Paginierung.",
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 Suche nach Problemen mit einer Freitextabfrage.
50 Dabei wird eine Vektorsuche verwendet, um ähnliche Ausgaben zu finden, so dass der Text nicht genau übereinstimmen muss.
51 Gibt den Schlüssel der Ausgabe zurück, der mit get_issue verwendet werden kann, um weitere Informationen über die Ausgabe zu erhalten.
52 `,
53 parameters: {
54 type: "object",
55 properties: {
56 query: {
57 type: "string",
58 description: "Der Text, nach dem gesucht werden soll.",
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 Ermittelt ein Issue anhand seines Schlüssels.
75 Rufe diese Funktion auf, um z. B. verwandte Einträge aus einem Eintrag zu laden, den Sie über eine Vektorsuche oder eine Jql-Suche erhalten haben.
76
77 Geben Sie NICHT Ihre eigenen Schlüssel ein.
78 `,
79 parameters: {
80 type: "object",
81 properties: {
82 key: {
83 type: "string",
84 description: "Der Schlüssel der Ausgabe zu bekommen.",
85 }
86 }
87 }
88 },
89 fn: async (params: any) => {
90 return getIssue(params.key);
91 }
92}];
93
Die Entwicklung der Funktionsbeschreibungen und Eingabeaufforderungen erwies sich als die schwierigste Aufgabe, da diese für eine reibungslose Nutzung des KI-Assistenten erforderlich sind.
Abrufen der Daten
Wir nutzten Jira.js, um Issues abzurufen, während ein Tool namens Vectra für die Vektorsuche sehr nützlich ist.
Fazit
Erfreut über die vielversprechenden Ergebnisse beschlossen wir, das Modell mit Express in einen Server einzubauen und fügten mit React eine einfache Chat-Oberfläche hinzu. Das Endergebnis ist ein Chatbot, der in der Lage ist, wertvolle Einblicke in komplexe Jira- und Confluence-Projekte zu geben - der vertrauenswürdige Kollege eines jeden Entwicklers.