Dynamic tool selection for Ollama based on user intent

Instead of sending all 12 tools every request, match the user's message
against keyword groups (status, workload, assign, create, advance, revision)
and only send relevant tools. search_entities always included for name
resolution. Falls back to basic query tools if no keywords match.

This cuts the tool definitions from ~12 to ~2-6 per request, significantly
reducing context size for gemma4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DJP 2026-04-08 15:55:07 -04:00
parent e99391b824
commit 697b015675

View file

@ -273,34 +273,65 @@ async function checkOllamaHealth(): Promise<boolean> {
}
/**
* Tools to send to Ollama a trimmed subset to keep context small.
* Smaller models struggle with 17 tool definitions, so we send only
* the most commonly used ones. Bulk tools are excluded (handled by
* the RBAC layer + confirmation flow anyway).
* Dynamic tool selection for Ollama pick only the tools relevant to
* the user's request so smaller models aren't overwhelmed by 20 definitions.
* `search_entities` is always included (needed for nameID resolution).
*/
const OLLAMA_TOOL_ALLOWLIST = new Set([
"search_entities",
"list_projects",
"get_project",
"list_deliverables",
"list_users",
"get_blocked_stages",
"list_overdue",
"get_workload",
"assign_artist",
"advance_stage",
"create_project",
"create_deliverable",
]);
const TOOL_GROUPS: Record<string, { keywords: RegExp; tools: string[] }> = {
status: {
keywords: /status|overview|how.?s|progress|update me|what.?s going|summary|blocked|overdue|late|behind|bottleneck/i,
tools: ["list_projects", "get_project", "list_deliverables", "get_blocked_stages", "list_overdue"],
},
workload: {
keywords: /workload|capacity|busy|bandwidth|availab|who.?s free|how many|assigned/i,
tools: ["get_workload", "list_users", "get_available_artists", "get_suggested_artists"],
},
assign: {
keywords: /assign|reassign|move .* to|put .* on|give .* to|allocat/i,
tools: ["assign_artist", "remove_assignment", "get_available_artists", "get_suggested_artists", "list_users"],
},
create: {
keywords: /create|new project|new deliverable|add.*project|add.*deliverable|set up|setup/i,
tools: ["create_project", "create_deliverable"],
},
advance: {
keywords: /advance|approve|move.*stage|next stage|complete.*stage|mark.*done|progress.*stage|skip/i,
tools: ["advance_stage"],
},
revision: {
keywords: /revision|review|feedback|note|comment|round/i,
tools: ["create_revision", "list_revisions"],
},
};
function getOllamaTools(userMessage: string) {
// Always include search_entities for name resolution
const selected = new Set(["search_entities"]);
// Match user message against keyword groups
let matched = false;
for (const group of Object.values(TOOL_GROUPS)) {
if (group.keywords.test(userMessage)) {
matched = true;
for (const tool of group.tools) selected.add(tool);
}
}
// If nothing matched (generic question), include basic query tools
if (!matched) {
for (const t of ["list_projects", "get_project", "list_deliverables", "list_users"]) {
selected.add(t);
}
}
console.log(`[Ollama] Selected ${selected.size} tools for: "${userMessage.slice(0, 60)}…"`);
function getOllamaTools() {
return TOOL_DEFINITIONS
.filter((t) => OLLAMA_TOOL_ALLOWLIST.has(t.name))
.filter((t) => selected.has(t.name))
.map((t) => ({
type: "function" as const,
function: {
name: t.name,
// Shorten descriptions for smaller context
description: t.description.split(".")[0] + ".",
parameters: t.input_schema,
},
@ -388,10 +419,18 @@ async function chatWithOllama(
ollamaMessages[0].content = sp.trim() + "\n\nUse bullet points, not tables. Be concise.";
}
// Extract the last user message for dynamic tool selection
const lastUserContent = [...messages].reverse().find((m) => m.role === "user");
const userText = typeof lastUserContent?.content === "string"
? lastUserContent.content
: Array.isArray(lastUserContent?.content)
? lastUserContent.content.map((b: any) => b.text || b.content || "").join(" ")
: "";
const requestBody = JSON.stringify({
model: getOllamaChatModel(),
messages: ollamaMessages,
tools: getOllamaTools(),
tools: getOllamaTools(userText),
stream: false,
options: {
temperature: 0.3,