mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-30 19:33:05 +08:00
feat(agent): enforce model readiness and polish selector UX
Block chat when agents lack an explicit chat or rerank model, guide users through the selector and editor with field highlight, and align session QA checks with the same readiness rules on backend and frontend.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,12 @@ import {
|
||||
toolsConsumeFiles,
|
||||
type ScopeCapabilities,
|
||||
} from '@/utils/tool-capabilities';
|
||||
import {
|
||||
getAgentNotReadyReasonKeys,
|
||||
resolveAgentNotReadySection,
|
||||
resolveAgentNotReadyHighlight,
|
||||
type AgentNotReadyReasonKey,
|
||||
} from '@/utils/agent-readiness';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -37,6 +43,7 @@ const chatResources = useChatResourcesStore();
|
||||
const {
|
||||
agents,
|
||||
disabledOwnAgentIds,
|
||||
allModels,
|
||||
chatModels: availableModels,
|
||||
webSearchProviders,
|
||||
} = storeToRefs(chatResources);
|
||||
@@ -1608,10 +1615,14 @@ const createSession = async (val: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chatResources.isFresh('models')) {
|
||||
await loadChatModels()
|
||||
}
|
||||
|
||||
// 发送前校验当前选中的智能体(含默认快速问答)是否已配置完成
|
||||
const agentToCheck = selectedAgent.value;
|
||||
let actualAgent = agentToCheck;
|
||||
if (agentToCheck.is_builtin) {
|
||||
if (agentToCheck.is_builtin && !settingsStore.selectedAgentSourceTenantId) {
|
||||
let builtin = agents.value.find(a => a.id === selectedAgentId.value);
|
||||
if (!builtin) {
|
||||
await loadAgents();
|
||||
@@ -1620,11 +1631,9 @@ const createSession = async (val: string) => {
|
||||
actualAgent = builtin || agentToCheck;
|
||||
}
|
||||
const isAgentMode = actualAgent.config?.agent_mode === 'smart-reasoning';
|
||||
const notReadyReasons = actualAgent.is_builtin
|
||||
? getBuiltinAgentNotReadyReasons(actualAgent, isAgentMode)
|
||||
: getCustomAgentNotReadyReasons(actualAgent);
|
||||
const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons(actualAgent, isAgentMode);
|
||||
if (notReadyReasons.length > 0) {
|
||||
showAgentNotReadyMessage(actualAgent, notReadyReasons);
|
||||
showAgentNotReadyMessage(actualAgent, notReadyReasons, notReadyKeys);
|
||||
return;
|
||||
}
|
||||
// 获取@提及的知识库和文件信息
|
||||
@@ -1762,14 +1771,20 @@ const toggleAgentModeSelector = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const selectAgentMode = (mode: 'quick-answer' | 'smart-reasoning') => {
|
||||
const selectAgentMode = async (mode: 'quick-answer' | 'smart-reasoning') => {
|
||||
if (!chatResources.isFresh('models')) {
|
||||
await loadChatModels()
|
||||
}
|
||||
|
||||
const builtinAgentId = mode === 'smart-reasoning' ? BUILTIN_SMART_REASONING_ID : BUILTIN_QUICK_ANSWER_ID;
|
||||
const builtinAgent = agents.value.find(a => a.id === builtinAgentId);
|
||||
|
||||
if (builtinAgent) {
|
||||
const notReadyReasons = getBuiltinAgentNotReadyReasons(builtinAgent, mode === 'smart-reasoning');
|
||||
const { labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
builtinAgent,
|
||||
mode === 'smart-reasoning',
|
||||
);
|
||||
if (notReadyReasons.length > 0) {
|
||||
showAgentNotReadyMessage(builtinAgent, notReadyReasons);
|
||||
showAgentModeSelector.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -1786,21 +1801,27 @@ const selectAgentMode = (mode: 'quick-answer' | 'smart-reasoning') => {
|
||||
}
|
||||
|
||||
// 选择智能体(新版);sourceTenantId 为共享智能体时传入
|
||||
const handleSelectAgent = (agent: CustomAgent, sourceTenantId?: string) => {
|
||||
const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) => {
|
||||
if (!chatResources.isFresh('models')) {
|
||||
await loadChatModels()
|
||||
}
|
||||
|
||||
// 根据智能体的 agent_mode 判断是否为 Agent 模式
|
||||
const isAgentType = agent.config?.agent_mode === 'smart-reasoning';
|
||||
|
||||
// 统一检查智能体是否就绪(内置和自定义智能体使用相同逻辑)
|
||||
const actualAgent = agent.is_builtin
|
||||
const actualAgent = agent.is_builtin && !sourceTenantId
|
||||
? (agents.value.find(a => a.id === agent.id) || agent)
|
||||
: agent;
|
||||
|
||||
const notReadyReasons = agent.is_builtin
|
||||
? getBuiltinAgentNotReadyReasons(actualAgent, isAgentType)
|
||||
: getCustomAgentNotReadyReasons(actualAgent);
|
||||
const { labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
actualAgent,
|
||||
isAgentType,
|
||||
sourceTenantId,
|
||||
);
|
||||
|
||||
if (notReadyReasons.length > 0) {
|
||||
showAgentNotReadyMessage(agent, notReadyReasons);
|
||||
showAgentModeSelector.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1933,64 +1954,77 @@ const handleGoToWebSearchSettings = () => {
|
||||
};
|
||||
|
||||
const handleGoToAgentSettings = (section?: string) => {
|
||||
// 跳转到智能体列表页并打开编辑弹窗
|
||||
if (selectedAgent.value && !selectedAgent.value.is_builtin) {
|
||||
const query: Record<string, string> = { edit: selectedAgent.value.id };
|
||||
if (section) {
|
||||
query.section = section;
|
||||
}
|
||||
router.push({ path: '/platform/agents', query });
|
||||
} else {
|
||||
const agent = selectedAgent.value;
|
||||
if (!agent) {
|
||||
router.push('/platform/agents');
|
||||
return;
|
||||
}
|
||||
const query: Record<string, string> = { edit: agent.id };
|
||||
if (section) {
|
||||
query.section = section;
|
||||
}
|
||||
router.push({ path: '/platform/agents', query });
|
||||
};
|
||||
|
||||
// 获取内置智能体不就绪的原因
|
||||
const getBuiltinAgentNotReadyReasons = (agent: CustomAgent, isAgentMode: boolean): string[] => {
|
||||
const reasons: string[] = []
|
||||
const config = agent.config || {}
|
||||
|
||||
// 内置智能体会自动回退到租户的默认模型,因此不再在前端强制校验 model_id
|
||||
|
||||
// 检查重排模型(Rerank Model)- 仅当允许使用 knowledge_search 工具时需要
|
||||
// 内置智能体允许重排模型为空(使用默认配置)
|
||||
// const hasKnowledgeSearchTool = config.allowed_tools && config.allowed_tools.includes('knowledge_search')
|
||||
// if (hasKnowledgeSearchTool) {
|
||||
// if (!config.rerank_model_id || config.rerank_model_id.trim() === '') {
|
||||
// reasons.push(t('input.customAgentMissingRerankModel'))
|
||||
// }
|
||||
// }
|
||||
|
||||
// Agent 模式还需要检查允许的工具
|
||||
if (isAgentMode) {
|
||||
if (!config.allowed_tools || config.allowed_tools.length === 0) {
|
||||
reasons.push(t('input.agentMissingAllowedTools'))
|
||||
const formatAgentNotReadyReasons = (
|
||||
reasonKeys: AgentNotReadyReasonKey[],
|
||||
isBuiltin: boolean,
|
||||
): string[] => {
|
||||
return reasonKeys.map((key) => {
|
||||
if (key === 'summary_model') {
|
||||
return isBuiltin
|
||||
? t('input.agentMissingSummaryModel')
|
||||
: t('input.customAgentMissingSummaryModel');
|
||||
}
|
||||
}
|
||||
if (key === 'rerank_model') {
|
||||
return isBuiltin
|
||||
? t('input.agentMissingRerankModel')
|
||||
: t('input.customAgentMissingRerankModel');
|
||||
}
|
||||
return t('input.agentMissingAllowedTools');
|
||||
});
|
||||
};
|
||||
|
||||
return reasons
|
||||
}
|
||||
const collectAgentNotReadyReasons = (
|
||||
agent: CustomAgent,
|
||||
isAgentMode: boolean,
|
||||
sourceTenantId?: string,
|
||||
): { keys: AgentNotReadyReasonKey[]; labels: string[] } => {
|
||||
const isSharedAgent = !!(sourceTenantId || settingsStore.selectedAgentSourceTenantId);
|
||||
const keys = getAgentNotReadyReasonKeys(agent.config, allModels.value, {
|
||||
isAgentMode,
|
||||
isSharedAgent,
|
||||
});
|
||||
return {
|
||||
keys,
|
||||
labels: formatAgentNotReadyReasons(keys, agent.is_builtin),
|
||||
};
|
||||
};
|
||||
|
||||
// 获取自定义智能体不就绪的原因(非 Agent 模式,快速回答)
|
||||
const getCustomAgentNotReadyReasons = (agent: CustomAgent): string[] => {
|
||||
const reasons: string[] = []
|
||||
const config = agent.config || {}
|
||||
|
||||
// 检查对话模型(Summary Model)
|
||||
if (!config.model_id || config.model_id.trim() === '') {
|
||||
reasons.push(t('input.customAgentMissingSummaryModel'))
|
||||
}
|
||||
// Rerank 模型不在此处强制校验:当 knowledge_search 实际命中 RAG 知识库时,
|
||||
// 后端会优先使用 agent.rerank_model_id,未配置则回退到租户默认 rerank 模型;
|
||||
// 仅在两者都缺失时由后端报错。这样可以避免"作用域内无 RAG KB 却被拦"的误报,
|
||||
// 并支持后续添加 RAG KB 时的自动兜底。
|
||||
|
||||
return reasons
|
||||
}
|
||||
const goToAgentEditor = (
|
||||
agent: CustomAgent,
|
||||
section = 'model',
|
||||
highlight?: AgentNotReadyReasonKey,
|
||||
) => {
|
||||
router.push({
|
||||
path: '/platform/agents',
|
||||
query: {
|
||||
edit: agent.id,
|
||||
section,
|
||||
...(highlight ? { highlight } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 显示智能体未就绪的消息(统一处理内置和自定义智能体)
|
||||
const showAgentNotReadyMessage = (agent: CustomAgent, reasons: string[]) => {
|
||||
const showAgentNotReadyMessage = (
|
||||
agent: CustomAgent,
|
||||
reasons: string[],
|
||||
reasonKeys?: AgentNotReadyReasonKey[],
|
||||
) => {
|
||||
const reasonsText = reasons.join('、')
|
||||
const section = resolveAgentNotReadySection(reasonKeys || ['summary_model'])
|
||||
const highlight = resolveAgentNotReadyHighlight(reasonKeys || ['summary_model'])
|
||||
|
||||
const messageContent = h('div', { style: 'display: flex; flex-direction: column; gap: 8px; max-width: 320px;' }, [
|
||||
h('span', { style: 'color: var(--td-text-color-primary); line-height: 1.5;' }, t('input.agentNotReadyDetail', { agentName: agent.name, reasons: reasonsText })),
|
||||
@@ -1998,7 +2032,7 @@ const showAgentNotReadyMessage = (agent: CustomAgent, reasons: string[]) => {
|
||||
href: '#',
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault();
|
||||
router.push(`/platform/agents?edit=${agent.id}`);
|
||||
goToAgentEditor(agent, section, highlight);
|
||||
},
|
||||
style: 'color: var(--td-brand-color); text-decoration: none; font-weight: 500; cursor: pointer; align-self: flex-start;',
|
||||
onMouseenter: (e: Event) => {
|
||||
@@ -2174,7 +2208,8 @@ defineExpose({
|
||||
|
||||
<!-- Agent 选择器下拉菜单 -->
|
||||
<AgentSelector :visible="showAgentModeSelector" :anchorEl="agentModeButtonRef"
|
||||
:currentAgentId="selectedAgentId" :agents="enabledAgents" @close="closeAgentModeSelector"
|
||||
:currentAgentId="selectedAgentId" :agents="enabledAgents" :all-models="allModels"
|
||||
@close="closeAgentModeSelector"
|
||||
@select="handleSelectAgent" />
|
||||
|
||||
<!-- WebSearch 开关按钮 -->
|
||||
@@ -2308,18 +2343,21 @@ defineExpose({
|
||||
</button>
|
||||
</div>
|
||||
<div class="model-selector-content">
|
||||
<div v-for="model in availableModels" :key="model.id" class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }" @click="handleModelChange(model.id || '')">
|
||||
<div class="model-option-main">
|
||||
<span class="model-option-name">{{ modelDisplayName(model) }}</span>
|
||||
<span v-if="model.display_name" class="model-option-raw-name">{{ model.name }}</span>
|
||||
<span v-if="model.source === 'remote'" class="model-badge-remote">{{ $t('input.remote') }}</span>
|
||||
<span v-else-if="model.parameters?.parameter_size" class="model-badge-local">
|
||||
{{ model.parameters.parameter_size }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="model.description" class="model-option-desc">
|
||||
{{ model.description }}
|
||||
<div
|
||||
v-for="model in availableModels"
|
||||
:key="model.id"
|
||||
class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }"
|
||||
@click="handleModelChange(model.id || '')"
|
||||
>
|
||||
<div class="model-option-left">
|
||||
<div class="model-option-icon">
|
||||
<t-icon name="chat" size="14px" />
|
||||
</div>
|
||||
<div class="model-option-name-wrap">
|
||||
<span class="model-option-name">{{ modelDisplayName(model) }}</span>
|
||||
<span v-if="model.display_name" class="model-option-raw-name">{{ model.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="availableModels.length === 0" class="model-option empty">
|
||||
@@ -3105,41 +3143,49 @@ const getImgSrc = (url: string) => {
|
||||
.model-selector-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
z-index: 9999;
|
||||
background: transparent;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.model-selector-dropdown {
|
||||
position: fixed !important;
|
||||
z-index: 9999;
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
z-index: 10000;
|
||||
background: var(--td-bg-color-container);
|
||||
border: .5px solid var(--td-component-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--td-shadow-2, 0 6px 28px rgba(15, 23, 42, 0.08));
|
||||
border: .5px solid var(--td-component-border, #e7e9eb);
|
||||
box-shadow: var(--td-shadow-2);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
transform: none !important;
|
||||
transform-origin: top left;
|
||||
animation: modelSelectorFadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modelSelectorFadeIn {
|
||||
from { opacity: 0; transform: scale(0.98); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.model-selector-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: .5px solid var(--td-component-stroke, #f0f0f0);
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
padding: 8px 10px;
|
||||
border-bottom: .5px solid var(--td-component-stroke);
|
||||
background: var(--td-bg-color-container);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.model-selector-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -3151,14 +3197,14 @@ const getImgSrc = (url: string) => {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
border: .5px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--td-brand-color, #07c05f);
|
||||
color: var(--td-brand-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.12s;
|
||||
|
||||
.add-icon {
|
||||
font-size: 14px;
|
||||
@@ -3167,12 +3213,14 @@ const getImgSrc = (url: string) => {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--td-brand-color-hover, #05a04f);
|
||||
background: var(--td-bg-color-secondarycontainer, #f3f3f3);
|
||||
color: var(--td-brand-color-hover);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
}
|
||||
}
|
||||
|
||||
.model-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
@@ -3183,21 +3231,13 @@ const getImgSrc = (url: string) => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--td-bg-color-container-hover, #f6f8f7);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: var(--td-brand-color-light, #eefdf5);
|
||||
|
||||
.model-option-name {
|
||||
color: var(--td-success-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
}
|
||||
|
||||
&.empty {
|
||||
color: var(--td-text-color-disabled, #9aa0a6);
|
||||
color: var(--td-text-color-placeholder);
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
padding: 20px 8px;
|
||||
@@ -3208,62 +3248,47 @@ const getImgSrc = (url: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
.model-option-main {
|
||||
.model-option-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 1px;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.model-option-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.model-option-name-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.model-option-name {
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-primary, #222);
|
||||
flex-shrink: 0;
|
||||
color: var(--td-text-color-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.model-option-raw-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-placeholder, #b0b6bd);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-option-desc {
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-secondary, #8b9196);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.model-badge-remote,
|
||||
.model-badge-local {
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.model-badge-remote {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--td-success-color);
|
||||
}
|
||||
|
||||
.model-badge-local {
|
||||
background: rgba(139, 145, 150, 0.1);
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
/* Agent 模式选择下拉菜单 */
|
||||
.agent-mode-selector-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -1120,6 +1120,10 @@ export default {
|
||||
current: 'Current',
|
||||
goToSettings: 'Settings',
|
||||
sharedLabel: 'Shared',
|
||||
notReadyHint: 'Not ready. Configure: {items}',
|
||||
notReadyStatus: 'Setup needed',
|
||||
notReadyMissing: 'Missing: {items}',
|
||||
configureAction: 'Configure',
|
||||
},
|
||||
// Built-in agent information
|
||||
builtinInfo: {
|
||||
@@ -2118,13 +2122,13 @@ export default {
|
||||
agentModeDesc: 'Multi-step thinking, deep analysis',
|
||||
agentNotReadyTooltip: 'Agent is not ready. Please finish configuration first.',
|
||||
agentMissingAllowedTools: 'Allowed tools',
|
||||
agentMissingSummaryModel: 'Chat Model (Summary Model)',
|
||||
agentMissingRerankModel: 'Rerank Model',
|
||||
agentMissingSummaryModel: 'Chat model',
|
||||
agentMissingRerankModel: 'Rerank model',
|
||||
goToSettings: 'Go to settings →',
|
||||
customAgentNotReadyTooltip: 'Agent is not ready. Please finish configuration first.',
|
||||
customAgentNotReadyDetail: 'Agent is not ready. Please configure the following: {reasons}',
|
||||
customAgentMissingSummaryModel: 'Chat Model (Summary Model)',
|
||||
customAgentMissingRerankModel: 'Rerank Model',
|
||||
customAgentMissingSummaryModel: 'Chat model',
|
||||
customAgentMissingRerankModel: 'Rerank model',
|
||||
goToAgentEditor: 'Go to configure →',
|
||||
agentNotReadyDetail: 'Agent "{agentName}" is not ready. Please configure: {reasons}',
|
||||
builtinAgentNotReadyDetail: 'Built-in agent "{agentName}" is not ready. Please configure: {reasons}',
|
||||
@@ -3972,7 +3976,7 @@ export default {
|
||||
notReady: 'Not Ready',
|
||||
hint: 'Once configuration is complete, the status will change to "Ready". You can then enable Agent mode in the chat.',
|
||||
missingThinkingModel: 'Thinking model',
|
||||
missingSummaryModel: 'Chat Model (Summary Model)',
|
||||
missingSummaryModel: 'Chat model',
|
||||
missingRerankModel: 'Rerank model',
|
||||
missingAllowedTools: 'Allowed tools',
|
||||
pleaseConfigure: 'Please configure {items}',
|
||||
|
||||
@@ -2116,6 +2116,10 @@ export default {
|
||||
current: "현재 선택",
|
||||
goToSettings: "설정",
|
||||
sharedLabel: "공유",
|
||||
notReadyHint: "아직 구성 필요: {items}",
|
||||
notReadyStatus: "구성 필요",
|
||||
notReadyMissing: "누락: {items}",
|
||||
configureAction: "구성",
|
||||
},
|
||||
// 내장 에이전트 정보
|
||||
builtinInfo: {
|
||||
@@ -3890,13 +3894,13 @@ export default {
|
||||
agentModeDesc: "ReAct 추론 프레임워크, 다단계 사고",
|
||||
agentNotReadyTooltip: "Agent가 준비되지 않았습니다. 먼저 설정에서 구성을 완료해주세요",
|
||||
agentMissingAllowedTools: "허용된 도구",
|
||||
agentMissingSummaryModel: "대화 모델 (Summary Model)",
|
||||
agentMissingRerankModel: "재정렬 모델 (Rerank Model)",
|
||||
agentMissingSummaryModel: "대화 모델",
|
||||
agentMissingRerankModel: "재정렬 모델",
|
||||
goToSettings: "설정으로 이동 →",
|
||||
customAgentNotReadyTooltip: "에이전트가 준비되지 않았습니다. 먼저 구성을 완료하세요.",
|
||||
customAgentNotReadyDetail: "에이전트가 준비되지 않았으며 다음을 구성해야 합니다: {reasons}",
|
||||
customAgentMissingSummaryModel: "대화 모델(요약 모델)",
|
||||
customAgentMissingRerankModel: "리랭크 모델 (Rerank Model)",
|
||||
customAgentMissingSummaryModel: "대화 모델",
|
||||
customAgentMissingRerankModel: "재정렬 모델",
|
||||
goToAgentEditor: "구성으로 이동 →",
|
||||
agentNotReadyDetail:
|
||||
'Agent "{agentName}"가 준비되지 않았습니다. 다음 항목을 설정해야 합니다: {reasons}',
|
||||
|
||||
@@ -1033,7 +1033,11 @@ export default {
|
||||
addNew: 'Add New Agent',
|
||||
current: 'Current',
|
||||
goToSettings: 'Settings',
|
||||
sharedLabel: 'Shared'
|
||||
sharedLabel: 'Shared',
|
||||
notReadyHint: 'Still needs: {items}',
|
||||
notReadyStatus: 'Setup needed',
|
||||
notReadyMissing: 'Missing: {items}',
|
||||
configureAction: 'Configure',
|
||||
},
|
||||
builtinInfo: {
|
||||
quickAnswer: {
|
||||
@@ -4239,8 +4243,8 @@ export default {
|
||||
agentNotReadyTooltip: 'Agent не готов. Пожалуйста, завершите настройку.',
|
||||
agentNotReadyDetail: 'Agent не готов. Пожалуйста, настройте следующее: {reasons}',
|
||||
agentMissingAllowedTools: 'Разрешённые инструменты',
|
||||
agentMissingSummaryModel: 'Модель беседы (Summary Model)',
|
||||
agentMissingRerankModel: 'Модель переранжирования (Rerank Model)',
|
||||
agentMissingSummaryModel: 'Модель беседы',
|
||||
agentMissingRerankModel: 'Модель переранжирования',
|
||||
goToSettings: 'Перейти к настройкам →',
|
||||
webSearch: {
|
||||
toggleOn: 'Включить веб-поиск',
|
||||
@@ -4277,8 +4281,8 @@ export default {
|
||||
},
|
||||
customAgentNotReadyTooltip: 'Agent is not ready. Please finish configuration first.',
|
||||
customAgentNotReadyDetail: 'Agent is not ready. Please configure the following: {reasons}',
|
||||
customAgentMissingSummaryModel: 'Chat Model (Summary Model)',
|
||||
customAgentMissingRerankModel: 'Rerank Model',
|
||||
customAgentMissingSummaryModel: 'Модель беседы',
|
||||
customAgentMissingRerankModel: 'Модель переранжирования',
|
||||
goToAgentEditor: 'Go to configure →',
|
||||
builtinAgentNotReadyDetail: 'Built-in agent "{agentName}" is not ready. Please configure: {reasons}',
|
||||
builtinAgentSettingName: 'Intelligent Reasoning',
|
||||
|
||||
@@ -2112,6 +2112,10 @@ export default {
|
||||
current: "当前",
|
||||
goToSettings: "设置",
|
||||
sharedLabel: "共享",
|
||||
notReadyHint: "尚未就绪,还需配置:{items}",
|
||||
notReadyStatus: "待配置",
|
||||
notReadyMissing: "缺少 {items}",
|
||||
configureAction: "去配置",
|
||||
},
|
||||
// 内置智能体信息
|
||||
builtinInfo: {
|
||||
@@ -3899,13 +3903,13 @@ export default {
|
||||
agentModeDesc: "多步思考,深度分析",
|
||||
agentNotReadyTooltip: "Agent 未就绪,请先在设置中完成配置",
|
||||
agentMissingAllowedTools: "允许的工具",
|
||||
agentMissingSummaryModel: "对话模型(Summary Model)",
|
||||
agentMissingRerankModel: "重排模型(Rerank Model)",
|
||||
agentMissingSummaryModel: "对话模型",
|
||||
agentMissingRerankModel: "重排模型",
|
||||
goToSettings: "前往设置 →",
|
||||
customAgentNotReadyTooltip: "智能体未就绪,请先完成配置",
|
||||
customAgentNotReadyDetail: "智能体未就绪,需要配置以下内容:{reasons}",
|
||||
customAgentMissingSummaryModel: "对话模型(Summary Model)",
|
||||
customAgentMissingRerankModel: "重排模型(Rerank Model)",
|
||||
customAgentMissingSummaryModel: "对话模型",
|
||||
customAgentMissingRerankModel: "重排模型",
|
||||
goToAgentEditor: "前往配置 →",
|
||||
agentNotReadyDetail: "智能体「{agentName}」未就绪,需要配置以下内容:{reasons}",
|
||||
builtinAgentNotReadyDetail: "内置智能体「{agentName}」未就绪,需要配置以下内容:{reasons}",
|
||||
@@ -3970,8 +3974,8 @@ export default {
|
||||
notReady: "未就绪",
|
||||
hint: '配置完成后,Agent 状态将自动变为"可用",此时可在对话界面开启 Agent 模式',
|
||||
missingThinkingModel: "思考模型",
|
||||
missingSummaryModel: "对话模型(Summary Model)",
|
||||
missingRerankModel: "Rerank 模型",
|
||||
missingSummaryModel: "对话模型",
|
||||
missingRerankModel: "重排模型",
|
||||
missingAllowedTools: "允许的工具",
|
||||
pleaseConfigure: "请配置{items}",
|
||||
goToConfig: "前往配置对话模型",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
agentHasConfiguredChatModel,
|
||||
agentRequiresRerankModel,
|
||||
getAgentNotReadyReasonKeys,
|
||||
resolveAgentNotReadySection,
|
||||
resolveAgentNotReadyHighlight,
|
||||
} from './agent-readiness.ts'
|
||||
|
||||
test('does not treat an unrelated built-in chat model as agent configuration', () => {
|
||||
assert.equal(agentHasConfiguredChatModel(
|
||||
{},
|
||||
[{ id: 'builtin-chat', type: 'KnowledgeQA' }],
|
||||
), false)
|
||||
})
|
||||
|
||||
test('accepts the chat model explicitly configured on the agent', () => {
|
||||
assert.equal(agentHasConfiguredChatModel(
|
||||
{ model_id: 'builtin-chat' },
|
||||
[{ id: 'builtin-chat', type: 'KnowledgeQA' }],
|
||||
), true)
|
||||
})
|
||||
|
||||
test('rejects a configured chat model that no longer exists', () => {
|
||||
assert.equal(agentHasConfiguredChatModel(
|
||||
{ model_id: 'deleted-chat' },
|
||||
[{ id: 'builtin-chat', type: 'KnowledgeQA' }],
|
||||
), false)
|
||||
})
|
||||
|
||||
test('requires rerank when knowledge_search has a knowledge-base scope', () => {
|
||||
assert.equal(agentRequiresRerankModel({
|
||||
kb_selection_mode: 'all',
|
||||
allowed_tools: ['knowledge_search'],
|
||||
}), true)
|
||||
})
|
||||
|
||||
test('does not require rerank when knowledge bases are disabled', () => {
|
||||
assert.equal(agentRequiresRerankModel({
|
||||
kb_selection_mode: 'none',
|
||||
allowed_tools: ['knowledge_search'],
|
||||
}), false)
|
||||
})
|
||||
|
||||
test('does not require rerank for wiki-only tools', () => {
|
||||
assert.equal(agentRequiresRerankModel({
|
||||
kb_selection_mode: 'all',
|
||||
allowed_tools: ['wiki_search', 'wiki_read_page'],
|
||||
}), false)
|
||||
})
|
||||
|
||||
test('matches backend default-tool behavior', () => {
|
||||
assert.equal(agentRequiresRerankModel({
|
||||
kb_selection_mode: 'all',
|
||||
allowed_tools: [],
|
||||
}), true)
|
||||
assert.equal(agentRequiresRerankModel({
|
||||
kb_selection_mode: 'none',
|
||||
allowed_tools: [],
|
||||
}), false)
|
||||
})
|
||||
|
||||
test('resolveAgentNotReadySection opens model config for model issues', () => {
|
||||
assert.equal(resolveAgentNotReadySection(['summary_model']), 'model')
|
||||
assert.equal(resolveAgentNotReadySection(['rerank_model']), 'model')
|
||||
assert.equal(resolveAgentNotReadySection(['summary_model', 'allowed_tools']), 'model')
|
||||
})
|
||||
|
||||
test('resolveAgentNotReadySection opens tools config when only tools are missing', () => {
|
||||
assert.equal(resolveAgentNotReadySection(['allowed_tools']), 'tools')
|
||||
})
|
||||
|
||||
test('resolveAgentNotReadyHighlight returns the first missing item', () => {
|
||||
assert.equal(resolveAgentNotReadyHighlight(['summary_model', 'rerank_model']), 'summary_model')
|
||||
assert.equal(resolveAgentNotReadyHighlight(['allowed_tools']), 'allowed_tools')
|
||||
assert.equal(resolveAgentNotReadyHighlight([]), undefined)
|
||||
})
|
||||
|
||||
test('getAgentNotReadyReasonKeys flags missing chat model', () => {
|
||||
assert.deepEqual(getAgentNotReadyReasonKeys(
|
||||
{},
|
||||
[{ id: 'chat-1', type: 'KnowledgeQA' }],
|
||||
{ isAgentMode: false, isSharedAgent: false },
|
||||
), ['summary_model'])
|
||||
})
|
||||
|
||||
test('getAgentNotReadyReasonKeys requires rerank only in agent mode with KB search', () => {
|
||||
assert.deepEqual(getAgentNotReadyReasonKeys(
|
||||
{ kb_selection_mode: 'all', allowed_tools: ['knowledge_search'] },
|
||||
[{ id: 'chat-1', type: 'KnowledgeQA' }, { id: 'rerank-1', type: 'Rerank' }],
|
||||
{ isAgentMode: true, isSharedAgent: false },
|
||||
), ['summary_model', 'rerank_model'])
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { CustomAgentConfig } from '@/api/agent'
|
||||
import type { ModelConfig } from '@/api/model'
|
||||
|
||||
export type AgentNotReadyReasonKey = 'summary_model' | 'rerank_model' | 'allowed_tools'
|
||||
|
||||
/**
|
||||
* An agent is chat-ready only when it explicitly references a usable chat
|
||||
* model. Merely having some built-in/default model in the tenant must not hide
|
||||
* incomplete agent configuration.
|
||||
*
|
||||
* For shared agents the model belongs to the source tenant, so the caller can
|
||||
* require a non-empty ID but must leave existence validation to the backend.
|
||||
*/
|
||||
export function agentHasConfiguredChatModel(
|
||||
config: Pick<CustomAgentConfig, 'model_id'> | undefined,
|
||||
models: Pick<ModelConfig, 'id' | 'type'>[],
|
||||
sourceModelIsRemote = false,
|
||||
): boolean {
|
||||
const modelID = config?.model_id?.trim()
|
||||
if (!modelID) return false
|
||||
if (sourceModelIsRemote) return true
|
||||
return models.some(model => model.type === 'KnowledgeQA' && model.id === modelID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep this aligned with backend agentRequiresRerankModel.
|
||||
*
|
||||
* Rerank is needed only when knowledge_search can actually run. Explicitly
|
||||
* disabling the knowledge-base scope makes KB tools ineffective, even if an
|
||||
* older agent configuration still contains knowledge_search in allowed_tools.
|
||||
*/
|
||||
export function agentRequiresRerankModel(
|
||||
config: Pick<CustomAgentConfig, 'kb_selection_mode' | 'allowed_tools'> | undefined,
|
||||
): boolean {
|
||||
if (!config || config.kb_selection_mode === 'none') return false
|
||||
|
||||
const allowedTools = config.allowed_tools || []
|
||||
// The backend falls back to DefaultAllowedTools when the list is empty,
|
||||
// and that default includes knowledge_search.
|
||||
if (allowedTools.length === 0) return true
|
||||
|
||||
return allowedTools.includes('knowledge_search')
|
||||
}
|
||||
|
||||
export function getAgentNotReadyReasonKeys(
|
||||
config: Pick<
|
||||
CustomAgentConfig,
|
||||
'model_id' | 'rerank_model_id' | 'kb_selection_mode' | 'allowed_tools' | 'agent_mode'
|
||||
> | undefined,
|
||||
models: Pick<ModelConfig, 'id' | 'type'>[],
|
||||
options: { isAgentMode: boolean; isSharedAgent: boolean },
|
||||
): AgentNotReadyReasonKey[] {
|
||||
const reasons: AgentNotReadyReasonKey[] = []
|
||||
|
||||
if (!agentHasConfiguredChatModel(config, models, options.isSharedAgent)) {
|
||||
reasons.push('summary_model')
|
||||
}
|
||||
|
||||
if (options.isAgentMode && agentRequiresRerankModel(config)) {
|
||||
const rerankModelID = config?.rerank_model_id?.trim()
|
||||
const rerankExists = !!rerankModelID && (
|
||||
options.isSharedAgent
|
||||
|| models.some(model => model.type === 'Rerank' && model.id === rerankModelID)
|
||||
)
|
||||
if (!rerankExists) {
|
||||
reasons.push('rerank_model')
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isAgentMode && (!config?.allowed_tools || config.allowed_tools.length === 0)) {
|
||||
reasons.push('allowed_tools')
|
||||
}
|
||||
|
||||
return reasons
|
||||
}
|
||||
|
||||
/** Map missing-config reasons to the agent editor section that fixes them. */
|
||||
export function resolveAgentNotReadySection(reasons: AgentNotReadyReasonKey[]): string {
|
||||
if (reasons.includes('summary_model') || reasons.includes('rerank_model')) {
|
||||
return 'model'
|
||||
}
|
||||
if (reasons.includes('allowed_tools')) {
|
||||
return 'tools'
|
||||
}
|
||||
return 'model'
|
||||
}
|
||||
|
||||
/** First missing item — used to highlight the corresponding editor field after navigation. */
|
||||
export function resolveAgentNotReadyHighlight(
|
||||
reasons: AgentNotReadyReasonKey[],
|
||||
): AgentNotReadyReasonKey | undefined {
|
||||
return reasons[0]
|
||||
}
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<div class="settings-content">
|
||||
<div class="content-wrapper" :class="{ 'content-wrapper--prompts': currentSection === 'prompts' }">
|
||||
<div ref="contentWrapperRef" class="content-wrapper" :class="{ 'content-wrapper--prompts': currentSection === 'prompts' }">
|
||||
<!-- 基础设置 -->
|
||||
<div v-show="currentSection === 'basic'" class="section">
|
||||
<div class="section-header">
|
||||
@@ -565,7 +565,12 @@
|
||||
|
||||
<div class="settings-group">
|
||||
<!-- 模型选择 -->
|
||||
<div class="setting-row" data-guide="agent-create-model">
|
||||
<div
|
||||
class="setting-row"
|
||||
data-guide="agent-create-model"
|
||||
data-agent-field="summary_model"
|
||||
:class="{ 'setting-row--field-highlight': highlightedField === 'summary_model' }"
|
||||
>
|
||||
<div class="setting-info">
|
||||
<label>{{ $t('agent.editor.model') }} <span class="required">*</span></label>
|
||||
<p class="desc">{{ $t('agentEditor.desc.model') }}</p>
|
||||
@@ -616,7 +621,12 @@
|
||||
</div>
|
||||
|
||||
<!-- ReRank 模型(启用知识库时显示) -->
|
||||
<div v-if="hasKnowledgeBase" class="setting-row">
|
||||
<div
|
||||
v-if="hasKnowledgeBase"
|
||||
class="setting-row"
|
||||
data-agent-field="rerank_model"
|
||||
:class="{ 'setting-row--field-highlight': highlightedField === 'rerank_model' }"
|
||||
>
|
||||
<div class="setting-info">
|
||||
<label>
|
||||
{{ $t('agent.editor.rerankModel') }}
|
||||
@@ -849,7 +859,11 @@
|
||||
|
||||
<div class="settings-group">
|
||||
<!-- 允许的工具(按组渲染,统一网格) -->
|
||||
<div class="setting-row setting-row-vertical">
|
||||
<div
|
||||
class="setting-row setting-row-vertical"
|
||||
data-agent-field="allowed_tools"
|
||||
:class="{ 'setting-row--field-highlight': highlightedField === 'allowed_tools' }"
|
||||
>
|
||||
<div class="setting-info">
|
||||
<label>{{ $t('agent.editor.allowedTools') }}</label>
|
||||
<p class="desc">{{ $t('agentEditor.desc.selectTools') }}</p>
|
||||
@@ -1380,6 +1394,7 @@ import {
|
||||
type KBCapabilities,
|
||||
} from '@/api/agent';
|
||||
import { type ModelConfig } from '@/api/model';
|
||||
import { type AgentNotReadyReasonKey } from '@/utils/agent-readiness';
|
||||
import { type MCPService } from '@/api/mcp-service';
|
||||
import { type SkillInfo } from '@/api/skill';
|
||||
import { type WebSearchProviderEntity } from '@/api/web-search-provider';
|
||||
@@ -1416,6 +1431,7 @@ const props = defineProps<{
|
||||
mode: 'create' | 'edit';
|
||||
agent?: CustomAgent | null;
|
||||
initialSection?: string;
|
||||
initialHighlightField?: string;
|
||||
// readOnly hides the save button so a Viewer who clicks an agent
|
||||
// card to inspect its config doesn't see a "确定" that 403s on the
|
||||
// backend update endpoint. Field-level disable is intentionally NOT
|
||||
@@ -1459,6 +1475,68 @@ const copyAgentId = async () => {
|
||||
};
|
||||
|
||||
const currentSection = ref(props.initialSection || 'basic');
|
||||
const contentWrapperRef = ref<HTMLElement | null>(null);
|
||||
const highlightedField = ref<AgentNotReadyReasonKey | null>(null);
|
||||
let highlightClearTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const VALID_HIGHLIGHT_FIELDS: AgentNotReadyReasonKey[] = ['summary_model', 'rerank_model', 'allowed_tools'];
|
||||
|
||||
const sectionForHighlightField = (field: AgentNotReadyReasonKey): string => {
|
||||
if (field === 'allowed_tools') return 'tools';
|
||||
return 'model';
|
||||
};
|
||||
|
||||
const FIELD_FLASH_DURATION_MS = 2400;
|
||||
|
||||
const clearFieldHighlight = () => {
|
||||
if (highlightClearTimer) {
|
||||
clearTimeout(highlightClearTimer);
|
||||
highlightClearTimer = null;
|
||||
}
|
||||
highlightedField.value = null;
|
||||
};
|
||||
|
||||
const applyInitialFieldHighlight = async (field: string) => {
|
||||
if (!VALID_HIGHLIGHT_FIELDS.includes(field as AgentNotReadyReasonKey)) return;
|
||||
|
||||
const targetField = field as AgentNotReadyReasonKey;
|
||||
currentSection.value = sectionForHighlightField(targetField);
|
||||
|
||||
await nextTick();
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
|
||||
clearFieldHighlight();
|
||||
highlightedField.value = null;
|
||||
|
||||
const wrapper = contentWrapperRef.value;
|
||||
const row = wrapper?.querySelector(`[data-agent-field="${targetField}"]`) as HTMLElement | null;
|
||||
if (row && wrapper) {
|
||||
const rowTop = row.offsetTop;
|
||||
const scrollTarget = rowTop - wrapper.clientHeight / 2 + row.clientHeight / 2;
|
||||
wrapper.scrollTo({ top: Math.max(0, scrollTarget), behavior: 'auto' });
|
||||
|
||||
await nextTick();
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
highlightedField.value = targetField;
|
||||
|
||||
if (row) {
|
||||
const focusTarget = row.querySelector('.t-input, .t-select-input, input, .t-checkbox') as HTMLElement | null;
|
||||
focusTarget?.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
highlightClearTimer = setTimeout(() => {
|
||||
if (highlightedField.value === targetField) {
|
||||
highlightedField.value = null;
|
||||
}
|
||||
highlightClearTimer = null;
|
||||
}, FIELD_FLASH_DURATION_MS);
|
||||
};
|
||||
|
||||
const onAgentEditorFocusSection = (event: Event) => {
|
||||
const section = (event as CustomEvent<{ section?: string }>).detail?.section
|
||||
@@ -2568,7 +2646,12 @@ watch(() => props.visible, async (val) => {
|
||||
}
|
||||
applyDefaultChatModelIfEmpty()
|
||||
}
|
||||
|
||||
if (props.initialHighlightField) {
|
||||
await applyInitialFieldHighlight(props.initialHighlightField);
|
||||
}
|
||||
} else {
|
||||
clearFieldHighlight();
|
||||
agentIMChannelCount.value = 0;
|
||||
agentEmbedChannelCount.value = 0;
|
||||
}
|
||||
@@ -3779,9 +3862,8 @@ const handleSave = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// ReRank 模型(可选)
|
||||
// 运行时若 rerank_model_id 为空会自动跳过 rerank,无需在保存时强制要求。
|
||||
// 仅当用户已选择 rerank 模型时,才校验相关参数。
|
||||
// ReRank 模型按运行范围按需使用:知识库范围为 none,或未启用
|
||||
// knowledge_search 时不需要;其余情况由对话入口在使用前给出明确提示。
|
||||
|
||||
// 过滤空推荐问题
|
||||
if (formData.value.config.suggested_prompts) {
|
||||
@@ -4200,6 +4282,24 @@ const handleSave = async () => {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.setting-row--field-highlight {
|
||||
border-radius: 6px;
|
||||
animation: agent-field-flash 0.8s ease-in-out 3;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-field-flash {
|
||||
0%,
|
||||
100% {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: var(--td-warning-color-light, #fff7e8);
|
||||
box-shadow: inset 0 0 0 1px rgba(237, 123, 47, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
|
||||
@@ -807,6 +807,7 @@
|
||||
<!-- 智能体编辑器弹窗 -->
|
||||
<AgentEditorModal :visible="editorVisible" :mode="editorMode" :agent="editingAgent"
|
||||
:initialSection="editorInitialSection"
|
||||
:initialHighlightField="editorInitialHighlightField"
|
||||
:readOnly="editorMode === 'edit' && editingAgent != null && !canManageAgent(editingAgent as AgentWithUI)"
|
||||
@update:visible="editorVisible = $event" @success="handleEditorSuccess" />
|
||||
|
||||
@@ -1083,6 +1084,7 @@ const editorVisible = ref(false)
|
||||
const editorMode = ref<'create' | 'edit'>('create')
|
||||
const editingAgent = ref<CustomAgent | null>(null)
|
||||
const editorInitialSection = ref<string>('basic')
|
||||
const editorInitialHighlightField = ref<string>('')
|
||||
/** 当前打开三点菜单的卡片 agent.id(用于受控弹出层,避免 computed 项无持久引用导致菜单不响应) */
|
||||
const openMoreAgentId = ref<string | null>(null)
|
||||
|
||||
@@ -1142,11 +1144,12 @@ const checkAndOpenEditModal = () => {
|
||||
editingAgent.value = agent
|
||||
editorMode.value = 'edit'
|
||||
editorInitialSection.value = section || 'basic'
|
||||
editorInitialHighlightField.value = (route.query.highlight as string) || ''
|
||||
editorVisible.value = true
|
||||
}
|
||||
// Drop the transient edit/section params but preserve other filter
|
||||
// state (scope / creator / q) so refreshing doesn't reset the view.
|
||||
const { edit: _e, section: _s, ...rest } = route.query
|
||||
const { edit: _e, section: _s, highlight: _h, ...rest } = route.query
|
||||
router.replace({ path: route.path, query: rest })
|
||||
}
|
||||
}
|
||||
@@ -1294,6 +1297,8 @@ const handleEdit = (agent: AgentWithUI) => {
|
||||
openMoreAgentId.value = null
|
||||
editingAgent.value = agent
|
||||
editorMode.value = 'edit'
|
||||
editorInitialSection.value = 'basic'
|
||||
editorInitialHighlightField.value = ''
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1527,6 +1532,8 @@ const formatDate = (dateStr: string) => {
|
||||
const openCreateModal = () => {
|
||||
editingAgent.value = null
|
||||
editorMode.value = 'create'
|
||||
editorInitialSection.value = 'basic'
|
||||
editorInitialHighlightField.value = ''
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/agent/tools"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAgentRequiresRerankModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
agent *types.CustomAgent
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "knowledge search with all knowledge bases",
|
||||
agent: &types.CustomAgent{Config: types.CustomAgentConfig{
|
||||
KBSelectionMode: "all",
|
||||
AllowedTools: []string{tools.ToolKnowledgeSearch},
|
||||
}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "knowledge search with selected knowledge bases",
|
||||
agent: &types.CustomAgent{Config: types.CustomAgentConfig{
|
||||
KBSelectionMode: "selected",
|
||||
AllowedTools: []string{tools.ToolKnowledgeSearch},
|
||||
}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "knowledge search with knowledge bases disabled",
|
||||
agent: &types.CustomAgent{Config: types.CustomAgentConfig{
|
||||
KBSelectionMode: "none",
|
||||
AllowedTools: []string{tools.ToolKnowledgeSearch},
|
||||
}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "default tools with knowledge bases disabled",
|
||||
agent: &types.CustomAgent{Config: types.CustomAgentConfig{
|
||||
KBSelectionMode: "none",
|
||||
}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wiki tools do not use reranker",
|
||||
agent: &types.CustomAgent{Config: types.CustomAgentConfig{
|
||||
KBSelectionMode: "all",
|
||||
AllowedTools: []string{"wiki_search", "wiki_read_page"},
|
||||
}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil agent",
|
||||
agent: nil,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, agentRequiresRerankModel(tt.agent))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,13 @@ var (
|
||||
ErrAgentNotConfigured = errors.New("agent is not fully configured (missing required chat model, or rerank model when the knowledge_search tool is enabled)")
|
||||
)
|
||||
|
||||
// agentRequiresRerankModel returns true when the agent's configured tools
|
||||
// will actually invoke the reranker at runtime. This mirrors the runtime
|
||||
// check in session_agent_qa.go: only `knowledge_search` uses the reranker.
|
||||
// agentRequiresRerankModel returns true when the agent's configured scope and
|
||||
// tools will actually invoke the reranker at runtime. An agent whose knowledge
|
||||
// base scope is explicitly disabled cannot run knowledge_search, so it does not
|
||||
// need a rerank model even if that tool remains in AllowedTools.
|
||||
//
|
||||
// This mirrors the runtime check in session_agent_qa.go: only
|
||||
// `knowledge_search` with an enabled knowledge-base scope uses the reranker.
|
||||
// Wiki-first agents (wiki_search / wiki_read_page / …) never call it and
|
||||
// therefore don't need a rerank model configured, even when knowledge bases
|
||||
// are attached.
|
||||
@@ -37,6 +41,9 @@ func agentRequiresRerankModel(agent *types.CustomAgent) bool {
|
||||
if agent == nil {
|
||||
return false
|
||||
}
|
||||
if agent.Config.KBSelectionMode == "none" {
|
||||
return false
|
||||
}
|
||||
allowed := agent.Config.AllowedTools
|
||||
if len(allowed) == 0 {
|
||||
allowed = tools.DefaultAllowedTools()
|
||||
|
||||
@@ -89,17 +89,11 @@ func (s *sessionService) AgentQA(
|
||||
return fmt.Errorf("failed to get chat model: %w", err)
|
||||
}
|
||||
|
||||
// Get rerank model from custom agent config (only required when knowledge_search is allowed)
|
||||
// Get rerank model from custom agent config only when knowledge_search can
|
||||
// actually run. A disabled KB scope makes all KB tools ineffective, so it
|
||||
// must not force users to configure an otherwise-unused rerank model.
|
||||
var rerankModel rerank.Reranker
|
||||
hasKnowledgeSearchTool := false
|
||||
for _, tool := range agentConfig.AllowedTools {
|
||||
if tool == tools.ToolKnowledgeSearch {
|
||||
hasKnowledgeSearchTool = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasKnowledgeSearchTool {
|
||||
if agentRequiresRerankModel(req.CustomAgent) {
|
||||
// Rerank model is resolved purely from the agent config now.
|
||||
// We used to fall back to ConversationConfig.RerankModelID at
|
||||
// the tenant level, but that path encouraged "leave rerank
|
||||
@@ -121,7 +115,7 @@ func (s *sessionService) AgentQA(
|
||||
return fmt.Errorf("failed to get rerank model: %w", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof(ctx, "knowledge_search tool not enabled, skipping rerank model initialization")
|
||||
logger.Infof(ctx, "knowledge_search is unavailable for the effective agent scope, skipping rerank model initialization")
|
||||
}
|
||||
|
||||
// Load multi-turn history directly from DB (the single source of truth).
|
||||
|
||||
@@ -48,15 +48,16 @@ func (m *captureChatModel) GetModelName() string { return "capture" }
|
||||
func (m *captureChatModel) GetModelID() string { return "capture" }
|
||||
|
||||
type stubModelService struct {
|
||||
chatModel chat.Chat
|
||||
chatModel chat.Chat
|
||||
modelsByID map[string]*types.Model
|
||||
}
|
||||
|
||||
func (s *stubModelService) CreateModel(context.Context, *types.Model) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubModelService) GetModelByID(context.Context, string) (*types.Model, error) {
|
||||
return nil, nil
|
||||
func (s *stubModelService) GetModelByID(_ context.Context, id string) (*types.Model, error) {
|
||||
return s.modelsByID[id], nil
|
||||
}
|
||||
|
||||
func (s *stubModelService) ListModels(context.Context) ([]*types.Model, error) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveChatModelIDRequiresConfiguredAgentModel(t *testing.T) {
|
||||
svc := &sessionService{
|
||||
modelService: &stubModelService{
|
||||
modelsByID: map[string]*types.Model{
|
||||
"builtin-chat": {
|
||||
ID: "builtin-chat",
|
||||
Type: types.ModelTypeKnowledgeQA,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req := &types.QARequest{
|
||||
Session: &types.Session{},
|
||||
CustomAgent: &types.CustomAgent{
|
||||
ID: "agent-1",
|
||||
},
|
||||
// Even a valid request-level model must not hide incomplete agent config.
|
||||
SummaryModelID: "builtin-chat",
|
||||
}
|
||||
|
||||
modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, modelID)
|
||||
assert.Contains(t, err.Error(), "model_id")
|
||||
}
|
||||
|
||||
func TestResolveChatModelIDRejectsUnavailableConfiguredAgentModel(t *testing.T) {
|
||||
svc := &sessionService{
|
||||
modelService: &stubModelService{modelsByID: map[string]*types.Model{}},
|
||||
}
|
||||
req := &types.QARequest{
|
||||
Session: &types.Session{},
|
||||
CustomAgent: &types.CustomAgent{
|
||||
ID: "agent-1",
|
||||
Config: types.CustomAgentConfig{
|
||||
ModelID: "deleted-model",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, modelID)
|
||||
assert.Contains(t, err.Error(), "unavailable")
|
||||
}
|
||||
|
||||
func TestResolveChatModelIDUsesValidConfiguredAgentModel(t *testing.T) {
|
||||
svc := &sessionService{
|
||||
modelService: &stubModelService{
|
||||
modelsByID: map[string]*types.Model{
|
||||
"agent-chat": {
|
||||
ID: "agent-chat",
|
||||
Type: types.ModelTypeKnowledgeQA,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req := &types.QARequest{
|
||||
Session: &types.Session{},
|
||||
CustomAgent: &types.CustomAgent{
|
||||
ID: "agent-1",
|
||||
Config: types.CustomAgentConfig{
|
||||
ModelID: "agent-chat",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "agent-chat", modelID)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
@@ -48,10 +49,13 @@ func (s *sessionService) resolveKnowledgeBases(
|
||||
}
|
||||
|
||||
// resolveChatModelID resolves the effective chat model ID for a QA request.
|
||||
// Priority:
|
||||
// 1. Request's SummaryModelID (explicit override, validated)
|
||||
// 2. Custom agent's ModelID
|
||||
// 3. KB / session / system default (via selectChatModelID)
|
||||
//
|
||||
// When an agent is selected, its model configuration must be complete and
|
||||
// valid. A request-level override may choose another valid model for this
|
||||
// request, but it must not make an unconfigured or stale agent appear usable.
|
||||
//
|
||||
// Without an agent, the legacy KB / session / system fallback remains
|
||||
// available for non-agent callers.
|
||||
func (s *sessionService) resolveChatModelID(
|
||||
ctx context.Context,
|
||||
req *types.QARequest,
|
||||
@@ -62,6 +66,17 @@ func (s *sessionService) resolveChatModelID(
|
||||
customAgent := req.CustomAgent
|
||||
session := req.Session
|
||||
|
||||
if customAgent != nil {
|
||||
configuredModelID := customAgent.Config.ModelID
|
||||
if configuredModelID == "" {
|
||||
return "", fmt.Errorf("chat model is not configured: please set model_id on agent %s", customAgent.ID)
|
||||
}
|
||||
model, err := s.modelService.GetModelByID(ctx, configuredModelID)
|
||||
if err != nil || model == nil || model.Type != types.ModelTypeKnowledgeQA {
|
||||
return "", fmt.Errorf("configured chat model %s is unavailable for agent %s", configuredModelID, customAgent.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if summaryModelID != "" {
|
||||
if model, err := s.modelService.GetModelByID(ctx, summaryModelID); err == nil && model != nil {
|
||||
logger.Infof(ctx, "Using request's summary model override: %s", summaryModelID)
|
||||
|
||||
Reference in New Issue
Block a user