/** * AI Assistant Tool definitions for OpenAI Function Calling. * Each tool has a JSON schema (for the AI) and an execute function (for the server). */ import { Prisma, VacationType } from "@capakraken/db"; import { CreateAssignmentSchema, AllocationStatus, PermissionKey, SystemRole, toIsoDateOrNull, } from "@capakraken/shared"; import type { WeekdayAvailability } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { ZodError } from "zod"; import { fmtEur } from "../lib/format-utils.js"; import { timelineRouter } from "./timeline.js"; import { logger } from "../lib/logger.js"; import { createCallerFactory, type TRPCContext } from "../trpc.js"; import { auditLogRouter } from "./audit-log.js"; import { chargeabilityReportRouter } from "./chargeability-report.js"; import { computationGraphRouter } from "./computation-graph.js"; import { dispoRouter } from "./dispo.js"; import { importExportRouter } from "./import-export.js"; import { resourceRouter } from "./resource.js"; import { settingsRouter } from "./settings.js"; import { systemRoleConfigRouter } from "./system-role-config.js"; import { userRouter } from "./user.js"; import { notificationRouter } from "./notification.js"; import { estimateRouter } from "./estimate.js"; import { webhookRouter } from "./webhook.js"; import { countryRouter } from "./country.js"; import { holidayCalendarRouter } from "./holiday-calendar.js"; import { blueprintRouter } from "./blueprint.js"; import { roleRouter } from "./role.js"; import { clientRouter } from "./client.js"; import { orgUnitRouter } from "./org-unit.js"; import { projectRouter } from "./project.js"; import { rateCardRouter } from "./rate-card.js"; import { reportRouter } from "./report.js"; import { vacationRouter } from "./vacation.js"; import { entitlementRouter } from "./entitlement.js"; import { commentRouter } from "./comment.js"; import { managementLevelRouter } from "./management-level.js"; import { utilizationCategoryRouter } from "./utilization-category.js"; import { calculationRuleRouter } from "./calculation-rules.js"; import { effortRuleRouter } from "./effort-rule.js"; import { experienceMultiplierRouter } from "./experience-multiplier.js"; import { dashboardRouter } from "./dashboard.js"; import { insightsRouter } from "./insights.js"; import { scenarioRouter } from "./scenario.js"; import { allocationRouter } from "./allocation/index.js"; import { staffingRouter } from "./staffing.js"; import { advancedTimelineToolDefinitions, createAdvancedTimelineExecutors } from "./assistant-tools/advanced-timeline.js"; import { allocationPlanningMutationToolDefinitions, allocationPlanningReadToolDefinitions, createAllocationPlanningExecutors, } from "./assistant-tools/allocation-planning.js"; import { settingsAdminToolDefinitions, createSettingsAdminExecutors } from "./assistant-tools/settings-admin.js"; import { createVacationHolidayExecutors, vacationHolidayMutationToolDefinitions, vacationHolidayReadToolDefinitions, } from "./assistant-tools/vacation-holidays.js"; import { createRolesAnalyticsExecutors, rolesAnalyticsMutationToolDefinitions, rolesAnalyticsReadToolDefinitions, } from "./assistant-tools/roles-analytics.js"; import { clientMutationToolDefinitions, createClientsOrgUnitsExecutors, orgUnitMutationToolDefinitions, } from "./assistant-tools/clients-org-units.js"; import { chargeabilityComputationReadToolDefinitions, createChargeabilityComputationExecutors, } from "./assistant-tools/chargeability-computation.js"; import { configReadmodelToolDefinitions, createConfigReadmodelExecutors, } from "./assistant-tools/config-readmodels.js"; import { countryMetroAdminToolDefinitions, createCountryMetroAdminExecutors, } from "./assistant-tools/country-metro-admin.js"; import { countryReadmodelToolDefinitions, createCountryReadmodelExecutors, } from "./assistant-tools/country-readmodels.js"; import { createUserSelfServiceExecutors, userSelfServiceToolDefinitions, } from "./assistant-tools/user-self-service.js"; import { createUserAdminExecutors, userAdminToolDefinitions, } from "./assistant-tools/user-admin.js"; import { createNotificationsTasksExecutors, notificationInboxToolDefinitions, notificationTaskToolDefinitions, } from "./assistant-tools/notifications-tasks.js"; import { createEstimateExecutors, estimateMutationToolDefinitions, estimateReadToolDefinitions, } from "./assistant-tools/estimates.js"; import { createProjectExecutors, projectMutationToolDefinitions, projectReadToolDefinitions, } from "./assistant-tools/projects.js"; import { createStaffingDemandExecutors, staffingDemandMutationToolDefinitions, staffingDemandReadToolDefinitions, } from "./assistant-tools/staffing-demand.js"; import { createResourceExecutors, resourceMutationToolDefinitions, resourceReadToolDefinitions, } from "./assistant-tools/resources.js"; import { blueprintsRateCardsToolDefinitions, createBlueprintsRateCardsExecutors, } from "./assistant-tools/blueprints-rate-cards.js"; import { createDashboardInsightsReportsExecutors, dashboardInsightsReportsToolDefinitions, } from "./assistant-tools/dashboard-insights-reports.js"; import { createScenarioRateAnalysisExecutors, scenarioRateAnalysisToolDefinitions, } from "./assistant-tools/scenario-rate-analysis.js"; import { createImportExportDispoExecutors, importExportDispoToolDefinitions, } from "./assistant-tools/import-export-dispo.js"; import { commentMutationToolDefinitions, commentReadToolDefinitions, createCommentExecutors, } from "./assistant-tools/comments.js"; import { auditHistoryToolDefinitions, createAuditHistoryExecutors, } from "./assistant-tools/audit-history.js"; import { createPlanningNavigationExecutors, planningNavigationToolDefinitions, } from "./assistant-tools/planning-navigation.js"; import { createVacationEntitlementExecutors, vacationEntitlementToolDefinitions, } from "./assistant-tools/vacation-entitlements.js"; import { withToolAccess, type ToolAccessRequirements, type ToolContext, type ToolDef, type ToolExecutor, } from "./assistant-tools/shared.js"; export type { ToolContext } from "./assistant-tools/shared.js"; // ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ────── export const MUTATION_TOOLS = new Set([ "import_csv_data", "update_system_settings", "clear_stored_runtime_secrets", "test_ai_connection", "test_smtp_connection", "test_gemini_connection", "update_system_role_config", "create_webhook", "update_webhook", "delete_webhook", "test_webhook", "stage_dispo_import_batch", "cancel_dispo_import_batch", "resolve_dispo_staged_record", "commit_dispo_import_batch", "create_allocation", "cancel_allocation", "update_allocation_status", "update_timeline_allocation_inline", "apply_timeline_project_shift", "quick_assign_timeline_resource", "batch_quick_assign_timeline_resources", "batch_shift_timeline_allocations", "update_resource", "deactivate_resource", "create_resource", "update_project", "create_project", "delete_project", "create_vacation", "approve_vacation", "reject_vacation", "cancel_vacation", "set_entitlement", "create_demand", "fill_demand", "generate_project_cover", "remove_project_cover", "create_role", "update_role", "delete_role", "create_client", "update_client", "delete_client", "create_org_unit", "update_org_unit", "create_country", "update_country", "create_metro_city", "update_metro_city", "delete_metro_city", "create_holiday_calendar", "update_holiday_calendar", "delete_holiday_calendar", "create_holiday_calendar_entry", "update_holiday_calendar_entry", "delete_holiday_calendar_entry", "send_broadcast", "create_task_for_user", "create_reminder", "update_task_status", "execute_task_action", "create_comment", "resolve_comment", "mark_notification_read", "save_dashboard_layout", "toggle_favorite_project", "set_column_preferences", "generate_totp_secret", "verify_and_enable_totp", "create_user", "set_user_password", "update_user_role", "update_user_name", "link_user_resource", "auto_link_users_by_email", "set_user_permissions", "reset_user_permissions", "disable_user_totp", "create_notification", "update_reminder", "delete_reminder", "delete_notification", "assign_task", "clone_estimate", "update_estimate_draft", "submit_estimate_version", "approve_estimate_version", "create_estimate_revision", "create_estimate_export", "create_estimate_planning_handoff", "generate_estimate_weekly_phasing", "update_estimate_commercial_terms", ]); export const ADVANCED_ASSISTANT_TOOLS = new Set([ "find_best_project_resource", "get_timeline_entries_view", "get_timeline_holiday_overlays", "get_project_timeline_context", "preview_project_shift", "update_timeline_allocation_inline", "apply_timeline_project_shift", "quick_assign_timeline_resource", "batch_quick_assign_timeline_resources", "batch_shift_timeline_allocations", "get_chargeability_report", "get_resource_computation_graph", "get_project_computation_graph", ]); const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter); const createComputationGraphCaller = createCallerFactory(computationGraphRouter); const createTimelineCaller = createCallerFactory(timelineRouter); const createAuditLogCaller = createCallerFactory(auditLogRouter); const createImportExportCaller = createCallerFactory(importExportRouter); const createDispoCaller = createCallerFactory(dispoRouter); const createResourceCaller = createCallerFactory(resourceRouter); const createSettingsCaller = createCallerFactory(settingsRouter); const createSystemRoleConfigCaller = createCallerFactory(systemRoleConfigRouter); const createUserCaller = createCallerFactory(userRouter); const createNotificationCaller = createCallerFactory(notificationRouter); const createEstimateCaller = createCallerFactory(estimateRouter); const createWebhookCaller = createCallerFactory(webhookRouter); const createCountryCaller = createCallerFactory(countryRouter); const createHolidayCalendarCaller = createCallerFactory(holidayCalendarRouter); const createBlueprintCaller = createCallerFactory(blueprintRouter); const createRoleCaller = createCallerFactory(roleRouter); const createClientCaller = createCallerFactory(clientRouter); const createOrgUnitCaller = createCallerFactory(orgUnitRouter); const createProjectCaller = createCallerFactory(projectRouter); const createRateCardCaller = createCallerFactory(rateCardRouter); const createReportCaller = createCallerFactory(reportRouter); const createVacationCaller = createCallerFactory(vacationRouter); const createEntitlementCaller = createCallerFactory(entitlementRouter); const createCommentCaller = createCallerFactory(commentRouter); const createManagementLevelCaller = createCallerFactory(managementLevelRouter); const createUtilizationCategoryCaller = createCallerFactory(utilizationCategoryRouter); const createCalculationRuleCaller = createCallerFactory(calculationRuleRouter); const createEffortRuleCaller = createCallerFactory(effortRuleRouter); const createExperienceMultiplierCaller = createCallerFactory(experienceMultiplierRouter); const createDashboardCaller = createCallerFactory(dashboardRouter); const createInsightsCaller = createCallerFactory(insightsRouter); const createScenarioCaller = createCallerFactory(scenarioRouter); const createAllocationCaller = createCallerFactory(allocationRouter); const createStaffingCaller = createCallerFactory(staffingRouter); // ─── Helpers ──────────────────────────────────────────────────────────────── const fmtDate = toIsoDateOrNull; class AssistantVisibleError extends Error { constructor(message: string) { super(message); this.name = "AssistantVisibleError"; } } function assertPermission(ctx: ToolContext, perm: PermissionKey): void { if (!ctx.permissions.has(perm)) { throw new AssistantVisibleError(`Permission denied: you need the "${perm}" permission to perform this action.`); } } function assertAdminRole(ctx: ToolContext): void { if (ctx.userRole !== SystemRole.ADMIN) { throw new AssistantVisibleError("Admin role required to perform this action."); } } function formatHolidayCalendarEntry(entry: { id: string; date: Date; name: string; isRecurringAnnual?: boolean | null; source?: string | null; }) { return { id: entry.id, date: toIsoDateOrNull(entry.date), name: entry.name, isRecurringAnnual: entry.isRecurringAnnual ?? false, source: entry.source ?? null, }; } function formatHolidayCalendar(calendar: { id: string; name: string; scopeType: string; stateCode?: string | null; isActive?: boolean | null; priority?: number | null; country?: { id: string; code: string; name: string } | null; metroCity?: { id: string; name: string } | null; _count?: { entries?: number | null } | null; entries?: Array<{ id: string; date: Date; name: string; isRecurringAnnual?: boolean | null; source?: string | null; }> | null; }) { const entries = calendar.entries?.map(formatHolidayCalendarEntry) ?? []; return { id: calendar.id, name: calendar.name, scopeType: calendar.scopeType, stateCode: calendar.stateCode ?? null, isActive: calendar.isActive ?? true, priority: calendar.priority ?? 0, country: calendar.country ? { id: calendar.country.id, code: calendar.country.code, name: calendar.country.name, } : null, metroCity: calendar.metroCity ? { id: calendar.metroCity.id, name: calendar.metroCity.name, } : null, entryCount: calendar._count?.entries ?? entries.length, entries, }; } function formatCountry(country: { id: string; code: string; name: string; dailyWorkingHours: number; scheduleRules?: Prisma.JsonValue | null; isActive?: boolean | null; metroCities?: Array<{ id: string; name: string }> | null; _count?: { resources?: number | null } | null; }) { return { id: country.id, code: country.code, name: country.name, dailyWorkingHours: country.dailyWorkingHours, scheduleRules: country.scheduleRules ?? null, isActive: country.isActive ?? true, resourceCount: country._count?.resources ?? null, metroCities: (country.metroCities ?? []).map((city) => ({ id: city.id, name: city.name, })), cities: (country.metroCities ?? []).map((city) => city.name), }; } function createUtcDate(year: number, monthIndex: number, day: number): Date { return new Date(Date.UTC(year, monthIndex, day)); } function resolveHolidayPeriod(input: { year?: number; periodStart?: string; periodEnd?: string; }): { year: number | null; periodStart: Date; periodEnd: Date } { if (input.periodStart || input.periodEnd) { if (!input.periodStart || !input.periodEnd) { throw new AssistantVisibleError("periodStart and periodEnd must both be provided when using a custom holiday range."); } const periodStart = new Date(`${input.periodStart}T00:00:00.000Z`); const periodEnd = new Date(`${input.periodEnd}T00:00:00.000Z`); if (Number.isNaN(periodStart.getTime())) { throw new AssistantVisibleError(`Invalid periodStart: ${input.periodStart}`); } if (Number.isNaN(periodEnd.getTime())) { throw new AssistantVisibleError(`Invalid periodEnd: ${input.periodEnd}`); } if (periodEnd < periodStart) { throw new AssistantVisibleError("periodEnd must be on or after periodStart."); } return { year: null, periodStart, periodEnd }; } const year = input.year ?? new Date().getUTCFullYear(); return { year, periodStart: createUtcDate(year, 0, 1), periodEnd: createUtcDate(year, 11, 31), }; } const CONTROLLER_ASSISTANT_ROLES = [ SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER, ] as const; const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial> = { search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, get_project: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, }; const ASSISTANT_VACATION_REQUEST_TYPES = [ VacationType.ANNUAL, VacationType.SICK, VacationType.OTHER, ] as const; function parseAssistantVacationRequestType(input: string): VacationType { const normalized = input.trim().toUpperCase(); if (normalized === VacationType.PUBLIC_HOLIDAY) { throw new AssistantVisibleError("PUBLIC_HOLIDAY requests cannot be created manually. Manage public holidays through holiday calendars instead."); } if ((ASSISTANT_VACATION_REQUEST_TYPES as readonly string[]).includes(normalized)) { return normalized as VacationType; } throw new AssistantVisibleError(`Invalid vacation type: ${input}. Valid types: ${ASSISTANT_VACATION_REQUEST_TYPES.join(", ")}.`); } function parseIsoDate(value: string, fieldName: string): Date { const parsed = new Date(`${value}T00:00:00.000Z`); if (Number.isNaN(parsed.getTime())) { throw new AssistantVisibleError(`Invalid ${fieldName}: ${value}`); } return parsed; } function parseOptionalIsoDate( value: string | undefined, fieldName: string, ): Date | undefined { return value ? parseIsoDate(value, fieldName) : undefined; } function parseDateTime(value: string, fieldName: string): Date { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { throw new AssistantVisibleError(`Invalid ${fieldName}: ${value}`); } return parsed; } function parseOptionalDateTime( value: string | undefined, fieldName: string, ): Date | undefined { return value ? parseDateTime(value, fieldName) : undefined; } function toDate(value: Date | string): Date { return value instanceof Date ? value : new Date(value); } type AssistantToolErrorResult = { error: string }; type AssistantIndexedFieldErrorResult = AssistantToolErrorResult & { field: string; index: number; }; type BatchQuickAssignmentInput = { resourceId: string; projectId: string; startDate: Date; endDate: Date; hoursPerDay?: number; role?: string; status?: AllocationStatus; }; function toAssistantNotFoundError( error: unknown, message: string, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: message }; } return null; } function toAssistantAllocationNotFoundError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError) { if (error.code === "NOT_FOUND") { return { error: "Allocation not found with the given criteria." }; } if (error.message === "Record not found" || error.message.includes("Assignment not found")) { return { error: "Allocation not found with the given criteria." }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (prismaError?.code === "P2025") { return { error: "Allocation not found with the given criteria." }; } return null; } function toAssistantProjectNotFoundError( error: unknown, identifier: string, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, `Project not found: ${identifier}`, ); } function toAssistantTimelineMutationError( error: unknown, context: "updateInline" | "applyShift" | "quickAssign" | "batchShift", ): AssistantToolErrorResult | null { if (error instanceof TRPCError) { if (error.code === "NOT_FOUND") { if (error.message.includes("Resource not found")) { return { error: "Resource not found with the given criteria." }; } if (error.message.includes("Project not found")) { return { error: "Project not found with the given criteria." }; } if (error.message.includes("Demand requirement not found")) { return { error: "Demand requirement not found with the given criteria." }; } if (error.message.includes("No allocations found")) { return { error: "Allocation not found with the given criteria." }; } } if (error.code === "BAD_REQUEST" || error.code === "CONFLICT") { return { error: error.message }; } } const allocationNotFound = toAssistantAllocationNotFoundError(error); if (allocationNotFound && (context === "updateInline" || context === "batchShift")) { return allocationNotFound; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("resource")) { return { error: "Resource not found with the given criteria." }; } if (errorText.includes("project")) { return { error: "Project not found with the given criteria." }; } if (errorText.includes("demand")) { return { error: "Demand requirement not found with the given criteria." }; } if (prismaError.code === "P2025" && (context === "updateInline" || context === "batchShift")) { return { error: "Allocation not found with the given criteria." }; } return null; } function toAssistantVacationNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Vacation not found with the given criteria.", ); } function toAssistantVacationMutationError( error: unknown, action: "approve" | "reject" | "cancel", ): AssistantToolErrorResult | null { const notFound = toAssistantVacationNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { if (action === "approve") { return { error: "Vacation cannot be approved in its current status." }; } if (action === "reject") { return { error: "Vacation cannot be rejected in its current status." }; } return { error: "Vacation cannot be cancelled in its current status." }; } return null; } function toAssistantProjectCreationError( error: unknown, shortCode: string, ): AssistantToolErrorResult | null { if (error instanceof TRPCError) { if (error.code === "CONFLICT") { return { error: `A project with short code "${shortCode}" already exists.` }; } if (error.code === "NOT_FOUND") { if (error.message.includes("Blueprint")) { return { error: "Blueprint not found with the given criteria." }; } if (error.message.includes("Client")) { return { error: "Client not found with the given criteria." }; } } if (error.code === "BAD_REQUEST" || error.code === "UNPROCESSABLE_CONTENT") { return { error: error.message }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("blueprint")) { return { error: "Blueprint not found with the given criteria." }; } if (errorText.includes("client")) { return { error: "Client not found with the given criteria." }; } return null; } function toAssistantDemandNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Demand not found with the given criteria.", ); } function toAssistantDemandFillError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantDemandNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { return { error: "Demand cannot be filled in its current status." }; } return null; } function toAssistantEstimateNotFoundError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error.message.includes("version")) { return { error: "Estimate version not found with the given criteria." }; } return { error: "Estimate not found with the given criteria." }; } return null; } function toAssistantEstimateReadError( error: unknown, context: "weeklyPhasing" | "commercialTerms", ): AssistantToolErrorResult | null { const notFound = toAssistantEstimateNotFoundError(error); if (notFound) { return notFound; } if ( context === "weeklyPhasing" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED" && error.message === "Estimate has no versions" ) { return { error: "Estimate version not found with the given criteria." }; } return null; } function toAssistantHolidayCalendarNotFoundError( error: unknown, identifier?: string, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, identifier ? `Holiday calendar not found: ${identifier}` : "Holiday calendar not found with the given criteria.", ); } function toAssistantHolidayCalendarMutationError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantHolidayCalendarNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { return { error: "Holiday calendar scope is invalid." }; } if (error instanceof TRPCError && error.code === "CONFLICT") { return { error: "A holiday calendar for this scope already exists." }; } return null; } function toAssistantHolidayEntryNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Holiday calendar entry not found with the given criteria.", ); } function toAssistantHolidayEntryMutationError( error: unknown, ): AssistantToolErrorResult | null { const calendarNotFound = toAssistantHolidayCalendarNotFoundError(error); if (calendarNotFound) { return calendarNotFound; } const entryNotFound = toAssistantHolidayEntryNotFoundError(error); if (entryNotFound) { return entryNotFound; } if (error instanceof TRPCError && error.code === "CONFLICT") { return { error: "A holiday entry for this calendar and date already exists." }; } return null; } function toAssistantRoleNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Role not found with the given criteria.", ); } function toAssistantRoleMutationError( error: unknown, action: "create" | "update" | "delete", ): AssistantToolErrorResult | null { const notFound = toAssistantRoleNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "CONFLICT") { return { error: "A role with this name already exists." }; } if (action === "delete" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED") { return { error: "Role cannot be deleted while it is still assigned. Deactivate it instead." }; } return null; } function toAssistantClientMutationError( error: unknown, action: "create" | "update" | "delete" = "update", ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error.message.includes("Parent client")) { return { error: "Parent client not found with the given criteria." }; } return { error: "Client not found with the given criteria." }; } if (error instanceof TRPCError && error.code === "CONFLICT") { return { error: "A client with this code already exists." }; } if (action === "delete" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED") { if (error.message.includes("project")) { return { error: "Client cannot be deleted while it still has projects. Deactivate it instead." }; } if (error.message.includes("child client")) { return { error: "Client cannot be deleted while it still has child clients. Remove or reassign them first." }; } } return null; } function toAssistantOrgUnitNotFoundError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error.message.includes("Parent org unit")) { return { error: "Parent org unit not found with the given criteria." }; } return { error: "Org unit not found with the given criteria." }; } return null; } function toAssistantOrgUnitMutationError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantOrgUnitNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { if (error.message.includes("must be greater than parent level")) { return { error: "Org unit level must be greater than the parent org unit level." }; } } return null; } function toAssistantCountryNotFoundError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Country not found with the given criteria." }; } return null; } function toAssistantCountryMutationError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantCountryNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "CONFLICT") { return { error: "A country with this code already exists." }; } return null; } function toAssistantResourceCreationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError) { if (error.code === "CONFLICT") { return { error: "A resource with this EID or email already exists." }; } if (error.code === "BAD_REQUEST" || error.code === "UNPROCESSABLE_CONTENT") { return { error: error.message }; } if (error.code === "NOT_FOUND") { if (error.message.includes("Role")) { return { error: "Role not found with the given criteria." }; } if (error.message.includes("Country")) { return { error: "Country not found with the given criteria." }; } if (error.message.includes("Org unit")) { return { error: "Org unit not found with the given criteria." }; } } } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("role")) { return { error: "Role not found with the given criteria." }; } if (errorText.includes("country")) { return { error: "Country not found with the given criteria." }; } if (errorText.includes("orgunit") || errorText.includes("org_unit") || errorText.includes("org unit")) { return { error: "Org unit not found with the given criteria." }; } return { error: "The selected role, country, or org unit no longer exists." }; } function toAssistantResourceMutationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Resource not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code === "P2025") { return { error: "Resource not found with the given criteria." }; } if (prismaError.code !== "P2003") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("role")) { return { error: "Role not found with the given criteria." }; } if (errorText.includes("country")) { return { error: "Country not found with the given criteria." }; } if (errorText.includes("orgunit") || errorText.includes("org_unit") || errorText.includes("org unit")) { return { error: "Org unit not found with the given criteria." }; } return { error: "Resource not found with the given criteria." }; } function toAssistantProjectMutationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Project not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code === "P2025") { return { error: "Project not found with the given criteria." }; } if (prismaError.code !== "P2003") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("blueprint")) { return { error: "Blueprint not found with the given criteria." }; } if (errorText.includes("client")) { return { error: "Client not found with the given criteria." }; } return { error: "Project not found with the given criteria." }; } function toAssistantMetroCityMutationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error.message.includes("Country")) { return { error: "Country not found with the given criteria." }; } return { error: "Metro city not found with the given criteria." }; } if (error instanceof TRPCError && error.code === "PRECONDITION_FAILED") { return { error: "Metro city cannot be deleted while it is still assigned to resources." }; } return null; } function toAssistantDemandCreationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error.message.includes("Role")) { return { error: "Role not found with the given criteria." }; } return { error: "Project not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("role")) { return { error: "Role not found with the given criteria." }; } if (errorText.includes("project")) { return { error: "Project not found with the given criteria." }; } return null; } function toAssistantVacationCreationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError) { if (error.code === "FORBIDDEN") { return { error: "You can only create vacation requests for your own resource." }; } if (error.code === "BAD_REQUEST") { return { error: error.message }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code === "P2025") { return { error: "Resource not found with the given criteria." }; } if (prismaError.code !== "P2003") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("resource")) { return { error: "Resource not found with the given criteria." }; } return { error: "Resource not found with the given criteria." }; } function toAssistantEntitlementMutationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Resource not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code === "P2025") { return { error: "Resource not found with the given criteria." }; } if (prismaError.code !== "P2003") { return null; } return { error: "Resource not found with the given criteria." }; } function toAssistantEstimateCreationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Project not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("project")) { return { error: "Project not found with the given criteria." }; } if (errorText.includes("role")) { return { error: "Role not found with the given criteria." }; } if (errorText.includes("resource")) { return { error: "Resource not found with the given criteria." }; } if (errorText.includes("scopeitem") || errorText.includes("scope_item") || errorText.includes("scope item")) { return { error: "Estimate scope item not found with the given criteria." }; } return { error: "One of the referenced project, role, resource, or scope items no longer exists." }; } function toAssistantEstimateMutationError( error: unknown, action: | "clone" | "updateDraft" | "submitVersion" | "approveVersion" | "createRevision" | "createExport" | "createPlanningHandoff" | "generateWeeklyPhasing" | "updateCommercialTerms", ): AssistantToolErrorResult | null { if (error instanceof TRPCError) { if (error.code === "NOT_FOUND") { if (error.message.includes("Linked project")) { return { error: "Project not found with the given criteria." }; } if (action === "clone" && error.message === "Source estimate has no versions") { return { error: "Source estimate has no versions and cannot be cloned." }; } if (error.message.includes("version") || error.message.includes("versions")) { return { error: "Estimate version not found with the given criteria." }; } return { error: "Estimate not found with the given criteria." }; } if (error.code === "PRECONDITION_FAILED") { switch (error.message) { case "Estimate has no working version": return { error: "Estimate has no working version." }; case "Only working versions can be submitted": return { error: "Only working versions can be submitted." }; case "Estimate has no submitted version": return { error: "Estimate has no submitted version." }; case "Only submitted versions can be approved": return { error: "Only submitted versions can be approved." }; case "Estimate already has a working version": return { error: "Estimate already has a working version." }; case "Estimate has no locked version to revise": return { error: "Estimate has no locked version to revise." }; case "Source version must be locked before creating a revision": return { error: "Source version must be locked before creating a revision." }; case "Estimate has no approved version": return { error: "Estimate has no approved version." }; case "Only approved versions can be handed off to planning": return { error: "Only approved versions can be handed off to planning." }; case "Estimate must be linked to a project before planning handoff": return { error: "Estimate must be linked to a project before planning handoff." }; case "Planning handoff already exists for this approved version": return { error: "Planning handoff already exists for this approved version." }; case "Linked project has an invalid date range": return { error: "The linked project has an invalid date range for planning handoff." }; case "Commercial terms can only be edited on working versions": return { error: "Commercial terms can only be edited on working versions." }; default: if (error.message.startsWith("Project window has no working days for demand line")) { return { error: "The linked project window has no working days for at least one demand line." }; } } } if (error.code === "BAD_REQUEST" && action === "updateCommercialTerms") { return { error: "Commercial terms input is invalid." }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code === "P2003") { const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("project")) { return { error: "Project not found with the given criteria." }; } if (errorText.includes("role")) { return { error: "Role not found with the given criteria." }; } if (errorText.includes("resource")) { return { error: "Resource not found with the given criteria." }; } if (errorText.includes("scopeitem") || errorText.includes("scope_item") || errorText.includes("scope item")) { return { error: "Estimate scope item not found with the given criteria." }; } return { error: "One of the referenced project, role, resource, or scope items no longer exists." }; } if (prismaError.code === "P2025") { const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("estimatedemandline") || errorText.includes("estimate_demand_line") || errorText.includes("estimate demand line")) { return { error: "Estimate demand line not found with the given criteria." }; } if (errorText.includes("estimateversion") || errorText.includes("estimate_version") || errorText.includes("estimate version")) { return { error: "Estimate version not found with the given criteria." }; } if (errorText.includes("estimate")) { return { error: "Estimate not found with the given criteria." }; } switch (action) { case "generateWeeklyPhasing": return { error: "Estimate demand line not found with the given criteria." }; case "updateCommercialTerms": case "submitVersion": case "approveVersion": case "createRevision": case "createExport": return { error: "Estimate version not found with the given criteria." }; default: return { error: "Estimate not found with the given criteria." }; } } return null; } function toAssistantUserMutationError( error: unknown, action: "create" | "update" | "password" = "update", ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "CONFLICT" && action === "create") { return { error: "User with this email already exists." }; } if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "User not found with the given criteria." }; } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { const validationIssues = getTrpcValidationIssues(error); for (const issue of validationIssues) { const field = issue.path[0]; if (field === "password" && issue.code === "too_small") { return { error: "Password must be at least 8 characters." }; } if (field === "name" && issue.code === "too_small") { return { error: "Name is required." }; } if (field === "name" && issue.code === "too_big") { return { error: "Name must be at most 200 characters." }; } } if (error.message.includes("Password must be at least 8 characters")) { return { error: "Password must be at least 8 characters." }; } if (error.message.includes("Name is required")) { return { error: "Name is required." }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (prismaError?.code === "P2025") { return { error: "User not found with the given criteria." }; } return null; } function getTrpcValidationIssues(error: TRPCError): Array<{ code?: string; path: string[]; }> { if (error.cause instanceof ZodError) { return error.cause.issues.map((issue) => ({ code: issue.code, path: issue.path.map((segment) => String(segment)), })); } try { const parsed = JSON.parse(error.message); if (!Array.isArray(parsed)) { return []; } return parsed .filter((issue): issue is { code?: unknown; path?: unknown } => issue !== null && typeof issue === "object") .map((issue) => ( typeof issue.code === "string" ? { code: issue.code, path: Array.isArray(issue.path) ? issue.path.map((segment) => String(segment)) : [], } : { path: Array.isArray(issue.path) ? issue.path.map((segment) => String(segment)) : [], } )); } catch { return []; } } function toAssistantUserResourceLinkError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "CONFLICT") { if (error.message.includes("already linked")) { return { error: "Resource is already linked to another user." }; } if (error.message.includes("changed during update")) { return { error: "Resource link changed during update. Please retry." }; } } if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error.message.includes("Resource")) { return { error: "Resource not found with the given criteria." }; } return { error: "User not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); const pointsToUser = errorText.includes("userid") || errorText.includes("user_id") || errorText.includes(" user "); const pointsToResource = errorText.includes("resourceid") || errorText.includes("resource_id") || errorText.includes(" resource "); if (prismaError.code === "P2025") { return { error: "Resource not found with the given criteria." }; } if (prismaError.code === "P2003") { if (pointsToUser) { return { error: "User not found with the given criteria." }; } if (pointsToResource || errorText.includes("resource")) { return { error: "Resource not found with the given criteria." }; } return { error: "User not found with the given criteria." }; } return null; } function toAssistantTotpEnableError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "BAD_REQUEST") { if (error.message.includes("No TOTP secret generated")) { return { error: "No TOTP secret generated. Call generate_totp_secret first." }; } if (error.message.includes("already enabled")) { return { error: "TOTP is already enabled." }; } if (error.message.includes("Invalid TOTP token")) { return { error: "Invalid TOTP token." }; } } if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "User not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (prismaError?.code === "P2025") { return { error: "User not found with the given criteria." }; } return null; } function toAssistantWebhookNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Webhook not found with the given criteria.", ); } function toAssistantWebhookMutationError( error: unknown, action: "create" | "update" = "update", ): AssistantToolErrorResult | null { const notFound = toAssistantWebhookNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { return { error: action === "create" ? "Webhook input is invalid." : "Webhook update input is invalid.", }; } const prismaError = getPrismaRequestErrorMetadata(error); if (prismaError?.code === "P2025") { return { error: "Webhook not found with the given criteria." }; } return null; } function toAssistantAuditLogEntryNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Audit log entry not found with the given criteria.", ); } function toAssistantTaskNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Task not found with the given criteria.", ); } function toAssistantTaskActionError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantTaskNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "PRECONDITION_FAILED") { if (error.message.includes("no executable action")) { return { error: "Task has no executable action." }; } if (error.message.includes("already completed")) { return { error: "Task is already completed." }; } if (error.message.includes("dismissed")) { return { error: "Task has been dismissed and cannot be executed." }; } } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { if (error.message.includes("Invalid taskAction format") || error.message.includes("Unknown action")) { return { error: "Task action is invalid and cannot be executed." }; } if (error.message === "Vacation not found") { return { error: "Vacation not found with the given criteria." }; } if (error.message.startsWith("Vacation is ") && error.message.includes(", not PENDING")) { return { error: "Vacation is not pending and cannot be approved or rejected via this task action." }; } if (error.message === "Assignment not found") { return { error: "Assignment not found with the given criteria." }; } if (error.message === "Assignment is already CONFIRMED") { return { error: "Assignment is already confirmed." }; } return { error: error.message }; } if (error instanceof TRPCError && error.code === "FORBIDDEN") { return { error: "You do not have permission to execute this task action." }; } return null; } function toAssistantTaskAssignmentError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantTaskNotFoundError(error); if (notFound) { return notFound; } const prismaError = getPrismaRequestErrorMetadata(error); if (prismaError && (prismaError.code === "P2003" || prismaError.code === "P2025")) { const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("assignee")) { return { error: "Assignee user not found with the given criteria." }; } } if (error instanceof TRPCError && error.code === "BAD_REQUEST") { return { error: "Only tasks and approvals can be assigned." }; } return null; } function toAssistantBroadcastNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Broadcast not found with the given criteria.", ); } function toAssistantDispoImportBatchNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Import batch not found with the given criteria.", ); } function toAssistantReminderNotFoundError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotFoundError( error, "Reminder not found with the given criteria.", ); } function toAssistantNotificationNotFoundError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Notification not found with the given criteria." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (prismaError?.code === "P2025") { return { error: "Notification not found with the given criteria." }; } return null; } function toAssistantNotificationReadError( error: unknown, ): AssistantToolErrorResult | null { return toAssistantNotificationNotFoundError(error); } function toAssistantNotificationDeletionError( error: unknown, ): AssistantToolErrorResult | null { const notFound = toAssistantNotificationNotFoundError(error); if (notFound) { return notFound; } if (error instanceof TRPCError && error.code === "FORBIDDEN") { return { error: "Tasks created by other users cannot be deleted." }; } return null; } function toAssistantReminderCreationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "BAD_REQUEST") { return { error: "Reminder input is invalid." }; } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("user")) { return { error: "Authenticated user not found with the given criteria." }; } return null; } function toAssistantCommentResolveError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return { error: "Comment not found with the given criteria." }; } if (error instanceof TRPCError && error.code === "FORBIDDEN") { return { error: "Only the comment author or an admin can resolve comments." }; } return null; } function toAssistantCommentCreationError( error: unknown, ): AssistantToolErrorResult | null { if (error instanceof TRPCError && error.code === "BAD_REQUEST") { if (error.message.includes("at least 1 character")) { return { error: "Comment body is required." }; } if (error.message.includes("at most 10000")) { return { error: "Comment body must be at most 10000 characters." }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("author") || errorText.includes("sender")) { return { error: "Comment author not found with the given criteria." }; } if (errorText.includes("user") || errorText.includes("recipient")) { return { error: "Mentioned user not found with the given criteria." }; } return null; } function getPrismaRequestErrorMetadata(error: unknown): { code: string; message: string; metaText: string; } | null { const collectMetaStrings = (value: unknown): string[] => { if (typeof value === "string") { return [value]; } if (Array.isArray(value)) { return value.flatMap((entry) => collectMetaStrings(entry)); } if (value && typeof value === "object") { return Object.values(value).flatMap((entry) => collectMetaStrings(entry)); } return []; }; const queue: unknown[] = [error]; const visited = new Set(); while (queue.length > 0) { const current = queue.shift(); if (current === undefined || current === null || visited.has(current)) { continue; } visited.add(current); if (current instanceof Prisma.PrismaClientKnownRequestError) { const metaText = Object.values(current.meta ?? {}) .flatMap((value) => collectMetaStrings(value)) .join(" "); return { code: current.code, message: current.message, metaText, }; } if (typeof current !== "object") { continue; } const candidate = current as { code?: unknown; message?: unknown; meta?: Record; cause?: unknown; }; if (typeof candidate.code === "string" && /^P\d{4}$/.test(candidate.code)) { const metaText = Object.values(candidate.meta ?? {}) .flatMap((value) => collectMetaStrings(value)) .join(" "); return { code: candidate.code, message: typeof candidate.message === "string" ? candidate.message : "", metaText, }; } if ("cause" in candidate) { queue.push(candidate.cause); } } return null; } function getTrpcErrorMetadata(error: unknown): { code: string; message: string; } | null { const queue: unknown[] = [error]; const visited = new Set(); while (queue.length > 0) { const current = queue.shift(); if (current === undefined || current === null || visited.has(current)) { continue; } visited.add(current); if (current instanceof TRPCError) { return { code: current.code, message: current.message, }; } if (typeof current !== "object") { continue; } const candidate = current as { code?: unknown; message?: unknown; cause?: unknown; data?: { code?: unknown }; shape?: { code?: unknown; message?: unknown; data?: { cause?: unknown } }; }; const candidateCode = typeof candidate.code === "string" ? candidate.code : typeof candidate.data?.code === "string" ? candidate.data.code : typeof candidate.shape?.code === "string" ? candidate.shape.code : null; const candidateMessage = typeof candidate.message === "string" ? candidate.message : typeof candidate.shape?.message === "string" ? candidate.shape.message : ""; if (candidateCode && /^[A-Z_]+$/.test(candidateCode)) { return { code: candidateCode, message: candidateMessage, }; } if ("cause" in candidate) { queue.push(candidate.cause); } if (candidate.shape?.data?.cause) { queue.push(candidate.shape.data.cause); } } return null; } function toAssistantNotificationCreationError( error: unknown, context: "notification" | "task" | "broadcast", ): AssistantToolErrorResult | null { const trpcError = getTrpcErrorMetadata(error); if ( context === "broadcast" && trpcError?.code === "BAD_REQUEST" && trpcError.message === "No recipients matched the broadcast target." ) { return { error: "No recipients matched the broadcast target." }; } if ( context === "broadcast" && trpcError?.code === "BAD_REQUEST" && trpcError.message === "Scheduled broadcasts with task metadata are not supported yet." ) { return { error: "Scheduled broadcasts with task metadata are not supported yet." }; } if (trpcError?.code === "NOT_FOUND") { if (trpcError.message.includes("broadcast")) { return { error: "Broadcast not found with the given criteria." }; } if (trpcError.message.includes("Sender user not found")) { return { error: "Sender user not found with the given criteria." }; } if (trpcError.message.includes("Assignee user not found")) { return { error: "Assignee user not found with the given criteria." }; } if (trpcError.message.includes("recipient")) { return context === "broadcast" ? { error: "Broadcast recipient user not found with the given criteria." } : context === "task" ? { error: "Task recipient user not found with the given criteria." } : { error: "Notification recipient user not found with the given criteria." }; } } const prismaError = getPrismaRequestErrorMetadata(error); if (!prismaError) { return null; } if (prismaError.code !== "P2003" && prismaError.code !== "P2025") { return null; } const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase(); if (errorText.includes("assignee")) { return { error: "Assignee user not found with the given criteria." }; } if (errorText.includes("sender")) { return { error: "Sender user not found with the given criteria." }; } if (context === "broadcast" && (errorText.includes("notificationbroadcast") || errorText.includes("broadcast"))) { return { error: "Broadcast not found with the given criteria." }; } if (context === "broadcast" && prismaError.code === "P2025") { return { error: "Broadcast not found with the given criteria." }; } if (context === "task") { return { error: "Task recipient user not found with the given criteria." }; } if (context === "broadcast") { return { error: "Broadcast recipient user not found with the given criteria." }; } return { error: "Notification recipient user not found with the given criteria." }; } function normalizeAssistantExecutionError( error: unknown, ): AssistantToolErrorResult { if (error instanceof AssistantVisibleError) { return { error: error.message }; } const trpcError = getTrpcErrorMetadata(error); if (trpcError) { if (trpcError.code === "INTERNAL_SERVER_ERROR") { return { error: "The tool could not complete due to an internal error.", }; } if (trpcError.code === "UNAUTHORIZED") { return { error: "Authentication is required to use this tool.", }; } if (trpcError.code === "FORBIDDEN") { return { error: "You do not have permission to perform this action.", }; } return { error: "The tool could not complete due to a request error." }; } if (error instanceof Error) { return { error: "The tool could not complete due to an unexpected error." }; } return { error: "The tool could not complete due to an unexpected error." }; } function isAssistantToolErrorResult( value: unknown, ): value is AssistantToolErrorResult { return value !== null && typeof value === "object" && "error" in value; } function toAssistantIndexedFieldError( index: number, field: string, message: string, ): AssistantIndexedFieldErrorResult { return { error: `assignments[${index}].${field}: ${message}`, field: `assignments[${index}].${field}`, index, }; } async function resolveEntityOrAssistantError( resolve: () => Promise, notFoundMessage: string, ): Promise { try { return await resolve(); } catch (error) { const mapped = toAssistantNotFoundError(error, notFoundMessage); if (mapped) { return mapped; } if (error instanceof TRPCError && error.code === "INTERNAL_SERVER_ERROR") { return normalizeAssistantExecutionError(error); } throw error; } } async function resolveProjectIdentifier( ctx: ToolContext, identifier: string, ) { const caller = createProjectCaller(createScopedCallerContext(ctx)); return resolveEntityOrAssistantError( () => caller.resolveByIdentifier({ identifier }), `Project not found: ${identifier}`, ); } async function resolveResourceIdentifier( ctx: ToolContext, identifier: string, ) { const caller = createResourceCaller(createScopedCallerContext(ctx)); return resolveEntityOrAssistantError( () => caller.resolveByIdentifier({ identifier }), `Resource not found: ${identifier}`, ); } function createScopedCallerContext(ctx: ToolContext): TRPCContext { if (!ctx.session?.user || !ctx.dbUser) { throw new AssistantVisibleError("Authenticated assistant context is required for this tool."); } return { session: ctx.session, db: ctx.db, dbUser: ctx.dbUser, roleDefaults: ctx.roleDefaults ?? null, }; } function sanitizeWebhook(webhook: T) { const { secret: _secret, ...rest } = webhook; return { ...rest, hasSecret: Boolean(webhook.secret), }; } function sanitizeWebhookList(webhooks: T[]) { return webhooks.map((webhook) => sanitizeWebhook(webhook)); } // ─── Tool Definitions ─────────────────────────────────────────────────────── export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([ // ── READ TOOLS ── ...resourceReadToolDefinitions, ...projectReadToolDefinitions, ...advancedTimelineToolDefinitions, ...allocationPlanningReadToolDefinitions, ...vacationHolidayReadToolDefinitions, ...vacationHolidayMutationToolDefinitions, ...rolesAnalyticsReadToolDefinitions, ...chargeabilityComputationReadToolDefinitions, ...planningNavigationToolDefinitions, // ── WRITE TOOLS ── ...allocationPlanningMutationToolDefinitions, ...resourceMutationToolDefinitions, ...projectMutationToolDefinitions, ...vacationEntitlementToolDefinitions, // ── DEMAND / STAFFING ── ...staffingDemandReadToolDefinitions, ...staffingDemandMutationToolDefinitions, // ── BLUEPRINT ── ...blueprintsRateCardsToolDefinitions, // ── ESTIMATES ── ...estimateReadToolDefinitions, ...estimateMutationToolDefinitions, // ── ROLES ── ...rolesAnalyticsMutationToolDefinitions, // ── CLIENTS ── ...clientMutationToolDefinitions, // ── ADMIN / CONFIG READ TOOLS ── ...countryReadmodelToolDefinitions, ...countryMetroAdminToolDefinitions, ...configReadmodelToolDefinitions, ...userAdminToolDefinitions, ...userSelfServiceToolDefinitions, ...notificationInboxToolDefinitions, ...dashboardInsightsReportsToolDefinitions, // ── ORG UNIT MANAGEMENT ── ...orgUnitMutationToolDefinitions, // ── TASK MANAGEMENT ── ...notificationTaskToolDefinitions, ...commentReadToolDefinitions, ...scenarioRateAnalysisToolDefinitions, ...commentMutationToolDefinitions, ...auditHistoryToolDefinitions, ...importExportDispoToolDefinitions, ...settingsAdminToolDefinitions, ], LEGACY_MONOLITHIC_TOOL_ACCESS); const TOOL_DEFINITIONS_BY_NAME = new Map( TOOL_DEFINITIONS.map((tool) => [tool.function.name, tool]), ); type AssistantToolAccessEvaluationContext = Pick; type AssistantToolAccessFailure = | { type: "role" } | { type: "permission"; permission?: PermissionKey; message?: string; }; function hasAssistantResourceOverviewAccess( permissions: Set, ): boolean { return permissions.has(PermissionKey.VIEW_ALL_RESOURCES) || permissions.has(PermissionKey.MANAGE_RESOURCES); } function getAssistantToolAccessRequirements( tool: ToolDef, ): ToolAccessRequirements | undefined { return tool.access ?? LEGACY_MONOLITHIC_TOOL_ACCESS[tool.function.name]; } function getAssistantToolAccessFailure( tool: ToolDef, ctx: AssistantToolAccessEvaluationContext, ): AssistantToolAccessFailure | null { const access = getAssistantToolAccessRequirements(tool); if (!access) { return null; } if ( access.allowedSystemRoles && !access.allowedSystemRoles.includes(ctx.userRole as SystemRole) ) { return { type: "role" }; } const missingRequiredPermission = access.requiredPermissions?.find( (permission) => !ctx.permissions.has(permission), ); if (missingRequiredPermission) { return { type: "permission", permission: missingRequiredPermission, }; } if (access.requiresPlanningRead && !ctx.permissions.has(PermissionKey.VIEW_PLANNING)) { return { type: "permission", permission: PermissionKey.VIEW_PLANNING, }; } if (access.requiresCostView && !ctx.permissions.has(PermissionKey.VIEW_COSTS)) { return { type: "permission", permission: PermissionKey.VIEW_COSTS, }; } if ( access.requiresAdvancedAssistant && !ctx.permissions.has(PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS) ) { return { type: "permission", permission: PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, }; } if ( access.requiresResourceOverview && !hasAssistantResourceOverviewAccess(ctx.permissions) ) { return { type: "permission", message: "Permission denied: you need resource overview access to perform this action.", }; } return null; } function toAssistantToolAccessError( failure: AssistantToolAccessFailure, ): AssistantVisibleError { if (failure.type === "role") { return new AssistantVisibleError("You do not have permission to perform this action."); } if (failure.permission) { return new AssistantVisibleError( `Permission denied: you need the "${failure.permission}" permission to perform this action.`, ); } return new AssistantVisibleError( failure.message ?? "You do not have permission to perform this action.", ); } export function canAccessAssistantTool( tool: ToolDef, ctx: AssistantToolAccessEvaluationContext, ): boolean { return getAssistantToolAccessFailure(tool, ctx) === null; } export function getAvailableAssistantToolsForContext( permissions: Set, userRole: string, ): ToolDef[] { return TOOL_DEFINITIONS.filter((tool) => canAccessAssistantTool(tool, { permissions, userRole })); } // ─── Helpers ──────────────────────────────────────────────────────────────── /** Resolve a responsible person name against existing resources. Returns the exact displayName or an error object. */ async function resolveResponsiblePerson( name: string, ctx: ToolContext, ): Promise<{ displayName: string } | { error: string }> { const caller = createResourceCaller(createScopedCallerContext(ctx)); const result = await caller.resolveResponsiblePersonName({ name }); if (result.status === "resolved") { return { displayName: result.displayName }; } if (result.status === "ambiguous" || result.status === "missing") { return { error: result.message }; } return { error: `Unable to resolve responsible person: ${name}` }; } // ─── Tool Executors ───────────────────────────────────────────────────────── const executors = { ...createResourceExecutors({ assertPermission, createResourceCaller, createRoleCaller, createCountryCaller, createOrgUnitCaller, createScopedCallerContext, resolveResourceIdentifier, resolveEntityOrAssistantError, toAssistantResourceMutationError, toAssistantResourceCreationError, }), ...createProjectExecutors({ assertPermission, createProjectCaller, createBlueprintCaller, createClientCaller, createScopedCallerContext, resolveProjectIdentifier, resolveResponsiblePerson, resolveEntityOrAssistantError, toAssistantNotFoundError, toAssistantProjectMutationError, toAssistantProjectCreationError, toAssistantProjectNotFoundError, }), ...createStaffingDemandExecutors({ assertPermission, createAllocationCaller, createStaffingCaller, createRoleCaller, createScopedCallerContext, resolveProjectIdentifier, resolveResourceIdentifier, resolveEntityOrAssistantError, parseIsoDate, parseOptionalIsoDate, fmtDate, toAssistantDemandCreationError, toAssistantDemandFillError, }), ...createAdvancedTimelineExecutors({ assertPermission, createStaffingCaller, createTimelineCaller, createScopedCallerContext, resolveProjectIdentifier, resolveResourceIdentifier, parseIsoDate, fmtDate, isAssistantToolErrorResult, toAssistantIndexedFieldError, toAssistantTimelineMutationError, }), ...createAllocationPlanningExecutors({ assertPermission, createAllocationCaller, createTimelineCaller, createScopedCallerContext, resolveProjectIdentifier, resolveResourceIdentifier, parseIsoDate, parseOptionalIsoDate, fmtDate, toAssistantAllocationNotFoundError, }), ...createVacationHolidayExecutors({ createEntitlementCaller, createVacationCaller, createHolidayCalendarCaller, createScopedCallerContext, resolveResourceIdentifier, resolveHolidayPeriod, resolveEntityOrAssistantError, assertAdminRole, fmtDate, formatHolidayCalendar, formatHolidayCalendarEntry, toAssistantHolidayCalendarMutationError, toAssistantHolidayCalendarNotFoundError, toAssistantHolidayEntryMutationError, toAssistantHolidayEntryNotFoundError, }), ...createRolesAnalyticsExecutors({ createRoleCaller, createResourceCaller, createDashboardCaller, createScopedCallerContext, resolveResourceIdentifier, toAssistantRoleMutationError, }), ...createClientsOrgUnitsExecutors({ createClientCaller, createOrgUnitCaller, createScopedCallerContext, toAssistantClientMutationError, toAssistantOrgUnitMutationError, }), ...createChargeabilityComputationExecutors({ assertPermission, createChargeabilityReportCaller, createComputationGraphCaller, createScopedCallerContext, resolveResourceIdentifier, resolveProjectIdentifier, }), ...createBlueprintsRateCardsExecutors({ createBlueprintCaller, createRateCardCaller, createScopedCallerContext, resolveResourceIdentifier, resolveEntityOrAssistantError, parseOptionalIsoDate, fmtDate, }), ...createDashboardInsightsReportsExecutors({ assertPermission, createDashboardCaller, createInsightsCaller, createReportCaller, createScopedCallerContext, }), ...createPlanningNavigationExecutors({ createEstimateCaller, createClientCaller, createOrgUnitCaller, createTimelineCaller, createScopedCallerContext, resolveProjectIdentifier, parseIsoDate, }), ...createScenarioRateAnalysisExecutors({ assertPermission, createRateCardCaller, createScenarioCaller, createInsightsCaller, createScopedCallerContext, fmtEur, }), ...createCommentExecutors({ createCommentCaller, createScopedCallerContext, toAssistantCommentCreationError, toAssistantCommentResolveError, }), ...createAuditHistoryExecutors({ createAuditLogCaller, createScopedCallerContext, }), ...createVacationEntitlementExecutors({ createVacationCaller, createEntitlementCaller, createScopedCallerContext, resolveResourceIdentifier, parseIsoDate, fmtDate, parseAssistantVacationRequestType, toAssistantVacationCreationError, toAssistantVacationMutationError, toAssistantEntitlementMutationError, }), // ── ESTIMATES ── ...createEstimateExecutors({ assertPermission, createEstimateCaller, createScopedCallerContext, resolveProjectIdentifier, toAssistantEstimateNotFoundError, toAssistantEstimateReadError, toAssistantEstimateCreationError, toAssistantEstimateMutationError, }), // ── ROLES ── // ── CLIENTS ── // ── ADMIN / CONFIG ── ...createCountryReadmodelExecutors({ createCountryCaller, createScopedCallerContext, formatCountry, toAssistantCountryNotFoundError, }), ...createCountryMetroAdminExecutors({ createCountryCaller, createScopedCallerContext, assertAdminRole, formatCountry, toAssistantCountryMutationError, toAssistantMetroCityMutationError, }), ...createConfigReadmodelExecutors({ createManagementLevelCaller, createUtilizationCategoryCaller, createCalculationRuleCaller, createEffortRuleCaller, createExperienceMultiplierCaller, createScopedCallerContext, }), ...createUserAdminExecutors({ createUserCaller, createScopedCallerContext, toAssistantUserMutationError, toAssistantUserResourceLinkError, }), ...createUserSelfServiceExecutors({ createUserCaller, createScopedCallerContext, toAssistantCurrentUserError: toAssistantUserMutationError, toAssistantTotpEnableError, }), ...createNotificationsTasksExecutors({ createNotificationCaller, createScopedCallerContext, parseDateTime, parseOptionalDateTime, toAssistantTaskNotFoundError, toAssistantTaskActionError, toAssistantTaskAssignmentError, toAssistantBroadcastNotFoundError, toAssistantReminderNotFoundError, toAssistantNotificationReadError, toAssistantNotificationDeletionError, toAssistantReminderCreationError, toAssistantNotificationCreationError, }), ...createImportExportDispoExecutors({ assertPermission, createImportExportCaller, createDispoCaller, createScopedCallerContext, toAssistantDispoImportBatchNotFoundError, }), ...createSettingsAdminExecutors({ createSettingsCaller, createSystemRoleConfigCaller, createWebhookCaller, createAuditLogCaller, createProjectCaller, createScopedCallerContext, parseIsoDate, resolveProjectIdentifier, sanitizeWebhook, sanitizeWebhookList, toAssistantWebhookNotFoundError, toAssistantWebhookMutationError, toAssistantAuditLogEntryNotFoundError, }), }; // ─── Executor ─────────────────────────────────────────────────────────────── export interface ToolAction { type: string; url?: string; scope?: string[]; description?: string; } export interface ToolResult { content: string; action?: ToolAction; data?: unknown; } export async function executeTool( name: string, args: string, ctx: ToolContext, ): Promise { const executor = executors[name as keyof typeof executors]; if (!executor) return { content: JSON.stringify({ error: `Unknown tool: ${name}` }) }; try { const toolDefinition = TOOL_DEFINITIONS_BY_NAME.get(name); const accessFailure = toolDefinition ? getAssistantToolAccessFailure(toolDefinition, ctx) : null; if (accessFailure) { throw toAssistantToolAccessError(accessFailure); } const params = JSON.parse(args); // Audit-log all mutation tool executions (EGAI 4.1.3.1 / IAAI 3.6.26) if (MUTATION_TOOLS.has(name)) { logger.info( { tool: name, params, userId: ctx.userId, userRole: ctx.userRole }, "AI assistant mutation tool executed", ); } const result = await executor(params, ctx); // Detect action payloads (e.g. navigation, invalidation) if (result && typeof result === "object" && "__action" in (result as Record)) { const actionResult = result as Record; const actionType = actionResult.__action as string; if (actionType === "navigate") { const url = actionResult.url as string; const desc = (actionResult.description as string | undefined) ?? url; return { content: JSON.stringify({ description: desc }), data: { description: desc }, action: { type: "navigate", url, description: desc }, }; } if (actionType === "invalidate") { const scope = actionResult.scope as string[]; // Strip __action, scope, and large data from the result sent back to the AI const { __action: _, scope: _s, coverImageUrl: _img, ...rest } = actionResult; const content = JSON.stringify(rest); return { content: content.length > 4000 ? content.slice(0, 4000) + '..."' : content, data: rest, action: { type: "invalidate", scope }, }; } } // Cap tool result size to prevent oversized OpenAI conversation payloads const content = typeof result === "string" ? result : JSON.stringify(result); return { content: content.length > 8000 ? content.slice(0, 8000) + '..."' : content, ...(typeof result === "string" ? {} : { data: result }), }; } catch (err) { const normalizedError = normalizeAssistantExecutionError(err); logger.error( { tool: name, userId: ctx.userId, userRole: ctx.userRole, error: err instanceof Error ? { message: err.message, stack: err.stack } : err, }, "AI assistant tool execution failed", ); return { content: JSON.stringify(normalizedError), data: normalizedError }; } }