use crate::memory::store::{EphemeralEntry, MemoryEntry, MemoryStore, SkillEntry};
use crate::tool::ToolSpec;
pub fn build_injected_prompt(
base_prompt: &str,
store: &MemoryStore,
user_input: &str,
_tool_list: &[ToolSpec],
max_memories: usize,
max_skills: usize,
ephemeral: &[EphemeralEntry],
) -> String {
let mut parts = vec![base_prompt.to_string()];
if !ephemeral.is_empty() {
let mut e_lines = vec!["\n<session_context>".into()];
e_lines.push("Current session context (topics discussed this conversation):".into());
for e in ephemeral {
e_lines.push(format!(" • {}: {}", e.topic, e.detail));
}
e_lines.push("</session_context>".into());
parts.push(e_lines.join("\n"));
}
let all_memories = store
.list_memories(None, (max_memories * 3) as i64)
.unwrap_or_default();
let now = chrono::Utc::now();
let mut scored: Vec<(f64, &MemoryEntry)> = all_memories
.iter()
.map(|m| {
let days_since = chrono::NaiveDateTime::parse_from_str(
&m.updated_at,
"%Y-%m-%d %H:%M:%S",
)
.ok()
.and_then(|dt| {
let updated = dt.and_utc();
let days = (now - updated).num_days().max(0) as f64;
Some(days)
})
.unwrap_or(0.0);
let freshness = 1.0 / (days_since + 1.0);
let keywords = extract_keywords(user_input);
let topic_match = keywords
.iter()
.filter(|kw| m.content.to_lowercase().contains(kw.as_str()))
.count() as f64
* 2.0;
let score = m.importance as f64 * 2.0
+ m.access_count as f64 * 0.1
+ freshness * 5.0
+ topic_match;
(score, m)
})
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let selected: Vec<&MemoryEntry> = scored
.iter()
.take(max_memories)
.map(|(_, e)| *e)
.collect();
if !selected.is_empty() {
let mut lines = vec!["\n<memory_context>".into()];
lines.push(
"The following are facts I have learned, ranked by relevance and confidence:"
.into(),
);
let mut seen = std::collections::HashSet::new();
for m in &selected {
if seen.contains(&m.id) {
continue;
}
seen.insert(m.id);
let tag_str = if m.tags.is_empty() {
String::new()
} else {
format!(" [{}]", m.tags.join(", "))
};
let confidence = if m.access_count >= 5 {
"high"
} else if m.access_count >= 3 {
"medium"
} else {
"low"
};
lines.push(format!(
" [{}][{}] (confidence: {}, accessed {}x) {}{}",
m.target, m.category, confidence, m.access_count, m.content, tag_str
));
}
lines.push("</memory_context>".into());
parts.push(lines.join("\n"));
}
if !user_input.is_empty() {
let keywords = extract_keywords(user_input);
let mut matched_skills: Vec<SkillEntry> = Vec::new();
for kw in &keywords {
if let Ok(results) = store.search_skills(kw, 3) {
for s in results {
if !matched_skills.iter().any(|ms| ms.id == s.id) {
matched_skills.push(s);
if matched_skills.len() >= max_skills {
break;
}
}
}
}
if matched_skills.len() >= max_skills {
break;
}
}
if !matched_skills.is_empty() {
let mut s_lines = vec!["\n<relevant_skills>".into()];
s_lines.push(
"The following skills match the current task and should be followed:"
.into(),
);
for s in &matched_skills {
s_lines.push(format!(" 📋 {} (v{})", s.name, s.version));
s_lines.push(format!(" {}", s.description));
}
s_lines.push("</relevant_skills>".into());
parts.push(s_lines.join("\n"));
}
}
parts.push(
"\n\
## Self-Learning Protocol (proactive)\n\
\n\
I learn automatically from every interaction:\n\
\n\
1. **Save facts** — When the user shares anything useful (name, preferences,\n\
project details, tools they use), IMMEDIATELY call save_memory.\n\
Target='user' for personal facts, 'memory' for general knowledge.\n\
Include relevant tags and importance level (1-5).\n\
\n\
2. **Recall before answering** — If a question might reference previous\n\
conversations, search_memory first.\n\
\n\
3. **Learn preferences** — Detect patterns like 'I use X', 'I prefer Y',\n\
'my project is Z' and save them as preferences.\n\
\n\
4. **Skills from workflows** — After multi-step tasks, offer to save as\n\
a skill with create_skill.\n\
\n\
5. **Error lessons** — If tools fail twice, save an error→fix lesson.\n\
\n\
6. **NEVER use self_inspect/self_read/self_patch** unless directly asked\n\
to debug or fix a bug.\n"
.into(),
);
parts.join("\n")
}
fn extract_keywords(text: &str) -> Vec<String> {
let cleaned: String = text
.chars()
.map(|c| if c.is_alphanumeric() || c.is_whitespace() { c } else { ' ' })
.collect();
let words: Vec<&str> = cleaned.split_whitespace().collect();
let stopwords: std::collections::HashSet<&str> = [
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
"have", "has", "had", "do", "does", "did", "will", "would", "could",
"should", "may", "might", "shall", "can", "need", "dare", "ought",
"used", "to", "of", "in", "for", "on", "with", "at", "by", "from",
"as", "into", "through", "during", "before", "after", "above", "below",
"between", "out", "off", "over", "under", "again", "further", "then",
"once", "here", "there", "when", "where", "why", "how", "all", "each",
"every", "both", "few", "more", "most", "other", "some", "such", "no",
"nor", "not", "only", "own", "same", "so", "than", "too", "very",
"just", "because", "but", "and", "or", "if", "while", "that", "this",
"these", "those", "it", "its", "what", "which", "who", "whom",
"about", "up", "down", "like", "also", "get", "got", "tell", "lets",
"let", "know", "make", "doesnt", "dont", "cant", "wont",
]
.iter()
.cloned()
.collect();
let mut seen = std::collections::HashSet::new();
let mut keywords: Vec<String> = Vec::new();
for w in words {
let lower = w.to_lowercase();
if lower.len() > 2 && !stopwords.contains(lower.as_str()) && seen.insert(lower.clone()) {
keywords.push(lower);
if keywords.len() >= 8 {
break;
}
}
}
keywords
}