Skip to content

Commit bc1b9a4

Browse files
committed
Update Jira endpoint to v3
Update Jira query to return all issue fields, otherwise, no fields will be returned. Add ADFNode and utillty functions to format to text Add optional filter function to filter Jira tickets Make use of destructuring the Jira fields instead of repeatedly access issue.field.<attribute>
1 parent 6f8fa47 commit bc1b9a4

File tree

2 files changed

+128
-66
lines changed

2 files changed

+128
-66
lines changed

‎.changeset/metal-llamas-shave.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@langchain/community": major
3+
---
4+
5+
Update Jira document loader with updated v3 API jql endpoint
6+
7+
Update Jira JQL query to specify fields, otherwise no fields will be returned
8+
9+
Add option filter function
10+
11+
Refactor to use object de-structuring of the issue fields

‎libs/langchain-community/src/document_loaders/web/jira.ts‎

Lines changed: 117 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export type JiraIssue = {
122122
fields: {
123123
assignee?: JiraUser;
124124
created: string;
125-
description: string;
125+
description: ADFNode;
126126
issuelinks: JiraIssueLink[];
127127
issuetype: JiraIssueType;
128128
labels?: string[];
@@ -151,6 +151,28 @@ export type JiraAPIResponse = {
151151
issues: JiraIssue[];
152152
};
153153

154+
export interface ADFNode {
155+
type: string;
156+
text?: string;
157+
content?: ADFNode[];
158+
}
159+
160+
export interface ADFDocument extends ADFNode {
161+
type: "doc";
162+
version: number;
163+
content: ADFNode[];
164+
}
165+
166+
export function adfToText(adf: ADFNode | null | undefined): string {
167+
if (!adf || !adf.content) return "";
168+
const recur = (node: ADFNode): string => {
169+
if (node.text) return node.text;
170+
if (node.content) return node.content.map(recur).join("");
171+
return "";
172+
};
173+
return recur(adf).trim();
174+
}
175+
154176
/**
155177
* Interface representing the parameters for configuring the
156178
* JiraDocumentConverter.
@@ -194,88 +216,93 @@ export class JiraDocumentConverter {
194216
private formatIssueInfo({
195217
issue,
196218
host,
197-
}: {
219+
}: {
198220
issue: JiraIssue;
199221
host: string;
200-
}): string {
222+
}): string {
223+
const {
224+
project,
225+
status,
226+
priority,
227+
issuetype,
228+
creator,
229+
labels,
230+
created,
231+
updated,
232+
reporter,
233+
assignee,
234+
duedate,
235+
timeestimate,
236+
timespent,
237+
resolutiondate,
238+
description,
239+
progress,
240+
parent,
241+
subtasks,
242+
issuelinks,
243+
} = issue.fields;
244+
201245
let text = `Issue: ${this.formatMainIssueInfoText({ issue, host })}\n`;
202-
text += `Project: ${issue.fields.project.name} (${issue.fields.project.key}, ID ${issue.fields.project.id})\n`;
203-
text += `Status: ${issue.fields.status.name}\n`;
204-
text += `Priority: ${issue.fields.priority.name}\n`;
205-
text += `Type: ${issue.fields.issuetype.name}\n`;
206-
text += `Creator: ${issue.fields.creator?.displayName}\n`;
207-
208-
if (issue.fields.labels && issue.fields.labels.length > 0) {
209-
text += `Labels: ${issue.fields.labels.join(", ")}\n`;
246+
text += `Project: ${project.name} (${project.key}, ID ${project.id})\n`;
247+
text += `Status: ${status.name}\n`;
248+
text += `Priority: ${priority.name}\n`;
249+
text += `Type: ${issuetype.name}\n`;
250+
text += `Creator: ${creator?.displayName}\n`;
251+
252+
if (labels?.length) {
253+
text += `Labels: ${labels.join(", ")}\n`;
210254
}
211255

212-
text += `Created: ${issue.fields.created}\n`;
213-
text += `Updated: ${issue.fields.updated}\n`;
256+
text += `Created: ${created}\n`;
257+
text += `Updated: ${updated}\n`;
214258

215-
if (issue.fields.reporter) {
216-
text += `Reporter: ${issue.fields.reporter.displayName}\n`;
259+
if (reporter) {
260+
text += `Reporter: ${reporter.displayName}\n`;
217261
}
218262

219-
text += `Assignee: ${issue.fields.assignee?.displayName ?? "Unassigned"}\n`;
263+
text += `Assignee: ${assignee?.displayName ?? "Unassigned"}\n`;
220264

221-
if (issue.fields.duedate) {
222-
text += `Due Date: ${issue.fields.duedate}\n`;
265+
if (duedate) {
266+
text += `Due Date: ${duedate}\n`;
223267
}
224-
225-
if (issue.fields.timeestimate) {
226-
text += `Time Estimate: ${issue.fields.timeestimate}\n`;
268+
if (timeestimate) {
269+
text += `Time Estimate: ${timeestimate}\n`;
227270
}
228-
229-
if (issue.fields.timespent) {
230-
text += `Time Spent: ${issue.fields.timespent}\n`;
271+
if (timespent) {
272+
text += `Time Spent: ${timespent}\n`;
231273
}
232-
233-
if (issue.fields.resolutiondate) {
234-
text += `Resolution Date: ${issue.fields.resolutiondate}\n`;
274+
if (resolutiondate) {
275+
text += `Resolution Date: ${resolutiondate}\n`;
235276
}
236-
237-
if (issue.fields.description) {
238-
text += `Description: ${issue.fields.description}\n`;
277+
if (description) {
278+
text += `Description: ${adfToText(description)}\n`;
239279
}
240-
241-
if (issue.fields.progress?.percent) {
242-
text += `Progress: ${issue.fields.progress.percent}%\n`;
280+
if (progress?.percent) {
281+
text += `Progress: ${progress.percent}%\n`;
243282
}
244283

245-
if (issue.fields.parent) {
246-
text += `Parent Issue: ${this.formatMainIssueInfoText({
247-
issue: issue.fields.parent,
248-
host,
249-
})}\n`;
284+
if (parent) {
285+
text += `Parent Issue: ${this.formatMainIssueInfoText({ issue: parent, host })}\n`;
250286
}
251287

252-
if (issue.fields.subtasks?.length > 0) {
253-
text += `Subtasks:\n`;
254-
issue.fields.subtasks.forEach((subtask) => {
255-
text += ` - ${this.formatMainIssueInfoText({
256-
issue: subtask,
257-
host,
258-
})}\n`;
259-
});
288+
if (subtasks?.length) {
289+
text += `Subtasks:\n`;
290+
subtasks.forEach(subtask => {
291+
text += ` - ${this.formatMainIssueInfoText({ issue: subtask, host })}\n`;
292+
});
260293
}
261294

262-
if (issue.fields.issuelinks?.length > 0) {
263-
text += `Issue Links:\n`;
264-
issue.fields.issuelinks.forEach((link) => {
295+
if (issuelinks?.length) {
296+
text += `Issue Links:\n`;
297+
issuelinks.forEach(link => {
265298
text += ` - ${link.type.name}\n`;
266299
if (link.inwardIssue) {
267-
text += ` - ${this.formatMainIssueInfoText({
268-
issue: link.inwardIssue,
269-
host,
270-
})}\n`;
300+
text += ` - ${this.formatMainIssueInfoText({ issue: link.inwardIssue, host })}\n`;
271301
}
272302
if (link.outwardIssue) {
273-
text += ` - ${this.formatMainIssueInfoText({
274-
issue: link.outwardIssue,
275-
host,
276-
})}\n`;
303+
text += ` - ${this.formatMainIssueInfoText({ issue: link.outwardIssue, host })}\n`;
277304
}
278-
});
305+
});
279306
}
280307

281308
return text;
@@ -321,10 +348,11 @@ export interface JiraProjectLoaderParams {
321348
personalAccessToken?: string;
322349
limitPerRequest?: number;
323350
createdAfter?: Date;
351+
filterFn?: (issue: JiraIssue) => boolean;
324352
}
325353

326354
const API_ENDPOINTS = {
327-
SEARCH: "/rest/api/2/search",
355+
SEARCH: "/rest/api/3/search/jql",
328356
};
329357

330358
/**
@@ -343,6 +371,8 @@ export class JiraProjectLoader extends BaseDocumentLoader {
343371

344372
private readonly createdAfter?: Date;
345373

374+
private readonly filterFn?: (issue: JiraIssue) => boolean;
375+
346376
private readonly documentConverter: JiraDocumentConverter;
347377

348378
private readonly personalAccessToken?: string;
@@ -355,6 +385,7 @@ export class JiraProjectLoader extends BaseDocumentLoader {
355385
limitPerRequest = 100,
356386
createdAfter,
357387
personalAccessToken,
388+
filterFn,
358389
}: JiraProjectLoaderParams) {
359390
super();
360391
this.host = host;
@@ -365,6 +396,7 @@ export class JiraProjectLoader extends BaseDocumentLoader {
365396
this.createdAfter = createdAfter;
366397
this.documentConverter = new JiraDocumentConverter({ host, projectKey });
367398
this.personalAccessToken = personalAccessToken;
399+
this.filterFn = filterFn;
368400
}
369401

370402
private buildAuthorizationHeader(): string {
@@ -379,7 +411,13 @@ export class JiraProjectLoader extends BaseDocumentLoader {
379411
public async load(): Promise<Document[]> {
380412
try {
381413
const allJiraIssues = await this.loadAsIssues();
382-
return this.documentConverter.convertToDocuments(allJiraIssues);
414+
const filtered = allJiraIssues.filter(issue => {
415+
if (this.filterFn) {
416+
return this.filterFn(issue);
417+
}
418+
return true;
419+
});
420+
return this.documentConverter.convertToDocuments(filtered);
383421
} catch (error) {
384422
console.error("Error:", error);
385423
return [];
@@ -416,19 +454,22 @@ export class JiraProjectLoader extends BaseDocumentLoader {
416454
try {
417455
const jqlProps = [
418456
`project=${this.projectKey}`,
419-
...(createdAfterAsString ? [`created>=${createdAfterAsString}`] : []),
457+
...(createdAfterAsString ? [`created>= "${createdAfterAsString}"`] : []),
420458
];
459+
const jql = `${jqlProps.join(" AND ")} ORDER BY created ASC, key ASC`;
460+
421461
const params = new URLSearchParams({
422-
jql: jqlProps.join(" AND "),
462+
jql,
423463
startAt: `${startAt}`,
424464
maxResults: `${this.limitPerRequest}`,
465+
fields: '*all',
425466
});
426467
const pageUrl = `${url}?${params}`;
427468

428469
const options = {
429470
method: "GET",
430471
headers: {
431-
Authorization: authorizationHeader,
472+
Authorization: authorizationHeader,
432473
Accept: "application/json",
433474
},
434475
};
@@ -438,12 +479,22 @@ export class JiraProjectLoader extends BaseDocumentLoader {
438479

439480
if (!data.issues || data.issues.length === 0) break;
440481

441-
yield data.issues;
482+
const allIssues = [];
483+
for (const issue of data.issues) {
484+
allIssues.push(issue);
485+
}
486+
487+
if (allIssues.length > 0) yield allIssues;
488+
442489
startAt += this.limitPerRequest;
490+
491+
if (data.issues.length < this.limitPerRequest) break;
492+
443493
} catch (error) {
444-
console.error(error);
494+
console.error("Error fetching Jira issues:", error);
445495
yield [];
496+
break;
446497
}
447498
}
448499
}
449-
}
500+
}

0 commit comments

Comments
 (0)