diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java index 03025097..ac164036 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java @@ -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.AihrKnowledgeQueryService; 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.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -32,19 +34,26 @@ public class AihrAgentOrchestrator { private final AihrKnowledgePrincipalResolver principalResolver; private final AihrKnowledgeQueryService queryService; private final AihrAgentActionService actionService; + private final AihrWebAiService webAiService; public AgentResponse handle(AgentRequest request) { requireQuestion(request); 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()) { case SOCIAL -> simple(plan, AgentStatus.COMPLETED, "你好,我是数字师傅。你可以问制度、待办、训练或现场问题。", null); case CLARIFY, DRAFT_ACTION, MEDIA_UNDERSTANDING -> simple(plan, AgentStatus.NEEDS_INPUT, "请再说明你想查询、记录或处理什么。", new Clarification("请补充具体目标", List.of("具体问题"))); - case WEB_RESEARCH -> simple(plan, AgentStatus.NEEDS_INPUT, - "这项问题需要使用独立的全网查询能力。", new Clarification("请确认是否查询全网", List.of())); + case WEB_RESEARCH -> fromWeb(plan, request, + webAiService.query(new AihrWebAiDto.QueryRequest(plan.rewrittenRequest(), true))); 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) { if (request == null || request.question() == null || request.question().isBlank()) { throw new ServiceException("问题不能为空", 400); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentMediaTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentMediaTest.java index 62539124..c6745726 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentMediaTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentMediaTest.java @@ -54,6 +54,7 @@ class AihrAgentMediaTest { } }, query, + null, null ); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java index 9e18f220..bbfa9c99 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java @@ -110,7 +110,8 @@ class AihrAgentOrchestratorTest { } }, query, - actionService + actionService, + null ); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentWebResearchTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentWebResearchTest.java new file mode 100644 index 00000000..6748a639 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentWebResearchTest.java @@ -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 + ); + } + } +}