feat: add semantic WeChat session actions
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
isTopicResetText,
|
||||
wantsDocxDownload,
|
||||
} from './patterns.mjs';
|
||||
import { WECHAT_SESSION_ACTION } from './session-action.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {object} WechatPageGenerateIntent
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
*
|
||||
* @typedef {object} WechatSessionResetIntent
|
||||
* @property {'session.reset'} kind
|
||||
* @property {string} sessionAction
|
||||
*
|
||||
* @typedef {object} WechatStatusProbeIntent
|
||||
* @property {'status.probe'} kind
|
||||
@@ -46,7 +48,9 @@ export function classifyWechatIntent(intent) {
|
||||
const text = String(intent?.agentText ?? intent?.displayText ?? '').trim();
|
||||
|
||||
if (msgType === 'text' || msgType === 'voice') {
|
||||
if (isTopicResetText(text)) return { kind: 'session.reset' };
|
||||
if (isTopicResetText(text)) {
|
||||
return { kind: 'session.reset', sessionAction: WECHAT_SESSION_ACTION.RESET };
|
||||
}
|
||||
if (STATUS_PROBE_PATTERN.test(text)) return { kind: 'status.probe' };
|
||||
if (GREETING_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) return { kind: 'greeting' };
|
||||
if (CONNECTIVITY_TEST_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/** Session actions are broader than one fixed reset phrase. */
|
||||
export const WECHAT_SESSION_ACTION = Object.freeze({
|
||||
CONTINUE: 'continue',
|
||||
RESET: 'reset',
|
||||
IGNORE_PREVIOUS: 'ignore_previous',
|
||||
UNCLEAR: 'unclear',
|
||||
});
|
||||
|
||||
const RESET_VERBS = /(?:换|开|新建|开启|开始|另起|重新|从头|清空|作废)/u;
|
||||
const RESET_TARGETS = /(?:新?会话|对话|聊天|上下文|话题|主题)/u;
|
||||
const CONTEXT_REFERENCES = /(?:之前|刚才|前面|上面|当前|这个|刚刚)/u;
|
||||
const IGNORE_VERBS = /(?:忽略|不要管|别管|不参考|不要参考|不用参考|忘掉|忘记)/u;
|
||||
const CONTINUE_VERBS = /(?:继续|接着|还是|回到|沿着)/u;
|
||||
const SESSION_SEMANTIC_HINT = /(?:换个思路|另一个话题|不同话题|重置|清除|忘记刚才|刚才.*不要|从现在开始)/u;
|
||||
|
||||
function normalizeText(text) {
|
||||
return String(text ?? '')
|
||||
.trim()
|
||||
.replace(/[\u200b-\u200d\ufeff]/g, '')
|
||||
.replace(/[\s,。!?、;:“”‘’()()【】\[\]{}]+/gu, '');
|
||||
}
|
||||
|
||||
function classifyByMeaning(text) {
|
||||
const normalized = normalizeText(text);
|
||||
if (!normalized) return { action: WECHAT_SESSION_ACTION.UNCLEAR, confidence: 0 };
|
||||
|
||||
const hasTarget = RESET_TARGETS.test(normalized);
|
||||
const hasContext = CONTEXT_REFERENCES.test(normalized);
|
||||
const hasResetVerb = RESET_VERBS.test(normalized);
|
||||
const hasIgnoreVerb = IGNORE_VERBS.test(normalized);
|
||||
const hasContinueVerb = CONTINUE_VERBS.test(normalized);
|
||||
|
||||
if (hasContinueVerb && hasTarget && !hasResetVerb) {
|
||||
return { action: WECHAT_SESSION_ACTION.CONTINUE, confidence: 0.94, source: 'semantic_rules' };
|
||||
}
|
||||
if (hasIgnoreVerb && (hasContext || hasTarget) && !/(?:新|重新|另起|开启|开始)/u.test(normalized)) {
|
||||
return { action: WECHAT_SESSION_ACTION.IGNORE_PREVIOUS, confidence: 0.9, source: 'semantic_rules' };
|
||||
}
|
||||
if ((hasResetVerb && hasTarget) || /(?:重新开始|从头来|重来一次|另开一个)/u.test(normalized)) {
|
||||
return { action: WECHAT_SESSION_ACTION.RESET, confidence: 0.98, source: 'semantic_rules' };
|
||||
}
|
||||
return { action: WECHAT_SESSION_ACTION.UNCLEAR, confidence: 0.35, source: 'fallback' };
|
||||
}
|
||||
|
||||
function normalizeSemanticResult(result) {
|
||||
if (!result) return null;
|
||||
const raw = String(result.action ?? result.intent ?? result.label ?? '').trim().toLowerCase();
|
||||
const aliases = {
|
||||
'session.reset': WECHAT_SESSION_ACTION.RESET,
|
||||
reset: WECHAT_SESSION_ACTION.RESET,
|
||||
new_session: WECHAT_SESSION_ACTION.RESET,
|
||||
continue: WECHAT_SESSION_ACTION.CONTINUE,
|
||||
reuse: WECHAT_SESSION_ACTION.CONTINUE,
|
||||
ignore_previous: WECHAT_SESSION_ACTION.IGNORE_PREVIOUS,
|
||||
ignore: WECHAT_SESSION_ACTION.IGNORE_PREVIOUS,
|
||||
unclear: WECHAT_SESSION_ACTION.UNCLEAR,
|
||||
};
|
||||
const action = aliases[raw];
|
||||
if (!action) return null;
|
||||
return {
|
||||
action,
|
||||
confidence: Number.isFinite(Number(result.confidence)) ? Number(result.confidence) : 0.7,
|
||||
source: 'semantic_model',
|
||||
reason: String(result.reason ?? '').trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a session action. A channel may inject a small semantic model for
|
||||
* ambiguous phrases; the meaning-based fallback keeps this path safe when the
|
||||
* model is unavailable or times out.
|
||||
*/
|
||||
export async function resolveWechatSessionAction(text, { semanticClassifier = null, logger = console } = {}) {
|
||||
const deterministic = classifyByMeaning(text);
|
||||
if (deterministic.action !== WECHAT_SESSION_ACTION.UNCLEAR) return deterministic;
|
||||
if (typeof semanticClassifier !== 'function') return deterministic;
|
||||
const normalized = normalizeText(text);
|
||||
if (!RESET_TARGETS.test(normalized) && !CONTEXT_REFERENCES.test(normalized) && !SESSION_SEMANTIC_HINT.test(normalized)) {
|
||||
return deterministic;
|
||||
}
|
||||
try {
|
||||
const semantic = normalizeSemanticResult(await semanticClassifier({
|
||||
text: String(text ?? '').trim(),
|
||||
actions: Object.values(WECHAT_SESSION_ACTION),
|
||||
}));
|
||||
if (semantic) return semantic;
|
||||
} catch (err) {
|
||||
logger?.warn?.('[wechat-session-intent] semantic classifier failed:', err);
|
||||
}
|
||||
return deterministic;
|
||||
}
|
||||
|
||||
export function isWechatSessionResetAction(result) {
|
||||
return result?.action === WECHAT_SESSION_ACTION.RESET;
|
||||
}
|
||||
Reference in New Issue
Block a user