1use 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;
25const EXISTING_KNOWLEDGE_CAP: usize = 3500;
27
28const 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
35pub 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 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}