From aa0bcc91d953271a1d2ee7621458be023cc32c56 Mon Sep 17 00:00:00 2001 From: Leivur Djurhuus Date: Thu, 12 Mar 2026 17:27:11 -0500 Subject: [PATCH] feat: add relevanceScore to EntityCard and update entity extraction logic --- src/app/api/chat/route.ts | 16 ++++++++++++++-- src/hooks/use-chat.ts | 1 + src/lib/chat/tool-definitions.ts | 6 +++++- src/lib/chat/tool-executor.ts | 4 ++++ src/lib/services/workload-service.ts | 8 ++++++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 68e12bb..89e6e73 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -18,6 +18,7 @@ export interface EntityCard { projectName?: string; projectId?: string; link: string; + relevanceScore?: number; } /** Tools whose results may contain navigable entities */ @@ -77,6 +78,7 @@ function extractEntities(toolName: string, data: any): EntityCard[] { priority: item.priority, code: item.projectCode || item.code, link: `/projects/${item.id}`, + relevanceScore: item._relevanceScore, }); } else if (itemType === "deliverable") { const projId = item.projectId || item.project?.id; @@ -92,6 +94,7 @@ function extractEntities(toolName: string, data: any): EntityCard[] { link: projId ? `/projects/${projId}/deliverables/${item.id}` : `/projects`, + relevanceScore: item._relevanceScore, }); } else if (itemType === "user") { entities.push({ @@ -100,6 +103,7 @@ function extractEntities(toolName: string, data: any): EntityCard[] { name: item.name || "Unnamed User", status: item.department || item.role || "", link: `/workload`, + relevanceScore: item._relevanceScore, }); } else if ( toolName === "list_overdue" || @@ -174,6 +178,7 @@ function extractWorkloadEntities(data: { status: assignment.stageStatus || "", priority: assignment.priority, link: `/projects/${assignment.projectId}`, + relevanceScore: 0.9, }); } } @@ -391,7 +396,7 @@ export async function POST(req: NextRequest) { } } - // Deduplicate, prioritize (projects/deliverables first, then users), cap at 10 + // Deduplicate, prioritize by relevance score first, then type (projects/deliverables first, then users), cap at 10 const TYPE_PRIORITY: Record = { project: 0, deliverable: 1, @@ -399,7 +404,14 @@ export async function POST(req: NextRequest) { user: 3, }; const entities = deduplicateEntities(allEntities) - .sort((a, b) => (TYPE_PRIORITY[a.type] ?? 9) - (TYPE_PRIORITY[b.type] ?? 9)) + .sort((a, b) => { + const scoreA = a.relevanceScore ?? 0; + const scoreB = b.relevanceScore ?? 0; + if (scoreA !== scoreB) { + return scoreB - scoreA; + } + return (TYPE_PRIORITY[a.type] ?? 9) - (TYPE_PRIORITY[b.type] ?? 9); + }) .slice(0, 10); // Send final response diff --git a/src/hooks/use-chat.ts b/src/hooks/use-chat.ts index 6face45..cdb2c65 100644 --- a/src/hooks/use-chat.ts +++ b/src/hooks/use-chat.ts @@ -27,6 +27,7 @@ export interface EntityCard { projectName?: string; projectId?: string; link: string; + relevanceScore?: number; } export interface ChatMessage { diff --git a/src/lib/chat/tool-definitions.ts b/src/lib/chat/tool-definitions.ts index 49d5fbe..732f2bc 100644 --- a/src/lib/chat/tool-definitions.ts +++ b/src/lib/chat/tool-definitions.ts @@ -146,7 +146,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ { name: "get_workload", description: - "Get workload data for all team members showing assignments per week, utilization, and capacity.", + "Get workload data for team members showing assignments per week, utilization, and capacity. Can be filtered by a specific user.", input_schema: { type: "object", properties: { @@ -158,6 +158,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: "string", description: "Filter to a specific project (optional)", }, + userId: { + type: "string", + description: "Filter to a specific user (optional)", + }, }, }, }, diff --git a/src/lib/chat/tool-executor.ts b/src/lib/chat/tool-executor.ts index e031a56..f513544 100644 --- a/src/lib/chat/tool-executor.ts +++ b/src/lib/chat/tool-executor.ts @@ -243,6 +243,7 @@ export async function executeTool( priority: p.priority, dueDate: p.dueDate, _matchedTerms: [] as string[], + _relevanceScore: 1.0, }); } projectMap.get(p.id)._matchedTerms.push(term); @@ -295,6 +296,7 @@ export async function executeTool( subStatus: s.subStatus, })), _matchedTerms: [] as string[], + _relevanceScore: 1.0, }); } deliverableMap.get(d.id)._matchedTerms.push(term); @@ -335,6 +337,7 @@ export async function executeTool( email: u.email, role: u.role, department: u.department, + _relevanceScore: 1.0, }); } } @@ -422,6 +425,7 @@ export async function executeTool( data = await getWorkloadData(context.organizationId, { numWeeks: input.numWeeks, projectId: input.projectId, + userId: input.userId, }); break; } diff --git a/src/lib/services/workload-service.ts b/src/lib/services/workload-service.ts index 5971f28..5d7d7fd 100644 --- a/src/lib/services/workload-service.ts +++ b/src/lib/services/workload-service.ts @@ -95,14 +95,18 @@ export async function getWorkloadData( numWeeks?: number; projectId?: string; stageType?: string; + userId?: string; } = {} ): Promise<{ users: UserWorkload[]; weeks: WeekBucket[] }> { - const { numWeeks = 8, projectId, stageType } = options; + const { numWeeks = 8, projectId, stageType, userId } = options; const weeks = generateWeekBuckets(numWeeks); // Get all users in the organization with their assignments const users = await prisma.user.findMany({ - where: { organizationId }, + where: { + organizationId, + ...(userId ? { id: userId } : {}), + }, select: { id: true, name: true,