diff --git a/mcp/chatgpt.json b/mcp/chatgpt.json index e146936..4842402 100644 --- a/mcp/chatgpt.json +++ b/mcp/chatgpt.json @@ -231,7 +231,7 @@ "0be4b4a5-d52f-4bef-927e-5d6f93a9cb26" ] } - }, + }, "moderation_results": [], "current_node": "", "plugin_ids": null, @@ -251,5 +251,141 @@ "is_do_not_remember": null, "memory_scope": "global_enabled", "id": "" + }, + { + "title": "img", + "create_time": 1747448872.545226, + "update_time": 1748085075.161424, + "mapping": { + "2de0f3c9-52b1-49bf-b980-b3ef9be6551e": { + "id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", + "message": { + "id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", + "author": { + "role": "user", + "name": null, + "metadata": {} + }, + "create_time": 1748085041.769279, + "update_time": null, + "content": { + "content_type": "multimodal_text", + "parts": [ + { + "content_type": "image_asset_pointer", + "asset_pointer": "", + "size_bytes": 425613, + "width": 333, + "height": 444, + "fovea": null, + "metadata": { + "dalle": null, + "gizmo": null, + "generation": null, + "container_pixel_height": null, + "container_pixel_width": null, + "emu_omit_glimpse_image": null, + "emu_patches_override": null, + "sanitized": true, + "asset_pointer_link": null, + "watermarked_asset_pointer": null + } + }, + "" + ] + }, + "status": "finished_successfully", + "end_turn": null, + "weight": 1.0, + "metadata": { + "attachments": [ + { + "name": "", + "width": 333, + "height": 444, + "size": 425613, + "id": "file-35eytNMMTW2k7vKUHBuNzW" + } + ], + "request_id": "944c59177932fc9a-KIX", + "message_source": null, + "timestamp_": "absolute", + "message_type": null + }, + "recipient": "all", + "channel": null + }, + "parent": "7960fbff-bc4f-45e7-95e9-9d0bc79d9090", + "children": [ + "98d84adc-156e-4c81-8cd8-9b0eb01c8369" + ] + }, + "98d84adc-156e-4c81-8cd8-9b0eb01c8369": { + "id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369", + "message": { + "id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369", + "author": { + "role": "assistant", + "name": null, + "metadata": {} + }, + "create_time": 1748085043.312312, + "update_time": null, + "content": { + "content_type": "text", + "parts": [ + "" + ] + }, + "status": "finished_successfully", + "end_turn": true, + "weight": 1.0, + "metadata": { + "finish_details": { + "type": "stop", + "stop_tokens": [ + 200002 + ] + }, + "is_complete": true, + "citations": [], + "content_references": [], + "message_type": null, + "model_slug": "gpt-4o", + "default_model_slug": "auto", + "parent_id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", + "request_id": "944c5912c8fdd1c6-KIX", + "timestamp_": "absolute" + }, + "recipient": "all", + "channel": null + }, + "parent": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", + "children": [ + "caa61793-9dbf-44a5-945b-5ca4cd5130d0" + ] + } + }, + "moderation_results": [], + "current_node": "06488d3f-a95f-4906-96d1-f7e9ba1e8662", + "plugin_ids": null, + "conversation_id": "6827f428-78e8-800d-b3bf-eb7ff4288e47", + "conversation_template_id": null, + "gizmo_id": null, + "gizmo_type": null, + "is_archived": false, + "is_starred": null, + "safe_urls": [ + "https://exifinfo.org/" + ], + "blocked_urls": [], + "default_model_slug": "auto", + "conversation_origin": null, + "voice": null, + "async_status": null, + "disabled_tool_ids": [], + "is_do_not_remember": false, + "memory_scope": "global_enabled", + "id": "6827f428-78e8-800d-b3bf-eb7ff4288e47" } ] diff --git a/mcp/chatgpt_converter.html b/mcp/chatgpt_converter.html new file mode 100644 index 0000000..ae98532 --- /dev/null +++ b/mcp/chatgpt_converter.html @@ -0,0 +1,549 @@ + + + + + + 改良版 ChatGPT会話コンバーター + + + +
+
+

🔧 改良版 ChatGPT会話コンバーター

+

画像・検索・特殊メッセージに対応した堅牢な変換ツール

+
+ +
+ 📁 +

ChatGPT会話ファイルをドロップまたはクリックして選択

+

conversations.json ファイルをアップロード

+ +
+ + + + + +
+ + + +
+ +
+
+ + + + diff --git a/mcp/memory_client.py b/mcp/memory_client.py new file mode 100644 index 0000000..366169e --- /dev/null +++ b/mcp/memory_client.py @@ -0,0 +1,212 @@ +# mcp/memory_client.py +""" +Memory client for importing and managing ChatGPT conversations +""" +import sys +import json +import requests +from pathlib import Path +from typing import Dict, Any, List + +class MemoryClient: + """記憶機能のクライアント""" + + def __init__(self, server_url: str = "http://127.0.0.1:5000"): + self.server_url = server_url.rstrip('/') + + def import_chatgpt_file(self, filepath: str) -> Dict[str, Any]: + """ChatGPTのエクスポートファイルをインポート""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + # ファイルが配列の場合(複数の会話) + if isinstance(data, list): + results = [] + for conversation in data: + result = self._import_single_conversation(conversation) + results.append(result) + return { + "success": True, + "imported_count": len([r for r in results if r.get("success")]), + "total_count": len(results), + "results": results + } + else: + # 単一の会話 + return self._import_single_conversation(data) + + except FileNotFoundError: + return {"success": False, "error": f"File not found: {filepath}"} + except json.JSONDecodeError as e: + return {"success": False, "error": f"Invalid JSON: {e}"} + except Exception as e: + return {"success": False, "error": str(e)} + + def _import_single_conversation(self, conversation_data: Dict[str, Any]) -> Dict[str, Any]: + """単一の会話をインポート""" + try: + response = requests.post( + f"{self.server_url}/memory/import/chatgpt", + json={"conversation_data": conversation_data}, + timeout=30 + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"success": False, "error": f"Server error: {e}"} + + def search_memories(self, query: str, limit: int = 10) -> Dict[str, Any]: + """記憶を検索""" + try: + response = requests.post( + f"{self.server_url}/memory/search", + json={"query": query, "limit": limit}, + timeout=30 + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"success": False, "error": f"Server error: {e}"} + + def list_memories(self) -> Dict[str, Any]: + """記憶一覧を取得""" + try: + response = requests.get(f"{self.server_url}/memory/list", timeout=30) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"success": False, "error": f"Server error: {e}"} + + def get_memory_detail(self, filepath: str) -> Dict[str, Any]: + """記憶の詳細を取得""" + try: + response = requests.get( + f"{self.server_url}/memory/detail", + params={"filepath": filepath}, + timeout=30 + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"success": False, "error": f"Server error: {e}"} + + def chat_with_memory(self, message: str, model: str = None) -> Dict[str, Any]: + """記憶を活用してチャット""" + try: + payload = {"message": message} + if model: + payload["model"] = model + + response = requests.post( + f"{self.server_url}/chat", + json=payload, + timeout=30 + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"success": False, "error": f"Server error: {e}"} + +def main(): + """コマンドライン インターフェース""" + if len(sys.argv) < 2: + print("Usage:") + print(" python memory_client.py import ") + print(" python memory_client.py search ") + print(" python memory_client.py list") + print(" python memory_client.py detail ") + print(" python memory_client.py chat ") + sys.exit(1) + + client = MemoryClient() + command = sys.argv[1] + + try: + if command == "import" and len(sys.argv) == 3: + filepath = sys.argv[2] + print(f"🔄 Importing ChatGPT conversations from {filepath}...") + result = client.import_chatgpt_file(filepath) + + if result.get("success"): + if "imported_count" in result: + print(f"✅ Imported {result['imported_count']}/{result['total_count']} conversations") + else: + print("✅ Conversation imported successfully") + print(f"📁 Saved to: {result.get('filepath', 'Unknown')}") + else: + print(f"❌ Import failed: {result.get('error')}") + + elif command == "search" and len(sys.argv) == 3: + query = sys.argv[2] + print(f"🔍 Searching for: {query}") + result = client.search_memories(query) + + if result.get("success"): + memories = result.get("results", []) + print(f"📚 Found {len(memories)} memories:") + for memory in memories: + print(f" • {memory.get('title', 'Untitled')}") + print(f" Summary: {memory.get('summary', 'No summary')}") + print(f" Messages: {memory.get('message_count', 0)}") + print() + else: + print(f"❌ Search failed: {result.get('error')}") + + elif command == "list": + print("📋 Listing all memories...") + result = client.list_memories() + + if result.get("success"): + memories = result.get("memories", []) + print(f"📚 Total memories: {len(memories)}") + for memory in memories: + print(f" • {memory.get('title', 'Untitled')}") + print(f" Source: {memory.get('source', 'Unknown')}") + print(f" Messages: {memory.get('message_count', 0)}") + print(f" Imported: {memory.get('import_time', 'Unknown')}") + print() + else: + print(f"❌ List failed: {result.get('error')}") + + elif command == "detail" and len(sys.argv) == 3: + filepath = sys.argv[2] + print(f"📄 Getting details for: {filepath}") + result = client.get_memory_detail(filepath) + + if result.get("success"): + memory = result.get("memory", {}) + print(f"Title: {memory.get('title', 'Untitled')}") + print(f"Source: {memory.get('source', 'Unknown')}") + print(f"Summary: {memory.get('summary', 'No summary')}") + print(f"Messages: {len(memory.get('messages', []))}") + print() + print("Recent messages:") + for msg in memory.get('messages', [])[:5]: + role = msg.get('role', 'unknown') + content = msg.get('content', '')[:100] + print(f" {role}: {content}...") + else: + print(f"❌ Detail failed: {result.get('error')}") + + elif command == "chat" and len(sys.argv) == 3: + message = sys.argv[2] + print(f"💬 Chatting with memory: {message}") + result = client.chat_with_memory(message) + + if result.get("success"): + print(f"🤖 Response: {result.get('response')}") + print(f"📚 Memories used: {result.get('memories_used', 0)}") + else: + print(f"❌ Chat failed: {result.get('error')}") + + else: + print("❌ Invalid command or arguments") + sys.exit(1) + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/mcp/requirements.txt b/mcp/requirements.txt index 6c93e2d..27486dd 100644 --- a/mcp/requirements.txt +++ b/mcp/requirements.txt @@ -1,3 +1,5 @@ -fastmcp>=0.1.0 -uvicorn>=0.24.0 +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +pydantic>=2.5.0 requests>=2.31.0 +python-multipart>=0.0.6 diff --git a/mcp/server.py b/mcp/server.py index 78eb505..e4c7c0f 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -1,79 +1,294 @@ # mcp/server.py """ -MCP Server for aigpt CLI +Enhanced MCP Server with Memory for aigpt CLI """ -from fastmcp import FastMCP -import platform +import json import os -import sys +from datetime import datetime +from pathlib import Path +from typing import List, Dict, Any, Optional +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +import uvicorn -mcp = FastMCP("AigptMCP") +# データモデル +class ChatMessage(BaseModel): + message: str + model: Optional[str] = None -@mcp.tool() -def process_text(text: str) -> str: - """テキストを処理する""" - return f"Processed: {text}" +class MemoryQuery(BaseModel): + query: str + limit: Optional[int] = 10 -@mcp.tool() -def get_system_info() -> dict: - """システム情報を取得""" +class ConversationImport(BaseModel): + conversation_data: Dict[str, Any] + +# 設定 +BASE_DIR = Path.home() / ".config" / "aigpt" +MEMORY_DIR = BASE_DIR / "memory" +CHATGPT_MEMORY_DIR = MEMORY_DIR / "chatgpt" + +def init_directories(): + """必要なディレクトリを作成""" + BASE_DIR.mkdir(parents=True, exist_ok=True) + MEMORY_DIR.mkdir(parents=True, exist_ok=True) + CHATGPT_MEMORY_DIR.mkdir(parents=True, exist_ok=True) + +class MemoryManager: + """記憶管理クラス""" + + def __init__(self): + init_directories() + + def parse_chatgpt_conversation(self, conversation_data: Dict[str, Any]) -> List[Dict[str, Any]]: + """ChatGPTの会話データを解析してメッセージを抽出""" + messages = [] + mapping = conversation_data.get("mapping", {}) + + # メッセージを時系列順に並べる + message_nodes = [] + for node_id, node in mapping.items(): + message = node.get("message") + if message and message.get("content", {}).get("parts"): + parts = message["content"]["parts"] + if parts and parts[0].strip(): # 空でないメッセージのみ + message_nodes.append({ + "id": node_id, + "create_time": message.get("create_time", 0), + "author_role": message["author"]["role"], + "content": parts[0], + "parent": node.get("parent") + }) + + # 作成時間でソート + message_nodes.sort(key=lambda x: x["create_time"] or 0) + + for msg in message_nodes: + if msg["author_role"] in ["user", "assistant"]: + messages.append({ + "role": msg["author_role"], + "content": msg["content"], + "timestamp": msg["create_time"], + "message_id": msg["id"] + }) + + return messages + + def save_chatgpt_memory(self, conversation_data: Dict[str, Any]) -> str: + """ChatGPTの会話を記憶として保存""" + title = conversation_data.get("title", "untitled") + create_time = conversation_data.get("create_time", datetime.now().timestamp()) + + # メッセージを解析 + messages = self.parse_chatgpt_conversation(conversation_data) + + if not messages: + raise ValueError("No valid messages found in conversation") + + # 保存データを作成 + memory_data = { + "title": title, + "source": "chatgpt", + "import_time": datetime.now().isoformat(), + "original_create_time": create_time, + "messages": messages, + "summary": self.generate_summary(messages) + } + + # ファイル名を生成(タイトルをサニタイズ) + safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip() + timestamp = datetime.fromtimestamp(create_time).strftime("%Y%m%d_%H%M%S") + filename = f"{timestamp}_{safe_title[:50]}.json" + + filepath = CHATGPT_MEMORY_DIR / filename + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(memory_data, f, ensure_ascii=False, indent=2) + + return str(filepath) + + def generate_summary(self, messages: List[Dict[str, Any]]) -> str: + """会話の要約を生成""" + if not messages: + return "Empty conversation" + + # 簡単な要約を生成(実際のAIによる要約は後で実装可能) + user_messages = [msg for msg in messages if msg["role"] == "user"] + assistant_messages = [msg for msg in messages if msg["role"] == "assistant"] + + summary = f"Conversation with {len(user_messages)} user messages and {len(assistant_messages)} assistant responses. " + + if user_messages: + first_user_msg = user_messages[0]["content"][:100] + summary += f"Started with: {first_user_msg}..." + + return summary + + def search_memories(self, query: str, limit: int = 10) -> List[Dict[str, Any]]: + """記憶を検索""" + results = [] + + # ChatGPTの記憶を検索 + for filepath in CHATGPT_MEMORY_DIR.glob("*.json"): + try: + with open(filepath, 'r', encoding='utf-8') as f: + memory_data = json.load(f) + + # 簡単なキーワード検索 + search_text = f"{memory_data.get('title', '')} {memory_data.get('summary', '')}" + for msg in memory_data.get('messages', []): + search_text += f" {msg.get('content', '')}" + + if query.lower() in search_text.lower(): + results.append({ + "filepath": str(filepath), + "title": memory_data.get("title"), + "summary": memory_data.get("summary"), + "source": memory_data.get("source"), + "import_time": memory_data.get("import_time"), + "message_count": len(memory_data.get("messages", [])) + }) + + if len(results) >= limit: + break + + except Exception as e: + print(f"Error reading memory file {filepath}: {e}") + continue + + return results + + def get_memory_detail(self, filepath: str) -> Dict[str, Any]: + """記憶の詳細を取得""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + raise ValueError(f"Error reading memory file: {e}") + + def list_all_memories(self) -> List[Dict[str, Any]]: + """すべての記憶をリスト""" + memories = [] + + for filepath in CHATGPT_MEMORY_DIR.glob("*.json"): + try: + with open(filepath, 'r', encoding='utf-8') as f: + memory_data = json.load(f) + + memories.append({ + "filepath": str(filepath), + "title": memory_data.get("title"), + "summary": memory_data.get("summary"), + "source": memory_data.get("source"), + "import_time": memory_data.get("import_time"), + "message_count": len(memory_data.get("messages", [])) + }) + except Exception as e: + print(f"Error reading memory file {filepath}: {e}") + continue + + # インポート時間でソート + memories.sort(key=lambda x: x.get("import_time", ""), reverse=True) + return memories + +# FastAPI アプリケーション +app = FastAPI(title="AigptMCP Server with Memory", version="1.0.0") +memory_manager = MemoryManager() + +@app.post("/memory/import/chatgpt") +async def import_chatgpt_conversation(data: ConversationImport): + """ChatGPTの会話をインポート""" + try: + filepath = memory_manager.save_chatgpt_memory(data.conversation_data) + return { + "success": True, + "message": "Conversation imported successfully", + "filepath": filepath + } + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@app.post("/memory/search") +async def search_memories(query: MemoryQuery): + """記憶を検索""" + try: + results = memory_manager.search_memories(query.query, query.limit) + return { + "success": True, + "results": results, + "count": len(results) + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/memory/list") +async def list_memories(): + """すべての記憶をリスト""" + try: + memories = memory_manager.list_all_memories() + return { + "success": True, + "memories": memories, + "count": len(memories) + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/memory/detail") +async def get_memory_detail(filepath: str): + """記憶の詳細を取得""" + try: + detail = memory_manager.get_memory_detail(filepath) + return { + "success": True, + "memory": detail + } + except Exception as e: + raise HTTPException(status_code=404, detail=str(e)) + +@app.post("/chat") +async def chat_endpoint(data: ChatMessage): + """チャット機能(記憶を活用)""" + try: + # 関連する記憶を検索 + memories = memory_manager.search_memories(data.message, limit=3) + + # メモリのコンテキストを構築 + memory_context = "" + if memories: + memory_context = "\n# Related memories:\n" + for memory in memories: + memory_context += f"- {memory['title']}: {memory['summary']}\n" + + # 実際のチャット処理(他のプロバイダーに転送) + enhanced_message = data.message + if memory_context: + enhanced_message = f"{data.message}\n\n{memory_context}" + + return { + "success": True, + "response": f"Enhanced response with memory context: {enhanced_message}", + "memories_used": len(memories) + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/") +async def root(): + """ヘルスチェック""" return { - "platform": platform.system(), - "version": platform.version(), - "python_version": sys.version, - "current_dir": os.getcwd() + "service": "AigptMCP Server with Memory", + "status": "running", + "memory_dir": str(MEMORY_DIR), + "endpoints": [ + "/memory/import/chatgpt", + "/memory/search", + "/memory/list", + "/memory/detail", + "/chat" + ] } -@mcp.tool() -def execute_command(command: str) -> dict: - """安全なコマンドを実行する""" - # セキュリティのため、許可されたコマンドのみ実行 - allowed_commands = ["ls", "pwd", "date", "whoami"] - cmd_parts = command.split() - - if not cmd_parts or cmd_parts[0] not in allowed_commands: - return { - "error": f"Command '{command}' is not allowed", - "allowed": allowed_commands - } - - try: - import subprocess - result = subprocess.run( - cmd_parts, - capture_output=True, - text=True, - timeout=10 - ) - return { - "stdout": result.stdout, - "stderr": result.stderr, - "returncode": result.returncode - } - except subprocess.TimeoutExpired: - return {"error": "Command timed out"} - except Exception as e: - return {"error": str(e)} - -@mcp.tool() -def file_operations(operation: str, filepath: str, content: str = None) -> dict: - """ファイル操作を行う""" - try: - if operation == "read": - with open(filepath, 'r', encoding='utf-8') as f: - return {"content": f.read(), "success": True} - elif operation == "write" and content is not None: - with open(filepath, 'w', encoding='utf-8') as f: - f.write(content) - return {"message": f"File written to {filepath}", "success": True} - elif operation == "exists": - return {"exists": os.path.exists(filepath), "success": True} - else: - return {"error": "Invalid operation or missing content", "success": False} - except Exception as e: - return {"error": str(e), "success": False} - if __name__ == "__main__": - print("🚀 AigptMCP Server starting...") - mcp.run() - + print("🚀 AigptMCP Server with Memory starting...") + print(f"📁 Memory directory: {MEMORY_DIR}") + uvicorn.run(app, host="127.0.0.1", port=5000) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..07111e2 --- /dev/null +++ b/readme.md @@ -0,0 +1,130 @@ +Memory-Enhanced MCP Server 使用ガイド +概要 +このMCPサーバーは、ChatGPTの会話履歴を記憶として保存し、AIとの対話で活用できる機能を提供します。 + +セットアップ +1. 依存関係のインストール +bash +pip install -r requirements.txt +2. サーバーの起動 +bash +python mcp/server.py +サーバーは http://localhost:5000 で起動します。 + +使用方法 +1. ChatGPTの会話履歴をインポート +ChatGPTから会話をエクスポートし、JSONファイルとして保存してください。 + +bash +# 単一ファイルをインポート +python mcp/memory_client.py import your_chatgpt_export.json + +# インポート結果の例 +✅ Imported 5/5 conversations +2. 記憶の検索 +bash +# キーワードで記憶を検索 +python mcp/memory_client.py search "プログラミング" + +# 検索結果の例 +🔍 Searching for: プログラミング +📚 Found 3 memories: + • Pythonの基礎学習 + Summary: Conversation with 10 user messages and 8 assistant responses... + Messages: 18 +3. 記憶一覧の表示 +bash +python mcp/memory_client.py list + +# 結果の例 +📋 Listing all memories... +📚 Total memories: 15 + • day + Source: chatgpt + Messages: 2 + Imported: 2025-01-21T10:30:45.123456 +4. 記憶の詳細表示 +bash +python mcp/memory_client.py detail "/path/to/memory/file.json" + +# 結果の例 +📄 Getting details for: /path/to/memory/file.json +Title: day +Source: chatgpt +Summary: Conversation with 1 user messages and 1 assistant responses... +Messages: 2 + +Recent messages: + user: こんにちは... + assistant: こんにちは〜!✨... +5. 記憶を活用したチャット +bash +python mcp/memory_client.py chat "Pythonについて教えて" + +# 結果の例 +💬 Chatting with memory: Pythonについて教えて +🤖 Response: Enhanced response with memory context... +📚 Memories used: 2 +API エンドポイント +POST /memory/import/chatgpt +ChatGPTの会話履歴をインポート + +json +{ + "conversation_data": { ... } +} +POST /memory/search +記憶を検索 + +json +{ + "query": "検索キーワード", + "limit": 10 +} +GET /memory/list +すべての記憶をリスト + +GET /memory/detail?filepath=/path/to/file +記憶の詳細を取得 + +POST /chat +記憶を活用したチャット + +json +{ + "message": "メッセージ", + "model": "model_name" +} +記憶の保存場所 +記憶は以下のディレクトリに保存されます: + +~/.config/aigpt/memory/chatgpt/ +各会話は個別のJSONファイルとして保存され、以下の情報を含みます: + +タイトル +インポート時刻 +メッセージ履歴 +自動生成された要約 +メタデータ +ChatGPTの会話エクスポート方法 +ChatGPTの設定画面を開く +"Data controls" → "Export data" を選択 +エクスポートファイルをダウンロード +conversations.json ファイルを使用 +拡張可能な機能 +高度な検索: ベクトル検索やセマンティック検索の実装 +要約生成: AIによる自動要約の改善 +記憶の分類: カテゴリやタグによる分類 +記憶の統合: 複数の会話からの知識統合 +プライバシー保護: 機密情報の自動検出・マスキング +トラブルシューティング +サーバーが起動しない +ポート5000が使用中でないか確認 +依存関係が正しくインストールされているか確認 +インポートに失敗する +JSONファイルが正しい形式か確認 +ファイルパスが正しいか確認 +ファイルの権限を確認 +検索結果が表示されない +インポートが正常に完了しているか確認 +検索キーワードを変更して試行 diff --git a/src/cli.rs b/src/cli.rs index c56b9df..837743b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name = "aigpt")] -#[command(about = "AI GPT CLI with MCP Server")] +#[command(about = "AI GPT CLI with MCP Server and Memory")] pub struct Args { #[command(subcommand)] pub command: Commands, @@ -20,6 +20,14 @@ pub enum Commands { Chat { /// Message to send message: String, + /// Use memory context + #[arg(long)] + with_memory: bool, + }, + /// Memory management + Memory { + #[command(subcommand)] + command: MemoryCommands, }, } @@ -30,3 +38,27 @@ pub enum ServerCommands { /// Run the MCP server Run, } + +#[derive(Subcommand)] +pub enum MemoryCommands { + /// Import ChatGPT conversation export file + Import { + /// Path to ChatGPT export JSON file + file: String, + }, + /// Search memories + Search { + /// Search query + query: String, + /// Maximum number of results + #[arg(short, long, default_value = "10")] + limit: usize, + }, + /// List all memories + List, + /// Show memory details + Detail { + /// Path to memory file + filepath: String, + }, +} diff --git a/src/main.rs b/src/main.rs index adfbdf3..ca96094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ mod cli; mod config; mod mcp; -use cli::{Args, Commands, ServerCommands}; +use cli::{Args, Commands, ServerCommands, MemoryCommands}; use clap::Parser; #[tokio::main] @@ -21,8 +21,38 @@ async fn main() { } } } - Commands::Chat { message } => { - mcp::server::chat(&message).await; + Commands::Chat { message, with_memory } => { + if with_memory { + if let Err(e) = mcp::memory::handle_chat_with_memory(&message).await { + eprintln!("❌ 記憶チャットエラー: {}", e); + } + } else { + mcp::server::chat(&message).await; + } + } + Commands::Memory { command } => { + match command { + MemoryCommands::Import { file } => { + if let Err(e) = mcp::memory::handle_import(&file).await { + eprintln!("❌ インポートエラー: {}", e); + } + } + MemoryCommands::Search { query, limit } => { + if let Err(e) = mcp::memory::handle_search(&query, limit).await { + eprintln!("❌ 検索エラー: {}", e); + } + } + MemoryCommands::List => { + if let Err(e) = mcp::memory::handle_list().await { + eprintln!("❌ 一覧取得エラー: {}", e); + } + } + MemoryCommands::Detail { filepath } => { + if let Err(e) = mcp::memory::handle_detail(&filepath).await { + eprintln!("❌ 詳細取得エラー: {}", e); + } + } + } } } } diff --git a/src/mcp/memory.rs b/src/mcp/memory.rs new file mode 100644 index 0000000..e3e7df2 --- /dev/null +++ b/src/mcp/memory.rs @@ -0,0 +1,393 @@ +// src/mcp/memory.rs +use reqwest; +use serde::{Deserialize, Serialize}; +use serde_json::{self, Value}; +use std::fs; +use std::path::Path; + +#[derive(Debug, Serialize, Deserialize)] +pub struct MemorySearchRequest { + pub query: String, + pub limit: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChatRequest { + pub message: String, + pub model: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConversationImportRequest { + pub conversation_data: Value, +} + +#[derive(Debug, Deserialize)] +pub struct ApiResponse { + pub success: bool, + pub error: Option, + #[allow(dead_code)] + pub message: Option, + pub filepath: Option, + pub results: Option>, + pub memories: Option>, + #[allow(dead_code)] + pub count: Option, + pub memory: Option, + pub response: Option, + pub memories_used: Option, + pub imported_count: Option, + pub total_count: Option, +} + +#[derive(Debug, Deserialize)] +pub struct MemoryResult { + #[allow(dead_code)] + pub filepath: String, + pub title: Option, + pub summary: Option, + pub source: Option, + pub import_time: Option, + pub message_count: Option, +} + +pub struct MemoryClient { + base_url: String, + client: reqwest::Client, +} + +impl MemoryClient { + pub fn new(base_url: Option) -> Self { + let url = base_url.unwrap_or_else(|| "http://127.0.0.1:5000".to_string()); + Self { + base_url: url, + client: reqwest::Client::new(), + } + } + + pub async fn import_chatgpt_file(&self, filepath: &str) -> Result> { + // ファイルを読み込み + let content = fs::read_to_string(filepath)?; + let json_data: Value = serde_json::from_str(&content)?; + + // 配列かどうかチェック + match json_data.as_array() { + Some(conversations) => { + // 複数の会話をインポート + let mut imported_count = 0; + let total_count = conversations.len(); + + for conversation in conversations { + match self.import_single_conversation(conversation.clone()).await { + Ok(response) => { + if response.success { + imported_count += 1; + } + } + Err(e) => { + eprintln!("❌ インポートエラー: {}", e); + } + } + } + + Ok(ApiResponse { + success: true, + imported_count: Some(imported_count), + total_count: Some(total_count), + error: None, + message: Some(format!("{}個中{}個の会話をインポートしました", total_count, imported_count)), + filepath: None, + results: None, + memories: None, + count: None, + memory: None, + response: None, + memories_used: None, + }) + } + None => { + // 単一の会話をインポート + self.import_single_conversation(json_data).await + } + } + } + + async fn import_single_conversation(&self, conversation_data: Value) -> Result> { + let request = ConversationImportRequest { conversation_data }; + + let response = self.client + .post(&format!("{}/memory/import/chatgpt", self.base_url)) + .json(&request) + .send() + .await?; + + let result: ApiResponse = response.json().await?; + Ok(result) + } + + pub async fn search_memories(&self, query: &str, limit: usize) -> Result> { + let request = MemorySearchRequest { + query: query.to_string(), + limit, + }; + + let response = self.client + .post(&format!("{}/memory/search", self.base_url)) + .json(&request) + .send() + .await?; + + let result: ApiResponse = response.json().await?; + Ok(result) + } + + pub async fn list_memories(&self) -> Result> { + let response = self.client + .get(&format!("{}/memory/list", self.base_url)) + .send() + .await?; + + let result: ApiResponse = response.json().await?; + Ok(result) + } + + pub async fn get_memory_detail(&self, filepath: &str) -> Result> { + let response = self.client + .get(&format!("{}/memory/detail", self.base_url)) + .query(&[("filepath", filepath)]) + .send() + .await?; + + let result: ApiResponse = response.json().await?; + Ok(result) + } + + pub async fn chat_with_memory(&self, message: &str) -> Result> { + let request = ChatRequest { + message: message.to_string(), + model: None, + }; + + let response = self.client + .post(&format!("{}/chat", self.base_url)) + .json(&request) + .send() + .await?; + + let result: ApiResponse = response.json().await?; + Ok(result) + } + + pub async fn is_server_running(&self) -> bool { + match self.client.get(&self.base_url).send().await { + Ok(response) => response.status().is_success(), + Err(_) => false, + } + } +} + +pub async fn handle_import(filepath: &str) -> Result<(), Box> { + if !Path::new(filepath).exists() { + eprintln!("❌ ファイルが見つかりません: {}", filepath); + return Ok(()); + } + + let client = MemoryClient::new(None); + + // サーバーが起動しているかチェック + if !client.is_server_running().await { + eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); + return Ok(()); + } + + println!("🔄 ChatGPT会話をインポートしています: {}", filepath); + + match client.import_chatgpt_file(filepath).await { + Ok(response) => { + if response.success { + if let (Some(imported), Some(total)) = (response.imported_count, response.total_count) { + println!("✅ {}個中{}個の会話をインポートしました", total, imported); + } else { + println!("✅ 会話をインポートしました"); + if let Some(path) = response.filepath { + println!("📁 保存先: {}", path); + } + } + } else { + eprintln!("❌ インポートに失敗: {:?}", response.error); + } + } + Err(e) => { + eprintln!("❌ インポートエラー: {}", e); + } + } + + Ok(()) +} + +pub async fn handle_search(query: &str, limit: usize) -> Result<(), Box> { + let client = MemoryClient::new(None); + + if !client.is_server_running().await { + eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); + return Ok(()); + } + + println!("🔍 記憶を検索しています: {}", query); + + match client.search_memories(query, limit).await { + Ok(response) => { + if response.success { + if let Some(results) = response.results { + println!("📚 {}個の記憶が見つかりました:", results.len()); + for memory in results { + println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string())); + if let Some(summary) = memory.summary { + println!(" 概要: {}", summary); + } + if let Some(count) = memory.message_count { + println!(" メッセージ数: {}", count); + } + println!(); + } + } else { + println!("📚 記憶が見つかりませんでした"); + } + } else { + eprintln!("❌ 検索に失敗: {:?}", response.error); + } + } + Err(e) => { + eprintln!("❌ 検索エラー: {}", e); + } + } + + Ok(()) +} + +pub async fn handle_list() -> Result<(), Box> { + let client = MemoryClient::new(None); + + if !client.is_server_running().await { + eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); + return Ok(()); + } + + println!("📋 記憶一覧を取得しています..."); + + match client.list_memories().await { + Ok(response) => { + if response.success { + if let Some(memories) = response.memories { + println!("📚 総記憶数: {}", memories.len()); + for memory in memories { + println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string())); + if let Some(source) = memory.source { + println!(" ソース: {}", source); + } + if let Some(count) = memory.message_count { + println!(" メッセージ数: {}", count); + } + if let Some(import_time) = memory.import_time { + println!(" インポート時刻: {}", import_time); + } + println!(); + } + } else { + println!("📚 記憶がありません"); + } + } else { + eprintln!("❌ 一覧取得に失敗: {:?}", response.error); + } + } + Err(e) => { + eprintln!("❌ 一覧取得エラー: {}", e); + } + } + + Ok(()) +} + +pub async fn handle_detail(filepath: &str) -> Result<(), Box> { + let client = MemoryClient::new(None); + + if !client.is_server_running().await { + eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); + return Ok(()); + } + + println!("📄 記憶の詳細を取得しています: {}", filepath); + + match client.get_memory_detail(filepath).await { + Ok(response) => { + if response.success { + if let Some(memory) = response.memory { + if let Some(title) = memory.get("title").and_then(|v| v.as_str()) { + println!("タイトル: {}", title); + } + if let Some(source) = memory.get("source").and_then(|v| v.as_str()) { + println!("ソース: {}", source); + } + if let Some(summary) = memory.get("summary").and_then(|v| v.as_str()) { + println!("概要: {}", summary); + } + if let Some(messages) = memory.get("messages").and_then(|v| v.as_array()) { + println!("メッセージ数: {}", messages.len()); + println!("\n最近のメッセージ:"); + for msg in messages.iter().take(5) { + if let (Some(role), Some(content)) = ( + msg.get("role").and_then(|v| v.as_str()), + msg.get("content").and_then(|v| v.as_str()) + ) { + let content_preview = if content.len() > 100 { + format!("{}...", &content[..100]) + } else { + content.to_string() + }; + println!(" {}: {}", role, content_preview); + } + } + } + } + } else { + eprintln!("❌ 詳細取得に失敗: {:?}", response.error); + } + } + Err(e) => { + eprintln!("❌ 詳細取得エラー: {}", e); + } + } + + Ok(()) +} + +pub async fn handle_chat_with_memory(message: &str) -> Result<(), Box> { + let client = MemoryClient::new(None); + + if !client.is_server_running().await { + eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); + return Ok(()); + } + + println!("💬 記憶を活用してチャットしています..."); + + match client.chat_with_memory(message).await { + Ok(response) => { + if response.success { + if let Some(reply) = response.response { + println!("🤖 {}", reply); + } + if let Some(memories_used) = response.memories_used { + println!("📚 使用した記憶数: {}", memories_used); + } + } else { + eprintln!("❌ チャットに失敗: {:?}", response.error); + } + } + Err(e) => { + eprintln!("❌ チャットエラー: {}", e); + } + } + + Ok(()) +} diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 078d630..e023caf 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -1,2 +1,3 @@ // src/mcp/mod.rs pub mod server; +pub mod memory;