Skip to content

Commit 531e7ff

Browse files
committed
feat: add generic method to fetch issues by project, document and status
- Add findByProjectDocumentAndStatus method to IssuesRepository - Method accepts IssueStatus parameter (active/inactive) for flexibility - Implement buildStatusCondition helper to build SQL conditions based on status - Update filterOutInactiveCandidates to use the new generic method - Remove unused imports from discover.ts
1 parent 554bba7 commit 531e7ff

File tree

3 files changed

+171
-6
lines changed

3 files changed

+171
-6
lines changed

packages/core/src/repositories/issuesRepository.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,29 @@ export class IssuesRepository extends Repository<Issue> {
227227
.limit(20)
228228
}
229229

230+
async findByProjectDocumentAndStatus({
231+
project,
232+
documentUuid,
233+
status,
234+
}: {
235+
project: Project
236+
documentUuid: string
237+
status: IssueGroup
238+
}) {
239+
const statusCondition = this.buildGroupConditions(status)
240+
return this.db
241+
.select({ uuid: issues.uuid })
242+
.from(issues)
243+
.where(
244+
and(
245+
this.scopeFilter,
246+
eq(issues.projectId, project.id),
247+
eq(issues.documentUuid, documentUuid),
248+
statusCondition,
249+
),
250+
)
251+
}
252+
230253
async fetchIssuesFiltered({
231254
project,
232255
commit,

packages/core/src/services/issues/discover.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
44
import { SpanType } from '../../constants'
55
import { createEvaluationResultV2 } from '../../tests/factories/evaluationResultsV2'
66
import { createEvaluationV2 } from '../../tests/factories/evaluationsV2'
7+
import { createIssue } from '../../tests/factories/issues'
78
import { createProject } from '../../tests/factories/projects'
89
import { createSpan } from '../../tests/factories/spans'
10+
import { createUser } from '../../tests/factories/users'
911
import { createWorkspace } from '../../tests/factories/workspaces'
1012
import * as weaviate from '../../weaviate'
1113
import { discoverIssue } from './discover'
14+
import { ignoreIssue } from './ignore'
1215
import * as sharedModule from './shared'
1316

1417
vi.mock('../../voyage', () => ({
@@ -383,5 +386,124 @@ describe('discoverIssue', () => {
383386
expect(embedding).toBeDefined()
384387
expect(issue).toBeUndefined()
385388
})
389+
390+
it('filters out inactive candidates from discovery results', async () => {
391+
const { workspace } = await createWorkspace({ features: ['issues'] })
392+
const projectResult = await createProject({
393+
workspace,
394+
documents: {
395+
'test-prompt': 'This is a test prompt',
396+
},
397+
})
398+
const { project, commit, documents } = projectResult
399+
const document = documents[0]!
400+
401+
const evaluation = await createEvaluationV2({
402+
document,
403+
commit,
404+
workspace,
405+
})
406+
407+
const span = await createSpan({
408+
workspaceId: workspace.id,
409+
documentUuid: document.documentUuid,
410+
commitUuid: commit.uuid,
411+
type: SpanType.Prompt,
412+
})
413+
414+
const result = await createEvaluationResultV2({
415+
evaluation,
416+
span,
417+
commit,
418+
workspace,
419+
hasPassed: false,
420+
})
421+
422+
// Create an active issue
423+
const { issue: activeIssue } = await createIssue({
424+
workspace,
425+
project,
426+
document,
427+
createdAt: new Date(),
428+
histograms: [
429+
{
430+
commitId: commit.id,
431+
date: new Date(),
432+
count: 1,
433+
},
434+
],
435+
})
436+
437+
// Create an inactive issue (ignored)
438+
const { issue: inactiveIssue } = await createIssue({
439+
workspace,
440+
project,
441+
document,
442+
createdAt: new Date(),
443+
histograms: [
444+
{
445+
commitId: commit.id,
446+
date: new Date(),
447+
count: 1,
448+
},
449+
],
450+
})
451+
452+
const user = await createUser()
453+
await ignoreIssue({ issue: inactiveIssue, user }).then((r) => r.unwrap())
454+
455+
const mockEmbedding = Array(2048).fill(0.1)
456+
vi.spyOn(sharedModule, 'embedReason').mockResolvedValue({
457+
ok: true,
458+
value: mockEmbedding,
459+
unwrap: () => mockEmbedding,
460+
} as any)
461+
462+
// Mock Weaviate to return both active and inactive issues as candidates
463+
const mockHybrid = vi.fn().mockResolvedValue({
464+
objects: [
465+
{
466+
uuid: activeIssue.uuid,
467+
properties: {
468+
title: activeIssue.title,
469+
description: activeIssue.description,
470+
},
471+
metadata: { score: 0.95 },
472+
},
473+
{
474+
uuid: inactiveIssue.uuid,
475+
properties: {
476+
title: inactiveIssue.title,
477+
description: inactiveIssue.description,
478+
},
479+
metadata: { score: 0.9 },
480+
},
481+
],
482+
})
483+
484+
const mockCollection = {
485+
query: {
486+
hybrid: mockHybrid,
487+
},
488+
}
489+
490+
vi.spyOn(weaviate, 'getIssuesCollection').mockResolvedValue(
491+
mockCollection as any,
492+
)
493+
494+
const discovering = await discoverIssue({
495+
result: { result, evaluation },
496+
document,
497+
project,
498+
})
499+
500+
expect(discovering.error).toBeFalsy()
501+
const { embedding, issue } = discovering.unwrap()
502+
expect(embedding).toBeDefined()
503+
// Should only return the active issue, not the inactive one
504+
expect(issue).toBeDefined()
505+
expect(issue?.uuid).toBe(activeIssue.uuid)
506+
expect(issue?.uuid).not.toBe(inactiveIssue.uuid)
507+
})
386508
})
387509
})

packages/core/src/services/issues/discover.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { UnprocessableEntityError } from '../../lib/errors'
2020
import { hashContent } from '../../lib/hashContent'
2121
import { Result, TypedResult } from '../../lib/Result'
22+
import { IssuesRepository } from '../../repositories'
2223
import { type DocumentVersion } from '../../schema/models/types/DocumentVersion'
2324
import { type Project } from '../../schema/models/types/Project'
2425
import { type ResultWithEvaluationV2 } from '../../schema/types'
@@ -76,7 +77,12 @@ export async function discoverIssue<
7677

7778
embedding = normalizeEmbedding(embedding)
7879

79-
const finding = await findCandidates({ reason, embedding, document, project })
80+
const finding = await findCandidates({
81+
reason,
82+
embedding,
83+
document,
84+
project,
85+
})
8086
if (finding.error) {
8187
return Result.error(finding.error)
8288
}
@@ -114,11 +120,23 @@ async function findCandidates({
114120
document: DocumentVersion
115121
project: Project
116122
}) {
123+
async function filterOutInactiveCandidates(
124+
candidates: IssueCandidate[],
125+
): Promise<IssueCandidate[]> {
126+
const issuesRepo = new IssuesRepository(project.workspaceId)
127+
const inactiveIssues = await issuesRepo.findByProjectDocumentAndStatus({
128+
project,
129+
documentUuid: document.documentUuid,
130+
status: 'inactive',
131+
})
132+
const inactiveUuids = new Set(inactiveIssues.map((issue) => issue.uuid))
133+
return candidates.filter((candidate) => !inactiveUuids.has(candidate.uuid))
134+
}
135+
117136
try {
118137
const tenantName = ISSUES_COLLECTION_TENANT_NAME(project.workspaceId, project.id, document.documentUuid) // prettier-ignore
119-
const issues = await getIssuesCollection({ tenantName })
120-
121-
const { objects } = await issues.query.hybrid(reason, {
138+
const issuesCollection = await getIssuesCollection({ tenantName })
139+
const { objects } = await issuesCollection.query.hybrid(reason, {
122140
vector: embedding,
123141
alpha: ISSUE_DISCOVERY_SEARCH_RATIO,
124142
maxVectorDistance: 1 - ISSUE_DISCOVERY_MIN_SIMILARITY,
@@ -130,7 +148,6 @@ async function findCandidates({
130148
returnProperties: ['title', 'description'],
131149
returnMetadata: ['score'],
132150
})
133-
134151
const candidates = objects
135152
.map((object) => ({
136153
uuid: object.uuid,
@@ -139,8 +156,11 @@ async function findCandidates({
139156
score: object.metadata!.score!,
140157
}))
141158
.slice(0, ISSUE_DISCOVERY_MAX_CANDIDATES)
159+
if (candidates.length === 0) return Result.ok<IssueCandidate[]>([])
142160

143-
return Result.ok<IssueCandidate[]>(candidates)
161+
const activeCandidates = await filterOutInactiveCandidates(candidates)
162+
163+
return Result.ok<IssueCandidate[]>(activeCandidates)
144164
} catch (error) {
145165
return Result.error(error as Error)
146166
}

0 commit comments

Comments
 (0)