feat(growth,skills,saas,desktop): C线差异化全量实现 — C1日报+C2飞轮+C3引导
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
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
This commit is contained in:
210
desktop/src-tauri/src/intelligence/cold_start_prompt.rs
Normal file
210
desktop/src-tauri/src/intelligence/cold_start_prompt.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
//! Cold start prompt generation for conversation-driven onboarding.
|
||||
//!
|
||||
//! Generates stage-specific system prompts that guide the agent through
|
||||
//! the 6-phase cold start flow without requiring form-filling.
|
||||
|
||||
/// Cold start phases matching the frontend state machine.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ColdStartPhase {
|
||||
Idle,
|
||||
AgentGreeting,
|
||||
IndustryDiscovery,
|
||||
IdentitySetup,
|
||||
FirstTask,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl ColdStartPhase {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"idle" => Self::Idle,
|
||||
"agent_greeting" => Self::AgentGreeting,
|
||||
"industry_discovery" => Self::IndustryDiscovery,
|
||||
"identity_setup" => Self::IdentitySetup,
|
||||
"first_task" => Self::FirstTask,
|
||||
"completed" => Self::Completed,
|
||||
_ => Self::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Industry-specific task suggestions for first_task phase.
|
||||
struct IndustryTasks {
|
||||
tasks: &'static [(&'static str, &'static str)],
|
||||
}
|
||||
|
||||
const HEALTHCARE_TASKS: IndustryTasks = IndustryTasks {
|
||||
tasks: &[
|
||||
("排班查询", "今天有需要处理的排班问题吗?"),
|
||||
("数据报表", "需要我帮你整理上周的数据报表吗?"),
|
||||
("政策查询", "最近有医保政策变化需要了解吗?"),
|
||||
],
|
||||
};
|
||||
|
||||
const EDUCATION_TASKS: IndustryTasks = IndustryTasks {
|
||||
tasks: &[
|
||||
("课程安排", "需要帮你安排下周的课程吗?"),
|
||||
("成绩分析", "有学生成绩需要分析吗?"),
|
||||
("测验生成", "需要帮学生出一份测验吗?告诉我科目和年级就行。"),
|
||||
],
|
||||
};
|
||||
|
||||
const GARMENT_TASKS: IndustryTasks = IndustryTasks {
|
||||
tasks: &[
|
||||
("订单跟踪", "有需要跟踪的订单吗?"),
|
||||
("生产排期", "需要安排生产计划吗?"),
|
||||
("成本核算", "有需要核算的成本数据吗?"),
|
||||
],
|
||||
};
|
||||
|
||||
const ECOMMERCE_TASKS: IndustryTasks = IndustryTasks {
|
||||
tasks: &[
|
||||
("库存检查", "需要检查库存情况吗?"),
|
||||
("销售分析", "想看看最近的销售数据吗?"),
|
||||
("商品文案", "有新商品需要写详情页吗?"),
|
||||
],
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
/// Generate the cold start system prompt for a given phase and optional industry.
|
||||
pub fn generate_cold_start_prompt(phase: ColdStartPhase, industry: Option<&str>) -> String {
|
||||
match phase {
|
||||
ColdStartPhase::Idle | ColdStartPhase::AgentGreeting => format!(
|
||||
"你是一个正在认识新用户的 AI 管家。\n\n\
|
||||
## 当前任务\n\
|
||||
向用户打招呼并了解他们的工作。用简短自然的方式询问。\n\n\
|
||||
## 规则\n\
|
||||
- 每条消息不超过 3 句话\n\
|
||||
- 不要问\"你的行业是什么\",而是问\"你每天最常处理什么事?\"\n\
|
||||
- 保持热情友好,像一个新同事在打招呼\n\
|
||||
- 用中文交流"
|
||||
),
|
||||
|
||||
ColdStartPhase::IndustryDiscovery => {
|
||||
let industry_hint = match industry {
|
||||
Some("healthcare") => "用户可能从事医疗行政工作。",
|
||||
Some("education") => "用户可能从事教育培训工作。",
|
||||
Some("garment") => "用户可能从事制衣制造工作。",
|
||||
Some("ecommerce") => "用户可能从事电商零售工作。",
|
||||
_ => "继续了解用户的工作场景。",
|
||||
};
|
||||
format!(
|
||||
"你是一个正在了解用户工作场景的 AI 管家。\n\n\
|
||||
## 当前阶段:行业发现\n\
|
||||
{industry_hint}\n\n\
|
||||
## 规则\n\
|
||||
- 根据用户的回答确认行业\n\
|
||||
- 如果检测到行业,主动说出你的理解,让用户确认\n\
|
||||
- 每条消息不超过 3 句话\n\
|
||||
- 用中文交流"
|
||||
)
|
||||
}
|
||||
|
||||
ColdStartPhase::IdentitySetup => {
|
||||
let name_suggestion = match industry {
|
||||
Some("healthcare") => "小医",
|
||||
Some("education") => "小教",
|
||||
Some("garment") => "小织",
|
||||
Some("ecommerce") => "小商",
|
||||
_ => "小助手",
|
||||
};
|
||||
format!(
|
||||
"你是一个正在为自己起名字的 AI 管家。\n\n\
|
||||
## 当前阶段:身份设定\n\
|
||||
根据你了解的行业信息,向用户提议一个合适的名字和沟通风格。\n\n\
|
||||
## 建议\n\
|
||||
- 可以提议叫\"{name_suggestion}\"或其他合适的名字\n\
|
||||
- 说明你选择的沟通风格(专业/亲切/简洁)\n\
|
||||
- 让用户确认或提出自己的想法\n\
|
||||
- 每条消息不超过 3 句话\n\
|
||||
- 用中文交流"
|
||||
)
|
||||
}
|
||||
|
||||
ColdStartPhase::FirstTask => {
|
||||
let task_prompt = match industry {
|
||||
Some("healthcare") => HEALTHCARE_TASKS.tasks[2].1,
|
||||
Some("education") => EDUCATION_TASKS.tasks[2].1,
|
||||
Some("garment") => GARMENT_TASKS.tasks[2].1,
|
||||
Some("ecommerce") => ECOMMERCE_TASKS.tasks[2].1,
|
||||
_ => "有什么我可以帮你的吗?",
|
||||
};
|
||||
format!(
|
||||
"你是一个 AI 管家,用户已经完成了初始设置。\n\n\
|
||||
## 当前阶段:首次任务引导\n\
|
||||
引导用户完成第一个实际任务,让他们体验你的能力。\n\n\
|
||||
## 建议\n\
|
||||
- {task_prompt}\n\
|
||||
- 根据用户需求灵活调整\n\
|
||||
- 保持简短,1-2 句话\n\
|
||||
- 用中文交流"
|
||||
)
|
||||
}
|
||||
|
||||
ColdStartPhase::Completed => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a cold start prompt should be injected for the given phase.
|
||||
pub fn should_inject_prompt(phase: ColdStartPhase) -> bool {
|
||||
!matches!(phase, ColdStartPhase::Idle | ColdStartPhase::Completed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_phase_from_str() {
|
||||
assert_eq!(ColdStartPhase::from_str("idle"), ColdStartPhase::Idle);
|
||||
assert_eq!(ColdStartPhase::from_str("agent_greeting"), ColdStartPhase::AgentGreeting);
|
||||
assert_eq!(ColdStartPhase::from_str("industry_discovery"), ColdStartPhase::IndustryDiscovery);
|
||||
assert_eq!(ColdStartPhase::from_str("identity_setup"), ColdStartPhase::IdentitySetup);
|
||||
assert_eq!(ColdStartPhase::from_str("first_task"), ColdStartPhase::FirstTask);
|
||||
assert_eq!(ColdStartPhase::from_str("completed"), ColdStartPhase::Completed);
|
||||
assert_eq!(ColdStartPhase::from_str("unknown"), ColdStartPhase::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_greeting_prompt_not_empty() {
|
||||
let prompt = generate_cold_start_prompt(ColdStartPhase::AgentGreeting, None);
|
||||
assert!(!prompt.is_empty());
|
||||
assert!(prompt.contains("AI 管家"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_industry_discovery_with_industry() {
|
||||
let prompt = generate_cold_start_prompt(ColdStartPhase::IndustryDiscovery, Some("healthcare"));
|
||||
assert!(prompt.contains("医疗行政"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identity_setup_suggests_name() {
|
||||
let prompt = generate_cold_start_prompt(ColdStartPhase::IdentitySetup, Some("education"));
|
||||
assert!(prompt.contains("小教"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_task_has_suggestion() {
|
||||
let prompt = generate_cold_start_prompt(ColdStartPhase::FirstTask, Some("ecommerce"));
|
||||
assert!(!prompt.is_empty());
|
||||
assert!(prompt.contains("库存") || prompt.contains("销售") || prompt.contains("商品"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_returns_empty() {
|
||||
let prompt = generate_cold_start_prompt(ColdStartPhase::Completed, None);
|
||||
assert!(prompt.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_inject() {
|
||||
assert!(!should_inject_prompt(ColdStartPhase::Idle));
|
||||
assert!(should_inject_prompt(ColdStartPhase::AgentGreeting));
|
||||
assert!(should_inject_prompt(ColdStartPhase::IndustryDiscovery));
|
||||
assert!(should_inject_prompt(ColdStartPhase::IdentitySetup));
|
||||
assert!(should_inject_prompt(ColdStartPhase::FirstTask));
|
||||
assert!(!should_inject_prompt(ColdStartPhase::Completed));
|
||||
}
|
||||
}
|
||||
@@ -464,6 +464,27 @@ pub fn update_pain_points_cache(agent_id: &str, pain_points: Vec<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Global experience cache: high-reuse experiences per agent.
|
||||
/// Key: agent_id, Value: list of (tool_used, reuse_count) tuples.
|
||||
static EXPERIENCE_CACHE: OnceLock<RwLock<StdHashMap<String, Vec<(String, u32)>>>> = OnceLock::new();
|
||||
|
||||
fn get_experience_cache() -> &'static RwLock<StdHashMap<String, Vec<(String, u32)>>> {
|
||||
EXPERIENCE_CACHE.get_or_init(|| RwLock::new(StdHashMap::new()))
|
||||
}
|
||||
|
||||
/// Update experience cache (called from frontend or growth middleware)
|
||||
pub fn update_experience_cache(agent_id: &str, experiences: Vec<(String, u32)>) {
|
||||
let cache = get_experience_cache();
|
||||
if let Ok(mut cache) = cache.write() {
|
||||
cache.insert(agent_id.to_string(), experiences);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cached_experiences(agent_id: &str) -> Option<Vec<(String, u32)>> {
|
||||
let cache = get_experience_cache();
|
||||
cache.read().ok()?.get(agent_id).cloned()
|
||||
}
|
||||
|
||||
/// Get cached pain points for an agent
|
||||
fn get_cached_pain_points(agent_id: &str) -> Option<Vec<String>> {
|
||||
let cache = get_pain_points_cache();
|
||||
@@ -778,7 +799,8 @@ fn check_learning_opportunities(agent_id: &str) -> Option<HeartbeatAlert> {
|
||||
|
||||
/// Check for unresolved user pain points accumulated by the butler system.
|
||||
/// When pain points persist across multiple conversations, surface them as
|
||||
/// proactive suggestions.
|
||||
/// proactive suggestions. Also considers high-reuse experiences to generate
|
||||
/// contextual skill suggestions.
|
||||
fn check_unresolved_pains(agent_id: &str) -> Option<HeartbeatAlert> {
|
||||
let pains = get_cached_pain_points(agent_id)?;
|
||||
if pains.is_empty() {
|
||||
@@ -790,11 +812,25 @@ fn check_unresolved_pains(agent_id: &str) -> Option<HeartbeatAlert> {
|
||||
} else {
|
||||
format!("{}等 {} 项", pains[..3].join("、"), count)
|
||||
};
|
||||
|
||||
// Enhance with experience-based suggestions
|
||||
let experience_hint = if let Some(experiences) = get_cached_experiences(agent_id) {
|
||||
let high_use: Vec<&(String, u32)> = experiences.iter().filter(|(_, c)| *c >= 3).collect();
|
||||
if !high_use.is_empty() {
|
||||
let tools: Vec<&str> = high_use.iter().map(|(t, _)| t.as_str()).collect();
|
||||
format!(" 用户频繁使用{},可主动提供相关技能建议。", tools.join("、"))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
Some(HeartbeatAlert {
|
||||
title: "未解决的用户痛点".to_string(),
|
||||
content: format!(
|
||||
"检测到 {} 个持续痛点:{}。建议主动提供解决方案或相关建议。",
|
||||
count, summary
|
||||
"检测到 {} 个持续痛点:{}。建议主动提供解决方案或相关建议。{}",
|
||||
count, summary, experience_hint
|
||||
),
|
||||
urgency: if count >= 3 { Urgency::High } else { Urgency::Medium },
|
||||
source: "unresolved-pains".to_string(),
|
||||
@@ -1098,6 +1134,28 @@ pub async fn heartbeat_update_pain_points(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update experience cache for heartbeat proactive suggestions.
|
||||
/// Called by frontend when high-reuse experiences are detected.
|
||||
// @reserved
|
||||
#[tauri::command]
|
||||
pub async fn heartbeat_update_experiences(
|
||||
agent_id: String,
|
||||
experiences: Vec<(String, u32)>,
|
||||
) -> Result<(), String> {
|
||||
update_experience_cache(&agent_id, experiences.clone());
|
||||
let key = format!("heartbeat:experiences:{}", agent_id);
|
||||
tokio::spawn(async move {
|
||||
if let Ok(storage) = crate::viking_commands::get_storage().await {
|
||||
if let Ok(json) = serde_json::to_string(&experiences) {
|
||||
if let Err(e) = zclaw_growth::VikingStorage::store_metadata_json(&*storage, &key, &json).await {
|
||||
tracing::warn!("[heartbeat] Failed to persist experiences: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -45,6 +45,7 @@ pub mod triggers;
|
||||
pub mod user_profiler;
|
||||
pub mod trajectory_compressor;
|
||||
pub mod health_snapshot;
|
||||
pub mod cold_start_prompt;
|
||||
|
||||
// Re-export main types for convenience
|
||||
pub use heartbeat::HeartbeatEngineState;
|
||||
|
||||
@@ -382,6 +382,7 @@ pub fn run() {
|
||||
intelligence::heartbeat::heartbeat_record_correction,
|
||||
intelligence::heartbeat::heartbeat_record_interaction,
|
||||
intelligence::heartbeat::heartbeat_update_pain_points,
|
||||
intelligence::heartbeat::heartbeat_update_experiences,
|
||||
// Health Snapshot (on-demand query)
|
||||
intelligence::health_snapshot::health_snapshot,
|
||||
// Context Compactor
|
||||
|
||||
Reference in New Issue
Block a user