feat(agent): require web research consent

This commit is contained in:
2026-07-24 20:37:51 +08:00
parent 6f8c549752
commit 434bab6fed
4 changed files with 134 additions and 4 deletions
@@ -15,6 +15,8 @@ import org.dromara.aihr.knowledge.service.AihrKnowledgeDataToolService.CurrentTa
import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver; import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver;
import org.dromara.aihr.knowledge.service.AihrKnowledgeQueryService; import org.dromara.aihr.knowledge.service.AihrKnowledgeQueryService;
import org.dromara.aihr.knowledge.service.AihrKnowledgeQueryService.MediaMode; import org.dromara.aihr.knowledge.service.AihrKnowledgeQueryService.MediaMode;
import org.dromara.aihr.webai.AihrWebAiDto;
import org.dromara.aihr.webai.AihrWebAiService;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -32,19 +34,26 @@ public class AihrAgentOrchestrator {
private final AihrKnowledgePrincipalResolver principalResolver; private final AihrKnowledgePrincipalResolver principalResolver;
private final AihrKnowledgeQueryService queryService; private final AihrKnowledgeQueryService queryService;
private final AihrAgentActionService actionService; private final AihrAgentActionService actionService;
private final AihrWebAiService webAiService;
public AgentResponse handle(AgentRequest request) { public AgentResponse handle(AgentRequest request) {
requireQuestion(request); requireQuestion(request);
AgentPlan plan = planner.plan(request.question(), false); AgentPlan plan = planner.plan(request.question(), false);
policy.authorize(principalResolver.current(), plan, request.externalConsent()); var principal = principalResolver.current();
if (plan.requiresExternalConsent() && !request.externalConsent()) {
return simple(plan, AgentStatus.NEEDS_INPUT,
"全网查询会将脱敏后的问题发送给外部公开检索服务。",
new Clarification("是否同意查询全网?", List.of()));
}
policy.authorize(principal, plan, request.externalConsent());
return switch (plan.intent()) { return switch (plan.intent()) {
case SOCIAL -> simple(plan, AgentStatus.COMPLETED, "你好,我是数字师傅。你可以问制度、待办、训练或现场问题。", case SOCIAL -> simple(plan, AgentStatus.COMPLETED, "你好,我是数字师傅。你可以问制度、待办、训练或现场问题。",
null); null);
case CLARIFY, DRAFT_ACTION, MEDIA_UNDERSTANDING -> simple(plan, AgentStatus.NEEDS_INPUT, case CLARIFY, DRAFT_ACTION, MEDIA_UNDERSTANDING -> simple(plan, AgentStatus.NEEDS_INPUT,
"请再说明你想查询、记录或处理什么。", "请再说明你想查询、记录或处理什么。",
new Clarification("请补充具体目标", List.of("具体问题"))); new Clarification("请补充具体目标", List.of("具体问题")));
case WEB_RESEARCH -> simple(plan, AgentStatus.NEEDS_INPUT, case WEB_RESEARCH -> fromWeb(plan, request,
"这项问题需要使用独立的全网查询能力。", new Clarification("请确认是否查询全网", List.of())); webAiService.query(new AihrWebAiDto.QueryRequest(plan.rewrittenRequest(), true)));
default -> fromQuery(plan, request); default -> fromQuery(plan, request);
}; };
} }
@@ -99,6 +108,28 @@ public class AihrAgentOrchestrator {
); );
} }
private static AgentResponse fromWeb(AgentPlan plan, AgentRequest request,
AihrWebAiDto.QueryResponse response) {
AgentStatus status = switch (response.status()) {
case "OK" -> AgentStatus.COMPLETED;
case "NO_SOURCES", "NO_ANSWER" -> AgentStatus.NO_EVIDENCE;
case "CONSENT_REQUIRED" -> AgentStatus.NEEDS_INPUT;
default -> AgentStatus.UNAVAILABLE;
};
String answer = response.answer() == null || response.answer().isBlank()
? response.notice()
: response.answer();
return new AgentResponse(
runId(), request.conversationId(), request.contextVersion(), plan.intent(), status, answer,
List.of(new SourceSummary("WEB", response.provider(), OffsetDateTime.now().toString())),
List.of(), List.of(), response, null,
status == AgentStatus.NEEDS_INPUT
? new Clarification("是否同意查询全网?", List.of())
: null,
List.of()
);
}
private static void requireQuestion(AgentRequest request) { private static void requireQuestion(AgentRequest request) {
if (request == null || request.question() == null || request.question().isBlank()) { if (request == null || request.question() == null || request.question().isBlank()) {
throw new ServiceException("问题不能为空", 400); throw new ServiceException("问题不能为空", 400);
@@ -54,6 +54,7 @@ class AihrAgentMediaTest {
} }
}, },
query, query,
null,
null null
); );
} }
@@ -110,7 +110,8 @@ class AihrAgentOrchestratorTest {
} }
}, },
query, query,
actionService actionService,
null
); );
} }
@@ -0,0 +1,97 @@
package org.dromara.aihr.agent;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.aihr.agent.AihrAgentDto.AgentRequest;
import org.dromara.aihr.agent.AihrAgentDto.AgentStatus;
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryRequest;
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryResponse;
import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver;
import org.dromara.aihr.knowledge.service.AihrKnowledgeQueryService;
import org.dromara.aihr.webai.AihrWebAiDto;
import org.dromara.aihr.webai.AihrWebAiService;
import org.dromara.aihr.webai.AihrWebSearchClient;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("dev")
class AihrAgentWebResearchTest {
@Test
void missingConsentReturnsConsentCardWithoutExternalCall() {
var web = new FakeWebService();
var result = orchestrator(web).handle(request(false));
assertEquals(AgentStatus.NEEDS_INPUT, result.status());
assertEquals(0, web.calls);
assertTrue(result.citations().isEmpty());
}
@Test
void consentedResearchReturnsOnlySeparatedWebData() {
var web = new FakeWebService();
var result = orchestrator(web).handle(request(true));
assertEquals(AgentStatus.COMPLETED, result.status());
assertEquals(1, web.calls);
assertTrue(result.citations().isEmpty());
assertEquals("WEB", result.sourceSummary().get(0).type());
var data = (AihrWebAiDto.QueryResponse) result.data();
assertEquals("https://example.com/rule", data.sources().get(0).url());
}
private static AgentRequest request(boolean consent) {
return new AgentRequest("帮我查全网,国家对高空抛物怎么规定", "conversation_1", 0L,
"P1", null, consent);
}
private static AihrAgentOrchestrator orchestrator(FakeWebService web) {
var principal = new AihrKnowledgePrincipal("000000", 1L, "app_user", "employee-1",
Set.of("employee"), Set.of("P1"), "app");
return new AihrAgentOrchestrator(
new AihrAgentPlanner(null, new ObjectMapper()),
new AihrAgentPolicy(),
new AihrKnowledgePrincipalResolver(null) {
@Override
public AihrKnowledgePrincipal current() {
return principal;
}
},
new AihrKnowledgeQueryService(null, null, null, null, null, null, null, null, null) {
@Override
public QueryResponse queryInternal(QueryRequest request) {
throw new AssertionError("全网查询不得进入企业知识检索");
}
},
null,
web
);
}
private static final class FakeWebService extends AihrWebAiService {
private int calls;
private FakeWebService() {
super(null, null, null, null);
}
@Override
public AihrWebAiDto.QueryResponse query(AihrWebAiDto.QueryRequest request) {
calls++;
return new AihrWebAiDto.QueryResponse(
"web", "OK", "公开规定要求依法承担相应责任。",
List.of(new AihrWebSearchClient.WebSource(
"公开规定", "https://example.com/rule", "法规摘要", 0.9)),
"test-provider", "回答来自全网公开资料,不代表企业内部 SOP", true, true
);
}
}
}