feat(mesh): assistant config RPCs + live toggle + Ollama detect (#50)

Phase 2 backend. AssistantConfig is now live-updatable (RwLock) so the UI
toggle applies without a listener restart. New RPCs:
- mesh.assistant-status  -> {enabled, model, trusted_only, default_model,
  ollama_detected, models[]} (probes local Ollama :11434/api/tags)
- mesh.assistant-configure -> set enabled/model/trusted_only live + persist

MeshService::assistant_config / configure_assistant. Compiles clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-17 18:29:36 -04:00
co-authored by Claude Opus 4.8
parent ef601c6d26
commit 0947ecee11
8 changed files with 147 additions and 7 deletions
@@ -381,6 +381,8 @@ impl RpcHandler {
"mesh.deadman-status" => self.handle_mesh_deadman_status().await,
"mesh.deadman-configure" => self.handle_mesh_deadman_configure(params).await,
"mesh.deadman-checkin" => self.handle_mesh_deadman_checkin().await,
"mesh.assistant-status" => self.handle_mesh_assistant_status().await,
"mesh.assistant-configure" => self.handle_mesh_assistant_configure(params).await,
"mesh.test-send" => self.handle_mesh_test_send(params).await,
// Transport layer (unified routing)
@@ -0,0 +1,95 @@
//! Mesh-AI assistant RPCs (issue #50): read/update the local assistant config
//! and report whether a local Ollama is available (for the install deep-link).
use super::super::RpcHandler;
use anyhow::Result;
use std::time::Duration;
/// Default model when the node hasn't picked one (kept in sync with the mesh
/// assistant handler's `DEFAULT_MODEL`).
const DEFAULT_MODEL: &str = "qwen2.5-coder";
impl RpcHandler {
/// mesh.assistant-status — current settings + local Ollama availability.
pub(in crate::api::rpc) async fn handle_mesh_assistant_status(
&self,
) -> Result<serde_json::Value> {
let cfg = {
let service = self.mesh_service.read().await;
let svc = service
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?;
svc.assistant_config().await
};
let (ollama_detected, models) = detect_ollama().await;
Ok(serde_json::json!({
"enabled": cfg.enabled,
"model": cfg.model,
"trusted_only": cfg.trusted_only,
"default_model": DEFAULT_MODEL,
"ollama_detected": ollama_detected,
"models": models,
}))
}
/// mesh.assistant-configure — update assistant settings live.
/// Params: `enabled?: bool`, `trusted_only?: bool`,
/// `model?: string|null` (string sets, null clears to default, absent leaves).
pub(in crate::api::rpc) async fn handle_mesh_assistant_configure(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.unwrap_or_default();
let service = self.mesh_service.read().await;
let svc = service
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?;
let enabled = params.get("enabled").and_then(|v| v.as_bool());
let trusted_only = params.get("trusted_only").and_then(|v| v.as_bool());
// model: key present + string => set; present + null => clear; absent => leave
let model = if let Some(v) = params.get("model") {
Some(v.as_str().map(|s| s.to_string()))
} else {
None
};
svc.configure_assistant(enabled, model, trusted_only).await?;
let cfg = svc.assistant_config().await;
Ok(serde_json::json!({
"enabled": cfg.enabled,
"model": cfg.model,
"trusted_only": cfg.trusted_only,
}))
}
}
/// Probe the local Ollama HTTP API; return (detected, model_names).
async fn detect_ollama() -> (bool, Vec<String>) {
let client = match reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()
{
Ok(c) => c,
Err(_) => return (false, Vec::new()),
};
match client.get("http://localhost:11434/api/tags").send().await {
Ok(resp) if resp.status().is_success() => {
let json: serde_json::Value = resp.json().await.unwrap_or_default();
let models = json
.get("models")
.and_then(|m| m.as_array())
.map(|arr| {
arr.iter()
.filter_map(|m| {
m.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())
})
.collect()
})
.unwrap_or_default();
(true, models)
}
_ => (false, Vec::new()),
}
}
+1
View File
@@ -1,3 +1,4 @@
mod assistant;
mod bitcoin_ops;
mod messaging;
mod safety;