This commit is contained in:
2025-05-25 19:39:11 +09:00
parent cd25af7bf0
commit 979e55cfce
10 changed files with 1775 additions and 75 deletions

View File

@ -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"
}
]

549
mcp/chatgpt_converter.html Normal file
View File

@ -0,0 +1,549 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>改良版 ChatGPT会話コンバーター</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header h1 {
color: #2c3e50;
margin: 0;
font-size: 2.5em;
font-weight: 300;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
}
.upload-area {
border: 3px dashed #3498db;
border-radius: 15px;
padding: 40px 20px;
text-align: center;
background: linear-gradient(45deg, #f8f9ff, #e8f4f8);
margin-bottom: 30px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #2980b9;
background: linear-gradient(45deg, #f0f8ff, #e0f0f8);
transform: translateY(-2px);
}
.upload-area.dragover {
border-color: #27ae60;
background: linear-gradient(45deg, #f0fff0, #e0f8e0);
}
.upload-icon {
font-size: 4em;
color: #3498db;
margin-bottom: 20px;
display: block;
}
input[type="file"] {
display: none;
}
.btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
margin: 10px;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
}
.btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 30px 0;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.stat-value {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
.progress-bar {
width: 100%;
height: 8px;
background: #ecf0f1;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3498db, #27ae60);
transition: width 0.3s ease;
width: 0%;
}
.log {
background: #2c3e50;
color: #ecf0f1;
padding: 20px;
border-radius: 10px;
height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
margin-top: 20px;
white-space: pre-wrap;
}
.error {
color: #e74c3c;
}
.success {
color: #27ae60;
}
.warning {
color: #f39c12;
}
.info {
color: #3498db;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.processing {
animation: pulse 2s infinite;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 改良版 ChatGPT会話コンバーター</h1>
<p>画像・検索・特殊メッセージに対応した堅牢な変換ツール</p>
</div>
<div class="upload-area" onclick="document.getElementById('file-input').click()">
<span class="upload-icon">📁</span>
<h3>ChatGPT会話ファイルをドロップまたはクリックして選択</h3>
<p>conversations.json ファイルをアップロード</p>
<input type="file" id="file-input" accept=".json" />
</div>
<div class="stats" id="stats" style="display: none;">
<div class="stat-card">
<div class="stat-value" id="total-conversations">0</div>
<div class="stat-label">総会話数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="processed-conversations">0</div>
<div class="stat-label">処理済み会話</div>
</div>
<div class="stat-card">
<div class="stat-value" id="success-conversations">0</div>
<div class="stat-label">変換成功</div>
</div>
<div class="stat-card">
<div class="stat-value" id="failed-conversations">0</div>
<div class="stat-label">変換失敗</div>
</div>
</div>
<div class="progress-bar" id="progress-container" style="display: none;">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div style="text-align: center;">
<button class="btn" id="convert-btn" disabled>🔄 変換開始</button>
<button class="btn" id="download-btn" disabled style="background: linear-gradient(135deg, #27ae60, #2ecc71);">📥 結果をダウンロード</button>
<button class="btn" id="clear-btn" style="background: linear-gradient(135deg, #e74c3c, #c0392b);">🗑️ クリア</button>
</div>
<div class="log" id="log"></div>
</div>
<script>
let originalData = null;
let convertedResults = [];
// DOM要素
const fileInput = document.getElementById('file-input');
const uploadArea = document.querySelector('.upload-area');
const convertBtn = document.getElementById('convert-btn');
const downloadBtn = document.getElementById('download-btn');
const clearBtn = document.getElementById('clear-btn');
const logElement = document.getElementById('log');
const statsElement = document.getElementById('stats');
const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill');
// 統計要素
const totalConversationsEl = document.getElementById('total-conversations');
const processedConversationsEl = document.getElementById('processed-conversations');
const successConversationsEl = document.getElementById('success-conversations');
const failedConversationsEl = document.getElementById('failed-conversations');
// ドラッグ&ドロップ
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
// ファイル選択
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
// ファイル処理
function handleFile(file) {
if (!file.name.endsWith('.json')) {
log('❌ JSONファイルを選択してください', 'error');
return;
}
log(`📁 ファイルを読み込み中: ${file.name}`, 'info');
const reader = new FileReader();
reader.onload = (e) => {
try {
originalData = JSON.parse(e.target.result);
log(`✅ ファイル読み込み完了 (${(file.size / 1024 / 1024).toFixed(2)}MB)`, 'success');
// 統計表示
const totalCount = Array.isArray(originalData) ? originalData.length : 1;
totalConversationsEl.textContent = totalCount;
statsElement.style.display = 'grid';
convertBtn.disabled = false;
log('🔄 変換準備完了。「変換開始」ボタンをクリックしてください', 'info');
} catch (error) {
log(`❌ JSONファイルの解析に失敗: ${error.message}`, 'error');
}
};
reader.readAsText(file);
}
// ログ出力
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const className = type;
logElement.innerHTML += `<span class="${className}">[${timestamp}] ${message}</span>\n`;
logElement.scrollTop = logElement.scrollHeight;
}
// メッセージの内容を安全に取得
function extractMessageContent(message) {
if (!message || !message.content) return '';
const content = message.content;
// テキストコンテンツの場合
if (content.content_type === 'text' && content.parts) {
return content.parts
.filter(part => part && typeof part === 'string' && part.trim())
.join('\n')
.trim();
}
// マルチモーダル(画像付き)コンテンツの場合
if (content.content_type === 'multimodal_text' && content.parts) {
const textParts = [];
for (const part of content.parts) {
if (typeof part === 'string' && part.trim()) {
textParts.push(part);
} else if (part && typeof part === 'object') {
// 画像や他のメディアの場合
if (part.image_url) {
textParts.push('[画像が添付されています]');
} else if (part.type === 'text' && part.text) {
textParts.push(part.text);
}
}
}
return textParts.join('\n').trim();
}
// ユーザープロファイル情報の場合
if (content.content_type === 'user_editable_context') {
return '[システム設定情報]';
}
// その他の特殊コンテンツ
if (content.content_type && content.content_type !== 'text') {
return `[${content.content_type}]`;
}
return '';
}
// 会話の線形化(親子関係を辿って順序付け)
function linearizeConversation(mapping) {
const messages = [];
const visited = new Set();
// ルートノードを見つける
const rootNode = Object.values(mapping).find(node => node.parent === null);
if (!rootNode) {
return messages;
}
// 深度優先探索で会話を辿る
function traverse(nodeId) {
if (visited.has(nodeId) || !mapping[nodeId]) {
return;
}
visited.add(nodeId);
const node = mapping[nodeId];
// メッセージが存在し、有効なコンテンツがある場合のみ追加
if (node.message) {
const message = node.message;
const content = extractMessageContent(message);
// 以下の条件で有効なメッセージとして扱う
const isValid = content &&
content.length > 0 &&
content !== '[システム設定情報]' &&
(!message.metadata?.is_visually_hidden_from_conversation ||
(message.author?.role === 'user' || message.author?.role === 'assistant'));
if (isValid) {
messages.push({
role: message.author?.role || 'unknown',
content: content,
timestamp: message.create_time || message.update_time || Date.now() / 1000
});
}
}
// 子ードを処理通常は1つだが、分岐がある場合もある
if (node.children && node.children.length > 0) {
// 最初の子ノードのみを辿る(最も新しい応答を優先)
traverse(node.children[0]);
}
}
traverse(rootNode.id);
return messages;
}
// 単一会話の変換
function convertSingleConversation(conversation, index) {
try {
if (!conversation.mapping) {
throw new Error('mapping が見つかりません');
}
// 会話を線形化
const messages = linearizeConversation(conversation.mapping);
if (messages.length === 0) {
throw new Error('有効なメッセージが見つかりません');
}
// 結果の構築
const result = {
title: conversation.title || `会話 ${index + 1}`,
create_time: conversation.create_time || Date.now() / 1000,
update_time: conversation.update_time || Date.now() / 1000,
conversation_id: conversation.conversation_id || conversation.id || `conv_${index}`,
messages: messages,
metadata: {
original_message_count: Object.keys(conversation.mapping).length,
processed_message_count: messages.length,
is_archived: conversation.is_archived || false,
model_slug: conversation.default_model_slug || 'unknown'
}
};
return { success: true, result, error: null };
} catch (error) {
return {
success: false,
result: null,
error: error.message,
conversation_title: conversation.title || `会話 ${index + 1}`
};
}
}
// 変換処理
convertBtn.addEventListener('click', async () => {
if (!originalData) return;
convertBtn.disabled = true;
downloadBtn.disabled = true;
convertedResults = [];
const conversations = Array.isArray(originalData) ? originalData : [originalData];
const total = conversations.length;
let processed = 0;
let success = 0;
let failed = 0;
log(`🔄 ${total}個の会話の変換を開始します...`, 'info');
progressContainer.style.display = 'block';
for (let i = 0; i < conversations.length; i++) {
const conversation = conversations[i];
log(`[${i + 1}/${total}] "${conversation.title || `会話${i + 1}`}" を処理中...`, 'info');
const result = convertSingleConversation(conversation, i);
if (result.success) {
convertedResults.push(result.result);
success++;
log(`✅ [${i + 1}/${total}] 変換成功: ${result.result.messages.length}メッセージ`, 'success');
} else {
failed++;
log(`❌ [${i + 1}/${total}] 変換失敗: ${result.error}`, 'error');
}
processed++;
// 統計更新
processedConversationsEl.textContent = processed;
successConversationsEl.textContent = success;
failedConversationsEl.textContent = failed;
// プログレスバー更新
const progress = (processed / total) * 100;
progressFill.style.width = `${progress}%`;
// UIを更新するため少し待機
await new Promise(resolve => setTimeout(resolve, 1));
}
log(`🎉 変換完了! 成功: ${success}個, 失敗: ${failed}`, success > 0 ? 'success' : 'warning');
if (success > 0) {
downloadBtn.disabled = false;
}
convertBtn.disabled = false;
});
// ダウンロード
downloadBtn.addEventListener('click', () => {
if (convertedResults.length === 0) return;
const blob = new Blob([JSON.stringify(convertedResults, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chatgpt_conversations_converted_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
log('📥 変換結果をダウンロードしました', 'success');
});
// クリア
clearBtn.addEventListener('click', () => {
originalData = null;
convertedResults = [];
logElement.innerHTML = '';
statsElement.style.display = 'none';
progressContainer.style.display = 'none';
progressFill.style.width = '0%';
// ボタン状態リセット
convertBtn.disabled = true;
downloadBtn.disabled = true;
// ファイル入力リセット
fileInput.value = '';
log('🗑️ すべてクリアしました', 'info');
});
// 初期メッセージ
log('👋 ChatGPT会話コンバーターへようこそ', 'info');
log('📁 conversations.json ファイルをアップロードしてください', 'info');
</script>
</body>
</html>

212
mcp/memory_client.py Normal file
View File

@ -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 <chatgpt_export.json>")
print(" python memory_client.py search <query>")
print(" python memory_client.py list")
print(" python memory_client.py detail <filepath>")
print(" python memory_client.py chat <message>")
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()

View File

@ -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

View File

@ -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)

130
readme.md Normal file
View File

@ -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ファイルが正しい形式か確認
ファイルパスが正しいか確認
ファイルの権限を確認
検索結果が表示されない
インポートが正常に完了しているか確認
検索キーワードを変更して試行

View File

@ -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,
},
}

View File

@ -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);
}
}
}
}
}
}

