diff --git a/document/development_guide.md b/document/development_guide.md index c2e1b4d..33f3662 100644 --- a/document/development_guide.md +++ b/document/development_guide.md @@ -25,9 +25,9 @@ ## 📁 主要なディレクトリ構成と責務 -- `src-tauri/src/mcp.rs`: MCP の全ツールロジックが集約されています。新しいツールを追加する場合はここを編集します。 -- `src-tauri/src/db.rs`: テーブルの定義、インデックス、トリガーなどの DB スキーマ管理。 -- `src-tauri/src/entities/`: SeaORM のモデル。DB スキーマを変更した際は再生成が必要です。 +- `src/backend/src/mcp.rs`: MCP の全ツールロジックが集約されています。新しいツールを追加する場合はここを編集します。 +- `src/backend/src/db.rs`: テーブルの定義、インデックス、トリガーなどの DB スキーマ管理。 +- `src/backend/src/entities/`: SeaORM のモデル。DB スキーマを変更した際は再生成が必要です。 - `test/`: JavaScript/Bun による外部結合テスト。 --- @@ -38,7 +38,7 @@ ### 1. ツール名の定義 -`src-tauri/src/mcp.rs` 内で、JSON-RPC でやり取りするメソッド名(例: `tools/list`)を確認し、新しいツール用の定義を追加します。 +`src/backend/src/mcp.rs` 内で、JSON-RPC でやり取りするメソッド名(例: `tools/list`)を確認し、新しいツール用の定義を追加します。 ### 2. ハンドラーの実装 @@ -50,7 +50,7 @@ ### 4. 単体テストの追加 -`mcp.rs` の末尾にある `mod tests` に、新しいツールの入出力を検証するテストを追加してください。 +`src/backend/src/mcp.rs` の末尾にある `mod tests` に、新しいツールの入出力を検証するテストを追加してください。 --- @@ -61,7 +61,7 @@ コアロジック(DB・MCP・LLM連係)のテストです。 ```bash -cd src-tauri +cd src/backend cargo test ``` @@ -79,7 +79,7 @@ ### 1. バージョンアップ -`package.json` および `src-tauri/cargo.toml` のバージョンを更新します。 +`package.json` および `src/backend/Cargo.toml` のバージョンを更新します。 ### 2. ビルドの実行 @@ -87,7 +87,7 @@ bun tauri build ``` -`src-tauri/target/release/bundle/` 内に MSI インストーラーや EXE ファイルが生成されます。 +`src/backend/target/release/bundle/` 内に MSI インストーラーや EXE ファイルが生成されます。 --- diff --git a/document/overview.md b/document/overview.md index d048b35..8254c4c 100644 --- a/document/overview.md +++ b/document/overview.md @@ -8,15 +8,15 @@ Tauri v2 への移植に伴い、システムのコアロジックは高性能かつ型安全な Rust 側に集約されました。 -### 1. Tauri Backend (Rust) +### 1. Tauri Backend (Rust / `src/backend`) | モジュール | 責務 | 使用技術 | | :--- | :--- | :--- | -| `lib.rs` | アプリケーションのライフサイクル、Sidecar 起動、システムトレイ制御。 | `tauri`, `tokio` | -| `mcp.rs` | MCP プロトコル(JSON-RPC / SSE)の実装、ツールハンドリング。 | `axum`, `serde_json` | -| `db.rs` | データベース接続管理、スキーマ初期化、トリガー設定。 | `sqlx`, `rusqlite` | -| `entities/` | データベーステーブルの Rust 構造体マッピング。 | `sea-orm` | -| `llama.rs` | `llama-server` との HTTP 通信、Embedding/Completion 依頼。 | `reqwest` | +| `src/backend/src/lib.rs` | アプリケーションのライフサイクル、Sidecar 起動、システムトレイ制御。 | `tauri`, `tokio` | +| `src/backend/src/mcp.rs` | MCP プロトコル(JSON-RPC / SSE)の実装、ツールハンドリング。 | `axum`, `serde_json` | +| `src/backend/src/db.rs` | データベース接続管理、スキーマ初期化、トリガー設定。 | `sqlx`, `rusqlite` | +| `src/backend/src/entities/` | データベーステーブルの Rust 構造体マッピング。 | `sea-orm` | +| `src/backend/src/llama.rs` | `llama-server` と整合した HTTP 通信、Embedding/Completion 依頼。 | `reqwest` | ### 2. Sidecar (LLM Server) diff --git a/package.json b/package.json index 1b818ec..97978ba 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test:tools": "bun test test/mcp-tools.test.js", "test:handlers": "bun test test/mcp-handlers.test.js", "test:integration": "bun test test/integration.test.js", - "tauri": "tauri", + "tauri": "tauri --config src/backend/tauri.conf.json", "test:watch": "bun test --watch test/**/*.test.js" }, "devDependencies": { @@ -45,10 +45,6 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", - "better-sqlite3": "^12.6.2", - "express": "^5.2.1", - "knex": "^3.1.0", - "sharer.js": "^0.5.3", - "sqlite-vec": "^0.1.7-alpha.2" + "better-sqlite3": "^12.6.2" } } \ No newline at end of file diff --git a/src/backend/src/lib.rs b/src/backend/src/lib.rs new file mode 100644 index 0000000..d560267 --- /dev/null +++ b/src/backend/src/lib.rs @@ -0,0 +1,216 @@ +pub mod db; +pub mod entities; +pub mod llama; +pub mod mcp; + +use crate::llama::LlamaClient; +use dotenvy::dotenv; +use sea_orm::DatabaseConnection; +use std::env; +use std::path::PathBuf; +use std::sync::Arc; +use tauri::menu::{Menu, MenuItem}; +use tauri::tray::{TrayIconBuilder, TrayIconEvent}; +use tauri::Manager; + +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) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![greet]) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window.hide(); + } + }) + .setup(|app| { + dotenv().ok(); + + let app_handle = app.handle().clone(); + + // Initialize Shell Plugin + app.handle().plugin(tauri_plugin_shell::init())?; + + // Spawn llama-server sidecar + let _model_path = env::var("LLAMA_CPP_MODEL_PATH").ok(); + // サイドカーの起動 (std::process::Command を使用してDLL問題を確実に回避) + let mut sidecar_path = std::env::current_dir().unwrap_or_default(); + println!("Current working directory: {:?}", sidecar_path); + + // src/backend フォルダの中にいる場合は2つ上がる (src/backend -> root) + if sidecar_path.ends_with(format!("src{}backend", std::path::MAIN_SEPARATOR)) { + sidecar_path.pop(); + sidecar_path.pop(); + } else if sidecar_path.ends_with("src-tauri") { + // 念のため古い名前も残しておく + sidecar_path.pop(); + } + + let project_root = sidecar_path.clone(); + sidecar_path.push("src"); + sidecar_path.push("backend"); + sidecar_path.push("bin"); + let sidecar_exe = sidecar_path.join("llama-server-x86_64-pc-windows-msvc.exe"); + + println!("Calculated sidecar path: {:?}", sidecar_exe); + + // モデルパスもルートからの絶対パスに変換 + let model_rel_path = env::var("LLAMA_CPP_MODEL_PATH").unwrap_or_default(); + let model_abs_path = project_root.join(&model_rel_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); + cmd.current_dir(&sidecar_path); // DLLのあるディレクトリをカレントにする + + // PATHにも追加 + let mut current_path = env::var("PATH").unwrap_or_default(); + current_path = format!("{};{}", sidecar_path.display(), current_path); + cmd.env("PATH", current_path); + + match cmd.spawn() { + Ok(child) => { + println!("llama-server started with 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: {}", e); + } + } + + tauri::async_runtime::block_on(async move { + let db_path = "vector.db"; + + // 拡張機能 (vec0.dll) のパスを動的に解決する + let exe_dir = env::current_exe() + .map(|p| p.parent().unwrap().to_path_buf()) + .unwrap_or_else(|_| env::current_dir().unwrap()); + + let candidates = [ + exe_dir.join("vec0.dll"), // 実行ファイルと同階層 + exe_dir.join("../node_modules/sqlite-vec-windows-x64/vec0.dll"), // Tauri dev (target/debug/..) + 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"), // src/backend 分1階層深い + PathBuf::from("node_modules/sqlite-vec-windows-x64/vec0.dll"), // root + ]; + + 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 sqlite-vec 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 state = Arc::new(AppState { + db: conn, + llama: Arc::new(llama), + }); + + 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; + }); + }); + + // Logging implementation + let mut log_builder = tauri_plugin_log::Builder::default() + .targets([ + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }), + ]) + .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, + }, + )); + } + + 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(); + } + } + _ => {} + }) + .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(); + } + } + }) + .build(app)?; + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src/backend/tauri.conf.json b/src/backend/tauri.conf.json new file mode 100644 index 0000000..13eec50 --- /dev/null +++ b/src/backend/tauri.conf.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "sqlitevector", + "version": "0.1.0", + "identifier": "com.sqlitevector.app", + "build": { + "frontendDist": "../frontend", + "beforeDevCommand": "", + "beforeBuildCommand": "" + }, + "app": { + "windows": [ + { + "title": "SQLite Vector", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false, + "visible": true + } + ], + "withGlobalTauri": true, + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": [ + "bin/llama-server" + ], + "resources": [ + "resources/*" + ], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} \ No newline at end of file