LobsterProactiveGreetingService.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package com.fs.company.service.workflow.execution;
  2. import com.fs.company.domain.CompanyWorkflowLobsterNode;
  3. import com.fs.company.service.llm.MultiModelRouter;
  4. import com.fs.company.service.workflow.VariableSubstitutionEngine;
  5. import com.fs.company.service.workflow.prompt.EnginePromptHelper;
  6. import com.fs.company.service.workflow.prompt.LobsterPromptGuardrails;
  7. import com.fs.company.service.workflow.prompt.LobsterEnginePromptCatalog;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.beans.factory.annotation.Value;
  12. import org.springframework.stereotype.Service;
  13. import java.util.Map;
  14. /**
  15. * 主动问候:客户未开口时 START/GREET 节点优先租户配置话术,否则 100% AI 生成;含 cooldown + 去重。
  16. */
  17. @Service
  18. public class LobsterProactiveGreetingService {
  19. private static final Logger log = LoggerFactory.getLogger(LobsterProactiveGreetingService.class);
  20. static final String VAR_LAST_GREET_MS = "_lastProactiveGreetMs";
  21. static final String VAR_GREET_COUNT = "_proactiveGreetCount";
  22. static final String VAR_LAST_GREET_TEXT = "_lastProactiveGreetText";
  23. @Autowired(required = false)
  24. private MultiModelRouter multiModelRouter;
  25. @Autowired(required = false)
  26. private EnginePromptHelper enginePrompts;
  27. @Autowired(required = false)
  28. private VariableSubstitutionEngine varEngine;
  29. @Value("${lobster.proactive-greet.cooldown-hours:24}")
  30. private int cooldownHours;
  31. @Value("${lobster.proactive-greet.max-without-reply:2}")
  32. private int maxWithoutReply;
  33. @Value("${lobster.proactive-greet.ai-enabled:true}")
  34. private boolean aiEnabled;
  35. public boolean isProactiveContext(Map<String, Object> variables) {
  36. if (variables == null) {
  37. return false;
  38. }
  39. if (Boolean.TRUE.equals(variables.get("_proactiveOutbound"))) {
  40. return true;
  41. }
  42. Object trigger = variables.get("triggerSource");
  43. if (trigger != null) {
  44. String src = trigger.toString();
  45. return "proactive_task".equals(src) || "binding_provision".equals(src)
  46. || "scheduler".equals(src) || "tag_bind_or_hook".equals(src);
  47. }
  48. return false;
  49. }
  50. public boolean isOpeningGreetNode(CompanyWorkflowLobsterNode node) {
  51. if (node == null) {
  52. return false;
  53. }
  54. if (node.getNodeType() != null && node.getNodeType() == 1) {
  55. return true;
  56. }
  57. String code = node.getNodeCode();
  58. return code != null && ("START".equalsIgnoreCase(code) || "GREET".equalsIgnoreCase(code));
  59. }
  60. /**
  61. * START/GREET 开场:有配置话术则渲染后投递(可 AI 变体),无配置则 100% AI 生成。
  62. */
  63. public String resolveOpeningGreeting(Long companyId, Long instanceId, CompanyWorkflowLobsterNode node,
  64. Map<String, Object> variables) {
  65. if (LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) {
  66. return "";
  67. }
  68. String configuredRaw = LobsterMessageTemplateRenderer.extractConfiguredOpeningScript(node, variables);
  69. if (!configuredRaw.isEmpty()) {
  70. String rendered = LobsterMessageTemplateRenderer.render(configuredRaw, variables, varEngine);
  71. log.debug("[ProactiveGreet] opening use configured script, node={}, instanceId={}",
  72. node != null ? node.getNodeCode() : null, instanceId);
  73. return resolveConfiguredScriptDelivery(companyId, instanceId, variables, rendered);
  74. }
  75. log.debug("[ProactiveGreet] opening no script configured, use AI, node={}, instanceId={}",
  76. node != null ? node.getNodeCode() : null, instanceId);
  77. return resolveAiOpeningGreeting(companyId, instanceId, variables, "");
  78. }
  79. /**
  80. * 非 START 开场节点:主动任务场景下对配置话术做 cooldown/去重处理。
  81. */
  82. public String resolveForDelivery(Long companyId, Long instanceId, CompanyWorkflowLobsterNode node,
  83. Map<String, Object> variables, String renderedTemplate,
  84. boolean openingNode) {
  85. if (renderedTemplate == null || renderedTemplate.isBlank()) {
  86. if (openingNode && !LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) {
  87. return resolveOpeningGreeting(companyId, instanceId, node, variables);
  88. }
  89. return renderedTemplate;
  90. }
  91. if (LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) {
  92. return renderedTemplate;
  93. }
  94. if (openingNode) {
  95. return resolveConfiguredScriptDelivery(companyId, instanceId, variables, renderedTemplate);
  96. }
  97. boolean proactive = isProactiveContext(variables);
  98. if (!proactive) {
  99. return renderedTemplate;
  100. }
  101. return resolveConfiguredScriptDelivery(companyId, instanceId, variables, renderedTemplate);
  102. }
  103. private String resolveAiOpeningGreeting(Long companyId, Long instanceId,
  104. Map<String, Object> variables, String templateHint) {
  105. if (!aiEnabled || multiModelRouter == null) {
  106. log.warn("[ProactiveGreet] opening AI disabled or router unavailable, instanceId={}", instanceId);
  107. return null;
  108. }
  109. int sentCount = greetCount(variables);
  110. long lastMs = lastGreetMs(variables);
  111. long now = System.currentTimeMillis();
  112. long cooldownMs = Math.max(1, cooldownHours) * 3600_000L;
  113. if (sentCount >= maxWithoutReply && now - lastMs < cooldownMs) {
  114. log.info("[ProactiveGreet] opening skip: max {} without reply, instanceId={}",
  115. maxWithoutReply, instanceId);
  116. return null;
  117. }
  118. String lastText = lastGreetText(variables);
  119. String ai = generateAiGreeting(companyId, variables, templateHint, sentCount);
  120. if (ai == null || ai.isBlank()) {
  121. log.warn("[ProactiveGreet] opening AI empty, instanceId={}", instanceId);
  122. return null;
  123. }
  124. ai = ai.trim();
  125. if (lastText != null && isSameGreeting(ai, lastText) && now - lastMs < cooldownMs) {
  126. log.info("[ProactiveGreet] opening AI duplicate within cooldown, instanceId={}", instanceId);
  127. return null;
  128. }
  129. recordSent(variables, ai, now);
  130. LobsterOutboundQualityGate.markAiGenerated(variables, "ai_opening");
  131. return ai;
  132. }
  133. /** 配置话术投递:cooldown/去重;必要时 AI 变体,租户脚本跳过 quality gate。 */
  134. private String resolveConfiguredScriptDelivery(Long companyId, Long instanceId,
  135. Map<String, Object> variables, String renderedTemplate) {
  136. int sentCount = greetCount(variables);
  137. long lastMs = lastGreetMs(variables);
  138. long now = System.currentTimeMillis();
  139. long cooldownMs = Math.max(1, cooldownHours) * 3600_000L;
  140. if (sentCount >= maxWithoutReply && now - lastMs < cooldownMs) {
  141. log.info("[ProactiveGreet] skip: already sent {} without reply, instanceId={}", sentCount, instanceId);
  142. return null;
  143. }
  144. String lastText = lastGreetText(variables);
  145. if (lastText != null && isSameGreeting(lastText, renderedTemplate) && now - lastMs < cooldownMs) {
  146. if (aiEnabled && multiModelRouter != null) {
  147. String varied = generateAiGreeting(companyId, variables, renderedTemplate, sentCount);
  148. if (varied != null && !varied.isBlank() && !isSameGreeting(varied, lastText)) {
  149. recordSent(variables, varied.trim(), now);
  150. LobsterOutboundQualityGate.markAiGenerated(variables, "ai_opening_varied");
  151. return varied.trim();
  152. }
  153. }
  154. log.info("[ProactiveGreet] skip duplicate script within cooldown, instanceId={}", instanceId);
  155. return null;
  156. }
  157. recordSent(variables, renderedTemplate, now);
  158. LobsterOutboundQualityGate.markTenantConfiguredScript(variables);
  159. return renderedTemplate;
  160. }
  161. private String generateAiGreeting(Long companyId, Map<String, Object> variables,
  162. String templateHint, int priorCount) {
  163. try {
  164. Map<String, String> brands = LobsterMessageTemplateRenderer.brandMapFromVariables(variables);
  165. String customerName = variables.get("customerName") != null
  166. ? variables.get("customerName").toString() : "您";
  167. String nodeCode = variables.get("_greetNodeCode") != null
  168. ? variables.get("_greetNodeCode").toString() : "START";
  169. String prompt = enginePrompts != null
  170. ? enginePrompts.content("proactive_silent_greeting", companyId,
  171. EnginePromptHelper.vars(
  172. "customerName", customerName,
  173. "companyShortName", brands.getOrDefault("companyShortName", "我们公司"),
  174. "advisorName", brands.getOrDefault("advisorName", "专属顾问"),
  175. "mainProduct", brands.getOrDefault("mainProduct", "产品服务"),
  176. "nodeCode", nodeCode,
  177. "referenceTemplate", templateHint != null ? templateHint : "",
  178. "priorCount", String.valueOf(priorCount)))
  179. : LobsterEnginePromptCatalog.defaultContent("proactive_silent_greeting");
  180. String reply = multiModelRouter.generateResponse(prompt, null, "proactive_silent_greeting");
  181. return sanitizeAiGreeting(reply);
  182. } catch (Exception e) {
  183. log.debug("[ProactiveGreet] AI greeting failed: {}", e.getMessage());
  184. return null;
  185. }
  186. }
  187. private static String sanitizeAiGreeting(String reply) {
  188. if (reply == null || reply.isBlank()) {
  189. return null;
  190. }
  191. String trimmed = reply.trim();
  192. if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length() > 2) {
  193. trimmed = trimmed.substring(1, trimmed.length() - 1);
  194. }
  195. trimmed = LobsterPromptGuardrails.finalizeCustomerFacingReply(trimmed, trimmed);
  196. return trimmed != null && !trimmed.isBlank() ? trimmed.trim() : null;
  197. }
  198. private static void recordSent(Map<String, Object> variables, String message, long now) {
  199. if (variables == null) {
  200. return;
  201. }
  202. variables.put(VAR_LAST_GREET_MS, now);
  203. variables.put(VAR_LAST_GREET_TEXT, message);
  204. variables.put(VAR_GREET_COUNT, greetCount(variables) + 1);
  205. }
  206. private static int greetCount(Map<String, Object> variables) {
  207. if (variables == null) {
  208. return 0;
  209. }
  210. Object raw = variables.get(VAR_GREET_COUNT);
  211. return raw instanceof Number ? ((Number) raw).intValue() : 0;
  212. }
  213. private static long lastGreetMs(Map<String, Object> variables) {
  214. if (variables == null) {
  215. return 0L;
  216. }
  217. Object raw = variables.get(VAR_LAST_GREET_MS);
  218. return raw instanceof Number ? ((Number) raw).longValue() : 0L;
  219. }
  220. private static String lastGreetText(Map<String, Object> variables) {
  221. if (variables == null) {
  222. return null;
  223. }
  224. Object raw = variables.get(VAR_LAST_GREET_TEXT);
  225. return raw != null ? raw.toString() : null;
  226. }
  227. private static boolean isSameGreeting(String a, String b) {
  228. if (a == null || b == null) {
  229. return false;
  230. }
  231. return normalize(a).equals(normalize(b));
  232. }
  233. private static String normalize(String text) {
  234. return text.replaceAll("\\s+", "")
  235. .replace("${customerName}", "您")
  236. .replace("${customer_name}", "您");
  237. }
  238. }