diff --git a/scratch_schema.rs b/scratch_schema.rs new file mode 100644 index 0000000..041c74f --- /dev/null +++ b/scratch_schema.rs @@ -0,0 +1,25 @@ +use schemars::schema_for; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(JsonSchema, Deserialize, Serialize)] +#[serde(tag = "action", rename_all = "snake_case")] +pub enum TasksToolEnum { + /// Create task. + Add { + /// Task title. + title: String, + /// Task description. + description: Option, + }, + /// Delete task. + Delete { + /// Task ID. + id: String, + } +} + +fn main() { + let schema = schema_for!(TasksToolEnum); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/server/src/bin/dump_schema.rs b/server/src/bin/dump_schema.rs new file mode 100644 index 0000000..77bc773 --- /dev/null +++ b/server/src/bin/dump_schema.rs @@ -0,0 +1,6 @@ +use mcp_memory_server::tools::{DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool}; +use schemars::schema_for; + +fn main() { + println!("{}", serde_json::to_string_pretty(&schema_for!(AgentSignalsTool)).unwrap()); +} diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index c5494cf..efdc4dc 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -24,24 +24,7 @@ impl McpTool for LogErrorFixHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution); - let mut solution = req.solution; - - if state.ollama.is_available().await { - let prompt = format!( - "Analyze this error signature and solution. Output 1 sentence summarizing the root cause and fix:\nSignature: {}\nSolution: {}", - req.signature, solution - ); - if let Ok(summary) = state - .ollama - .generate(&prompt, Some(&state.ollama.reasoning_model), None, None) - .await - { - let clean = summary.trim(); - if !clean.is_empty() { - solution = format!("{} (AI Analysis: {})", solution, clean); - } - } - } + let solution = req.solution; let embedding = crate::embedding::generate_embedding_async(text_to_embed) .await @@ -204,18 +187,6 @@ impl McpTool for LogCodeChangeHandler { description = format!("{} [Symbols: {}]", description, symbols.join(", ")); } - if state.ollama.is_available().await { - let prompt = format!( - "Summarize in 1 concise sentence the architectural impact of changing file '{}': {}", - req.file_path, description - ); - if let Ok(summary) = state.ollama.generate(&prompt, None, None, None).await { - let clean = summary.trim(); - if !clean.is_empty() { - description = format!("{} (AI Summary: {})", description, clean); - } - } - } let change_kind = match req .change_kind .as_deref() diff --git a/server/src/lib.rs b/server/src/lib.rs index 3bf43cc..04298e9 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -285,76 +285,61 @@ pub async fn memory_consolidation_worker(state: Arc) { _ = interval.tick() => {}, } - let entities: Vec<_> = state.graph.read_with(|g| { - g.entities - .values() - .map(|e| (e.name.clone(), e.entity_type.clone())) - .collect() + let entities: Vec = state.graph.read_with(|g| { + g.entities.keys().cloned().collect() }); if entities.len() < 2 { continue; } - let mut entity_summaries = String::new(); - for (name, e_type) in entities.iter().take(50) { - entity_summaries.push_str(&format!("- [{}] {}\n", e_type, name)); - } + let mut duplicates = None; + let mut seen = std::collections::HashMap::new(); + for name in &entities { + let normalized: String = name + .chars() + .filter(|c| c.is_alphanumeric()) + .flat_map(|c| c.to_lowercase()) + .collect(); + + // Skip empty normalized names + if normalized.is_empty() { + continue; + } - let prompt = format!( - "Analyze the following list of entities and identify exactly TWO that represent the exact same concept or item but have slightly different names (e.g. 'auth_service' and 'AuthService'). Return ONLY a valid JSON array containing exactly two strings: the two names to merge. If no obvious duplicates exist, return an empty array []. Do not output any markdown formatting or extra text.\n\nEntities:\n{}", - entity_summaries - ); - - if let Ok(response) = state - .ollama - .generate( - &prompt, - None, - Some("You are a helpful JSON-only data deduplication assistant. Output only JSON."), - Some("json"), - ) - .await - { - let cleaned = response - .trim() - .trim_start_matches("```json") - .trim_start_matches("```") - .trim_end_matches("```") - .trim(); - if let Ok(duplicates) = serde_json::from_str::>(cleaned) - && duplicates.len() == 2 - { - let e1_name = &duplicates[0]; - let e2_name = &duplicates[1]; - - if e1_name != e2_name { - tracing::info!( - "Memory Consolidation Daemon: Merging '{}' into '{}'", - e2_name, - e1_name - ); - state.modify_graph(|g| { - if let Some(mut e2) = g.entities.remove(e2_name) { - if let Some(e1) = g.entities.get_mut(e1_name) { - e1.observations.append(&mut e2.observations); - } else { - g.entities.insert(e2_name.clone(), e2); - } - } - - for rel in g.relations.iter_mut() { - if rel.from == *e2_name { - rel.from = e1_name.clone(); - } - if rel.to == *e2_name { - rel.to = e1_name.clone(); - } - } - }); + if let Some(existing) = seen.insert(normalized, name.clone()) { + if existing != *name { + duplicates = Some((existing, name.clone())); + break; } } } + + if let Some((e1_name, e2_name)) = duplicates { + tracing::info!( + "Memory Consolidation Daemon: Merging '{}' into '{}'", + e2_name, + e1_name + ); + state.modify_graph(|g| { + if let Some(mut e2) = g.entities.remove(&e2_name) { + if let Some(e1) = g.entities.get_mut(&e1_name) { + e1.observations.append(&mut e2.observations); + } else { + g.entities.insert(e2_name.clone(), e2); + } + } + + for rel in g.relations.iter_mut() { + if rel.from == e2_name { + rel.from = e1_name.clone(); + } + if rel.to == e2_name { + rel.to = e1_name.clone(); + } + } + }); + } } }