166 lines
7.8 KiB
TypeScript
166 lines
7.8 KiB
TypeScript
import { MUTATION_TOOLS } from "./assistant-tools.js";
|
|
import type { ToolDef } from "./assistant-tools/shared.js";
|
|
|
|
const MAX_OPENAI_TOOL_DEFINITIONS = 128;
|
|
|
|
const ALWAYS_INCLUDED_TOOL_NAMES = new Set([
|
|
"get_current_user",
|
|
"get_resource",
|
|
"search_projects",
|
|
"get_project",
|
|
"list_allocations",
|
|
"get_statistics",
|
|
"navigate_to_page",
|
|
]);
|
|
|
|
const MUTATION_INTENT_KEYWORDS = [
|
|
"create", "add", "new", "update", "change", "edit", "delete", "remove", "cancel", "approve", "reject",
|
|
"anlegen", "erstellen", "neu", "aendern", "ändern", "bearbeiten", "loeschen", "löschen", "entfernen",
|
|
"stornieren", "genehmigen", "ablehnen", "setzen",
|
|
];
|
|
|
|
const TOOL_SELECTION_HINTS = [
|
|
{
|
|
keywords: ["holiday", "holidays", "feiertag", "feiertage", "vacation", "vacations", "urlaub", "ferien", "abwesen"],
|
|
nameFragments: ["holiday", "vacation", "entitlement"],
|
|
exactTools: ["list_holidays_by_region", "get_resource_holidays", "get_my_timeline_holiday_overlays", "list_holiday_calendars", "get_holiday_calendar", "preview_resolved_holiday_calendar"],
|
|
},
|
|
{
|
|
keywords: ["resource", "resources", "ressource", "ressourcen", "employee", "mitarbeiter", "person", "people", "team", "chapter", "skill", "skills"],
|
|
nameFragments: ["resource", "skill", "role", "user", "staffing", "capacity"],
|
|
exactTools: ["search_resources", "get_resource", "search_by_skill", "check_resource_availability", "get_staffing_suggestions", "find_capacity"],
|
|
},
|
|
{
|
|
keywords: ["capacity", "availability", "available", "kapazitaet", "kapazität", "verfuegbar", "verfügbar", "auslastung", "chargeability", "sah", "lcr"],
|
|
nameFragments: ["capacity", "availability", "chargeability", "staffing", "rate", "budget"],
|
|
exactTools: ["check_resource_availability", "get_staffing_suggestions", "find_capacity", "get_chargeability", "find_best_project_resource", "resolve_rate"],
|
|
},
|
|
{
|
|
keywords: ["project", "projects", "projekt", "projekte", "allocation", "allocations", "allokation", "allokationen", "assignment", "assignments", "demand", "demands", "timeline"],
|
|
nameFragments: ["project", "allocation", "demand", "timeline", "assignment", "blueprint"],
|
|
exactTools: ["search_projects", "get_project", "list_allocations", "list_demands", "get_my_timeline_entries_view", "get_my_timeline_holiday_overlays", "get_timeline_entries_view", "get_project_timeline_context"],
|
|
},
|
|
{
|
|
keywords: ["dashboard", "widget", "widgets", "peak", "forecast", "insight", "insights", "anomaly", "anomalies", "report", "reports", "analyse", "analysis", "bericht"],
|
|
nameFragments: ["dashboard", "statistics", "report", "insight", "anomal", "health", "forecast", "skill"],
|
|
exactTools: ["get_statistics", "get_dashboard_detail", "detect_anomalies", "get_skill_gaps", "get_project_health", "get_budget_forecast", "get_insights_summary", "run_report"],
|
|
},
|
|
{
|
|
keywords: ["estimate", "estimates", "angebot", "angebote", "budget", "budgets", "cost", "costs", "kosten", "rate", "rates", "preis", "preise"],
|
|
nameFragments: ["estimate", "budget", "rate", "cost"],
|
|
exactTools: ["get_budget_status", "list_rate_cards", "resolve_rate", "lookup_rate", "search_estimates", "get_estimate_detail"],
|
|
},
|
|
{
|
|
keywords: ["notification", "notifications", "benachrichtigung", "benachrichtigungen", "task", "tasks", "aufgabe", "aufgaben", "reminder", "reminders", "broadcast"],
|
|
nameFragments: ["notification", "task", "reminder", "broadcast"],
|
|
exactTools: ["list_notifications", "get_unread_notification_count", "list_tasks", "get_task_counts", "list_reminders", "get_broadcast_detail"],
|
|
},
|
|
{
|
|
keywords: ["country", "countries", "land", "laender", "länder", "city", "cities", "stadt", "staedte", "städte", "region", "regions", "state", "bundesland"],
|
|
nameFragments: ["country", "metro_city", "holiday_calendar"],
|
|
exactTools: ["list_countries", "get_country", "list_holidays_by_region", "list_holiday_calendars"],
|
|
},
|
|
{
|
|
keywords: ["user", "users", "permission", "permissions", "rolle", "rollen", "admin", "system", "webhook", "import", "audit", "history", "rechte"],
|
|
nameFragments: ["user", "permission", "role", "system", "webhook", "import", "audit", "history", "org_unit", "country"],
|
|
exactTools: ["list_users", "get_effective_user_permissions", "list_audit_log_entries", "query_change_history", "get_system_settings", "list_webhooks"],
|
|
},
|
|
];
|
|
|
|
const TOOL_SELECTION_STOP_WORDS = new Set([
|
|
"the", "and", "for", "with", "from", "that", "this", "what", "when", "where", "who", "how",
|
|
"und", "der", "die", "das", "ein", "eine", "einer", "einem", "einen", "mit", "von", "fuer", "für",
|
|
"auf", "ist", "sind", "im", "in", "am", "an", "zu", "zum", "zur", "mir", "bitte", "can", "you",
|
|
"mir", "alle", "all", "den", "dem", "des",
|
|
]);
|
|
|
|
export type AssistantChatMessage = { role: "user" | "assistant"; content: string };
|
|
|
|
export function normalizeAssistantText(input: string): string {
|
|
return input
|
|
.toLowerCase()
|
|
.normalize("NFD")
|
|
.replace(/\p{Diacritic}/gu, " ")
|
|
.replace(/[^a-z0-9_]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
export function tokenizeAssistantIntent(input: string): string[] {
|
|
return normalizeAssistantText(input)
|
|
.split(" ")
|
|
.map((token) => token.trim())
|
|
.filter((token) => token.length >= 3 && !TOOL_SELECTION_STOP_WORDS.has(token));
|
|
}
|
|
|
|
export function selectAssistantToolsForRequest(
|
|
availableTools: ToolDef[],
|
|
messages: AssistantChatMessage[],
|
|
pageContext?: string,
|
|
): ToolDef[] {
|
|
if (availableTools.length <= MAX_OPENAI_TOOL_DEFINITIONS) {
|
|
return availableTools;
|
|
}
|
|
|
|
const recentUserText = messages
|
|
.filter((message) => message.role === "user")
|
|
.slice(-4)
|
|
.map((message) => message.content)
|
|
.join(" ");
|
|
const intentText = [recentUserText, pageContext ?? ""].filter(Boolean).join(" ");
|
|
const normalizedIntent = normalizeAssistantText(intentText);
|
|
const intentTokens = tokenizeAssistantIntent(intentText);
|
|
const mutationIntent = MUTATION_INTENT_KEYWORDS.some((keyword) => normalizedIntent.includes(normalizeAssistantText(keyword)));
|
|
|
|
const selectedHintTools = new Set<string>();
|
|
for (const hint of TOOL_SELECTION_HINTS) {
|
|
const matchedKeyword = hint.keywords.some((keyword) => normalizedIntent.includes(normalizeAssistantText(keyword)));
|
|
if (!matchedKeyword) continue;
|
|
for (const toolName of hint.exactTools) {
|
|
selectedHintTools.add(toolName);
|
|
}
|
|
}
|
|
|
|
return availableTools
|
|
.map((tool, index) => {
|
|
const name = tool.function.name;
|
|
const normalizedName = normalizeAssistantText(name.replace(/_/g, " "));
|
|
const normalizedDescription = normalizeAssistantText(tool.function.description);
|
|
let score = 0;
|
|
|
|
if (ALWAYS_INCLUDED_TOOL_NAMES.has(name)) score += 1000;
|
|
if (selectedHintTools.has(name)) score += 400;
|
|
|
|
for (const hint of TOOL_SELECTION_HINTS) {
|
|
const matchedKeyword = hint.keywords.some((keyword) => normalizedIntent.includes(normalizeAssistantText(keyword)));
|
|
if (!matchedKeyword) continue;
|
|
if (hint.exactTools.includes(name)) score += 160;
|
|
if (hint.nameFragments.some((fragment) => name.includes(fragment))) score += 120;
|
|
if (hint.nameFragments.some((fragment) => normalizedDescription.includes(normalizeAssistantText(fragment)))) score += 40;
|
|
}
|
|
|
|
for (const token of intentTokens) {
|
|
if (normalizedName.includes(token)) score += 45;
|
|
if (normalizedDescription.includes(token)) score += 10;
|
|
}
|
|
|
|
if (name.startsWith("search_")) score += 18;
|
|
if (name.startsWith("get_")) score += 12;
|
|
if (name.startsWith("list_")) score += 10;
|
|
|
|
if (MUTATION_TOOLS.has(name)) {
|
|
score += mutationIntent ? 40 : -30;
|
|
} else {
|
|
score += 8;
|
|
}
|
|
|
|
return { tool, index, score };
|
|
})
|
|
.sort((left, right) => {
|
|
if (right.score !== left.score) return right.score - left.score;
|
|
return left.index - right.index;
|
|
})
|
|
.slice(0, MAX_OPENAI_TOOL_DEFINITIONS)
|
|
.map((entry) => entry.tool);
|
|
}
|