diff --git a/src/backend/src/lib.rs b/src/backend/src/lib.rs index f4a5a3b..6c66862 100644 --- a/src/backend/src/lib.rs +++ b/src/backend/src/lib.rs @@ -11,39 +11,30 @@ use std::sync::Arc; use tauri::menu::{Menu, MenuItem}; use tauri::tray::{TrayIconBuilder, TrayIconEvent}; -use tauri::Manager; +use tauri::{Emitter, Manager}; + +use std::sync::atomic::AtomicUsize; pub struct AppState { pub db: DatabaseConnection, pub llama: Arc, -} - -#[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) + pub mcp_tx: tokio::sync::broadcast::Sender, + pub connection_count: Arc, + pub app_handle: tauri::AppHandle, } #[tauri::command] fn get_mcp_info() -> Result { - // MCP.json ファイルを複数の場所から探す - let mut candidates = vec![ - PathBuf::from("mcp.json"), - ]; - - // exe ディレクトリから探す + let mut candidates = vec![PathBuf::from("mcp.json")]; if let Ok(exe_path) = env::current_exe() { if let Some(exe_dir) = exe_path.parent() { let mut p = exe_dir.to_path_buf(); for _ in 0..5 { candidates.push(p.join("mcp.json")); - if !p.pop() { - break; - } + if !p.pop() { break; } } } } - - // 最初に存在するファイルを使う let mut found_path = None; for candidate in candidates { if candidate.exists() { @@ -51,23 +42,150 @@ break; } } - - let mcp_path = found_path - .ok_or_else(|| "mcp.json not found in any expected location".to_string())?; - - let content = std::fs::read_to_string(&mcp_path) - .map_err(|e| format!("Failed to read mcp.json at {:?}: {}", mcp_path, e))?; - - let mcp_data: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("Failed to parse mcp.json: {}", e))?; - + let mcp_path = found_path.ok_or_else(|| "mcp.json not found".to_string())?; + let content = std::fs::read_to_string(&mcp_path).map_err(|e| e.to_string())?; + let mcp_data: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?; Ok(mcp_data) } +#[tauri::command] +async fn get_db_stats(state: tauri::State<'_, Arc>) -> Result { + use crate::entities::items; + use sea_orm::{EntityTrait, PaginatorTrait}; + let count = items::Entity::find().count(&state.db).await.map_err(|e| e.to_string())?; + Ok(serde_json::json!({ "itemCount": count })) +} + + +#[tauri::command] +async fn get_sidecar_status(state: tauri::State<'_, Arc>) -> Result { + Ok(state.llama.check_health().await) +} + +fn get_config(app_handle: &tauri::AppHandle) -> serde_json::Value { + let mut config_paths = vec![]; + if let Ok(app_data) = app_handle.path().app_config_dir() { config_paths.push(app_data.join("config.json")); } + if let Ok(exe_path) = env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + let mut p = exe_dir.to_path_buf(); + for _ in 0..5 { + config_paths.push(p.join("config.json")); + if !p.pop() { break; } + } + } + } + if let Ok(cwd) = env::current_dir() { config_paths.push(cwd.join("config.json")); } + if let Ok(res_dir) = app_handle.path().resource_dir() { config_paths.push(res_dir.join("config.json")); } + + for path in config_paths { + if path.exists() { + if let Ok(content) = std::fs::read_to_string(path) { + if let Ok(parsed) = serde_json::from_str::(&content) { return parsed; } + } + } + } + serde_json::json!({ + "database": { "path": "data/vector.db" }, + "model": { "path": "models/embeddinggemma-300m-q4_0.gguf" }, + "llama_server": { "port": 8080 } + }) +} + +fn resolve_db_path(app_handle: &tauri::AppHandle, config: &serde_json::Value) -> String { + if let Ok(p) = env::var("DB_PATH") { return p; } + if let Some(p) = config.get("database").and_then(|d| d.get("path")).and_then(|p| p.as_str()) { + let mut candidate = PathBuf::from(p); + if candidate.is_relative() { + if let Ok(exe_path) = env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + let mut pr = exe_dir.to_path_buf(); + for _ in 0..4 { + if pr.join("config.json").exists() { candidate = pr.join(p); break; } + if !pr.pop() { break; } + } + } + } + } + return candidate.to_string_lossy().to_string(); + } + if cfg!(debug_assertions) { "data/vector.db".to_string() } else { + let mut p = app_handle.path().app_data_dir().expect("App data dir not found"); + p.push("data"); + let _ = std::fs::create_dir_all(&p); + p.push("vector.db"); + p.to_string_lossy().to_string() + } +} + +fn resolve_extension_path(app_handle: &tauri::AppHandle) -> String { + let exe_dir = env::current_exe().map(|p| p.parent().unwrap().to_path_buf()).unwrap_or_else(|_| env::current_dir().unwrap()); + let mut candidates = vec![ + exe_dir.join("vec0.dll"), + exe_dir.join("../node_modules/sqlite-vec-windows-x64/vec0.dll"), + exe_dir.join("../../node_modules/sqlite-vec-windows-x64/vec0.dll"), + exe_dir.join("../../../node_modules/sqlite-vec-windows-x64/vec0.dll"), + exe_dir.join("../../../../node_modules/sqlite-vec-windows-x64/vec0.dll"), + PathBuf::from("node_modules/sqlite-vec-windows-x64/vec0.dll"), + ]; + if let Ok(res_dir) = app_handle.path().resource_dir() { candidates.insert(0, res_dir.join("vec0.dll")); } + for cand in candidates { if cand.exists() { return cand.to_str().unwrap().to_string(); } } + "vec0.dll".to_string() +} + +fn spawn_llama_server(app_handle: &tauri::AppHandle, config: &serde_json::Value) { + let model_path = if let Ok(p) = env::var("LLAMA_CPP_MODEL_PATH") { PathBuf::from(p) } else if let Some(p) = config.get("model").and_then(|m| m.get("path")).and_then(|p| p.as_str()) { + let mut candidate = PathBuf::from(p); + if candidate.is_relative() { + if let Ok(exe_path) = env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + let mut pr = exe_dir.to_path_buf(); + for _ in 0..4 { + if pr.join("config.json").exists() { candidate = pr.join(p); break; } + if !pr.pop() { break; } + } + } + } + } + candidate + } else { + let mut found = None; + if let Ok(exe_path) = env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + let mut pr = exe_dir.to_path_buf(); + for _ in 0..4 { + if pr.join("models").exists() { found = Some(pr.join("models").join("embeddinggemma-300m-q4_0.gguf")); break; } + if !pr.pop() { break; } + } + } + } + found.unwrap_or_else(|| PathBuf::from("models/embeddinggemma-300m-q4_0.gguf")) + }; + + let sidecar_exe = if cfg!(debug_assertions) { + let mut p = env::current_dir().unwrap(); + if p.ends_with(format!("src{}backend", std::path::MAIN_SEPARATOR)) { p.pop(); p.pop(); } + p.join("bin").join("llama-server-x86_64-pc-windows-msvc.exe") + } else { + app_handle.path().resource_dir().unwrap().join("_up_").join("_up_").join("bin").join("llama-server-x86_64-pc-windows-msvc.exe") + }; + + let mut cmd = std::process::Command::new(&sidecar_exe); + cmd.args(&["--model", &model_path.to_string_lossy(), "--port", "8080", "--embedding", "--host", "127.0.0.1"]); + if let Some(bin_dir) = sidecar_exe.parent() { + let path = env::var("PATH").unwrap_or_default(); + cmd.env("PATH", format!("{};{}", bin_dir.display(), path)); + cmd.current_dir(bin_dir); + } + match cmd.spawn() { + Ok(child) => { let pid = child.id(); println!("llama-server started (PID: {})", pid); std::thread::spawn(move || { let _ = child.wait_with_output(); }); } + Err(e) => eprintln!("Failed to spawn llama-server: {}", e), + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - .invoke_handler(tauri::generate_handler![greet, get_mcp_info]) + .invoke_handler(tauri::generate_handler![get_mcp_info, get_db_stats, get_sidecar_status]) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); @@ -76,179 +194,34 @@ }) .setup(|app| { dotenv().ok(); - let app_handle = app.handle().clone(); + let _ = app.handle().plugin(tauri_plugin_shell::init()); - // Initialize Shell Plugin - app.handle().plugin(tauri_plugin_shell::init())?; - - // Spawn llama-server sidecar - let model_path_env = env::var("LLAMA_CPP_MODEL_PATH").ok(); - - // 1. Resolve Sidecar Path (Robustly) - let sidecar_exe = if cfg!(debug_assertions) { - // Development: Use project root bin - let mut p = env::current_dir().unwrap_or_default(); - if p.ends_with(format!("src{}backend", std::path::MAIN_SEPARATOR)) { - p.pop(); p.pop(); - } - p.join("bin").join("llama-server-x86_64-pc-windows-msvc.exe") - } else { - // Production: Use sidecar resolving from Tauri - // Note: We use Command for sidecars to handle DLL path envs if needed - app_handle.path().resource_dir().unwrap_or_default() - .join("_up_").join("_up_").join("bin") // Tauri installs binaries in a specific nested struct - .join("llama-server-x86_64-pc-windows-msvc.exe") - }; - - // 2. Resolve Model Path - let model_abs_path = { - if let Some(p) = model_path_env { - // Use environment variable if set - PathBuf::from(p) - } else { - // Strategy: Find models dir relative to exe location or workspace root - let mut found_path = None; - if let Ok(exe_path) = env::current_exe() { - if let Some(exe_dir) = exe_path.parent() { - // From target/debug or target/release, go up 3-4 levels to reach project root - let mut project_root = exe_dir.to_path_buf(); - // target/debug -> target -> . (project root) - for _ in 0..4 { - if project_root.join("models").exists() { - found_path = Some(project_root.join("models").join("embeddinggemma-300m-q4_0.gguf")); - break; - } - if !project_root.pop() { - break; - } - } - } - } - - if let Some(p) = found_path { - p - } else { - // Fallback: Try current directory - let mut p = env::current_dir().unwrap_or_default(); - if p.ends_with(format!("src{}backend", std::path::MAIN_SEPARATOR)) { - p.pop(); p.pop(); - } - p.join("models").join("embeddinggemma-300m-q4_0.gguf") - } - } - }; - - println!("Sidecar: {:?}", sidecar_exe); - println!("Model: {:?}", model_abs_path); - - let args = vec![ - "--model".to_string(), - model_abs_path.to_string_lossy().to_string(), - "--port".to_string(), - "8080".to_string(), - "--embedding".to_string(), - "--host".to_string(), - "127.0.0.1".to_string(), - ]; - - let mut cmd = std::process::Command::new(&sidecar_exe); - cmd.args(&args); - - // DLL問題回避のため、バイナリのあるディレクトリをPATHに含める - if let Some(bin_dir) = sidecar_exe.parent() { - let current_path = env::var("PATH").unwrap_or_default(); - cmd.env("PATH", format!("{};{}", bin_dir.display(), current_path)); - cmd.current_dir(bin_dir); - } - - match cmd.spawn() { - Ok(child) => { - println!("llama-server started (PID: {})", child.id()); - let pid = child.id(); - std::thread::spawn(move || { - let _ = child.wait_with_output(); - println!("llama-server (PID {}) exited", pid); - }); - } - Err(e) => { - eprintln!("Failed to spawn llama-server at {:?}: {}", sidecar_exe, e); - } - } + let config = get_config(&app_handle); + spawn_llama_server(&app_handle, &config); tauri::async_runtime::block_on(async move { - // 1. Resolve DB Path (AppData for Prod, current_dir for Dev) - let db_path = env::var("DB_PATH").unwrap_or_else(|_| { - if cfg!(debug_assertions) { - "data/vector.db".to_string() - } else { - let mut p = app_handle.path().app_data_dir().expect("App data dir not found"); - p.push("data"); - std::fs::create_dir_all(&p).ok(); - p.push("vector.db"); - p.to_string_lossy().to_string() - } - }); - - // 2. Resolve DLL Path (ResourceDir for Prod, candidates for Dev) - let exe_dir = env::current_exe() - .map(|p| p.parent().unwrap().to_path_buf()) - .unwrap_or_else(|_| env::current_dir().unwrap()); - - let mut candidates = vec![ - exe_dir.join("vec0.dll"), - exe_dir.join("../node_modules/sqlite-vec-windows-x64/vec0.dll"), - exe_dir.join("../../node_modules/sqlite-vec-windows-x64/vec0.dll"), - exe_dir.join("../../../node_modules/sqlite-vec-windows-x64/vec0.dll"), - exe_dir.join("../../../../node_modules/sqlite-vec-windows-x64/vec0.dll"), - PathBuf::from("node_modules/sqlite-vec-windows-x64/vec0.dll"), - ]; - - // Add Resource dir candidate (Prod) - if let Ok(res_dir) = app_handle.path().resource_dir() { - candidates.insert(0, res_dir.join("vec0.dll")); - } - - let mut extension_path = candidates[0].to_str().unwrap().to_string(); - for cand in &candidates { - if cand.exists() { - extension_path = cand.to_str().unwrap().to_string(); - println!("Found extension at: {}", extension_path); - break; - } - } - - let conn = db::init_db(&db_path, &extension_path) - .await - .expect("Failed to init database"); - let llama_base_url = env::var("LLAMA_CPP_BASE_URL") - .unwrap_or_else(|_| "http://localhost:8080".to_string()); - let embedding_model = env::var("LLAMA_CPP_EMBEDDING_MODEL") - .unwrap_or_else(|_| "nomic-embed-text".to_string()); - let completion_model = - env::var("LLAMA_CPP_MODEL").unwrap_or_else(|_| "mistral".to_string()); - - let llama = LlamaClient::new(llama_base_url, embedding_model, completion_model); + let db_path = resolve_db_path(&app_handle, &config); + let ext_path = resolve_extension_path(&app_handle); + let vec_dim = env::var("VEC_DIM").unwrap_or_else(|_| "384".to_string()).parse::().unwrap_or(384); + let conn = db::init_db(&db_path, &ext_path, vec_dim).await.expect("Failed to init db"); let state = Arc::new(AppState { db: conn, - llama: Arc::new(llama), + llama: Arc::new(LlamaClient::new( + env::var("LLAMA_CPP_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()), + env::var("LLAMA_CPP_EMBEDDING_MODEL").unwrap_or_else(|_| "nomic-embed-text".to_string()), + env::var("LLAMA_CPP_MODEL").unwrap_or_else(|_| "mistral".to_string()), + )), + mcp_tx: tokio::sync::broadcast::channel(100).0, + connection_count: Arc::new(AtomicUsize::new(0)), + app_handle: app_handle.clone(), }); - app_handle.manage(state.clone()); - - // Start MCP Server (SSE) - let mcp_port = env::var("MCP_PORT") - .unwrap_or_else(|_| "3000".to_string()) - .parse::() - .unwrap_or(3000); - - tokio::spawn(async move { - mcp::start_mcp_server(state, mcp_port).await; - }); + let port = env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string()).parse::().unwrap_or(3000); + tokio::spawn(async move { mcp::start_mcp_server(state, port).await; }); }); - // Logging implementation let mut log_builder = tauri_plugin_log::Builder::default() .targets([ tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), @@ -256,49 +229,27 @@ ]) .max_file_size(10 * 1024 * 1024) .level(log::LevelFilter::Info); - if cfg!(debug_assertions) { - // デバッグ時はプロジェクトルートの logs フォルダにも出力 - log_builder = log_builder.target(tauri_plugin_log::Target::new( - tauri_plugin_log::TargetKind::Folder { - path: std::path::PathBuf::from("logs"), - file_name: None, - }, - )); + log_builder = log_builder.target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder { path: PathBuf::from("logs"), file_name: None })); } - - app.handle().plugin(log_builder.build())?; + let _ = app.handle().plugin(log_builder.build()); let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let show_i = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show_i, &quit_i])?; - let _tray = TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .on_menu_event(|app, event| match event.id.as_ref() { - "quit" => { - app.exit(0); - } - "show" => { - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - } - } + "quit" => app.exit(0), + "show" => if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } _ => {} }) - .on_tray_icon_event(|tray, event| { - if let TrayIconEvent::DoubleClick { .. } = event { - let app = tray.app_handle(); - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - } - } + .on_tray_icon_event(|tray, event| if let TrayIconEvent::DoubleClick { .. } = event { + let app = tray.app_handle(); + if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } }) .build(app)?; - Ok(()) }) .run(tauri::generate_context!()) diff --git a/src/backend/src/llama.rs b/src/backend/src/llama.rs index e5b82f2..e0f8e7e 100644 --- a/src/backend/src/llama.rs +++ b/src/backend/src/llama.rs @@ -94,6 +94,11 @@ Ok(res.content) } + + pub async fn check_health(&self) -> bool { + let url = format!("{}/health", self.base_url); + self.client.get(&url).send().await.is_ok() + } } #[cfg(test)] diff --git a/src/frontend/index.html b/src/frontend/index.html index d85d5c6..2d2c487 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -1,30 +1,104 @@ - + + SQLite Vector MCP Server - - + + +

SQLite Vector MCP Server

-

Status: Loading...

- -
-

Test Command

- - -

-
+

+ Status: Loading... | + Sidecar: Checking... | + Connected Clients: 0 | + Stored Items: 0 +

MCP Configuration

@@ -33,21 +107,31 @@ - - + + + \ No newline at end of file