fix memory
This commit is contained in:
@@ -5,10 +5,13 @@ use std::path::PathBuf;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
pub const DEFAULT_MEMORY: u64 = 100;
|
||||
|
||||
pub struct Config {
|
||||
pub path: Option<String>,
|
||||
pub did: Option<String>,
|
||||
pub handle: Option<String>,
|
||||
pub memory: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -21,6 +24,7 @@ struct BotConfig {
|
||||
did: Option<String>,
|
||||
handle: Option<String>,
|
||||
path: Option<String>,
|
||||
memory: Option<u64>,
|
||||
}
|
||||
|
||||
pub fn config_file() -> PathBuf {
|
||||
@@ -49,6 +53,7 @@ pub fn load() -> Config {
|
||||
path: bot.path,
|
||||
did: bot.did,
|
||||
handle: bot.handle,
|
||||
memory: bot.memory.unwrap_or(DEFAULT_MEMORY),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -58,6 +63,7 @@ pub fn load() -> Config {
|
||||
path: None,
|
||||
did: None,
|
||||
handle: None,
|
||||
memory: DEFAULT_MEMORY,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,24 +14,39 @@ pub fn read_core() -> Result<Value> {
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
pub fn read_memory() -> Result<Option<Value>> {
|
||||
pub fn read_memory_all() -> Result<Vec<Value>> {
|
||||
let cfg = config::load();
|
||||
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
|
||||
if !dir.exists() {
|
||||
return Ok(None);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut files: Vec<_> = fs::read_dir(&dir)?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
|
||||
.collect();
|
||||
if files.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
files.sort_by_key(|e| e.file_name());
|
||||
let latest = files.last().unwrap().path();
|
||||
let content = fs::read_to_string(&latest)
|
||||
.with_context(|| format!("Failed to read {}", latest.display()))?;
|
||||
let record: Value = serde_json::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse {}", latest.display()))?;
|
||||
Ok(Some(record))
|
||||
|
||||
let mut records = Vec::new();
|
||||
for entry in &files {
|
||||
let path = entry.path();
|
||||
let content = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
let record: Value = serde_json::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
records.push(record);
|
||||
}
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
pub fn memory_count() -> usize {
|
||||
let cfg = config::load();
|
||||
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
|
||||
fs::read_dir(&dir)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde_json::json;
|
||||
use std::fs;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::core::{config, reader};
|
||||
use crate::core::config;
|
||||
|
||||
fn generate_tid() -> String {
|
||||
// ATProto TID: 64-bit integer as 13 base32-sortable chars
|
||||
@@ -23,18 +23,11 @@ fn generate_tid() -> String {
|
||||
tid
|
||||
}
|
||||
|
||||
fn next_version() -> u64 {
|
||||
match reader::read_memory() {
|
||||
Ok(Some(record)) => record["value"]["version"].as_u64().unwrap_or(0) + 1,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a single memory element as a new TID file
|
||||
pub fn save_memory(content: &str) -> Result<()> {
|
||||
let cfg = config::load();
|
||||
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
|
||||
let tid = generate_tid();
|
||||
let version = next_version();
|
||||
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
let record = json!({
|
||||
@@ -46,7 +39,6 @@ pub fn save_memory(content: &str) -> Result<()> {
|
||||
"$type": "ai.syui.gpt.memory#markdown",
|
||||
"text": content
|
||||
},
|
||||
"version": version,
|
||||
"createdAt": now
|
||||
}
|
||||
});
|
||||
@@ -59,3 +51,62 @@ pub fn save_memory(content: &str) -> Result<()> {
|
||||
fs::write(&path, json_str)
|
||||
.with_context(|| format!("Failed to write {}", path.display()))
|
||||
}
|
||||
|
||||
/// Delete all memory files, then write new ones from the given items
|
||||
pub fn compress_memory(items: &[String]) -> Result<()> {
|
||||
let cfg = config::load();
|
||||
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
|
||||
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
|
||||
|
||||
// delete all existing memory files
|
||||
if dir.exists() {
|
||||
for entry in fs::read_dir(&dir)? {
|
||||
let entry = entry?;
|
||||
if entry.path().extension().is_some_and(|ext| ext == "json") {
|
||||
fs::remove_file(entry.path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("Failed to create {}", dir.display()))?;
|
||||
|
||||
// write each item as a new TID file
|
||||
for item in items {
|
||||
let micros = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_micros() as u64;
|
||||
// small delay to ensure unique TIDs
|
||||
std::thread::sleep(std::time::Duration::from_micros(1));
|
||||
|
||||
let v = (micros << 10) & 0x7FFFFFFFFFFFFFFF;
|
||||
const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
|
||||
let mut tid = String::with_capacity(13);
|
||||
for i in (0..13).rev() {
|
||||
let idx = ((v >> (i * 5)) & 0x1f) as usize;
|
||||
tid.push(CHARSET[idx] as char);
|
||||
}
|
||||
|
||||
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
let record = json!({
|
||||
"uri": format!("at://{}/ai.syui.gpt.memory/{}", did, tid),
|
||||
"value": {
|
||||
"$type": "ai.syui.gpt.memory",
|
||||
"did": did,
|
||||
"content": {
|
||||
"$type": "ai.syui.gpt.memory#markdown",
|
||||
"text": item
|
||||
},
|
||||
"createdAt": now
|
||||
}
|
||||
});
|
||||
|
||||
let path = dir.join(format!("{}.json", tid));
|
||||
let json_str = serde_json::to_string_pretty(&record)?;
|
||||
fs::write(&path, json_str)
|
||||
.with_context(|| format!("Failed to write {}", path.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
37
src/main.rs
37
src/main.rs
@@ -18,13 +18,13 @@ enum Commands {
|
||||
/// Start MCP server (JSON-RPC over stdio)
|
||||
Server,
|
||||
|
||||
/// Read core.json
|
||||
/// Read core record
|
||||
ReadCore,
|
||||
|
||||
/// Read latest memory record
|
||||
/// Read all memory records
|
||||
ReadMemory,
|
||||
|
||||
/// Create new memory record
|
||||
/// Add a single memory element
|
||||
SaveMemory {
|
||||
/// Content to write
|
||||
content: String,
|
||||
@@ -51,15 +51,19 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
Some(Commands::ReadMemory) => {
|
||||
match reader::read_memory()? {
|
||||
Some(record) => println!("{}", serde_json::to_string_pretty(&record)?),
|
||||
None => println!("No memory records found"),
|
||||
let records = reader::read_memory_all()?;
|
||||
if records.is_empty() {
|
||||
println!("No memory records found");
|
||||
} else {
|
||||
for record in &records {
|
||||
println!("{}", serde_json::to_string_pretty(record)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Commands::SaveMemory { content }) => {
|
||||
writer::save_memory(&content)?;
|
||||
println!("Saved.");
|
||||
println!("Saved. ({} records)", reader::memory_count());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,30 +76,17 @@ fn print_status() {
|
||||
let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string());
|
||||
let base = config::base_dir(&cfg);
|
||||
let id = config::identity(&cfg);
|
||||
|
||||
let memory_dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
|
||||
let memory_count = std::fs::read_dir(&memory_dir)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let latest_version = match reader::read_memory() {
|
||||
Ok(Some(record)) => record["value"]["version"].as_u64().unwrap_or(0),
|
||||
_ => 0,
|
||||
};
|
||||
let count = reader::memory_count();
|
||||
|
||||
println!("aigpt - AI memory MCP server\n");
|
||||
println!("config: {}", config::config_file().display());
|
||||
println!("did: {}", did);
|
||||
println!("handle: {}", handle);
|
||||
println!("memory: {}", cfg.memory);
|
||||
println!();
|
||||
println!("path: {}/", base.display());
|
||||
println!(" {}/{}", id, "ai.syui.gpt.core/self.json");
|
||||
println!(" {}/{}", id, "ai.syui.gpt.memory/*.json");
|
||||
println!();
|
||||
println!("memory: {} records (version: {})", memory_count, latest_version);
|
||||
println!("records: {}/{}", count, cfg.memory);
|
||||
}
|
||||
|
||||
@@ -90,8 +90,9 @@ impl MCPServer {
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(Some(memory)) = reader::read_memory() {
|
||||
if let Some(text) = memory["value"]["content"]["text"].as_str() {
|
||||
let records = reader::read_memory_all().unwrap_or_default();
|
||||
for record in &records {
|
||||
if let Some(text) = record["value"]["content"]["text"].as_str() {
|
||||
if !text.is_empty() {
|
||||
parts.push(text.to_string());
|
||||
}
|
||||
@@ -105,7 +106,7 @@ impl MCPServer {
|
||||
let tools = vec![
|
||||
json!({
|
||||
"name": "read_core",
|
||||
"description": "Read core.md - the AI's identity and instructions",
|
||||
"description": "Read the AI's identity and instructions (core record)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -113,7 +114,7 @@ impl MCPServer {
|
||||
}),
|
||||
json!({
|
||||
"name": "read_memory",
|
||||
"description": "Read memory.md - the AI's accumulated memories",
|
||||
"description": "Read all memory records. Each record is a single memory element.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -121,13 +122,13 @@ impl MCPServer {
|
||||
}),
|
||||
json!({
|
||||
"name": "save_memory",
|
||||
"description": "Overwrite memory.md with new content",
|
||||
"description": "Add a single memory element as a new record",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write to memory.md"
|
||||
"description": "A single memory element to save"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
@@ -135,16 +136,17 @@ impl MCPServer {
|
||||
}),
|
||||
json!({
|
||||
"name": "compress",
|
||||
"description": "Compress conversation into memory. AI decides what to keep, tool writes the result to memory.md",
|
||||
"description": "Replace all memory records with a compressed set. Deletes all existing records and creates new ones from the provided items.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conversation": {
|
||||
"type": "string",
|
||||
"description": "Compressed memory content to save"
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Array of memory elements to keep after compression"
|
||||
}
|
||||
},
|
||||
"required": ["conversation"]
|
||||
"required": ["items"]
|
||||
}
|
||||
}),
|
||||
];
|
||||
@@ -192,9 +194,8 @@ impl MCPServer {
|
||||
}
|
||||
|
||||
fn tool_read_memory(&self) -> Value {
|
||||
match reader::read_memory() {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => json!({ "content": "" }),
|
||||
match reader::read_memory_all() {
|
||||
Ok(records) => json!({ "records": records, "count": records.len() }),
|
||||
Err(e) => json!({ "error": e.to_string() }),
|
||||
}
|
||||
}
|
||||
@@ -202,15 +203,23 @@ impl MCPServer {
|
||||
fn tool_save_memory(&self, arguments: &Value) -> Value {
|
||||
let content = arguments["content"].as_str().unwrap_or("");
|
||||
match writer::save_memory(content) {
|
||||
Ok(()) => json!({ "success": true }),
|
||||
Ok(()) => json!({ "success": true, "count": reader::memory_count() }),
|
||||
Err(e) => json!({ "error": e.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_compress(&self, arguments: &Value) -> Value {
|
||||
let conversation = arguments["conversation"].as_str().unwrap_or("");
|
||||
match writer::save_memory(conversation) {
|
||||
Ok(()) => json!({ "success": true }),
|
||||
let items: Vec<String> = arguments["items"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
match writer::compress_memory(&items) {
|
||||
Ok(()) => json!({ "success": true, "count": items.len() }),
|
||||
Err(e) => json!({ "error": e.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user