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:
@@ -25,7 +25,7 @@ use crate::config::KernelConfig;
|
||||
use zclaw_memory::MemoryStore;
|
||||
use zclaw_runtime::{LlmDriver, ToolRegistry, tool::SkillExecutor};
|
||||
use zclaw_skills::SkillRegistry;
|
||||
use zclaw_hands::{HandRegistry, hands::{BrowserHand, QuizHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, ReminderHand, quiz::LlmQuizGenerator}};
|
||||
use zclaw_hands::{HandRegistry, hands::{BrowserHand, QuizHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, ReminderHand, DailyReportHand, quiz::LlmQuizGenerator}};
|
||||
|
||||
pub use adapters::KernelSkillExecutor;
|
||||
pub use adapters::KernelHandExecutor;
|
||||
@@ -103,6 +103,7 @@ impl Kernel {
|
||||
hands.register(Arc::new(ClipHand::new())).await;
|
||||
hands.register(Arc::new(TwitterHand::new())).await;
|
||||
hands.register(Arc::new(ReminderHand::new())).await;
|
||||
hands.register(Arc::new(DailyReportHand::new())).await;
|
||||
|
||||
// Cache hand configs for tool registry (sync access from create_tool_registry)
|
||||
let hand_configs = hands.list().await;
|
||||
|
||||
@@ -299,3 +299,68 @@ pub async fn seed_builtin_industries(pool: &PgPool) -> SaasResult<()> {
|
||||
tracing::info!("Seeded {} builtin industries", builtin_industries().len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Auto-optimize industry config based on actual usage data.
|
||||
///
|
||||
/// Analyzes experience data for all agents under an account and updates
|
||||
/// `skill_priorities` and `pain_seed_categories` to reflect actual usage
|
||||
/// patterns rather than static configuration.
|
||||
pub async fn auto_optimize_config(
|
||||
pool: &sqlx::PgPool,
|
||||
account_id: i64,
|
||||
usage_signals: &std::collections::HashMap<String, u32>,
|
||||
) -> crate::Result<()> {
|
||||
// Find active industries for this account
|
||||
let industries: Vec<(String, serde_json::Value)> = sqlx::query_as(
|
||||
"SELECT i.id, i.skill_priorities FROM industries i
|
||||
JOIN account_industries ai ON ai.industry_id = i.id
|
||||
WHERE ai.account_id = $1 AND i.status = 'active'",
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(crate::SaasError::from)?;
|
||||
|
||||
if industries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Build updated skill_priorities based on actual usage
|
||||
let mut new_priorities: Vec<(String, i32)> = Vec::new();
|
||||
for (skill, count) in usage_signals {
|
||||
let priority = (*count as i32).min(10);
|
||||
if priority > 0 {
|
||||
new_priorities.push((skill.clone(), priority));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority descending
|
||||
new_priorities.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
if new_priorities.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Update each linked industry's skill_priorities
|
||||
let priorities_json = serde_json::to_string(&new_priorities)
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
for (industry_id, _old_priorities) in &industries {
|
||||
sqlx::query(
|
||||
"UPDATE industries SET skill_priorities = $1, updated_at = NOW() WHERE id = $2",
|
||||
)
|
||||
.bind(&priorities_json)
|
||||
.bind(industry_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(crate::SaasError::from)?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"[auto_optimize] Updated skill_priorities for {} industries under account {}",
|
||||
industries.len(),
|
||||
account_id,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ pub struct SemanticSkillRouter {
|
||||
confidence_threshold: f32,
|
||||
/// LLM fallback for ambiguous queries (confidence below threshold)
|
||||
llm_fallback: Option<Arc<dyn RuntimeLlmIntent>>,
|
||||
/// Experience-based boost factors: tool_name → boost weight (0.0 - 0.15)
|
||||
experience_boosts: HashMap<String, f32>,
|
||||
}
|
||||
|
||||
impl SemanticSkillRouter {
|
||||
@@ -104,6 +106,7 @@ impl SemanticSkillRouter {
|
||||
tfidf_index: SkillTfidfIndex::new(),
|
||||
skill_embeddings: HashMap::new(),
|
||||
confidence_threshold: 0.85,
|
||||
experience_boosts: HashMap::new(),
|
||||
llm_fallback: None,
|
||||
};
|
||||
router.rebuild_index_sync();
|
||||
@@ -194,7 +197,7 @@ impl SemanticSkillRouter {
|
||||
for (skill_id, manifest) in &manifests {
|
||||
let tfidf_score = self.tfidf_index.score(query, &skill_id.to_string());
|
||||
|
||||
let final_score = if let Some(ref q_emb) = query_embedding {
|
||||
let base_score = if let Some(ref q_emb) = query_embedding {
|
||||
// Hybrid: embedding (70%) + TF-IDF (30%)
|
||||
if let Some(s_emb) = self.skill_embeddings.get(&skill_id.to_string()) {
|
||||
let emb_sim = cosine_similarity(q_emb, s_emb);
|
||||
@@ -206,6 +209,10 @@ impl SemanticSkillRouter {
|
||||
tfidf_score
|
||||
};
|
||||
|
||||
// Apply experience-based boost for frequently used tools
|
||||
let boost = self.experience_boosts.get(&skill_id.to_string()).copied().unwrap_or(0.0);
|
||||
let final_score = base_score + boost;
|
||||
|
||||
scored.push(ScoredCandidate {
|
||||
manifest: manifest.clone(),
|
||||
score: final_score,
|
||||
@@ -281,6 +288,22 @@ impl SemanticSkillRouter {
|
||||
confidence_threshold: self.confidence_threshold,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update experience-based boost factors.
|
||||
///
|
||||
/// `experiences` maps tool/skill names to reuse counts.
|
||||
/// Higher reuse count → higher boost (capped at 0.15).
|
||||
/// This lets the router prefer skills the user frequently uses.
|
||||
pub fn update_experience_boosts(&mut self, experiences: &HashMap<String, u32>) {
|
||||
self.experience_boosts.clear();
|
||||
for (tool, count) in experiences {
|
||||
// Boost = min(0.05 * ln(count + 1), 0.15) — logarithmic scaling
|
||||
let boost = (0.05 * (*count as f32 + 1.0).ln()).min(0.15);
|
||||
if boost > 0.01 {
|
||||
self.experience_boosts.insert(tool.clone(), boost);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Router statistics
|
||||
@@ -720,4 +743,40 @@ mod tests {
|
||||
// Should still return best TF-IDF match even below threshold
|
||||
assert_eq!(result.unwrap().skill_id, "skill-x");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_experience_boost_applied() {
|
||||
let registry = Arc::new(SkillRegistry::new());
|
||||
let embedder = Arc::new(NoOpEmbedder);
|
||||
let mut router = SemanticSkillRouter::new(registry.clone(), embedder);
|
||||
|
||||
let skill_a = make_manifest("researcher", "研究员", "深度研究分析报告", vec!["研究", "分析"]);
|
||||
let skill_b = make_manifest("collector", "收集器", "数据采集整理汇总", vec!["收集", "采集"]);
|
||||
registry.register(
|
||||
Arc::new(crate::runner::PromptOnlySkill::new(skill_a.clone(), String::new())),
|
||||
skill_a,
|
||||
).await;
|
||||
registry.register(
|
||||
Arc::new(crate::runner::PromptOnlySkill::new(skill_b.clone(), String::new())),
|
||||
skill_b,
|
||||
).await;
|
||||
|
||||
router.rebuild_index().await;
|
||||
|
||||
let mut exp = HashMap::new();
|
||||
exp.insert("researcher".to_string(), 10);
|
||||
router.update_experience_boosts(&exp);
|
||||
|
||||
let candidates = router.retrieve_candidates("帮我研究一下", 5).await;
|
||||
assert!(!candidates.is_empty());
|
||||
|
||||
let rid = SkillId::new("researcher");
|
||||
let cid = SkillId::new("collector");
|
||||
let researcher_score = candidates.iter().find(|c| c.manifest.id == rid).map(|c| c.score);
|
||||
let collector_score = candidates.iter().find(|c| c.manifest.id == cid).map(|c| c.score);
|
||||
|
||||
if let (Some(r), Some(c)) = (researcher_score, collector_score) {
|
||||
assert!(r >= c, "Experience-boosted researcher should score >= collector");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user