/** * cold-start-mapper - Extract configuration from conversation content * * Maps user messages to cold start config (industry, name, personality, skills). * Uses keyword matching for deterministic extraction; LLM can refine later. */ // cold-start-mapper: keyword-based extraction for cold start configuration // Future: LLM-based extraction fallback will use structured logger // === Industry Detection === interface IndustryPattern { id: string; keywords: string[]; } const INDUSTRY_PATTERNS: IndustryPattern[] = [ { id: 'healthcare', keywords: ['医院', '医疗', '护士', '医生', '科室', '排班', '病历', '门诊', '住院', '行政', '护理', '医保', '挂号'], }, { id: 'education', keywords: ['学校', '教育', '教师', '老师', '学生', '课程', '培训', '教学', '考试', '成绩', '教务', '班级'], }, { id: 'garment', keywords: ['制衣', '服装', '面料', '打版', '缝纫', '裁床', '纺织', '生产', '工厂', '订单', '出货'], }, { id: 'ecommerce', keywords: ['电商', '店铺', '商品', '库存', '物流', '客服', '促销', '直播', '选品', 'SKU', '运营', '零售'], }, ]; export interface ColdStartMapping { detectedIndustry?: string; confidence: number; suggestedName?: string; personality?: { tone: string; formality: string; proactiveness: string }; prioritySkills?: string[]; } const INDUSTRY_SKILL_MAP: Record = { healthcare: ['data_report', 'schedule_query', 'policy_search', 'meeting_notes'], education: ['data_report', 'schedule_query', 'content_writing', 'meeting_notes'], garment: ['data_report', 'schedule_query', 'inventory_mgmt', 'order_tracking'], ecommerce: ['data_report', 'inventory_mgmt', 'order_tracking', 'content_writing'], }; const INDUSTRY_NAME_SUGGESTIONS: Record = { healthcare: ['小医', '医管家', '康康'], education: ['小教', '学伴', '知了'], garment: ['小织', '裁缝', '布管家'], ecommerce: ['小商', '掌柜', '店小二'], }; const INDUSTRY_PERSONALITY: Record = { healthcare: { tone: 'professional', formality: 'formal', proactiveness: 'moderate' }, education: { tone: 'friendly', formality: 'semi-formal', proactiveness: 'moderate' }, garment: { tone: 'practical', formality: 'semi-formal', proactiveness: 'low' }, ecommerce: { tone: 'energetic', formality: 'casual', proactiveness: 'high' }, }; /** * Detect industry from user message using keyword matching. */ export function detectIndustry(message: string): ColdStartMapping { if (!message || message.trim().length === 0) { return { confidence: 0 }; } const lower = message.toLowerCase(); let bestMatch = ''; let bestScore = 0; for (const pattern of INDUSTRY_PATTERNS) { let score = 0; for (const keyword of pattern.keywords) { if (lower.includes(keyword)) { score += 1; } } if (score > bestScore) { bestScore = score; bestMatch = pattern.id; } } // Require at least 1 keyword match if (bestScore === 0) { return { confidence: 0 }; } const confidence = Math.min(bestScore / 3, 1); const names = INDUSTRY_NAME_SUGGESTIONS[bestMatch] ?? []; const suggestedName = names.length > 0 ? names[0] : undefined; return { detectedIndustry: bestMatch, confidence, suggestedName, personality: INDUSTRY_PERSONALITY[bestMatch], prioritySkills: INDUSTRY_SKILL_MAP[bestMatch], }; } /** * Detect if user is agreeing/confirming something. */ export function detectAffirmative(message: string): boolean { if (!message) return false; const affirmativePatterns = ['好', '可以', '行', '没问题', '是的', '对', '嗯', 'OK', 'ok', '确认', '同意']; const lower = message.toLowerCase().trim(); return affirmativePatterns.some((p) => lower === p || lower.startsWith(p)); } /** * Detect if user is rejecting something. */ export function detectNegative(message: string): boolean { if (!message) return false; const negativePatterns = ['不', '不要', '算了', '换一个', '换', '不好', '不行', '其他', '别的']; const lower = message.toLowerCase().trim(); return negativePatterns.some((p) => lower === p || lower.startsWith(p)); } /** * Detect if user provides a name suggestion. */ export function detectNameSuggestion(message: string): string | undefined { if (!message) return undefined; // Match patterns like "叫我小王" "叫XX" "用XX" "叫 XX 吧" const patterns = [/叫[我它他她]?[""''「」]?(\S{1,8})[""''「」]?[吧。!]?$/, /用[""''「」]?(\S{1,8})[""''「」]?[吧。!]?$/]; for (const pattern of patterns) { const match = message.match(pattern); if (match && match[1]) { const name = match[1].replace(/[吧。!,、]/g, '').trim(); if (name.length >= 1 && name.length <= 8) { return name; } } } return undefined; } /** * Determine the next cold start phase based on current phase and user message. */ export function determinePhaseTransition( currentPhase: string, userMessage: string, ): { nextPhase: string; mapping?: ColdStartMapping } | null { switch (currentPhase) { case 'agent_greeting': { const mapping = detectIndustry(userMessage); if (mapping.detectedIndustry && mapping.confidence > 0.3) { return { nextPhase: 'industry_discovery', mapping }; } // User responded but no industry detected — keep probing return null; } case 'industry_discovery': { if (detectAffirmative(userMessage)) { return { nextPhase: 'identity_setup' }; } if (detectNegative(userMessage)) { // Try to re-detect from the rejection const mapping = detectIndustry(userMessage); if (mapping.detectedIndustry) { return { nextPhase: 'industry_discovery', mapping }; } return null; } // Direct industry mention const mapping = detectIndustry(userMessage); if (mapping.detectedIndustry) { return { nextPhase: 'identity_setup', mapping }; } return null; } case 'identity_setup': { const customName = detectNameSuggestion(userMessage); if (customName) { return { nextPhase: 'first_task', mapping: { confidence: 1, suggestedName: customName }, }; } if (detectAffirmative(userMessage)) { return { nextPhase: 'first_task' }; } if (detectNegative(userMessage)) { return null; // Stay in identity_setup for another suggestion } // User said something else, treat as name preference return { nextPhase: 'first_task', mapping: { confidence: 0.5, suggestedName: userMessage.trim().slice(0, 8) }, }; } case 'first_task': { // Any message in first_task is a real task — mark completed return { nextPhase: 'completed' }; } default: return null; } }