fix(agent): preserve broadcast consultation context
This commit is contained in:
+2
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.aihr.agent;
|
package org.dromara.aihr.agent;
|
||||||
|
|
||||||
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.BroadcastContext;
|
||||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.Citation;
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.Citation;
|
||||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.Resource;
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.Resource;
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ public final class AihrAgentDto {
|
|||||||
List<SourceSummary> sourceSummary,
|
List<SourceSummary> sourceSummary,
|
||||||
List<Citation> citations,
|
List<Citation> citations,
|
||||||
List<Resource> resources,
|
List<Resource> resources,
|
||||||
|
BroadcastContext broadcastContext,
|
||||||
Object data,
|
Object data,
|
||||||
ActionDraft actionDraft,
|
ActionDraft actionDraft,
|
||||||
Clarification clarification,
|
Clarification clarification,
|
||||||
|
|||||||
+4
-4
@@ -147,7 +147,7 @@ public class AihrAgentOrchestrator {
|
|||||||
return new AgentResponse(
|
return new AgentResponse(
|
||||||
runId(), response.conversationId(), response.contextVersion(), plan.intent(), status, answer,
|
runId(), response.conversationId(), response.contextVersion(), plan.intent(), status, answer,
|
||||||
sources,
|
sources,
|
||||||
response.citations(), response.resources(), response.data(), actionDraft, null, List.of()
|
response.citations(), response.resources(), response.broadcastContext(), response.data(), actionDraft, null, List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ public class AihrAgentOrchestrator {
|
|||||||
return new AgentResponse(
|
return new AgentResponse(
|
||||||
runId(), request.conversationId(), request.contextVersion(), plan.intent(), status, answer,
|
runId(), request.conversationId(), request.contextVersion(), plan.intent(), status, answer,
|
||||||
List.of(new SourceSummary("WEB", response.provider(), OffsetDateTime.now().toString())),
|
List.of(new SourceSummary("WEB", response.provider(), OffsetDateTime.now().toString())),
|
||||||
List.of(), List.of(), response, null,
|
List.of(), List.of(), null, response, null,
|
||||||
status == AgentStatus.NEEDS_INPUT
|
status == AgentStatus.NEEDS_INPUT
|
||||||
? new Clarification("是否同意查询全网?", List.of())
|
? new Clarification("是否同意查询全网?", List.of())
|
||||||
: null,
|
: null,
|
||||||
@@ -199,7 +199,7 @@ public class AihrAgentOrchestrator {
|
|||||||
Clarification clarification) {
|
Clarification clarification) {
|
||||||
return new AgentResponse(
|
return new AgentResponse(
|
||||||
runId(), null, null, plan.intent(), status, answer, List.of(), List.of(), List.of(),
|
runId(), null, null, plan.intent(), status, answer, List.of(), List.of(), List.of(),
|
||||||
null, null, clarification, List.of()
|
null, null, null, clarification, List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ public class AihrAgentOrchestrator {
|
|||||||
return new AgentResponse(
|
return new AgentResponse(
|
||||||
runId(), request == null ? null : request.conversationId(),
|
runId(), request == null ? null : request.conversationId(),
|
||||||
request == null ? null : request.contextVersion(), plan.intent(), status, "",
|
request == null ? null : request.contextVersion(), plan.intent(), status, "",
|
||||||
List.of(), List.of(), List.of(), null, null, null, List.of()
|
List.of(), List.of(), List.of(), null, null, null, null, List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ class AihrAgentAuditServiceTest {
|
|||||||
return new AgentResponse("agent_run_test", "conversation_1", 1L, Intent.LIVE_MY_WORK,
|
return new AgentResponse("agent_run_test", "conversation_1", 1L, Intent.LIVE_MY_WORK,
|
||||||
AgentStatus.COMPLETED, "敏感答案",
|
AgentStatus.COMPLETED, "敏感答案",
|
||||||
List.of(new SourceSummary("LIVE_DATA", "我的待办", "2026-07-24T20:00:00+08:00")),
|
List.of(new SourceSummary("LIVE_DATA", "我的待办", "2026-07-24T20:00:00+08:00")),
|
||||||
List.of(), List.of(), null, null, null, List.of());
|
List.of(), List.of(), null, null, null, null, List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CapturingJdbcTemplate extends JdbcTemplate {
|
private static final class CapturingJdbcTemplate extends JdbcTemplate {
|
||||||
|
|||||||
+20
-1
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import org.dromara.aihr.agent.AihrAgentDto.AgentRequest;
|
import org.dromara.aihr.agent.AihrAgentDto.AgentRequest;
|
||||||
import org.dromara.aihr.agent.AihrAgentDto.AgentStatus;
|
import org.dromara.aihr.agent.AihrAgentDto.AgentStatus;
|
||||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||||
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.BroadcastContext;
|
||||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryRequest;
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryRequest;
|
||||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryResponse;
|
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryResponse;
|
||||||
import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver;
|
import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver;
|
||||||
@@ -64,6 +65,18 @@ class AihrAgentOrchestratorTest {
|
|||||||
assertTrue(result.sourceSummary().isEmpty());
|
assertTrue(result.sourceSummary().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void broadcastConsultationPreservesServerVerifiedContext() {
|
||||||
|
var query = new CapturingQueryService(response("按高温作业规定执行。", null, false,
|
||||||
|
new BroadcastContext(42L, "高温作业提醒", "2026-07-24T09:00:00")));
|
||||||
|
|
||||||
|
var result = orchestrator(query).handle(new AgentRequest(
|
||||||
|
"这条消息里的高温规定是什么?", "conversation_1", 0L, null, 42L, false));
|
||||||
|
|
||||||
|
assertEquals(42L, query.lastRequest.broadcastMessageId());
|
||||||
|
assertEquals(42L, result.broadcastContext().messageId());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void socialAndClarificationDoNotInvokeKnowledge() {
|
void socialAndClarificationDoNotInvokeKnowledge() {
|
||||||
var query = new CapturingQueryService(response("unused", null, false));
|
var query = new CapturingQueryService(response("unused", null, false));
|
||||||
@@ -147,8 +160,14 @@ class AihrAgentOrchestratorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static QueryResponse response(String answer, Object data, boolean noEvidence) {
|
private static QueryResponse response(String answer, Object data, boolean noEvidence) {
|
||||||
|
return response(answer, data, noEvidence, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QueryResponse response(String answer, Object data, boolean noEvidence,
|
||||||
|
BroadcastContext broadcastContext) {
|
||||||
return new QueryResponse("request-1", "question", answer, List.of(), List.of(), noEvidence,
|
return new QueryResponse("request-1", "question", answer, List.of(), List.of(), noEvidence,
|
||||||
"test", null, data, "conversation_1", 1L, "QA", "question", List.of());
|
"test", null, data, "conversation_1", 1L, "QA", "question", List.of(), null,
|
||||||
|
broadcastContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CapturingQueryService extends AihrKnowledgeQueryService {
|
private static final class CapturingQueryService extends AihrKnowledgeQueryService {
|
||||||
|
|||||||
@@ -2991,7 +2991,7 @@ onUnload(() => {
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #eef3f8;
|
background: #eef3f8;
|
||||||
color: #5b6b7f;
|
color: #5b6b7f;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-clarification {
|
.agent-clarification {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const normalizeAgentResponse = (
|
|||||||
rewrittenQuery: question,
|
rewrittenQuery: question,
|
||||||
resources: response.resources || [],
|
resources: response.resources || [],
|
||||||
memoryCandidate,
|
memoryCandidate,
|
||||||
broadcastContext: null,
|
broadcastContext: response.broadcastContext || null,
|
||||||
agentStatus: response.status,
|
agentStatus: response.status,
|
||||||
agentIntent: response.intent,
|
agentIntent: response.intent,
|
||||||
sourceSummary: response.sourceSummary || [],
|
sourceSummary: response.sourceSummary || [],
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ export interface AgentResponse {
|
|||||||
sourceSummary: AgentSourceSummary[];
|
sourceSummary: AgentSourceSummary[];
|
||||||
citations: KnowledgeCitation[];
|
citations: KnowledgeCitation[];
|
||||||
resources: KnowledgeResource[];
|
resources: KnowledgeResource[];
|
||||||
|
broadcastContext?: BroadcastKnowledgeContext | null;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
actionDraft?: AgentActionDraft | null;
|
actionDraft?: AgentActionDraft | null;
|
||||||
clarification?: AgentClarification | null;
|
clarification?: AgentClarification | null;
|
||||||
|
|||||||
@@ -81,17 +81,19 @@ test('问师傅在真正开始会话后收起首次使用引导,保留对话
|
|||||||
test('消息咨询只以消息 ID 作为受控上下文,并让每轮文字或语音请求携带它', async () => {
|
test('消息咨询只以消息 ID 作为受控上下文,并让每轮文字或语音请求携带它', async () => {
|
||||||
const [page, service, types] = await Promise.all([
|
const [page, service, types] = await Promise.all([
|
||||||
read('../src/pages/user/sop/index.vue'),
|
read('../src/pages/user/sop/index.vue'),
|
||||||
read('../src/services/knowledge.ts'),
|
read('../src/services/agent.ts'),
|
||||||
read('../src/types/api.ts')
|
read('../src/types/api.ts')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.match(page, /getBroadcastMessage\(pending\.messageId\)/);
|
assert.match(page, /getBroadcastMessage\(pending\.messageId\)/);
|
||||||
assert.match(page, /messageId: message\.id,\s*title: message\.title,\s*publishedAt: message\.publishedAt/);
|
assert.match(page, /messageId: message\.id,\s*title: message\.title,\s*publishedAt: message\.publishedAt/);
|
||||||
assert.match(page, /broadcastMessageId \? undefined : currentProject\.value\?\.projectCode/);
|
assert.match(page, /broadcastMessageId \? undefined : currentProject\.value\?\.projectCode/);
|
||||||
assert.match(page, /options\.voice \? 'mobile_uni_voice' : 'mobile_uni_sop',\s*broadcastMessageId/);
|
assert.match(page, /const agentOptions = \{[\s\S]*?broadcastMessageId,[\s\S]*?\}/);
|
||||||
|
assert.match(page, /askAgent\(text, agentOptions\)/);
|
||||||
assert.match(service, /broadcastMessageId\?: number/);
|
assert.match(service, /broadcastMessageId\?: number/);
|
||||||
assert.match(service, /\? \{ broadcastMessageId \}/);
|
assert.match(service, /broadcastMessageId: options\.broadcastMessageId/);
|
||||||
assert.match(service, /broadcastContext: response\.broadcastContext \|\| legacy\.broadcastContext \|\| null/);
|
assert.match(service, /\/api\/aihr\/agent\/messages/);
|
||||||
|
assert.match(service, /broadcastContext: response\.broadcastContext \|\| null/);
|
||||||
assert.match(types, /export interface BroadcastKnowledgeContext/);
|
assert.match(types, /export interface BroadcastKnowledgeContext/);
|
||||||
assert.match(types, /broadcastContext\?: BroadcastKnowledgeContext \| null/);
|
assert.match(types, /broadcastContext\?: BroadcastKnowledgeContext \| null/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ test('语音 profile 透传方言枚举,并保持普通话默认值', async ()
|
|||||||
pageSource('../src/services/speech.ts')
|
pageSource('../src/services/speech.ts')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.match(apiTypes, /dialect\?: 'mandarin' \| 'cantonese' \| 'sichuanese'/);
|
assert.match(apiTypes, /dialect\?: 'mandarin' \| 'cantonese'[\s\S]*\| 'sichuanese'/);
|
||||||
assert.match(speechService, /dialect: 'mandarin'/);
|
assert.match(speechService, /dialect: 'mandarin'/);
|
||||||
assert.match(speechService, /customerVoiceProfile = \([\s\S]*?dialect:/);
|
assert.match(speechService, /customerVoiceProfile = \([\s\S]*?dialect:/);
|
||||||
assert.match(speechService, /dialect === 'cantonese'.*'zh-HK'/);
|
assert.match(speechService, /dialect === 'cantonese'.*'zh-HK'/);
|
||||||
assert.match(speechService, /dialect === 'sichuanese'[\s\S]*?当前设备语音不支持四川话/);
|
assert.match(speechService, /dialect && dialect !== 'mandarin'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('M3 将业主 TTS 绑定到当前训练会话与准确回合,供主管复盘回放', async () => {
|
test('M3 将业主 TTS 绑定到当前训练会话与准确回合,供主管复盘回放', async () => {
|
||||||
@@ -77,7 +77,7 @@ test('M3 将业主 TTS 绑定到当前训练会话与准确回合,供主管复
|
|||||||
|
|
||||||
assert.match(apiTypes, /export interface PracticeTtsContext/);
|
assert.match(apiTypes, /export interface PracticeTtsContext/);
|
||||||
assert.match(speechService, /practiceContext\?: PracticeTtsContext/);
|
assert.match(speechService, /practiceContext\?: PracticeTtsContext/);
|
||||||
assert.match(speechService, /data: \{ text: text\.trim\(\)\.slice\(0, 300\), voiceProfile, practiceContext \}/);
|
assert.match(speechService, /data: \{ text: text\.trim\(\)\.slice\(0, 300\), voiceProfile: resolveTtsVoiceProfile\(voiceProfile\), practiceContext \}/);
|
||||||
assert.match(practicePage, /\{ sessionId: sessionId\.value, turnIndex: Number\(customerTurnIndex\), role: 'customer' \}/);
|
assert.match(practicePage, /\{ sessionId: sessionId\.value, turnIndex: Number\(customerTurnIndex\), role: 'customer' \}/);
|
||||||
assert.match(practicePage, /speechPlayback\.preload\(spoken, profile, practiceContext\)/);
|
assert.match(practicePage, /speechPlayback\.preload\(spoken, profile, practiceContext\)/);
|
||||||
assert.match(practicePage, /turnVoice\(index\)\.profile, turnVoice\(index\)\.practiceContext/);
|
assert.match(practicePage, /turnVoice\(index\)\.profile, turnVoice\(index\)\.practiceContext/);
|
||||||
@@ -481,10 +481,10 @@ test('主管专项从正式团队名单按姓名多选并一次批量派发', as
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('问师傅支持现场媒体、答案播报和总结卡图片下载', async () => {
|
test('问师傅支持现场媒体、答案播报和总结卡图片下载', async () => {
|
||||||
const [source, composerSource, knowledgeService, speechService, imageService] = await Promise.all([
|
const [source, composerSource, agentService, speechService, imageService] = await Promise.all([
|
||||||
pageSource('../src/pages/user/sop/index.vue'),
|
pageSource('../src/pages/user/sop/index.vue'),
|
||||||
pageSource('../src/components/chat/ChatComposer.vue'),
|
pageSource('../src/components/chat/ChatComposer.vue'),
|
||||||
pageSource('../src/services/knowledge.ts'),
|
pageSource('../src/services/agent.ts'),
|
||||||
pageSource('../src/services/speech.ts'),
|
pageSource('../src/services/speech.ts'),
|
||||||
pageSource('../src/services/summary-card-image.ts')
|
pageSource('../src/services/summary-card-image.ts')
|
||||||
]);
|
]);
|
||||||
@@ -492,11 +492,11 @@ test('问师傅支持现场媒体、答案播报和总结卡图片下载', async
|
|||||||
assert.match(source, /@pick-media="pickMedia"/);
|
assert.match(source, /@pick-media="pickMedia"/);
|
||||||
assert.match(composerSource, /图片\/视频/);
|
assert.match(composerSource, /图片\/视频/);
|
||||||
assert.match(composerSource, /仅用于本次现场分析/);
|
assert.match(composerSource, /仅用于本次现场分析/);
|
||||||
assert.match(source, /searchKnowledgeWithMedia/);
|
assert.match(source, /askAgentWithMedia/);
|
||||||
assert.match(source, /toggleAnswerSpeech/);
|
assert.match(source, /toggleAnswerSpeech/);
|
||||||
assert.match(source, /toggleSummarySpeech/);
|
assert.match(source, /toggleSummarySpeech/);
|
||||||
assert.match(source, /downloadSummaryCardImage/);
|
assert.match(source, /downloadSummaryCardImage/);
|
||||||
assert.match(knowledgeService, /\/api\/knowledge\/query-media/);
|
assert.match(agentService, /\/api\/aihr\/agent\/messages\/media/);
|
||||||
assert.match(speechService, /\/api\/ai\/tts/);
|
assert.match(speechService, /\/api\/ai\/tts/);
|
||||||
assert.match(speechService, /SpeechSynthesisUtterance/);
|
assert.match(speechService, /SpeechSynthesisUtterance/);
|
||||||
assert.match(speechService, /playWithBrowserSpeech/);
|
assert.match(speechService, /playWithBrowserSpeech/);
|
||||||
@@ -580,7 +580,7 @@ test('播报复用已合成音频且总结图按内容高度生成并及时释
|
|||||||
|
|
||||||
assert.match(speechService, /const sourceCache = new Map<string, string>\(\)/);
|
assert.match(speechService, /const sourceCache = new Map<string, string>\(\)/);
|
||||||
assert.match(speechService, /const maxCachedSources = 4/);
|
assert.match(speechService, /const maxCachedSources = 4/);
|
||||||
assert.match(speechService, /const sourceKey = cacheKey\(value, profile, practiceContext\)/);
|
assert.match(speechService, /const sourceKey = cacheKey\(value, effectiveProfile, practiceContext\)/);
|
||||||
assert.match(speechService, /const cachedSource = sourceCache\.get\(sourceKey\)/);
|
assert.match(speechService, /const cachedSource = sourceCache\.get\(sourceKey\)/);
|
||||||
assert.match(speechService, /sourceCache\.clear\(\)/);
|
assert.match(speechService, /sourceCache\.clear\(\)/);
|
||||||
assert.match(speechService, /registerAbort\?\.\(\(\) => task\.abort\(\)\)/);
|
assert.match(speechService, /registerAbort\?\.\(\(\) => task\.abort\(\)\)/);
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ test('个人助理客户端使用确认式记忆接口且确认请求携带稳
|
|||||||
const card = await read('../src/components/chat/MemoryCandidateCard.vue');
|
const card = await read('../src/components/chat/MemoryCandidateCard.vue');
|
||||||
|
|
||||||
assert.match(service, /\/api\/aihr\/personal-assistant\/memory-candidates/);
|
assert.match(service, /\/api\/aihr\/personal-assistant\/memory-candidates/);
|
||||||
assert.match(service, /\$\{candidateBase\}\/\$\{candidateId\}\/confirm/);
|
assert.match(service, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/confirm/);
|
||||||
assert.match(service, /\$\{candidateBase\}\/\$\{candidateId\}\/dismiss/);
|
assert.match(service, /\$\{candidateBase\}\/\$\{candidateRef\}\/confirm/);
|
||||||
|
assert.match(service, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/dismiss/);
|
||||||
|
assert.match(service, /\$\{candidateBase\}\/\$\{candidateRef\}\/dismiss/);
|
||||||
assert.match(service, /\/api\/aihr\/service-memories/);
|
assert.match(service, /\/api\/aihr\/service-memories/);
|
||||||
assert.match(service, /\/api\/aihr\/personal-assistant\/assistant-captures/);
|
assert.match(service, /\/api\/aihr\/personal-assistant\/assistant-captures/);
|
||||||
assert.match(card, /const idempotencyKey = ref\(createMemoryIdempotencyKey\(props\.candidate\.id\)\)/);
|
assert.match(card, /const idempotencyKey = ref\(createMemoryIdempotencyKey\(props\.candidate\.id\)\)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user