rust_agent/memory/
utils.rs1use std::path::{Path, PathBuf};
3use anyhow::{Error, Result};
4use serde::{Serialize, Deserialize};
5use serde_json::Value;
6use log::warn;
7
8pub async fn ensure_data_dir_exists(data_dir: &Path) -> Result<()> {
10 if !data_dir.exists() {
11 tokio::fs::create_dir_all(data_dir).await?;
12 }
13 Ok(())
14}
15
16pub fn estimate_token_count(text: &str) -> usize {
19 text.len() / 4
22}
23
24pub fn estimate_text_tokens(text: &str) -> usize {
27 let chinese_chars = text.chars().filter(|c| {
31 let c = *c as u32;
32 (0x4E00..=0x9FFF).contains(&c) ||
34 (0x3400..=0x4DBF).contains(&c) ||
35 (0x20000..=0x2A6DF).contains(&c) ||
36 (0x2A700..=0x2B73F).contains(&c) ||
37 (0x2B740..=0x2B81F).contains(&c) ||
38 (0x2B820..=0x2CEAF).contains(&c) ||
39 (0xF900..=0xFAFF).contains(&c) ||
40 (0x2F800..=0x2FA1F).contains(&c)
41 }).count();
42
43 let non_chinese_chars = text.chars().count() - chinese_chars;
44
45 chinese_chars + non_chinese_chars / 4
47}
48
49pub fn estimate_json_token_count(value: &Value) -> usize {
51 match value {
52 Value::String(s) => estimate_token_count(s),
53 Value::Number(_) => 1, Value::Bool(_) => 1, Value::Null => 1, Value::Array(arr) => {
57 let mut count = 2; for item in arr {
60 count += estimate_json_token_count(item) + 1; }
62 count
63 }
64 Value::Object(obj) => {
65 let mut count = 2; for (key, value) in obj {
68 count += estimate_token_count(key) + 1; count += estimate_json_token_count(value) + 1; }
71 count
72 }
73 }
74}
75
76pub fn serialize_to_string(value: &Value) -> Result<String> {
78 serde_json::to_string(value).map_err(|e| {
79 warn!("Failed to serialize JSON value: {}", e);
80 Error::from(e)
81 })
82}
83
84pub fn deserialize_from_str(json_str: &str) -> Result<Value> {
86 serde_json::from_str(json_str).map_err(|e| {
87 warn!("Failed to deserialize JSON string: {}", e);
88 Error::from(e)
89 })
90}
91
92pub fn current_timestamp() -> String {
94 chrono::Utc::now().to_rfc3339()
95}
96
97pub fn parse_timestamp(timestamp: &str) -> Result<chrono::DateTime<chrono::Utc>> {
99 timestamp.parse::<chrono::DateTime<chrono::Utc>>().map_err(|e| {
100 warn!("Failed to parse timestamp '{}': {}", timestamp, e);
101 Error::from(e)
102 })
103}
104
105pub fn get_session_file_path(data_dir: &Path, session_id: &str, suffix: &str) -> PathBuf {
107 data_dir.join(format!("{}_{}", session_id, suffix))
108}
109
110pub fn create_backup_path(file_path: &Path) -> PathBuf {
112 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
113 let parent = file_path.parent().unwrap_or_else(|| Path::new("."));
114 let file_stem = file_path.file_stem().unwrap_or_else(|| std::ffi::OsStr::new("backup"));
115 let extension = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
116
117 if extension.is_empty() {
118 parent.join(format!("{}_{}.backup", file_stem.to_string_lossy(), timestamp))
119 } else {
120 parent.join(format!("{}_{}_{}.backup", file_stem.to_string_lossy(), timestamp, extension))
121 }
122}
123
124pub async fn read_file_content(file_path: &Path) -> Result<String> {
126 tokio::fs::read_to_string(file_path).await.map_err(|e| {
127 warn!("Failed to read file '{}': {}", file_path.display(), e);
128 Error::from(e)
129 })
130}
131
132pub async fn write_file_content(file_path: &Path, content: &str) -> Result<()> {
134 tokio::fs::write(file_path, content).await.map_err(|e| {
135 warn!("Failed to write file '{}': {}", file_path.display(), e);
136 Error::from(e)
137 })
138}
139
140pub async fn atomic_write_file(file_path: &Path, content: &str) -> Result<()> {
142 let temp_path = file_path.with_extension("tmp");
144
145 if let Some(parent) = file_path.parent() {
147 ensure_data_dir_exists(parent).await?;
148 }
149
150 write_file_content(&temp_path, content).await?;
152
153 tokio::fs::rename(&temp_path, file_path).await.map_err(|e| {
155 warn!("Failed to rename temporary file to '{}': {}", file_path.display(), e);
156 Error::from(e)
157 })?;
158
159 Ok(())
160}
161
162pub async fn append_to_file(file_path: &Path, content: &str) -> Result<()> {
164 use tokio::io::AsyncWriteExt;
165
166 if let Some(parent) = file_path.parent() {
168 ensure_data_dir_exists(parent).await?;
169 }
170
171 let mut file = tokio::fs::OpenOptions::new()
173 .create(true)
174 .append(true)
175 .open(file_path)
176 .await?;
177
178 file.write_all(content.as_bytes()).await?;
179 file.flush().await?;
180
181 Ok(())
182}
183
184pub async fn file_exists(file_path: &Path) -> bool {
186 tokio::fs::metadata(file_path).await.is_ok()
187}
188
189pub async fn delete_file(file_path: &Path) -> Result<()> {
191 if file_exists(file_path).await {
192 tokio::fs::remove_file(file_path).await.map_err(|e| {
193 warn!("Failed to delete file '{}': {}", file_path.display(), e);
194 Error::from(e)
195 })?;
196 }
197 Ok(())
198}
199
200pub async fn ensure_dir_exists(dir_path: &Path) -> Result<()> {
202 if !dir_path.exists() {
203 tokio::fs::create_dir_all(dir_path).await.map_err(|e| {
204 warn!("Failed to create directory '{}': {}", dir_path.display(), e);
205 Error::from(e)
206 })?;
207 }
208 Ok(())
209}
210
211pub fn get_env_var(key: &str, default: &str) -> String {
213 std::env::var(key).unwrap_or_else(|_| default.to_string())
214}
215
216pub fn get_data_dir_from_env() -> PathBuf {
218 let default_dir = "./data/memory";
219 let dir_str = get_env_var("MEMORY_DATA_DIR", default_dir);
220 PathBuf::from(dir_str)
221}
222
223pub fn get_summary_threshold_from_env() -> usize {
225 let default_threshold = 3500;
226 let threshold_str = get_env_var("MEMORY_SUMMARY_THRESHOLD", &default_threshold.to_string());
227 threshold_str.parse().unwrap_or(default_threshold)
228}
229
230pub fn get_recent_messages_count_from_env() -> usize {
232 let default_count = 10;
233 let count_str = get_env_var("MEMORY_RECENT_MESSAGES_COUNT", &default_count.to_string());
234 count_str.parse().unwrap_or(default_count)
235}
236
237pub fn generate_session_id() -> String {
239 uuid::Uuid::new_v4().to_string()
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use tempfile::TempDir;
246
247 #[tokio::test]
248 async fn test_ensure_data_dir_exists() {
249 let temp_dir = TempDir::new().unwrap();
250 let dir_path = temp_dir.path().join("test_dir");
251
252 assert!(!dir_path.exists());
253 ensure_data_dir_exists(&dir_path).await.unwrap();
254 assert!(dir_path.exists());
255 }
256
257 #[test]
258 fn test_estimate_token_count() {
259 assert_eq!(estimate_token_count(""), 0);
260 assert_eq!(estimate_token_count("hello world"), 2); assert_eq!(estimate_token_count("a".repeat(20).as_str()), 5); }
263
264 #[test]
265 fn test_current_timestamp() {
266 let timestamp = current_timestamp();
267 assert!(parse_timestamp(×tamp).is_ok());
268 }
269
270 #[test]
271 fn test_get_session_file_path() {
272 let data_dir = Path::new("/tmp");
273 let session_id = "test_session";
274 let suffix = "json";
275
276 let path = get_session_file_path(data_dir, session_id, suffix);
277 assert_eq!(path, PathBuf::from("/tmp/test_session_json"));
278 }
279
280 #[test]
281 fn test_get_env_var() {
282 let key = "MEMORY_TEST_VAR";
283 let default = "default_value";
284
285 std::env::remove_var(key);
287 assert_eq!(get_env_var(key, default), default);
288
289 std::env::set_var(key, "test_value");
291 assert_eq!(get_env_var(key, default), "test_value");
292
293 std::env::remove_var(key);
295 }
296
297 #[test]
298 fn test_generate_session_id() {
299 let id1 = generate_session_id();
300 let id2 = generate_session_id();
301
302 assert_ne!(id1, id2);
303 assert_eq!(id1.len(), 36); assert_eq!(id2.len(), 36); }
306}