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)