feat(agent): add conservative intent planner

This commit is contained in:
2026-07-24 20:08:28 +08:00
parent 36b5bc8564
commit ac6b14926a
3 changed files with 365 additions and 0 deletions
@@ -0,0 +1,53 @@
package org.dromara.aihr.agent;
public final class AihrAgentDto {
private AihrAgentDto() {
}
public enum Intent {
KNOWLEDGE_QA,
RESOURCE_DELIVERY,
LIVE_MY_WORK,
LIVE_TEAM_WORK,
PRACTICE_COACHING,
CAPTURE_FACT,
DRAFT_ACTION,
MEDIA_UNDERSTANDING,
WEB_RESEARCH,
CLARIFY,
SOCIAL
}
public enum Tool {
NONE,
KNOWLEDGE_SEARCH,
KNOWLEDGE_RESOURCE,
MY_CURRENT_TASKS,
MY_PRACTICE_SUMMARY,
TEAM_PRACTICE_SUMMARY,
CAPTURE_MEMORY_DRAFT,
MEDIA_ANALYZE,
WEB_RESEARCH
}
public enum ResponseStyle {
FACT,
GROUNDED,
DRAFT,
MEDIA,
WEB,
CLARIFICATION,
SOCIAL
}
public record AgentPlan(
Intent intent,
String rewrittenRequest,
Tool tool,
boolean requiresConfirmation,
boolean requiresExternalConsent,
ResponseStyle responseStyle
) {
}
}
@@ -0,0 +1,163 @@
package org.dromara.aihr.agent;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.aihr.agent.AihrAgentDto.AgentPlan;
import org.dromara.aihr.agent.AihrAgentDto.Intent;
import org.dromara.aihr.agent.AihrAgentDto.ResponseStyle;
import org.dromara.aihr.agent.AihrAgentDto.Tool;
import org.dromara.aihr.service.AihrModelSeedService;
import org.springframework.stereotype.Service;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@Service
public class AihrAgentPlanner {
private static final Set<String> PLAN_FIELDS = Set.of(
"intent", "rewrittenRequest", "tool", "requiresConfirmation", "requiresExternalConsent", "responseStyle"
);
private static final String PLANNER_PROMPT = """
你是企业员工助手的意图规划器。只输出一个 JSON 对象,字段必须且只能是:
intent, rewrittenRequest, tool, requiresConfirmation, requiresExternalConsent, responseStyle。
intent 可选 KNOWLEDGE_QA, RESOURCE_DELIVERY, LIVE_MY_WORK, LIVE_TEAM_WORK, PRACTICE_COACHING,
CAPTURE_FACT, DRAFT_ACTION, MEDIA_UNDERSTANDING, WEB_RESEARCH, CLARIFY, SOCIAL。
tool 可选 NONE, KNOWLEDGE_SEARCH, KNOWLEDGE_RESOURCE, MY_CURRENT_TASKS, MY_PRACTICE_SUMMARY,
TEAM_PRACTICE_SUMMARY, CAPTURE_MEMORY_DRAFT, MEDIA_ANALYZE, WEB_RESEARCH。
不得输出用户、员工、项目、SQL、URL 等标识或参数;不确定时返回 CLARIFY 与 NONE。
""";
private final AihrModelSeedService modelService;
private final ObjectMapper objectMapper;
public AihrAgentPlanner(AihrModelSeedService modelService, ObjectMapper objectMapper) {
this.modelService = modelService;
this.objectMapper = objectMapper;
}
public AgentPlan plan(String rawRequest, boolean hasMedia) {
String request = clean(rawRequest);
if (hasMedia) {
return plan(Intent.MEDIA_UNDERSTANDING, request, Tool.MEDIA_ANALYZE, false, false, ResponseStyle.MEDIA);
}
if (containsAny(request, "记一下", "帮我记", "记录一下", "保存一下")) {
return plan(Intent.CAPTURE_FACT, request, Tool.CAPTURE_MEMORY_DRAFT, true, false, ResponseStyle.DRAFT);
}
if (containsAny(request, "查全网", "上网查", "联网查", "网上查")) {
return plan(Intent.WEB_RESEARCH, request, Tool.WEB_RESEARCH, false, true, ResponseStyle.WEB);
}
if (containsAny(request, "原文件", "源文件", "原文", "附件", "文件发我", "发我文件", "下载文件")) {
return plan(Intent.RESOURCE_DELIVERY, request, Tool.KNOWLEDGE_RESOURCE, false, false,
ResponseStyle.GROUNDED);
}
if (containsAny(request, "我今天有什么待办", "我有什么待办", "我的待办", "我的任务",
"我今天该做什么", "我现在该做什么", "今天要做什么", "当前待办")) {
return plan(Intent.LIVE_MY_WORK, request, Tool.MY_CURRENT_TASKS, false, false, ResponseStyle.FACT);
}
if (isSocial(request)) {
return plan(Intent.SOCIAL, request, Tool.NONE, false, false, ResponseStyle.SOCIAL);
}
if (request.isBlank() || containsAny(request, "帮我处理一下", "帮我弄一下", "你看着办")) {
return plan(Intent.CLARIFY, request, Tool.NONE, false, false, ResponseStyle.CLARIFICATION);
}
if (containsAny(request.toLowerCase(Locale.ROOT), "sop", "怎么", "如何", "流程", "规定", "制度",
"标准", "操作", "处理", "进场", "办理", "应该")) {
return plan(Intent.KNOWLEDGE_QA, request, Tool.KNOWLEDGE_SEARCH, false, false,
ResponseStyle.GROUNDED);
}
return modelPlan(request).orElseGet(() ->
plan(Intent.CLARIFY, request, Tool.NONE, false, false, ResponseStyle.CLARIFICATION));
}
private Optional<AgentPlan> modelPlan(String request) {
if (modelService == null) {
return Optional.empty();
}
try {
Optional<String> result = modelService.tryChat(PLANNER_PROMPT, request, 0.0);
if (result.isEmpty()) {
return Optional.empty();
}
JsonNode node = objectMapper.readTree(result.get());
if (!node.isObject() || !node.has("intent") || !node.has("rewrittenRequest") || !node.has("tool")
|| !node.has("requiresConfirmation") || !node.has("requiresExternalConsent")
|| !node.has("responseStyle")) {
return Optional.empty();
}
var fields = node.fieldNames();
while (fields.hasNext()) {
if (!PLAN_FIELDS.contains(fields.next())) {
return Optional.empty();
}
}
String rewritten = clean(node.path("rewrittenRequest").asText());
if (rewritten.isBlank() || rewritten.length() > 600
|| !node.path("requiresConfirmation").isBoolean()
|| !node.path("requiresExternalConsent").isBoolean()) {
return Optional.empty();
}
AgentPlan candidate = plan(
Intent.valueOf(node.path("intent").asText()),
rewritten,
Tool.valueOf(node.path("tool").asText()),
node.path("requiresConfirmation").asBoolean(),
node.path("requiresExternalConsent").asBoolean(),
ResponseStyle.valueOf(node.path("responseStyle").asText())
);
return compatible(candidate) ? Optional.of(candidate) : Optional.empty();
} catch (Exception ignored) {
return Optional.empty();
}
}
private static boolean compatible(AgentPlan candidate) {
return switch (candidate.intent()) {
case KNOWLEDGE_QA -> candidate.tool() == Tool.KNOWLEDGE_SEARCH
&& candidate.responseStyle() == ResponseStyle.GROUNDED;
case RESOURCE_DELIVERY -> candidate.tool() == Tool.KNOWLEDGE_RESOURCE
&& candidate.responseStyle() == ResponseStyle.GROUNDED;
case LIVE_MY_WORK -> candidate.tool() == Tool.MY_CURRENT_TASKS
&& candidate.responseStyle() == ResponseStyle.FACT;
case LIVE_TEAM_WORK -> candidate.tool() == Tool.TEAM_PRACTICE_SUMMARY
&& candidate.responseStyle() == ResponseStyle.FACT;
case PRACTICE_COACHING -> candidate.tool() == Tool.MY_PRACTICE_SUMMARY
&& candidate.responseStyle() == ResponseStyle.FACT;
case CAPTURE_FACT -> candidate.tool() == Tool.CAPTURE_MEMORY_DRAFT
&& candidate.requiresConfirmation() && candidate.responseStyle() == ResponseStyle.DRAFT;
case DRAFT_ACTION -> candidate.tool() == Tool.NONE
&& candidate.requiresConfirmation() && candidate.responseStyle() == ResponseStyle.DRAFT;
case MEDIA_UNDERSTANDING -> candidate.tool() == Tool.MEDIA_ANALYZE
&& candidate.responseStyle() == ResponseStyle.MEDIA;
case WEB_RESEARCH -> candidate.tool() == Tool.WEB_RESEARCH
&& candidate.requiresExternalConsent() && candidate.responseStyle() == ResponseStyle.WEB;
case CLARIFY -> candidate.tool() == Tool.NONE
&& candidate.responseStyle() == ResponseStyle.CLARIFICATION;
case SOCIAL -> candidate.tool() == Tool.NONE && candidate.responseStyle() == ResponseStyle.SOCIAL;
};
}
private static AgentPlan plan(Intent intent, String request, Tool tool, boolean confirmation,
boolean externalConsent, ResponseStyle style) {
return new AgentPlan(intent, request, tool, confirmation, externalConsent, style);
}
private static boolean isSocial(String request) {
String value = clean(request).toLowerCase(Locale.ROOT);
return value.matches("^(你好|您好|嗨|hi|hello|在吗|谢谢|感谢)[!!。.??]*$");
}
private static boolean containsAny(String value, String... needles) {
for (String needle : needles) {
if (value.contains(needle)) {
return true;
}
}
return false;
}
private static String clean(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,149 @@
package org.dromara.aihr.agent;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.aihr.service.AihrModelSeedService;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.CAPTURE_FACT;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.CLARIFY;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.KNOWLEDGE_QA;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.LIVE_MY_WORK;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.MEDIA_UNDERSTANDING;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.RESOURCE_DELIVERY;
import static org.dromara.aihr.agent.AihrAgentDto.Intent.SOCIAL;
import static org.dromara.aihr.agent.AihrAgentDto.Tool.CAPTURE_MEMORY_DRAFT;
import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_RESOURCE;
import static org.dromara.aihr.agent.AihrAgentDto.Tool.KNOWLEDGE_SEARCH;
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MEDIA_ANALYZE;
import static org.dromara.aihr.agent.AihrAgentDto.Tool.MY_CURRENT_TASKS;
import static org.dromara.aihr.agent.AihrAgentDto.Tool.NONE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("dev")
class AihrAgentPlannerTest {
private final AihrAgentPlanner planner = new AihrAgentPlanner(null, new ObjectMapper());
@Test
void routesCurrentTasksToLiveDataInsteadOfKnowledgeSearch() {
var plan = planner.plan("我今天有什么待办", false);
assertEquals(LIVE_MY_WORK, plan.intent());
assertEquals(MY_CURRENT_TASKS, plan.tool());
assertFalse(plan.requiresConfirmation());
}
@Test
void routesExplicitCaptureToConfirmationDraft() {
var plan = planner.plan("记一下,3栋1201要回访", false);
assertEquals(CAPTURE_FACT, plan.intent());
assertEquals(CAPTURE_MEMORY_DRAFT, plan.tool());
assertTrue(plan.requiresConfirmation());
}
@Test
void routesGreetingWithoutCallingKnowledge() {
var plan = planner.plan("你好", false);
assertEquals(SOCIAL, plan.intent());
assertEquals(NONE, plan.tool());
}
@Test
void ambiguousActionAsksForClarification() {
var plan = planner.plan("帮我处理一下", false);
assertEquals(CLARIFY, plan.intent());
assertEquals(NONE, plan.tool());
}
@Test
void genericImageQuestionUsesMediaWithoutSopSearch() {
var plan = planner.plan("图中是什么宠物", true);
assertEquals(MEDIA_UNDERSTANDING, plan.intent());
assertEquals(MEDIA_ANALYZE, plan.tool());
}
@Test
void explicitSopQuestionUsesEnterpriseKnowledge() {
var plan = planner.plan("装修人员怎么进场?", false);
assertEquals(KNOWLEDGE_QA, plan.intent());
assertEquals(KNOWLEDGE_SEARCH, plan.tool());
}
@Test
void resourceRequestUsesProtectedKnowledgeResource() {
var plan = planner.plan("把刚才的原文件发我", false);
assertEquals(RESOURCE_DELIVERY, plan.intent());
assertEquals(KNOWLEDGE_RESOURCE, plan.tool());
}
@Test
void explicitWebRequestRequiresConsent() {
var plan = planner.plan("帮我查全网,国家对高空抛物怎么规定", false);
assertEquals(AihrAgentDto.Intent.WEB_RESEARCH, plan.intent());
assertEquals(AihrAgentDto.Tool.WEB_RESEARCH, plan.tool());
assertTrue(plan.requiresExternalConsent());
}
@Test
void validatedModelPlanCanRouteAnUnmatchedWorkRequest() {
AihrModelSeedService model = modelReturning("""
{
"intent": "LIVE_MY_WORK",
"rewrittenRequest": "查询当前认证员工今天的安排",
"tool": "MY_CURRENT_TASKS",
"requiresConfirmation": false,
"requiresExternalConsent": false,
"responseStyle": "FACT"
}
""");
var modelPlanner = new AihrAgentPlanner(model, new ObjectMapper());
var plan = modelPlanner.plan("看看我今天的安排", false);
assertEquals(LIVE_MY_WORK, plan.intent());
assertEquals(MY_CURRENT_TASKS, plan.tool());
assertEquals("查询当前认证员工今天的安排", plan.rewrittenRequest());
}
@Test
void modelPlanWithIdentityOrUnknownToolFailsClosed() {
AihrModelSeedService model = modelReturning("""
{
"intent": "LIVE_MY_WORK",
"rewrittenRequest": "查询其他员工任务",
"tool": "RAW_SQL",
"employeeId": "someone-else",
"requiresConfirmation": false,
"requiresExternalConsent": false,
"responseStyle": "FACT"
}
""");
var modelPlanner = new AihrAgentPlanner(model, new ObjectMapper());
var plan = modelPlanner.plan("看看那个人今天的安排", false);
assertEquals(CLARIFY, plan.intent());
assertEquals(NONE, plan.tool());
}
private static AihrModelSeedService modelReturning(String response) {
return new AihrModelSeedService(new ObjectMapper(), null) {
@Override
public Optional<String> tryChat(String systemPrompt, String userPrompt, double temperature) {
return Optional.of(response);
}
};
}
}