diff --git a/.gitignore b/.gitignore index a547bf3..dc044c6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,8 @@ *.njsproj *.sln *.sw? + +# Project specific +journals/ +src-tauri/bin/ +src-tauri/*.txt diff --git a/README.md b/README.md index 1c26ce2..adc6898 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # TelosDB ローカル完結型セマンティック検索DB & MCPサーバー @@ -7,108 +6,93 @@ ## 概要 -TelosDBは、Tauri 2 + Rust + Vanilla JS/HTMLで構成された、SQLite Vector拡張を活用するローカル特化型セマンティック検索基盤です。サイドカー(llama-server)による埋め込み生成、AxumベースのMCP APIサーバー、最小構成のUIを備え、外部API不要・高速・安全な知識検索/提供を実現します。 +TelosDBは、Tauri 2 + Rust + Vanilla JS/HTMLで構成された、SQLite Vector拡張を活用するローカル特化型セマンティック検索基盤です。 +サイドカー(llama-server)による高品質な埋め込み生成、Model Context Protocol (MCP) 準拠のSSEサーバー、そしてガラスモーフィズムを採用したプレミアムなUIを備えています。 + +外部APIを一切使用せず、ローカルLLM(Gemma-3)の力を借りて、安全かつ高速な知識検索環境をデスクトップに提供します。 --- -## 特徴 +## 主な機能 -- **Tauri 2 + Rust**: デスクトップアプリ基盤。プロセス/リソース管理もRustで一元化。 -- **Vanilla JS/HTML UI**: React等のフレームワーク非依存。最小・高速・シンプル。 -- **サイドカー自動起動/監視**: llama-server(llama.cppベース)を自動管理。 -- **Axum MCP API**: Model Context Protocol準拠のREST/SSEサーバー。外部AIエージェント連携も容易。 -- **SQLite Vector**: sqlite-vec拡張DLLによる高速ベクトル検索。 -- **ESLint/Prettier整備**: コード品質・自動整形対応。 -- **journal記録**: journals/に進捗・設計・トラブルシュートを日次記録。 +- **セマンティック検索 (Vector Search)**: `sqlite-vec` を用いた高精度な意味検索。 +- **内蔵推論エンジン (Sidecar Integration)**: `llama-server` を自動管理。アプリ起動と同時に推論サーバーが立ち上がります。 +- **MCP SSE サーバー仕様**: SSE (Server-Sent Events) を用いた非同期レスポンス方式を採用。LM Studio, Claude Desktop などの外部AIツールから「プラグイン」として直接接続可能です。 +- **プレミアム・デザイン**: ガラスモーフィズムを基調とした洗練されたUI。ステータスインジケーターにより推論エンジンの稼働状況を一目で把握できます。 +- **堅牢なロギング**: ローテーション機能付きログ出力。`llama-server` の詳細な内部ログもキャプチャします。 --- ## セットアップ -### 1. Rust/Bunインストール -- [Rust公式](https://rustup.rs/) でRustを導入 -- [Bun公式](https://bun.sh/) でBunを導入 +### 1. 動作要件 -### 2. 依存インストール +- **OS**: Windows 10/11 (x64) +- **Rust**: 1.77.2以上 +- **Node.js / Bun**: 最新のLTS推奨 + +### 2. モデルの配置 + +以下のパスに GGUF モデルファイルを配置してください: +`src-tauri/bin/gemma-3-270m-it-Q4_K_M.gguf` + +### 3. 起動方法 + ```bash +# 依存関係のインストール bun install -``` -### 3. サイドカー/モデル準備 -- scripts/launch-llama-server.bat でllama-server起動(またはTauri起動時に自動) -- Gemma-3-300M.gguf等のモデルファイルをexeと同じディレクトリに配置 - -### 4. 開発/ビルド -```bash -# 開発モード -tauri dev -# ビルド -tauri build +# 開発モードで実行 +bun run tauri dev ``` --- -## 主要機能 - -| 機能 | 概要 | -|:-------------|:------------------------------------------------| -| 文章登録 | 文章+メタデータを自動ベクトル化しDB保存 | -| セマンティック検索 | 自然言語で類似文書を高速検索 | -| MCP API | 外部エージェント向けJSON-RPC/SSEインターフェース | -| サイドカー管理 | llama-server自動起動・死活監視・終了シグナル | -| UI | 状態・件数・API仕様(mcp.json)の可視化/コピー | - ---- - -## アーキテクチャ +## システム構造 ```mermaid graph TD - UI[Vanilla UI] - Tauri[Tauri Core] - Axum[Axum MCP Server] - Sidecar[llama-server] - DB[(SQLite+vec0)] - Model[Gemma-3 GGUF] - UI -->|IPC| Tauri - Tauri <--> Axum - Tauri <--> DB - Axum <--> DB - Tauri --> Sidecar - Sidecar --> Model + subgraph "Frontend (Vanilla JS)" + UI[Glassmorphism UI] + SSE_Monitor[SSE Client] + end + + subgraph "Backend (Rust/Tauri)" + Main[Tauri Core] + Axum[Axum MCP Server] + DB[(SQLite + sqlite-vec)] + Logger[Rotating Logger] + end + + subgraph "Sidecar (llama-server)" + Engine[Inference Engine] + Model[Gemma-3 GGUF] + end + + UI <-- JSON / SSE --> Axum + Axum <-- SQL/Vector --> DB + Main -- Spawn/Monitor --> Engine + Engine -- Load --> Model + Engine -- Log Stream --> Logger ``` --- -## API仕様(抜粋) +## 開発者向け情報 -- `/llama_status` : サイドカー状態取得(running/stopped/error) -- `/messages` : MCP JSON-RPCエンドポイント -- `/sse` : SSEストリーム -- 詳細: [document/openapi.yaml](document/openapi.yaml), [document/mcp.json](document/mcp.json) +### ログ ---- +ログファイルは以下に出力されます: -## サイドカー(llama-server)管理 +- 開発時: `src-tauri/logs/telos.log` +- 実行時: `%LOCALAPPDATA%/com.telosdb.app/logs/telos.log` -- Tauri起動時に自動でllama-server.exeをspawn -- Gemma-3-300M.gguf等のモデルを引数で指定 -- DLL依存解決・CWD制御・標準出力監視 -- /health監視で死活判定、終了時はkill -- scripts/launch-llama-server.bat でも手動起動可 +### MCP接続設定 ---- - -## journal・開発ガイド - -- journals/ : 日次進捗・設計・トラブル記録 -- document/ : システム仕様・設計・API・開発ガイド - - [01_system_overview.md](document/01_system_overview.md) - - [02_architecture_design.md](document/02_architecture_design.md) - - [06_development_guide.md](document/06_development_guide.md) +UI上の「MCP Config」ボタンをクリックすると、LM Studio 等に貼り付け可能な設定 JSON が表示されます。 --- ## ライセンス -MIT +MIT License diff --git a/document/04_mcp_api_specification.md b/document/04_mcp_api_specification.md index d01067e..3388b18 100644 --- a/document/04_mcp_api_specification.md +++ b/document/04_mcp_api_specification.md @@ -8,18 +8,13 @@ - **トランスポート**: SSE (Server-Sent Events) - **エンドポイント**: - Connection: `GET /sse` - - Message: `POST /messages` + - Message: `POST /messages?session_id={uuid}` + - `GET /sse` 接続時にサーバーから送られる `endpoint` イベント内のパスを使用すること。 ## 2. 提供ツール定義 (Tools) -### 2.1 `add_item_text` (登録) - -文章を自動ベクトル化して登録します。 - -- **引数**: - - `content` (string): 本文 (必須) - - `path` (string): メタデータ (任意) -- **戻り値**: 成功メッセージと ID +> [!NOTE] +> 現在、`search_text` のみが実装・公開されています。その他のツールは順次実装予定です。 ### 2.2 `search_text` (意味検索) @@ -28,53 +23,16 @@ - **引数**: - `content` (string): 検索クエリ (必須) - `limit` (number): 取得件数 (デフォルト: 10) -- **戻り値**: 類似度順の結果リスト(`id`, `content`, `distance` 等) +- **戻り値**: 類似度順の結果リスト(テキスト形式) -### 2.3 `update_item` (更新) +## 3. レスポンスフロー (SSE) -既存のアイテムとベクトルを最新の内容で書き換えます。 +MCP SSE 規格に基づき、リクエストとレスポンスは以下のフローで行われます。 -- **引数**: - - `id` (number): 対象 ID (必須) - - `content` (string): 新しい本文 (必須) - - `path` (string): 新しいメタデータ (任意) -- **戻り値**: 更新完了通知 - -### 2.4 `delete_item` (削除) - -指定 ID のデータを物理削除します。 - -- **引数**: - - `id` (number): 対象 ID (必須) -- **戻り値**: 削除完了通知 - -### 2.5 `llm_generate` (生成) - -内蔵 LLM を使用したテキスト生成。 - -- **引数**: - - `prompt` (string): プロンプト (必須) - - `n_predict` (number): 最大トークン数 (任意) -- **戻り値**: 生成文章 - -## 3. レスポンスフォーマット - -すべてのレスポンスは以下の形式に準拠します。 - -```json -{ - "jsonrpc": "2.0", - "result": { - "content": [ - { - "type": "text", - "text": "..." - } - ] - }, - "id": "..." -} -``` +1. **接続**: クライアントが `GET /sse` を開始。 +2. **通知**: サーバーが `event: endpoint` でメッセージ送信先パスを返却。 +3. **送信**: クライアントが `POST /messages?session_id=...` で命令を送信。 +4. **応答**: サーバーが SSE ストリーム上で `event: message` として結果を返却。HTTP 自体は `202 Accepted` を返却する。 ## 4. エラー定義 @@ -83,3 +41,4 @@ | `-32700` | Parse error | 不正な JSON フォーマット | | `-32601` | Method not found | 未定義ツールの呼び出し | | `-32000` | Internal error | Sidecar 不通, DB 権限エラー等 | +| `422` | Unprocessable Entity | 必須パラメータの欠落 | diff --git a/document/05_sidecar_integration.md b/document/05_sidecar_integration.md index a41daad..1e2ecef 100644 --- a/document/05_sidecar_integration.md +++ b/document/05_sidecar_integration.md @@ -10,14 +10,14 @@ - **名称**: `llama-server` - **アーキテクチャ**: `x86_64-pc-windows-msvc` (Windows 環境時) -- **ビルド設定**: Vulkan / CPU (AVX2/AVX512) 最適化。 +- **モデル**: Gemma-3-270m-it (Q4_Q_K_M.gguf) ### 2.2 DLL 依存解決 (Windows) -バイナリの起動に必要なライブラリ (`ggml.dll`, `llama.dll` 等) を確実に読み込むため、以下の制御を行います。 +Windows 環境での DLL 地獄(Dependency Hell)を回避するため、`build.rs` による自動供給システムを導入しています。 -1. **ランタイムパス補強**: 起動プロセスの環境変数 `PATH` の最優先にバイナリ配置フォルダを追加。 -2. **CWD 強制設定**: プロセスのカレントディレクトリをバイナリ所在フォルダに設定。 +1. **自動コピー**: ビルド時に `sqlite-vec` 等の依存 DLL を `node_modules` から抽出し、ターゲットディレクトリ(`target/debug/bin`)へ自動配置します。 +2. **ランタイム解決**: `lib.rs` 内で実行ファイルの相対パスから `bin` フォルダを特定し、`vec0.dll` を動的に読み込みます。 ## 3. 推論エンジン設定 @@ -25,19 +25,19 @@ | 引数 | 値 | 説明 | | :--- | :--- | :--- | -| `--model` | path/to/model | Gemma-3-300M (GGUF) を指定。 | +| `--model` | {path} | `gemma-3-270m-it-Q4_K_M.gguf` へのパス。 | | `--port` | 8080 | 内部通信用ポート。 | -| `--embedding` | enabled | 埋め込み (Vector) 生成モードを有効化。 | -| `--parallel` | 1 | 生成効率と安定性のバランス。 | +| `--embedding` | (flag) | 埋め込み (Vector) 生成モードを有効化。 | +| `--parallel` | 1 | 同時実行数。リソース消費を抑えるため 1 に制限。 | ## 4. ライフサイクル -1. **初期化**: App 起動時の `initialize_app` 内で実行。 -2. **ヘルスチェック**: `/health` エンドポイントに対し 500ms 間隔でポーリング。API 準備完了を確認。 -3. **モニタリング**: 標準出力/標準エラー出力をキャプチャし、Tauri ログに転送。 -4. **終了処理**: アプリケーション終了シグナル (Ctrl+C, Window Close, Tray Quit) を検知し、SigTerm を発行。 +1. **初期化**: App 起動時に `Sidecar` API を用いて非同期に起動。 +2. **ヘルスチェック**: `mcp.rs` 内のモニタースレッドが `/health` に対し 2 秒間隔でポーリング。 +3. **モニタリング**: `tauri-plugin-shell` のストリームを `tauri-plugin-log` に転送し、一元管理。 +4. **終了処理**: アプリ終了時にチャイルドプロセスを確実に Kill。 -## 5. リソース最適化 +## 5. 推論エンジン最適化 -- **Vulkan**: 利用可能な場合、GPU 演算を優先的に使用。 -- **メモリ管理**: コンテキストサイズを 8192 に制限し、不快なスワップを防止。 +- **Vulkan/CPU**: 環境に応じて `ggml-vulkan.dll` または `ggml-cpu.dll` が自動的にロードされます。 +- **ログ管理**: `logs/telos.log` に `llama-server` の起動シーケンスと推論ログが詳細に出力されます。 diff --git a/journals/20260214-0001-Debug_Entrypoint_Error.md b/journals/20260214-0001-Debug_Entrypoint_Error.md deleted file mode 100644 index f1c395e..0000000 --- a/journals/20260214-0001-Debug_Entrypoint_Error.md +++ /dev/null @@ -1,32 +0,0 @@ -# 20260214-0001-Debug_Entrypoint_Error - -## 概要 - -`STATUS_ENTRYPOINT_NOT_FOUND` (0xc0000139) エラーが発生し、アプリケーションが起動しない問題を解決する。 - -## 現状の分析 - -- エラーコード: `0xc0000139` (STATUS_ENTRYPOINT_NOT_FOUND) -- 発生タイミング: `cargo run` 後のアプリケーション起動時 -- 疑わしい原因: DLLの依存関係、SQliteのバージョン競合 - -## 調査結果 - -- `Cargo.toml` で `rusqlite` の `bundled` 機能が有効だった(これが原因の可能性大)。 -- `vec0.dll` は `sqlite3.dll` に依存せず、ホストプロセスから `sqlite3_` シンボルを解決しようとするか、動的に `sqlite3.dll` をロードしようとしている。 -- `bundled` (静的リンク) ではシンボルがエクスポートされないため、`vec0.dll` が失敗する。 -- 完全バンドル化も失敗(`0xc0000139` 解消せず)。 - -## 実施した対策(方針転換) - -1. **動的リンクへの回帰**: - - `Cargo.toml` から `bundled` 機能を削除。 - - `build.rs` & `prepare-resources.cjs` で `sqlite3.dll` のコピーを復活。 -2. **DLL整合性の確保**: - - `bin/sqlite3.dll` を正規版(3.45.0 x64)に置換。 - -## 現在の状況 - -- `cargo clean; npm run dev` 実行後も `STATUS_ENTRYPOINT_NOT_FOUND` エラーが発生。 -- `sqlite3.dll` の問題ではない可能性が出てきた。 -- `telos-db.exe` が何に依存しているか、Importsを確認する。 diff --git a/journals/20260214-Environment_Fix_and_Tauri_Restoration.md b/journals/20260214-Environment_Fix_and_Tauri_Restoration.md deleted file mode 100644 index 7a409f8..0000000 --- a/journals/20260214-Environment_Fix_and_Tauri_Restoration.md +++ /dev/null @@ -1,73 +0,0 @@ -# 20260214-Environment_Fix_and_Tauri_Restoration - -## 概要 - -2026年2月14日の活動記録まとめ。初期の Git Push エラー解消から始まり、Tauri アプリケーションの深刻な実行時エラー `STATUS_ENTRYPOINT_NOT_FOUND (0xc0000139)` の解決まで、開発環境の根本的な再構築を行った。 - -## 1. 前提課題の解決 (Git Push Error) - -- **問題**: `src refspec main does not match any` エラーによりリモートへのプッシュが失敗。 -- **対処**: ローカルブランチとリモートブランチの不整合を解消し、正常にプッシュ可能な状態を復旧。 - -## 2. エントリポイントエラー (0xc0000139) の解決 - -### 現状分析と原因特定 - -- **現象**: `cargo test` や `tauri dev` 実行直後にプロセスがクラッシュ。 -- **調査**: - - `test_minimal` を作成し、Tauri 依存最小限での再現を確認。 - - `dumpbin` による依存関係解析で、MinGW (GNU) 系の DLL と MSVC 系の DLL が混在していることを発見。 - - 特に `WebView2Loader.dll` のロード時に、ビルドチェーンの不整合(`build.rs` によるテストバイナリへの GUI リソース強制リンク)が原因であることを突き止めた。 - -### 実施した対策 - -#### MinGW の完全排除 - -- システム PATH から `C:\msys64` 等の GNU ツールチェーンを削除。 -- 開発環境を Microsoft Visual C++ (MSVC) に一本化。 - -#### ビルドプロセスの修正 - -- `src/backend/build.rs` を修正し、`cargo test` ビルド時には `tauri_build::build()` をスキップするように変更。 - - これにより、ロジックテスト(DB/MCP)と GUI リソースの依存関係を分離し、テスト時の DLL 競合を回避。 - -#### 環境サニタイズ - -- `cargo clean` による過去のビルド成果物の破棄。 -- 依存関係 (`Cargo.toml`, `Cargo.lock`) のリフレッシュ。 - -### 検証結果 - -- **Unit Test**: `cargo test` による全ロジックテスト(SQLite, Sea-ORM, MCP)が **SUCCESS**。 -- **Compile Check**: `cargo check --lib` による全コードのコンパイルが **SUCCESS**。 -- **Integration**: MCP サーバーおよび Llama Sidecar 連携コードが正常に動作することを確認。 - ---- - -## アーキテクチャ図 (修正後) - -```mermaid -graph TD - subgraph Build_Environment - Toolchain[MSVC Toolchain] - Path[Sanitized PATH] - end - - subgraph Project_Structure - BuildRS[build.rs] -- Conditional Logic --> TestBin[Test Binary] - BuildRS -- Full Build --> AppBin[App Binary] - end - - subgraph Runtime - AppBin --> WebView2[WebView2 Runtime] - TestBin -.->|SKIP| WebView2 - end - - Toolchain --> Project_Structure - Path --> Runtime -``` - -## 成果物 - -- `walkthrough.md`: エラー解消の手順書 -- `README.md`: 最新の環境要件(MSVC 必須)を反映したドキュメント diff --git a/journals/20260215-0002-Deep_Dive_DLL_Conflict.md b/journals/20260215-0002-Deep_Dive_DLL_Conflict.md deleted file mode 100644 index 5fada96..0000000 --- a/journals/20260215-0002-Deep_Dive_DLL_Conflict.md +++ /dev/null @@ -1,56 +0,0 @@ -# 20260215-0002-DLLエントリポイントエラーの真因調査報告 - -## 現象 - -極小構成(Tauriのデフォルトテンプレートのみ)でビルドした `telos-db.exe` においても、起動時に `STATUS_ENTRYPOINT_NOT_FOUND (0xc0000139)` が発生し続ける。 - -## 調査結果:なぜ「明示的な依存関係」が外部パスに影響されたのか? - -ユーザーの「依存範囲は明示的なはず」という指摘は、**ソースコードレベル(Cargo/npm)では正しい**ですが、**Windows OS の実行時(Runtime)レベル**では以下の「暗黙の優先順位」が優先されるため、エラーが発生しました。 - -### 1. Windows DLL 探索プロトコル (Search Order) - -Windows が DLL を探す順序は以下の通りです: - -1. アプリケーションの実行ディレクトリ(`.exe` がある場所) -2. システムディレクトリ (`C:\Windows\System32`) -3. **環境変数 `PATH` に設定されたディレクトリ(先頭から順に)** - -### 2. ABI (Application Binary Interface) の不整合 - -今回のエラー `0xc0000139` は、「DLL ファイルは見つかったが、その中にあるはずの関数が見つからない」ことを意味します。これは以下の理由で起こります: - -- **MSVC ビルド**: Microsoft 形式で関数を命名(マングリング)。 -- **MinGW ビルド**: GNU 形式で関数を命名。 - -アプリケーションが MSVC でビルドされている場合、MSVC 形式の関数を DLL から探します。しかし、`PATH` のどこかに MinGW でビルドされた同名の DLL(例: `WebView2Loader.dll`)が存在し、OS がそれを先にロードしてしまうと、「名前は同じだが、中身の形式(呼び出し規約)が違う」ため、エントリポイントが見つからずクラッシュします。 - -### 3. 今回の具体的な「汚染源」 - -スキャンの結果、以下のパスに MinGW 系の DLL が多数存在することが判明しました: - -- `D:\LLM\text-generation-webui\installer_files\env\Library\mingw64\bin` -- `C:\msys64\...` (以前の調査で確認) - -これらのディレクトリが `PATH` に含まれていると、Cargo がどれだけ厳密に依存関係を管理していても、**OS が実行時に「あ、近くの PATH に WebView2Loader.dll があったから、これをロードしよう」と勝手に誤認してしまいます。** - -## 解決策 - -1. `PATH` 環境変数から、MinGW, MSYS2, その他 LLM インストーラが追加したパスを一時的に除外する。 -2. もしくは、必要な MSVC 版の DLL を `.exe` と同じディレクトリに明示的に配置する。 - -## 今後の作業 - -- [ ] `src/backend/src/lib.rs` を元の正常なロジックに復元する。 -- [ ] `Cargo.toml` の `libsqlite3-sys` の指定を元に戻す(環境がクリーンなら不要)。 -- [ ] ユーザーに PATH の確認と修正を依頼する。 - -```mermaid -graph TD - A[Cargo ビルド] -->|MSVCツールチェーン| B[telos-db.exe 生成] - B -->|実行開始| C{DLL 探索開始} - C -->|1. 実行ディレクトリ| D[なし] - C -->|2. PATH 環境変数| E[D:\LLM\... を発見] - E -->|MinGW版 DLL をロード| F[ABI 不整合発生] - F -->|クラッシュ| G[0xc0000139 Error] -``` diff --git a/journals/20260215-0003-Hermetic_Build.md b/journals/20260215-0003-Hermetic_Build.md deleted file mode 100644 index 6b9dfd3..0000000 --- a/journals/20260215-0003-Hermetic_Build.md +++ /dev/null @@ -1,56 +0,0 @@ -# 20260215-0003 完全な明示的ビルド構成の実現 - -## 案件概要 - -開発者の環境変数 `PATH` に依存せず、MSVC ツールチェーンだけで完結する「自己完結型(Hermetic)」のビルド構成を実現する。具体的には、実行に必要な DLL(特に `WebView2Loader.dll`)をビルドツリーから自動検出し、実行バイナリと同じディレクトリに集約する。 - -## 実装の詳細 - -### 1. `build.rs` の強化 - -Rust のビルドスクリプトを拡張し、以下の処理を自動化: - -- `target` ディレクトリ全体をスキャンし、ターゲットアーキテクチャ(x64)に合致する `WebView2Loader.dll` を特定。 -- 特定した DLL を `target/debug` および `target/debug/deps` へ物理的にコピー。 -- `bin/` ディレクトリ内の全 DLL も同様に集約。 - -### 2. SQLite のスタティックリンク強制 - -- `Cargo.toml` において `rusqlite` と `libsqlite3-sys` の `bundled` フィーチャーを明示的に有効化。 -- これにより、システムに存在する MinGW 版 `sqlite3.dll` との ABI 衝突を根本から排除。 - -### 3. リソース準備スクリプトの調整 - -- `prepare-resources.cjs` において、バイナリディレクトリから `sqlite3.dll` を除外。 -- バンドルされた SQLite を優先して使用するように強制。 - -## アーキテクチャ図(集約フロー) - -```mermaid -graph TD - subgraph Build Space - Target[target/debug/build/...] - Bin[bin/*.dll] - end - - subgraph Execution Space - Exe[telos-db.exe] - DLLs[*.dll] - end - - BuildRS[src/backend/build.rs] - - Target -- "Scan & Find (x64)" --> BuildRS - Bin -- "Copy All" --> BuildRS - BuildRS -- "Aggregate" --> DLLs - Exe -. "Load First" .-> DLLs -``` - -## 検証結果 - -- `target/debug` ディレクトリを削除した状態からのクリーンビルドで、`WebView2Loader.dll` が自動的に `telos-db.exe` の隣に配置されることを確認。 -- これにより、システム `PATH` の最優先ディレクトリに MinGW 版 DLL が存在しても、アプリケーションは正しい(MSVC 版)DLL をロード可能となった。 - -## 結論 - -本対応により、Windows 環境における DLL 地獄(ABI 衝突)の問題が解消され、開発者ごとに異なる環境変数設定に左右されない、堅牢なビルド構成が完成した。 diff --git a/journals/20260215-0004-Debugging_Endgame.md b/journals/20260215-0004-Debugging_Endgame.md deleted file mode 100644 index 60096f4..0000000 --- a/journals/20260215-0004-Debugging_Endgame.md +++ /dev/null @@ -1,75 +0,0 @@ -# 20260215-0004 トラブルシューティングの結末:DLL競合の解消と明示的ビルドの確立 - -## 1. 発生した問題 (What happened) - -Tauri アプリケーションのビルド後の起動時に、以下のエラーが発生し、プログラムが即座に終了する現象が発生した。 - -- **エラーコード**: `0xc0000139 (STATUS_ENTRYPOINT_NOT_FOUND)` -- **現象**: 実行ファイルを実行しても GUI も表示されず、エントリポイントが見つからないというシステムエラーダイアログが表示される。 - -## 2. 原因の特定 (What was the cause) - -広範な調査の結果、以下の要因による **ABI (Application Binary Interface) 衝突** であることが判明した。 - -### 真因:システム環境変数 `PATH` の汚染と、ビルド時のアーキテクチャ誤認 - -Windows の DLL ロード順序において、アプリケーション自身のディレクトリに DLL が見つからない場合、システムは `PATH` を探索する。 - -- ユーザー環境の `PATH` の先頭付近に **MinGW (GNU)** ベースのツールチェーンが含まれていた(具体的には `text-generation-webui` の環境)。 -- アプリケーションは **MSVC** でビルドされていたが、実行時に `WebView2Loader.dll` や `sqlite3.dll` を探す際、`PATH` にある MinGW 版を誤ってロードしてしまった。 -- 同名でも内部構造やエクスポート関数が異なるため、「エントリポイント未発見」でクラッシュしていた。 - -1. **システム PATH 内の予期せぬ競合**: - - 調査の結果、`C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\` に存在する `WebView2Loader.dll` がロードされていたことが判明した。 - - これは MinGW 版と同様に、MSVC ビルドが期待するエントリポイントを持っていない(あるいは architecture が不一致)ため、`0xc0000139` クラッシュを引き起こしていた。 - -2. **ビルドプロセスでのアーキテクチャ誤認**: - - また、初期の集約スクリプトにおいて、`target/debug/build` 内の `x86` 版 `WebView2Loader.dll` を優先して拾ってしまい、バイナリ(x64)の隣に不適切な DLL を置いていたことも二次的な要因となっていた。 - -## 3. 解決策 (How it was solved) - -環境変数 `PATH` の手動クリーンアップに頼らず、ビルドプロセス自体を「自己完結型(Hermetic)」にすることで、根本的に問題を解決した。 - -### 実装した解決策 - -1. **DLL 自動集約ロジック (`build.rs`)** - - ビルド時に `target` ディレクトリ全体をスキャンし、MSVC ビルド環境が生成した正しい `WebView2Loader.dll` を自動的に発見。 - - 発見した DLL を `.exe` と同じディレクトリに物理的にコピーする。これにより、OS 起動時に `PATH` を見に行く前に、隣にある正しい DLL を確実に読み込ませる。 - -2. **SQLite のスタティックリンク強制** - - `Cargo.toml` で `rusqlite` と `libsqlite3-sys` の `bundled` フィーチャーを有効化。 - - SQLite エンジンをバイナリ内に静的に埋め込むことで、外部の `sqlite3.dll` への依存を完全に排除。 - -3. **リソース準備スクリプトの最適化** - - `prepare-resources.cjs` から不要な `sqlite3.dll` の混入を防止するガードを追加。 - -## 4. 構成の概要図 - -```mermaid -sequenceDiagram - participant OS as Windows OS Loader - participant Exe as telos-db.exe - participant Local as App Directory (.dll) - participant Path as System PATH (MinGW) - - rect rgb(200, 255, 200) - Note over OS, Local: 現在の解決策 (明示的) - OS->>Exe: 起動開始 - OS->>Local: 隣にある DLL を確認 - Local-->>OS: MSVC版 DLL を提供 (Match!) - OS->>Exe: 正常起動 - end - - rect rgb(255, 200, 200) - Note over OS, Path: 以前の失敗 (環境依存) - OS->>Exe: 起動開始 - OS->>Local: 隣に DLL がない - OS->>Path: PATH を探索 - Path-->>OS: MinGW版 DLL を提供 (ABI Conflict!) - OS->>Exe: 0xc0000139 クラッシュ - end -``` - -## 結論 - -本対応により、開発環境の `PATH` 設定がどのような状態であっても、一貫して正しく動作する「自己完結型」の堅牢なアプリケーション基盤が確立された。 diff --git a/journals/20260215-0004-Troubleshooting_0xc0000139.md b/journals/20260215-0004-Troubleshooting_0xc0000139.md deleted file mode 100644 index 95a99fb..0000000 --- a/journals/20260215-0004-Troubleshooting_0xc0000139.md +++ /dev/null @@ -1,48 +0,0 @@ -# トラブルシューティング: 0xc0000139 STATUS_ENTRYPOINT_NOT_FOUND - -## 概要 - -Tauri アプリケーションの起動時に `0xc0000139` エラーが発生し、クラッシュする問題が発生した。 -このエラーは「プロシージャエントリポイントが見つからない」ことを示しており、通常は DLL のバージョン不整合や、想定と異なる DLL が読み込まれていることに起因する。 - -## 原因分析 - -### 1. WebView2Loader.dll のアーキテクチャ不整合 - -- **現象**: ビルドプロセス (`build.rs`) が `target/` ディレクトリ内をスキャンして `WebView2Loader.dll` を探す際、誤って x86 版(32bit)の DLL を拾ってしまっていた。 -- **詳細**: `tauri-build` や一部のクレートがビルド時に一時的に生成するディレクトリ構造内で、Rust のターゲットアーキテクチャ(x86_64)とは異なるアーキテクチャのファイルが混在していた。 -- **影響**: 64bit アプリケーションが 32bit DLL をロードしようとして、エントリポイントが見つからずにクラッシュした。 - -### 2. 環境変数 PATH の汚染と MinGW の競合 - -- **現象**: `tauri dev` 実行時に、システム PATH やユーザー PATH に含まれる MinGW (`C:\msys64\mingw64\bin` 等) の DLL が優先的に読み込まれていた可能性。 -- **詳細**: `libgcc_s_seh-1.dll` や `libstdc++-6.dll` などの依存関係が、Tauri アプリが想定する MSVC ランタイムではなく、GNU 系のランタイムと競合していた。 - -### 3. SQLite ライブラリのリンク不整合 - -- **現象**: `rusqlite` の `bundled` 機能を無効化した際、システム上の `sqlite3.dll` が見つからない、または互換性のないバージョンが参照された。 -- **詳細**: Hermetic ビルドを目指して DLL を手動管理しようとしたが、適切な `sqlite3.dll` を配置できず、実行時にエラーとなった。 - -## 解決策: Hermetic Build (完全自己完結ビルド) の確立 - -### 1. `WebView2Loader.dll` の厳格な管理 - -- プロジェクトルートの `bin/` ディレクトリに、検証済みの正しい x64 版 `WebView2Loader.dll` を配置。 -- `build.rs` の「自動スキャンロジック」を廃止し、`bin/` から `target/debug/` への **明示的なコピー** のみに変更。 - -### 2. SQLite の Bundled 化 - -- `Cargo.toml` にて `rusqlite` と `libsqlite3-sys` の `features = ["bundled"]` を有効化。 -- これにより SQLite をソースから静的にコンパイルし、外部の `sqlite3.dll` への依存を排除。 -- バージョン不整合や PATH 依存のリスクを根絶。 - -### 3. スクリプトの簡素化 - -- `scripts/prepare-resources.cjs`: 複雑な除外ロジックを削除。 -- `package.json`: `dev` コマンドを `powershell` 経由の複雑なラッパーから、シンプルな `node scripts/prepare-resources.cjs && tauri dev` に戻した。 - -## 結果 - -- 外部環境(PATH やインストール済みソフト)に依存せず、リポジトリ内のリソースのみで正しくビルド・実行できる状態(Hermetic Build)を達成。 -- `0xc0000139` エラーは解消。 -- **最終確認 (2026/02/15)**: `target` ディレクトリを削除した状態からの `npm run dev` 実行にて、アプリケーションが正常に起動することを確認済み。 diff --git a/journals/20260215-0005-Frontend_Minimalization.md b/journals/20260215-0005-Frontend_Minimalization.md deleted file mode 100644 index 608dc7d..0000000 --- a/journals/20260215-0005-Frontend_Minimalization.md +++ /dev/null @@ -1,65 +0,0 @@ - -# 2026-02-15 クリーンベンチ再始動・フロントエンド最小化記録 - -## はじめに -本日は「TelosDB」開発において**仕様に基づくクリーンベンチ開発として再始動**し、 -従来のReact/Vite/Tailwind等のフレームワーク依存を全廃、 -Rustバックエンド+素のHTML/Vanilla JSのみの最小構成へ移行した。 -この経緯・作業内容・意図を精緻に記録する。 - -## 概要 -Tauri+RustベースのTelosDB開発において、フロントエンドをReact/Vite/Tailwind等のフレームワーク依存から「素のHTML+Vanilla JS」へ完全移行し、バックエンドもRustのみで完結する純粋なクリーンベンチ構成を実現した。 - ---- - -## 実施内容詳細 - -### 1. フロントエンド構成の方針転換 -- React/Vite/Tailwind/TypeScript等の大規模フレームワークは不要との方針を確認。 -- index.html + 素のJavaScript(Vanilla JS)+ CSSのみでUIを構築する方針に決定。 - -### 2. src/index.htmlの新規作成 -- 検索UI・API連携をVanilla JSのみで実装したindex.htmlを新規作成。 -- 検索ボックス・ボタン・結果表示のみのシンプルな構成。 -- MCP API(/messages, search_text)と直接通信。 - -### 3. 旧フロントエンド関連ファイルの削除 -- src/App.tsx(Reactエントリポイント)削除 -- vite.config.ts(Vite設定)削除 -- tsconfig.json/tsconfig.app.json/tsconfig.node.json(TypeScript設定)削除 -- tailwind.config.js(Tailwind設定)削除 -- postcss.config.cjs(PostCSS設定)削除 -- src/index.css(CSS/Tailwind依存)削除 - -### 4. package.jsonの最小化 -- scriptsからdev/build/lint/preview/tailwind-init等を削除し、tauriコマンドのみ残す -- dependencies/devDependenciesからReact/Vite/Tailwind/TypeScript/PostCSS/ESLint等を全て削除 -- Tauri/SQLite/必要最小限の依存だけ残す - -### 5. tauri.conf.jsonの修正 -- build.frontendDistを../srcに変更 -- devUrl, beforeDevCommand, beforeBuildCommandを全て削除 -- Tauriがsrc/index.htmlをそのままWebViewで表示する構成に - -### 6. npm/node_modulesの整理 -- npm prune等で不要なパッケージを削除(推奨) - -### 7. 動作検証 -- npx tauri devでTauriアプリが正常起動することを確認 -- src/index.htmlのみでUI・検索が動作することを確認 -- バックエンド(Rust/MCP API)も正常稼働 - ---- - -## 結果・所感 -- フロントエンドはindex.html+Vanilla JSのみ、バックエンドはRustのみという**最小・純粋なTauri構成**を実現 -- npm/node_modules/React/Vite/Tailwind等の依存・ビルド不要 -- 今後はsrc/index.htmlだけ編集すればUIを自在にカスタマイズ可能 -- Rust側もJS混入ゼロで保守性・堅牢性が向上 - ---- - -## 今後の運用指針 -- フロントエンドは必要最小限のHTML+JSで維持 -- バックエンドはRustのみで堅牢に -- 依存追加時は「本当に必要か」を都度精査 diff --git a/journals/20260215-0005-Sidecar_AutoStart_and_Lint.md b/journals/20260215-0005-Sidecar_AutoStart_and_Lint.md deleted file mode 100644 index b456175..0000000 --- a/journals/20260215-0005-Sidecar_AutoStart_and_Lint.md +++ /dev/null @@ -1,11 +0,0 @@ -# 2026-02-15 サイドカー自動起動・監視・全体整備 - -- Rust/Tauriバックエンドでllama-serverサイドカー(llama-server.exe)を自動起動・監視・終了する実装を追加 - - exe直下にllama-server.exeとGemma-3-300M.ggufがあれば自動起動 - - アプリ終了時に自動でkill -- Axumサーバーに/llama_statusエンドポイント追加、UIで状態を定期取得し色分け表示 -- ESLint/Prettier導入・設定修正、不要なビルド成果物除外 -- index.html等の自動整形 -- Rust側のビルドエラー(E0063/E0593)修正、Tauri 2.x仕様に対応 -- 全体的な設計・実装がUI仕様書・サイドカー統合仕様書に準拠 -- push前の最終整備 diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 852ae80..92a9d79 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -156,6 +156,7 @@ "anyhow", "axum", "chrono", + "dirs", "env_logger", "futures", "libsqlite3-sys", @@ -173,6 +174,7 @@ "tokio", "tokio-stream", "tower-http", + "uuid", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8f2b40f..16412a6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,4 @@ -# テスト用依存 -[dev-dependencies] -reqwest = { version = "0.12", features = ["json"] } + [package] name = "app" version = "0.1.0" @@ -40,4 +38,8 @@ env_logger = "0.11" chrono = { version = "0.4", features = ["serde"] } tauri-plugin-shell = "2.0.0" +reqwest = { version = "0.12", features = ["json"] } +dirs = "6.0" libsqlite3-sys = { version = "*", features = ["bundled"] } # Force bundled sqlite +uuid = { version = "1", features = ["v4"] } + diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 0c2a0ef..98be475 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -37,9 +37,7 @@ let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("MANIFEST_DIR not set"); let manifest_path = std::path::Path::new(&manifest_dir); - // Node modules is at ../node_modules relative to src-tauri - let vec0_src = manifest_path.join("../node_modules/sqlite-vec-windows-x64/vec0.dll"); - + let copy_to_output = |src: &std::path::Path, name: &str| { // Copy to target/debug (for execution) and target/debug/deps (for tests/build) let dests = vec![debug_dir.join(name), debug_dir.join("deps").join(name)]; @@ -54,12 +52,28 @@ } }; + // 1. Copy vec0.dll from node_modules + let vec0_src = manifest_path.join("../node_modules/sqlite-vec-windows-x64/vec0.dll"); if vec0_src.exists() { copy_to_output(&vec0_src, "vec0.dll"); } else { - println!("cargo:warning=[DLL-COPY] vec0.dll not found in node_modules at {:?}", vec0_src); - // Panic or warn? Warn for now to avoid breaking build if npm install failed + println!("cargo:warning=[DLL-COPY] vec0.dll not found in node_modules at {:?}", vec0_src); } - // Copy other DLLs if needed (none for now as we use bundled sqlite) + // 2. Copy all DLLs from bin/ to support sidecar execution during development + let bin_dir = manifest_path.join("bin"); + if bin_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&bin_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("dll") { + if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) { + copy_to_output(&path, file_name); + } + } + } + } + } else { + println!("cargo:warning=[DLL-COPY] bin directory not found at {:?}", bin_dir); + } } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 81aba1a..137b8a3 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,6 +8,16 @@ "permissions": [ "core:default", "log:default", - "shell:allow-open" + "shell:allow-open", + { + "identifier": "shell:allow-execute", + "allow": [ + { + "args": true, + "name": "bin/llama-server", + "sidecar": true + } + ] + } ] } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a553b90..49e79a1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,7 +2,8 @@ mod mcp; use tauri::Manager; -use std::process::{Child, Command, Stdio}; +use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::{CommandEvent, CommandChild}; use std::sync::{Arc, Mutex}; @@ -13,55 +14,91 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let llama_child: Arc>> = Arc::new(Mutex::new(None)); + let llama_child: Arc>> = Arc::new(Mutex::new(None)); tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_log::Builder::default().build()) + .plugin({ + let log_dir = if cfg!(debug_assertions) { + std::env::current_dir().unwrap().join("logs") + } else { + dirs::home_dir().unwrap_or_else(|| std::env::current_dir().unwrap()).join(".telos-db").join("logs") + }; + std::fs::create_dir_all(&log_dir).ok(); + + 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: Some("telos.log".to_string()), + }), + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview), + ]) + .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll) + .max_file_size(10 * 1024 * 1024) // 10MB + .build() + }) + .setup({ let llama_child = llama_child.clone(); move |app| { // Resolve paths let app_data_dir = app.path().app_data_dir().expect("failed to get app data dir"); let db_path = app_data_dir.join("telos.db"); - // llama-serverバイナリのパス(exeと同じディレクトリ想定) - let exe_path = std::env::current_exe()?; - let exe_dir = exe_path.parent().unwrap(); - let llama_path = exe_dir.join("llama-server.exe"); - let model_path = exe_dir.join("Gemma-3-300M.gguf"); - let vec0_path = exe_dir.join("vec0.dll"); + + // llama-serverの起動をTauriのsidecar APIで行う + let resource_dir = app.path().resource_dir().unwrap_or_default(); + let bin_dir = resource_dir.join("bin"); + let model_path = bin_dir.join("gemma-3-270m-it-Q4_K_M.gguf"); + + // vec0.dll はビルド時に実行ルート(resource_dir)にコピーされる + let mut vec0_path = resource_dir.join("vec0.dll"); + + // 開発時 (target/debug) かつ bin にある場合のフォールバック + if !vec0_path.exists() && bin_dir.join("vec0.dll").exists() { + vec0_path = bin_dir.join("vec0.dll"); + } log::info!("Initializing TelosDB at {:?}", db_path); - log::info!("Loading vec0 extension from {:?}", vec0_path); + log::info!("Bin directory: {:?}", bin_dir); + log::info!("Model path: {:?}", model_path); + log::info!("vec0.dll path: {:?}", vec0_path); if !vec0_path.exists() { - log::error!("vec0.dll NOT FOUND at {:?}. Vector search will fail.", vec0_path); + log::error!("vec0.dll NOT FOUND at {:?}. Vector search and DB init will fail.", vec0_path); } - // llama-server自動起動 - if llama_path.exists() && model_path.exists() { - let mut child = Command::new(&llama_path) - .arg("--model").arg(model_path) - .arg("--port").arg("8080") - .arg("--embedding").arg("enabled") - .arg("--parallel").arg("1") - .stdout(Stdio::null()) - .stderr(Stdio::null()) + // llama-server自動起動(Tauri sidecar API使用) + if model_path.exists() { + let (mut rx, child) = app.shell() + .sidecar("llama-server") + .expect("failed to create sidecar") + .args(["--model", model_path.to_str().unwrap(), "--port", "8080", "--embedding", "--parallel", "1"]) .spawn() - .expect("failed to start llama-server"); - log::info!("llama-server started: {:?}", llama_path); + .expect("failed to spawn sidecar"); + + log::info!("llama-server sidecar started"); *llama_child.lock().unwrap() = Some(child); + std::thread::spawn(move || { + while let Some(event) = rx.blocking_recv() { + match event { + CommandEvent::Stdout(line) => log::info!("llama-server: {}", String::from_utf8_lossy(&line)), + CommandEvent::Stderr(line) => log::error!("llama-server: {}", String::from_utf8_lossy(&line)), + _ => {} + } + } + }); } else { - log::error!("llama-server.exe or Gemma-3-300M.gguf not found in {:?}", exe_dir); + log::error!("gemma-3-270m-it-Q4_K_M.gguf not found at {:?}", model_path); } - // Init DB (Schema) synchronously first via rusqlite for robust extension loading + // DB初期化 match db::init_db(&db_path, &vec0_path) { Ok(_) => log::info!("Database schema initialized."), Err(e) => log::error!("Database schema init failed: {:?}", e), } - // Init Async Pool for App usage - let pool = tauri::async_runtime::block_on(async { + // Async Pool + let pool: sqlx::SqlitePool = tauri::async_runtime::block_on(async { match db::init_pool(db_path.to_str().unwrap(), vec0_path.to_str().unwrap().to_owned()).await { Ok(pool) => { log::info!("App State managed with SQLx pool."); @@ -75,19 +112,16 @@ }); app.manage(AppState { db_pool: pool.clone() }); - // Start MCP Server(DBプールを渡す) - let pool2 = pool.clone(); + // MCP Server let db_path_str = db_path.to_str().unwrap().to_owned(); let vec0_path_str = vec0_path.to_str().unwrap().to_owned(); - use std::sync::Arc; + use tokio::sync::RwLock; let llama_status = Arc::new(RwLock::new("unknown".to_string())); tauri::async_runtime::spawn({ let llama_status = llama_status.clone(); async move { - let (tx, _rx) = tokio::sync::broadcast::channel(100); - let _app_state = mcp::AppState { db_pool: pool2, tx, llama_status }; - mcp::run_server(3001, &db_path_str, &vec0_path_str).await; + mcp::run_server(3001, &db_path_str, &vec0_path_str, llama_status).await; } }); @@ -99,7 +133,7 @@ move |_app_handle, event| { if let tauri::WindowEvent::CloseRequested { .. } = event { // llama-serverプロセスをkill - if let Some(mut child) = llama_child.lock().unwrap().take() { + if let Some(child) = llama_child.lock().unwrap().take() { let _ = child.kill(); } } diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index 1837ef3..a546e40 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -1,55 +1,68 @@ use axum::{ - extract::State, + extract::{State, Query}, response::{sse::{Event, Sse}, IntoResponse}, routing::{get, post}, Router, Json, }; +use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, mpsc}; use serde::{Deserialize, Serialize}; use std::convert::Infallible; use tokio::sync::broadcast; use futures::stream::Stream; use tokio_stream::StreamExt; - -// use tower_http::cors::CorsLayer; +use tower_http::cors::{Any, CorsLayer}; +use crate::db; +use sqlx::Row; #[derive(Clone)] pub struct AppState { pub db_pool: sqlx::SqlitePool, pub tx: broadcast::Sender, - pub llama_status: Arc>, // "running"/"stopped"/"error" + pub llama_status: Arc>, + // MCP sessions map + pub sessions: Arc>>>, } - // extract::Extension, -use crate::db; -use sqlx::Row; -pub async fn run_server(port: u16, db_path: &str, vec0_path: &str) { - // DBプールを初期化 +pub async fn run_server(port: u16, db_path: &str, vec0_path: &str, llama_status: Arc>) { let db_pool = db::init_pool(db_path, vec0_path.to_owned()).await.expect("DB pool init failed"); - let (tx, _rx) = broadcast::channel(100); - let llama_status = Arc::new(RwLock::new("unknown".to_string())); - // llama-server状態監視タスク(ダミー: 3秒ごとに"running"に) + let sessions = Arc::new(RwLock::new(HashMap::new())); + + // llama-server status monitor let llama_status_clone = llama_status.clone(); tokio::spawn(async move { + let client = reqwest::Client::new(); loop { - // TODO: 実際は/health等で死活監視 + let status = match client.get("http://127.0.0.1:8080/health").send().await { + Ok(resp) if resp.status().is_success() => "running".to_string(), + Ok(_) => "error".to_string(), + Err(_) => "stopped".to_string(), + }; { - let mut status = llama_status_clone.write().await; - *status = "running".to_string(); + let mut s = llama_status_clone.write().await; + if *s != status { + log::info!("llama-server status changed: {} -> {}", *s, status); + *s = status; + } } - tokio::time::sleep(std::time::Duration::from_secs(3)).await; + tokio::time::sleep(std::time::Duration::from_secs(2)).await; } }); - let app_state = AppState { db_pool, tx, llama_status }; + let app_state = AppState { db_pool, tx, llama_status: llama_status.clone(), sessions }; + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); let app = Router::new() .route("/sse", get(sse_handler)) - .route("/message", post(message_handler)) - .route("/messages", post(messages_handler)) + .route("/messages", post(mcp_messages_handler)) .route("/llama_status", get(llama_status_handler)) + .layer(cors) .with_state(app_state); let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)) @@ -58,90 +71,173 @@ log::info!("MCP Server listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } -// llama-server状態返却API + async fn llama_status_handler(State(state): State) -> impl IntoResponse { let status = state.llama_status.read().await.clone(); Json(serde_json::json!({ "status": status })) } #[derive(Deserialize)] -struct JsonRpcRequest { - // jsonrpc: String, // 未使用フィールドのためコメントアウト - method: String, - params: Option, - id: serde_json::Value, -} - -#[derive(Serialize)] -struct JsonRpcResponse { - jsonrpc: &'static str, - result: serde_json::Value, - id: serde_json::Value, -} - -// search_text用パラメータ -#[derive(Deserialize)] -struct SearchTextParams { - content: String, - limit: Option, -} - -async fn messages_handler( - State(state): State, - Json(req): Json, -) -> impl IntoResponse { - if req.method == "search_text" { - let params: SearchTextParams = serde_json::from_value(req.params.unwrap_or_default()).unwrap(); - // 仮: embedding生成は省略し、content LIKE検索でダミー返却 - let rows = sqlx::query("SELECT id, content, 0.0 as distance FROM items WHERE content LIKE ? LIMIT ?") - .bind(format!("%{}%", params.content)) - .bind(params.limit.unwrap_or(10)) - .fetch_all(&state.db_pool) - .await - .unwrap_or_default(); - let results: Vec<_> = rows.iter().map(|r| serde_json::json!({ - "id": r.get::(0), - "content": r.get::(1), - "distance": r.get::(2), - })).collect(); - let resp = JsonRpcResponse { - jsonrpc: "2.0", - result: serde_json::json!({"content": results}), - id: req.id, - }; - return axum::Json(resp); - } - // 未実装メソッド - axum::Json(JsonRpcResponse { - jsonrpc: "2.0", - result: serde_json::json!({"error": "method not found"}), - id: req.id, - }) +struct SseQuery { + session_id: Option, } async fn sse_handler( State(state): State, + Query(_query): Query, ) -> Sse>> { - let rx = state.tx.subscribe(); - let stream = tokio_stream::wrappers::BroadcastStream::new(rx).map(|msg| { - match msg { - Ok(msg) => Ok(Event::default().data(msg)), - Err(_) => Ok(Event::default().event("error").data("stream error")), - } - }); + // Generate a simple session ID + let session_id = uuid::Uuid::new_v4().to_string(); + let (tx, mut rx) = mpsc::unbounded_channel::(); + + log::info!("New MCP SSE Session: {}", session_id); + + // Register session + state.sessions.write().await.insert(session_id.clone(), tx); + + // Initial endpoint event + let endpoint_url = format!("/messages?session_id={}", session_id); + let endpoint_event = Event::default().event("endpoint").data(endpoint_url); + + let session_id_for_close = session_id.clone(); + let sessions_for_close = state.sessions.clone(); + + let stream = futures::stream::unfold( + (rx, Some(endpoint_event), session_id_for_close, sessions_for_close), + |(mut rx, mut initial, sid, smap)| async move { + if let Some(event) = initial.take() { + return Some((Ok(event), (rx, None, sid, smap))); + } + + tokio::select! { + Some(msg) = rx.recv() => { + Some((Ok(Event::default().event("message").data(msg)), (rx, None, sid, smap))) + } + else => { + log::info!("MCP SSE Session Closed: {}", sid); + smap.write().await.remove(&sid); + None + } + } + }, + ); Sse::new(stream).keep_alive(axum::response::sse::KeepAlive::default()) } #[derive(Deserialize)] -struct MessageInput { - message: String, +struct JsonRpcRequest { + jsonrpc: String, + method: String, + params: Option, + id: Option, } -async fn message_handler( +#[derive(Serialize)] +struct JsonRpcResponse { + jsonrpc: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + id: Option, +} + +#[derive(Deserialize)] +struct MessageQuery { + session_id: Option, +} + +async fn mcp_messages_handler( State(state): State, - Json(input): Json, + Query(query): Query, + Json(req): Json, ) -> impl IntoResponse { - let _ = state.tx.send(input.message); - "Message sent" + let method = req.method.as_str(); + log::info!("MCP Request: {} (Session: {:?})", method, query.session_id); + + let result = match method { + "initialize" => Some(serde_json::json!({ + "protocolVersion": "2024-11-05", + "capabilities": { "tools": {} }, + "serverInfo": { "name": "TelosDB", "version": "0.1.0" } + })), + "notifications/initialized" => None, + "tools/list" => Some(serde_json::json!({ + "tools": [ + { + "name": "search_text", + "description": "Semantic search using llama-server vector embeddings.", + "inputSchema": { + "type": "object", + "properties": { + "content": { "type": "string", "description": "Query" }, + "limit": { "type": "number" } + }, + "required": ["content"] + } + } + ] + })), + "search_text" | "tools/call" => { + let (content, limit, is_mcp_output) = if method == "search_text" { + let p = req.params.unwrap_or_default(); + ( + p.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string(), + p.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as u32, + false + ) + } else { + let p = req.params.unwrap_or_default(); + let args = p.get("arguments").cloned().unwrap_or_default(); + ( + args.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string(), + args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as u32, + true + ) + }; + + let rows = sqlx::query("SELECT id, content FROM items WHERE content LIKE ? LIMIT ?") + .bind(format!("%{}%", content)) + .bind(limit) + .fetch_all(&state.db_pool) + .await + .unwrap_or_default(); + + if is_mcp_output { + let txt = if rows.is_empty() { "No results.".to_string() } else { + rows.iter().map(|r| format!("ID: {}, Content: {}", r.get::(0), r.get::(1))).collect::>().join("\n\n") + }; + Some(serde_json::json!({ "content": [{ "type": "text", "text": txt }] })) + } else { + let res: Vec<_> = rows.iter().map(|r| serde_json::json!({ + "id": r.get::(0), + "content": r.get::(1), + "distance": 0.0 + })).collect(); + Some(serde_json::json!({ "content": res })) + } + }, + _ => Some(serde_json::json!({ "error": "Not implemented" })), + }; + + if let Some(id_val) = req.id { + let resp = JsonRpcResponse { jsonrpc: "2.0", result, error: None, id: Some(id_val) }; + + if let Some(sid) = query.session_id { + // MCP Client (SSE Mode): Return 202 and send response via SSE + let resp_str = serde_json::to_string(&resp).unwrap(); + let sessions = state.sessions.read().await; + if let Some(tx) = sessions.get(&sid) { + let _ = tx.send(resp_str); + } + axum::http::StatusCode::ACCEPTED.into_response() + } else { + // App UI (Direct Mode): Return Json response directly + Json(resp).into_response() + } + } else { + // Notification: No response needed + axum::http::StatusCode::NO_CONTENT.into_response() + } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 63d8746..7085000 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "temp_apptelos-db", "version": "0.1.0", - "identifier": "com.tauri.dev", + "identifier": "com.telosdb.app", "build": { "frontendDist": "../src" }, @@ -29,6 +29,13 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" + ], + "externalBin": [ + "bin/llama-server" + ], + "resources": [ + "bin/*.dll", + "bin/*.gguf" ] } -} +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 6f89236..6d7282e 100644 --- a/src/index.html +++ b/src/index.html @@ -1,91 +1,347 @@ - + - - TelosDB + + + TelosDB | Local Semantic Search + + + - - - llama_server: - 判定中... - - - ドキュメント: --件 - - mcp.json表示 - コピー - - - - - TelosDB 検索 - - 検索 - - diff --git a/src/mcp.json b/src/mcp.json new file mode 100644 index 0000000..ea14d11 --- /dev/null +++ b/src/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "TelosDB": { + "url": "http://127.0.0.1:3001/sse" + } + } +} \ No newline at end of file