| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- package com.fs.company.service.workflow.execution;
- import com.fs.company.domain.CompanyWorkflowLobsterNode;
- import com.fs.company.service.llm.MultiModelRouter;
- import com.fs.company.service.workflow.VariableSubstitutionEngine;
- import com.fs.company.service.workflow.prompt.EnginePromptHelper;
- import com.fs.company.service.workflow.prompt.LobsterPromptGuardrails;
- import com.fs.company.service.workflow.prompt.LobsterEnginePromptCatalog;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Service;
- import java.util.Map;
- /**
- * 主动问候:客户未开口时 START/GREET 节点优先租户配置话术,否则 100% AI 生成;含 cooldown + 去重。
- */
- @Service
- public class LobsterProactiveGreetingService {
- private static final Logger log = LoggerFactory.getLogger(LobsterProactiveGreetingService.class);
- static final String VAR_LAST_GREET_MS = "_lastProactiveGreetMs";
- static final String VAR_GREET_COUNT = "_proactiveGreetCount";
- static final String VAR_LAST_GREET_TEXT = "_lastProactiveGreetText";
- @Autowired(required = false)
- private MultiModelRouter multiModelRouter;
- @Autowired(required = false)
- private EnginePromptHelper enginePrompts;
- @Autowired(required = false)
- private VariableSubstitutionEngine varEngine;
- @Value("${lobster.proactive-greet.cooldown-hours:24}")
- private int cooldownHours;
- @Value("${lobster.proactive-greet.max-without-reply:2}")
- private int maxWithoutReply;
- @Value("${lobster.proactive-greet.ai-enabled:true}")
- private boolean aiEnabled;
- public boolean isProactiveContext(Map<String, Object> variables) {
- if (variables == null) {
- return false;
- }
- if (Boolean.TRUE.equals(variables.get("_proactiveOutbound"))) {
- return true;
- }
- Object trigger = variables.get("triggerSource");
- if (trigger != null) {
- String src = trigger.toString();
- return "proactive_task".equals(src) || "binding_provision".equals(src)
- || "scheduler".equals(src) || "tag_bind_or_hook".equals(src);
- }
- return false;
- }
- public boolean isOpeningGreetNode(CompanyWorkflowLobsterNode node) {
- if (node == null) {
- return false;
- }
- if (node.getNodeType() != null && node.getNodeType() == 1) {
- return true;
- }
- String code = node.getNodeCode();
- return code != null && ("START".equalsIgnoreCase(code) || "GREET".equalsIgnoreCase(code));
- }
- /**
- * START/GREET 开场:有配置话术则渲染后投递(可 AI 变体),无配置则 100% AI 生成。
- */
- public String resolveOpeningGreeting(Long companyId, Long instanceId, CompanyWorkflowLobsterNode node,
- Map<String, Object> variables) {
- if (LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) {
- return "";
- }
- String configuredRaw = LobsterMessageTemplateRenderer.extractConfiguredOpeningScript(node, variables);
- if (!configuredRaw.isEmpty()) {
- String rendered = LobsterMessageTemplateRenderer.render(configuredRaw, variables, varEngine);
- log.debug("[ProactiveGreet] opening use configured script, node={}, instanceId={}",
- node != null ? node.getNodeCode() : null, instanceId);
- return resolveConfiguredScriptDelivery(companyId, instanceId, variables, rendered);
- }
- log.debug("[ProactiveGreet] opening no script configured, use AI, node={}, instanceId={}",
- node != null ? node.getNodeCode() : null, instanceId);
- return resolveAiOpeningGreeting(companyId, instanceId, variables, "");
- }
- /**
- * 非 START 开场节点:主动任务场景下对配置话术做 cooldown/去重处理。
- */
- public String resolveForDelivery(Long companyId, Long instanceId, CompanyWorkflowLobsterNode node,
- Map<String, Object> variables, String renderedTemplate,
- boolean openingNode) {
- if (renderedTemplate == null || renderedTemplate.isBlank()) {
- if (openingNode && !LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) {
- return resolveOpeningGreeting(companyId, instanceId, node, variables);
- }
- return renderedTemplate;
- }
- if (LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) {
- return renderedTemplate;
- }
- if (openingNode) {
- return resolveConfiguredScriptDelivery(companyId, instanceId, variables, renderedTemplate);
- }
- boolean proactive = isProactiveContext(variables);
- if (!proactive) {
- return renderedTemplate;
- }
- return resolveConfiguredScriptDelivery(companyId, instanceId, variables, renderedTemplate);
- }
- private String resolveAiOpeningGreeting(Long companyId, Long instanceId,
- Map<String, Object> variables, String templateHint) {
- if (!aiEnabled || multiModelRouter == null) {
- log.warn("[ProactiveGreet] opening AI disabled or router unavailable, instanceId={}", instanceId);
- return null;
- }
- int sentCount = greetCount(variables);
- long lastMs = lastGreetMs(variables);
- long now = System.currentTimeMillis();
- long cooldownMs = Math.max(1, cooldownHours) * 3600_000L;
- if (sentCount >= maxWithoutReply && now - lastMs < cooldownMs) {
- log.info("[ProactiveGreet] opening skip: max {} without reply, instanceId={}",
- maxWithoutReply, instanceId);
- return null;
- }
- String lastText = lastGreetText(variables);
- String ai = generateAiGreeting(companyId, variables, templateHint, sentCount);
- if (ai == null || ai.isBlank()) {
- log.warn("[ProactiveGreet] opening AI empty, instanceId={}", instanceId);
- return null;
- }
- ai = ai.trim();
- if (lastText != null && isSameGreeting(ai, lastText) && now - lastMs < cooldownMs) {
- log.info("[ProactiveGreet] opening AI duplicate within cooldown, instanceId={}", instanceId);
- return null;
- }
- recordSent(variables, ai, now);
- LobsterOutboundQualityGate.markAiGenerated(variables, "ai_opening");
- return ai;
- }
- /** 配置话术投递:cooldown/去重;必要时 AI 变体,租户脚本跳过 quality gate。 */
- private String resolveConfiguredScriptDelivery(Long companyId, Long instanceId,
- Map<String, Object> variables, String renderedTemplate) {
- int sentCount = greetCount(variables);
- long lastMs = lastGreetMs(variables);
- long now = System.currentTimeMillis();
- long cooldownMs = Math.max(1, cooldownHours) * 3600_000L;
- if (sentCount >= maxWithoutReply && now - lastMs < cooldownMs) {
- log.info("[ProactiveGreet] skip: already sent {} without reply, instanceId={}", sentCount, instanceId);
- return null;
- }
- String lastText = lastGreetText(variables);
- if (lastText != null && isSameGreeting(lastText, renderedTemplate) && now - lastMs < cooldownMs) {
- if (aiEnabled && multiModelRouter != null) {
- String varied = generateAiGreeting(companyId, variables, renderedTemplate, sentCount);
- if (varied != null && !varied.isBlank() && !isSameGreeting(varied, lastText)) {
- recordSent(variables, varied.trim(), now);
- LobsterOutboundQualityGate.markAiGenerated(variables, "ai_opening_varied");
- return varied.trim();
- }
- }
- log.info("[ProactiveGreet] skip duplicate script within cooldown, instanceId={}", instanceId);
- return null;
- }
- recordSent(variables, renderedTemplate, now);
- LobsterOutboundQualityGate.markTenantConfiguredScript(variables);
- return renderedTemplate;
- }
- private String generateAiGreeting(Long companyId, Map<String, Object> variables,
- String templateHint, int priorCount) {
- try {
- Map<String, String> brands = LobsterMessageTemplateRenderer.brandMapFromVariables(variables);
- String customerName = variables.get("customerName") != null
- ? variables.get("customerName").toString() : "您";
- String nodeCode = variables.get("_greetNodeCode") != null
- ? variables.get("_greetNodeCode").toString() : "START";
- String prompt = enginePrompts != null
- ? enginePrompts.content("proactive_silent_greeting", companyId,
- EnginePromptHelper.vars(
- "customerName", customerName,
- "companyShortName", brands.getOrDefault("companyShortName", "我们公司"),
- "advisorName", brands.getOrDefault("advisorName", "专属顾问"),
- "mainProduct", brands.getOrDefault("mainProduct", "产品服务"),
- "nodeCode", nodeCode,
- "referenceTemplate", templateHint != null ? templateHint : "",
- "priorCount", String.valueOf(priorCount)))
- : LobsterEnginePromptCatalog.defaultContent("proactive_silent_greeting");
- String reply = multiModelRouter.generateResponse(prompt, null, "proactive_silent_greeting");
- return sanitizeAiGreeting(reply);
- } catch (Exception e) {
- log.debug("[ProactiveGreet] AI greeting failed: {}", e.getMessage());
- return null;
- }
- }
- private static String sanitizeAiGreeting(String reply) {
- if (reply == null || reply.isBlank()) {
- return null;
- }
- String trimmed = reply.trim();
- if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length() > 2) {
- trimmed = trimmed.substring(1, trimmed.length() - 1);
- }
- trimmed = LobsterPromptGuardrails.finalizeCustomerFacingReply(trimmed, trimmed);
- return trimmed != null && !trimmed.isBlank() ? trimmed.trim() : null;
- }
- private static void recordSent(Map<String, Object> variables, String message, long now) {
- if (variables == null) {
- return;
- }
- variables.put(VAR_LAST_GREET_MS, now);
- variables.put(VAR_LAST_GREET_TEXT, message);
- variables.put(VAR_GREET_COUNT, greetCount(variables) + 1);
- }
- private static int greetCount(Map<String, Object> variables) {
- if (variables == null) {
- return 0;
- }
- Object raw = variables.get(VAR_GREET_COUNT);
- return raw instanceof Number ? ((Number) raw).intValue() : 0;
- }
- private static long lastGreetMs(Map<String, Object> variables) {
- if (variables == null) {
- return 0L;
- }
- Object raw = variables.get(VAR_LAST_GREET_MS);
- return raw instanceof Number ? ((Number) raw).longValue() : 0L;
- }
- private static String lastGreetText(Map<String, Object> variables) {
- if (variables == null) {
- return null;
- }
- Object raw = variables.get(VAR_LAST_GREET_TEXT);
- return raw != null ? raw.toString() : null;
- }
- private static boolean isSameGreeting(String a, String b) {
- if (a == null || b == null) {
- return false;
- }
- return normalize(a).equals(normalize(b));
- }
- private static String normalize(String text) {
- return text.replaceAll("\\s+", "")
- .replace("${customerName}", "您")
- .replace("${customer_name}", "您");
- }
- }
|