|
|
@@ -1,12 +1,12 @@
|
|
|
package com.fs.company.service.workflow.learning.impl;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
-import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.fs.company.mapper.LobsterAuxiliaryMapper;
|
|
|
import com.fs.company.mapper.LobsterTenantLearningMapper;
|
|
|
import com.fs.company.service.llm.MultiModelRouter;
|
|
|
import com.fs.company.service.workflow.learning.*;
|
|
|
+import com.fs.company.service.workflow.cache.LobsterContextCacheService;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
@@ -20,6 +20,7 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(TenantLearningEngineImpl.class);
|
|
|
private static final int MIN_EVENTS_FOR_LEARNING = 10;
|
|
|
+ private static final double HIGH_CONFIDENCE = 0.75;
|
|
|
|
|
|
@Autowired(required = false)
|
|
|
private LobsterTenantLearningMapper learningMapper;
|
|
|
@@ -30,18 +31,64 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
@Autowired(required = false)
|
|
|
private LobsterAuxiliaryMapper auxMapper;
|
|
|
|
|
|
+ @Autowired(required = false)
|
|
|
+ private DistributedLearningServiceImpl distributedLearningService;
|
|
|
+
|
|
|
+ @Autowired(required = false)
|
|
|
+ private LobsterContextCacheService contextCache;
|
|
|
+
|
|
|
@Override
|
|
|
public void recordEvent(Long companyId, LearningEvent event) {
|
|
|
- if (learningMapper == null) return;
|
|
|
+ if (learningMapper == null || event == null) return;
|
|
|
try {
|
|
|
String vars = event.getVariables() != null ? JSON.toJSONString(event.getVariables()) : null;
|
|
|
+ String eventType = event.getOutcome() != null ? event.getOutcome() : "interaction";
|
|
|
+ Integer qualityScore = null;
|
|
|
+ if (event.getVariables() != null && event.getVariables().get("qualityScore") instanceof Number) {
|
|
|
+ qualityScore = ((Number) event.getVariables().get("qualityScore")).intValue();
|
|
|
+ }
|
|
|
learningMapper.insertEventLog(companyId, event.getInstanceId(), event.getNodeCode(),
|
|
|
- event.getOutcome(), null, vars);
|
|
|
+ eventType, qualityScore, vars);
|
|
|
} catch (Exception e) {
|
|
|
logger.error("[TenantLearning] 记录学习事件失败: companyId={}", companyId, e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void recordEvolutionInteraction(Long companyId, Long workflowId, Long instanceId,
|
|
|
+ String nodeCode, String customerMessage, String aiReply,
|
|
|
+ Integer qualityScore, String outcome,
|
|
|
+ Map<String, Object> variables) {
|
|
|
+ if (learningMapper == null || companyId == null) return;
|
|
|
+ try {
|
|
|
+ String eventType = outcome != null ? outcome : "interaction";
|
|
|
+ Map<String, Object> snapshot = new LinkedHashMap<>();
|
|
|
+ if (variables != null) snapshot.putAll(variables);
|
|
|
+ snapshot.put("workflowId", workflowId);
|
|
|
+ snapshot.put("qualityScore", qualityScore);
|
|
|
+ learningMapper.insertEventLog(companyId, instanceId, nodeCode, eventType,
|
|
|
+ qualityScore, JSON.toJSONString(snapshot));
|
|
|
+
|
|
|
+ if (customerMessage != null && aiReply != null && !aiReply.isEmpty()) {
|
|
|
+ learningMapper.insertReplayBuffer(companyId, instanceId, nodeCode,
|
|
|
+ customerMessage, aiReply, qualityScore);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 高质量样本即时沉淀为可复用模式(Hermes 式 Skill 雏形)
|
|
|
+ if (qualityScore != null && qualityScore >= 120 && aiReply != null && nodeCode != null) {
|
|
|
+ String skillKey = "skill_" + nodeCode;
|
|
|
+ String skillValue = "节点[" + nodeCode + "]优质话术样例: " + truncate(aiReply, 200);
|
|
|
+ learningMapper.upsertPattern(companyId, "skill", skillKey, skillValue,
|
|
|
+ Math.min(1.0, qualityScore / 160.0), "EvolutionReplay");
|
|
|
+ if (contextCache != null) {
|
|
|
+ contextCache.invalidateLearnedStrategies(companyId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[TenantLearning] recordEvolutionInteraction: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public LearningSummary triggerLearningCycle(Long companyId) {
|
|
|
long startTime = System.currentTimeMillis();
|
|
|
@@ -53,28 +100,36 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
summary.setEventsAnalyzed(eventCount);
|
|
|
if (eventCount < MIN_EVENTS_FOR_LEARNING) {
|
|
|
summary.setSummary("数据量不足(" + eventCount + "条),需要至少" + MIN_EVENTS_FOR_LEARNING + "条");
|
|
|
- // 数据不足也做一次轻量分析:至少统计当前模式
|
|
|
List<Map<String, Object>> existing = learningMapper.selectPatterns(companyId);
|
|
|
summary.setNewDiscoveries(existing != null ? existing.size() : 0);
|
|
|
summary.setUpdatedPatterns(0);
|
|
|
return summary;
|
|
|
}
|
|
|
+
|
|
|
+ int updatedBefore = countNonAppliedPatterns(companyId);
|
|
|
int newDiscoveries = 0;
|
|
|
- // 执行4维度分析
|
|
|
newDiscoveries += analyzeMessageEffectiveness(companyId);
|
|
|
newDiscoveries += analyzeTimingOptimization(companyId);
|
|
|
newDiscoveries += analyzeFlowBottlenecks(companyId);
|
|
|
newDiscoveries += analyzeCustomerProfileCorrelations(companyId);
|
|
|
- // 生成策略推荐
|
|
|
int suggestionsGenerated = generateStrategyRecommendations(companyId);
|
|
|
+ suggestionsGenerated += synthesizeSkillDocument(companyId);
|
|
|
|
|
|
+ int updatedAfter = countNonAppliedPatterns(companyId);
|
|
|
summary.setNewDiscoveries(newDiscoveries);
|
|
|
- summary.setUpdatedPatterns(0);
|
|
|
+ summary.setUpdatedPatterns(Math.max(0, updatedAfter - updatedBefore));
|
|
|
summary.setSuggestionsGenerated(suggestionsGenerated);
|
|
|
summary.setLearningDurationMs(System.currentTimeMillis() - startTime);
|
|
|
- summary.setSummary("学习周期完成: 分析" + eventCount + "条事件,发现" + newDiscoveries + "个新模式,生成" + suggestionsGenerated + "条策略推荐");
|
|
|
+ summary.setSummary("学习周期完成: 分析" + eventCount + "条事件,发现" + newDiscoveries
|
|
|
+ + "个新模式,更新" + summary.getUpdatedPatterns() + "条,生成" + suggestionsGenerated + "条策略");
|
|
|
+
|
|
|
+ contributeHighConfidencePatterns(companyId);
|
|
|
+ if (contextCache != null) {
|
|
|
+ contextCache.invalidateLearnedStrategies(companyId);
|
|
|
+ }
|
|
|
} catch (Exception e) {
|
|
|
logger.error("[TenantLearning] 学习周期执行失败: companyId={}", companyId, e);
|
|
|
+ summary.setSummary("学习周期失败: " + e.getMessage());
|
|
|
}
|
|
|
return summary;
|
|
|
}
|
|
|
@@ -86,15 +141,11 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
try {
|
|
|
List<Map<String, Object>> rows = learningMapper.selectPatterns(companyId);
|
|
|
for (Map<String, Object> row : rows) {
|
|
|
- LearningResult r = new LearningResult();
|
|
|
- r.setCompanyId(companyId);
|
|
|
- r.setTitle((String) row.get("pattern_key"));
|
|
|
- r.setDescription((String) row.get("pattern_value"));
|
|
|
- r.setConfidence(row.get("confidence") instanceof Number ? ((Number) row.get("confidence")).doubleValue() : 0.0);
|
|
|
- r.setStatus("discovered");
|
|
|
- results.add(r);
|
|
|
+ results.add(mapPatternRow(companyId, row));
|
|
|
}
|
|
|
- } catch (Exception e) { logger.error("[TenantLearning] 获取学习结果失败", e); }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("[TenantLearning] 获取学习结果失败", e);
|
|
|
+ }
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
@@ -103,49 +154,77 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
List<StrategyRecommendation> recommendations = new ArrayList<>();
|
|
|
if (learningMapper == null) return recommendations;
|
|
|
try {
|
|
|
- List<Map<String, Object>> rows = learningMapper.selectPatternsByScenario(companyId, scenario);
|
|
|
+ List<Map<String, Object>> rows = scenario != null && !scenario.isEmpty()
|
|
|
+ ? learningMapper.selectPatternsByScenario(companyId, scenario)
|
|
|
+ : learningMapper.selectPatterns(companyId);
|
|
|
for (Map<String, Object> row : rows) {
|
|
|
+ if (isAppliedPattern(row)) continue;
|
|
|
+ double conf = toDouble(row.get("confidence"));
|
|
|
+ if (conf < 0.5) continue;
|
|
|
StrategyRecommendation rec = new StrategyRecommendation();
|
|
|
rec.setCompanyId(companyId);
|
|
|
- rec.setScenario(scenario);
|
|
|
- rec.setStrategyContent((String) row.get("pattern_value"));
|
|
|
- rec.setConfidence(row.get("confidence") instanceof Number ? ((Number) row.get("confidence")).doubleValue() : 0.0);
|
|
|
+ rec.setScenario(scenario != null ? scenario : String.valueOf(row.get("pattern_type")));
|
|
|
+ rec.setStrategyContent(String.valueOf(row.get("pattern_value")));
|
|
|
+ rec.setConfidence(conf);
|
|
|
recommendations.add(rec);
|
|
|
+ if (recommendations.size() >= 5) break;
|
|
|
}
|
|
|
- } catch (Exception e) { logger.error("[TenantLearning] 策略推荐失败", e); }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("[TenantLearning] 策略推荐失败", e);
|
|
|
+ }
|
|
|
return recommendations;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public ApplyResult applyLearningResult(Long companyId, Long resultId, Map<String, Object> applyOptions) {
|
|
|
if (learningMapper == null) return ApplyResult.fail("学习引擎不可用");
|
|
|
+ if (resultId == null) return ApplyResult.fail("缺少学习结果ID");
|
|
|
try {
|
|
|
- List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
|
|
|
- if (patterns == null || patterns.isEmpty()) return ApplyResult.fail("无学习结果可应用");
|
|
|
- // 将学习模式写入 patch 表,由 WorkflowPatcher 统一落盘
|
|
|
+ Map<String, Object> pattern = learningMapper.selectPatternById(companyId, resultId);
|
|
|
+ if (pattern == null) return ApplyResult.fail("学习结果不存在");
|
|
|
+ if (isAppliedPattern(pattern)) {
|
|
|
+ return ApplyResult.success("该学习结果已应用", 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ String patternType = String.valueOf(pattern.getOrDefault("pattern_type", "strategy"));
|
|
|
+ String patternKey = String.valueOf(pattern.getOrDefault("pattern_key", "auto"));
|
|
|
+ String patternValue = String.valueOf(pattern.getOrDefault("pattern_value", ""));
|
|
|
+
|
|
|
if (auxMapper != null) {
|
|
|
- for (Map<String, Object> p : patterns) {
|
|
|
- auxMapper.insertPatch(companyId, "company_workflow_lobster_node", 0L,
|
|
|
- (String) p.getOrDefault("pattern_key", "auto"),
|
|
|
- "...", (String) p.getOrDefault("pattern_value", ""),
|
|
|
- "TenantLearning auto-apply");
|
|
|
- }
|
|
|
+ String fieldName = resolvePatchField(patternType);
|
|
|
+ auxMapper.insertPatch(companyId, "company_workflow_lobster_node", 0L,
|
|
|
+ fieldName, patternKey, patternValue,
|
|
|
+ "TenantLearning apply pattern#" + resultId);
|
|
|
}
|
|
|
- return ApplyResult.success("学习结果已提交为补丁,等待审核应用", patterns != null ? patterns.size() : 0);
|
|
|
+ learningMapper.markPatternApplied(companyId, resultId);
|
|
|
+ if (contextCache != null) {
|
|
|
+ contextCache.invalidateLearnedStrategies(companyId);
|
|
|
+ }
|
|
|
+ return ApplyResult.success("学习结果已提交为补丁,等待审核应用", 1);
|
|
|
} catch (Exception e) {
|
|
|
return ApplyResult.fail("应用失败: " + e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 每1小时触发一次学习周期:遍历活跃租户 */
|
|
|
+ /** 每1小时:遍历活跃租户触发学习周期 */
|
|
|
@Scheduled(cron = "0 0 */1 * * ?")
|
|
|
public void scheduledLearningCycle() {
|
|
|
- if (learningMapper == null || auxMapper == null) return;
|
|
|
+ if (learningMapper == null) return;
|
|
|
try {
|
|
|
- logger.debug("[TenantLearning] 定时学习周期触发");
|
|
|
- LearningSummary summary = triggerLearningCycle(0L); // 全租户分析
|
|
|
- logger.info("[TenantLearning] 学习周期完成: {}", summary.getSummary());
|
|
|
- } catch (Exception e) { logger.debug("[TenantLearning] 学习周期异常: {}", e.getMessage()); }
|
|
|
+ List<Long> tenants = loadActiveTenants();
|
|
|
+ int triggered = 0;
|
|
|
+ for (Long tenantId : tenants) {
|
|
|
+ if (tenantId == null || tenantId <= 0) continue;
|
|
|
+ LearningSummary summary = triggerLearningCycle(tenantId);
|
|
|
+ logger.info("[TenantLearning] 租户 {} 学习周期: {}", tenantId, summary.getSummary());
|
|
|
+ triggered++;
|
|
|
+ }
|
|
|
+ if (triggered == 0) {
|
|
|
+ logger.debug("[TenantLearning] 定时学习周期:无活跃租户");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[TenantLearning] 学习周期异常: {}", e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -158,8 +237,10 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
try {
|
|
|
int eventCount = learningMapper.countQualityEvents(companyId, null);
|
|
|
List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
|
|
|
+ Double avgQuality = learningMapper.selectAvgQualityScore(companyId);
|
|
|
metrics.put("eventCount", eventCount);
|
|
|
metrics.put("patternCount", patterns != null ? patterns.size() : 0);
|
|
|
+ metrics.put("avgQualityScore", avgQuality != null ? String.format("%.1f", avgQuality) : "-");
|
|
|
metrics.put("status", "active");
|
|
|
metrics.put("minEventsForLearning", MIN_EVENTS_FOR_LEARNING);
|
|
|
} catch (Exception e) {
|
|
|
@@ -176,7 +257,9 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
try {
|
|
|
learningMapper.upsertPattern(companyId, "corpus", report.getSummary(),
|
|
|
report.getSummary(), 0.7, "SalesCorpusAnalyzer");
|
|
|
- } catch (Exception e) { logger.debug("[TenantLearning] 语料导入失败: {}", e.getMessage()); }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[TenantLearning] 语料导入失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private int analyzeMessageEffectiveness(Long companyId) {
|
|
|
@@ -192,7 +275,7 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
? ((Number) event.get("quality_score")).intValue() : null;
|
|
|
String reply = (String) event.get("ai_reply");
|
|
|
if (reply == null || reply.isEmpty()) continue;
|
|
|
- String snippet = reply.length() > 40 ? reply.substring(0, 40) + "..." : reply;
|
|
|
+ String snippet = truncate(reply, 40);
|
|
|
if (score != null && score >= 120) {
|
|
|
high++;
|
|
|
if (highSamples.length() < 200) highSamples.append(snippet).append("; ");
|
|
|
@@ -214,7 +297,9 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
discoveries++;
|
|
|
}
|
|
|
return discoveries;
|
|
|
- } catch (Exception e) { return 0; }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private int analyzeTimingOptimization(Long companyId) {
|
|
|
@@ -225,21 +310,7 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
Map<Integer, Integer> hourCounts = new LinkedHashMap<>();
|
|
|
Map<Integer, Integer> hourHighQuality = new LinkedHashMap<>();
|
|
|
for (Map<String, Object> event : events) {
|
|
|
- Object ct = event.get("create_time");
|
|
|
- if (ct == null) continue;
|
|
|
- int hour = -1;
|
|
|
- if (ct instanceof java.sql.Timestamp) {
|
|
|
- hour = ((java.sql.Timestamp) ct).toLocalDateTime().getHour();
|
|
|
- } else if (ct instanceof java.util.Date) {
|
|
|
- java.util.Calendar cal = java.util.Calendar.getInstance();
|
|
|
- cal.setTime((java.util.Date) ct);
|
|
|
- hour = cal.get(java.util.Calendar.HOUR_OF_DAY);
|
|
|
- } else {
|
|
|
- String s = ct.toString();
|
|
|
- if (s.length() >= 13) {
|
|
|
- try { hour = Integer.parseInt(s.substring(11, 13)); } catch (Exception ignored) { }
|
|
|
- }
|
|
|
- }
|
|
|
+ int hour = parseHour(event.get("create_time"));
|
|
|
if (hour < 0) continue;
|
|
|
hourCounts.merge(hour, 1, Integer::sum);
|
|
|
Integer score = event.get("quality_score") instanceof Number
|
|
|
@@ -249,6 +320,8 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
}
|
|
|
}
|
|
|
int discoveries = 0;
|
|
|
+ int bestHour = -1;
|
|
|
+ double bestRate = 0;
|
|
|
for (Integer hour : hourCounts.keySet()) {
|
|
|
int total = hourCounts.get(hour);
|
|
|
if (total < 2) continue;
|
|
|
@@ -257,9 +330,21 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
String insight = hour + "点时段交互" + total + "次,高质量率" + String.format("%.0f%%", rate);
|
|
|
learningMapper.upsertPattern(companyId, "timing", "hour_" + hour, insight, rate / 100.0, "TimingAnalyzer");
|
|
|
discoveries++;
|
|
|
+ if (rate > bestRate) {
|
|
|
+ bestRate = rate;
|
|
|
+ bestHour = hour;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (bestHour >= 0 && bestRate >= 60) {
|
|
|
+ learningMapper.upsertPattern(companyId, "timing", "best_window",
|
|
|
+ "建议优先在 " + bestHour + ":00-" + (bestHour + 1) + ":00 触达客户(高质量率"
|
|
|
+ + String.format("%.0f%%", bestRate) + ")", bestRate / 100.0, "TimingAnalyzer");
|
|
|
+ discoveries++;
|
|
|
}
|
|
|
return discoveries;
|
|
|
- } catch (Exception e) { return 0; }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private int analyzeFlowBottlenecks(Long companyId) {
|
|
|
@@ -322,13 +407,14 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
}
|
|
|
}
|
|
|
return discoveries;
|
|
|
- } catch (Exception e) { return 0; }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private int analyzeCustomerProfileCorrelations(Long companyId) {
|
|
|
if (learningMapper == null) return 0;
|
|
|
try {
|
|
|
- // 从replay buffer中分析客户画像与对话效果的关联
|
|
|
List<Map<String, Object>> events = learningMapper.selectReplayBuffer(companyId);
|
|
|
if (events == null || events.isEmpty()) return 0;
|
|
|
|
|
|
@@ -340,9 +426,7 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
String customerMessage = (String) event.get("customer_message");
|
|
|
Integer qualityScore = event.get("quality_score") instanceof Number
|
|
|
? ((Number) event.get("quality_score")).intValue() : null;
|
|
|
-
|
|
|
if (customerMessage == null || customerMessage.isEmpty()) continue;
|
|
|
-
|
|
|
String profileTag = inferProfileTag(customerMessage);
|
|
|
profileStats.merge(profileTag, 1, Integer::sum);
|
|
|
if (qualityScore != null && qualityScore >= 120) {
|
|
|
@@ -360,12 +444,12 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
discoveries++;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
return discoveries;
|
|
|
- } catch (Exception e) { return 0; }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /** 从消息内容中推断客户画像标签 */
|
|
|
private String inferProfileTag(String message) {
|
|
|
if (message == null || message.isEmpty()) return "unknown";
|
|
|
if (message.contains("价格") || message.contains("优惠") || message.contains("便宜") || message.contains("贵"))
|
|
|
@@ -388,22 +472,186 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
|
|
|
try {
|
|
|
List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
|
|
|
if (patterns == null || patterns.isEmpty()) return 0;
|
|
|
- int discoveries = 0;
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
for (Map<String, Object> p : patterns) {
|
|
|
- Object conf = p.get("confidence");
|
|
|
- double c = conf instanceof Number ? ((Number) conf).doubleValue() : 0;
|
|
|
+ if (isAppliedPattern(p)) continue;
|
|
|
+ double c = toDouble(p.get("confidence"));
|
|
|
if (c >= 0.6) {
|
|
|
- sb.append("[").append(p.get("pattern_key")).append("] ")
|
|
|
+ sb.append("[").append(p.get("pattern_type")).append("/").append(p.get("pattern_key")).append("] ")
|
|
|
.append(p.get("pattern_value")).append("; ");
|
|
|
}
|
|
|
}
|
|
|
if (sb.length() > 0) {
|
|
|
learningMapper.upsertPattern(companyId, "strategy", "recommendation",
|
|
|
"综合策略建议: " + sb, 0.65, "StrategySynthesizer");
|
|
|
- discoveries++;
|
|
|
+ return 1;
|
|
|
}
|
|
|
- return discoveries;
|
|
|
- } catch (Exception e) { return 0; }
|
|
|
+ return 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** AI 合成 Skill 文档(借鉴 Hermes 自进化 Skill 机制,企业审核门控) */
|
|
|
+ private int synthesizeSkillDocument(Long companyId) {
|
|
|
+ if (learningMapper == null || multiModelRouter == null) return 0;
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
|
|
|
+ if (patterns == null || patterns.size() < 3) return 0;
|
|
|
+
|
|
|
+ StringBuilder input = new StringBuilder("以下是租户历史交互学习模式,请合成一份可执行的销冠 Skill 文档:\n");
|
|
|
+ int count = 0;
|
|
|
+ for (Map<String, Object> p : patterns) {
|
|
|
+ if (isAppliedPattern(p)) continue;
|
|
|
+ double c = toDouble(p.get("confidence"));
|
|
|
+ if (c < HIGH_CONFIDENCE) continue;
|
|
|
+ input.append("- [").append(p.get("pattern_type")).append("] ")
|
|
|
+ .append(p.get("pattern_value")).append("\n");
|
|
|
+ count++;
|
|
|
+ if (count >= 8) break;
|
|
|
+ }
|
|
|
+ if (count < 2) return 0;
|
|
|
+
|
|
|
+ input.append("\n输出 JSON: {\"title\":\"Skill标题\",\"content\":\"具体执行步骤和话术要点\",\"expectedImpact\":\"预期效果\"}");
|
|
|
+ String aiResponse = multiModelRouter.generateResponse(input.toString(), null, "learning_synthesizer");
|
|
|
+ JSONObject json = parseJsonObject(aiResponse);
|
|
|
+ if (json == null) return 0;
|
|
|
+
|
|
|
+ String title = json.getString("title");
|
|
|
+ String content = json.getString("content");
|
|
|
+ if (content == null || content.isEmpty()) return 0;
|
|
|
+
|
|
|
+ String skillBody = (title != null ? title + "\n" : "") + content;
|
|
|
+ if (json.getString("expectedImpact") != null) {
|
|
|
+ skillBody += "\n预期效果: " + json.getString("expectedImpact");
|
|
|
+ }
|
|
|
+ learningMapper.upsertPattern(companyId, "skill", "ai_synthesized",
|
|
|
+ skillBody, 0.72, "AiSkillSynthesizer");
|
|
|
+ return 1;
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[TenantLearning] AI Skill 合成失败: {}", e.getMessage());
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void contributeHighConfidencePatterns(Long companyId) {
|
|
|
+ if (distributedLearningService == null || learningMapper == null) return;
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
|
|
|
+ if (patterns == null) return;
|
|
|
+ List<Map<String, Object>> toShare = new ArrayList<>();
|
|
|
+ for (Map<String, Object> p : patterns) {
|
|
|
+ if (toDouble(p.get("confidence")) >= HIGH_CONFIDENCE) {
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+ item.put("scenario", p.get("pattern_type"));
|
|
|
+ item.put("patternType", p.get("pattern_key"));
|
|
|
+ item.put("content", p.get("pattern_value"));
|
|
|
+ item.put("score", p.get("confidence"));
|
|
|
+ toShare.add(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!toShare.isEmpty()) {
|
|
|
+ distributedLearningService.contributePatterns(companyId, "general", toShare);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[TenantLearning] 分布式贡献失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private LearningResult mapPatternRow(Long companyId, Map<String, Object> row) {
|
|
|
+ LearningResult r = new LearningResult();
|
|
|
+ r.setId(row.get("id") instanceof Number ? ((Number) row.get("id")).longValue() : null);
|
|
|
+ r.setCompanyId(companyId);
|
|
|
+ r.setLearningType(String.valueOf(row.getOrDefault("pattern_type", "unknown")));
|
|
|
+ r.setTitle(String.valueOf(row.getOrDefault("pattern_key", "")));
|
|
|
+ r.setDescription(String.valueOf(row.getOrDefault("pattern_value", "")));
|
|
|
+ r.setConfidence(toDouble(row.get("confidence")));
|
|
|
+ r.setStatus(isAppliedPattern(row) ? "applied" : "discovered");
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isAppliedPattern(Map<String, Object> row) {
|
|
|
+ String source = row.get("source") != null ? row.get("source").toString() : "";
|
|
|
+ return source.contains("applied");
|
|
|
+ }
|
|
|
+
|
|
|
+ private int countNonAppliedPatterns(Long companyId) {
|
|
|
+ List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
|
|
|
+ if (patterns == null) return 0;
|
|
|
+ int count = 0;
|
|
|
+ for (Map<String, Object> p : patterns) {
|
|
|
+ if (!isAppliedPattern(p)) count++;
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolvePatchField(String patternType) {
|
|
|
+ if ("skill".equals(patternType) || "message".equals(patternType)) return "messageTemplate";
|
|
|
+ if ("timing".equals(patternType)) return "sendTimeWindow";
|
|
|
+ if ("flow".equals(patternType)) return "nodeConfig";
|
|
|
+ return "learningPatch";
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Long> loadActiveTenants() {
|
|
|
+ if (auxMapper == null) return Collections.emptyList();
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> rows = auxMapper.selectPaged("company_info", "status=1", 0, 200);
|
|
|
+ List<Long> tenants = new ArrayList<>();
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
+ Object id = row.get("id");
|
|
|
+ if (id instanceof Number) {
|
|
|
+ tenants.add(((Number) id).longValue());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ id = row.get("company_id");
|
|
|
+ if (id instanceof Number) tenants.add(((Number) id).longValue());
|
|
|
+ }
|
|
|
+ return tenants;
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[TenantLearning] 加载租户列表失败: {}", e.getMessage());
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int parseHour(Object ct) {
|
|
|
+ if (ct == null) return -1;
|
|
|
+ if (ct instanceof java.sql.Timestamp) {
|
|
|
+ return ((java.sql.Timestamp) ct).toLocalDateTime().getHour();
|
|
|
+ }
|
|
|
+ if (ct instanceof Date) {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime((Date) ct);
|
|
|
+ return cal.get(Calendar.HOUR_OF_DAY);
|
|
|
+ }
|
|
|
+ String s = ct.toString();
|
|
|
+ if (s.length() >= 13) {
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(s.substring(11, 13));
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static double toDouble(Object v) {
|
|
|
+ return v instanceof Number ? ((Number) v).doubleValue() : 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String truncate(String s, int max) {
|
|
|
+ if (s == null) return "";
|
|
|
+ return s.length() > max ? s.substring(0, max) + "..." : s;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JSONObject parseJsonObject(String aiResponse) {
|
|
|
+ if (aiResponse == null) return null;
|
|
|
+ try {
|
|
|
+ int start = aiResponse.indexOf("{");
|
|
|
+ int end = aiResponse.lastIndexOf("}");
|
|
|
+ String jsonStr = start >= 0 && end > start ? aiResponse.substring(start, end + 1) : aiResponse;
|
|
|
+ return JSON.parseObject(jsonStr);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
}
|
|
|
}
|