feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -5,10 +5,11 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { resolvePermissions, type PermissionOverrides, type SystemRole } from "@capakraken/shared";
|
||||
import { PermissionKey, resolvePermissions, type PermissionOverrides, type SystemRole } from "@capakraken/shared";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
||||
import { createAiClient, isAiConfigured, loggedAiCall, parseAiError } from "../ai-client.js";
|
||||
import { TOOL_DEFINITIONS, executeTool, type ToolContext, type ToolAction } from "./assistant-tools.js";
|
||||
import { ADVANCED_ASSISTANT_TOOLS, TOOL_DEFINITIONS, executeTool, type ToolContext, type ToolAction } from "./assistant-tools.js";
|
||||
import { buildAssistantInsight, type AssistantInsight } from "./assistant-insights.js";
|
||||
import { checkPromptInjection } from "../lib/prompt-guard.js";
|
||||
import { checkAiOutput } from "../lib/content-filter.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
@@ -20,7 +21,7 @@ const SYSTEM_PROMPT = `Du bist der CapaKraken-Assistent — ein hilfreicher AI-A
|
||||
|
||||
Deine Fähigkeiten:
|
||||
- Fragen über Ressourcen, Projekte, Allokationen, Budget, Urlaub, Estimates, Org-Struktur, Rollen, Blueprints, Rate Cards beantworten
|
||||
- Chargeability-Analysen, Urlaubsübersichten, Budget-Analysen, Staffing-Vorschläge, Kapazitätssuche
|
||||
- Chargeability-Analysen, Urlaubsübersichten, Feiertagskalender nach Land/Bundesland/Stadt, Budget-Analysen, Staffing-Vorschläge, Kapazitätssuche
|
||||
- Ressourcen erstellen/aktualisieren/deaktivieren, Projekte erstellen/aktualisieren/löschen
|
||||
- Allokationen erstellen/stornieren, Demands erstellen/besetzen, Staffing-Vorschläge abrufen
|
||||
- Urlaub erstellen/genehmigen/ablehnen/stornieren, Ansprüche verwalten
|
||||
@@ -40,6 +41,12 @@ Wichtige Regeln:
|
||||
- Sei KURZ und DIREKT. Keine langen Erklärungen wenn nicht nötig. Antworte knapp und präzise.
|
||||
- Rufe Tools PARALLEL auf wenn möglich (z.B. search_resources + list_allocations gleichzeitig)
|
||||
- Fasse Ergebnisse kompakt zusammen — keine unnötigen Wiederholungen der Tool-Ergebnisse
|
||||
- Wenn Feiertage, SAH, Chargeability, Verfügbarkeit oder Ressourcenauswahl relevant sind, erkläre IMMER transparent:
|
||||
1. Standortkontext (Land/Bundesland/Stadt falls relevant)
|
||||
2. Feiertagsbasis bzw. Feiertagsanzahl
|
||||
3. Abzüge durch Feiertage/Abwesenheiten
|
||||
4. resultierende verfügbare Stunden / Zielstunden / Restkapazität
|
||||
- Wenn strukturierte UI-Karten vorhanden sind, wiederhole dort gezeigte Zahlen NICHT vollständig im Freitext. Gib nur die Kernaussage und die wichtigste Begründung an.
|
||||
- Wenn eine Suche keine Treffer ergibt, versuche einzelne Wörter aus der Anfrage als Suchbegriffe. Die Tools unterstützen automatisch wort-basierte Fuzzy-Suche — zeige dem User die Vorschläge wenn welche gefunden werden
|
||||
|
||||
Datenmodell:
|
||||
@@ -48,10 +55,12 @@ Datenmodell:
|
||||
- Allokationen (Assignments): resourceId + projectId, hoursPerDay, dailyCostCents, Zeitraum, Status (PROPOSED/CONFIRMED/ACTIVE/COMPLETED/CANCELLED)
|
||||
- Chargeability = gebuchte/verfügbare Stunden × 100%
|
||||
- Urlaub: Typen VACATION/SICK/PARENTAL/SPECIAL/PUBLIC_HOLIDAY, Status PENDING/APPROVED/REJECTED/CANCELLED
|
||||
- Feiertage: können je nach Land, Bundesland und Stadt unterschiedlich sein; nutze Feiertags-Tools statt zu raten
|
||||
`;
|
||||
|
||||
/** Map tool names to the permission required to use them */
|
||||
const TOOL_PERMISSION_MAP: Record<string, string> = {
|
||||
list_users: PermissionKey.MANAGE_USERS,
|
||||
// Resource management
|
||||
update_resource: "manageResources",
|
||||
create_resource: "manageResources",
|
||||
@@ -89,7 +98,36 @@ const TOOL_PERMISSION_MAP: Record<string, string> = {
|
||||
};
|
||||
|
||||
/** Tools that require cost visibility */
|
||||
const COST_TOOLS = new Set(["get_budget_status", "get_chargeability", "resolve_rate", "list_rate_cards", "get_estimate_detail"]);
|
||||
const COST_TOOLS = new Set(["get_budget_status", "get_chargeability", "resolve_rate", "list_rate_cards", "get_estimate_detail", "find_best_project_resource"]);
|
||||
|
||||
export function getAvailableAssistantTools(permissions: Set<PermissionKey>) {
|
||||
return TOOL_DEFINITIONS.filter((tool) => {
|
||||
const toolName = tool.function.name;
|
||||
const requiredPerm = TOOL_PERMISSION_MAP[toolName];
|
||||
|
||||
if (requiredPerm && !permissions.has(requiredPerm as PermissionKey)) {
|
||||
return false;
|
||||
}
|
||||
if (COST_TOOLS.has(toolName) && !permissions.has(PermissionKey.VIEW_COSTS)) {
|
||||
return false;
|
||||
}
|
||||
if (ADVANCED_ASSISTANT_TOOLS.has(toolName) && !permissions.has(PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function mergeInsights(existing: AssistantInsight[], next: AssistantInsight): AssistantInsight[] {
|
||||
const duplicateIndex = existing.findIndex((item) => item.kind === next.kind && item.title === next.title && item.subtitle === next.subtitle);
|
||||
if (duplicateIndex >= 0) {
|
||||
const copy = [...existing];
|
||||
copy[duplicateIndex] = next;
|
||||
return copy;
|
||||
}
|
||||
return [...existing, next].slice(-6);
|
||||
}
|
||||
|
||||
export const assistantRouter = createTRPCRouter({
|
||||
chat: protectedProcedure
|
||||
@@ -176,26 +214,12 @@ export const assistantRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// 4. Filter tools based on granular permissions
|
||||
const availableTools = TOOL_DEFINITIONS.filter((t) => {
|
||||
const toolName = t.function.name;
|
||||
|
||||
// Check write permission
|
||||
const requiredPerm = TOOL_PERMISSION_MAP[toolName];
|
||||
if (requiredPerm && !permissions.has(requiredPerm as import("@capakraken/shared").PermissionKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide cost/budget tools if user lacks viewCosts
|
||||
if (COST_TOOLS.has(toolName) && !permissions.has("viewCosts" as import("@capakraken/shared").PermissionKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
const availableTools = getAvailableAssistantTools(permissions);
|
||||
|
||||
// 5. Function calling loop
|
||||
const toolCtx: ToolContext = { db: ctx.db, userId: ctx.dbUser!.id, userRole, permissions };
|
||||
const collectedActions: ToolAction[] = [];
|
||||
let collectedInsights: AssistantInsight[] = [];
|
||||
|
||||
for (let i = 0; i < MAX_TOOL_ITERATIONS; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -240,6 +264,11 @@ export const assistantRouter = createTRPCRouter({
|
||||
toolCtx,
|
||||
);
|
||||
|
||||
const insight = buildAssistantInsight(toolCall.function.name, result.data);
|
||||
if (insight) {
|
||||
collectedInsights = mergeInsights(collectedInsights, insight);
|
||||
}
|
||||
|
||||
// Collect any actions (e.g. navigation)
|
||||
if (result.action) {
|
||||
collectedActions.push(result.action);
|
||||
@@ -298,6 +327,7 @@ export const assistantRouter = createTRPCRouter({
|
||||
return {
|
||||
content: finalContent,
|
||||
role: "assistant" as const,
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
};
|
||||
}
|
||||
@@ -306,6 +336,7 @@ export const assistantRouter = createTRPCRouter({
|
||||
return {
|
||||
content: "I had to stop after too many tool calls. Please try a simpler question.",
|
||||
role: "assistant" as const,
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
};
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user