feat(server): refactor handlers, router, state management, and memory tools
This commit is contained in:
1 parent
87ddb01063
commit
a083719cf1
36 files changed
+1899
-597
No files matched your search
+208
-88
@@ -39,9 +39,9 @@ impl McpTool for LogDecisionHandler {
|
||||
final_id = format!("ADR-{:04}", adrs.len() + 1);
|
||||
let a = Adr {
|
||||
id: final_id.clone(),
|
||||
title: req.title,
|
||||
title: req.title.clone(),
|
||||
context: req.context,
|
||||
decision: req.decision,
|
||||
decision: req.decision.clone(),
|
||||
consequence: req.consequence,
|
||||
status: "accepted".to_string(),
|
||||
supersedes: req.supersedes,
|
||||
@@ -52,7 +52,8 @@ impl McpTool for LogDecisionHandler {
|
||||
adrs.push(a);
|
||||
});
|
||||
|
||||
Ok(format!("Decision logged as {}", final_id))
|
||||
state.record_activity("decision", &format!("Logged {}: {}", final_id, req.title), Some(&req.decision));
|
||||
Ok(format!("Logged decision {}: {}", final_id, req.title))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +120,9 @@ impl McpTool for DeleteDecisionHandler {
|
||||
state.rebuild_index().await;
|
||||
Ok("Decision deleted successfully".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Decision not found".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Decision not found".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,18 +142,40 @@ impl McpTool for LogErrorFixHandler {
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
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 embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
|
||||
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)
|
||||
.await
|
||||
{
|
||||
let clean = summary.trim();
|
||||
if !clean.is_empty() {
|
||||
solution = format!("{} (AI Analysis: {})", solution, clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
||||
.await
|
||||
.ok();
|
||||
state.code.error_fixes.modify(|fixes| {
|
||||
fixes.push(crate::models::ErrorFix {
|
||||
signature: req.signature,
|
||||
solution: req.solution,
|
||||
signature: req.signature.clone(),
|
||||
solution: solution.clone(),
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
git_commit: req.git_commit,
|
||||
git_branch: req.git_branch,
|
||||
embedding,
|
||||
})
|
||||
});
|
||||
Ok("Error fix logged".to_string())
|
||||
state.record_activity("error_fix", &format!("Fixed error: {}", req.signature), Some(&solution));
|
||||
Ok(format!("Logged error fix for {}: {}", req.signature, solution))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,16 +225,32 @@ impl McpTool for LogCodeChangeHandler {
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut description = req.description;
|
||||
|
||||
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).await {
|
||||
let clean = summary.trim();
|
||||
if !clean.is_empty() {
|
||||
description = format!("{} (AI Summary: {})", description, clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.code.ledger.modify(|ledger| {
|
||||
ledger.push(CodeChange {
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
file_path: req.file_path,
|
||||
description: req.description,
|
||||
file_path: req.file_path.clone(),
|
||||
description: description.clone(),
|
||||
git_commit: req.git_commit,
|
||||
git_branch: req.git_branch,
|
||||
});
|
||||
});
|
||||
Ok("Code change logged".to_string())
|
||||
state.record_activity("code_change", &format!("Modified {}", req.file_path), Some(&description));
|
||||
Ok(format!("Logged code change for {}: {}", req.file_path, description))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +271,8 @@ impl McpTool for QueryRecentChangesHandler {
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let data = state
|
||||
.code.ledger
|
||||
.code
|
||||
.ledger
|
||||
.read_with(|l| Ok::<String, crate::error::AppError>(serde_json::to_string(l)?))?;
|
||||
Ok(data)
|
||||
}
|
||||
@@ -278,7 +320,8 @@ impl McpTool for ReadPreferencesHandler {
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
state
|
||||
.env.prefs
|
||||
.env
|
||||
.prefs
|
||||
.read_with(|prefs| Ok::<String, crate::error::AppError>(serde_json::to_string(prefs)?))
|
||||
}
|
||||
}
|
||||
@@ -297,8 +340,13 @@ impl McpTool for LogTechDebtHandler {
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution);
|
||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
|
||||
let text_to_embed = format!(
|
||||
"Description: {}\nIdeal Solution: {}",
|
||||
req.description, req.ideal_solution
|
||||
);
|
||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
||||
.await
|
||||
.ok();
|
||||
state.code.tech_debts.modify(|debts| {
|
||||
debts.push(crate::models::TechDebt {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
@@ -346,7 +394,10 @@ impl McpTool for ResolveTechDebtHandler {
|
||||
if found {
|
||||
Ok("Tech debt resolved".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Tech debt not found. Please verify the tech debt ID using list_tech_debt.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Tech debt not found. Please verify the tech debt ID using list_tech_debt."
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,10 +445,15 @@ impl McpTool for OmniSearchHandler {
|
||||
let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let limit = req.limit.unwrap_or(5);
|
||||
let include_body = req.include_body.unwrap_or(false);
|
||||
let matches = state.search().keyword_search(&req.query, req.namespace.as_deref(), limit).unwrap_or_default();
|
||||
let matches = state
|
||||
.search()
|
||||
.keyword_search(&req.query, req.namespace.as_deref(), limit)
|
||||
.unwrap_or_default();
|
||||
// tracing::info!("OMNI SEARCH MATCHES: {:?}", matches);
|
||||
let q = req.query.clone();
|
||||
let query_emb = crate::embedding::generate_embedding_async(q.clone()).await.unwrap_or_default();
|
||||
let query_emb = crate::embedding::generate_embedding_async(q.clone())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let kg_json = state.read_graph(|full| {
|
||||
let mut kg_entities = std::collections::HashMap::new();
|
||||
@@ -460,27 +516,38 @@ impl McpTool for OmniSearchHandler {
|
||||
})?;
|
||||
|
||||
let snippets_json = state.code.snippets.read_with(|all_snippets| {
|
||||
let mut scored: Vec<_> = all_snippets.iter().map(|s| {
|
||||
let mut score = 0.0;
|
||||
if matched_snippets.contains(s.name.as_str()) || contains_ignore_ascii_case(&s.name, &q) || contains_ignore_ascii_case(&s.description, &q) {
|
||||
score += 1.0;
|
||||
}
|
||||
if let Some(emb) = &s.embedding {
|
||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||
}
|
||||
(s, score)
|
||||
}).filter(|(_, score)| *score > 0.4).collect();
|
||||
let mut scored: Vec<_> = all_snippets
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let mut score = 0.0;
|
||||
if matched_snippets.contains(s.name.as_str())
|
||||
|| contains_ignore_ascii_case(&s.name, &q)
|
||||
|| contains_ignore_ascii_case(&s.description, &q)
|
||||
{
|
||||
score += 1.0;
|
||||
}
|
||||
if let Some(emb) = &s.embedding {
|
||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||
}
|
||||
(s, score)
|
||||
})
|
||||
.filter(|(_, score)| *score > 0.4)
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(s, _)| {
|
||||
if !include_body {
|
||||
let mut summary = s.clone();
|
||||
summary.code = "".to_string();
|
||||
summary
|
||||
} else {
|
||||
s.clone()
|
||||
}
|
||||
}).collect();
|
||||
let filtered: Vec<_> = scored
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(s, _)| {
|
||||
if !include_body {
|
||||
let mut summary = s.clone();
|
||||
summary.code = "".to_string();
|
||||
summary
|
||||
} else {
|
||||
s.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
||||
})?;
|
||||
|
||||
@@ -506,31 +573,41 @@ impl McpTool for OmniSearchHandler {
|
||||
|
||||
let q = req.query;
|
||||
let tech_debts_json = state.code.tech_debts.read_with(|debts| {
|
||||
let mut scored: Vec<_> = debts.iter().map(|d| {
|
||||
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
|
||||
return (d, 0.0);
|
||||
}
|
||||
let mut score = 0.0;
|
||||
if contains_ignore_ascii_case(&d.description, &q) || contains_ignore_ascii_case(&d.ideal_solution, &q) {
|
||||
score += 1.0;
|
||||
}
|
||||
if let Some(emb) = &d.embedding {
|
||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||
}
|
||||
(d, score)
|
||||
}).filter(|(_, score)| *score > 0.4).collect();
|
||||
let mut scored: Vec<_> = debts
|
||||
.iter()
|
||||
.map(|d| {
|
||||
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
|
||||
return (d, 0.0);
|
||||
}
|
||||
let mut score = 0.0;
|
||||
if contains_ignore_ascii_case(&d.description, &q)
|
||||
|| contains_ignore_ascii_case(&d.ideal_solution, &q)
|
||||
{
|
||||
score += 1.0;
|
||||
}
|
||||
if let Some(emb) = &d.embedding {
|
||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||
}
|
||||
(d, score)
|
||||
})
|
||||
.filter(|(_, score)| *score > 0.4)
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(d, _)| {
|
||||
if !include_body {
|
||||
let mut summary = d.clone();
|
||||
summary.description = "".to_string();
|
||||
summary.ideal_solution = "".to_string();
|
||||
summary
|
||||
} else {
|
||||
d.clone()
|
||||
}
|
||||
}).collect();
|
||||
let filtered: Vec<_> = scored
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(d, _)| {
|
||||
if !include_body {
|
||||
let mut summary = d.clone();
|
||||
summary.description = "".to_string();
|
||||
summary.ideal_solution = "".to_string();
|
||||
summary
|
||||
} else {
|
||||
d.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
||||
})?;
|
||||
|
||||
@@ -556,19 +633,29 @@ impl McpTool for OmniSearchHandler {
|
||||
})?;
|
||||
|
||||
let error_fixes_json = state.code.error_fixes.read_with(|fixes| {
|
||||
let mut scored: Vec<_> = fixes.iter().map(|f| {
|
||||
let mut score = 0.0;
|
||||
if contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) {
|
||||
score += 1.0;
|
||||
}
|
||||
if let Some(emb) = &f.embedding {
|
||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||
}
|
||||
(f, score)
|
||||
}).filter(|(_, score)| *score > 0.4).collect();
|
||||
let mut scored: Vec<_> = fixes
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let mut score = 0.0;
|
||||
if contains_ignore_ascii_case(&f.signature, &q)
|
||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
||||
{
|
||||
score += 1.0;
|
||||
}
|
||||
if let Some(emb) = &f.embedding {
|
||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||
}
|
||||
(f, score)
|
||||
})
|
||||
.filter(|(_, score)| *score > 0.4)
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(f, _)| f.clone()).collect();
|
||||
|
||||
let filtered: Vec<_> = scored
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(f, _)| f.clone())
|
||||
.collect();
|
||||
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
||||
})?;
|
||||
|
||||
@@ -603,7 +690,8 @@ impl McpTool for GetProjectHealthHandler {
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let active_tasks = state
|
||||
.project.tasks
|
||||
.project
|
||||
.tasks
|
||||
.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
|
||||
let unresolved_debt = state.code.tech_debts.read_with(|debts| {
|
||||
debts
|
||||
@@ -663,8 +751,12 @@ mod tests {
|
||||
"git_branch": "main"
|
||||
});
|
||||
|
||||
let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
assert!(res.contains("Error fix logged"));
|
||||
let res = handler
|
||||
.execute(args, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res.contains("Logged error fix"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -675,7 +767,11 @@ mod tests {
|
||||
let handler = GetProjectHealthHandler;
|
||||
let args = json!({"namespace": "global"});
|
||||
|
||||
let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
let res = handler
|
||||
.execute(args, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res.contains("unresolved_tech_debt"));
|
||||
}
|
||||
|
||||
@@ -693,8 +789,10 @@ mod tests {
|
||||
});
|
||||
let res1 = decision_handler
|
||||
.execute(args_dec, state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
assert_eq!(res1, "Decision logged as ADR-0001");
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res1, "Logged decision ADR-0001: Architecture");
|
||||
|
||||
let debt_handler = LogTechDebtHandler;
|
||||
let args_debt = json!({
|
||||
@@ -709,7 +807,9 @@ mod tests {
|
||||
});
|
||||
let res2 = debt_handler
|
||||
.execute(args_debt, state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res2, "Tech debt logged");
|
||||
|
||||
let list_debt = ListTechDebtHandler;
|
||||
@@ -718,7 +818,9 @@ mod tests {
|
||||
json!({"namespace": "global", "include_resolved": false}),
|
||||
state.clone(),
|
||||
)
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res3.contains("Hardcoded path"));
|
||||
|
||||
let pref_handler = LearnPreferenceHandler;
|
||||
@@ -728,11 +830,17 @@ mod tests {
|
||||
});
|
||||
let res4 = pref_handler
|
||||
.execute(args_pref, state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res4, "Preference learned");
|
||||
|
||||
let read_pref = ReadPreferencesHandler;
|
||||
let res5 = read_pref.execute(json!({}), state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
let res5 = read_pref
|
||||
.execute(json!({}), state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res5.contains("use spaces"));
|
||||
}
|
||||
|
||||
@@ -750,12 +858,16 @@ mod tests {
|
||||
});
|
||||
code_handler
|
||||
.execute(args_code, state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let query_changes = QueryRecentChangesHandler;
|
||||
let res_changes = query_changes
|
||||
.execute(json!({}), state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res_changes.contains("main.rs"));
|
||||
|
||||
let debt_handler = LogTechDebtHandler;
|
||||
@@ -771,7 +883,9 @@ mod tests {
|
||||
});
|
||||
debt_handler
|
||||
.execute(args_debt, state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
|
||||
// resolve it
|
||||
let list_debt = ListTechDebtHandler;
|
||||
@@ -780,14 +894,18 @@ mod tests {
|
||||
json!({"namespace": "global", "include_resolved": false}),
|
||||
state.clone(),
|
||||
)
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
let uuid_start = debt_list.find("id\":\"").unwrap() + 5;
|
||||
let uuid = &debt_list[uuid_start..uuid_start + 36];
|
||||
|
||||
let resolve_debt = ResolveTechDebtHandler;
|
||||
resolve_debt
|
||||
.execute(json!({"id": uuid}), state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -821,7 +939,9 @@ mod tests {
|
||||
let omni = OmniSearchHandler;
|
||||
let omni_res = omni
|
||||
.execute(json!({"query": "Omni"}), state.clone())
|
||||
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
// tracing::info!("OMNI RES: {}", omni_res);
|
||||
assert!(
|
||||
omni_res.contains("omni-1"),
|
||||
|
||||
Reference in new issue
Block a user