fix mcp
This commit is contained in:
138
mcp/chatgpt.json
138
mcp/chatgpt.json
@ -231,7 +231,7 @@
|
|||||||
"0be4b4a5-d52f-4bef-927e-5d6f93a9cb26"
|
"0be4b4a5-d52f-4bef-927e-5d6f93a9cb26"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moderation_results": [],
|
"moderation_results": [],
|
||||||
"current_node": "",
|
"current_node": "",
|
||||||
"plugin_ids": null,
|
"plugin_ids": null,
|
||||||
@ -251,5 +251,141 @@
|
|||||||
"is_do_not_remember": null,
|
"is_do_not_remember": null,
|
||||||
"memory_scope": "global_enabled",
|
"memory_scope": "global_enabled",
|
||||||
"id": ""
|
"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
549
mcp/chatgpt_converter.html
Normal 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
212
mcp/memory_client.py
Normal 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()
|
@ -1,3 +1,5 @@
|
|||||||
fastmcp>=0.1.0
|
fastapi>=0.104.0
|
||||||
uvicorn>=0.24.0
|
uvicorn[standard]>=0.24.0
|
||||||
|
pydantic>=2.5.0
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
|
python-multipart>=0.0.6
|
||||||
|
351
mcp/server.py
351
mcp/server.py
@ -1,79 +1,294 @@
|
|||||||
# mcp/server.py
|
# mcp/server.py
|
||||||
"""
|
"""
|
||||||
MCP Server for aigpt CLI
|
Enhanced MCP Server with Memory for aigpt CLI
|
||||||
"""
|
"""
|
||||||
from fastmcp import FastMCP
|
import json
|
||||||
import platform
|
|
||||||
import os
|
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()
|
class MemoryQuery(BaseModel):
|
||||||
def process_text(text: str) -> str:
|
query: str
|
||||||
"""テキストを処理する"""
|
limit: Optional[int] = 10
|
||||||
return f"Processed: {text}"
|
|
||||||
|
|
||||||
@mcp.tool()
|
class ConversationImport(BaseModel):
|
||||||
def get_system_info() -> dict:
|
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 {
|
return {
|
||||||
"platform": platform.system(),
|
"service": "AigptMCP Server with Memory",
|
||||||
"version": platform.version(),
|
"status": "running",
|
||||||
"python_version": sys.version,
|
"memory_dir": str(MEMORY_DIR),
|
||||||
"current_dir": os.getcwd()
|
"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__":
|
if __name__ == "__main__":
|
||||||
print("🚀 AigptMCP Server starting...")
|
print("🚀 AigptMCP Server with Memory starting...")
|
||||||
mcp.run()
|
print(f"📁 Memory directory: {MEMORY_DIR}")
|
||||||
|
uvicorn.run(app, host="127.0.0.1", port=5000)
|
||||||
|
130
readme.md
Normal file
130
readme.md
Normal 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ファイルが正しい形式か確認
|
||||||
|
ファイルパスが正しいか確認
|
||||||
|
ファイルの権限を確認
|
||||||
|
検索結果が表示されない
|
||||||
|
インポートが正常に完了しているか確認
|
||||||
|
検索キーワードを変更して試行
|
34
src/cli.rs
34
src/cli.rs
@ -3,7 +3,7 @@ use clap::{Parser, Subcommand};
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "aigpt")]
|
#[command(name = "aigpt")]
|
||||||
#[command(about = "AI GPT CLI with MCP Server")]
|
#[command(about = "AI GPT CLI with MCP Server and Memory")]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
@ -20,6 +20,14 @@ pub enum Commands {
|
|||||||
Chat {
|
Chat {
|
||||||
/// Message to send
|
/// Message to send
|
||||||
message: String,
|
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 the MCP server
|
||||||
Run,
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
36
src/main.rs
36
src/main.rs
@ -3,7 +3,7 @@ mod cli;
|
|||||||
mod config;
|
mod config;
|
||||||
mod mcp;
|
mod mcp;
|
||||||
|
|
||||||
use cli::{Args, Commands, ServerCommands};
|
use cli::{Args, Commands, ServerCommands, MemoryCommands};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -21,8 +21,38 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Chat { message } => {
|
Commands::Chat { message, with_memory } => {
|
||||||
mcp::server::chat(&message).await;
|
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
393
src/mcp/memory.rs
Normal 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(())
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
// src/mcp/mod.rs
|
// src/mcp/mod.rs
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
pub mod memory;
|
||||||
|
Reference in New Issue
Block a user