feat(agent): audit orchestrated runs
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
package org.dromara.aihr.agent;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentPlan;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentResponse;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AihrAgentAuditService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public void record(AihrKnowledgePrincipal principal, AgentPlan plan, AgentResponse response,
|
||||
long durationMs, String errorCode) {
|
||||
if (principal == null || plan == null || response == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String sourceType = response.sourceSummary().isEmpty()
|
||||
? null
|
||||
: clean(response.sourceSummary().get(0).type(), 32);
|
||||
String resultRef = response.actionDraft() == null
|
||||
? null
|
||||
: clean("DRAFT:" + response.actionDraft().type(), 100);
|
||||
jdbcTemplate.update("""
|
||||
insert into aihr_agent_run
|
||||
(run_id, tenant_id, client_key, user_id, conversation_id, context_version,
|
||||
intent, tool, status, source_type, duration_ms, error_code, result_ref, create_time)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now())
|
||||
""",
|
||||
clean(response.runId(), 64), clean(principal.tenantId(), 20),
|
||||
clean(principal.clientKey(), 64), principal.userId(),
|
||||
clean(response.conversationId(), 64), response.contextVersion(),
|
||||
plan.intent().name(), plan.tool().name(), response.status().name(), sourceType,
|
||||
Math.max(0, durationMs), safeCode(errorCode), resultRef
|
||||
);
|
||||
} catch (RuntimeException ignored) {
|
||||
log.warn("agent audit write failed (details hidden)");
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeCode(String value) {
|
||||
String code = clean(value, 32);
|
||||
return code != null && code.matches("[A-Za-z0-9_-]+") ? code : null;
|
||||
}
|
||||
|
||||
private static String clean(String value, int maxLength) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String cleaned = value.trim();
|
||||
return cleaned.length() <= maxLength ? cleaned : cleaned.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
+56
-10
@@ -9,6 +9,7 @@ import org.dromara.aihr.agent.AihrAgentDto.ActionDraft;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Clarification;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Intent;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.SourceSummary;
|
||||
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.AihrKnowledgeDataToolService.CurrentTaskSummary;
|
||||
@@ -35,11 +36,51 @@ public class AihrAgentOrchestrator {
|
||||
private final AihrKnowledgeQueryService queryService;
|
||||
private final AihrAgentActionService actionService;
|
||||
private final AihrWebAiService webAiService;
|
||||
private final AihrAgentAuditService auditService;
|
||||
|
||||
public AgentResponse handle(AgentRequest request) {
|
||||
requireQuestion(request);
|
||||
AgentPlan plan = planner.plan(request.question(), false);
|
||||
var principal = principalResolver.current();
|
||||
return execute(request, null, false);
|
||||
}
|
||||
|
||||
public AgentResponse handleMedia(AgentRequest request, MultipartFile file) {
|
||||
return execute(request, file, true);
|
||||
}
|
||||
|
||||
private AgentResponse execute(AgentRequest request, MultipartFile file, boolean media) {
|
||||
long started = System.nanoTime();
|
||||
AgentPlan plan = null;
|
||||
AihrKnowledgePrincipal principal = null;
|
||||
AgentResponse response = null;
|
||||
String errorCode = null;
|
||||
try {
|
||||
requireQuestion(request);
|
||||
if (media && (file == null || file.isEmpty())) {
|
||||
throw new ServiceException("附件不能为空", 400);
|
||||
}
|
||||
plan = planner.plan(request.question(), media);
|
||||
principal = principalResolver.current();
|
||||
response = media
|
||||
? executeMedia(plan, request, file, principal)
|
||||
: executeText(plan, request, principal);
|
||||
return response;
|
||||
} catch (ServiceException ex) {
|
||||
errorCode = ex.getCode() == null ? "REJECTED" : "HTTP_" + ex.getCode();
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
errorCode = "INTERNAL";
|
||||
throw ex;
|
||||
} finally {
|
||||
if (auditService != null && plan != null && principal != null) {
|
||||
AgentResponse audited = response == null
|
||||
? failed(plan, request, errorCode)
|
||||
: response;
|
||||
auditService.record(principal, plan, audited,
|
||||
(System.nanoTime() - started) / 1_000_000L, errorCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AgentResponse executeText(AgentPlan plan, AgentRequest request, AihrKnowledgePrincipal principal) {
|
||||
if (plan.requiresExternalConsent() && !request.externalConsent()) {
|
||||
return simple(plan, AgentStatus.NEEDS_INPUT,
|
||||
"全网查询会将脱敏后的问题发送给外部公开检索服务。",
|
||||
@@ -58,13 +99,9 @@ public class AihrAgentOrchestrator {
|
||||
};
|
||||
}
|
||||
|
||||
public AgentResponse handleMedia(AgentRequest request, MultipartFile file) {
|
||||
requireQuestion(request);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("附件不能为空", 400);
|
||||
}
|
||||
AgentPlan plan = planner.plan(request.question(), true);
|
||||
policy.authorize(principalResolver.current(), plan, request.externalConsent());
|
||||
private AgentResponse executeMedia(AgentPlan plan, AgentRequest request, MultipartFile file,
|
||||
AihrKnowledgePrincipal principal) {
|
||||
policy.authorize(principal, plan, request.externalConsent());
|
||||
MediaMode mode = needsKnowledge(request.question())
|
||||
? MediaMode.MEDIA_WITH_KNOWLEDGE
|
||||
: MediaMode.MEDIA_ONLY;
|
||||
@@ -157,6 +194,15 @@ public class AihrAgentOrchestrator {
|
||||
);
|
||||
}
|
||||
|
||||
private static AgentResponse failed(AgentPlan plan, AgentRequest request, String errorCode) {
|
||||
AgentStatus status = "HTTP_403".equals(errorCode) ? AgentStatus.FORBIDDEN : AgentStatus.FAILED;
|
||||
return new AgentResponse(
|
||||
runId(), request == null ? null : request.conversationId(),
|
||||
request == null ? null : request.contextVersion(), plan.intent(), status, "",
|
||||
List.of(), List.of(), List.of(), null, null, null, List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private static String runId() {
|
||||
return "agent_run_" + UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package org.dromara.aihr.agent;
|
||||
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentPlan;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentResponse;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentStatus;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Intent;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.ResponseStyle;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.SourceSummary;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Tool;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrAgentAuditServiceTest {
|
||||
|
||||
@Test
|
||||
void auditStoresOnlyMinimalMetadataWithoutQuestionOrAnswer() {
|
||||
var jdbc = new CapturingJdbcTemplate(false);
|
||||
var service = new AihrAgentAuditService(jdbc);
|
||||
|
||||
service.record(principal(), plan(), response(), 23L, null);
|
||||
|
||||
String stored = jdbc.sql + Arrays.toString(jdbc.args);
|
||||
assertTrue(stored.contains("agent_run_test"));
|
||||
assertTrue(stored.contains("MY_CURRENT_TASKS"));
|
||||
assertFalse(stored.contains("敏感问题"));
|
||||
assertFalse(stored.contains("敏感答案"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditFailureNeverBreaksAValidResponse() {
|
||||
var service = new AihrAgentAuditService(new CapturingJdbcTemplate(true));
|
||||
|
||||
assertDoesNotThrow(() -> service.record(principal(), plan(), response(), 23L, "INTERNAL"));
|
||||
}
|
||||
|
||||
private static AihrKnowledgePrincipal principal() {
|
||||
return new AihrKnowledgePrincipal("000000", 1L, "app_user", "employee-1",
|
||||
Set.of("employee"), Set.of("P1"), "mobile");
|
||||
}
|
||||
|
||||
private static AgentPlan plan() {
|
||||
return new AgentPlan(Intent.LIVE_MY_WORK, "敏感问题", Tool.MY_CURRENT_TASKS,
|
||||
false, false, ResponseStyle.FACT);
|
||||
}
|
||||
|
||||
private static AgentResponse response() {
|
||||
return new AgentResponse("agent_run_test", "conversation_1", 1L, Intent.LIVE_MY_WORK,
|
||||
AgentStatus.COMPLETED, "敏感答案",
|
||||
List.of(new SourceSummary("LIVE_DATA", "我的待办", "2026-07-24T20:00:00+08:00")),
|
||||
List.of(), List.of(), null, null, null, List.of());
|
||||
}
|
||||
|
||||
private static final class CapturingJdbcTemplate extends JdbcTemplate {
|
||||
private final boolean fail;
|
||||
private String sql;
|
||||
private Object[] args;
|
||||
|
||||
private CapturingJdbcTemplate(boolean fail) {
|
||||
this.fail = fail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(String sql, Object... args) {
|
||||
if (fail) {
|
||||
throw new IllegalStateException("database unavailable");
|
||||
}
|
||||
this.sql = sql;
|
||||
this.args = args;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -55,6 +55,7 @@ class AihrAgentMediaTest {
|
||||
},
|
||||
query,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -111,6 +111,7 @@ class AihrAgentOrchestratorTest {
|
||||
},
|
||||
query,
|
||||
actionService,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -72,7 +72,8 @@ class AihrAgentWebResearchTest {
|
||||
}
|
||||
},
|
||||
null,
|
||||
web
|
||||
web,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user