diff --git a/docs/specification/03_database_specification.md b/docs/specification/03_database_specification.md index 7647933..94bf687 100644 --- a/docs/specification/03_database_specification.md +++ b/docs/specification/03_database_specification.md @@ -22,6 +22,7 @@ documents { integer id PK "文書ID (自動採番)" text path "出典・ファイルパス (Unique)" + text mime "MIMEタイプ (text/markdown等)" datetime created_at "作成日" datetime updated_at "更新日" } @@ -69,7 +70,7 @@ ### 4.1 `documents` (文書メタデータ管理) -各ソース(ファイル等)の一意な情報を保持します。`path` カラムにより同一ソースの重複登録を防ぎます。 +各ソース(ファイル等)の一意な情報を保持します。`path` カラムにより同一ソースの重複登録を防ぎ、`mime` カラムでファイル形式を識別します。 ### 4.2 `items` (チャンク管理) diff --git "a/journals/20260223-0015-MIME\343\202\277\343\202\244\343\203\227\345\257\276\345\277\234\343\201\256\350\277\275\345\212\240.md" "b/journals/20260223-0015-MIME\343\202\277\343\202\244\343\203\227\345\257\276\345\277\234\343\201\256\350\277\275\345\212\240.md" new file mode 100644 index 0000000..f6016ed --- /dev/null +++ "b/journals/20260223-0015-MIME\343\202\277\343\202\244\343\203\227\345\257\276\345\277\234\343\201\256\350\277\275\345\212\240.md" @@ -0,0 +1,84 @@ +# Journal: 20260223-0015-MIMEタイプ対応の追加 (Adding MIME Type Support) + +## 日本語 (Japanese) + +### 1. 作業実施の理由 + +ユーザーの提案に基づき、`documents` テーブルに MIME タイプを保持することで、将来的なファイル形式の拡張(PDF, 画像, 各種コード等)に対応しやすくするため。 + +### 2. 指示(背景、観点、意図を含む) + +- **背景**: 現状はテキストチャンクのみだが、ソースファイルの形式をメタデータとして保持したい。 +- **観点**: スキーマの正規化を維持しつつ、自動検知と明示的指定の両方をサポートする。 +- **意図**: フロントエンドでの表示切り替えや、バックエンドでの適切なパース処理の布石とする。 + +### 3. 指示事項とその対応 + +- **スキーマ更新**: `documents` テーブルに `mime TEXT` カラムを追加。 +- **自動検知の実装**: `mcp.rs` 内で、`path` の拡張子から主要な MIME タイプ(markdown, rust, javascript 等)を自動判別するロジックを追加。 +- **ツール更新**: `add_item_text` に `mime` 引数を追加。また、検索結果に `mime` 情報を返すように変更。 +- **ドキュメント更新**: データベース仕様書(`03_database_specification.md`)に新設カラムを反映。 + +### 4. 作業詳細 + +AIエージェントは以下の手順で作業を実施した。 + +1. `src/backend/src/db.rs` を修正し、テーブル作成 SQL に `mime` カラムを追加。 +2. `src/backend/src/mcp.rs` をリファクタリング。 + - `add_item_text` の引数処理と、拡張子ベースの MIME 推測ロジックを実装。 + - すでにドキュメントが存在する場合の MIME アップデート処理を追加。 + - `search_text` および `get_item_by_id` の SQL クエリとレスポンス JSON に `mime` カラムを追加。 +3. `docs/specification/03_database_specification.md` を更新。 +4. `cargo check` により、バックエンドの整合性とビルド可能性を確認。 + +### 5. AI視点での結果 + +今回の変更により、TelosDB は単なるテキスト断片の集まりではなく、「型(MIME)を持ったドキュメントの集合」としての性質が強まりました。自動検知ロジックにより、ユーザーが意識せずとも適切なメタデータが蓄積されるようになり、将来的な UI 改善やマルチモーダル対応への強力な基盤となりました。 + +--- + +## English + +### 1. Reason for Work + +Based on the user's suggestion, adding MIME types to the `documents` table to facilitate future support for various file formats (PDF, images, various code types, etc.). + +### 2. Instructions (Background, Perspective, Intent) + +- **Background**: Currently only text chunks are handled, but we want to retain the source file format as metadata. +- **Perspective**: Maintain schema normalization while supporting both automatic detection and explicit specification. +- **Intent**: Lay the groundwork for UI display logic and appropriate backend parsing processes. + +### 3. Points and Responses + +- **Schema Update**: Added a `mime TEXT` column to the `documents` table. +- **Automatic Detection Implementation**: Added logic in `mcp.rs` to automatically determine common MIME types (markdown, rust, javascript, etc.) based on file extensions. +- **Tool Updates**: Added a `mime` argument to `add_item_text` and updated search results to include `mime` information. +- **Documentation Update**: Reflected the new column in the database specification (`03_database_specification.md`). + +### 4. Work Details + +The AI agent carried out the work in the following steps: + +1. Modified `src/backend/src/db.rs` to add the `mime` column to the table creation SQL. +2. Refactored `src/backend/src/mcp.rs`: + - Implemented argument processing and extension-based MIME inference logic for `add_item_text`. + - Added logic to update the MIME type if the document already exists. + - Added the `mime` column to SQL queries and JSON responses for `search_text` and `get_item_by_id`. +3. Updated `docs/specification/03_database_specification.md`. +4. Verified backend consistency and buildability with `cargo check`. + +### 5. Results from AI Perspective + +With this change, TelosDB has evolved from a collection of text fragments into a "collection of typed (MIME) documents." The automatic detection logic ensures that appropriate metadata is accumulated without extra user effort, providing a strong foundation for future UI improvements and multimodal support. + +```mermaid +graph LR + A[File Path] --> B{Extension?} + B -->|md| C[text/markdown] + B -->|rs| D[text/x-rust] + B -->|other| E[application/octet-stream] + C --> F[documents.mime] + D --> F + E --> F +``` diff --git a/src/backend/src/db.rs b/src/backend/src/db.rs index 49ac457..c3b53c7 100644 --- a/src/backend/src/db.rs +++ b/src/backend/src/db.rs @@ -50,6 +50,7 @@ "CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE, + mime TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) )", diff --git a/src/backend/src/mcp.rs b/src/backend/src/mcp.rs index 7e66e74..5104dcf 100644 --- a/src/backend/src/mcp.rs +++ b/src/backend/src/mcp.rs @@ -405,7 +405,8 @@ "type": "object", "properties": { "content": { "type": "string" }, - "path": { "type": "string" } + "path": { "type": "string" }, + "mime": { "type": "string" } }, "required": ["content"] } @@ -482,7 +483,7 @@ "get_item_by_id" => { let id = args.get("id").and_then(|v| v.as_i64()).unwrap_or(0); let row = sqlx::query( - "SELECT i.id, i.content, d.path + "SELECT i.id, i.content, d.path, d.mime FROM items i JOIN documents d ON i.document_id = d.id WHERE i.id = ?" @@ -494,10 +495,12 @@ if let Some(row) = row { let content: String = row.get("content"); let path: String = row.get("path"); + let mime: Option = row.get("mime"); Some(serde_json::json!({ "id": id, "content": content, - "path": path + "path": path, + "mime": mime })) } else { Some(serde_json::json!({ "error": format!("Item not found: {}", id) })) @@ -506,11 +509,31 @@ "add_item_text" => { let content = args.get("content").and_then(|v| v.as_str()).unwrap_or(""); let path_str = args.get("path").and_then(|v| v.as_str()).unwrap_or("unknown"); + let mut mime_str = args.get("mime").and_then(|v| v.as_str()).map(|s| s.to_string()); + + // MIMEタイプが未指定なら拡張子から推測 + if mime_str.is_none() && path_str != "unknown" { + let path = std::path::Path::new(path_str); + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + mime_str = Some(match ext.to_lowercase().as_str() { + "md" | "markdown" => "text/markdown".to_string(), + "txt" => "text/plain".to_string(), + "rs" => "text/x-rust".to_string(), + "js" | "mjs" => "text/javascript".to_string(), + "ts" => "text/typescript".to_string(), + "json" => "application/json".to_string(), + "html" => "text/html".to_string(), + "css" => "text/css".to_string(), + _ => "application/octet-stream".to_string(), + }); + } + } log::info!( - "Executing add_item_text (LSA-only): content length={}, path='{}'", + "Executing add_item_text (LSA-only): content length={}, path='{}', mime='{:?}'", content.chars().count(), - path_str + path_str, + mime_str ); // 800文字ずつに分割 @@ -528,10 +551,23 @@ .fetch_optional(&state.db_pool) .await { - Ok(Some(row)) => Ok(row.get::(0)), + Ok(Some(row)) => { + let id = row.get::(0); + // MIMEタイプが渡されているか、以前が空なら更新を試みる + if let Some(m) = &mime_str { + let _ = sqlx::query("UPDATE documents SET mime = ? WHERE id = ? AND (mime IS NULL OR mime != ?)") + .bind(m) + .bind(id) + .bind(m) + .execute(&state.db_pool) + .await; + } + Ok(id) + }, Ok(None) => { - match sqlx::query("INSERT INTO documents (path) VALUES (?)") + match sqlx::query("INSERT INTO documents (path, mime) VALUES (?, ?)") .bind(path_str) + .bind(mime_str) .execute(&state.db_pool) .await { @@ -705,7 +741,7 @@ let sim: f32 = 1.0 - dist; if let Ok(row) = sqlx::query( - "SELECT i.content, d.path + "SELECT i.content, d.path, d.mime FROM items i JOIN documents d ON i.document_id = d.id WHERE i.id = ?" @@ -714,6 +750,7 @@ "id": id, "content": row.get::(0), "path": row.get::(1), + "mime": row.get::, _>(2), "similarity": sim.clamp(0.0, 1.0) })); } @@ -724,7 +761,7 @@ if search_result.is_none() { let rows = sqlx::query( - "SELECT i.id, i.content, d.path, v.distance + "SELECT i.id, i.content, d.path, d.mime, v.distance FROM items i JOIN documents d ON i.document_id = d.id JOIN vec_items v ON i.id = v.id @@ -742,12 +779,14 @@ let id = r.get::(0); let content = r.get::(1); let path = r.get::(2); - let distance = r.get::(3); + let mime = r.get::, _>(3); + let distance = r.get::(4); let sim = 1.0 - (distance / 2.0); serde_json::json!({ "id": id, "content": content, "path": path, + "mime": mime, "similarity": sim.clamp(0.0, 1.0) }) }).collect(); @@ -762,7 +801,7 @@ if search_result.is_none() { let rows = sqlx::query( - "SELECT i.id, i.content, d.path + "SELECT i.id, i.content, d.path, d.mime FROM items i JOIN documents d ON i.document_id = d.id WHERE i.content LIKE ? LIMIT ?" @@ -777,6 +816,7 @@ "id": r.get::(0), "content": r.get::(1), "path": r.get::(2), + "mime": r.get::,_>(3), "similarity": 0.0 }) }).collect();