fix_customer_facing_splitter_utf8.mjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { writeFileSync } from 'node:fs';
  2. import { dirname, join } from 'node:path';
  3. import { fileURLToPath } from 'node:url';
  4. const scriptDir = dirname(fileURLToPath(import.meta.url));
  5. const root = join(scriptDir, '../../..');
  6. function u(s) {
  7. return s.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
  8. }
  9. const content = u(`package com.fs.company.service.workflow.deepdialogue;
  10. import java.util.ArrayList;
  11. import java.util.LinkedHashSet;
  12. import java.util.List;
  13. import java.util.regex.Pattern;
  14. /**
  15. * Split customer-facing text into: main answer first, then each question as its own message.
  16. */
  17. public final class CustomerFacingMessageSplitter {
  18. private static final Pattern SENTENCE_SPLIT = Pattern.compile("[\\u3002\\uff01\\uff1f?!\\uff1b\\\\n]+");
  19. private CustomerFacingMessageSplitter() {
  20. }
  21. /**
  22. * @return non-empty list: index 0 = answer body; rest = follow-up / rhetorical questions
  23. */
  24. public static List<String> split(String reply, String plannedProbe, boolean shouldProbe) {
  25. List<String> out = new ArrayList<>();
  26. if (reply == null || reply.isBlank()) {
  27. appendUnique(out, plannedProbe, shouldProbe);
  28. return out;
  29. }
  30. String body = UniversalDeepDialogueService.stripProbeFromReply(reply, plannedProbe);
  31. List<String> answerParts = new ArrayList<>();
  32. List<String> questionParts = new ArrayList<>();
  33. for (String sentence : SENTENCE_SPLIT.split(body)) {
  34. String part = sentence != null ? sentence.trim() : "";
  35. if (part.isEmpty()) {
  36. continue;
  37. }
  38. if (isQuestionSentence(part)) {
  39. questionParts.add(normalizeQuestion(part));
  40. } else {
  41. answerParts.add(part);
  42. }
  43. }
  44. String answer = String.join("\\u3002", answerParts).trim();
  45. if (!answer.isEmpty() && !answer.endsWith("\\u3002") && !answer.endsWith("\\uff01")
  46. && !answer.endsWith("!") && !answer.endsWith("~") && !answer.endsWith("\\u2026")) {
  47. answer = answer + "\\u3002";
  48. }
  49. appendUnique(out, answer.isEmpty() ? null : answer.replaceAll("\\u3002+$", "\\u3002"), false);
  50. LinkedHashSet<String> seen = new LinkedHashSet<>();
  51. if (!out.isEmpty() && out.get(0) != null) {
  52. seen.add(out.get(0));
  53. }
  54. for (String q : questionParts) {
  55. appendUnique(out, q, false, seen);
  56. }
  57. appendUnique(out, plannedProbe, shouldProbe, seen);
  58. return out;
  59. }
  60. /** Keep declarative sentences only; questions are sent as separate follow-up messages. */
  61. public static String extractAnswerOnly(String reply) {
  62. if (reply == null || reply.isBlank()) {
  63. return reply;
  64. }
  65. List<String> parts = split(reply, null, false);
  66. if (parts.isEmpty()) {
  67. return reply.trim();
  68. }
  69. String answer = parts.get(0);
  70. return answer != null && !answer.isBlank() ? answer.trim() : reply.trim();
  71. }
  72. public static List<String> followUps(String reply, String plannedProbe, boolean shouldProbe) {
  73. List<String> all = split(reply, plannedProbe, shouldProbe);
  74. if (all.size() <= 1) {
  75. return List.of();
  76. }
  77. return new ArrayList<>(all.subList(1, all.size()));
  78. }
  79. private static void appendUnique(List<String> out, String text, boolean required) {
  80. appendUnique(out, text, required, null);
  81. }
  82. private static void appendUnique(List<String> out, String text, boolean required, LinkedHashSet<String> seen) {
  83. if (!required && (text == null || text.isBlank())) {
  84. return;
  85. }
  86. if (text == null || text.isBlank()) {
  87. return;
  88. }
  89. String normalized = text.trim();
  90. if (seen != null) {
  91. String key = normalized.length() > 12 ? normalized.substring(0, 12) : normalized;
  92. if (seen.contains(key) || seen.stream().anyMatch(s -> s != null && s.contains(key))) {
  93. return;
  94. }
  95. seen.add(key);
  96. }
  97. out.add(normalized);
  98. }
  99. static boolean isQuestionSentence(String sentence) {
  100. if (sentence == null || sentence.isBlank()) {
  101. return false;
  102. }
  103. String t = sentence.trim();
  104. if (t.endsWith("?") || t.endsWith("\\uff1f")) {
  105. return true;
  106. }
  107. return t.matches(".*(\\u5417|\\u5462|\\u5417\\uff1f|\\u5462\\uff1f|\\u4e48|\\u554a|\\u5427|\\u561b|\\u662f\\u4e0d\\u662f|\\u80fd\\u4e0d\\u80fd|\\u53ef\\u4e0d\\u53ef\\u4ee5|\\u8981\\u4e0d\\u8981|\\u6709\\u6ca1\\u6709|\\u591a\\u5c11|\\u51e0|\\u54ea|\\u8c01)[\\uff1f?]?$");
  108. }
  109. private static String normalizeQuestion(String sentence) {
  110. String t = sentence.trim();
  111. if (!t.endsWith("?") && !t.endsWith("\\uff1f")) {
  112. t = t + "\\uff1f";
  113. }
  114. return t;
  115. }
  116. }
  117. `);
  118. for (const rel of [
  119. 'java/fs-service/scripts/lobster_utf8_sources/CustomerFacingMessageSplitter.java',
  120. 'java/fs-service/src/main/java/com/fs/company/service/workflow/deepdialogue/CustomerFacingMessageSplitter.java',
  121. ]) {
  122. writeFileSync(join(root, rel), content.replace(/\r\n/g, '\n'), 'utf8');
  123. console.log('fixed', rel);
  124. }