feat(agent): move mobile ask to unified agent
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ public class AihrAgentActionService {
|
|||||||
index.set(draftId, DRAFT_TTL);
|
index.set(draftId, DRAFT_TTL);
|
||||||
}
|
}
|
||||||
redissonClient.<Long>getBucket(PREFIX + "draft:" + draftId).set(candidate.id(), DRAFT_TTL);
|
redissonClient.<Long>getBucket(PREFIX + "draft:" + draftId).set(candidate.id(), DRAFT_TTL);
|
||||||
return new ActionDraft(draftId, candidate.targetDomain(), candidate.version(), candidate.draft());
|
return new ActionDraft(draftId, candidate.targetDomain(), candidate.version(), candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConfirmMemoryResponse confirm(String draftId, ConfirmMemoryRequest request) {
|
public ConfirmMemoryResponse confirm(String draftId, ConfirmMemoryRequest request) {
|
||||||
|
|||||||
+9
-3
@@ -134,13 +134,19 @@ public class AihrAgentOrchestrator {
|
|||||||
: AgentStatus.NEEDS_INPUT;
|
: AgentStatus.NEEDS_INPUT;
|
||||||
sourceType = "MEMORY_DRAFT";
|
sourceType = "MEMORY_DRAFT";
|
||||||
}
|
}
|
||||||
String sourceLabel = response.citations().isEmpty() ? sourceType
|
List<SourceSummary> sources = response.citations().isEmpty()
|
||||||
: response.citations().get(0).title();
|
&& "ENTERPRISE_KNOWLEDGE".equals(sourceType)
|
||||||
|
? List.of()
|
||||||
|
: List.of(new SourceSummary(
|
||||||
|
sourceType,
|
||||||
|
response.citations().isEmpty() ? sourceType : response.citations().get(0).title(),
|
||||||
|
OffsetDateTime.now().toString()
|
||||||
|
));
|
||||||
String answer = actionDraft == null ? response.answer()
|
String answer = actionDraft == null ? response.answer()
|
||||||
: "已整理为待确认记录,请核对后保存。";
|
: "已整理为待确认记录,请核对后保存。";
|
||||||
return new AgentResponse(
|
return new AgentResponse(
|
||||||
runId(), response.conversationId(), response.contextVersion(), plan.intent(), status, answer,
|
runId(), response.conversationId(), response.contextVersion(), plan.intent(), status, answer,
|
||||||
List.of(new SourceSummary(sourceType, sourceLabel, OffsetDateTime.now().toString())),
|
sources,
|
||||||
response.citations(), response.resources(), response.data(), actionDraft, null, List.of()
|
response.citations(), response.resources(), response.data(), actionDraft, null, List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -52,6 +52,14 @@ public class AihrAgentPlanner {
|
|||||||
return plan(Intent.RESOURCE_DELIVERY, request, Tool.KNOWLEDGE_RESOURCE, false, false,
|
return plan(Intent.RESOURCE_DELIVERY, request, Tool.KNOWLEDGE_RESOURCE, false, false,
|
||||||
ResponseStyle.GROUNDED);
|
ResponseStyle.GROUNDED);
|
||||||
}
|
}
|
||||||
|
if (containsAny(request, "团队训练概况", "团队训练情况", "团队完训情况")) {
|
||||||
|
return plan(Intent.LIVE_TEAM_WORK, request, Tool.TEAM_PRACTICE_SUMMARY, false, false,
|
||||||
|
ResponseStyle.FACT);
|
||||||
|
}
|
||||||
|
if (containsAny(request, "我的训练概况", "我的训练情况", "我的练习概况", "我练得怎么样")) {
|
||||||
|
return plan(Intent.PRACTICE_COACHING, request, Tool.MY_PRACTICE_SUMMARY, false, false,
|
||||||
|
ResponseStyle.FACT);
|
||||||
|
}
|
||||||
if (containsAny(request, "我今天有什么待办", "我有什么待办", "我的待办", "我的任务",
|
if (containsAny(request, "我今天有什么待办", "我有什么待办", "我的待办", "我的任务",
|
||||||
"我今天该做什么", "我现在该做什么", "今天要做什么", "当前待办")) {
|
"我今天该做什么", "我现在该做什么", "今天要做什么", "当前待办")) {
|
||||||
return plan(Intent.LIVE_MY_WORK, request, Tool.MY_CURRENT_TASKS, false, false, ResponseStyle.FACT);
|
return plan(Intent.LIVE_MY_WORK, request, Tool.MY_CURRENT_TASKS, false, false, ResponseStyle.FACT);
|
||||||
|
|||||||
+1
@@ -38,6 +38,7 @@ class AihrAgentActionServiceTest {
|
|||||||
|
|
||||||
var result = service.confirm(draft.draftId(), request);
|
var result = service.confirm(draft.draftId(), request);
|
||||||
|
|
||||||
|
assertEquals(candidate, draft.payload());
|
||||||
assertEquals("ASSISTANT_CAPTURE", result.targetDomain());
|
assertEquals("ASSISTANT_CAPTURE", result.targetDomain());
|
||||||
assertEquals(88L, result.targetId());
|
assertEquals(88L, result.targetId());
|
||||||
verify(memory).confirm(7L, request);
|
verify(memory).confirm(7L, request);
|
||||||
|
|||||||
+11
@@ -21,6 +21,7 @@ import static org.dromara.aihr.agent.AihrAgentDto.Intent.KNOWLEDGE_QA;
|
|||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@Tag("dev")
|
@Tag("dev")
|
||||||
class AihrAgentOrchestratorTest {
|
class AihrAgentOrchestratorTest {
|
||||||
@@ -52,6 +53,16 @@ class AihrAgentOrchestratorTest {
|
|||||||
assertEquals(AgentStatus.COMPLETED, result.status());
|
assertEquals(AgentStatus.COMPLETED, result.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void knowledgeNoEvidenceDoesNotInventASource() {
|
||||||
|
var query = new CapturingQueryService(response("当前授权资料中没有明确依据。", null, true));
|
||||||
|
|
||||||
|
var result = orchestrator(query).handle(request("装修人员怎么进场?"));
|
||||||
|
|
||||||
|
assertEquals(AgentStatus.NO_EVIDENCE, result.status());
|
||||||
|
assertTrue(result.sourceSummary().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void socialAndClarificationDoNotInvokeKnowledge() {
|
void socialAndClarificationDoNotInvokeKnowledge() {
|
||||||
var query = new CapturingQueryService(response("unused", null, false));
|
var query = new CapturingQueryService(response("unused", null, false));
|
||||||
|
|||||||
+20
@@ -11,7 +11,9 @@ import static org.dromara.aihr.agent.AihrAgentDto.Intent.CAPTURE_FACT;
|
|||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.CLARIFY;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.CLARIFY;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.KNOWLEDGE_QA;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.KNOWLEDGE_QA;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
|
||||||
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_TEAM_WORK;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.MEDIA_UNDERSTANDING;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.MEDIA_UNDERSTANDING;
|
||||||
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.PRACTICE_COACHING;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.RESOURCE_DELIVERY;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.RESOURCE_DELIVERY;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Intent.SOCIAL;
|
import static org.dromara.aihr.agent.AihrAgentDto.Intent.SOCIAL;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.CAPTURE_MEMORY_DRAFT;
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.CAPTURE_MEMORY_DRAFT;
|
||||||
@@ -19,7 +21,9 @@ import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_RESOURCE;
|
|||||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_SEARCH;
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_SEARCH;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MEDIA_ANALYZE;
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MEDIA_ANALYZE;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_CURRENT_TASKS;
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_CURRENT_TASKS;
|
||||||
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_PRACTICE_SUMMARY;
|
||||||
import static org.dromara.aihr.agent.AihrAgentDto.Tool.NONE;
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.NONE;
|
||||||
|
import static org.dromara.aihr.agent.AihrAgentDto.Tool.TEAM_PRACTICE_SUMMARY;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -38,6 +42,22 @@ class AihrAgentPlannerTest {
|
|||||||
assertFalse(plan.requiresConfirmation());
|
assertFalse(plan.requiresConfirmation());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void routesOwnTrainingSummaryToLivePracticeData() {
|
||||||
|
var plan = planner.plan("查询我的训练概况", false);
|
||||||
|
|
||||||
|
assertEquals(PRACTICE_COACHING, plan.intent());
|
||||||
|
assertEquals(MY_PRACTICE_SUMMARY, plan.tool());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void routesTeamTrainingSummaryToSupervisorData() {
|
||||||
|
var plan = planner.plan("查询我的团队训练概况", false);
|
||||||
|
|
||||||
|
assertEquals(LIVE_TEAM_WORK, plan.intent());
|
||||||
|
assertEquals(TEAM_PRACTICE_SUMMARY, plan.tool());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void routesExplicitCaptureToConfirmationDraft() {
|
void routesExplicitCaptureToConfirmationDraft() {
|
||||||
var plan = planner.plan("记一下,3栋1201要回访", false);
|
var plan = planner.plan("记一下,3栋1201要回访", false);
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ const confirm = async (saveScope: '' | 'PRIVATE' | 'COMPANY' = '') => {
|
|||||||
errorText.value = '';
|
errorText.value = '';
|
||||||
const requestAccountKey = currentAccountKey();
|
const requestAccountKey = currentAccountKey();
|
||||||
try {
|
try {
|
||||||
const result = await confirmMemoryCandidate(props.candidate.id, {
|
const result = await confirmMemoryCandidate(props.candidate.agentDraftId || props.candidate.id, {
|
||||||
expectedVersion: props.candidate.version,
|
expectedVersion: props.candidate.version,
|
||||||
idempotencyKey: idempotencyKey.value,
|
idempotencyKey: idempotencyKey.value,
|
||||||
draft: {
|
draft: {
|
||||||
@@ -306,7 +306,7 @@ const dismiss = async () => {
|
|||||||
errorText.value = '';
|
errorText.value = '';
|
||||||
const requestAccountKey = currentAccountKey();
|
const requestAccountKey = currentAccountKey();
|
||||||
try {
|
try {
|
||||||
await dismissMemoryCandidate(props.candidate.id);
|
await dismissMemoryCandidate(props.candidate.agentDraftId || props.candidate.id);
|
||||||
if (requestAccountKey !== currentAccountKey()) return;
|
if (requestAccountKey !== currentAccountKey()) return;
|
||||||
emit('dismissed', props.candidate.id);
|
emit('dismissed', props.candidate.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -121,7 +121,26 @@
|
|||||||
v-if="msg.result.rewrittenQuery && msg.result.rewrittenQuery !== msg.result.queryText"
|
v-if="msg.result.rewrittenQuery && msg.result.rewrittenQuery !== msg.result.queryText"
|
||||||
class="context-used"
|
class="context-used"
|
||||||
>已结合本次对话理解</text>
|
>已结合本次对话理解</text>
|
||||||
|
<view v-if="msg.result.sourceSummary?.length" class="agent-source-row">
|
||||||
|
<text
|
||||||
|
v-for="source in msg.result.sourceSummary"
|
||||||
|
:key="`${source.type}-${source.label}`"
|
||||||
|
class="agent-source-chip"
|
||||||
|
>{{ source.label }}</text>
|
||||||
|
</view>
|
||||||
<text class="bubble-text">{{ msg.result.answer || '未生成直接答案,请查看引用依据。' }}</text>
|
<text class="bubble-text">{{ msg.result.answer || '未生成直接答案,请查看引用依据。' }}</text>
|
||||||
|
<view
|
||||||
|
v-if="msg.result.agentStatus === 'NEEDS_INPUT' && msg.result.clarification"
|
||||||
|
class="agent-clarification"
|
||||||
|
>
|
||||||
|
<text class="agent-clarification-text">{{ msg.result.clarification.prompt }}</text>
|
||||||
|
<button
|
||||||
|
v-if="msg.result.agentIntent === 'WEB_RESEARCH'"
|
||||||
|
class="bubble-action primary"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="consentWebResearch(msg)"
|
||||||
|
>同意查询全网</button>
|
||||||
|
</view>
|
||||||
<view v-if="msg.result.keyPoints?.length" class="bubble-keypoints">
|
<view v-if="msg.result.keyPoints?.length" class="bubble-keypoints">
|
||||||
<text v-for="(point, index) in msg.result.keyPoints" :key="point" class="bubble-keypoint">
|
<text v-for="(point, index) in msg.result.keyPoints" :key="point" class="bubble-keypoint">
|
||||||
{{ index + 1 }}. {{ cleanOrderedText(point) }}
|
{{ index + 1 }}. {{ cleanOrderedText(point) }}
|
||||||
@@ -133,7 +152,7 @@
|
|||||||
<text>{{ item }}</text>
|
<text>{{ item }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="msg.result.noEvidence" class="bubble-noevidence">当前授权空间内没有足够依据。</text>
|
<text v-if="msg.result.noEvidence" class="bubble-noevidence">{{ noEvidenceText(msg.result) }}</text>
|
||||||
|
|
||||||
<view class="bubble-actions">
|
<view class="bubble-actions">
|
||||||
<button v-if="msg.voiceStatus === 'unavailable'" class="bubble-action" @click="toggleAnswerSpeech(msg)">
|
<button v-if="msg.voiceStatus === 'unavailable'" class="bubble-action" @click="toggleAnswerSpeech(msg)">
|
||||||
@@ -142,12 +161,12 @@
|
|||||||
<button v-if="msg.result.snippets?.length" class="bubble-action" @click="msg.citationsOpen = !msg.citationsOpen">
|
<button v-if="msg.result.snippets?.length" class="bubble-action" @click="msg.citationsOpen = !msg.citationsOpen">
|
||||||
引用 {{ msg.result.snippets.length }} 条 {{ msg.citationsOpen ? '▲' : '▼' }}
|
引用 {{ msg.result.snippets.length }} 条 {{ msg.citationsOpen ? '▲' : '▼' }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="msg.toolCode === '' && !broadcastContext" class="bubble-action" :disabled="summaryLoading" @click="buildSummaryCard(msg)">
|
<button v-if="msg.result.agentIntent === 'KNOWLEDGE_QA' && !broadcastContext" class="bubble-action" :disabled="summaryLoading" @click="buildSummaryCard(msg)">
|
||||||
{{ summaryLoading ? '生成中' : '总结卡' }}
|
{{ summaryLoading ? '生成中' : '总结卡' }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="msg.result.noEvidence && !broadcastContext" class="bubble-action primary" @click="openWebAi">去查全网</button>
|
<button v-if="msg.result.noEvidence && msg.result.agentIntent === 'KNOWLEDGE_QA' && !broadcastContext" class="bubble-action primary" @click="openWebAi">去查全网</button>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="msg.toolCode === ''" class="bubble-actions feedback-row">
|
<view v-if="msg.result.snippets?.length" class="bubble-actions feedback-row">
|
||||||
<button
|
<button
|
||||||
class="bubble-action"
|
class="bubble-action"
|
||||||
:class="{ active: msg.feedbackVerdict === 'up' }"
|
:class="{ active: msg.feedbackVerdict === 'up' }"
|
||||||
@@ -422,12 +441,10 @@ import MemoryCandidateCard from '@/components/chat/MemoryCandidateCard.vue';
|
|||||||
import {
|
import {
|
||||||
chooseKnowledgeMedia,
|
chooseKnowledgeMedia,
|
||||||
generateSummaryCard,
|
generateSummaryCard,
|
||||||
queryKnowledgeTool,
|
|
||||||
requestKnowledgeResourceDownloadLink,
|
requestKnowledgeResourceDownloadLink,
|
||||||
searchKnowledge,
|
|
||||||
searchKnowledgeWithMedia,
|
|
||||||
submitAnswerFeedback
|
submitAnswerFeedback
|
||||||
} from '@/services/knowledge';
|
} from '@/services/knowledge';
|
||||||
|
import { askAgent, askAgentWithMedia } from '@/services/agent';
|
||||||
import {
|
import {
|
||||||
getSelectedPosition,
|
getSelectedPosition,
|
||||||
getSelectedPositionProfile,
|
getSelectedPositionProfile,
|
||||||
@@ -909,7 +926,10 @@ const requestErrorText = (error: unknown, fallback: string) => {
|
|||||||
return error instanceof Error ? error.message : fallback;
|
return error instanceof Error ? error.message : fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendQuestion = async (rawText: string, options: { voice?: boolean } = {}) => {
|
const sendQuestion = async (
|
||||||
|
rawText: string,
|
||||||
|
options: { voice?: boolean; externalConsent?: boolean } = {}
|
||||||
|
) => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
uni.showToast({ title: '上一条还在检索中,请稍候', icon: 'none' });
|
uni.showToast({ title: '上一条还在检索中,请稍候', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
@@ -921,7 +941,7 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
|
|||||||
uni.showToast({ title: '消息咨询暂不支持图片或视频,请改用文字或语音', icon: 'none' });
|
uni.showToast({ title: '消息咨询暂不支持图片或视频,请改用文字或语音', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text = rawText.trim() || (media ? '请根据附件现场情况,告诉我应该按什么 SOP 处理' : '');
|
const text = rawText.trim() || (media ? '请分析附件内容并回答我的问题' : '');
|
||||||
if (!text) {
|
if (!text) {
|
||||||
uni.showToast({ title: '请先输入问题', icon: 'none' });
|
uni.showToast({ title: '请先输入问题', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
@@ -955,18 +975,17 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const generation = ++knowledgeSearchGeneration;
|
const generation = ++knowledgeSearchGeneration;
|
||||||
try {
|
try {
|
||||||
|
const agentOptions = {
|
||||||
|
conversation: conversationState(),
|
||||||
|
projectCode: broadcastMessageId ? undefined : currentProject.value?.projectCode,
|
||||||
|
broadcastMessageId,
|
||||||
|
externalConsent: Boolean(options.externalConsent)
|
||||||
|
};
|
||||||
const nextResult = media
|
const nextResult = media
|
||||||
? await searchKnowledgeWithMedia(media, text, getSelectedPosition(), (task) => {
|
? await askAgentWithMedia(media, text, agentOptions, (task) => {
|
||||||
if (generation === knowledgeSearchGeneration) activeMediaUploadTask = task;
|
if (generation === knowledgeSearchGeneration) activeMediaUploadTask = task;
|
||||||
}, conversationState(), currentProject.value?.projectCode)
|
})
|
||||||
: await searchKnowledge(
|
: await askAgent(text, agentOptions);
|
||||||
text,
|
|
||||||
getSelectedPosition(),
|
|
||||||
conversationState(),
|
|
||||||
broadcastMessageId ? undefined : currentProject.value?.projectCode,
|
|
||||||
options.voice ? 'mobile_uni_voice' : 'mobile_uni_sop',
|
|
||||||
broadcastMessageId
|
|
||||||
);
|
|
||||||
if (generation !== knowledgeSearchGeneration) return;
|
if (generation !== knowledgeSearchGeneration) return;
|
||||||
applyConversationState(nextResult);
|
applyConversationState(nextResult);
|
||||||
if (broadcastMessageId && nextResult.broadcastContext?.messageId === broadcastMessageId) {
|
if (broadcastMessageId && nextResult.broadcastContext?.messageId === broadcastMessageId) {
|
||||||
@@ -1038,38 +1057,7 @@ const runDataTool = async (toolCode: 'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMM
|
|||||||
: toolCode;
|
: toolCode;
|
||||||
const question = effectiveToolCode === 'TEAM_PRACTICE_SUMMARY' ? '查询我的团队训练概况' : '查询我的训练概况';
|
const question = effectiveToolCode === 'TEAM_PRACTICE_SUMMARY' ? '查询我的团队训练概况' : '查询我的训练概况';
|
||||||
selectedMedia.value = null;
|
selectedMedia.value = null;
|
||||||
pushMsg<UserMsg>({ role: 'user', text: question });
|
void sendQuestion(question);
|
||||||
const pending = pushMsg<PendingMsg>({ role: 'pending' });
|
|
||||||
loading.value = true;
|
|
||||||
const generation = ++knowledgeSearchGeneration;
|
|
||||||
try {
|
|
||||||
const nextResult = await queryKnowledgeTool(
|
|
||||||
effectiveToolCode, question, conversationState(), currentProject.value?.projectCode);
|
|
||||||
if (generation !== knowledgeSearchGeneration) return;
|
|
||||||
applyConversationState(nextResult);
|
|
||||||
replaceMsg(pending.id, {
|
|
||||||
id: pending.id,
|
|
||||||
role: 'master',
|
|
||||||
result: nextResult,
|
|
||||||
toolCode: effectiveToolCode,
|
|
||||||
feedbackVerdict: '',
|
|
||||||
feedbackSubmitting: false,
|
|
||||||
feedbackPanelOpen: false,
|
|
||||||
feedbackReasonCodes: [],
|
|
||||||
feedbackComment: '',
|
|
||||||
citationsOpen: false,
|
|
||||||
voiceStatus: 'unavailable',
|
|
||||||
voiceDuration: null,
|
|
||||||
unread: false,
|
|
||||||
transcriptOpen: false
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (generation !== knowledgeSearchGeneration) return;
|
|
||||||
replaceMsg(pending.id, null);
|
|
||||||
pushSystem(requestErrorText(error, '训练概况查询失败'));
|
|
||||||
} finally {
|
|
||||||
if (generation === knowledgeSearchGeneration) loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pickMedia = async () => {
|
const pickMedia = async () => {
|
||||||
@@ -1271,6 +1259,12 @@ const serviceMemoryStatusLabel = (status?: string | null) => ({
|
|||||||
|
|
||||||
const displayCitationDate = (value?: string | null) => value ? value.replace('T', ' ').slice(0, 16) : '未记录';
|
const displayCitationDate = (value?: string | null) => value ? value.replace('T', ' ').slice(0, 16) : '未记录';
|
||||||
|
|
||||||
|
const noEvidenceText = (result: KnowledgeSearchResponse) => {
|
||||||
|
if (result.agentIntent === 'WEB_RESEARCH') return '公开网络中没有找到可核验来源。';
|
||||||
|
if (result.agentIntent === 'MEDIA_UNDERSTANDING') return '已分析附件,但当前资料中没有补充依据。';
|
||||||
|
return '当前授权空间内没有足够依据。';
|
||||||
|
};
|
||||||
|
|
||||||
const feedbackReasonOptions = [
|
const feedbackReasonOptions = [
|
||||||
{ code: 'NO_ANSWER', label: '缺少制度或SOP依据' },
|
{ code: 'NO_ANSWER', label: '缺少制度或SOP依据' },
|
||||||
{ code: 'NOT_SPECIFIC', label: '处理步骤不够具体' },
|
{ code: 'NOT_SPECIFIC', label: '处理步骤不够具体' },
|
||||||
@@ -1419,6 +1413,11 @@ const openWebAi = () => {
|
|||||||
uni.navigateTo({ url: '/pages/user/web-ai/index' });
|
uni.navigateTo({ url: '/pages/user/web-ai/index' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const consentWebResearch = (msg: MasterMsg) => {
|
||||||
|
if (loading.value || msg.result.agentIntent !== 'WEB_RESEARCH') return;
|
||||||
|
void sendQuestion(msg.result.queryText, { externalConsent: true });
|
||||||
|
};
|
||||||
|
|
||||||
const openTodayResult = () => {
|
const openTodayResult = () => {
|
||||||
uni.navigateTo({ url: '/pages/user/work-results/index' });
|
uni.navigateTo({ url: '/pages/user/work-results/index' });
|
||||||
};
|
};
|
||||||
@@ -1437,7 +1436,10 @@ const refreshMemoryCandidate = async (msg: MasterMsg, candidateId: number) => {
|
|||||||
try {
|
try {
|
||||||
const candidates = await getMemoryCandidates('DRAFT');
|
const candidates = await getMemoryCandidates('DRAFT');
|
||||||
if (generation !== candidateRefreshGeneration || accountKey !== currentAccountKey()) return;
|
if (generation !== candidateRefreshGeneration || accountKey !== currentAccountKey()) return;
|
||||||
msg.result.memoryCandidate = candidates.find((item) => item.id === candidateId) || null;
|
const refreshed = candidates.find((item) => item.id === candidateId);
|
||||||
|
msg.result.memoryCandidate = refreshed
|
||||||
|
? { ...refreshed, agentDraftId: msg.result.memoryCandidate?.agentDraftId }
|
||||||
|
: null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (generation !== candidateRefreshGeneration || accountKey !== currentAccountKey()) return;
|
if (generation !== candidateRefreshGeneration || accountKey !== currentAccountKey()) return;
|
||||||
uni.showToast({ title: requestErrorText(error, '确认卡刷新失败'), icon: 'none' });
|
uni.showToast({ title: requestErrorText(error, '确认卡刷新失败'), icon: 'none' });
|
||||||
@@ -2977,6 +2979,36 @@ onUnload(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-source-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-source-chip {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef3f8;
|
||||||
|
color: #5b6b7f;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-clarification {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px dashed #eceff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-clarification-text {
|
||||||
|
color: #38424e;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.bubble-actions {
|
.bubble-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import type {
|
||||||
|
AgentResponse,
|
||||||
|
KnowledgeConversationState,
|
||||||
|
KnowledgeMediaSelectedFile,
|
||||||
|
KnowledgeSearchResponse,
|
||||||
|
MemoryCandidate
|
||||||
|
} from '@/types/api';
|
||||||
|
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
|
||||||
|
|
||||||
|
interface AgentMessageOptions {
|
||||||
|
conversation?: KnowledgeConversationState;
|
||||||
|
projectCode?: string;
|
||||||
|
broadcastMessageId?: number;
|
||||||
|
externalConsent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const askAgent = async (question: string, options: AgentMessageOptions = {}) =>
|
||||||
|
normalizeAgentResponse(await apiRequest<AgentResponse>({
|
||||||
|
url: '/api/aihr/agent/messages',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
question,
|
||||||
|
conversationId: options.conversation?.conversationId,
|
||||||
|
contextVersion: options.conversation?.contextVersion,
|
||||||
|
projectCode: options.projectCode,
|
||||||
|
broadcastMessageId: options.broadcastMessageId,
|
||||||
|
externalConsent: Boolean(options.externalConsent)
|
||||||
|
},
|
||||||
|
timeout: 45000
|
||||||
|
}), question);
|
||||||
|
|
||||||
|
export const askAgentWithMedia = (
|
||||||
|
file: KnowledgeMediaSelectedFile,
|
||||||
|
question: string,
|
||||||
|
options: AgentMessageOptions = {},
|
||||||
|
onUploadTask?: (task: ReturnType<typeof uni.uploadFile> | null) => void
|
||||||
|
) => new Promise<KnowledgeSearchResponse>((resolve, reject) => {
|
||||||
|
const task = uni.uploadFile({
|
||||||
|
url: apiUrl('/api/aihr/agent/messages/media'),
|
||||||
|
filePath: file.path,
|
||||||
|
name: 'file',
|
||||||
|
header: authHeaders(false),
|
||||||
|
formData: {
|
||||||
|
question,
|
||||||
|
...(options.conversation ? {
|
||||||
|
conversationId: options.conversation.conversationId,
|
||||||
|
contextVersion: String(options.conversation.contextVersion)
|
||||||
|
} : {}),
|
||||||
|
...(options.projectCode ? { projectCode: options.projectCode } : {}),
|
||||||
|
...(typeof options.broadcastMessageId === 'number'
|
||||||
|
? { broadcastMessageId: String(options.broadcastMessageId) }
|
||||||
|
: {}),
|
||||||
|
externalConsent: String(Boolean(options.externalConsent))
|
||||||
|
},
|
||||||
|
timeout: 360000,
|
||||||
|
success: (result) => {
|
||||||
|
try {
|
||||||
|
resolve(normalizeAgentResponse(
|
||||||
|
readTextPayload<AgentResponse>(result.statusCode, result.data),
|
||||||
|
question
|
||||||
|
));
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => reject(new Error(file.kind === 'video' ? '视频上传或分析超时' : '图片上传失败')),
|
||||||
|
complete: () => onUploadTask?.(null)
|
||||||
|
});
|
||||||
|
onUploadTask?.(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const normalizeAgentResponse = (
|
||||||
|
response: AgentResponse,
|
||||||
|
question: string
|
||||||
|
): KnowledgeSearchResponse => {
|
||||||
|
const memoryCandidate = response.actionDraft?.payload
|
||||||
|
? {
|
||||||
|
...(response.actionDraft.payload as MemoryCandidate),
|
||||||
|
agentDraftId: response.actionDraft.draftId
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const snippets = (response.citations || []).map((citation) => ({
|
||||||
|
docTitle: citation.title,
|
||||||
|
content: citation.snippet,
|
||||||
|
docId: citation.docId,
|
||||||
|
fragmentId: citation.fragmentId,
|
||||||
|
spaceCode: citation.spaceCode,
|
||||||
|
sourceType: citation.sourceType,
|
||||||
|
domain: citation.domain,
|
||||||
|
status: citation.status,
|
||||||
|
occurredAt: citation.occurredAt,
|
||||||
|
updatedAt: citation.updatedAt
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
requestId: response.runId,
|
||||||
|
queryText: question,
|
||||||
|
category: 'sop',
|
||||||
|
answer: response.answer || response.clarification?.prompt || '',
|
||||||
|
reference: (response.sourceSummary || []).map((source) => source.label).filter(Boolean).join('、'),
|
||||||
|
docs: [],
|
||||||
|
snippets,
|
||||||
|
training: [],
|
||||||
|
records: [],
|
||||||
|
citations: response.citations || [],
|
||||||
|
usedSpaceCodes: [],
|
||||||
|
noEvidence: response.status === 'NO_EVIDENCE',
|
||||||
|
data: response.data,
|
||||||
|
conversationId: response.conversationId,
|
||||||
|
contextVersion: response.contextVersion,
|
||||||
|
intent: response.intent,
|
||||||
|
rewrittenQuery: question,
|
||||||
|
resources: response.resources || [],
|
||||||
|
memoryCandidate,
|
||||||
|
broadcastContext: null,
|
||||||
|
agentStatus: response.status,
|
||||||
|
agentIntent: response.intent,
|
||||||
|
sourceSummary: response.sourceSummary || [],
|
||||||
|
clarification: response.clarification || null
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -44,17 +44,21 @@ export const getMemoryCandidates = async (status = 'DRAFT') =>
|
|||||||
timeout: 15000
|
timeout: 15000
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const confirmMemoryCandidate = (candidateId: number, request: MemoryConfirmRequest) =>
|
export const confirmMemoryCandidate = (candidateRef: number | string, request: MemoryConfirmRequest) =>
|
||||||
apiRequest<MemoryConfirmResult>({
|
apiRequest<MemoryConfirmResult>({
|
||||||
url: `${candidateBase}/${candidateId}/confirm`,
|
url: typeof candidateRef === 'string'
|
||||||
|
? `/api/aihr/agent/actions/${encodeURIComponent(String(candidateRef))}/confirm`
|
||||||
|
: `${candidateBase}/${candidateRef}/confirm`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: request,
|
data: request,
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dismissMemoryCandidate = (candidateId: number) =>
|
export const dismissMemoryCandidate = (candidateRef: number | string) =>
|
||||||
apiRequest<{ id: number; status: string }>({
|
apiRequest<{ id: number; status: string }>({
|
||||||
url: `${candidateBase}/${candidateId}/dismiss`,
|
url: typeof candidateRef === 'string'
|
||||||
|
? `/api/aihr/agent/actions/${encodeURIComponent(String(candidateRef))}/dismiss`
|
||||||
|
: `${candidateBase}/${candidateRef}/dismiss`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ export interface KnowledgeSearchResponse {
|
|||||||
resources?: KnowledgeResource[];
|
resources?: KnowledgeResource[];
|
||||||
memoryCandidate?: MemoryCandidate | null;
|
memoryCandidate?: MemoryCandidate | null;
|
||||||
broadcastContext?: BroadcastKnowledgeContext | null;
|
broadcastContext?: BroadcastKnowledgeContext | null;
|
||||||
|
agentStatus?: AgentStatus;
|
||||||
|
agentIntent?: AgentIntent;
|
||||||
|
sourceSummary?: AgentSourceSummary[];
|
||||||
|
clarification?: AgentClarification | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeMediaSelectedFile {
|
export interface KnowledgeMediaSelectedFile {
|
||||||
@@ -214,6 +218,55 @@ export interface UnifiedKnowledgeResponse {
|
|||||||
broadcastContext?: BroadcastKnowledgeContext | null;
|
broadcastContext?: BroadcastKnowledgeContext | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AgentStatus = 'COMPLETED' | 'NEEDS_INPUT' | 'NEEDS_CONFIRMATION' | 'NO_EVIDENCE' | 'FORBIDDEN' | 'UNAVAILABLE' | 'FAILED';
|
||||||
|
|
||||||
|
export type AgentIntent =
|
||||||
|
| 'KNOWLEDGE_QA'
|
||||||
|
| 'RESOURCE_DELIVERY'
|
||||||
|
| 'LIVE_MY_WORK'
|
||||||
|
| 'LIVE_TEAM_WORK'
|
||||||
|
| 'PRACTICE_COACHING'
|
||||||
|
| 'CAPTURE_FACT'
|
||||||
|
| 'DRAFT_ACTION'
|
||||||
|
| 'MEDIA_UNDERSTANDING'
|
||||||
|
| 'WEB_RESEARCH'
|
||||||
|
| 'CLARIFY'
|
||||||
|
| 'SOCIAL';
|
||||||
|
|
||||||
|
export interface AgentSourceSummary {
|
||||||
|
type: 'ENTERPRISE_KNOWLEDGE' | 'LIVE_DATA' | 'MEMORY_DRAFT' | 'MEDIA' | 'WEB' | string;
|
||||||
|
label: string;
|
||||||
|
verifiedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentActionDraft {
|
||||||
|
draftId: string;
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
payload: MemoryCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentClarification {
|
||||||
|
prompt: string;
|
||||||
|
fields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentResponse {
|
||||||
|
runId: string;
|
||||||
|
conversationId?: string | null;
|
||||||
|
contextVersion?: number | null;
|
||||||
|
intent: AgentIntent;
|
||||||
|
status: AgentStatus;
|
||||||
|
answer: string;
|
||||||
|
sourceSummary: AgentSourceSummary[];
|
||||||
|
citations: KnowledgeCitation[];
|
||||||
|
resources: KnowledgeResource[];
|
||||||
|
data?: unknown;
|
||||||
|
actionDraft?: AgentActionDraft | null;
|
||||||
|
clarification?: AgentClarification | null;
|
||||||
|
nextActions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export type MemoryCandidateStatus = 'DRAFT' | 'NEEDS_INPUT' | 'CONFIRMED' | 'DISMISSED' | 'EXPIRED' | string;
|
export type MemoryCandidateStatus = 'DRAFT' | 'NEEDS_INPUT' | 'CONFIRMED' | 'DISMISSED' | 'EXPIRED' | string;
|
||||||
|
|
||||||
export type MemoryTargetDomain = 'ASSISTANT_CAPTURE' | 'PERSONAL' | 'COMPANY' | 'PROJECT_SERVICE' | string;
|
export type MemoryTargetDomain = 'ASSISTANT_CAPTURE' | 'PERSONAL' | 'COMPANY' | 'PROJECT_SERVICE' | string;
|
||||||
@@ -254,6 +307,7 @@ export interface MemoryCandidate {
|
|||||||
missingFields: string[];
|
missingFields: string[];
|
||||||
expiresAt: string | null;
|
expiresAt: string | null;
|
||||||
source?: WorkSourceSnapshot | null;
|
source?: WorkSourceSnapshot | null;
|
||||||
|
agentDraftId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemoryConfirmRequest {
|
export interface MemoryConfirmRequest {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const source = async (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
test('问数字师傅统一使用 Agent 文本与媒体入口', async () => {
|
||||||
|
const [service, page] = await Promise.all([
|
||||||
|
source('../src/services/agent.ts'),
|
||||||
|
source('../src/pages/user/sop/index.vue')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(service, /\/api\/aihr\/agent\/messages'/);
|
||||||
|
assert.match(service, /\/api\/aihr\/agent\/messages\/media/);
|
||||||
|
assert.doesNotMatch(service, /toolCode/);
|
||||||
|
assert.doesNotMatch(service, /\/api\/knowledge\/query/);
|
||||||
|
assert.match(page, /askAgent/);
|
||||||
|
assert.match(page, /askAgentWithMedia/);
|
||||||
|
assert.doesNotMatch(page, /await searchKnowledge\(/);
|
||||||
|
assert.doesNotMatch(page, /await queryKnowledgeTool\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Agent 响应按结构化状态渲染来源、澄清和确认动作', async () => {
|
||||||
|
const [types, service, page] = await Promise.all([
|
||||||
|
source('../src/types/api.ts'),
|
||||||
|
source('../src/services/agent.ts'),
|
||||||
|
source('../src/pages/user/sop/index.vue')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(types, /export type AgentStatus = 'COMPLETED' \| 'NEEDS_INPUT' \| 'NEEDS_CONFIRMATION'/);
|
||||||
|
assert.match(types, /sourceSummary\?: AgentSourceSummary\[\]/);
|
||||||
|
assert.match(types, /agentDraftId\?: string/);
|
||||||
|
assert.match(service, /status === 'NO_EVIDENCE'/);
|
||||||
|
assert.match(service, /agentDraftId: response\.actionDraft\.draftId/);
|
||||||
|
assert.match(page, /agent-source-chip/);
|
||||||
|
assert.match(page, /msg\.result\.agentStatus === 'NEEDS_INPUT'/);
|
||||||
|
assert.match(page, /consentWebResearch/);
|
||||||
|
assert.doesNotMatch(page, /result\.answer\.(?:includes|match|startsWith)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Agent 确认卡通过不透明 draftId 调用动作端点', async () => {
|
||||||
|
const [assistantService, card] = await Promise.all([
|
||||||
|
source('../src/services/personal-assistant.ts'),
|
||||||
|
source('../src/components/chat/MemoryCandidateCard.vue')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(assistantService, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/confirm/);
|
||||||
|
assert.match(assistantService, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/dismiss/);
|
||||||
|
assert.match(card, /props\.candidate\.agentDraftId \|\| props\.candidate\.id/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user