393
src/mcp/memory.rs Normal file
View File

@ -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<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConversationImportRequest {
pub conversation_data: Value,
}
#[derive(Debug, Deserialize)]
pub struct ApiResponse {
pub success: bool,
pub error: Option<String>,
#[allow(dead_code)]
pub message: Option<String>,
pub filepath: Option<String>,
pub results: Option<Vec<MemoryResult>>,
pub memories: Option<Vec<MemoryResult>>,
#[allow(dead_code)]
pub count: Option<usize>,
pub memory: Option<Value>,
pub response: Option<String>,
pub memories_used: Option<usize>,
pub imported_count: Option<usize>,
pub total_count: Option<usize>,
}
#[derive(Debug, Deserialize)]
pub struct MemoryResult {
#[allow(dead_code)]
pub filepath: String,
pub title: Option<String>,
pub summary: Option<String>,
pub source: Option<String>,
pub import_time: Option<String>,
pub message_count: Option<usize>,
}
pub struct MemoryClient {
base_url: String,
client: reqwest::Client,
}
impl MemoryClient {
pub fn new(base_url: Option<String>) -> 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<ApiResponse, Box<dyn std::error::Error>> {
// ファイルを読み込み
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<ApiResponse, Box<dyn std::error::Error>> {
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<ApiResponse, Box<dyn std::error::Error>> {
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<ApiResponse, Box<dyn std::error::Error>> {
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<ApiResponse, Box<dyn std::error::Error>> {
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<ApiResponse, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}

View File

@ -1,2 +1,3 @@
// src/mcp/mod.rs
pub mod server;
pub mod memory;