use super::common::{HistoryItem, HistoryProcessingOptions, messages_to_history};
use once_cell::sync::Lazy;
use regex::Regex;
use stakpak_shared::models::{
integrations::openai::{ChatMessage, Role},
llm::{LLMMessage, LLMMessageContent},
};
use std::collections::HashMap;
pub struct ScratchpadContextManager {
options: HistoryProcessingOptions,
}
impl super::ContextManager for ScratchpadContextManager {
fn reduce_context(&self, messages: Vec<ChatMessage>) -> Vec<LLMMessage> {
let mut scratchpad = HashMap::new();
for message in messages.iter() {
if let Some(content) = message.content.clone()
&& let Some(scratchpad_content) = extract_scratchpad(&content.to_string())
{
scratchpad.extend(scratchpad_content);
}
}
let history = messages_to_history(&messages, &self.options);
let context_content = self.history_to_text(&history, &scratchpad);
vec![LLMMessage {
role: Role::User.to_string(),
content: LLMMessageContent::String(context_content),
}]
}
}
pub struct ScratchpadContextManagerOptions {
pub history_action_message_size_limit: usize,
pub history_action_message_keep_last_n: usize,
pub history_action_result_keep_last_n: usize,
}
impl ScratchpadContextManager {
pub fn new(options: ScratchpadContextManagerOptions) -> Self {
Self {
options: HistoryProcessingOptions {
history_action_message_size_limit: options.history_action_message_size_limit,
history_action_message_keep_last_n: options.history_action_message_keep_last_n,
history_action_result_keep_last_n: options.history_action_result_keep_last_n,
truncation_hint: "consult the scratchpad instead".to_string(),
},
}
}
fn history_to_text(
&self,
history: &[HistoryItem],
scratchpad: &HashMap<String, String>,
) -> String {
let mut content = String::new();
content.push_str(&format!(
r#"
<scratchpad>
{}
</scratchpad>"#,
scratchpad
.iter()
.map(|(key, value)| format!(
r#"
<{}>
{}
</{}>
"#,
key,
value, key
))
.collect::<Vec<String>>()
.join("\n")
));
content.push_str(&format!(
r#"
<history>
{}
</history>"#,
history
.iter()
.map(|item| item.to_string())
.collect::<Vec<String>>()
.join("\n"),
));
content.trim().to_string()
}
}
fn extract_scratchpad(content: &str) -> Option<HashMap<String, String>> {
static SCRATCHPAD_RE: Lazy<Option<Regex>> =
Lazy::new(|| match Regex::new(r"<scratchpad>(?s)(.*?)</scratchpad>") {
Ok(re) => Some(re),
Err(e) => {
println!("Failed to create scratchpad regex: {}", e);
None
}
});
static XML_TAG_RE: Lazy<Option<Regex>> = Lazy::new(|| {
match Regex::new(r"<([a-zA-Z_][a-zA-Z0-9_-]*?)>(?s)(.*?)</([a-zA-Z_][a-zA-Z0-9_-]*?)>") {
Ok(re) => Some(re),
Err(e) => {
println!("Failed to create XML tag regex: {}", e);
None
}
}
});
let scratchpad_content = match SCRATCHPAD_RE.as_ref() {
Some(re) => re
.captures(content)
.and_then(|cap| cap.get(1))
.map(|m| m.as_str().trim().to_string())?,
None => return None,
};
let mut result = HashMap::new();
if let Some(xml_re) = XML_TAG_RE.as_ref() {
for cap in xml_re.captures_iter(&scratchpad_content) {
if let (Some(opening_tag), Some(tag_content), Some(closing_tag)) =
(cap.get(1), cap.get(2), cap.get(3))
{
let opening_name = opening_tag.as_str();
let closing_name = closing_tag.as_str();
if opening_name == closing_name {
result.insert(
opening_name.to_string(),
tag_content.as_str().trim().to_string(),
);
}
}
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hashmap_extend_overrides_old_values() {
let mut scratchpad = HashMap::new();
scratchpad.insert("key1".to_string(), "old_value1".to_string());
scratchpad.insert("key2".to_string(), "old_value2".to_string());
scratchpad.insert("key3".to_string(), "unchanged".to_string());
let mut new_content = HashMap::new();
new_content.insert("key1".to_string(), "new_value1".to_string());
new_content.insert("key2".to_string(), "new_value2".to_string());
new_content.insert("key4".to_string(), "added_value".to_string());
scratchpad.extend(new_content);
assert_eq!(scratchpad.get("key1"), Some(&"new_value1".to_string()));
assert_eq!(scratchpad.get("key2"), Some(&"new_value2".to_string()));
assert_eq!(scratchpad.get("key3"), Some(&"unchanged".to_string()));
assert_eq!(scratchpad.get("key4"), Some(&"added_value".to_string()));
assert_eq!(scratchpad.len(), 4);
}
}