fix(agent): address readiness review findings
Tighten shared-agent readiness checks, surface not-ready toasts in the selector, validate chat-model overrides in resolveChatModelID, and improve configure deep-links for shared agents.
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
<span class="agent-option-name">{{ agent.name }}</span>
|
||||
<div v-if="getAgentNotReadyLabels(agent).length" class="agent-option-actions">
|
||||
<t-tooltip
|
||||
:content="$t('agent.selector.notReadyHint', { items: getAgentNotReadyLabels(agent).join('、') })"
|
||||
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
|
||||
placement="top"
|
||||
>
|
||||
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
|
||||
@@ -66,7 +66,7 @@
|
||||
<span class="agent-option-name">{{ agent.name }}</span>
|
||||
<div v-if="getAgentNotReadyLabels(agent).length" class="agent-option-actions">
|
||||
<t-tooltip
|
||||
:content="$t('agent.selector.notReadyHint', { items: getAgentNotReadyLabels(agent).join('、') })"
|
||||
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
|
||||
placement="top"
|
||||
>
|
||||
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
|
||||
@@ -95,7 +95,7 @@
|
||||
class="agent-option-actions"
|
||||
>
|
||||
<t-tooltip
|
||||
:content="$t('agent.selector.notReadyHint', { items: getAgentNotReadyLabels(shared.agent, String(shared.source_tenant_id)).join('、') })"
|
||||
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(shared.agent, String(shared.source_tenant_id)) })"
|
||||
placement="top"
|
||||
>
|
||||
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
|
||||
@@ -216,8 +216,9 @@ import {
|
||||
resolveAgentNotReadyHighlight,
|
||||
type AgentNotReadyReasonKey,
|
||||
} from '@/utils/agent-readiness';
|
||||
import { formatLocalizedList } from '@/utils/format-list';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const orgStore = useOrganizationStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -233,6 +234,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'select', agent: CustomAgent, sourceTenantId?: string): void;
|
||||
(e: 'not-ready', agent: CustomAgent, labels: string[], keys: AgentNotReadyReasonKey[], sourceTenantId?: string): void;
|
||||
}>();
|
||||
|
||||
type AgentDetailTarget = {
|
||||
@@ -347,6 +349,16 @@ const getAgentNotReadyLabels = (agent: CustomAgent, sourceTenantId?: string): st
|
||||
);
|
||||
};
|
||||
|
||||
const formatNotReadyHint = (agent: CustomAgent, sourceTenantId?: string): string => {
|
||||
return formatLocalizedList(getAgentNotReadyLabels(agent, sourceTenantId), locale.value);
|
||||
};
|
||||
|
||||
const emitAgentNotReady = (agent: CustomAgent, sourceTenantId?: string) => {
|
||||
const keys = getAgentNotReadyReasonKeysFor(agent, sourceTenantId);
|
||||
const labels = formatAgentNotReadyReasons(keys, agent.is_builtin);
|
||||
emit('not-ready', agent, labels, keys, sourceTenantId);
|
||||
};
|
||||
|
||||
const clearDetailHideTimer = () => {
|
||||
if (detailHideTimer) {
|
||||
clearTimeout(detailHideTimer);
|
||||
@@ -418,13 +430,19 @@ const hideDetailPanel = () => {
|
||||
};
|
||||
|
||||
const selectAgent = (agent: CustomAgent) => {
|
||||
if (getAgentNotReadyLabels(agent).length > 0) return;
|
||||
if (getAgentNotReadyLabels(agent).length > 0) {
|
||||
emitAgentNotReady(agent);
|
||||
return;
|
||||
}
|
||||
emit('select', agent);
|
||||
};
|
||||
|
||||
const selectSharedAgent = (shared: SharedAgentInfo) => {
|
||||
const sourceTenantId = String(shared.source_tenant_id);
|
||||
if (getAgentNotReadyLabels(shared.agent, sourceTenantId).length > 0) return;
|
||||
if (getAgentNotReadyLabels(shared.agent, sourceTenantId).length > 0) {
|
||||
emitAgentNotReady(shared.agent as CustomAgent, sourceTenantId);
|
||||
return;
|
||||
}
|
||||
emit('select', shared.agent as CustomAgent, sourceTenantId);
|
||||
};
|
||||
|
||||
@@ -440,6 +458,7 @@ const goToSettings = (agent: CustomAgent, sourceTenantId?: string) => {
|
||||
edit: agent.id,
|
||||
section,
|
||||
...(highlight ? { highlight } : {}),
|
||||
...(sourceTenantId ? { sourceTenantId } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
resolveAgentNotReadyHighlight,
|
||||
type AgentNotReadyReasonKey,
|
||||
} from '@/utils/agent-readiness';
|
||||
import { formatLocalizedList } from '@/utils/format-list';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -47,7 +48,7 @@ const {
|
||||
chatModels: availableModels,
|
||||
webSearchProviders,
|
||||
} = storeToRefs(chatResources);
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
let query = ref("");
|
||||
const showKbSelector = ref(false);
|
||||
@@ -1631,9 +1632,18 @@ const createSession = async (val: string) => {
|
||||
actualAgent = builtin || agentToCheck;
|
||||
}
|
||||
const isAgentMode = actualAgent.config?.agent_mode === 'smart-reasoning';
|
||||
const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons(actualAgent, isAgentMode);
|
||||
const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
actualAgent,
|
||||
isAgentMode,
|
||||
settingsStore.selectedAgentSourceTenantId ?? undefined,
|
||||
);
|
||||
if (notReadyReasons.length > 0) {
|
||||
showAgentNotReadyMessage(actualAgent, notReadyReasons, notReadyKeys);
|
||||
showAgentNotReadyMessage(
|
||||
actualAgent,
|
||||
notReadyReasons,
|
||||
notReadyKeys,
|
||||
settingsStore.selectedAgentSourceTenantId ?? undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 获取@提及的知识库和文件信息
|
||||
@@ -1780,12 +1790,13 @@ const selectAgentMode = async (mode: 'quick-answer' | 'smart-reasoning') => {
|
||||
const builtinAgent = agents.value.find(a => a.id === builtinAgentId);
|
||||
|
||||
if (builtinAgent) {
|
||||
const { labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
builtinAgent,
|
||||
mode === 'smart-reasoning',
|
||||
);
|
||||
if (notReadyReasons.length > 0) {
|
||||
showAgentModeSelector.value = false;
|
||||
showAgentNotReadyMessage(builtinAgent, notReadyReasons, notReadyKeys);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1801,6 +1812,15 @@ const selectAgentMode = async (mode: 'quick-answer' | 'smart-reasoning') => {
|
||||
}
|
||||
|
||||
// 选择智能体(新版);sourceTenantId 为共享智能体时传入
|
||||
const handleAgentNotReady = (
|
||||
agent: CustomAgent,
|
||||
labels: string[],
|
||||
keys: AgentNotReadyReasonKey[],
|
||||
sourceTenantId?: string,
|
||||
) => {
|
||||
showAgentNotReadyMessage(agent, labels, keys, sourceTenantId);
|
||||
};
|
||||
|
||||
const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) => {
|
||||
if (!chatResources.isFresh('models')) {
|
||||
await loadChatModels()
|
||||
@@ -1814,7 +1834,7 @@ const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) =>
|
||||
? (agents.value.find(a => a.id === agent.id) || agent)
|
||||
: agent;
|
||||
|
||||
const { labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons(
|
||||
actualAgent,
|
||||
isAgentType,
|
||||
sourceTenantId,
|
||||
@@ -1822,6 +1842,7 @@ const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) =>
|
||||
|
||||
if (notReadyReasons.length > 0) {
|
||||
showAgentModeSelector.value = false;
|
||||
showAgentNotReadyMessage(actualAgent, notReadyReasons, notReadyKeys, sourceTenantId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1990,7 +2011,7 @@ const collectAgentNotReadyReasons = (
|
||||
isAgentMode: boolean,
|
||||
sourceTenantId?: string,
|
||||
): { keys: AgentNotReadyReasonKey[]; labels: string[] } => {
|
||||
const isSharedAgent = !!(sourceTenantId || settingsStore.selectedAgentSourceTenantId);
|
||||
const isSharedAgent = !!sourceTenantId;
|
||||
const keys = getAgentNotReadyReasonKeys(agent.config, allModels.value, {
|
||||
isAgentMode,
|
||||
isSharedAgent,
|
||||
@@ -2005,6 +2026,7 @@ const goToAgentEditor = (
|
||||
agent: CustomAgent,
|
||||
section = 'model',
|
||||
highlight?: AgentNotReadyReasonKey,
|
||||
sourceTenantId?: string,
|
||||
) => {
|
||||
router.push({
|
||||
path: '/platform/agents',
|
||||
@@ -2012,6 +2034,7 @@ const goToAgentEditor = (
|
||||
edit: agent.id,
|
||||
section,
|
||||
...(highlight ? { highlight } : {}),
|
||||
...(sourceTenantId ? { sourceTenantId } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -2021,8 +2044,9 @@ const showAgentNotReadyMessage = (
|
||||
agent: CustomAgent,
|
||||
reasons: string[],
|
||||
reasonKeys?: AgentNotReadyReasonKey[],
|
||||
sourceTenantId?: string,
|
||||
) => {
|
||||
const reasonsText = reasons.join('、')
|
||||
const reasonsText = formatLocalizedList(reasons, locale.value)
|
||||
const section = resolveAgentNotReadySection(reasonKeys || ['summary_model'])
|
||||
const highlight = resolveAgentNotReadyHighlight(reasonKeys || ['summary_model'])
|
||||
|
||||
@@ -2032,7 +2056,7 @@ const showAgentNotReadyMessage = (
|
||||
href: '#',
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault();
|
||||
goToAgentEditor(agent, section, highlight);
|
||||
goToAgentEditor(agent, section, highlight, sourceTenantId);
|
||||
},
|
||||
style: 'color: var(--td-brand-color); text-decoration: none; font-weight: 500; cursor: pointer; align-self: flex-start;',
|
||||
onMouseenter: (e: Event) => {
|
||||
@@ -2210,7 +2234,8 @@ defineExpose({
|
||||
<AgentSelector :visible="showAgentModeSelector" :anchorEl="agentModeButtonRef"
|
||||
:currentAgentId="selectedAgentId" :agents="enabledAgents" :all-models="allModels"
|
||||
@close="closeAgentModeSelector"
|
||||
@select="handleSelectAgent" />
|
||||
@select="handleSelectAgent"
|
||||
@not-ready="handleAgentNotReady" />
|
||||
|
||||
<!-- WebSearch 开关按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
|
||||
@@ -92,3 +92,11 @@ test('getAgentNotReadyReasonKeys requires rerank only in agent mode with KB sear
|
||||
{ isAgentMode: true, isSharedAgent: false },
|
||||
), ['summary_model', 'rerank_model'])
|
||||
})
|
||||
|
||||
test('getAgentNotReadyReasonKeys does not treat current shared context as shared without sourceTenantId', () => {
|
||||
assert.deepEqual(getAgentNotReadyReasonKeys(
|
||||
{ model_id: 'deleted-chat' },
|
||||
[{ id: 'chat-1', type: 'KnowledgeQA' }],
|
||||
{ isAgentMode: false, isSharedAgent: false },
|
||||
), ['summary_model'])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Locale-aware conjunction for short UI lists (e.g. missing config items). */
|
||||
export function formatLocalizedList(items: string[], locale = 'en-US'): string {
|
||||
if (items.length === 0) return ''
|
||||
if (typeof Intl !== 'undefined' && typeof Intl.ListFormat !== 'undefined') {
|
||||
try {
|
||||
return new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' }).format(items)
|
||||
} catch {
|
||||
// Unsupported locale — fall through.
|
||||
}
|
||||
}
|
||||
return items.join(', ')
|
||||
}
|
||||
@@ -620,9 +620,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ReRank 模型(启用知识库时显示) -->
|
||||
<!-- ReRank 模型(启用知识库或 knowledge_search 工具时显示) -->
|
||||
<div
|
||||
v-if="hasKnowledgeBase"
|
||||
v-if="showRerankModelField"
|
||||
class="setting-row"
|
||||
data-agent-field="rerank_model"
|
||||
:class="{ 'setting-row--field-highlight': highlightedField === 'rerank_model' }"
|
||||
@@ -1394,7 +1394,7 @@ import {
|
||||
type KBCapabilities,
|
||||
} from '@/api/agent';
|
||||
import { type ModelConfig } from '@/api/model';
|
||||
import { type AgentNotReadyReasonKey } from '@/utils/agent-readiness';
|
||||
import { type AgentNotReadyReasonKey, agentRequiresRerankModel } 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';
|
||||
@@ -1675,6 +1675,11 @@ const hasKnowledgeBase = computed(() => {
|
||||
return kbSelectionMode.value !== 'none';
|
||||
});
|
||||
|
||||
const showRerankModelField = computed(() => {
|
||||
if (!isAgentMode.value) return hasKnowledgeBase.value;
|
||||
return hasKnowledgeBase.value || agentRequiresRerankModel(formData.value.config);
|
||||
});
|
||||
|
||||
// 当前配置下进入到智能体作用域的知识库列表
|
||||
// 注意:用户可能选了 knowledge_bases(按库级),也可能选了 knowledge_ids(按文档级)
|
||||
// 这里仅用于 UI 上的工具可用性判定,按库级来计算
|
||||
|
||||
@@ -1119,7 +1119,9 @@ const fetchList = (force = false) => {
|
||||
return Promise.all([
|
||||
chatResources.fetchAgentsForList({ creator: creatorFilter.value }, force).then(applyAgentListData),
|
||||
orgStore.fetchOrganizations({ force }),
|
||||
orgStore.fetchSharedAgents({ force }),
|
||||
]).finally(() => { loading.value = false }).then(() => {
|
||||
checkAndOpenEditModal()
|
||||
// 各空间智能体数量已由 GET /organizations 的 resource_counts 带回,存于 orgStore.resourceCounts
|
||||
const counts = orgStore.resourceCounts?.agents?.by_organization
|
||||
if (counts) spaceAgentCountByOrg.value = { ...counts }
|
||||
@@ -1127,9 +1129,22 @@ const fetchList = (force = false) => {
|
||||
}
|
||||
|
||||
// 检查 URL 参数并打开编辑模态框
|
||||
const resolveAgentForEdit = (editId: string, sourceTenantId?: string): CustomAgent | null => {
|
||||
const own = agents.value.find(a => a.id === editId)
|
||||
if (own) return own
|
||||
if (sourceTenantId) {
|
||||
const shared = sharedAgents.value.find(
|
||||
s => s.agent?.id === editId && String(s.source_tenant_id) === sourceTenantId,
|
||||
)
|
||||
if (shared?.agent) return shared.agent as CustomAgent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const checkAndOpenEditModal = () => {
|
||||
const editId = route.query.edit as string
|
||||
const section = route.query.section as string
|
||||
const sourceTenantId = route.query.sourceTenantId as string | undefined
|
||||
if (editId && (section === 'im' || section === 'embed' || section === 'integrations')) {
|
||||
const tab = section === 'embed' ? 'embed' : 'im'
|
||||
router.replace({
|
||||
@@ -1139,7 +1154,7 @@ const checkAndOpenEditModal = () => {
|
||||
return
|
||||
}
|
||||
if (editId) {
|
||||
const agent = agents.value.find(a => a.id === editId)
|
||||
const agent = resolveAgentForEdit(editId, sourceTenantId)
|
||||
if (agent) {
|
||||
editingAgent.value = agent
|
||||
editorMode.value = 'edit'
|
||||
@@ -1149,7 +1164,7 @@ const checkAndOpenEditModal = () => {
|
||||
}
|
||||
// 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, highlight: _h, ...rest } = route.query
|
||||
const { edit: _e, section: _s, highlight: _h, sourceTenantId: _st, ...rest } = route.query
|
||||
router.replace({ path: route.path, query: rest })
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1176,7 @@ const checkAndOpenEditModal = () => {
|
||||
watch(
|
||||
() => route.query.edit,
|
||||
(v) => {
|
||||
if (v && agents.value.length > 0) {
|
||||
if (v && (agents.value.length > 0 || sharedAgents.value.length > 0)) {
|
||||
checkAndOpenEditModal()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -83,3 +83,67 @@ func TestResolveChatModelIDUsesValidConfiguredAgentModel(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "agent-chat", modelID)
|
||||
}
|
||||
|
||||
func TestResolveChatModelIDRejectsNonChatSummaryModelOverride(t *testing.T) {
|
||||
svc := &sessionService{
|
||||
modelService: &stubModelService{
|
||||
modelsByID: map[string]*types.Model{
|
||||
"agent-chat": {
|
||||
ID: "agent-chat",
|
||||
Type: types.ModelTypeKnowledgeQA,
|
||||
},
|
||||
"rerank-only": {
|
||||
ID: "rerank-only",
|
||||
Type: types.ModelTypeRerank,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req := &types.QARequest{
|
||||
Session: &types.Session{},
|
||||
CustomAgent: &types.CustomAgent{
|
||||
ID: "agent-1",
|
||||
Config: types.CustomAgentConfig{
|
||||
ModelID: "agent-chat",
|
||||
},
|
||||
},
|
||||
SummaryModelID: "rerank-only",
|
||||
}
|
||||
|
||||
modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "agent-chat", modelID)
|
||||
}
|
||||
|
||||
func TestResolveChatModelIDUsesValidSummaryModelOverride(t *testing.T) {
|
||||
svc := &sessionService{
|
||||
modelService: &stubModelService{
|
||||
modelsByID: map[string]*types.Model{
|
||||
"agent-chat": {
|
||||
ID: "agent-chat",
|
||||
Type: types.ModelTypeKnowledgeQA,
|
||||
},
|
||||
"override-chat": {
|
||||
ID: "override-chat",
|
||||
Type: types.ModelTypeKnowledgeQA,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req := &types.QARequest{
|
||||
Session: &types.Session{},
|
||||
CustomAgent: &types.CustomAgent{
|
||||
ID: "agent-1",
|
||||
Config: types.CustomAgentConfig{
|
||||
ModelID: "agent-chat",
|
||||
},
|
||||
},
|
||||
SummaryModelID: "override-chat",
|
||||
}
|
||||
|
||||
modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "override-chat", modelID)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
@@ -67,7 +68,7 @@ func (s *sessionService) resolveChatModelID(
|
||||
session := req.Session
|
||||
|
||||
if customAgent != nil {
|
||||
configuredModelID := customAgent.Config.ModelID
|
||||
configuredModelID := strings.TrimSpace(customAgent.Config.ModelID)
|
||||
if configuredModelID == "" {
|
||||
return "", fmt.Errorf("chat model is not configured: please set model_id on agent %s", customAgent.ID)
|
||||
}
|
||||
@@ -77,16 +78,18 @@ func (s *sessionService) resolveChatModelID(
|
||||
}
|
||||
}
|
||||
|
||||
summaryModelID = strings.TrimSpace(summaryModelID)
|
||||
if summaryModelID != "" {
|
||||
if model, err := s.modelService.GetModelByID(ctx, summaryModelID); err == nil && model != nil {
|
||||
if model, err := s.modelService.GetModelByID(ctx, summaryModelID); err == nil && model != nil &&
|
||||
model.Type == types.ModelTypeKnowledgeQA {
|
||||
logger.Infof(ctx, "Using request's summary model override: %s", summaryModelID)
|
||||
return summaryModelID, nil
|
||||
}
|
||||
logger.Warnf(ctx, "Request provided invalid summary model ID %s, falling back", summaryModelID)
|
||||
}
|
||||
if customAgent != nil && customAgent.Config.ModelID != "" {
|
||||
logger.Infof(ctx, "Using custom agent's model_id: %s", customAgent.Config.ModelID)
|
||||
return customAgent.Config.ModelID, nil
|
||||
if customAgent != nil && strings.TrimSpace(customAgent.Config.ModelID) != "" {
|
||||
logger.Infof(ctx, "Using custom agent's model_id: %s", strings.TrimSpace(customAgent.Config.ModelID))
|
||||
return strings.TrimSpace(customAgent.Config.ModelID), nil
|
||||
}
|
||||
return s.selectChatModelID(ctx, session, knowledgeBaseIDs, knowledgeIDs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user