243 lines
7.6 KiB
TypeScript
243 lines
7.6 KiB
TypeScript
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
|
import type { TRPCContext } from "../../trpc.js";
|
|
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
|
|
|
type RateLookupCandidate = {
|
|
rateCardName: string;
|
|
clientId: string | null;
|
|
clientName: string | null;
|
|
lineId: string;
|
|
chapter: string | null;
|
|
seniority: string | null;
|
|
roleName: string | null;
|
|
costRateCents: number;
|
|
billRateCents: number | null;
|
|
score: number;
|
|
};
|
|
|
|
type ScenarioSimulationResult = {
|
|
baseline: {
|
|
totalCostCents: number;
|
|
totalHours: number;
|
|
headcount: number;
|
|
skillCount: number;
|
|
};
|
|
scenario: {
|
|
totalCostCents: number;
|
|
totalHours: number;
|
|
headcount: number;
|
|
skillCount: number;
|
|
};
|
|
delta: {
|
|
costCents: number;
|
|
hours: number;
|
|
headcount: number;
|
|
skillCoveragePct: number;
|
|
};
|
|
resourceImpacts: unknown[];
|
|
warnings: string[];
|
|
budgetCents: number;
|
|
};
|
|
|
|
type ScenarioRateAnalysisDeps = {
|
|
assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
|
|
createRateCardCaller: (ctx: TRPCContext) => {
|
|
lookupBestMatch: (params: {
|
|
clientId?: string;
|
|
chapter?: string;
|
|
managementLevelId?: string;
|
|
roleName?: string;
|
|
seniority?: string;
|
|
}) => Promise<{
|
|
message?: string;
|
|
bestMatch: RateLookupCandidate | null;
|
|
alternatives: RateLookupCandidate[];
|
|
totalCandidates: number;
|
|
}>;
|
|
};
|
|
createScenarioCaller: (ctx: TRPCContext) => {
|
|
simulate: (params: {
|
|
projectId: string;
|
|
changes: Array<{
|
|
assignmentId?: string;
|
|
resourceId?: string;
|
|
roleId?: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
hoursPerDay: number;
|
|
remove?: boolean;
|
|
}>;
|
|
}) => Promise<ScenarioSimulationResult>;
|
|
};
|
|
createInsightsCaller: (ctx: TRPCContext) => {
|
|
generateProjectNarrative: (params: { projectId: string }) => Promise<unknown>;
|
|
};
|
|
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
|
fmtEur: (value: number) => string;
|
|
};
|
|
|
|
export const scenarioRateAnalysisToolDefinitions: ToolDef[] = withToolAccess([
|
|
{
|
|
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" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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"],
|
|
},
|
|
},
|
|
},
|
|
], {
|
|
lookup_rate: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
},
|
|
simulate_scenario: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
},
|
|
generate_project_narrative: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
},
|
|
});
|
|
|
|
export function createScenarioRateAnalysisExecutors(
|
|
deps: ScenarioRateAnalysisDeps,
|
|
): Record<string, ToolExecutor> {
|
|
return {
|
|
async lookup_rate(
|
|
params: {
|
|
clientId?: string;
|
|
chapter?: string;
|
|
managementLevelId?: string;
|
|
roleName?: string;
|
|
seniority?: string;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);
|
|
const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx));
|
|
const result = await caller.lookupBestMatch(params);
|
|
|
|
return {
|
|
...(result.message ? { message: result.message } : {}),
|
|
bestMatch: result.bestMatch
|
|
? {
|
|
...result.bestMatch,
|
|
costRate: deps.fmtEur(result.bestMatch.costRateCents),
|
|
billRate: result.bestMatch.billRateCents ? deps.fmtEur(result.bestMatch.billRateCents) : null,
|
|
}
|
|
: null,
|
|
alternatives: result.alternatives.map((alternative) => ({
|
|
...alternative,
|
|
costRate: deps.fmtEur(alternative.costRateCents),
|
|
billRate: alternative.billRateCents ? deps.fmtEur(alternative.billRateCents) : null,
|
|
})),
|
|
totalCandidates: result.totalCandidates,
|
|
};
|
|
},
|
|
|
|
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 = deps.createScenarioCaller(deps.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: deps.fmtEur(result.baseline.totalCostCents),
|
|
},
|
|
scenario: {
|
|
...result.scenario,
|
|
totalCost: deps.fmtEur(result.scenario.totalCostCents),
|
|
},
|
|
delta: {
|
|
...result.delta,
|
|
cost: deps.fmtEur(result.delta.costCents),
|
|
},
|
|
resourceImpacts: result.resourceImpacts,
|
|
warnings: result.warnings,
|
|
budgetCents: result.budgetCents,
|
|
};
|
|
},
|
|
|
|
async generate_project_narrative(
|
|
params: { projectId: string },
|
|
ctx: ToolContext,
|
|
) {
|
|
const caller = deps.createInsightsCaller(deps.createScopedCallerContext(ctx));
|
|
return caller.generateProjectNarrative({ projectId: params.projectId });
|
|
},
|
|
};
|
|
}
|