refactor(api): extract assistant scenario rate-analysis slice
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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 });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user