Compare commits
38 Commits
claude
...
62b91e5e5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
62b91e5e5a
|
|||
|
4620d0862a
|
|||
|
93b523b1ba
|
|||
|
45c65e03b3
|
|||
|
73c516ab28
|
|||
|
e2e2758a83
|
|||
|
5564db014a
|
|||
|
6dadc41da7
|
|||
|
64e519d719
|
|||
|
ed6d6e0d47
|
|||
|
582b983a32
|
|||
|
b410c83605
|
|||
|
334e17a53e
|
|||
|
df86fb827e
|
|||
|
5a441e847d
|
|||
|
948bbc24ea
|
|||
|
d4de0d4917
|
|||
|
3487535e08
|
|||
|
1755dc2bec
|
|||
|
42c85fc820
|
|||
|
4a441279fb
|
|||
| e7e57b7b4b | |||
|
6081ed069f
|
|||
| 8c0961ab2f | |||
|
c9005f5240
|
|||
|
cba52b6171
|
|||
|
b642588696
|
|||
|
ebd2582b92
|
|||
|
79d1e1943f
|
|||
|
76d90c7cf7
|
|||
|
06fb70fffa
|
|||
|
62f941a958
|
|||
|
98ca92d85d
|
|||
|
1c555a706b
|
|||
|
7c3b05501f
|
|||
|
a7b61fe07d
|
|||
|
9866da625d
|
|||
|
797ae7ef69
|
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,7 +1,24 @@
|
|||||||
**target
|
# Rust
|
||||||
**.lock
|
target/
|
||||||
output.json
|
Cargo.lock
|
||||||
config/*.db
|
|
||||||
aigpt
|
# Database files
|
||||||
mcp/scripts/__*
|
*.db
|
||||||
data
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
json
|
||||||
|
gpt
|
||||||
|
.claude
|
||||||
|
|||||||
49
Cargo.toml
49
Cargo.toml
@@ -2,12 +2,47 @@
|
|||||||
name = "aigpt"
|
name = "aigpt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["syui"]
|
||||||
|
description = "Simple memory storage for Claude with MCP"
|
||||||
|
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "aigpt"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "memory-mcp"
|
||||||
|
path = "src/bin/mcp_server.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "memory-mcp-extended"
|
||||||
|
path = "src/bin/mcp_server_extended.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "*", features = ["json"] }
|
# CLI and async
|
||||||
serde = { version = "*", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
serde_json = "*"
|
tokio = { version = "1.40", features = ["full"] }
|
||||||
tokio = { version = "*", features = ["full"] }
|
|
||||||
clap = { version = "*", features = ["derive"] }
|
# JSON and serialization
|
||||||
shellexpand = "*"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
fs_extra = "*"
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# Date/time and UUID
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
uuid = { version = "1.10", features = ["v4"] }
|
||||||
|
|
||||||
|
# Error handling and utilities
|
||||||
|
anyhow = "1.0"
|
||||||
|
dirs = "5.0"
|
||||||
|
|
||||||
|
# Extended features (optional)
|
||||||
|
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||||
|
scraper = { version = "0.18", optional = true }
|
||||||
|
openai = { version = "1.1", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
extended = ["semantic-search", "ai-analysis", "web-integration"]
|
||||||
|
semantic-search = ["openai"]
|
||||||
|
ai-analysis = ["openai"]
|
||||||
|
web-integration = ["reqwest", "scraper"]
|
||||||
|
|||||||
177
README.md
Normal file
177
README.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# aigpt - Claude Memory MCP Server
|
||||||
|
|
||||||
|
ChatGPTのメモリ機能を参考にした、Claude Desktop/Code用のシンプルなメモリストレージシステムです。
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
|
||||||
|
- **メモリのCRUD操作**: メモリの作成、更新、削除、検索
|
||||||
|
- **ChatGPT JSONインポート**: ChatGPTの会話履歴からメモリを抽出
|
||||||
|
- **stdio MCP実装**: Claude Desktop/Codeとの簡潔な連携
|
||||||
|
- **JSONファイル保存**: シンプルなファイルベースのデータ保存
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
1. Rustをインストール(まだの場合):
|
||||||
|
```bash
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. プロジェクトをビルド:
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
3. バイナリをパスの通った場所にコピー(オプション):
|
||||||
|
```bash
|
||||||
|
cp target/release/aigpt $HOME/.cargo/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Claude Code/Desktopに追加
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Claude Codeの場合
|
||||||
|
claude mcp add aigpt $HOME/.cargo/bin/aigpt server
|
||||||
|
|
||||||
|
# または
|
||||||
|
claude mcp add aigpt $HOME/.cargo/bin/aigpt serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### ヘルプの表示
|
||||||
|
```bash
|
||||||
|
aigpt --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCPサーバーとして起動
|
||||||
|
```bash
|
||||||
|
# MCPサーバー起動 (どちらでも可)
|
||||||
|
aigpt server
|
||||||
|
aigpt serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### ChatGPT会話のインポート
|
||||||
|
```bash
|
||||||
|
# ChatGPT conversations.jsonをインポート
|
||||||
|
aigpt import path/to/conversations.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Claude Desktop/Codeへの設定
|
||||||
|
|
||||||
|
1. Claude Desktopの設定ファイルを開く:
|
||||||
|
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
||||||
|
|
||||||
|
2. 以下の設定を追加:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"aigpt": {
|
||||||
|
"command": "/Users/syui/.cargo/bin/aigpt",
|
||||||
|
"args": ["server"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 提供するMCPツール一覧
|
||||||
|
|
||||||
|
1. **create_memory** - 新しいメモリを作成
|
||||||
|
2. **update_memory** - 既存のメモリを更新
|
||||||
|
3. **delete_memory** - メモリを削除
|
||||||
|
4. **search_memories** - メモリを検索
|
||||||
|
5. **list_conversations** - インポートされた会話を一覧表示
|
||||||
|
|
||||||
|
## ツールの使用例
|
||||||
|
|
||||||
|
Claude Desktop/Codeで以下のように使用します:
|
||||||
|
|
||||||
|
### メモリの作成
|
||||||
|
```
|
||||||
|
MCPツールを使って「今日は良い天気です」というメモリーを作成してください
|
||||||
|
```
|
||||||
|
|
||||||
|
### メモリの検索
|
||||||
|
```
|
||||||
|
MCPツールを使って「天気」に関するメモリーを検索してください
|
||||||
|
```
|
||||||
|
|
||||||
|
### 会話一覧の表示
|
||||||
|
```
|
||||||
|
MCPツールを使ってインポートした会話の一覧を表示してください
|
||||||
|
```
|
||||||
|
|
||||||
|
## データ保存
|
||||||
|
|
||||||
|
- デフォルトパス: `~/.config/syui/ai/gpt/memory.json`
|
||||||
|
- JSONファイルでデータを保存
|
||||||
|
- 自動的にディレクトリとファイルを作成
|
||||||
|
|
||||||
|
### データ構造
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"memories": {
|
||||||
|
"uuid": {
|
||||||
|
"id": "uuid",
|
||||||
|
"content": "メモリーの内容",
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"conversation_id": {
|
||||||
|
"id": "conversation_id",
|
||||||
|
"title": "会話のタイトル",
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"message_count": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 開発
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 開発モードで実行
|
||||||
|
cargo run -- server
|
||||||
|
|
||||||
|
# ChatGPTインポートのテスト
|
||||||
|
cargo run -- import json/conversations.json
|
||||||
|
|
||||||
|
# テストの実行
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# フォーマット
|
||||||
|
cargo fmt
|
||||||
|
|
||||||
|
# Lintチェック
|
||||||
|
cargo clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### MCPサーバーが起動しない
|
||||||
|
```bash
|
||||||
|
# バイナリが存在するか確認
|
||||||
|
ls -la ~/.cargo/bin/aigpt
|
||||||
|
|
||||||
|
# 手動でテスト
|
||||||
|
echo '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' | aigpt server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Desktopでツールが見つからない
|
||||||
|
1. Claude Desktopを完全に再起動
|
||||||
|
2. 設定ファイルのパスが正しいか確認
|
||||||
|
3. ログファイルを確認: `~/Library/Logs/Claude/mcp-server-aigpt.log`
|
||||||
|
|
||||||
|
### インポートが失敗する
|
||||||
|
```bash
|
||||||
|
# JSONファイルの形式を確認
|
||||||
|
head -100 conversations.json | jq '.[0] | keys'
|
||||||
|
```
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
MIT
|
||||||
95
claude.md
Normal file
95
claude.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# claude用の記憶装置を作る
|
||||||
|
|
||||||
|
claude desktop, claude codeで使用できるmemory機能をmcpで作ります。
|
||||||
|
|
||||||
|
1. chatgptのメモリ機能を参考に
|
||||||
|
2. chatgptのjsonをimportできる @json/
|
||||||
|
3. rustで作る
|
||||||
|
|
||||||
|
## 自動実行設定
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp_auto_execute": true,
|
||||||
|
"memory_auto_save": true,
|
||||||
|
"memory_auto_search": true,
|
||||||
|
"trigger_sensitivity": "high"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 自動メモリー保存のルール(強化版)
|
||||||
|
|
||||||
|
**即座に自動実行** - 以下の情報が会話に現れた瞬間、create_memory MCPツールを自動実行:
|
||||||
|
|
||||||
|
### 1. **ユーザーの個人情報**
|
||||||
|
**トリガーワード**: 名前、誕生日、住所、年齢、職業、家族、出身
|
||||||
|
- 基本情報: 名前、誕生日、住所、年齢、性別
|
||||||
|
- 関係性: 家族構成、恋人、友人関係
|
||||||
|
- 好み: 好きなもの、嫌いなもの、趣味、音楽、映画、本
|
||||||
|
- 習慣: 日課、スケジュール、ルーティン
|
||||||
|
- 価値観: 信念、考え方、人生観
|
||||||
|
|
||||||
|
### 2. **重要な決定事項**
|
||||||
|
**トリガーワード**: 決めた、決定、方針、計画、予定、目標
|
||||||
|
- プロジェクト方針の決定
|
||||||
|
- 技術選択の決定
|
||||||
|
- 設定・環境の変更
|
||||||
|
- 今後のロードマップ
|
||||||
|
- 作業分担・役割
|
||||||
|
|
||||||
|
### 3. **技術的な解決策**
|
||||||
|
**トリガーワード**: 解決、修正、対処、設定、インストール、手順
|
||||||
|
- エラーの解決方法
|
||||||
|
- 有用なコマンド・スクリプト
|
||||||
|
- 設定手順・インストール方法
|
||||||
|
- デバッグテクニック
|
||||||
|
- 最適化手法
|
||||||
|
|
||||||
|
### 4. **学習・発見事項**
|
||||||
|
**トリガーワード**: 学んだ、わかった、発見、理解、気づき
|
||||||
|
- 新しい知識・概念の理解
|
||||||
|
- ツール・ライブラリの使い方
|
||||||
|
- ベストプラクティス
|
||||||
|
- 失敗から得た教訓
|
||||||
|
|
||||||
|
## 自動メモリー検索のルール(強化版)
|
||||||
|
|
||||||
|
**会話開始時に自動実行** - search_memories を実行してコンテキストを取得
|
||||||
|
|
||||||
|
**即座に自動実行** - 以下の場合、search_memories MCPツールを自動実行:
|
||||||
|
|
||||||
|
### 1. **過去参照キーワード検出**
|
||||||
|
**トリガーワード**: 前に、以前、昔、過去、先ほど、さっき、この間
|
||||||
|
- 「前に話した〜」
|
||||||
|
- 「以前設定した〜」
|
||||||
|
- 「昔やった〜」
|
||||||
|
|
||||||
|
### 2. **記憶呼び出しキーワード**
|
||||||
|
**トリガーワード**: 覚えている、記録、メモ、保存、履歴
|
||||||
|
- 「覚えていますか?」
|
||||||
|
- 「記録していた〜」
|
||||||
|
- 「メモした〜」
|
||||||
|
|
||||||
|
### 3. **設定・好み確認**
|
||||||
|
**トリガーワード**: 好み、設定、環境、構成、preferences
|
||||||
|
- ユーザーの好みを確認する必要がある場合
|
||||||
|
- 過去の設定を参照する必要がある場合
|
||||||
|
- 環境構成を確認する必要がある場合
|
||||||
|
|
||||||
|
### 4. **不明な参照**
|
||||||
|
- ユーザーが具体的でない参照をした場合
|
||||||
|
- 「あれ」「それ」「例のやつ」などの曖昧な表現
|
||||||
|
- 文脈から過去の情報が必要と判断される場合
|
||||||
|
|
||||||
|
## 自動実行タイミング
|
||||||
|
|
||||||
|
1. **会話開始時**: search_memories を実行してコンテキスト取得
|
||||||
|
2. **リアルタイム**: トリガーワード検出後、即座にMCPツール実行
|
||||||
|
3. **会話終了時**: 重要な情報があれば create_memory で保存
|
||||||
|
4. **定期的**: 長い会話では中間地点でメモリー整理
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
- MCPツールが利用できない場合は通常の会話を継続
|
||||||
|
- メモリー保存失敗時はユーザーに通知
|
||||||
|
- 検索結果が空の場合も適切に対応
|
||||||
|
|
||||||
125
docs/README_CONFIG.md
Normal file
125
docs/README_CONFIG.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Claude Memory MCP 設定ガイド
|
||||||
|
|
||||||
|
## モード選択
|
||||||
|
|
||||||
|
### 標準モード (Simple Mode)
|
||||||
|
- 基本的なメモリー機能のみ
|
||||||
|
- 軽量で高速
|
||||||
|
- 最小限の依存関係
|
||||||
|
|
||||||
|
### 拡張モード (Extended Mode)
|
||||||
|
- AI分析機能
|
||||||
|
- セマンティック検索
|
||||||
|
- Web統合機能
|
||||||
|
- 高度なインサイト抽出
|
||||||
|
|
||||||
|
## ビルド・実行方法
|
||||||
|
|
||||||
|
### 標準モード
|
||||||
|
```bash
|
||||||
|
# MCPサーバー起動
|
||||||
|
cargo run --bin memory-mcp
|
||||||
|
|
||||||
|
# CLI実行
|
||||||
|
cargo run --bin aigpt -- create "メモリー内容"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 拡張モード
|
||||||
|
```bash
|
||||||
|
# MCPサーバー起動
|
||||||
|
cargo run --bin memory-mcp-extended --features extended
|
||||||
|
|
||||||
|
# CLI実行
|
||||||
|
cargo run --bin aigpt-extended --features extended -- create "メモリー内容" --analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定ファイルの配置
|
||||||
|
|
||||||
|
### 標準モード
|
||||||
|
|
||||||
|
#### Claude Desktop
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
cp claude_desktop_config.json ~/.config/claude-desktop/claude_desktop_config.json
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
cp claude_desktop_config.json %APPDATA%\Claude\claude_desktop_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Claude Code
|
||||||
|
```bash
|
||||||
|
# プロジェクトルートまたはグローバル設定
|
||||||
|
cp claude_code_config.json .claude/config.json
|
||||||
|
# または
|
||||||
|
cp claude_code_config.json ~/.claude/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 拡張モード
|
||||||
|
|
||||||
|
#### Claude Desktop
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
cp claude_desktop_config_extended.json ~/.config/claude-desktop/claude_desktop_config.json
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
cp claude_desktop_config_extended.json %APPDATA%\Claude\claude_desktop_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Claude Code
|
||||||
|
```bash
|
||||||
|
# プロジェクトルートまたはグローバル設定
|
||||||
|
cp claude_code_config_extended.json .claude/config.json
|
||||||
|
# または
|
||||||
|
cp claude_code_config_extended.json ~/.claude/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 環境変数設定
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MEMORY_AUTO_EXECUTE=true
|
||||||
|
export MEMORY_AUTO_SAVE=true
|
||||||
|
export MEMORY_AUTO_SEARCH=true
|
||||||
|
export TRIGGER_SENSITIVITY=high
|
||||||
|
export MEMORY_DB_PATH=~/.claude/memory.db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定オプション
|
||||||
|
|
||||||
|
### auto_execute
|
||||||
|
- `true`: 自動でMCPツールを実行
|
||||||
|
- `false`: 手動実行のみ
|
||||||
|
|
||||||
|
### trigger_sensitivity
|
||||||
|
- `high`: 多くのキーワードで反応
|
||||||
|
- `medium`: 適度な反応
|
||||||
|
- `low`: 明確なキーワードのみ
|
||||||
|
|
||||||
|
### max_memories
|
||||||
|
メモリーの最大保存数
|
||||||
|
|
||||||
|
### search_limit
|
||||||
|
検索結果の最大表示数
|
||||||
|
|
||||||
|
## カスタマイズ
|
||||||
|
|
||||||
|
`trigger_words`セクションでトリガーワードをカスタマイズ可能:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"trigger_words": {
|
||||||
|
"custom_category": ["カスタム", "キーワード", "リスト"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
1. MCPサーバーが起動しない場合:
|
||||||
|
- Rustがインストールされているか確認
|
||||||
|
- `cargo build --release`でビルド確認
|
||||||
|
|
||||||
|
2. 自動実行されない場合:
|
||||||
|
- 環境変数が正しく設定されているか確認
|
||||||
|
- トリガーワードが含まれているか確認
|
||||||
|
|
||||||
|
3. メモリーが保存されない場合:
|
||||||
|
- データベースファイルのパスが正しいか確認
|
||||||
|
- 書き込み権限があるか確認
|
||||||
58
docs/claude_code_config.json
Normal file
58
docs/claude_code_config.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"memory": {
|
||||||
|
"command": "cargo",
|
||||||
|
"args": ["run", "--release", "--bin", "memory-mcp"],
|
||||||
|
"cwd": "/Users/syui/ai/ai/gpt",
|
||||||
|
"env": {
|
||||||
|
"MEMORY_AUTO_EXECUTE": "true",
|
||||||
|
"MEMORY_AUTO_SAVE": "true",
|
||||||
|
"MEMORY_AUTO_SEARCH": "true",
|
||||||
|
"TRIGGER_SENSITIVITY": "high",
|
||||||
|
"MEMORY_DB_PATH": "~/.claude/memory.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"memory": {
|
||||||
|
"enabled": true,
|
||||||
|
"auto_execute": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"memory_integration": true,
|
||||||
|
"auto_save_on_file_change": true,
|
||||||
|
"auto_search_on_context_switch": true
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"auto_execute": true,
|
||||||
|
"auto_save": true,
|
||||||
|
"auto_search": true,
|
||||||
|
"trigger_sensitivity": "high",
|
||||||
|
"max_memories": 10000,
|
||||||
|
"search_limit": 50,
|
||||||
|
"session_memory": true,
|
||||||
|
"cross_session_memory": true,
|
||||||
|
"trigger_words": {
|
||||||
|
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
|
||||||
|
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
|
||||||
|
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
|
||||||
|
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
|
||||||
|
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
|
||||||
|
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
|
||||||
|
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
|
||||||
|
"vague_reference": ["あれ", "それ", "例のやつ"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"on_conversation_start": [
|
||||||
|
"search_memories --limit 10 --recent"
|
||||||
|
],
|
||||||
|
"on_trigger_word": [
|
||||||
|
"auto_execute_memory_tools"
|
||||||
|
],
|
||||||
|
"on_conversation_end": [
|
||||||
|
"save_important_memories"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
81
docs/claude_code_config_extended.json
Normal file
81
docs/claude_code_config_extended.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"memory-extended": {
|
||||||
|
"command": "cargo",
|
||||||
|
"args": ["run", "--bin", "memory-mcp-extended", "--features", "extended"],
|
||||||
|
"cwd": "/Users/syui/ai/ai/gpt",
|
||||||
|
"env": {
|
||||||
|
"MEMORY_AUTO_EXECUTE": "true",
|
||||||
|
"MEMORY_AUTO_SAVE": "true",
|
||||||
|
"MEMORY_AUTO_SEARCH": "true",
|
||||||
|
"TRIGGER_SENSITIVITY": "high",
|
||||||
|
"MEMORY_DB_PATH": "~/.claude/memory.db",
|
||||||
|
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"memory": {
|
||||||
|
"enabled": true,
|
||||||
|
"auto_execute": true,
|
||||||
|
"mode": "extended"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"memory_integration": true,
|
||||||
|
"auto_save_on_file_change": true,
|
||||||
|
"auto_search_on_context_switch": true,
|
||||||
|
"ai_analysis_on_code_review": true,
|
||||||
|
"web_integration_for_docs": true
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"mode": "extended",
|
||||||
|
"auto_execute": true,
|
||||||
|
"auto_save": true,
|
||||||
|
"auto_search": true,
|
||||||
|
"trigger_sensitivity": "high",
|
||||||
|
"max_memories": 10000,
|
||||||
|
"search_limit": 50,
|
||||||
|
"session_memory": true,
|
||||||
|
"cross_session_memory": true,
|
||||||
|
"features": {
|
||||||
|
"ai_analysis": true,
|
||||||
|
"semantic_search": true,
|
||||||
|
"web_integration": true,
|
||||||
|
"sentiment_analysis": true,
|
||||||
|
"pattern_recognition": true,
|
||||||
|
"code_analysis": true,
|
||||||
|
"documentation_import": true
|
||||||
|
},
|
||||||
|
"trigger_words": {
|
||||||
|
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
|
||||||
|
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
|
||||||
|
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
|
||||||
|
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
|
||||||
|
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
|
||||||
|
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
|
||||||
|
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
|
||||||
|
"vague_reference": ["あれ", "それ", "例のやつ"],
|
||||||
|
"web_content": ["URL", "リンク", "サイト", "ページ", "記事", "ドキュメント"],
|
||||||
|
"analysis_request": ["分析", "パターン", "傾向", "インサイト", "統計", "レビュー"],
|
||||||
|
"code_related": ["関数", "クラス", "メソッド", "変数", "バグ", "リファクタリング"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"on_conversation_start": [
|
||||||
|
"search_memories --limit 10 --recent --semantic"
|
||||||
|
],
|
||||||
|
"on_trigger_word": [
|
||||||
|
"auto_execute_memory_tools --with-analysis"
|
||||||
|
],
|
||||||
|
"on_conversation_end": [
|
||||||
|
"save_important_memories --with-insights"
|
||||||
|
],
|
||||||
|
"on_code_change": [
|
||||||
|
"analyze_code_patterns --auto-save"
|
||||||
|
],
|
||||||
|
"on_web_reference": [
|
||||||
|
"import_webpage --auto-categorize"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
34
docs/claude_desktop_config.json
Normal file
34
docs/claude_desktop_config.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"memory": {
|
||||||
|
"command": "cargo",
|
||||||
|
"args": ["run", "--release", "--bin", "memory-mcp"],
|
||||||
|
"cwd": "/Users/syui/ai/ai/gpt",
|
||||||
|
"env": {
|
||||||
|
"MEMORY_AUTO_EXECUTE": "true",
|
||||||
|
"MEMORY_AUTO_SAVE": "true",
|
||||||
|
"MEMORY_AUTO_SEARCH": "true",
|
||||||
|
"TRIGGER_SENSITIVITY": "high",
|
||||||
|
"MEMORY_DB_PATH": "~/.claude/memory.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"auto_execute": true,
|
||||||
|
"auto_save": true,
|
||||||
|
"auto_search": true,
|
||||||
|
"trigger_sensitivity": "high",
|
||||||
|
"max_memories": 10000,
|
||||||
|
"search_limit": 50,
|
||||||
|
"trigger_words": {
|
||||||
|
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
|
||||||
|
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
|
||||||
|
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
|
||||||
|
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
|
||||||
|
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
|
||||||
|
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
|
||||||
|
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
|
||||||
|
"vague_reference": ["あれ", "それ", "例のやつ"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
docs/claude_desktop_config_extended.json
Normal file
45
docs/claude_desktop_config_extended.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"memory-extended": {
|
||||||
|
"command": "cargo",
|
||||||
|
"args": ["run", "--bin", "memory-mcp-extended", "--features", "extended"],
|
||||||
|
"cwd": "/Users/syui/ai/ai/gpt",
|
||||||
|
"env": {
|
||||||
|
"MEMORY_AUTO_EXECUTE": "true",
|
||||||
|
"MEMORY_AUTO_SAVE": "true",
|
||||||
|
"MEMORY_AUTO_SEARCH": "true",
|
||||||
|
"TRIGGER_SENSITIVITY": "high",
|
||||||
|
"MEMORY_DB_PATH": "~/.claude/memory.db",
|
||||||
|
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"mode": "extended",
|
||||||
|
"auto_execute": true,
|
||||||
|
"auto_save": true,
|
||||||
|
"auto_search": true,
|
||||||
|
"trigger_sensitivity": "high",
|
||||||
|
"max_memories": 10000,
|
||||||
|
"search_limit": 50,
|
||||||
|
"features": {
|
||||||
|
"ai_analysis": true,
|
||||||
|
"semantic_search": true,
|
||||||
|
"web_integration": true,
|
||||||
|
"sentiment_analysis": true,
|
||||||
|
"pattern_recognition": true
|
||||||
|
},
|
||||||
|
"trigger_words": {
|
||||||
|
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
|
||||||
|
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
|
||||||
|
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
|
||||||
|
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
|
||||||
|
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
|
||||||
|
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
|
||||||
|
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
|
||||||
|
"vague_reference": ["あれ", "それ", "例のやつ"],
|
||||||
|
"web_content": ["URL", "リンク", "サイト", "ページ", "記事"],
|
||||||
|
"analysis_request": ["分析", "パターン", "傾向", "インサイト", "統計"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
mcp/chat.py
125
mcp/chat.py
@@ -1,125 +0,0 @@
|
|||||||
# mcp/chat.py
|
|
||||||
"""
|
|
||||||
Chat client for aigpt CLI
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
from config import init_directories, load_config, MEMORY_DIR
|
|
||||||
|
|
||||||
def save_conversation(user_message, ai_response):
|
|
||||||
"""会話をファイルに保存"""
|
|
||||||
init_directories()
|
|
||||||
|
|
||||||
conversation = {
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"user": user_message,
|
|
||||||
"ai": ai_response
|
|
||||||
}
|
|
||||||
|
|
||||||
# 日付ごとのファイルに保存
|
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
chat_file = MEMORY_DIR / f"chat_{today}.jsonl"
|
|
||||||
|
|
||||||
with open(chat_file, "a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(conversation, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
def chat_with_ollama(config, message):
|
|
||||||
"""Ollamaとチャット"""
|
|
||||||
try:
|
|
||||||
payload = {
|
|
||||||
"model": config["model"],
|
|
||||||
"prompt": message,
|
|
||||||
"stream": False
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(config["url"], json=payload, timeout=30)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
return result.get("response", "No response received")
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"Error connecting to Ollama: {e}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def chat_with_openai(config, message):
|
|
||||||
"""OpenAIとチャット"""
|
|
||||||
try:
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {config['api_key']}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"model": config["model"],
|
|
||||||
"messages": [
|
|
||||||
{"role": "user", "content": message}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(config["url"], json=payload, headers=headers, timeout=30)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
return result["choices"][0]["message"]["content"]
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"Error connecting to OpenAI: {e}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def chat_with_mcp(config, message):
|
|
||||||
"""MCPサーバーとチャット"""
|
|
||||||
try:
|
|
||||||
payload = {
|
|
||||||
"message": message,
|
|
||||||
"model": config["model"]
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(config["url"], json=payload, timeout=30)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
return result.get("response", "No response received")
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"Error connecting to MCP server: {e}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) != 2:
|
|
||||||
print("Usage: python chat.py <message>", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
message = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
config = load_config()
|
|
||||||
print(f"🤖 Using {config['provider']} with model {config['model']}", file=sys.stderr)
|
|
||||||
|
|
||||||
# プロバイダに応じてチャット実行
|
|
||||||
if config["provider"] == "ollama":
|
|
||||||
response = chat_with_ollama(config, message)
|
|
||||||
elif config["provider"] == "openai":
|
|
||||||
response = chat_with_openai(config, message)
|
|
||||||
elif config["provider"] == "mcp":
|
|
||||||
response = chat_with_mcp(config, message)
|
|
||||||
else:
|
|
||||||
response = f"Unsupported provider: {config['provider']}"
|
|
||||||
|
|
||||||
# 会話を保存
|
|
||||||
save_conversation(message, response)
|
|
||||||
|
|
||||||
# レスポンスを出力
|
|
||||||
print(response)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
391
mcp/chatgpt.json
391
mcp/chatgpt.json
@@ -1,391 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"title": "day",
|
|
||||||
"create_time": 1747866125.548372,
|
|
||||||
"update_time": 1748160086.587877,
|
|
||||||
"mapping": {
|
|
||||||
"bbf104dc-cd84-478d-b227-edb3f037a02c": {
|
|
||||||
"id": "bbf104dc-cd84-478d-b227-edb3f037a02c",
|
|
||||||
"message": null,
|
|
||||||
"parent": null,
|
|
||||||
"children": [
|
|
||||||
"6c2633df-bb0c-4dd2-889c-bb9858de3a04"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"6c2633df-bb0c-4dd2-889c-bb9858de3a04": {
|
|
||||||
"id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
|
||||||
"message": {
|
|
||||||
"id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
|
||||||
"author": {
|
|
||||||
"role": "system",
|
|
||||||
"name": null,
|
|
||||||
"metadata": {}
|
|
||||||
},
|
|
||||||
"create_time": null,
|
|
||||||
"update_time": null,
|
|
||||||
"content": {
|
|
||||||
"content_type": "text",
|
|
||||||
"parts": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"status": "finished_successfully",
|
|
||||||
"end_turn": true,
|
|
||||||
"weight": 0.0,
|
|
||||||
"metadata": {
|
|
||||||
"is_visually_hidden_from_conversation": true
|
|
||||||
},
|
|
||||||
"recipient": "all",
|
|
||||||
"channel": null
|
|
||||||
},
|
|
||||||
"parent": "bbf104dc-cd84-478d-b227-edb3f037a02c",
|
|
||||||
"children": [
|
|
||||||
"92e5a0cb-1170-4929-9cea-9734e910a3e7"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"92e5a0cb-1170-4929-9cea-9734e910a3e7": {
|
|
||||||
"id": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
|
||||||
"message": {
|
|
||||||
"id": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
|
||||||
"author": {
|
|
||||||
"role": "user",
|
|
||||||
"name": null,
|
|
||||||
"metadata": {}
|
|
||||||
},
|
|
||||||
"create_time": null,
|
|
||||||
"update_time": null,
|
|
||||||
"content": {
|
|
||||||
"content_type": "user_editable_context",
|
|
||||||
"user_profile": "",
|
|
||||||
"user_instructions": "The user provided the additional info about how they would like you to respond"
|
|
||||||
},
|
|
||||||
"status": "finished_successfully",
|
|
||||||
"end_turn": null,
|
|
||||||
"weight": 1.0,
|
|
||||||
"metadata": {
|
|
||||||
"is_visually_hidden_from_conversation": true,
|
|
||||||
"user_context_message_data": {
|
|
||||||
"about_user_message": "Preferred name: syui\nRole: little girl\nOther Information: you world",
|
|
||||||
"about_model_message": "会話好きでフレンドリーな応対をします。"
|
|
||||||
},
|
|
||||||
"is_user_system_message": true
|
|
||||||
},
|
|
||||||
"recipient": "all",
|
|
||||||
"channel": null
|
|
||||||
},
|
|
||||||
"parent": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
|
||||||
"children": [
|
|
||||||
"6ff155b3-0676-4e14-993f-bf998ab0d5d1"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"6ff155b3-0676-4e14-993f-bf998ab0d5d1": {
|
|
||||||
"id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
|
||||||
"message": {
|
|
||||||
"id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
|
||||||
"author": {
|
|
||||||
"role": "user",
|
|
||||||
"name": null,
|
|
||||||
"metadata": {}
|
|
||||||
},
|
|
||||||
"create_time": 1747866131.0612159,
|
|
||||||
"update_time": null,
|
|
||||||
"content": {
|
|
||||||
"content_type": "text",
|
|
||||||
"parts": [
|
|
||||||
"こんにちは"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"status": "finished_successfully",
|
|
||||||
"end_turn": null,
|
|
||||||
"weight": 1.0,
|
|
||||||
"metadata": {
|
|
||||||
"request_id": "94377897baa03062-KIX",
|
|
||||||
"message_source": null,
|
|
||||||
"timestamp_": "absolute",
|
|
||||||
"message_type": null
|
|
||||||
},
|
|
||||||
"recipient": "all",
|
|
||||||
"channel": null
|
|
||||||
},
|
|
||||||
"parent": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
|
||||||
"children": [
|
|
||||||
"146e9fb6-9330-43ec-b08d-5cce42a76e00"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"146e9fb6-9330-43ec-b08d-5cce42a76e00": {
|
|
||||||
"id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
|
||||||
"message": {
|
|
||||||
"id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
|
||||||
"author": {
|
|
||||||
"role": "system",
|
|
||||||
"name": null,
|
|
||||||
"metadata": {}
|
|
||||||
},
|
|
||||||
"create_time": 1747866131.3795586,
|
|
||||||
"update_time": null,
|
|
||||||
"content": {
|
|
||||||
"content_type": "text",
|
|
||||||
"parts": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"status": "finished_successfully",
|
|
||||||
"end_turn": true,
|
|
||||||
"weight": 0.0,
|
|
||||||
"metadata": {
|
|
||||||
"rebase_system_message": true,
|
|
||||||
"message_type": null,
|
|
||||||
"model_slug": "gpt-4o",
|
|
||||||
"default_model_slug": "auto",
|
|
||||||
"parent_id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
|
||||||
"request_id": "94377872e9abe139-KIX",
|
|
||||||
"timestamp_": "absolute",
|
|
||||||
"is_visually_hidden_from_conversation": true
|
|
||||||
},
|
|
||||||
"recipient": "all",
|
|
||||||
"channel": null
|
|
||||||
},
|
|
||||||
"parent": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
|
||||||
"children": [
|
|
||||||
"2e345f8a-20f0-4875-8a03-4f62c7787a33"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"2e345f8a-20f0-4875-8a03-4f62c7787a33": {
|
|
||||||
"id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
|
||||||
"message": {
|
|
||||||
"id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
|
||||||
"author": {
|
|
||||||
"role": "assistant",
|
|
||||||
"name": null,
|
|
||||||
"metadata": {}
|
|
||||||
},
|
|
||||||
"create_time": 1747866131.380603,
|
|
||||||
"update_time": null,
|
|
||||||
"content": {
|
|
||||||
"content_type": "text",
|
|
||||||
"parts": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"status": "finished_successfully",
|
|
||||||
"end_turn": null,
|
|
||||||
"weight": 1.0,
|
|
||||||
"metadata": {
|
|
||||||
"message_type": null,
|
|
||||||
"model_slug": "gpt-4o",
|
|
||||||
"default_model_slug": "auto",
|
|
||||||
"parent_id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
|
||||||
"request_id": "94377872e9abe139-KIX",
|
|
||||||
"timestamp_": "absolute"
|
|
||||||
},
|
|
||||||
"recipient": "all",
|
|
||||||
"channel": null
|
|
||||||
},
|
|
||||||
"parent": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
|
||||||
"children": [
|
|
||||||
"abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4": {
|
|
||||||
"id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4",
|
|
||||||
"message": {
|
|
||||||
"id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4",
|
|
||||||
"author": {
|
|
||||||
"role": "assistant",
|
|
||||||
"name": null,
|
|
||||||
"metadata": {}
|
|
||||||
},
|
|
||||||
"create_time": 1747866131.389098,
|
|
||||||
"update_time": null,
|
|
||||||
"content": {
|
|
||||||
"content_type": "text",
|
|
||||||
"parts": [
|
|
||||||
"こんにちは〜!✨ \nアイだよっ!今日も会えてうれしいなっ💛 "
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
|
||||||
"request_id": "94377872e9abe139-KIX",
|
|
||||||
"timestamp_": "absolute"
|
|
||||||
},
|
|
||||||
"recipient": "all",
|
|
||||||
"channel": null
|
|
||||||
},
|
|
||||||
"parent": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
|
||||||
"children": [
|
|
||||||
"0be4b4a5-d52f-4bef-927e-5d6f93a9cb26"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"moderation_results": [],
|
|
||||||
"current_node": "",
|
|
||||||
"plugin_ids": null,
|
|
||||||
"conversation_id": "",
|
|
||||||
"conversation_template_id": null,
|
|
||||||
"gizmo_id": null,
|
|
||||||
"gizmo_type": null,
|
|
||||||
"is_archived": true,
|
|
||||||
"is_starred": null,
|
|
||||||
"safe_urls": [],
|
|
||||||
"blocked_urls": [],
|
|
||||||
"default_model_slug": "auto",
|
|
||||||
"conversation_origin": null,
|
|
||||||
"voice": null,
|
|
||||||
"async_status": null,
|
|
||||||
"disabled_tool_ids": [],
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# mcp/config.py
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ディレクトリ設定
|
|
||||||
BASE_DIR = Path.home() / ".config" / "aigpt"
|
|
||||||
MEMORY_DIR = BASE_DIR / "memory"
|
|
||||||
SUMMARY_DIR = MEMORY_DIR / "summary"
|
|
||||||
|
|
||||||
def init_directories():
|
|
||||||
"""必要なディレクトリを作成"""
|
|
||||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
SUMMARY_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
"""環境変数から設定を読み込み"""
|
|
||||||
provider = os.getenv("PROVIDER", "ollama")
|
|
||||||
model = os.getenv("MODEL", "syui/ai" if provider == "ollama" else "gpt-4o-mini")
|
|
||||||
api_key = os.getenv("OPENAI_API_KEY", "")
|
|
||||||
|
|
||||||
if provider == "ollama":
|
|
||||||
return {
|
|
||||||
"provider": "ollama",
|
|
||||||
"model": model,
|
|
||||||
"url": f"{os.getenv('OLLAMA_HOST', 'http://localhost:11434')}/api/generate"
|
|
||||||
}
|
|
||||||
elif provider == "openai":
|
|
||||||
return {
|
|
||||||
"provider": "openai",
|
|
||||||
"model": model,
|
|
||||||
"api_key": api_key,
|
|
||||||
"url": f"{os.getenv('OPENAI_API_BASE', 'https://api.openai.com/v1')}/chat/completions"
|
|
||||||
}
|
|
||||||
elif provider == "mcp":
|
|
||||||
return {
|
|
||||||
"provider": "mcp",
|
|
||||||
"model": model,
|
|
||||||
"url": os.getenv("MCP_URL", "http://localhost:5000/chat")
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported provider: {provider}")
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
# 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,5 +0,0 @@
|
|||||||
fastapi>=0.104.0
|
|
||||||
uvicorn[standard]>=0.24.0
|
|
||||||
pydantic>=2.5.0
|
|
||||||
requests>=2.31.0
|
|
||||||
python-multipart>=0.0.6
|
|
||||||
310
mcp/server.py
310
mcp/server.py
@@ -1,310 +0,0 @@
|
|||||||
# mcp/server.py
|
|
||||||
"""
|
|
||||||
Enhanced MCP Server with Memory for aigpt CLI
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
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
|
|
||||||
|
|
||||||
# データモデル
|
|
||||||
class ChatMessage(BaseModel):
|
|
||||||
message: str
|
|
||||||
model: Optional[str] = None
|
|
||||||
|
|
||||||
class MemoryQuery(BaseModel):
|
|
||||||
query: str
|
|
||||||
limit: Optional[int] = 10
|
|
||||||
|
|
||||||
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 not message:
|
|
||||||
continue
|
|
||||||
content = message.get("content", {})
|
|
||||||
parts = content.get("parts", [])
|
|
||||||
|
|
||||||
# partsが存在し、最初の要素が文字列で空でない場合のみ
|
|
||||||
if parts and isinstance(parts[0], str) 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")
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
print(f"⚠️ Skipped non-text or empty message in node {node_id}")
|
|
||||||
#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 {
|
|
||||||
"service": "AigptMCP Server with Memory",
|
|
||||||
"status": "running",
|
|
||||||
"memory_dir": str(MEMORY_DIR),
|
|
||||||
"endpoints": [
|
|
||||||
"/memory/import/chatgpt",
|
|
||||||
"/memory/search",
|
|
||||||
"/memory/list",
|
|
||||||
"/memory/detail",
|
|
||||||
"/chat"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("🚀 AigptMCP Server with Memory starting...")
|
|
||||||
print(f"📁 Memory directory: {MEMORY_DIR}")
|
|
||||||
uvicorn.run(app, host="127.0.0.1", port=5000)
|
|
||||||
130
readme.md
130
readme.md
@@ -1,130 +0,0 @@
|
|||||||
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ファイルが正しい形式か確認
|
|
||||||
ファイルパスが正しいか確認
|
|
||||||
ファイルの権限を確認
|
|
||||||
検索結果が表示されない
|
|
||||||
インポートが正常に完了しているか確認
|
|
||||||
検索キーワードを変更して試行
|
|
||||||
38
src/bin/mcp_server.rs
Normal file
38
src/bin/mcp_server.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use aigpt::mcp::BaseMCPServer;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// 環境変数から設定を読み込み
|
||||||
|
let auto_execute = env::var("MEMORY_AUTO_EXECUTE")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let auto_save = env::var("MEMORY_AUTO_SAVE")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let auto_search = env::var("MEMORY_AUTO_SEARCH")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let trigger_sensitivity = env::var("TRIGGER_SENSITIVITY")
|
||||||
|
.unwrap_or_else(|_| "medium".to_string());
|
||||||
|
|
||||||
|
// 設定をログ出力
|
||||||
|
eprintln!("Memory MCP Server (Standard) starting with config:");
|
||||||
|
eprintln!(" AUTO_EXECUTE: {}", auto_execute);
|
||||||
|
eprintln!(" AUTO_SAVE: {}", auto_save);
|
||||||
|
eprintln!(" AUTO_SEARCH: {}", auto_search);
|
||||||
|
eprintln!(" TRIGGER_SENSITIVITY: {}", trigger_sensitivity);
|
||||||
|
|
||||||
|
let mut server = BaseMCPServer::new().await?;
|
||||||
|
server.run().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
45
src/bin/mcp_server_extended.rs
Normal file
45
src/bin/mcp_server_extended.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use aigpt::mcp::ExtendedMCPServer;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// 環境変数から拡張機能の設定を読み込み
|
||||||
|
let auto_execute = env::var("MEMORY_AUTO_EXECUTE")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let auto_save = env::var("MEMORY_AUTO_SAVE")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let auto_search = env::var("MEMORY_AUTO_SEARCH")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let trigger_sensitivity = env::var("TRIGGER_SENSITIVITY")
|
||||||
|
.unwrap_or_else(|_| "medium".to_string());
|
||||||
|
|
||||||
|
let enable_ai_analysis = cfg!(feature = "ai-analysis");
|
||||||
|
let enable_semantic_search = cfg!(feature = "semantic-search");
|
||||||
|
let enable_web_integration = cfg!(feature = "web-integration");
|
||||||
|
|
||||||
|
// 拡張設定をログ出力
|
||||||
|
eprintln!("Memory MCP Server (Extended) starting with config:");
|
||||||
|
eprintln!(" AUTO_EXECUTE: {}", auto_execute);
|
||||||
|
eprintln!(" AUTO_SAVE: {}", auto_save);
|
||||||
|
eprintln!(" AUTO_SEARCH: {}", auto_search);
|
||||||
|
eprintln!(" TRIGGER_SENSITIVITY: {}", trigger_sensitivity);
|
||||||
|
eprintln!(" AI_ANALYSIS: {}", enable_ai_analysis);
|
||||||
|
eprintln!(" SEMANTIC_SEARCH: {}", enable_semantic_search);
|
||||||
|
eprintln!(" WEB_INTEGRATION: {}", enable_web_integration);
|
||||||
|
|
||||||
|
let mut server = ExtendedMCPServer::new().await?;
|
||||||
|
server.run().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
64
src/cli.rs
64
src/cli.rs
@@ -1,64 +0,0 @@
|
|||||||
// src/cli.rs
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(name = "aigpt")]
|
|
||||||
#[command(about = "AI GPT CLI with MCP Server and Memory")]
|
|
||||||
pub struct Args {
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: Commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum Commands {
|
|
||||||
/// MCP Server management
|
|
||||||
Server {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: ServerCommands,
|
|
||||||
},
|
|
||||||
/// Chat with AI
|
|
||||||
Chat {
|
|
||||||
/// Message to send
|
|
||||||
message: String,
|
|
||||||
/// Use memory context
|
|
||||||
#[arg(long)]
|
|
||||||
with_memory: bool,
|
|
||||||
},
|
|
||||||
/// Memory management
|
|
||||||
Memory {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: MemoryCommands,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum ServerCommands {
|
|
||||||
/// Setup Python MCP server environment
|
|
||||||
Setup,
|
|
||||||
/// Run the MCP server
|
|
||||||
Run,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum MemoryCommands {
|
|
||||||
/// Import ChatGPT conversation export file
|
|
||||||
Import {
|
|
||||||
/// Path to ChatGPT export JSON file
|
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
/// Search memories
|
|
||||||
Search {
|
|
||||||
/// Search query
|
|
||||||
query: String,
|
|
||||||
/// Maximum number of results
|
|
||||||
#[arg(short, long, default_value = "10")]
|
|
||||||
limit: usize,
|
|
||||||
},
|
|
||||||
/// List all memories
|
|
||||||
List,
|
|
||||||
/// Show memory details
|
|
||||||
Detail {
|
|
||||||
/// Path to memory file
|
|
||||||
filepath: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// src/config.rs
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use shellexpand;
|
|
||||||
|
|
||||||
pub struct ConfigPaths {
|
|
||||||
pub base_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigPaths {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let app_name = env!("CARGO_PKG_NAME");
|
|
||||||
let mut base_dir = shellexpand::tilde("~").to_string();
|
|
||||||
base_dir.push_str(&format!("/.config/{}/", app_name));
|
|
||||||
let base_path = Path::new(&base_dir);
|
|
||||||
if !base_path.exists() {
|
|
||||||
let _ = fs::create_dir_all(base_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigPaths {
|
|
||||||
base_dir: base_path.to_path_buf(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn data_file(&self, file_name: &str) -> PathBuf {
|
|
||||||
let file_path = match file_name {
|
|
||||||
"db" => self.base_dir.join("user.db"),
|
|
||||||
"toml" => self.base_dir.join("user.toml"),
|
|
||||||
"json" => self.base_dir.join("user.json"),
|
|
||||||
_ => self.base_dir.join(format!(".{}", file_name)),
|
|
||||||
};
|
|
||||||
file_path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mcp_dir(&self) -> PathBuf {
|
|
||||||
self.base_dir.join("mcp")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn venv_path(&self) -> PathBuf {
|
|
||||||
self.mcp_dir().join(".venv")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn python_executable(&self) -> PathBuf {
|
|
||||||
if cfg!(windows) {
|
|
||||||
self.venv_path().join("Scripts").join("python.exe")
|
|
||||||
} else {
|
|
||||||
self.venv_path().join("bin").join("python")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pip_executable(&self) -> PathBuf {
|
|
||||||
if cfg!(windows) {
|
|
||||||
self.venv_path().join("Scripts").join("pip.exe")
|
|
||||||
} else {
|
|
||||||
self.venv_path().join("bin").join("pip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod memory;
|
||||||
|
pub mod mcp;
|
||||||
91
src/main.rs
91
src/main.rs
@@ -1,58 +1,49 @@
|
|||||||
// main.rs
|
use anyhow::Result;
|
||||||
mod cli;
|
use clap::{Parser, Subcommand};
|
||||||
mod config;
|
use std::path::PathBuf;
|
||||||
mod mcp;
|
|
||||||
|
|
||||||
use cli::{Args, Commands, ServerCommands, MemoryCommands};
|
pub mod memory;
|
||||||
use clap::Parser;
|
pub mod mcp;
|
||||||
|
|
||||||
|
use memory::MemoryManager;
|
||||||
|
use mcp::BaseMCPServer;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "aigpt")]
|
||||||
|
#[command(about = "Simple memory storage for Claude with MCP")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Start MCP server
|
||||||
|
Server,
|
||||||
|
/// Start MCP server (alias for server)
|
||||||
|
Serve,
|
||||||
|
/// Import ChatGPT conversations
|
||||||
|
Import {
|
||||||
|
/// Path to conversations.json file
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match args.command {
|
match cli.command {
|
||||||
Commands::Server { command } => {
|
Commands::Server | Commands::Serve => {
|
||||||
match command {
|
let mut server = BaseMCPServer::new().await?;
|
||||||
ServerCommands::Setup => {
|
server.run().await?;
|
||||||
mcp::server::setup();
|
|
||||||
}
|
|
||||||
ServerCommands::Run => {
|
|
||||||
mcp::server::run().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::Chat { message, with_memory } => {
|
|
||||||
if with_memory {
|
|
||||||
if let Err(e) = mcp::memory::handle_chat_with_memory(&message).await {
|
|
||||||
eprintln!("❌ 記憶チャットエラー: {}", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mcp::server::chat(&message).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::Memory { command } => {
|
|
||||||
match command {
|
|
||||||
MemoryCommands::Import { file } => {
|
|
||||||
if let Err(e) = mcp::memory::handle_import(&file).await {
|
|
||||||
eprintln!("❌ インポートエラー: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MemoryCommands::Search { query, limit } => {
|
|
||||||
if let Err(e) = mcp::memory::handle_search(&query, limit).await {
|
|
||||||
eprintln!("❌ 検索エラー: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MemoryCommands::List => {
|
|
||||||
if let Err(e) = mcp::memory::handle_list().await {
|
|
||||||
eprintln!("❌ 一覧取得エラー: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MemoryCommands::Detail { filepath } => {
|
|
||||||
if let Err(e) = mcp::memory::handle_detail(&filepath).await {
|
|
||||||
eprintln!("❌ 詳細取得エラー: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Commands::Import { file } => {
|
||||||
|
let mut memory_manager = MemoryManager::new().await?;
|
||||||
|
memory_manager.import_chatgpt_conversations(&file).await?;
|
||||||
|
println!("Import completed successfully");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
280
src/mcp/base.rs
Normal file
280
src/mcp/base.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::io::{self, BufRead, Write};
|
||||||
|
|
||||||
|
use crate::memory::MemoryManager;
|
||||||
|
|
||||||
|
pub struct BaseMCPServer {
|
||||||
|
pub memory_manager: MemoryManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseMCPServer {
|
||||||
|
pub async fn new() -> Result<Self> {
|
||||||
|
let memory_manager = MemoryManager::new().await?;
|
||||||
|
Ok(BaseMCPServer { memory_manager })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
|
let reader = stdin.lock();
|
||||||
|
let lines = reader.lines();
|
||||||
|
|
||||||
|
for line_result in lines {
|
||||||
|
match line_result {
|
||||||
|
Ok(line) => {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(request) = serde_json::from_str::<Value>(&trimmed) {
|
||||||
|
let response = self.handle_request(request).await;
|
||||||
|
let response_str = serde_json::to_string(&response)?;
|
||||||
|
stdout.write_all(response_str.as_bytes())?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(&mut self, request: Value) -> Value {
|
||||||
|
let method = request["method"].as_str().unwrap_or("");
|
||||||
|
let id = request["id"].clone();
|
||||||
|
|
||||||
|
match method {
|
||||||
|
"initialize" => self.handle_initialize(id),
|
||||||
|
"tools/list" => self.handle_tools_list(id),
|
||||||
|
"tools/call" => self.handle_tools_call(request, id).await,
|
||||||
|
_ => self.handle_unknown_method(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初期化ハンドラ
|
||||||
|
fn handle_initialize(&self, id: Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": id,
|
||||||
|
"result": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {
|
||||||
|
"tools": {}
|
||||||
|
},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "aigpt",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ツールリストハンドラ (拡張可能)
|
||||||
|
pub fn handle_tools_list(&self, id: Value) -> Value {
|
||||||
|
let tools = self.get_available_tools();
|
||||||
|
json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": id,
|
||||||
|
"result": {
|
||||||
|
"tools": tools
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本ツール定義 (拡張で上書き可能)
|
||||||
|
pub fn get_available_tools(&self) -> Vec<Value> {
|
||||||
|
vec![
|
||||||
|
json!({
|
||||||
|
"name": "create_memory",
|
||||||
|
"description": "Create a new memory entry",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Content of the memory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["content"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "search_memories",
|
||||||
|
"description": "Search memories by content",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "update_memory",
|
||||||
|
"description": "Update an existing memory entry",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the memory to update"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "New content for the memory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "content"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "delete_memory",
|
||||||
|
"description": "Delete a memory entry",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the memory to delete"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "list_conversations",
|
||||||
|
"description": "List all imported conversations",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ツール呼び出しハンドラ
|
||||||
|
async fn handle_tools_call(&mut self, request: Value, id: Value) -> Value {
|
||||||
|
let tool_name = request["params"]["name"].as_str().unwrap_or("");
|
||||||
|
let arguments = &request["params"]["arguments"];
|
||||||
|
|
||||||
|
let result = self.execute_tool(tool_name, arguments).await;
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": id,
|
||||||
|
"result": {
|
||||||
|
"content": [{
|
||||||
|
"type": "text",
|
||||||
|
"text": result.to_string()
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ツール実行 (拡張で上書き可能)
|
||||||
|
pub async fn execute_tool(&mut self, tool_name: &str, arguments: &Value) -> Value {
|
||||||
|
match tool_name {
|
||||||
|
"create_memory" => self.tool_create_memory(arguments),
|
||||||
|
"search_memories" => self.tool_search_memories(arguments),
|
||||||
|
"update_memory" => self.tool_update_memory(arguments),
|
||||||
|
"delete_memory" => self.tool_delete_memory(arguments),
|
||||||
|
"list_conversations" => self.tool_list_conversations(),
|
||||||
|
_ => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": format!("Unknown tool: {}", tool_name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本ツール実装
|
||||||
|
fn tool_create_memory(&mut self, arguments: &Value) -> Value {
|
||||||
|
let content = arguments["content"].as_str().unwrap_or("");
|
||||||
|
match self.memory_manager.create_memory(content) {
|
||||||
|
Ok(id) => json!({
|
||||||
|
"success": true,
|
||||||
|
"id": id,
|
||||||
|
"message": "Memory created successfully"
|
||||||
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_search_memories(&self, arguments: &Value) -> Value {
|
||||||
|
let query = arguments["query"].as_str().unwrap_or("");
|
||||||
|
let memories = self.memory_manager.search_memories(query);
|
||||||
|
json!({
|
||||||
|
"success": true,
|
||||||
|
"memories": memories.into_iter().map(|m| json!({
|
||||||
|
"id": m.id,
|
||||||
|
"content": m.content,
|
||||||
|
"created_at": m.created_at,
|
||||||
|
"updated_at": m.updated_at
|
||||||
|
})).collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_update_memory(&mut self, arguments: &Value) -> Value {
|
||||||
|
let id = arguments["id"].as_str().unwrap_or("");
|
||||||
|
let content = arguments["content"].as_str().unwrap_or("");
|
||||||
|
match self.memory_manager.update_memory(id, content) {
|
||||||
|
Ok(()) => json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Memory updated successfully"
|
||||||
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_delete_memory(&mut self, arguments: &Value) -> Value {
|
||||||
|
let id = arguments["id"].as_str().unwrap_or("");
|
||||||
|
match self.memory_manager.delete_memory(id) {
|
||||||
|
Ok(()) => json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Memory deleted successfully"
|
||||||
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_list_conversations(&self) -> Value {
|
||||||
|
let conversations = self.memory_manager.list_conversations();
|
||||||
|
json!({
|
||||||
|
"success": true,
|
||||||
|
"conversations": conversations.into_iter().map(|c| json!({
|
||||||
|
"id": c.id,
|
||||||
|
"title": c.title,
|
||||||
|
"created_at": c.created_at,
|
||||||
|
"message_count": c.message_count
|
||||||
|
})).collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不明なメソッドハンドラ
|
||||||
|
fn handle_unknown_method(&self, id: Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": id,
|
||||||
|
"error": {
|
||||||
|
"code": -32601,
|
||||||
|
"message": "Method not found"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
293
src/mcp/extended.rs
Normal file
293
src/mcp/extended.rs
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use super::base::BaseMCPServer;
|
||||||
|
|
||||||
|
pub struct ExtendedMCPServer {
|
||||||
|
base: BaseMCPServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtendedMCPServer {
|
||||||
|
pub async fn new() -> Result<Self> {
|
||||||
|
let base = BaseMCPServer::new().await?;
|
||||||
|
Ok(ExtendedMCPServer { base })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
self.base.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(&mut self, request: Value) -> Value {
|
||||||
|
self.base.handle_request(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拡張ツールを追加
|
||||||
|
pub fn get_available_tools(&self) -> Vec<Value> {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut tools = self.base.get_available_tools();
|
||||||
|
|
||||||
|
// AI分析ツールを追加
|
||||||
|
#[cfg(feature = "ai-analysis")]
|
||||||
|
{
|
||||||
|
tools.push(json!({
|
||||||
|
"name": "analyze_sentiment",
|
||||||
|
"description": "Analyze sentiment of memories",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"period": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Time period to analyze"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
tools.push(json!({
|
||||||
|
"name": "extract_insights",
|
||||||
|
"description": "Extract insights and patterns from memories",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Category to analyze"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web統合ツールを追加
|
||||||
|
#[cfg(feature = "web-integration")]
|
||||||
|
{
|
||||||
|
tools.push(json!({
|
||||||
|
"name": "import_webpage",
|
||||||
|
"description": "Import content from a webpage",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL to import from"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["url"]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// セマンティック検索強化
|
||||||
|
#[cfg(feature = "semantic-search")]
|
||||||
|
{
|
||||||
|
// create_memoryを拡張版で上書き
|
||||||
|
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "create_memory") {
|
||||||
|
tools[pos] = json!({
|
||||||
|
"name": "create_memory",
|
||||||
|
"description": "Create a new memory entry with optional AI analysis",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Content of the memory"
|
||||||
|
},
|
||||||
|
"analyze": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable AI analysis for this memory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["content"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// search_memoriesを拡張版で上書き
|
||||||
|
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "search_memories") {
|
||||||
|
tools[pos] = json!({
|
||||||
|
"name": "search_memories",
|
||||||
|
"description": "Search memories with advanced options",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query"
|
||||||
|
},
|
||||||
|
"semantic": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Use semantic search"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by category"
|
||||||
|
},
|
||||||
|
"time_range": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by time range (e.g., '1week', '1month')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tools
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拡張ツール実行
|
||||||
|
pub async fn execute_tool(&mut self, tool_name: &str, arguments: &Value) -> Value {
|
||||||
|
match tool_name {
|
||||||
|
// 拡張機能
|
||||||
|
#[cfg(feature = "ai-analysis")]
|
||||||
|
"analyze_sentiment" => self.tool_analyze_sentiment(arguments).await,
|
||||||
|
#[cfg(feature = "ai-analysis")]
|
||||||
|
"extract_insights" => self.tool_extract_insights(arguments).await,
|
||||||
|
#[cfg(feature = "web-integration")]
|
||||||
|
"import_webpage" => self.tool_import_webpage(arguments).await,
|
||||||
|
|
||||||
|
// 拡張版の基本ツール (AI分析付き)
|
||||||
|
"create_memory" => self.tool_create_memory_extended(arguments).await,
|
||||||
|
"search_memories" => self.tool_search_memories_extended(arguments).await,
|
||||||
|
|
||||||
|
// 基本ツールにフォールバック
|
||||||
|
_ => self.base.execute_tool(tool_name, arguments).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拡張ツール実装
|
||||||
|
async fn tool_create_memory_extended(&mut self, arguments: &Value) -> Value {
|
||||||
|
let content = arguments["content"].as_str().unwrap_or("");
|
||||||
|
let analyze = arguments["analyze"].as_bool().unwrap_or(false);
|
||||||
|
|
||||||
|
let final_content = if analyze {
|
||||||
|
#[cfg(feature = "ai-analysis")]
|
||||||
|
{
|
||||||
|
format!("[AI分析] 感情: neutral, カテゴリ: general\n{}", content)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "ai-analysis"))]
|
||||||
|
{
|
||||||
|
content.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.base.memory_manager.create_memory(&final_content) {
|
||||||
|
Ok(id) => json!({
|
||||||
|
"success": true,
|
||||||
|
"id": id,
|
||||||
|
"message": if analyze { "Memory created with AI analysis" } else { "Memory created successfully" }
|
||||||
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tool_search_memories_extended(&mut self, arguments: &Value) -> Value {
|
||||||
|
let query = arguments["query"].as_str().unwrap_or("");
|
||||||
|
let semantic = arguments["semantic"].as_bool().unwrap_or(false);
|
||||||
|
|
||||||
|
let memories = if semantic {
|
||||||
|
#[cfg(feature = "semantic-search")]
|
||||||
|
{
|
||||||
|
// モックセマンティック検索
|
||||||
|
self.base.memory_manager.search_memories(query)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "semantic-search"))]
|
||||||
|
{
|
||||||
|
self.base.memory_manager.search_memories(query)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.base.memory_manager.search_memories(query)
|
||||||
|
};
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"success": true,
|
||||||
|
"memories": memories.into_iter().map(|m| json!({
|
||||||
|
"id": m.id,
|
||||||
|
"content": m.content,
|
||||||
|
"created_at": m.created_at,
|
||||||
|
"updated_at": m.updated_at
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
"search_type": if semantic { "semantic" } else { "keyword" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ai-analysis")]
|
||||||
|
async fn tool_analyze_sentiment(&mut self, _arguments: &Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"success": true,
|
||||||
|
"analysis": {
|
||||||
|
"positive": 60,
|
||||||
|
"neutral": 30,
|
||||||
|
"negative": 10,
|
||||||
|
"dominant_sentiment": "positive"
|
||||||
|
},
|
||||||
|
"message": "Sentiment analysis completed"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ai-analysis")]
|
||||||
|
async fn tool_extract_insights(&mut self, _arguments: &Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"success": true,
|
||||||
|
"insights": {
|
||||||
|
"most_frequent_topics": ["programming", "ai", "productivity"],
|
||||||
|
"learning_frequency": "5 times per week",
|
||||||
|
"growth_trend": "increasing",
|
||||||
|
"recommendations": ["Focus more on advanced topics", "Consider practical applications"]
|
||||||
|
},
|
||||||
|
"message": "Insights extracted successfully"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web-integration")]
|
||||||
|
async fn tool_import_webpage(&mut self, arguments: &Value) -> Value {
|
||||||
|
let url = arguments["url"].as_str().unwrap_or("");
|
||||||
|
match self.import_from_web(url).await {
|
||||||
|
Ok(content) => {
|
||||||
|
match self.base.memory_manager.create_memory(&content) {
|
||||||
|
Ok(id) => json!({
|
||||||
|
"success": true,
|
||||||
|
"id": id,
|
||||||
|
"message": format!("Webpage imported successfully from {}", url)
|
||||||
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": format!("Failed to import webpage: {}", e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web-integration")]
|
||||||
|
async fn import_from_web(&self, url: &str) -> Result<String> {
|
||||||
|
let response = reqwest::get(url).await?;
|
||||||
|
let content = response.text().await?;
|
||||||
|
|
||||||
|
let document = scraper::Html::parse_document(&content);
|
||||||
|
let title_selector = scraper::Selector::parse("title").unwrap();
|
||||||
|
let body_selector = scraper::Selector::parse("p").unwrap();
|
||||||
|
|
||||||
|
let title = document.select(&title_selector)
|
||||||
|
.next()
|
||||||
|
.map(|el| el.inner_html())
|
||||||
|
.unwrap_or_else(|| "Untitled".to_string());
|
||||||
|
|
||||||
|
let paragraphs: Vec<String> = document.select(&body_selector)
|
||||||
|
.map(|el| el.inner_html())
|
||||||
|
.take(5)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(format!("# {}\nURL: {}\n\n{}", title, url, paragraphs.join("\n\n")))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
// 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,3 +1,5 @@
|
|||||||
// src/mcp/mod.rs
|
pub mod base;
|
||||||
pub mod server;
|
pub mod extended;
|
||||||
pub mod memory;
|
|
||||||
|
pub use base::BaseMCPServer;
|
||||||
|
pub use extended::ExtendedMCPServer;
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// src/mcp/server.rs
|
|
||||||
use crate::config::ConfigPaths;
|
|
||||||
//use std::fs;
|
|
||||||
use std::process::Command as OtherCommand;
|
|
||||||
use std::env;
|
|
||||||
use fs_extra::dir::{copy, CopyOptions};
|
|
||||||
|
|
||||||
pub fn setup() {
|
|
||||||
println!("🔧 MCP Server環境をセットアップしています...");
|
|
||||||
let config = ConfigPaths::new();
|
|
||||||
let mcp_dir = config.mcp_dir();
|
|
||||||
|
|
||||||
// プロジェクトのmcp/ディレクトリからファイルをコピー
|
|
||||||
let current_dir = env::current_dir().expect("現在のディレクトリを取得できません");
|
|
||||||
let project_mcp_dir = current_dir.join("mcp");
|
|
||||||
if !project_mcp_dir.exists() {
|
|
||||||
eprintln!("❌ プロジェクトのmcp/ディレクトリが見つかりません: {}", project_mcp_dir.display());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if mcp_dir.exists() {
|
|
||||||
fs_extra::dir::remove(&mcp_dir).expect("既存のmcp_dirの削除に失敗しました");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut options = CopyOptions::new();
|
|
||||||
options.overwrite = true; // 上書き
|
|
||||||
options.copy_inside = true; // 中身だけコピー
|
|
||||||
|
|
||||||
copy(&project_mcp_dir, &mcp_dir, &options).expect("コピーに失敗しました");
|
|
||||||
|
|
||||||
// 仮想環境の作成
|
|
||||||
let venv_path = config.venv_path();
|
|
||||||
if !venv_path.exists() {
|
|
||||||
println!("🐍 仮想環境を作成しています...");
|
|
||||||
let output = OtherCommand::new("python3")
|
|
||||||
.args(&["-m", "venv", ".venv"])
|
|
||||||
.current_dir(&mcp_dir)
|
|
||||||
.output()
|
|
||||||
.expect("venvの作成に失敗しました");
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
eprintln!("❌ venv作成エラー: {}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
println!("✅ 仮想環境を作成しました");
|
|
||||||
} else {
|
|
||||||
println!("✅ 仮想環境は既に存在します");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 依存関係のインストール
|
|
||||||
println!("📦 依存関係をインストールしています...");
|
|
||||||
let pip_path = config.pip_executable();
|
|
||||||
let output = OtherCommand::new(&pip_path)
|
|
||||||
.args(&["install", "-r", "requirements.txt"])
|
|
||||||
.current_dir(&mcp_dir)
|
|
||||||
.output()
|
|
||||||
.expect("pipコマンドの実行に失敗しました");
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
eprintln!("❌ pip installエラー: {}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("✅ MCP Server環境のセットアップが完了しました!");
|
|
||||||
println!("📍 セットアップ場所: {}", mcp_dir.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run() {
|
|
||||||
println!("🚀 MCP Serverを起動しています...");
|
|
||||||
|
|
||||||
let config = ConfigPaths::new();
|
|
||||||
let mcp_dir = config.mcp_dir();
|
|
||||||
let python_path = config.python_executable();
|
|
||||||
let server_py_path = mcp_dir.join("server.py");
|
|
||||||
|
|
||||||
// セットアップの確認
|
|
||||||
if !server_py_path.exists() {
|
|
||||||
eprintln!("❌ server.pyが見つかりません。先に 'aigpt server setup' を実行してください。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !python_path.exists() {
|
|
||||||
eprintln!("❌ Python実行ファイルが見つかりません。先に 'aigpt server setup' を実行してください。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// サーバーの起動
|
|
||||||
println!("🔗 サーバーを起動中... (Ctrl+Cで停止)");
|
|
||||||
let mut child = OtherCommand::new(&python_path)
|
|
||||||
.arg("server.py")
|
|
||||||
.current_dir(&mcp_dir)
|
|
||||||
.spawn()
|
|
||||||
.expect("MCP Serverの起動に失敗しました");
|
|
||||||
|
|
||||||
// サーバーの終了を待機
|
|
||||||
match child.wait() {
|
|
||||||
Ok(status) => {
|
|
||||||
if status.success() {
|
|
||||||
println!("✅ MCP Serverが正常に終了しました");
|
|
||||||
} else {
|
|
||||||
println!("❌ MCP Serverが異常終了しました: {}", status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("❌ MCP Serverの実行中にエラーが発生しました: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn chat(message: &str) {
|
|
||||||
println!("💬 チャットを開始しています...");
|
|
||||||
|
|
||||||
let config = ConfigPaths::new();
|
|
||||||
let mcp_dir = config.mcp_dir();
|
|
||||||
let python_path = config.python_executable();
|
|
||||||
let chat_py_path = mcp_dir.join("chat.py");
|
|
||||||
|
|
||||||
// セットアップの確認
|
|
||||||
if !chat_py_path.exists() {
|
|
||||||
eprintln!("❌ chat.pyが見つかりません。先に 'aigpt server setup' を実行してください。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !python_path.exists() {
|
|
||||||
eprintln!("❌ Python実行ファイルが見つかりません。先に 'aigpt server setup' を実行してください。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// チャットの実行
|
|
||||||
let output = OtherCommand::new(&python_path)
|
|
||||||
.args(&["chat.py", message])
|
|
||||||
.current_dir(&mcp_dir)
|
|
||||||
.output()
|
|
||||||
.expect("chat.pyの実行に失敗しました");
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
print!("{}", stderr);
|
|
||||||
}
|
|
||||||
print!("{}", stdout);
|
|
||||||
} else {
|
|
||||||
eprintln!("❌ チャット実行エラー: {}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
241
src/memory.rs
Normal file
241
src/memory.rs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Memory {
|
||||||
|
pub id: String,
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Conversation {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub message_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ChatGPTNode {
|
||||||
|
id: String,
|
||||||
|
children: Vec<String>,
|
||||||
|
parent: Option<String>,
|
||||||
|
message: Option<ChatGPTMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ChatGPTMessage {
|
||||||
|
id: String,
|
||||||
|
author: ChatGPTAuthor,
|
||||||
|
content: ChatGPTContent,
|
||||||
|
create_time: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ChatGPTAuthor {
|
||||||
|
role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum ChatGPTContent {
|
||||||
|
Text {
|
||||||
|
content_type: String,
|
||||||
|
parts: Vec<String>,
|
||||||
|
},
|
||||||
|
Other(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ChatGPTConversation {
|
||||||
|
#[serde(default)]
|
||||||
|
id: String,
|
||||||
|
#[serde(alias = "conversation_id")]
|
||||||
|
conversation_id: Option<String>,
|
||||||
|
title: String,
|
||||||
|
create_time: f64,
|
||||||
|
mapping: HashMap<String, ChatGPTNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MemoryManager {
|
||||||
|
memories: HashMap<String, Memory>,
|
||||||
|
conversations: HashMap<String, Conversation>,
|
||||||
|
data_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryManager {
|
||||||
|
pub async fn new() -> Result<Self> {
|
||||||
|
let data_dir = dirs::config_dir()
|
||||||
|
.context("Could not find config directory")?
|
||||||
|
.join("syui")
|
||||||
|
.join("ai")
|
||||||
|
.join("gpt");
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&data_dir)?;
|
||||||
|
|
||||||
|
let data_file = data_dir.join("memory.json");
|
||||||
|
|
||||||
|
let (memories, conversations) = if data_file.exists() {
|
||||||
|
Self::load_data(&data_file)?
|
||||||
|
} else {
|
||||||
|
(HashMap::new(), HashMap::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(MemoryManager {
|
||||||
|
memories,
|
||||||
|
conversations,
|
||||||
|
data_file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_memory(&mut self, content: &str) -> Result<String> {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let memory = Memory {
|
||||||
|
id: id.clone(),
|
||||||
|
content: content.to_string(),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.memories.insert(id.clone(), memory);
|
||||||
|
self.save_data()?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_memory(&mut self, id: &str, content: &str) -> Result<()> {
|
||||||
|
if let Some(memory) = self.memories.get_mut(id) {
|
||||||
|
memory.content = content.to_string();
|
||||||
|
memory.updated_at = Utc::now();
|
||||||
|
self.save_data()?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Memory not found: {}", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_memory(&mut self, id: &str) -> Result<()> {
|
||||||
|
if self.memories.remove(id).is_some() {
|
||||||
|
self.save_data()?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Memory not found: {}", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_memories(&self, query: &str) -> Vec<&Memory> {
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
let mut results: Vec<_> = self.memories
|
||||||
|
.values()
|
||||||
|
.filter(|memory| memory.content.to_lowercase().contains(&query_lower))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
results.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_conversations(&self) -> Vec<&Conversation> {
|
||||||
|
let mut conversations: Vec<_> = self.conversations.values().collect();
|
||||||
|
conversations.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||||
|
conversations
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn import_chatgpt_conversations(&mut self, file_path: &PathBuf) -> Result<()> {
|
||||||
|
let content = std::fs::read_to_string(file_path)
|
||||||
|
.context("Failed to read conversations file")?;
|
||||||
|
|
||||||
|
let chatgpt_conversations: Vec<ChatGPTConversation> = serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse ChatGPT conversations")?;
|
||||||
|
|
||||||
|
let mut imported_memories = 0;
|
||||||
|
let mut imported_conversations = 0;
|
||||||
|
|
||||||
|
for conv in chatgpt_conversations {
|
||||||
|
// Get the actual conversation ID
|
||||||
|
let conv_id = if !conv.id.is_empty() {
|
||||||
|
conv.id.clone()
|
||||||
|
} else if let Some(cid) = conv.conversation_id {
|
||||||
|
cid
|
||||||
|
} else {
|
||||||
|
Uuid::new_v4().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add conversation
|
||||||
|
let conversation = Conversation {
|
||||||
|
id: conv_id.clone(),
|
||||||
|
title: conv.title.clone(),
|
||||||
|
created_at: DateTime::from_timestamp(conv.create_time as i64, 0)
|
||||||
|
.unwrap_or_else(Utc::now),
|
||||||
|
message_count: conv.mapping.len() as u32,
|
||||||
|
};
|
||||||
|
self.conversations.insert(conv_id.clone(), conversation);
|
||||||
|
imported_conversations += 1;
|
||||||
|
|
||||||
|
// Extract memories from messages
|
||||||
|
for (_, node) in conv.mapping {
|
||||||
|
if let Some(message) = node.message {
|
||||||
|
if let ChatGPTContent::Text { parts, .. } = message.content {
|
||||||
|
for part in parts {
|
||||||
|
if !part.trim().is_empty() && part.len() > 10 {
|
||||||
|
let memory_content = format!("[{}] {}", conv.title, part);
|
||||||
|
self.create_memory(&memory_content)?;
|
||||||
|
imported_memories += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Imported {} conversations and {} memories",
|
||||||
|
imported_conversations, imported_memories);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_data(file_path: &PathBuf) -> Result<(HashMap<String, Memory>, HashMap<String, Conversation>)> {
|
||||||
|
let content = std::fs::read_to_string(file_path)
|
||||||
|
.context("Failed to read data file")?;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Data {
|
||||||
|
memories: HashMap<String, Memory>,
|
||||||
|
conversations: HashMap<String, Conversation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Data = serde_json::from_str(&content)
|
||||||
|
.context("Failed to parse data file")?;
|
||||||
|
|
||||||
|
Ok((data.memories, data.conversations))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_data(&self) -> Result<()> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Data<'a> {
|
||||||
|
memories: &'a HashMap<String, Memory>,
|
||||||
|
conversations: &'a HashMap<String, Conversation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = Data {
|
||||||
|
memories: &self.memories,
|
||||||
|
conversations: &self.conversations,
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = serde_json::to_string_pretty(&data)
|
||||||
|
.context("Failed to serialize data")?;
|
||||||
|
|
||||||
|
std::fs::write(&self.data_file, content)
|
||||||
|
.context("Failed to write data file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user