refactor(api): extract assistant blueprint rate-card slice
This commit is contained in:
@@ -38,12 +38,13 @@
|
|||||||
- the project search, lifecycle, and cover-art helpers now live in their own domain module, keeping project orchestration out of the monolithic assistant router without changing the assistant contract
|
- the project search, lifecycle, and cover-art helpers now live in their own domain module, keeping project orchestration out of the monolithic assistant router without changing the assistant contract
|
||||||
- the demand, staffing-suggestion, capacity, and resource-availability assistant helpers now live in their own domain module, keeping staffing orchestration out of the monolithic assistant router without changing the assistant contract
|
- the demand, staffing-suggestion, capacity, and resource-availability assistant helpers now live in their own domain module, keeping staffing 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 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
|
||||||
|
|
||||||
## Next Up
|
## Next Up
|
||||||
|
|
||||||
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 block such as blueprint/rate-card/reference-data reads or another tightly bound CRUD/read-model cluster still in the monolithic router.
|
The next clean slice should stay adjacent to the extracted domains and target one cohesive read-model block such as dashboard/insight/report helpers or another tightly bound cluster still in the monolithic router.
|
||||||
|
|
||||||
## Remaining Major Themes
|
## Remaining Major Themes
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ import {
|
|||||||
resourceMutationToolDefinitions,
|
resourceMutationToolDefinitions,
|
||||||
resourceReadToolDefinitions,
|
resourceReadToolDefinitions,
|
||||||
} from "./assistant-tools/resources.js";
|
} from "./assistant-tools/resources.js";
|
||||||
|
import {
|
||||||
|
blueprintsRateCardsToolDefinitions,
|
||||||
|
createBlueprintsRateCardsExecutors,
|
||||||
|
} from "./assistant-tools/blueprints-rate-cards.js";
|
||||||
import {
|
import {
|
||||||
withToolAccess,
|
withToolAccess,
|
||||||
type ToolAccessRequirements,
|
type ToolAccessRequirements,
|
||||||
@@ -408,16 +412,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
|
|||||||
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||||
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||||
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||||
list_blueprints: { requiresPlanningRead: true },
|
|
||||||
get_blueprint: { requiresPlanningRead: true },
|
|
||||||
list_rate_cards: {
|
|
||||||
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
|
|
||||||
requiresCostView: true,
|
|
||||||
},
|
|
||||||
resolve_rate: {
|
|
||||||
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
|
|
||||||
requiresCostView: true,
|
|
||||||
},
|
|
||||||
get_country: { requiresResourceOverview: true },
|
get_country: { requiresResourceOverview: true },
|
||||||
get_dashboard_detail: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
get_dashboard_detail: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||||
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||||
@@ -2275,59 +2269,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
|||||||
...staffingDemandMutationToolDefinitions,
|
...staffingDemandMutationToolDefinitions,
|
||||||
|
|
||||||
// ── BLUEPRINT ──
|
// ── BLUEPRINT ──
|
||||||
{
|
...blueprintsRateCardsToolDefinitions,
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_blueprints",
|
|
||||||
description: "List available project blueprints with their field definitions.",
|
|
||||||
parameters: { type: "object", properties: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "get_blueprint",
|
|
||||||
description: "Get detailed blueprint with all field definitions and role presets.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
|
|
||||||
},
|
|
||||||
required: ["identifier"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── RATE CARDS ──
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_rate_cards",
|
|
||||||
description: "List rate cards with their effective dates and line items.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
query: { type: "string", description: "Search by name" },
|
|
||||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "resolve_rate",
|
|
||||||
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
resourceId: { type: "string", description: "Resource ID or name" },
|
|
||||||
roleName: { type: "string", description: "Role name" },
|
|
||||||
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── ESTIMATES ──
|
// ── ESTIMATES ──
|
||||||
...estimateReadToolDefinitions,
|
...estimateReadToolDefinitions,
|
||||||
@@ -3020,6 +2962,15 @@ const executors = {
|
|||||||
resolveResourceIdentifier,
|
resolveResourceIdentifier,
|
||||||
resolveProjectIdentifier,
|
resolveProjectIdentifier,
|
||||||
}),
|
}),
|
||||||
|
...createBlueprintsRateCardsExecutors({
|
||||||
|
createBlueprintCaller,
|
||||||
|
createRateCardCaller,
|
||||||
|
createScopedCallerContext,
|
||||||
|
resolveResourceIdentifier,
|
||||||
|
resolveEntityOrAssistantError,
|
||||||
|
parseOptionalIsoDate,
|
||||||
|
fmtDate,
|
||||||
|
}),
|
||||||
|
|
||||||
async search_estimates(params: {
|
async search_estimates(params: {
|
||||||
projectCode?: string; query?: string; status?: string; limit?: number;
|
projectCode?: string; query?: string; status?: string; limit?: number;
|
||||||
@@ -3388,72 +3339,6 @@ const executors = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── BLUEPRINT ──
|
|
||||||
|
|
||||||
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
|
|
||||||
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
|
|
||||||
const blueprints = await caller.listSummaries();
|
|
||||||
return blueprints.map((b) => ({
|
|
||||||
id: b.id,
|
|
||||||
name: b.name,
|
|
||||||
projectCount: b._count.projects,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
async get_blueprint(params: { identifier: string }, ctx: ToolContext) {
|
|
||||||
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
|
|
||||||
const bp = await resolveEntityOrAssistantError(
|
|
||||||
() => caller.getByIdentifier({ identifier: params.identifier }),
|
|
||||||
`Blueprint not found: ${params.identifier}`,
|
|
||||||
);
|
|
||||||
if ("error" in bp) {
|
|
||||||
return bp;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: bp.id,
|
|
||||||
name: bp.name,
|
|
||||||
fieldDefs: bp.fieldDefs,
|
|
||||||
rolePresets: bp.rolePresets,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── RATE CARDS ──
|
|
||||||
|
|
||||||
async list_rate_cards(params: { query?: string; limit?: number }, ctx: ToolContext) {
|
|
||||||
const caller = createRateCardCaller(createScopedCallerContext(ctx));
|
|
||||||
const cards = await caller.list({
|
|
||||||
isActive: true,
|
|
||||||
...(params.query ? { search: params.query } : {}),
|
|
||||||
});
|
|
||||||
return cards.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
effectiveFrom: fmtDate(c.effectiveFrom),
|
|
||||||
effectiveTo: fmtDate(c.effectiveTo),
|
|
||||||
lineCount: c._count.lines,
|
|
||||||
})).slice(0, Math.min(params.limit ?? 20, 50));
|
|
||||||
},
|
|
||||||
|
|
||||||
async resolve_rate(params: { resourceId?: string; roleName?: string; date?: string }, ctx: ToolContext) {
|
|
||||||
const caller = createRateCardCaller(createScopedCallerContext(ctx));
|
|
||||||
const date = parseOptionalIsoDate(params.date, "date");
|
|
||||||
if (params.resourceId) {
|
|
||||||
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
|
|
||||||
if ("error" in resource) {
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
return caller.resolveBestRate({
|
|
||||||
resourceId: resource.id,
|
|
||||||
...(date ? { date } : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return caller.resolveBestRate({
|
|
||||||
...(params.roleName ? { roleName: params.roleName } : {}),
|
|
||||||
...(date ? { date } : {}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── ESTIMATES ──
|
// ── ESTIMATES ──
|
||||||
...createEstimateExecutors({
|
...createEstimateExecutors({
|
||||||
assertPermission,
|
assertPermission,
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
import type { TRPCContext } from "../../trpc.js";
|
||||||
|
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||||
|
|
||||||
|
type AssistantToolErrorResult = { error: string };
|
||||||
|
|
||||||
|
type BlueprintRecord = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
fieldDefs: unknown;
|
||||||
|
rolePresets: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResolvedResource = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BlueprintsRateCardsDeps = {
|
||||||
|
createBlueprintCaller: (ctx: TRPCContext) => {
|
||||||
|
listSummaries: () => Promise<Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
_count: { projects: number };
|
||||||
|
}>>;
|
||||||
|
getByIdentifier: (params: { identifier: string }) => Promise<BlueprintRecord>;
|
||||||
|
};
|
||||||
|
createRateCardCaller: (ctx: TRPCContext) => {
|
||||||
|
list: (params: {
|
||||||
|
isActive: boolean;
|
||||||
|
search?: string;
|
||||||
|
}) => Promise<Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
effectiveFrom: Date | null;
|
||||||
|
effectiveTo: Date | null;
|
||||||
|
_count: { lines: number };
|
||||||
|
}>>;
|
||||||
|
resolveBestRate: (params: {
|
||||||
|
resourceId?: string;
|
||||||
|
roleName?: string;
|
||||||
|
date?: Date;
|
||||||
|
}) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||||
|
resolveResourceIdentifier: (
|
||||||
|
ctx: ToolContext,
|
||||||
|
identifier: string,
|
||||||
|
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
||||||
|
resolveEntityOrAssistantError: <T>(
|
||||||
|
resolve: () => Promise<T>,
|
||||||
|
notFoundMessage: string,
|
||||||
|
) => Promise<T | AssistantToolErrorResult>;
|
||||||
|
parseOptionalIsoDate: (
|
||||||
|
value: string | undefined,
|
||||||
|
fieldName: string,
|
||||||
|
) => Date | undefined;
|
||||||
|
fmtDate: (value: Date | null | undefined) => string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const blueprintsRateCardsToolDefinitions: ToolDef[] = withToolAccess([
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_blueprints",
|
||||||
|
description: "List available project blueprints with their field definitions.",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_blueprint",
|
||||||
|
description: "Get detailed blueprint with all field definitions and role presets.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
|
||||||
|
},
|
||||||
|
required: ["identifier"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_rate_cards",
|
||||||
|
description: "List rate cards with their effective dates and line items.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string", description: "Search by name" },
|
||||||
|
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "resolve_rate",
|
||||||
|
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
resourceId: { type: "string", description: "Resource ID or name" },
|
||||||
|
roleName: { type: "string", description: "Role name" },
|
||||||
|
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], {
|
||||||
|
list_blueprints: {
|
||||||
|
requiresPlanningRead: true,
|
||||||
|
},
|
||||||
|
get_blueprint: {
|
||||||
|
requiresPlanningRead: true,
|
||||||
|
},
|
||||||
|
list_rate_cards: {
|
||||||
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||||
|
requiresCostView: true,
|
||||||
|
},
|
||||||
|
resolve_rate: {
|
||||||
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||||
|
requiresCostView: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createBlueprintsRateCardsExecutors(
|
||||||
|
deps: BlueprintsRateCardsDeps,
|
||||||
|
): Record<string, ToolExecutor> {
|
||||||
|
return {
|
||||||
|
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = deps.createBlueprintCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const blueprints = await caller.listSummaries();
|
||||||
|
return blueprints.map((blueprint) => ({
|
||||||
|
id: blueprint.id,
|
||||||
|
name: blueprint.name,
|
||||||
|
projectCount: blueprint._count.projects,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_blueprint(params: { identifier: string }, ctx: ToolContext) {
|
||||||
|
const caller = deps.createBlueprintCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const blueprint = await deps.resolveEntityOrAssistantError(
|
||||||
|
() => caller.getByIdentifier({ identifier: params.identifier }),
|
||||||
|
`Blueprint not found: ${params.identifier}`,
|
||||||
|
);
|
||||||
|
if ("error" in blueprint) {
|
||||||
|
return blueprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: blueprint.id,
|
||||||
|
name: blueprint.name,
|
||||||
|
fieldDefs: blueprint.fieldDefs,
|
||||||
|
rolePresets: blueprint.rolePresets,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async list_rate_cards(
|
||||||
|
params: { query?: string; limit?: number },
|
||||||
|
ctx: ToolContext,
|
||||||
|
) {
|
||||||
|
const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const cards = await caller.list({
|
||||||
|
isActive: true,
|
||||||
|
...(params.query ? { search: params.query } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return cards
|
||||||
|
.map((card) => ({
|
||||||
|
id: card.id,
|
||||||
|
name: card.name,
|
||||||
|
effectiveFrom: deps.fmtDate(card.effectiveFrom),
|
||||||
|
effectiveTo: deps.fmtDate(card.effectiveTo),
|
||||||
|
lineCount: card._count.lines,
|
||||||
|
}))
|
||||||
|
.slice(0, Math.min(params.limit ?? 20, 50));
|
||||||
|
},
|
||||||
|
|
||||||
|
async resolve_rate(
|
||||||
|
params: { resourceId?: string; roleName?: string; date?: string },
|
||||||
|
ctx: ToolContext,
|
||||||
|
) {
|
||||||
|
const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const date = deps.parseOptionalIsoDate(params.date, "date");
|
||||||
|
|
||||||
|
if (params.resourceId) {
|
||||||
|
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||||
|
if ("error" in resource) {
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return caller.resolveBestRate({
|
||||||
|
resourceId: resource.id,
|
||||||
|
...(date ? { date } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return caller.resolveBestRate({
|
||||||
|
...(params.roleName ? { roleName: params.roleName } : {}),
|
||||||
|
...(date ? { date } : {}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user