агенты: добавить поиск инструментов по каталогу
This commit is contained in:
@@ -1403,6 +1403,177 @@
|
||||
.agents-rec-callout svg { flex-shrink: 0; color: #d2991f; margin-top: 1px; }
|
||||
.agents-rec-callout strong { color: var(--text-primary); }
|
||||
|
||||
.tool-access-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.tool-access-option {
|
||||
min-height: 112px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--bg-canvas);
|
||||
color: var(--text-secondary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-access-option:hover { border-color: var(--border-muted, #444c56); }
|
||||
.tool-access-option.active {
|
||||
border-color: var(--accent);
|
||||
background: rgba(45, 212, 191, 0.07);
|
||||
}
|
||||
.tool-access-option-title {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.tool-access-option.active .tool-access-option-title { color: var(--accent); }
|
||||
.tool-access-option-body {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tool-search-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
.tool-search-config-header,
|
||||
.tool-group-card-header,
|
||||
.tool-search-preview-controls,
|
||||
.tool-search-result,
|
||||
.tool-group-assignment-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.tool-search-config-title {
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tool-search-config-header .drawer-section-sub { margin: 3px 0 0; }
|
||||
.tool-search-config-header .btn-ghost-sm { white-space: nowrap; }
|
||||
.tool-group-empty {
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 7px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tool-group-card,
|
||||
.tool-search-preview {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
.tool-group-card-header { margin-bottom: 10px; }
|
||||
.tool-group-card-header strong { font-size: 12px; color: var(--text-primary); }
|
||||
.tool-group-card-header .agent-action-btn img { width: 13px; height: 13px; }
|
||||
.tool-group-fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.tool-group-fields label,
|
||||
.tool-group-description,
|
||||
.tool-search-limit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
.tool-group-description { margin-top: 9px; }
|
||||
.tool-group-assignments {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tool-group-assignments > .tool-search-config-title {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
.tool-group-assignment-row {
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
.tool-group-assignment-row:last-child { border-bottom: 0; }
|
||||
.tool-group-assignment-tool { min-width: 130px; }
|
||||
.tool-group-assignment-tool strong,
|
||||
.tool-group-assignment-tool code,
|
||||
.tool-search-result strong,
|
||||
.tool-search-result code {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tool-group-assignment-tool strong,
|
||||
.tool-search-result strong { color: var(--text-primary); font-size: 11px; }
|
||||
.tool-group-assignment-tool code,
|
||||
.tool-search-result code { margin-top: 3px; color: var(--text-muted); font-size: 10px; }
|
||||
.tool-group-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
}
|
||||
.tool-group-chip {
|
||||
max-width: 150px;
|
||||
padding: 4px 7px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-group-chip.active {
|
||||
border-color: var(--accent);
|
||||
background: rgba(45, 212, 191, 0.08);
|
||||
color: var(--accent);
|
||||
}
|
||||
.tool-search-limit { max-width: 190px; }
|
||||
.tool-search-preview .drawer-section-sub { margin-top: 4px; }
|
||||
.tool-search-preview-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 132px auto;
|
||||
}
|
||||
.tool-search-results {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
.tool-search-result { padding: 9px 0; border-bottom: 1px solid var(--border-subtle); }
|
||||
.tool-search-result:last-child { border-bottom: 0; }
|
||||
.tool-search-result > div { min-width: 0; }
|
||||
.tool-search-result > span { color: var(--text-muted); font-size: 10px; }
|
||||
.tool-search-preview > .tool-group-empty { margin-top: 10px; }
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.tool-access-options,
|
||||
.tool-group-fields,
|
||||
.tool-search-preview-controls { grid-template-columns: 1fr; }
|
||||
.tool-group-assignment-row { flex-direction: column; }
|
||||
.tool-group-chips { justify-content: flex-start; }
|
||||
.tool-search-limit { max-width: none; }
|
||||
}
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
Settings — Members enhanced
|
||||
|
||||
+101
-3
@@ -359,24 +359,122 @@
|
||||
<!-- Footer count -->
|
||||
<div class="ops-picker-footer" x-show="form.selectedOps.length > 0">
|
||||
<span x-text="tfKey('agents.drawer.ops_selected', { count: form.selectedOps.length })"></span>
|
||||
<button @click="form.selectedOps = []" style="background:none;border:none;color:var(--text-muted);font-size:12px;cursor:pointer;margin-left:8px;" data-i18n="agents.drawer.clear_all">Clear all</button>
|
||||
<button @click="clearSelectedOperations()" style="background:none;border:none;color:var(--text-muted);font-size:12px;cursor:pointer;margin-left:8px;" data-i18n="agents.drawer.clear_all">Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommendation callout -->
|
||||
<div class="agents-rec-callout" x-show="form.selectedOps.length > 15">
|
||||
<div class="agents-rec-callout" x-show="form.selectedOps.length > 15 && form.accessMode === 'direct'">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polygon points="8,1 15,14 1,14" fill="none"/><path d="M8 6v4M8 11.5v.5"/></svg>
|
||||
<span x-text="tfKey('agents.drawer.recommendation', { count: form.selectedOps.length })">You've selected tools.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-title" data-i18n="agents.drawer.access.title">Tool access</div>
|
||||
<div class="drawer-section-sub" data-i18n="agents.drawer.access.subtitle">Choose how the model receives this agent's tool catalog.</div>
|
||||
|
||||
<div class="tool-access-options">
|
||||
<button class="tool-access-option" :class="{ active: form.accessMode === 'direct' }" @click="setAccessMode('direct')">
|
||||
<span class="tool-access-option-title" data-i18n="agents.drawer.access.direct">Show tools immediately</span>
|
||||
<span class="tool-access-option-body" data-i18n="agents.drawer.access.direct_hint">Best for a small curated catalog. MCP clients receive every tool in tools/list.</span>
|
||||
</button>
|
||||
<button class="tool-access-option" :class="{ active: form.accessMode === 'search' }" @click="setAccessMode('search')">
|
||||
<span class="tool-access-option-title" data-i18n="agents.drawer.access.search">Select tools on demand</span>
|
||||
<span class="tool-access-option-body" data-i18n="agents.drawer.access.search_hint">The model sees search_tools and call_tool, then discovers only relevant schemas.</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-search-config" x-show="form.accessMode === 'search'" style="display:none">
|
||||
<div class="tool-search-config-header">
|
||||
<div>
|
||||
<div class="tool-search-config-title" data-i18n="agents.drawer.groups.title">Catalog sections</div>
|
||||
<div class="drawer-section-sub" data-i18n="agents.drawer.groups.subtitle">Sections help the model narrow a search without blocking catalog-wide discovery.</div>
|
||||
</div>
|
||||
<button class="btn-ghost-sm" @click="addToolGroup()" data-i18n="agents.drawer.groups.add">Add section</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group-empty" x-show="form.groups.length === 0" data-i18n="agents.drawer.groups.empty">No sections yet. Search will use the entire selected catalog.</div>
|
||||
<template x-for="(group, groupIndex) in form.groups" :key="groupIndex">
|
||||
<div class="tool-group-card">
|
||||
<div class="tool-group-card-header">
|
||||
<strong x-text="group.name || tKey('agents.drawer.groups.untitled')"></strong>
|
||||
<button class="agent-action-btn danger" @click="removeToolGroup(groupIndex)" :title="tKey('agents.drawer.groups.remove')">
|
||||
<img src="/icons/general/trash.svg" alt="">
|
||||
</button>
|
||||
</div>
|
||||
<div class="tool-group-fields">
|
||||
<label>
|
||||
<span data-i18n="agents.drawer.groups.name">Name</span>
|
||||
<input class="form-input" type="text" :value="group.name" @input="onToolGroupName(groupIndex, $event.target.value)" data-i18n-ph="agents.drawer.groups.name_placeholder" placeholder="Finance">
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="agents.drawer.groups.id">Identifier</span>
|
||||
<input class="form-input input-mono" type="text" :value="group.id" @input="onToolGroupId(groupIndex, $event.target.value)" placeholder="finance">
|
||||
</label>
|
||||
</div>
|
||||
<label class="tool-group-description">
|
||||
<span data-i18n="agents.drawer.groups.description">Description for the model</span>
|
||||
<textarea class="form-textarea" rows="2" x-model="group.description" @input="resetSearchPreview()" data-i18n-ph="agents.drawer.groups.description_placeholder" placeholder="Invoices, payments and refunds"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="tool-group-assignments" x-show="form.groups.length > 0 && selectedOperations.length > 0">
|
||||
<div class="tool-search-config-title" data-i18n="agents.drawer.groups.assign">Assign tools to sections</div>
|
||||
<template x-for="operation in selectedOperations" :key="operation.id">
|
||||
<div class="tool-group-assignment-row">
|
||||
<div class="tool-group-assignment-tool">
|
||||
<strong x-text="operation.display_name || operation.name"></strong>
|
||||
<code x-text="operation.name"></code>
|
||||
</div>
|
||||
<div class="tool-group-chips">
|
||||
<template x-for="(group, groupIndex) in form.groups" :key="groupIndex">
|
||||
<button class="tool-group-chip" :class="{ active: toolInGroup(groupIndex, operation.name) }" @click="toggleToolGroup(groupIndex, operation.name)" x-text="group.name || group.id || '—'"></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<label class="tool-search-limit">
|
||||
<span data-i18n="agents.drawer.search.limit">Maximum results per search</span>
|
||||
<input class="form-input" type="number" min="1" max="20" x-model.number="form.searchMaxResults" @input="resetSearchPreview()">
|
||||
</label>
|
||||
|
||||
<div class="tool-search-preview">
|
||||
<div class="tool-search-config-title" data-i18n="agents.drawer.search.preview_title">Test tool selection</div>
|
||||
<div class="drawer-section-sub" data-i18n="agents.drawer.search.preview_subtitle">Enter a task and verify which tools the model will receive.</div>
|
||||
<div class="tool-search-preview-controls">
|
||||
<input class="form-input" type="text" x-model="searchPreviewQuery" @keydown.enter.prevent="previewToolSearch()" data-i18n-ph="agents.drawer.search.query_placeholder" placeholder="Create an invoice for a customer">
|
||||
<select class="form-select" x-model="searchPreviewGroup">
|
||||
<option value="" data-i18n="agents.drawer.search.all_groups">All sections</option>
|
||||
<template x-for="group in form.groups" :key="group.id">
|
||||
<option :value="group.id" x-text="group.name || group.id"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button class="btn-primary-sm" :disabled="searchPreviewLoading || !searchPreviewQuery.trim() || !catalogConfigValid" @click="previewToolSearch()" x-text="searchPreviewLoading ? tKey('agents.drawer.search.testing') : tKey('agents.drawer.search.test')">Test</button>
|
||||
</div>
|
||||
<div class="tool-search-results" x-show="searchPreviewItems.length > 0">
|
||||
<template x-for="item in searchPreviewItems" :key="item.tool.name">
|
||||
<div class="tool-search-result">
|
||||
<div><strong x-text="item.tool.title"></strong><code x-text="item.tool.name"></code></div>
|
||||
<span x-text="item.tool.group_ids.join(', ')"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="tool-group-empty" x-show="searchPreviewRan && !searchPreviewLoading && searchPreviewItems.length === 0" data-i18n="agents.drawer.search.no_results">No tools matched this task.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /drawer-body -->
|
||||
|
||||
<!-- Drawer footer -->
|
||||
<div class="drawer-footer">
|
||||
<button class="btn-ghost-sm" style="padding: 8px 16px; font-size: 13px;" @click="closeDrawer()" data-i18n="agents.drawer.cancel">Cancel</button>
|
||||
<button class="btn-primary-sm" style="padding: 8px 20px; font-size: 13px;"
|
||||
:disabled="!form.display_name.trim() || !form.slug.trim()"
|
||||
:disabled="!form.display_name.trim() || !form.slug.trim() || !catalogConfigValid"
|
||||
@click="saveAgent()"
|
||||
x-text="drawerMode === 'create' ? tKey('agents.drawer.create') : tKey('agents.drawer.save')">
|
||||
Create agent
|
||||
|
||||
+192
-20
@@ -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, {
|
||||
|
||||
@@ -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
@@ -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': 'Сохранить изменения',
|
||||
|
||||
@@ -21,3 +21,26 @@ test('agents page shows demo cards and edit drawer opens', async ({ page }) => {
|
||||
);
|
||||
await expect(page.locator('.drawer')).toContainText(/mcp/i);
|
||||
});
|
||||
|
||||
test('agent drawer configures on-demand tool discovery and catalog sections', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/agents');
|
||||
await page.getByRole('button', { name: localized('New agent', 'Новый агент') }).click();
|
||||
await page.locator('.ops-picker-item').first().click();
|
||||
|
||||
await page.locator('.tool-access-option').nth(1).click();
|
||||
await expect(page.locator('.tool-search-config')).toBeVisible();
|
||||
await page.getByRole('button', { name: localized('Add section', 'Добавить раздел') }).click();
|
||||
|
||||
const group = page.locator('.tool-group-card').first();
|
||||
await group.locator('input').nth(0).fill('Finance');
|
||||
await group.locator('input').nth(1).fill('finance');
|
||||
await group.locator('textarea').fill('Invoices, payments and refunds');
|
||||
|
||||
await expect(group.locator('input').nth(1)).toHaveValue('finance');
|
||||
await expect(page.locator('.tool-search-preview')).toBeVisible();
|
||||
await page.locator('.tool-group-chip').first().click();
|
||||
await page.locator('.tool-search-preview input').fill('currency rate');
|
||||
await page.locator('.tool-search-preview .btn-primary-sm').click();
|
||||
await expect(page.locator('.tool-search-result').first()).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user