feat(agent): require web research consent
This commit is contained in:
+34
-3
@@ -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);
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@ class AihrAgentMediaTest {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -110,7 +110,8 @@ class AihrAgentOrchestratorTest {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
actionService
|
actionService,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+97
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user