fix(growth): MEDIUM-12 ProfileUpdater 补齐 5 维度画像更新
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
根因: ProfileUpdater 只处理 industry 和 communication_style 2/5 维度, 跳过 recent_topic、pain_point、preferred_tool。 修复: - ProfileFieldUpdate 添加 kind 字段 (SetField | AppendArray) - collect_updates() 现在处理全部 5 个维度: - industry, communication_style → SetField (直接覆盖) - recent_topic, pain_point, preferred_tool → AppendArray (追加去重) - growth.rs 根据 ProfileUpdateKind 分派到不同的 UserProfileStore 方法: - SetField → update_field() - AppendArray → add_recent_topic() / add_pain_point() / add_preferred_tool() - ProfileUpdateKind re-exported from lib.rs 测试: test_collect_updates_all_five_dimensions 验证 5 个维度 + 2 种更新类型
This commit is contained in:
@@ -133,7 +133,7 @@ pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer}
|
|||||||
pub use summarizer::SummaryLlmDriver;
|
pub use summarizer::SummaryLlmDriver;
|
||||||
pub use experience_extractor::ExperienceExtractor;
|
pub use experience_extractor::ExperienceExtractor;
|
||||||
pub use json_utils::{extract_json_block, extract_string_array};
|
pub use json_utils::{extract_json_block, extract_string_array};
|
||||||
pub use profile_updater::{ProfileFieldUpdate, UserProfileUpdater};
|
pub use profile_updater::{ProfileFieldUpdate, ProfileUpdateKind, UserProfileUpdater};
|
||||||
pub use pattern_aggregator::{AggregatedPattern, PatternAggregator};
|
pub use pattern_aggregator::{AggregatedPattern, PatternAggregator};
|
||||||
pub use skill_generator::{SkillCandidate, SkillGenerator};
|
pub use skill_generator::{SkillCandidate, SkillGenerator};
|
||||||
pub use quality_gate::{QualityGate, QualityReport};
|
pub use quality_gate::{QualityGate, QualityReport};
|
||||||
|
|||||||
@@ -4,11 +4,21 @@
|
|||||||
|
|
||||||
use crate::types::CombinedExtraction;
|
use crate::types::CombinedExtraction;
|
||||||
|
|
||||||
|
/// 更新类型:字段覆盖 vs 数组追加
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum ProfileUpdateKind {
|
||||||
|
/// 直接覆盖字段值(industry, communication_style)
|
||||||
|
SetField,
|
||||||
|
/// 追加到 JSON 数组字段(recent_topic, pain_point, preferred_tool)
|
||||||
|
AppendArray,
|
||||||
|
}
|
||||||
|
|
||||||
/// 待更新的画像字段
|
/// 待更新的画像字段
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ProfileFieldUpdate {
|
pub struct ProfileFieldUpdate {
|
||||||
pub field: String,
|
pub field: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub kind: ProfileUpdateKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 用户画像更新器
|
/// 用户画像更新器
|
||||||
@@ -22,11 +32,7 @@ impl UserProfileUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 从提取结果中收集需要更新的画像字段
|
/// 从提取结果中收集需要更新的画像字段
|
||||||
/// 返回 (field, value) 列表,由调用方负责实际的异步写入
|
/// 返回 (field, value, kind) 列表,由调用方根据 kind 选择写入方式
|
||||||
///
|
|
||||||
/// 注意:只收集 UserProfileStore::update_field() 支持的字段。
|
|
||||||
/// ProfileSignals 中的 recent_topic / pain_point / preferred_tool
|
|
||||||
/// 需要 update_field 扩展后才能写入,当前跳过。
|
|
||||||
pub fn collect_updates(
|
pub fn collect_updates(
|
||||||
&self,
|
&self,
|
||||||
extraction: &CombinedExtraction,
|
extraction: &CombinedExtraction,
|
||||||
@@ -38,6 +44,7 @@ impl UserProfileUpdater {
|
|||||||
updates.push(ProfileFieldUpdate {
|
updates.push(ProfileFieldUpdate {
|
||||||
field: "industry".to_string(),
|
field: "industry".to_string(),
|
||||||
value: industry.clone(),
|
value: industry.clone(),
|
||||||
|
kind: ProfileUpdateKind::SetField,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +52,31 @@ impl UserProfileUpdater {
|
|||||||
updates.push(ProfileFieldUpdate {
|
updates.push(ProfileFieldUpdate {
|
||||||
field: "communication_style".to_string(),
|
field: "communication_style".to_string(),
|
||||||
value: style.clone(),
|
value: style.clone(),
|
||||||
|
kind: ProfileUpdateKind::SetField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref topic) = signals.recent_topic {
|
||||||
|
updates.push(ProfileFieldUpdate {
|
||||||
|
field: "recent_topic".to_string(),
|
||||||
|
value: topic.clone(),
|
||||||
|
kind: ProfileUpdateKind::AppendArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref pain) = signals.pain_point {
|
||||||
|
updates.push(ProfileFieldUpdate {
|
||||||
|
field: "pain_point".to_string(),
|
||||||
|
value: pain.clone(),
|
||||||
|
kind: ProfileUpdateKind::AppendArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref tool) = signals.preferred_tool {
|
||||||
|
updates.push(ProfileFieldUpdate {
|
||||||
|
field: "preferred_tool".to_string(),
|
||||||
|
value: tool.clone(),
|
||||||
|
kind: ProfileUpdateKind::AppendArray,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +105,7 @@ mod tests {
|
|||||||
assert_eq!(updates.len(), 1);
|
assert_eq!(updates.len(), 1);
|
||||||
assert_eq!(updates[0].field, "industry");
|
assert_eq!(updates[0].field, "industry");
|
||||||
assert_eq!(updates[0].value, "healthcare");
|
assert_eq!(updates[0].value, "healthcare");
|
||||||
|
assert_eq!(updates[0].kind, ProfileUpdateKind::SetField);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -94,4 +127,31 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(updates.len(), 2);
|
assert_eq!(updates.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collect_updates_all_five_dimensions() {
|
||||||
|
let mut extraction = CombinedExtraction::default();
|
||||||
|
extraction.profile_signals.industry = Some("healthcare".to_string());
|
||||||
|
extraction.profile_signals.communication_style = Some("concise".to_string());
|
||||||
|
extraction.profile_signals.recent_topic = Some("报表自动化".to_string());
|
||||||
|
extraction.profile_signals.pain_point = Some("手动汇总太慢".to_string());
|
||||||
|
extraction.profile_signals.preferred_tool = Some("researcher".to_string());
|
||||||
|
|
||||||
|
let updater = UserProfileUpdater::new();
|
||||||
|
let updates = updater.collect_updates(&extraction);
|
||||||
|
|
||||||
|
assert_eq!(updates.len(), 5);
|
||||||
|
let set_fields: Vec<_> = updates
|
||||||
|
.iter()
|
||||||
|
.filter(|u| u.kind == ProfileUpdateKind::SetField)
|
||||||
|
.map(|u| u.field.as_str())
|
||||||
|
.collect();
|
||||||
|
let append_fields: Vec<_> = updates
|
||||||
|
.iter()
|
||||||
|
.filter(|u| u.kind == ProfileUpdateKind::AppendArray)
|
||||||
|
.map(|u| u.field.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(set_fields, vec!["industry", "communication_style"]);
|
||||||
|
assert_eq!(append_fields, vec!["recent_topic", "pain_point", "preferred_tool"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,10 +325,40 @@ impl GrowthIntegration {
|
|||||||
let updates = self.profile_updater.collect_updates(&combined);
|
let updates = self.profile_updater.collect_updates(&combined);
|
||||||
let user_id = agent_id.to_string();
|
let user_id = agent_id.to_string();
|
||||||
for update in updates {
|
for update in updates {
|
||||||
if let Err(e) = profile_store
|
let result = match update.kind {
|
||||||
|
zclaw_growth::ProfileUpdateKind::SetField => {
|
||||||
|
profile_store
|
||||||
.update_field(&user_id, &update.field, &update.value)
|
.update_field(&user_id, &update.field, &update.value)
|
||||||
.await
|
.await
|
||||||
{
|
}
|
||||||
|
zclaw_growth::ProfileUpdateKind::AppendArray => {
|
||||||
|
match update.field.as_str() {
|
||||||
|
"recent_topic" => {
|
||||||
|
profile_store
|
||||||
|
.add_recent_topic(&user_id, &update.value, 10)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"pain_point" => {
|
||||||
|
profile_store
|
||||||
|
.add_pain_point(&user_id, &update.value, 10)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"preferred_tool" => {
|
||||||
|
profile_store
|
||||||
|
.add_preferred_tool(&user_id, &update.value, 10)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::warn!(
|
||||||
|
"[GrowthIntegration] Unknown array field: {}",
|
||||||
|
update.field
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = result {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"[GrowthIntegration] Profile update failed for {}: {}",
|
"[GrowthIntegration] Profile update failed for {}: {}",
|
||||||
update.field,
|
update.field,
|
||||||
|
|||||||
Reference in New Issue
Block a user