Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
C3 零配置引导 (P0): - use-cold-start.ts: 4阶段→6阶段对话驱动状态机 (idle→greeting→industry→identity→task→completed) - cold-start-mapper.ts: 关键词行业检测 + 肯定/否定/名字提取 - cold_start_prompt.rs: Rust侧6阶段system prompt生成 + 7个测试 - FirstConversationPrompt.tsx: 动态行业卡片 + 行业任务引导 + 通用快捷操作 C1 管家日报 (P0): - kernel注册DailyReportHand (第8个Hand) - DailyReportPanel.tsx已存在,事件监听+持久化完整 C2 行业知识飞轮 (P1): - heartbeat.rs: 经验缓存(EXPERIENCE_CACHE) + check_unresolved_pains增强经验感知 - heartbeat_update_experiences Tauri命令 + VikingStorage持久化 - semantic_router.rs: 经验权重boost(0.05*ln(count+1), 上限0.15) + update_experience_boosts方法 - service.rs: auto_optimize_config() 基于使用频率自动优化行业skill_priorities 验证: tsc 0 errors, cargo check 0 warnings, 7 cold_start + 5 daily_report + 1 experience_boost tests PASS
216 lines
6.9 KiB
TypeScript
216 lines
6.9 KiB
TypeScript
/**
|
|
* 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<string, string[]> = {
|
|
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<string, string[]> = {
|
|
healthcare: ['小医', '医管家', '康康'],
|
|
education: ['小教', '学伴', '知了'],
|
|
garment: ['小织', '裁缝', '布管家'],
|
|
ecommerce: ['小商', '掌柜', '店小二'],
|
|
};
|
|
|
|
const INDUSTRY_PERSONALITY: Record<string, { tone: string; formality: string; proactiveness: string }> = {
|
|
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;
|
|
}
|
|
}
|