refactor(api): extract assistant scenario rate-analysis slice
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
- the resource search, detail, and lifecycle assistant helpers now live in their own domain module, keeping resource CRUD orchestration out of the monolithic assistant router without changing the assistant contract
|
- the resource search, detail, and lifecycle assistant helpers now live in their own domain module, keeping resource CRUD orchestration out of the monolithic assistant router without changing the assistant contract
|
||||||
- the blueprint and rate-card read helpers now live in their own domain module, keeping reference-data and pricing lookups out of the monolithic assistant router without changing the assistant contract
|
- the blueprint and rate-card read helpers now live in their own domain module, keeping reference-data and pricing lookups out of the monolithic assistant router without changing the assistant contract
|
||||||
- the dashboard detail, insight summary, anomaly detection, and dynamic report read helpers now live in their own domain module, keeping controller-side analytics reads out of the monolithic assistant router without changing the assistant contract
|
- the dashboard detail, insight summary, anomaly detection, and dynamic report read helpers now live in their own domain module, keeping controller-side analytics reads out of the monolithic assistant router without changing the assistant contract
|
||||||
|
- the scenario simulation, project narrative, and rate lookup assistant helpers now live in their own domain module, keeping the remaining controller-side scenario/AI analytics wiring out of the monolithic assistant router without changing the assistant contract
|
||||||
- the comment listing and comment mutation assistant helpers now live in their own domain module, keeping collaboration-side comment flows out of the monolithic assistant router without changing the assistant contract
|
- the comment listing and comment mutation assistant helpers now live in their own domain module, keeping collaboration-side comment flows out of the monolithic assistant router without changing the assistant contract
|
||||||
- the audit-history assistant helpers now live in their own domain module, keeping controller-side change-history reads out of the monolithic assistant router without changing the assistant contract
|
- the audit-history assistant helpers now live in their own domain module, keeping controller-side change-history reads out of the monolithic assistant router without changing the assistant contract
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
|
|
||||||
Pin the next structural cleanup on the API side:
|
Pin the next structural cleanup on the API side:
|
||||||
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
|
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
|
||||||
The next clean slice should stay adjacent to the extracted domains and target one cohesive analytics block such as the remaining scenario/narrative/rate-analysis helpers still living in the monolithic router.
|
The next clean slice should stay adjacent to the extracted domains and target one cohesive leftover block such as the remaining import/export or staged-dispo helpers still living in the monolithic router.
|
||||||
|
|
||||||
## Remaining Major Themes
|
## Remaining Major Themes
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ import {
|
|||||||
createDashboardInsightsReportsExecutors,
|
createDashboardInsightsReportsExecutors,
|
||||||
dashboardInsightsReportsToolDefinitions,
|
dashboardInsightsReportsToolDefinitions,
|
||||||
} from "./assistant-tools/dashboard-insights-reports.js";
|
} from "./assistant-tools/dashboard-insights-reports.js";
|
||||||
|
import {
|
||||||
|
createScenarioRateAnalysisExecutors,
|
||||||
|
scenarioRateAnalysisToolDefinitions,
|
||||||
|
} from "./assistant-tools/scenario-rate-analysis.js";
|
||||||
import {
|
import {
|
||||||
commentMutationToolDefinitions,
|
commentMutationToolDefinitions,
|
||||||
commentReadToolDefinitions,
|
commentReadToolDefinitions,
|
||||||
@@ -426,9 +430,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
|
|||||||
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||||
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||||
remove_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_resources_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||||
export_projects_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
export_projects_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||||
import_csv_data: {
|
import_csv_data: {
|
||||||
@@ -2324,70 +2325,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
|||||||
// ── TASK MANAGEMENT ──
|
// ── TASK MANAGEMENT ──
|
||||||
...notificationTaskToolDefinitions,
|
...notificationTaskToolDefinitions,
|
||||||
...commentReadToolDefinitions,
|
...commentReadToolDefinitions,
|
||||||
{
|
...scenarioRateAnalysisToolDefinitions,
|
||||||
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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...commentMutationToolDefinitions,
|
...commentMutationToolDefinitions,
|
||||||
...auditHistoryToolDefinitions,
|
...auditHistoryToolDefinitions,
|
||||||
{
|
{
|
||||||
@@ -2787,6 +2725,14 @@ const executors = {
|
|||||||
createReportCaller,
|
createReportCaller,
|
||||||
createScopedCallerContext,
|
createScopedCallerContext,
|
||||||
}),
|
}),
|
||||||
|
...createScenarioRateAnalysisExecutors({
|
||||||
|
assertPermission,
|
||||||
|
createRateCardCaller,
|
||||||
|
createScenarioCaller,
|
||||||
|
createInsightsCaller,
|
||||||
|
createScopedCallerContext,
|
||||||
|
fmtEur,
|
||||||
|
}),
|
||||||
...createCommentExecutors({
|
...createCommentExecutors({
|
||||||
createCommentCaller,
|
createCommentCaller,
|
||||||
createScopedCallerContext,
|
createScopedCallerContext,
|
||||||
@@ -3260,83 +3206,6 @@ const executors = {
|
|||||||
toAssistantNotificationCreationError,
|
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) {
|
async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
const caller = createImportExportCaller(createScopedCallerContext(ctx));
|
const caller = createImportExportCaller(createScopedCallerContext(ctx));
|
||||||
const csv = await caller.exportResourcesCSV();
|
const csv = await caller.exportResourcesCSV();
|
||||||
|
|||||||
@@ -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