refactor(api): extract assistant scenario rate-analysis slice

This commit is contained in:
2026-03-30 22:38:01 +02:00
parent d55ab67e04
commit 4d8c91d705
3 changed files with 257 additions and 145 deletions
+13 -144
View File
@@ -127,6 +127,10 @@ import {
createDashboardInsightsReportsExecutors,
dashboardInsightsReportsToolDefinitions,
} from "./assistant-tools/dashboard-insights-reports.js";
import {
createScenarioRateAnalysisExecutors,
scenarioRateAnalysisToolDefinitions,
} from "./assistant-tools/scenario-rate-analysis.js";
import {
commentMutationToolDefinitions,
commentReadToolDefinitions,
@@ -426,9 +430,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
lookup_rate: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
simulate_scenario: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
generate_project_narrative: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
export_resources_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
export_projects_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
import_csv_data: {
@@ -2324,70 +2325,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
// ── TASK MANAGEMENT ──
...notificationTaskToolDefinitions,
...commentReadToolDefinitions,
{
type: "function",
function: {
name: "lookup_rate",
description: "Find the best matching rate card line for given criteria (client, chapter, management level, role, seniority).",
parameters: {
type: "object",
properties: {
clientId: { type: "string", description: "Client ID to find rate card for" },
chapter: { type: "string", description: "Chapter to match" },
managementLevelId: { type: "string", description: "Management level ID to match" },
roleName: { type: "string", description: "Role name to match" },
seniority: { type: "string", description: "Seniority level to match" },
},
},
},
},
// ── SCENARIO & AI ──
{
type: "function",
function: {
name: "simulate_scenario",
description: "Run a read-only what-if staffing simulation for a project. Shows cost/hours/utilization impact of adding, removing, or changing resource assignments without persisting changes.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
changes: {
type: "array",
items: {
type: "object",
properties: {
assignmentId: { type: "string", description: "Existing assignment ID to modify (omit for new)" },
resourceId: { type: "string", description: "Resource ID" },
roleId: { type: "string", description: "Role ID" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
hoursPerDay: { type: "number", description: "Hours per day" },
remove: { type: "boolean", description: "Set true to remove an existing assignment" },
},
required: ["startDate", "endDate", "hoursPerDay"],
},
description: "Array of staffing changes to simulate",
},
},
required: ["projectId", "changes"],
},
},
},
{
type: "function",
function: {
name: "generate_project_narrative",
description: "Generate an AI-powered executive narrative for a project covering budget, staffing, timeline risk, and action items. Requires AI to be configured.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
},
required: ["projectId"],
},
},
},
...scenarioRateAnalysisToolDefinitions,
...commentMutationToolDefinitions,
...auditHistoryToolDefinitions,
{
@@ -2787,6 +2725,14 @@ const executors = {
createReportCaller,
createScopedCallerContext,
}),
...createScenarioRateAnalysisExecutors({
assertPermission,
createRateCardCaller,
createScenarioCaller,
createInsightsCaller,
createScopedCallerContext,
fmtEur,
}),
...createCommentExecutors({
createCommentCaller,
createScopedCallerContext,
@@ -3260,83 +3206,6 @@ const executors = {
toAssistantNotificationCreationError,
}),
async lookup_rate(params: {
clientId?: string;
chapter?: string;
managementLevelId?: string;
roleName?: string;
seniority?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "viewCosts" as PermissionKey);
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const result = await caller.lookupBestMatch(params);
return {
...(result.message ? { message: result.message } : {}),
bestMatch: result.bestMatch
? {
...result.bestMatch,
costRate: fmtEur(result.bestMatch.costRateCents),
billRate: result.bestMatch.billRateCents ? fmtEur(result.bestMatch.billRateCents) : null,
}
: null,
alternatives: result.alternatives.map((alternative) => ({
...alternative,
costRate: fmtEur(alternative.costRateCents),
billRate: alternative.billRateCents ? fmtEur(alternative.billRateCents) : null,
})),
totalCandidates: result.totalCandidates,
};
},
// ── SCENARIO & AI ─────────────────────────────────────────────────────────
async simulate_scenario(params: {
projectId: string;
changes: Array<{
assignmentId?: string;
resourceId?: string;
roleId?: string;
startDate: string;
endDate: string;
hoursPerDay: number;
remove?: boolean;
}>;
}, ctx: ToolContext) {
const caller = createScenarioCaller(createScopedCallerContext(ctx));
const result = await caller.simulate({
projectId: params.projectId,
changes: params.changes.map((change) => ({
...change,
startDate: new Date(change.startDate),
endDate: new Date(change.endDate),
})),
});
return {
baseline: {
...result.baseline,
totalCost: fmtEur(result.baseline.totalCostCents),
},
scenario: {
...result.scenario,
totalCost: fmtEur(result.scenario.totalCostCents),
},
delta: {
...result.delta,
cost: fmtEur(result.delta.costCents),
},
resourceImpacts: result.resourceImpacts,
warnings: result.warnings,
budgetCents: result.budgetCents,
};
},
async generate_project_narrative(params: { projectId: string }, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.generateProjectNarrative({ projectId: params.projectId });
},
async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) {
const caller = createImportExportCaller(createScopedCallerContext(ctx));
const csv = await caller.exportResourcesCSV();