агенты: добавить поиск инструментов по каталогу

This commit is contained in:
2026-07-21 13:12:46 +03:00
parent 63f8ee333f
commit 99bd05c145
35 changed files with 2088 additions and 155 deletions
+192 -20
View File
@@ -14,6 +14,7 @@ function mapAgent(agent) {
raw_status: agent.status,
operation_count: agent.operation_count || 0,
operation_ids: agent.operation_ids || [],
tool_selection_policy: agent.tool_selection_policy || { mode: 'direct', groups: [], search: { max_results: 8 } },
key_count: agent.key_count || 0,
calls_today: agent.calls_today || 0,
created_at: agent.created_at,
@@ -66,10 +67,18 @@ document.addEventListener('alpine:init', function() {
description: '',
status: 'published',
selectedOps: [],
accessMode: 'direct',
groups: [],
searchMaxResults: 8,
},
opSearch: '',
slugManuallyEdited: false,
searchPreviewQuery: '',
searchPreviewGroup: '',
searchPreviewItems: [],
searchPreviewLoading: false,
searchPreviewRan: false,
async init() {
var self = this;
@@ -205,7 +214,7 @@ document.addEventListener('alpine:init', function() {
get agentToolFindings() {
var findings = [];
var selected = this.selectedOperations;
if (selected.length > 8) {
if (selected.length > 8 && this.form.accessMode === 'direct') {
findings.push(this.tKey('agents.drawer.finding.too_many_tools'));
}
@@ -242,13 +251,18 @@ document.addEventListener('alpine:init', function() {
description: '',
status: 'published',
selectedOps: [],
accessMode: 'direct',
groups: [],
searchMaxResults: 8,
};
this.opSearch = '';
this.slugManuallyEdited = false;
this.resetSearchPreview();
this.drawerOpen = true;
},
openEdit(agent) {
var policy = agent.tool_selection_policy || {};
this.drawerMode = 'edit';
this.editingId = agent.id;
this.form = {
@@ -257,9 +271,22 @@ document.addEventListener('alpine:init', function() {
description: agent.description,
status: agent.raw_status || agent.status || 'draft',
selectedOps: [].concat(agent.operation_ids || []),
accessMode: policy.mode === 'search' ? 'search' : 'direct',
groups: (policy.groups || []).map(function(group) {
return {
id: group.id || '',
name: group.name || '',
description: group.description || '',
tool_names: [].concat(group.tool_names || []),
};
}),
searchMaxResults: policy.search && policy.search.max_results
? policy.search.max_results
: 8,
};
this.opSearch = '';
this.slugManuallyEdited = true;
this.resetSearchPreview();
this.drawerOpen = true;
},
@@ -289,13 +316,170 @@ document.addEventListener('alpine:init', function() {
this.form.selectedOps.push(operationId);
} else {
this.form.selectedOps.splice(index, 1);
var operation = this.operations.find(function(item) { return item.id === operationId; });
if (operation) {
this.form.groups.forEach(function(group) {
group.tool_names = group.tool_names.filter(function(name) {
return name !== operation.name;
});
});
}
}
this.resetSearchPreview();
},
isOpSelected(operationId) {
return this.form.selectedOps.includes(operationId);
},
clearSelectedOperations() {
this.form.selectedOps = [];
this.form.groups.forEach(function(group) { group.tool_names = []; });
this.resetSearchPreview();
},
setAccessMode(mode) {
this.form.accessMode = mode === 'search' ? 'search' : 'direct';
this.resetSearchPreview();
},
addToolGroup() {
this.form.groups.push({ id: '', name: '', description: '', tool_names: [] });
this.resetSearchPreview();
},
removeToolGroup(index) {
this.form.groups.splice(index, 1);
this.resetSearchPreview();
},
onToolGroupName(index, value) {
var group = this.form.groups[index];
if (!group) return;
var previousSlug = this.slugifyGroupName(group.name);
group.name = value;
if (!group.id || group.id === previousSlug) {
group.id = this.slugifyGroupName(value);
}
this.resetSearchPreview();
},
onToolGroupId(index, value) {
var group = this.form.groups[index];
if (!group) return;
group.id = this.slugifyGroupName(value);
this.resetSearchPreview();
},
slugifyGroupName(value) {
return String(value || '')
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
},
toggleToolGroup(groupIndex, toolName) {
var group = this.form.groups[groupIndex];
if (!group) return;
var index = group.tool_names.indexOf(toolName);
if (index === -1) group.tool_names.push(toolName);
else group.tool_names.splice(index, 1);
this.resetSearchPreview();
},
toolInGroup(groupIndex, toolName) {
var group = this.form.groups[groupIndex];
return Boolean(group && group.tool_names.includes(toolName));
},
toolSelectionPolicy() {
return {
mode: this.form.accessMode,
groups: this.form.accessMode === 'search'
? this.form.groups.map(function(group) {
return {
id: group.id.trim(),
name: group.name.trim(),
description: group.description.trim(),
tool_names: [].concat(group.tool_names || []),
};
})
: [],
search: {
max_results: Math.max(1, Math.min(20, Number(this.form.searchMaxResults) || 8)),
},
};
},
get catalogConfigValid() {
if (this.form.accessMode !== 'search') return true;
var ids = [];
for (var index = 0; index < this.form.groups.length; index += 1) {
var group = this.form.groups[index];
if (!group.id.trim() || !group.name.trim() || !group.description.trim()) return false;
if (ids.includes(group.id.trim())) return false;
ids.push(group.id.trim());
}
return true;
},
agentBindings() {
var self = this;
return this.form.selectedOps.map(function(operationId) {
var operation = self.operations.find(function(item) { return item.id === operationId; });
return {
operation_id: operationId,
operation_version: operation && operation.latest_published_version
? operation.latest_published_version
: operation && operation.current_draft_version
? operation.current_draft_version
: 1,
tool_name: operation ? operation.name : operationId,
tool_title: operation ? (operation.display_name || operation.name) : operationId,
tool_description_override: null,
enabled: true,
};
});
},
resetSearchPreview() {
this.searchPreviewItems = [];
this.searchPreviewRan = false;
},
async previewToolSearch() {
if (
this.searchPreviewLoading
|| !this.workspaceId
|| !this.searchPreviewQuery.trim()
|| this.form.accessMode !== 'search'
) return;
this.searchPreviewLoading = true;
this.searchPreviewRan = false;
try {
var response = await window.CrankApi.previewAgentToolSearch(this.workspaceId, {
query: this.searchPreviewQuery.trim(),
group_ids: this.searchPreviewGroup ? [this.searchPreviewGroup] : [],
bindings: this.agentBindings(),
tool_selection_policy: this.toolSelectionPolicy(),
});
this.searchPreviewItems = response.items || [];
this.searchPreviewRan = true;
} catch (error) {
this.searchPreviewItems = [];
if (window.CrankUi) {
window.CrankUi.error(
error.message || this.tKey('agents.drawer.search.preview_error'),
this.tKey('agents.drawer.search.preview_error_title')
);
}
} finally {
this.searchPreviewLoading = false;
}
},
operationsLookSimilar(left, right) {
var leftTokens = this.operationTokens(left);
var rightTokens = this.operationTokens(right);
@@ -345,7 +529,7 @@ document.addEventListener('alpine:init', function() {
display_name: this.form.display_name,
description: this.form.description,
instructions: {},
tool_selection_policy: {},
tool_selection_policy: this.toolSelectionPolicy(),
});
agentId = created.agent_id;
currentVersion = created.version || 1;
@@ -359,27 +543,15 @@ document.addEventListener('alpine:init', function() {
currentVersion = agent.current_draft_version || 1;
}
await window.CrankApi.saveAgentBindings(
var savedVersion = await window.CrankApi.saveAgentBindings(
this.workspaceId,
agentId,
this.form.selectedOps.map(function(operationId) {
var operation = self.operations.find(function(item) {
return item.id === operationId;
});
return {
operation_id: operationId,
operation_version: operation && operation.latest_published_version
? operation.latest_published_version
: operation && operation.current_draft_version
? operation.current_draft_version
: 1,
tool_name: operation ? operation.name : operationId,
tool_title: operation ? (operation.display_name || operation.name) : operationId,
tool_description_override: null,
enabled: true,
};
}),
{
bindings: this.agentBindings(),
tool_selection_policy: this.toolSelectionPolicy(),
},
);
currentVersion = savedVersion.version || currentVersion;
if (this.form.status === 'published') {
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
+3
View File
@@ -262,6 +262,9 @@
saveAgentBindings: function(workspaceId, agentId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/bindings', payload);
},
previewAgentToolSearch: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/tool-search/preview', payload);
},
publishAgent: function(workspaceId, agentId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/publish', payload);
},
+60 -4
View File
@@ -819,13 +819,41 @@ var TRANSLATIONS = {
'agents.drawer.operations_sub': 'Select the MCP tools available to this agent.',
'agents.drawer.operations_sub_community': 'Select the MCP tools available to this agent.',
'agents.drawer.finding.title': 'Recommendation',
'agents.drawer.finding.too_many_tools': 'This agent has many tools. Keep only the tools needed for one concrete task.',
'agents.drawer.finding.too_many_tools': 'This agent has many tools. Use on-demand selection or keep only the tools needed for one concrete task.',
'agents.drawer.finding.similar_tools': 'Tools “{left}” and “{right}” look similar. Rename them more precisely or keep one of them.',
'agents.drawer.filter_ops': 'Filter operations…',
'agents.drawer.ops_no_match': 'No operations match "{query}"',
'agents.drawer.ops_selected': '{count} operations selected',
'agents.drawer.clear_all': 'Clear all',
'agents.drawer.recommendation': "You've selected {count} tools. LLMs usually work best when an agent has fewer than 15 tools. Consider splitting this into separate agents by use case.",
'agents.drawer.recommendation': "You've selected {count} tools. Switch to on-demand selection so the model receives only relevant schemas.",
'agents.drawer.access.title': 'Tool access',
'agents.drawer.access.subtitle': "Choose how the model receives this agent's tool catalog.",
'agents.drawer.access.direct': 'Show tools immediately',
'agents.drawer.access.direct_hint': 'Best for a small curated catalog. MCP clients receive every tool in tools/list.',
'agents.drawer.access.search': 'Select tools on demand',
'agents.drawer.access.search_hint': 'The model sees search_tools and call_tool, then discovers only relevant schemas.',
'agents.drawer.groups.title': 'Catalog sections',
'agents.drawer.groups.subtitle': 'Sections help the model narrow a search without blocking catalog-wide discovery.',
'agents.drawer.groups.add': 'Add section',
'agents.drawer.groups.empty': 'No sections yet. Search will use the entire selected catalog.',
'agents.drawer.groups.untitled': 'Untitled section',
'agents.drawer.groups.remove': 'Remove section',
'agents.drawer.groups.name': 'Name',
'agents.drawer.groups.name_placeholder': 'Finance',
'agents.drawer.groups.id': 'Identifier',
'agents.drawer.groups.description': 'Description for the model',
'agents.drawer.groups.description_placeholder': 'Invoices, payments and refunds',
'agents.drawer.groups.assign': 'Assign tools to sections',
'agents.drawer.search.limit': 'Maximum results per search',
'agents.drawer.search.preview_title': 'Test tool selection',
'agents.drawer.search.preview_subtitle': 'Enter a task and verify which tools the model will receive.',
'agents.drawer.search.query_placeholder': 'Create an invoice for a customer',
'agents.drawer.search.all_groups': 'All sections',
'agents.drawer.search.test': 'Test',
'agents.drawer.search.testing': 'Testing…',
'agents.drawer.search.no_results': 'No preview results yet.',
'agents.drawer.search.preview_error': 'Failed to test tool selection',
'agents.drawer.search.preview_error_title': 'Tool selection test failed',
'agents.drawer.cancel': 'Cancel',
'agents.drawer.create': 'Create agent',
'agents.drawer.save': 'Save changes',
@@ -1683,13 +1711,41 @@ var TRANSLATIONS = {
'agents.drawer.operations_sub': 'Выберите MCP инструменты, которые будут доступны для этого агента.',
'agents.drawer.operations_sub_community': 'Выберите MCP инструменты, которые будут доступны для этого агента.',
'agents.drawer.finding.title': 'Рекомендация',
'agents.drawer.finding.too_many_tools': 'У агента выбрано много инструментов. Оставьте только те, которые нужны для одной конкретной задачи.',
'agents.drawer.finding.too_many_tools': 'У агента выбрано много инструментов. Включите подбор по запросу или оставьте только инструменты для одной конкретной задачи.',
'agents.drawer.finding.similar_tools': 'Инструменты «{left}» и «{right}» похожи. Переименуйте их точнее или оставьте один вариант.',
'agents.drawer.filter_ops': 'Фильтр операций…',
'agents.drawer.ops_no_match': 'Нет операций по запросу "{query}"',
'agents.drawer.ops_selected': 'Выбрано операций: {count}',
'agents.drawer.clear_all': 'Очистить все',
'agents.drawer.recommendation': 'Сейчас выбрано {count} инструментов. LLM лучше работает, когда у агента меньше 15 инструментов. Подумайте о разбиении по сценариям.',
'agents.drawer.recommendation': 'Сейчас выбрано {count} инструментов. Включите подбор по запросу, чтобы модель получала только подходящие схемы.',
'agents.drawer.access.title': 'Доступ к инструментам',
'agents.drawer.access.subtitle': 'Выберите, как модель будет получать каталог инструментов этого агента.',
'agents.drawer.access.direct': 'Показывать сразу',
'agents.drawer.access.direct_hint': 'Для небольшого отобранного каталога. MCP-клиент получает все инструменты через tools/list.',
'agents.drawer.access.search': 'Подбирать по запросу',
'agents.drawer.access.search_hint': 'Модель видит search_tools и call_tool, а затем получает только подходящие схемы.',
'agents.drawer.groups.title': 'Разделы каталога',
'agents.drawer.groups.subtitle': 'Разделы сужают область поиска, но не мешают искать по всему каталогу.',
'agents.drawer.groups.add': 'Добавить раздел',
'agents.drawer.groups.empty': 'Разделов пока нет. Поиск будет выполняться по всему выбранному каталогу.',
'agents.drawer.groups.untitled': 'Раздел без названия',
'agents.drawer.groups.remove': 'Удалить раздел',
'agents.drawer.groups.name': 'Название',
'agents.drawer.groups.name_placeholder': 'Расчёты',
'agents.drawer.groups.id': 'Идентификатор',
'agents.drawer.groups.description': 'Описание для модели',
'agents.drawer.groups.description_placeholder': 'Счета, платежи и возвраты',
'agents.drawer.groups.assign': 'Распределение инструментов по разделам',
'agents.drawer.search.limit': 'Максимум результатов за один поиск',
'agents.drawer.search.preview_title': 'Проверка подбора',
'agents.drawer.search.preview_subtitle': 'Введите задачу и проверьте, какие инструменты получит модель.',
'agents.drawer.search.query_placeholder': 'Создать счёт для клиента',
'agents.drawer.search.all_groups': 'Все разделы',
'agents.drawer.search.test': 'Проверить',
'agents.drawer.search.testing': 'Проверяем…',
'agents.drawer.search.no_results': 'Результатов проверки пока нет.',
'agents.drawer.search.preview_error': 'Не удалось проверить подбор инструментов',
'agents.drawer.search.preview_error_title': 'Ошибка проверки подбора',
'agents.drawer.cancel': 'Отмена',
'agents.drawer.create': 'Создать агента',
'agents.drawer.save': 'Сохранить изменения',