feat(intelligence): Phase 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
Task 5.1+5.4: ButlerRouter/experience 注入格式升级为 <butler-context> XML fencing - butler_router: [路由上下文] → <butler-context><routing>...</routing></butler-context> - experience: [过往经验] → <butler-context><experience>...</experience></butler-context> - 统一 system-note 提示,引导 LLM 自然运用上下文 Task 5.2: 跨会话连续性 — pre_conversation_hook 注入活跃痛点 + 相关经验 - 从 VikingStorage 检索相关记忆(相似度>=0.3) - 从 pain_aggregator 获取 High severity 痛点(top 3) Task 5.3: 触发信号持久化 — post_conversation_hook 将触发信号存入 VikingStorage - store_trigger_experience(): 模板提取,零 LLM 成本 - 为未来 LLM 深度反思积累数据基础
This commit is contained in:
@@ -201,12 +201,15 @@ impl ButlerRouterMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Domain context to inject into system prompt based on routing hint.
|
/// Domain context to inject into system prompt based on routing hint.
|
||||||
|
///
|
||||||
|
/// Uses structured `<butler-context>` XML fencing (Hermes-inspired) for
|
||||||
|
/// reliable prompt cache preservation across turns.
|
||||||
fn build_context_injection(hint: &RoutingHint) -> String {
|
fn build_context_injection(hint: &RoutingHint) -> String {
|
||||||
// Semantic skill routing
|
// Semantic skill routing
|
||||||
if hint.category == "semantic_skill" {
|
if hint.category == "semantic_skill" {
|
||||||
if let Some(ref skill_id) = hint.skill_id {
|
if let Some(ref skill_id) = hint.skill_id {
|
||||||
return format!(
|
return format!(
|
||||||
"\n\n[语义路由] 匹配技能: {} (置信度: {:.0}%)\n系统检测到用户的意图与已注册技能高度相关,请在回答中充分利用该技能的能力。",
|
"\n\n<butler-context>\n<routing>匹配技能: {} (置信度: {:.0}%)</routing>\n<system-note>系统检测到用户的意图与已注册技能高度相关,请在回答中充分利用该技能的能力。</system-note>\n</butler-context>",
|
||||||
skill_id,
|
skill_id,
|
||||||
hint.confidence * 100.0
|
hint.confidence * 100.0
|
||||||
);
|
);
|
||||||
@@ -230,11 +233,11 @@ impl ButlerRouterMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let skill_info = hint.skill_id.as_ref().map_or(String::new(), |id| {
|
let skill_info = hint.skill_id.as_ref().map_or(String::new(), |id| {
|
||||||
format!("\n关联技能: {}", id)
|
format!("\n<skill>{}</skill>", id)
|
||||||
});
|
});
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"\n\n[路由上下文] (置信度: {:.0}%)\n{}{}",
|
"\n\n<butler-context>\n<routing confidence=\"{:.0}%\">{}</routing>{}<system-note>以上是管家系统对您当前意图的分析。在对话中自然运用这些信息,主动提供有帮助的建议。</system-note>\n</butler-context>",
|
||||||
hint.confidence * 100.0,
|
hint.confidence * 100.0,
|
||||||
domain_context,
|
domain_context,
|
||||||
skill_info
|
skill_info
|
||||||
@@ -357,7 +360,7 @@ mod tests {
|
|||||||
domain_prompt: None,
|
domain_prompt: None,
|
||||||
};
|
};
|
||||||
let injection = ButlerRouterMiddleware::build_context_injection(&hint);
|
let injection = ButlerRouterMiddleware::build_context_injection(&hint);
|
||||||
assert!(injection.contains("路由上下文"));
|
assert!(injection.contains("butler-context"));
|
||||||
assert!(injection.contains("医院"));
|
assert!(injection.contains("医院"));
|
||||||
assert!(injection.contains("80%"));
|
assert!(injection.contains("80%"));
|
||||||
}
|
}
|
||||||
@@ -435,7 +438,7 @@ mod tests {
|
|||||||
|
|
||||||
let decision = mw.before_completion(&mut ctx).await.unwrap();
|
let decision = mw.before_completion(&mut ctx).await.unwrap();
|
||||||
assert!(matches!(decision, MiddlewareDecision::Continue));
|
assert!(matches!(decision, MiddlewareDecision::Continue));
|
||||||
assert!(ctx.system_prompt.contains("路由上下文"));
|
assert!(ctx.system_prompt.contains("butler-context"));
|
||||||
assert!(ctx.system_prompt.contains("医院"));
|
assert!(ctx.system_prompt.contains("医院"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +467,7 @@ mod tests {
|
|||||||
|
|
||||||
let decision = mw.before_completion(&mut ctx).await.unwrap();
|
let decision = mw.before_completion(&mut ctx).await.unwrap();
|
||||||
assert!(matches!(decision, MiddlewareDecision::Continue));
|
assert!(matches!(decision, MiddlewareDecision::Continue));
|
||||||
assert!(ctx.system_prompt.contains("路由上下文"));
|
assert!(ctx.system_prompt.contains("butler-context"));
|
||||||
assert!(ctx.system_prompt.contains("电商运营管家"));
|
assert!(ctx.system_prompt.contains("电商运营管家"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ impl ExperienceExtractor {
|
|||||||
|
|
||||||
/// Format experiences for system prompt injection.
|
/// Format experiences for system prompt injection.
|
||||||
/// Returns a concise block capped at ~200 Chinese characters.
|
/// Returns a concise block capped at ~200 Chinese characters.
|
||||||
|
/// Uses `<butler-context>` XML fencing for structured injection.
|
||||||
/// Includes industry context when available.
|
/// Includes industry context when available.
|
||||||
pub fn format_for_injection(
|
pub fn format_for_injection(
|
||||||
experiences: &[zclaw_growth::experience_store::Experience],
|
experiences: &[zclaw_growth::experience_store::Experience],
|
||||||
@@ -224,14 +225,14 @@ impl ExperienceExtractor {
|
|||||||
.map(|s| truncate(s, 40))
|
.map(|s| truncate(s, 40))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let industry_tag = exp.industry_context.as_ref()
|
let industry_tag = exp.industry_context.as_ref()
|
||||||
.map(|i| format!(", 行业:{}", i))
|
.map(|i| format!(" 行业:{}", i))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let line = format!(
|
let line = format!(
|
||||||
"[过往经验{}] 类似「{}」做过:{},结果是{}",
|
"- 类似「{}」做过:{},结果是{} ({})",
|
||||||
industry_tag,
|
|
||||||
truncate(&exp.pain_pattern, 30),
|
truncate(&exp.pain_pattern, 30),
|
||||||
step_summary,
|
step_summary,
|
||||||
exp.outcome
|
exp.outcome,
|
||||||
|
industry_tag.trim_start()
|
||||||
);
|
);
|
||||||
total_chars += line.chars().count();
|
total_chars += line.chars().count();
|
||||||
parts.push(line);
|
parts.push(line);
|
||||||
@@ -241,7 +242,10 @@ impl ExperienceExtractor {
|
|||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
format!("\n\n--- 过往经验参考 ---\n{}", parts.join("\n"))
|
format!(
|
||||||
|
"\n\n<butler-context>\n<experience>\n{}\n</experience>\n</butler-context>",
|
||||||
|
parts.join("\n")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +349,8 @@ mod tests {
|
|||||||
"成功解决",
|
"成功解决",
|
||||||
);
|
);
|
||||||
let formatted = ExperienceExtractor::format_for_injection(&[exp]);
|
let formatted = ExperienceExtractor::format_for_injection(&[exp]);
|
||||||
assert!(formatted.contains("过往经验"));
|
assert!(formatted.contains("butler-context"));
|
||||||
|
assert!(formatted.contains("experience"));
|
||||||
assert!(formatted.contains("出口包装问题"));
|
assert!(formatted.contains("出口包装问题"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ use zclaw_runtime::driver::LlmDriver;
|
|||||||
|
|
||||||
/// Run pre-conversation intelligence hooks
|
/// Run pre-conversation intelligence hooks
|
||||||
///
|
///
|
||||||
/// Builds identity-enhanced system prompt (SOUL.md + instructions).
|
/// Builds identity-enhanced system prompt (SOUL.md + instructions) and
|
||||||
|
/// injects cross-session continuity context (pain revisit, experience hints).
|
||||||
///
|
///
|
||||||
/// NOTE: Memory context injection is NOT done here — it is handled by
|
/// NOTE: Memory context injection is NOT done here — it is handled by
|
||||||
/// `MemoryMiddleware.before_completion()` in the Kernel's middleware chain.
|
/// `MemoryMiddleware.before_completion()` in the Kernel's middleware chain.
|
||||||
@@ -40,7 +41,15 @@ pub async fn pre_conversation_hook(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(enhanced_prompt)
|
// Cross-session continuity: check for unresolved pain points and recent experiences
|
||||||
|
let continuity_context = build_continuity_context(agent_id, _user_message).await;
|
||||||
|
|
||||||
|
let mut result = enhanced_prompt;
|
||||||
|
if !continuity_context.is_empty() {
|
||||||
|
result.push_str(&continuity_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run post-conversation intelligence hooks
|
/// Run post-conversation intelligence hooks
|
||||||
@@ -129,7 +138,15 @@ pub async fn post_conversation_hook(
|
|||||||
"[intelligence_hooks] Learning triggers activated: {:?}",
|
"[intelligence_hooks] Learning triggers activated: {:?}",
|
||||||
signal_names
|
signal_names
|
||||||
);
|
);
|
||||||
// Future: Pass signals to LLM experience extraction (Phase 5)
|
// Store lightweight experiences from trigger signals (template-based, no LLM cost)
|
||||||
|
for signal in &signals {
|
||||||
|
if let Err(e) = store_trigger_experience(agent_id, signal, _user_message).await {
|
||||||
|
warn!(
|
||||||
|
"[intelligence_hooks] Failed to store trigger experience: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,3 +322,115 @@ async fn query_memories_for_reflection(
|
|||||||
|
|
||||||
Ok(memories)
|
Ok(memories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build cross-session continuity context for the current conversation.
|
||||||
|
///
|
||||||
|
/// Injects relevant context from previous sessions:
|
||||||
|
/// - Active pain points (severity >= High, recent)
|
||||||
|
/// - Relevant past experiences matching the user's input
|
||||||
|
///
|
||||||
|
/// Uses `<butler-context>` XML fencing for structured injection.
|
||||||
|
async fn build_continuity_context(agent_id: &str, user_message: &str) -> String {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
|
// 1. Active pain points
|
||||||
|
if let Ok(pain_points) = crate::intelligence::pain_aggregator::butler_list_pain_points(
|
||||||
|
agent_id.to_string(),
|
||||||
|
).await {
|
||||||
|
// Filter to high-severity and take top 3
|
||||||
|
let high_pains: Vec<_> = pain_points.iter()
|
||||||
|
.filter(|p| matches!(p.severity, crate::intelligence::pain_aggregator::PainSeverity::High))
|
||||||
|
.take(3)
|
||||||
|
.collect();
|
||||||
|
if !high_pains.is_empty() {
|
||||||
|
let pain_lines: Vec<String> = high_pains.iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
let summary = &p.summary;
|
||||||
|
let count = p.occurrence_count;
|
||||||
|
let conf = (p.confidence * 100.0) as u8;
|
||||||
|
Some(format!(
|
||||||
|
"- {} (出现{}次, 置信度 {}%)",
|
||||||
|
summary, count, conf
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !pain_lines.is_empty() {
|
||||||
|
parts.push(format!("<active-pain>\n{}\n</active-pain>", pain_lines.join("\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Relevant experiences (if user message is non-trivial)
|
||||||
|
if user_message.chars().count() >= 4 {
|
||||||
|
if let Ok(storage) = crate::viking_commands::get_storage().await {
|
||||||
|
let options = zclaw_growth::FindOptions {
|
||||||
|
scope: Some(format!("agent://{}", agent_id)),
|
||||||
|
limit: Some(3),
|
||||||
|
min_similarity: Some(0.3),
|
||||||
|
};
|
||||||
|
if let Ok(entries) = zclaw_growth::VikingStorage::find(
|
||||||
|
storage.as_ref(),
|
||||||
|
user_message,
|
||||||
|
options,
|
||||||
|
).await {
|
||||||
|
if !entries.is_empty() {
|
||||||
|
let exp_lines: Vec<String> = entries.iter()
|
||||||
|
.map(|e| {
|
||||||
|
let overview = e.overview.as_deref().unwrap_or(&e.content);
|
||||||
|
let truncated: String = overview.chars().take(60).collect();
|
||||||
|
let score_pct = (e.access_count as f64).min(10.0) / 10.0 * 100.0;
|
||||||
|
format!("- {} (相关度: {:.0}%)", truncated, score_pct)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
parts.push(format!("<experience>\n{}\n</experience>", exp_lines.join("\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"\n\n<butler-context>\n{}\n<system-note>以上是管家系统从过往对话中提取的信息。在对话中自然运用这些信息,主动提供有帮助的建议。不要逐条复述以上内容。</system-note>\n</butler-context>",
|
||||||
|
parts.join("\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a lightweight experience entry from a trigger signal.
|
||||||
|
///
|
||||||
|
/// Uses VikingStorage directly — template-based, no LLM cost.
|
||||||
|
/// Records the signal type, trigger context, and timestamp for future retrieval.
|
||||||
|
async fn store_trigger_experience(
|
||||||
|
agent_id: &str,
|
||||||
|
signal: &crate::intelligence::triggers::TriggerSignal,
|
||||||
|
user_message: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let storage = crate::viking_commands::get_storage().await?;
|
||||||
|
|
||||||
|
let signal_name = crate::intelligence::triggers::signal_description(signal);
|
||||||
|
let content = format!(
|
||||||
|
"[触发信号: {}]\n用户消息: {}\n时间: {}",
|
||||||
|
signal_name,
|
||||||
|
user_message.chars().take(200).collect::<String>(),
|
||||||
|
chrono::Utc::now().to_rfc3339(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry = zclaw_growth::MemoryEntry::new(
|
||||||
|
agent_id,
|
||||||
|
zclaw_growth::MemoryType::Experience,
|
||||||
|
&format!("trigger/{:?}", signal),
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
|
||||||
|
zclaw_growth::VikingStorage::store(storage.as_ref(), &entry)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to store trigger experience: {}", e))?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"[intelligence_hooks] Stored trigger experience: {} for agent {}",
|
||||||
|
signal_name, agent_id
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user