feat(intelligence): 新增 experience_find_relevant Tauri 命令 + ExperienceBrief

- 新增 ExperienceBrief 结构(痛点模式+方案摘要+复用次数)
- OnceLock 单例 + init_experience_extractor() 启动初始化
- experience_find_relevant 命令:按 agent_id + query 检索相关经验
- 注册到 invoke_handler + setup 阶段优雅降级初始化
- 新增序列化测试(10 tests PASS)
This commit is contained in:
iven
2026-04-23 17:52:33 +08:00
parent 00ebf18f23
commit e9e7ffd609
2 changed files with 66 additions and 0 deletions

View File

@@ -16,6 +16,21 @@ use zclaw_types::Result;
use super::pain_aggregator::PainPoint;
use super::solution_generator::Proposal;
/// Brief summary of a stored experience, for suggestion context enrichment.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExperienceBrief {
pub pain_pattern: String,
pub solution_summary: String,
pub reuse_count: u32,
}
static EXPERIENCE_EXTRACTOR: std::sync::OnceLock<std::sync::Arc<ExperienceExtractor>> = std::sync::OnceLock::new();
/// Get the global ExperienceExtractor singleton (if initialized).
pub(crate) fn get_experience_extractor() -> Option<std::sync::Arc<ExperienceExtractor>> {
EXPERIENCE_EXTRACTOR.get().cloned()
}
// ---------------------------------------------------------------------------
// Shared completion status
// ---------------------------------------------------------------------------
@@ -263,6 +278,36 @@ fn xml_escape(s: &str) -> String {
.replace('>', "&gt;")
}
/// Initialize the global ExperienceExtractor singleton.
/// Called once during app startup, after viking storage is ready.
pub async fn init_experience_extractor() -> Result<()> {
let sqlite_storage = crate::viking_commands::get_storage().await
.map_err(|e| zclaw_types::ZclawError::StorageError(e))?;
let viking = std::sync::Arc::new(zclaw_growth::VikingAdapter::new(sqlite_storage));
let store = std::sync::Arc::new(ExperienceStore::new(viking));
let extractor = std::sync::Arc::new(ExperienceExtractor::new(store));
EXPERIENCE_EXTRACTOR.set(extractor)
.map_err(|_| zclaw_types::ZclawError::StorageError("ExperienceExtractor already initialized".into()))?;
Ok(())
}
/// Find experiences relevant to the current conversation for suggestion enrichment.
#[tauri::command]
pub async fn experience_find_relevant(
agent_id: String,
query: String,
) -> std::result::Result<Vec<ExperienceBrief>, String> {
let extractor = get_experience_extractor()
.ok_or("ExperienceExtractor not initialized".to_string())?;
let experiences = extractor.find_relevant_experiences(&agent_id, &query).await;
Ok(experiences.into_iter().take(3).map(|e| ExperienceBrief {
pain_pattern: e.pain_pattern,
solution_summary: e.solution_steps.join("")
.chars().take(100).collect(),
reuse_count: e.reuse_count,
}).collect())
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
@@ -407,4 +452,17 @@ mod tests {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("这是一个很长的字符串用于测试截断", 10).chars().count(), 11); // 10 + …
}
#[test]
fn test_experience_brief_serialization() {
let brief = super::ExperienceBrief {
pain_pattern: "报表生成慢".to_string(),
solution_summary: "使用 researcher 技能自动收集".to_string(),
reuse_count: 3,
};
let json = serde_json::to_string(&brief).unwrap();
let parsed: super::ExperienceBrief = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.pain_pattern, "报表生成慢");
assert_eq!(parsed.reuse_count, 3);
}
}

View File

@@ -212,6 +212,12 @@ pub fn run() {
if let Err(e) = rt.block_on(intelligence::pain_aggregator::init_pain_storage(pool)) {
tracing::error!("[PainStorage] Init failed: {}, pain points will not persist", e);
}
// Initialize experience extractor for suggestion enrichment.
// Graceful degradation: failure does not block app startup.
if let Err(e) = rt.block_on(intelligence::experience::init_experience_extractor()) {
tracing::warn!("[ExperienceExtractor] Init failed: {}, suggestion context will be empty", e);
}
}
Ok(())
@@ -435,6 +441,8 @@ pub fn run() {
intelligence::pain_aggregator::butler_update_proposal_status,
// Industry config loader
viking_commands::viking_load_industry_keywords,
// Experience finder for suggestion enrichment
intelligence::experience::experience_find_relevant,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");