Skip to main content

skilllite_evolution/
memory_learner.rs

1//! 进化 Memory:从执行反馈中沉淀**事实与经历**,供检索与类比。
2//!
3//! 设计参考:MemGPT 层级记忆与检索、AriGraph/MemoriesDB 语义+情节图、
4//! 认知架构中语义/情节记忆划分。与规则进化(应然)、技能进化(可执行)明确分工:
5//! 本模块只产出**实然**(实体、关系、情节、倾向、模式),不产出「何时做什么」类规则。
6//! 详见 seed/evolution_prompts/memory_knowledge_extraction.seed.md 顶部设计说明。
7
8use std::path::Path;
9
10use anyhow::Result;
11use rusqlite::Connection;
12use tokio::task::block_in_place;
13
14use crate::feedback::open_evolution_db;
15use crate::gatekeeper_l1_path;
16use crate::gatekeeper_l3_content;
17use crate::EvolutionLlm;
18use crate::EvolutionMessage;
19
20const MEMORY_KNOWLEDGE_PROMPT: &str =
21    include_str!("seed/evolution_prompts/memory_knowledge_extraction.seed.md");
22
23const RECENT_DAYS: &str = "-7 days";
24const DECISION_LIMIT: i64 = 15;
25/// 已有知识摘要最大字符数,供 LLM 去重参考,避免重复抽取
26const EXISTING_KNOWLEDGE_CAP: usize = 3500;
27
28/// 单次进化中各类知识条数上限,避免单次写入过长
29const MAX_ENTITIES: usize = 12;
30const MAX_RELATIONS: usize = 10;
31const MAX_EPISODES: usize = 8;
32const MAX_PREFERENCES: usize = 8;
33const MAX_PATTERNS: usize = 5;
34
35/// 运行 memory 进化:从近期 decisions 抽取实体、关系、情节、倾向、模式,追加到 memory/evolution/knowledge.md。
36/// 返回 changelog 用 (change_type, target_id),无变更时返回空 Vec。
37pub async fn evolve_memory<L: EvolutionLlm>(
38    chat_root: &Path,
39    llm: &L,
40    model: &str,
41    _txn_id: &str,
42) -> Result<Vec<(String, String)>> {
43    let summary = block_in_place(|| {
44        let conn = open_evolution_db(chat_root)?;
45        query_decisions_for_memory(&conn)
46    })?;
47
48    if summary.is_empty() {
49        tracing::debug!("Memory evolution: no recent decisions with task_description, skipping");
50        return Ok(Vec::new());
51    }
52
53    let knowledge_path = chat_root
54        .join("memory")
55        .join("evolution")
56        .join("knowledge.md");
57    let existing_summary = if knowledge_path.exists() {
58        let full = skilllite_fs::read_file(&knowledge_path).unwrap_or_default();
59        if full.len() <= EXISTING_KNOWLEDGE_CAP {
60            full
61        } else {
62            // 取末尾一段(最近写入的),便于去重
63            full.chars()
64                .skip(full.len().saturating_sub(EXISTING_KNOWLEDGE_CAP))
65                .collect::<String>()
66        }
67    } else {
68        String::new()
69    };
70
71    let prompt = MEMORY_KNOWLEDGE_PROMPT
72        .replace("{{decisions_summary}}", &summary)
73        .replace("{{existing_knowledge_summary}}", existing_summary.trim());
74    let messages = vec![EvolutionMessage::user(&prompt)];
75    let content = llm
76        .complete(&messages, model, 0.3)
77        .await?
78        .trim()
79        .to_string();
80
81    let parsed = match parse_knowledge_response(&content) {
82        Ok(p) => p,
83        Err(e) => {
84            tracing::warn!(
85                "Memory knowledge extraction parse failed: {} — raw: {:.300}",
86                e,
87                content
88            );
89            let _ = block_in_place(|| {
90                let conn = open_evolution_db(chat_root)?;
91                let _ = crate::log_evolution_event(
92                    &conn,
93                    chat_root,
94                    "memory_extraction_parse_failed",
95                    "",
96                    &format!("{}", e),
97                    "",
98                );
99                Ok::<_, anyhow::Error>(())
100            });
101            return Ok(Vec::new());
102        }
103    };
104
105    let has_any = !parsed.entities.is_empty()
106        || !parsed.relations.is_empty()
107        || !parsed.episodes.is_empty()
108        || !parsed.preferences.is_empty()
109        || !parsed.patterns.is_empty();
110    if parsed.skip_reason.is_some() && !has_any {
111        tracing::debug!(
112            "Memory evolution: LLM skipped extraction — {}",
113            parsed.skip_reason.as_deref().unwrap_or("")
114        );
115        return Ok(Vec::new());
116    }
117
118    let entities = parsed
119        .entities
120        .into_iter()
121        .take(MAX_ENTITIES)
122        .collect::<Vec<_>>();
123    let relations = parsed
124        .relations
125        .into_iter()
126        .take(MAX_RELATIONS)
127        .collect::<Vec<_>>();
128    let episodes = parsed
129        .episodes
130        .into_iter()
131        .take(MAX_EPISODES)
132        .collect::<Vec<_>>();
133    let preferences = parsed
134        .preferences
135        .into_iter()
136        .take(MAX_PREFERENCES)
137        .collect::<Vec<_>>();
138    let patterns = parsed
139        .patterns
140        .into_iter()
141        .take(MAX_PATTERNS)
142        .collect::<Vec<_>>();
143    if entities.is_empty()
144        && relations.is_empty()
145        && episodes.is_empty()
146        && preferences.is_empty()
147        && patterns.is_empty()
148    {
149        return Ok(Vec::new());
150    }
151
152    let entity_block: String = entities
153        .iter()
154        .map(|e| format!("- **{}** ({}) {}", e.name, e.entity_type, e.note))
155        .collect::<Vec<_>>()
156        .join("\n");
157    let relation_block: String = relations
158        .iter()
159        .map(|r| format!("- {} → {}: {}", r.from, r.to, r.relation))
160        .collect::<Vec<_>>()
161        .join("\n");
162    let episode_block: String = episodes
163        .iter()
164        .map(|e| format!("- [{}] {} → 教训:{}", e.outcome, e.summary, e.lesson))
165        .collect::<Vec<_>>()
166        .join("\n");
167    let preference_block: String = preferences
168        .iter()
169        .map(|p| format!("- {}(情境:{})", p.description, p.context))
170        .collect::<Vec<_>>()
171        .join("\n");
172    let pattern_block: String = patterns
173        .iter()
174        .map(|p| format!("- {}({})", p.description, p.evidence))
175        .collect::<Vec<_>>()
176        .join("\n");
177
178    let full_content =
179        format!(
180        "## {}\n\n### 实体\n{}\n\n### 关系\n{}\n\n### 情节\n{}\n\n### 倾向\n{}\n\n### 模式\n{}\n",
181        chrono::Utc::now().format("%Y-%m-%d %H:%M"),
182        if entity_block.is_empty() { "*无*".to_string() } else { entity_block },
183        if relation_block.is_empty() { "*无*".to_string() } else { relation_block },
184        if episode_block.is_empty() { "*无*".to_string() } else { episode_block },
185        if preference_block.is_empty() { "*无*".to_string() } else { preference_block },
186        if pattern_block.is_empty() { "*无*".to_string() } else { pattern_block }
187    );
188
189    if let Err(e) = gatekeeper_l3_content(&full_content) {
190        tracing::warn!("Memory evolution L3 rejected content: {}", e);
191        return Ok(Vec::new());
192    }
193
194    let memory_dir = chat_root.join("memory").join("evolution");
195    let knowledge_path = memory_dir.join("knowledge.md");
196    if !gatekeeper_l1_path(chat_root, &knowledge_path, None) {
197        tracing::warn!(
198            "Memory evolution L1 path rejected: {}",
199            knowledge_path.display()
200        );
201        return Ok(Vec::new());
202    }
203
204    skilllite_fs::create_dir_all(&memory_dir)?;
205    let to_append = full_content;
206    let final_content = if knowledge_path.exists() {
207        let existing = skilllite_fs::read_file(&knowledge_path).unwrap_or_default();
208        format!("{}\n\n---\n\n{}", existing.trim_end(), to_append.trim())
209    } else {
210        format!(
211            "# 进化知识库(实体·关系·情节·倾向·模式)\n\n由 Memory 进化从任务执行记录中自动抽取,仅沉淀事实与经历供检索,不与规则/技能重复。\n\n---\n\n{}",
212            to_append.trim()
213        )
214    };
215    skilllite_fs::write_file(&knowledge_path, &final_content)?;
216
217    tracing::info!(
218        "Memory evolution: wrote {} entities, {} relations, {} episodes, {} preferences, {} patterns to knowledge.md",
219        entities.len(),
220        relations.len(),
221        episodes.len(),
222        preferences.len(),
223        patterns.len()
224    );
225
226    Ok(vec![(
227        "memory_knowledge_added".to_string(),
228        "knowledge".to_string(),
229    )])
230}
231
232fn query_decisions_for_memory(conn: &Connection) -> Result<String> {
233    let sql = format!(
234        "SELECT task_description, total_tools, failed_tools, replans, elapsed_ms, tools_detail, task_completed
235         FROM decisions
236         WHERE ts >= datetime('now', '{}') AND task_description IS NOT NULL
237         ORDER BY ts DESC LIMIT {}",
238        RECENT_DAYS, DECISION_LIMIT
239    );
240    let mut stmt = conn.prepare(&sql)?;
241    let rows: Vec<String> = stmt
242        .query_map([], |row| {
243            let desc: String = row.get(0)?;
244            let total: i64 = row.get(1)?;
245            let failed: i64 = row.get(2)?;
246            let replans: i64 = row.get(3)?;
247            let elapsed: i64 = row.get(4)?;
248            let tools_json: Option<String> = row.get(5)?;
249            let completed: bool = row.get(6)?;
250            let tool_summary = tools_json
251                .as_deref()
252                .and_then(|s| {
253                    let arr: Option<Vec<serde_json::Value>> = serde_json::from_str(s).ok()?;
254                    let names: Vec<String> = arr?
255                        .iter()
256                        .filter_map(|v| v.get("tool").and_then(|t| t.as_str()).map(String::from))
257                        .collect();
258                    Some(names.join(", "))
259                })
260                .unwrap_or_else(|| "—".to_string());
261            Ok(format!(
262                "- 任务: {} | 完成: {} | 工具: {} (失败: {}) | replan: {} | 耗时: {}ms | 工具序列: {}",
263                desc,
264                if completed { "是" } else { "否" },
265                total,
266                failed,
267                replans,
268                elapsed,
269                tool_summary
270            ))
271        })?
272        .filter_map(|r| r.ok())
273        .collect();
274    Ok(rows.join("\n"))
275}
276
277#[derive(Debug, Default, serde::Deserialize)]
278struct KnowledgeResponse {
279    #[serde(default)]
280    entities: Vec<EntityEntry>,
281    #[serde(default)]
282    relations: Vec<RelationEntry>,
283    #[serde(default)]
284    episodes: Vec<EpisodeEntry>,
285    #[serde(default)]
286    preferences: Vec<PreferenceEntry>,
287    #[serde(default)]
288    patterns: Vec<PatternEntry>,
289    #[serde(default)]
290    skip_reason: Option<String>,
291}
292
293#[derive(Debug, serde::Deserialize)]
294struct EpisodeEntry {
295    #[serde(default)]
296    summary: String,
297    #[serde(default)]
298    outcome: String,
299    #[serde(default)]
300    lesson: String,
301}
302
303#[derive(Debug, serde::Deserialize)]
304struct PreferenceEntry {
305    #[serde(default)]
306    description: String,
307    #[serde(default)]
308    context: String,
309}
310
311#[derive(Debug, serde::Deserialize)]
312struct PatternEntry {
313    #[serde(default)]
314    description: String,
315    #[serde(default)]
316    evidence: String,
317}
318
319#[derive(Debug, serde::Deserialize)]
320struct EntityEntry {
321    name: String,
322    #[serde(rename = "type")]
323    entity_type: String,
324    note: String,
325}
326
327#[derive(Debug, serde::Deserialize)]
328struct RelationEntry {
329    from: String,
330    to: String,
331    relation: String,
332}
333
334fn parse_knowledge_response(content: &str) -> Result<KnowledgeResponse> {
335    let cleaned = crate::strip_think_blocks(content.trim());
336    let json_str = crate::prompt_learner::extract_json_block(cleaned);
337    let parsed: KnowledgeResponse = serde_json::from_str(&json_str)
338        .map_err(|e| anyhow::anyhow!("memory knowledge JSON parse error: {}", e))?;
339    Ok(parsed)
340}