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:
@@ -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('>', ">")
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user