feat(agent): add conservative intent planner
This commit is contained in:
+53
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+163
@@ -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();
|
||||
}
|
||||
}
|
||||
+149
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user