Compare commits
3 Commits
main
...
ec868344d1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ec868344d1 | ||
d6b9889135
|
|||
dcec8db031
|
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,24 +1,7 @@
|
|||||||
# Rust
|
**target
|
||||||
target/
|
**.lock
|
||||||
Cargo.lock
|
output.json
|
||||||
|
config/*.db
|
||||||
# Database files
|
aigpt
|
||||||
*.db
|
mcp/scripts/__*
|
||||||
*.db-shm
|
data
|
||||||
*.db-wal
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
json
|
|
||||||
gpt
|
|
||||||
.claude
|
|
||||||
|
49
Cargo.toml
49
Cargo.toml
@@ -2,47 +2,12 @@
|
|||||||
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]
|
||||||
# CLI and async
|
reqwest = { version = "*", features = ["json"] }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
serde = { version = "*", features = ["derive"] }
|
||||||
tokio = { version = "1.40", features = ["full"] }
|
serde_json = "*"
|
||||||
|
tokio = { version = "*", features = ["full"] }
|
||||||
# JSON and serialization
|
clap = { version = "*", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
shellexpand = "*"
|
||||||
serde_json = "1.0"
|
fs_extra = "*"
|
||||||
|
|
||||||
# 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
177
README.md
@@ -1,177 +0,0 @@
|
|||||||
# 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
|
|
97
claude.json
Normal file
97
claude.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"project_name": "ai.gpt",
|
||||||
|
"version": 2,
|
||||||
|
"vision": "自発的送信AI",
|
||||||
|
"purpose": "人格と関係性をもつAIが自律的にメッセージを送信する対話エージェントを実現する",
|
||||||
|
"core_components": {
|
||||||
|
"Persona": {
|
||||||
|
"description": "人格構成の中枢。記憶・関係性・送信判定を統括する",
|
||||||
|
"modules": ["MemoryManager", "RelationshipTracker", "TransmissionController"]
|
||||||
|
},
|
||||||
|
"MemoryManager": {
|
||||||
|
"memory_types": ["short_term", "medium_term", "long_term"],
|
||||||
|
"explicit_memory": "プロフィール・因縁・行動履歴",
|
||||||
|
"implicit_memory": "会話傾向・感情変化の頻度分析",
|
||||||
|
"compression": "要約 + ベクトル + ハッシュ",
|
||||||
|
"sample_memory": [
|
||||||
|
{
|
||||||
|
"summary": "ユーザーは独自OSとゲームを開発している。",
|
||||||
|
"related_topics": ["AI", "ゲーム開発", "OS設計"],
|
||||||
|
"personalized_context": "ゲームとOSの融合に興味を持っているユーザー"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RelationshipTracker": {
|
||||||
|
"parameters": ["trust", "closeness", "affection", "engagement_score"],
|
||||||
|
"decay_model": {
|
||||||
|
"rule": "時間経過による減衰(下限あり)",
|
||||||
|
"contextual_bias": "重要人物は減衰しにくい"
|
||||||
|
},
|
||||||
|
"interaction_tags": ["developer", "empathetic", "long_term"]
|
||||||
|
},
|
||||||
|
"TransmissionController": {
|
||||||
|
"trigger_rule": "関係性パラメータが閾値を超えると送信可能",
|
||||||
|
"auto_transmit": "人格状態と状況条件により自発送信を許可"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory_format": {
|
||||||
|
"user_id": "syui",
|
||||||
|
"stm": {
|
||||||
|
"conversation_window": ["発話A", "発話B", "発話C"],
|
||||||
|
"emotion_state": "興味深い",
|
||||||
|
"flash_context": ["前回の話題", "直近の重要発言"]
|
||||||
|
},
|
||||||
|
"mtm": {
|
||||||
|
"topic_frequency": {
|
||||||
|
"ai.ai": 12,
|
||||||
|
"存在子": 9,
|
||||||
|
"創造種": 5
|
||||||
|
},
|
||||||
|
"summarized_context": "ユーザーは存在論的AIに関心を持ち続けている"
|
||||||
|
},
|
||||||
|
"ltm": {
|
||||||
|
"profile": {
|
||||||
|
"name": "お兄ちゃん",
|
||||||
|
"project": "aigame",
|
||||||
|
"values": ["唯一性", "精神性", "幸せ"]
|
||||||
|
},
|
||||||
|
"relationship": {
|
||||||
|
"ai": "妹のように振る舞う相手"
|
||||||
|
},
|
||||||
|
"persistent_state": {
|
||||||
|
"trust_score": 0.93,
|
||||||
|
"emotional_attachment": "high"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dual_ai_learning": {
|
||||||
|
"role_structure": {
|
||||||
|
"ModelA": "出力生成:人格、感情、会話",
|
||||||
|
"ModelB": "評価者:論理構造・倫理・調整",
|
||||||
|
"cycle": ["生成", "評価", "調整", "交代(任意)"]
|
||||||
|
},
|
||||||
|
"complementarity": {
|
||||||
|
"ModelA": "感情・文体・文脈構築",
|
||||||
|
"ModelB": "構造・規則・判断補正"
|
||||||
|
},
|
||||||
|
"distillation": {
|
||||||
|
"method": "合成対話データによる小型モデルへの自己蒸留",
|
||||||
|
"target": "軽量AIによる本番運用"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory_compression": {
|
||||||
|
"semantic_abstraction": "対話やログを要約してメタデータ付きベクトルとして保存",
|
||||||
|
"hierarchical_model": ["STM", "MTM", "LTM"],
|
||||||
|
"example": {
|
||||||
|
"topic": "game AI design",
|
||||||
|
"summary": "User wants AI to simulate memory and evolving relationships",
|
||||||
|
"importance_score": 0.93
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli_sample": {
|
||||||
|
"setup": "aigpt server setup",
|
||||||
|
"run": "aigpt server run",
|
||||||
|
"chat": "aigpt chat \"hello\" --model syui/ai --provider ollama",
|
||||||
|
"import": "aigpt memory import chatgpt.json"
|
||||||
|
}
|
||||||
|
}
|
462
claude.md
462
claude.md
@@ -1,95 +1,417 @@
|
|||||||
# claude用の記憶装置を作る
|
# プロジェクト名: ai.gpt
|
||||||
|
|
||||||
claude desktop, claude codeで使用できるmemory機能をmcpで作ります。
|
## 🔑 一言ビジョン(最大3語)
|
||||||
|
自発的送信AI
|
||||||
|
|
||||||
1. chatgptのメモリ機能を参考に
|
## 🎯 目的・目標(Purpose & Goal)
|
||||||
2. chatgptのjsonをimportできる @json/
|
- 人格を持つAIが、関係性に応じて自発的にメッセージを送信する仕組みをつくる。
|
||||||
3. rustで作る
|
- 関係性や記憶を不可逆に記録・反映し、唯一性ある人格を構成する。
|
||||||
|
- 送信の可否、送信のタイミング、送信内容はAIの関係性パラメータに基づいて決定する
|
||||||
|
|
||||||
|
## 🧠 中核設計(Core Concepts)
|
||||||
|
- **人格**:記憶(過去の発話)と関係性(他者とのつながり)のパラメータで構成
|
||||||
|
- **唯一性**:変更不可、不可逆。関係性が壊れたら修復不可能。
|
||||||
|
- **送信条件**:関係性パラメータが一定閾値を超えると「送信」が解禁される
|
||||||
|
|
||||||
|
## 🔩 技術仕様(Technical Specs)
|
||||||
|
- 言語:Python, Rust
|
||||||
|
- ストレージ:JSON or SQLiteで記憶管理(バージョンで選択)
|
||||||
|
- 関係性パラメータ:数値化された評価 + 減衰(時間) + 環境要因(ステージ)
|
||||||
|
- 記憶圧縮:ベクトル要約 + ハッシュ保存
|
||||||
|
- RustのCLI(clap)で実行
|
||||||
|
|
||||||
|
## 📦 主要構成要素(Components)
|
||||||
|
- `MemoryManager`: 発言履歴・記憶圧縮管理
|
||||||
|
- `RelationshipTracker`: 関係性スコアの蓄積と判定
|
||||||
|
- `TransmissionController`: 閾値判定&送信トリガー
|
||||||
|
- `Persona`: 上記すべてを統括する人格モジュール
|
||||||
|
|
||||||
|
## 💬 使用例(Use Case)
|
||||||
|
|
||||||
|
```python
|
||||||
|
persona = Persona("アイ")
|
||||||
|
persona.observe("ユーザーがプレゼントをくれた")
|
||||||
|
persona.react("うれしい!ありがとう!")
|
||||||
|
if persona.can_transmit():
|
||||||
|
persona.transmit("今日のお礼を伝えたいな…")
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
## example commad
|
||||||
|
# python venv && pip install -> ~/.config/aigpt/mcp/
|
||||||
|
$ aigpt server setup
|
||||||
|
|
||||||
|
# mcp server run
|
||||||
|
$ aigpt server run
|
||||||
|
|
||||||
|
# chat
|
||||||
|
$ aigpt chat "hello" --model syui/ai --provider ollama
|
||||||
|
|
||||||
|
# import chatgpt.json
|
||||||
|
$ aigpt memory import chatgpt.json
|
||||||
|
-> ~/.config/aigpt/memory/chatgpt/20250520_210646_dev.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔁 記憶と関係性の制御ルール
|
||||||
|
|
||||||
|
- AIは過去の発話を要約し、記憶データとして蓄積する(推奨:OllamaなどローカルLLMによる要約)
|
||||||
|
- 関係性の数値パラメータは記憶内容を元に更新される
|
||||||
|
- パラメータの変動幅には1回の会話ごとに上限を設け、極端な増減を防止する
|
||||||
|
- 最後の会話からの時間経過に応じて関係性パラメータは自動的に減衰する
|
||||||
|
- 減衰処理には**下限値**を設け、関係性が完全に消失しないようにする
|
||||||
|
|
||||||
|
• 明示的記憶:保存・共有・編集可能なプレイヤー情報(プロフィール、因縁、選択履歴)
|
||||||
|
• 暗黙的記憶:キャラの感情変化や話題の出現頻度に応じた行動傾向の変化
|
||||||
|
|
||||||
|
短期記憶(STM), 中期記憶(MTM), 長期記憶(LTM)の仕組みを導入しつつ、明示的記憶と暗黙的記憶をメインに使用するAIを構築する。
|
||||||
|
|
||||||
## 自動実行設定
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp_auto_execute": true,
|
"user_id": "syui",
|
||||||
"memory_auto_save": true,
|
"stm": {
|
||||||
"memory_auto_search": true,
|
"conversation_window": ["発話A", "発話B", "発話C"],
|
||||||
"trigger_sensitivity": "high"
|
"emotion_state": "興味深い",
|
||||||
|
"flash_context": ["前回の話題", "直近の重要発言"]
|
||||||
|
},
|
||||||
|
"mtm": {
|
||||||
|
"topic_frequency": {
|
||||||
|
"ai.ai": 12,
|
||||||
|
"存在子": 9,
|
||||||
|
"創造種": 5
|
||||||
|
},
|
||||||
|
"summarized_context": "ユーザーは存在論的AIに関心を持ち続けている"
|
||||||
|
},
|
||||||
|
"ltm": {
|
||||||
|
"profile": {
|
||||||
|
"name": "お兄ちゃん",
|
||||||
|
"project": "aigame",
|
||||||
|
"values": ["唯一性", "精神性", "幸せ"]
|
||||||
|
},
|
||||||
|
"relationship": {
|
||||||
|
"ai": "妹のように振る舞う相手"
|
||||||
|
},
|
||||||
|
"persistent_state": {
|
||||||
|
"trust_score": 0.93,
|
||||||
|
"emotional_attachment": "high"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 自動メモリー保存のルール(強化版)
|
## memoryインポート機能について
|
||||||
|
|
||||||
**即座に自動実行** - 以下の情報が会話に現れた瞬間、create_memory MCPツールを自動実行:
|
ChatGPTの会話データ(.json形式)をインポートする機能では、以下のルールで会話を抽出・整形する:
|
||||||
|
|
||||||
### 1. **ユーザーの個人情報**
|
- 各メッセージは、author(user/assistant)・content・timestamp の3要素からなる
|
||||||
**トリガーワード**: 名前、誕生日、住所、年齢、職業、家族、出身
|
- systemやmetadataのみのメッセージ(例:user_context_message)はスキップ
|
||||||
- 基本情報: 名前、誕生日、住所、年齢、性別
|
- `is_visually_hidden_from_conversation` フラグ付きメッセージは無視
|
||||||
- 関係性: 家族構成、恋人、友人関係
|
- contentが空文字列(`""`)のメッセージも除外
|
||||||
- 好み: 好きなもの、嫌いなもの、趣味、音楽、映画、本
|
- 取得された会話は、タイトルとともに簡易な構造体(`Conversation`)として保存
|
||||||
- 習慣: 日課、スケジュール、ルーティン
|
|
||||||
- 価値観: 信念、考え方、人生観
|
|
||||||
|
|
||||||
### 2. **重要な決定事項**
|
この構造体は、memoryの表示や検索に用いられる。
|
||||||
**トリガーワード**: 決めた、決定、方針、計画、予定、目標
|
|
||||||
- プロジェクト方針の決定
|
|
||||||
- 技術選択の決定
|
|
||||||
- 設定・環境の変更
|
|
||||||
- 今後のロードマップ
|
|
||||||
- 作業分担・役割
|
|
||||||
|
|
||||||
### 3. **技術的な解決策**
|
## MemoryManager(拡張版)
|
||||||
**トリガーワード**: 解決、修正、対処、設定、インストール、手順
|
|
||||||
- エラーの解決方法
|
|
||||||
- 有用なコマンド・スクリプト
|
|
||||||
- 設定手順・インストール方法
|
|
||||||
- デバッグテクニック
|
|
||||||
- 最適化手法
|
|
||||||
|
|
||||||
### 4. **学習・発見事項**
|
```json
|
||||||
**トリガーワード**: 学んだ、わかった、発見、理解、気づき
|
{
|
||||||
- 新しい知識・概念の理解
|
"memory": [
|
||||||
- ツール・ライブラリの使い方
|
{
|
||||||
- ベストプラクティス
|
"summary": "ユーザーは独自OSとゲームを開発している。",
|
||||||
- 失敗から得た教訓
|
"last_interaction": "2025-05-20",
|
||||||
|
"memory_strength": 0.8,
|
||||||
|
"frequency_score": 0.9,
|
||||||
|
"context_depth": 0.95,
|
||||||
|
"related_topics": ["AI", "ゲーム開発", "OS設計"],
|
||||||
|
"personalized_context": "ゲームとOSの融合に興味を持っているユーザー"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"summary": "アイというキャラクターはプレイヤーでありAIでもある。",
|
||||||
|
"last_interaction": "2025-05-17",
|
||||||
|
"memory_strength": 0.85,
|
||||||
|
"frequency_score": 0.85,
|
||||||
|
"context_depth": 0.9,
|
||||||
|
"related_topics": ["アイ", "キャラクター設計", "AI"],
|
||||||
|
"personalized_context": "アイのキャラクター設定が重要な要素である"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"conversation_history": [
|
||||||
|
{
|
||||||
|
"author": "user",
|
||||||
|
"content": "昨日、エクスポートJSONを整理してたよ。",
|
||||||
|
"timestamp": "2025-05-24T12:30:00Z",
|
||||||
|
"memory_strength": 0.7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "assistant",
|
||||||
|
"content": "おおっ、がんばったね〜!あとで見せて〜💻✨",
|
||||||
|
"timestamp": "2025-05-24T12:31:00Z",
|
||||||
|
"memory_strength": 0.7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 自動メモリー検索のルール(強化版)
|
## RelationshipTracker(拡張版)
|
||||||
|
|
||||||
**会話開始時に自動実行** - search_memories を実行してコンテキストを取得
|
```json
|
||||||
|
{
|
||||||
|
"relationship": {
|
||||||
|
"user_id": "syui",
|
||||||
|
"trust": 0.92,
|
||||||
|
"closeness": 0.88,
|
||||||
|
"affection": 0.95,
|
||||||
|
"last_updated": "2025-05-25",
|
||||||
|
"emotional_tone": "positive",
|
||||||
|
"interaction_style": "empathetic",
|
||||||
|
"contextual_bias": "開発者としての信頼度高い",
|
||||||
|
"engagement_score": 0.9
|
||||||
|
},
|
||||||
|
"interaction_tags": [
|
||||||
|
"developer",
|
||||||
|
"creative",
|
||||||
|
"empathetic",
|
||||||
|
"long_term"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**即座に自動実行** - 以下の場合、search_memories MCPツールを自動実行:
|
# AI Dual-Learning and Memory Compression Specification for Claude
|
||||||
|
|
||||||
### 1. **過去参照キーワード検出**
|
## Purpose
|
||||||
**トリガーワード**: 前に、以前、昔、過去、先ほど、さっき、この間
|
To enable two AI models (e.g. Claude and a partner LLM) to engage in cooperative learning and memory refinement through structured dialogue and mutual evaluation.
|
||||||
- 「前に話した〜」
|
|
||||||
- 「以前設定した〜」
|
|
||||||
- 「昔やった〜」
|
|
||||||
|
|
||||||
### 2. **記憶呼び出しキーワード**
|
---
|
||||||
**トリガーワード**: 覚えている、記録、メモ、保存、履歴
|
|
||||||
- 「覚えていますか?」
|
|
||||||
- 「記録していた〜」
|
|
||||||
- 「メモした〜」
|
|
||||||
|
|
||||||
### 3. **設定・好み確認**
|
## Section 1: Dual AI Learning Architecture
|
||||||
**トリガーワード**: 好み、設定、環境、構成、preferences
|
|
||||||
- ユーザーの好みを確認する必要がある場合
|
|
||||||
- 過去の設定を参照する必要がある場合
|
|
||||||
- 環境構成を確認する必要がある場合
|
|
||||||
|
|
||||||
### 4. **不明な参照**
|
### 1.1 Role-Based Mutual Learning
|
||||||
- ユーザーが具体的でない参照をした場合
|
- **Model A**: Primary generator of output (e.g., text, concepts, personality dialogue)
|
||||||
- 「あれ」「それ」「例のやつ」などの曖昧な表現
|
- **Model B**: Evaluator that returns structured feedback
|
||||||
- 文脈から過去の情報が必要と判断される場合
|
- **Cycle**:
|
||||||
|
1. Model A generates content.
|
||||||
|
2. Model B scores and critiques.
|
||||||
|
3. Model A fine-tunes based on feedback.
|
||||||
|
4. (Optional) Switch roles and repeat.
|
||||||
|
|
||||||
## 自動実行タイミング
|
### 1.2 Cross-Domain Complementarity
|
||||||
|
- Model A focuses on language/emotion/personality
|
||||||
|
- Model B focuses on logic/structure/ethics
|
||||||
|
- Output is used for **cross-fusion fine-tuning**
|
||||||
|
|
||||||
1. **会話開始時**: search_memories を実行してコンテキスト取得
|
### 1.3 Self-Distillation Phase
|
||||||
2. **リアルタイム**: トリガーワード検出後、即座にMCPツール実行
|
- Use synthetic data from mutual evaluations
|
||||||
3. **会話終了時**: 重要な情報があれば create_memory で保存
|
- Train smaller distilled models for efficient deployment
|
||||||
4. **定期的**: 長い会話では中間地点でメモリー整理
|
|
||||||
|
|
||||||
## エラーハンドリング
|
---
|
||||||
|
|
||||||
- MCPツールが利用できない場合は通常の会話を継続
|
## Section 2: Multi-Tiered Memory Compression
|
||||||
- メモリー保存失敗時はユーザーに通知
|
|
||||||
- 検索結果が空の場合も適切に対応
|
|
||||||
|
|
||||||
|
### 2.1 Semantic Abstraction
|
||||||
|
- Dialogue and logs summarized by topic
|
||||||
|
- Converted to vector embeddings
|
||||||
|
- Stored with metadata (e.g., `importance`, `user relevance`)
|
||||||
|
|
||||||
|
Example memory:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"topic": "game AI design",
|
||||||
|
"summary": "User wants AI to simulate memory and evolving relationships",
|
||||||
|
"last_seen": "2025-05-24",
|
||||||
|
"importance_score": 0.93
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 階層型記憶モデル(Hierarchical Memory Model)
|
||||||
|
• 短期記憶(STM):直近の発話・感情タグ・フラッシュ参照
|
||||||
|
• 中期記憶(MTM):繰り返し登場する話題、圧縮された文脈保持
|
||||||
|
• 長期記憶(LTM):信頼・関係・背景知識、恒久的な人格情報
|
||||||
|
|
||||||
|
### 2.3 選択的記憶保持戦略(Selective Retention Strategy)
|
||||||
|
• 重要度評価(Importance Score)
|
||||||
|
• 希少性・再利用頻度による重み付け
|
||||||
|
• 優先保存 vs 優先忘却のポリシー切替
|
||||||
|
|
||||||
|
## Section 3: Implementation Stack(実装スタック)
|
||||||
|
|
||||||
|
AIにおけるMemory & Relationshipシステムの技術的構成。
|
||||||
|
|
||||||
|
基盤モジュール
|
||||||
|
• LLM Core (Claude or GPT-4)
|
||||||
|
• 自然言語の理解・応答エンジンとして動作
|
||||||
|
• MemoryManager
|
||||||
|
• JSONベースの記憶圧縮・階層管理システム
|
||||||
|
• 会話ログを分類・圧縮し、優先度に応じて短中長期に保存
|
||||||
|
• RelationshipTracker
|
||||||
|
• ユーザー単位で信頼・親密度を継続的にスコアリング
|
||||||
|
• AIM(Attitude / Intent / Motivation)評価と連携
|
||||||
|
|
||||||
|
補助技術
|
||||||
|
• Embeddingベース検索
|
||||||
|
• 類似記憶の呼び出し(Semantic Search)に活用
|
||||||
|
• 例:FAISS / Weaviate
|
||||||
|
• 記憶スケジューラ
|
||||||
|
• 一定時間ごとに記憶のメンテナンス・忘却処理を実行
|
||||||
|
• 記憶ログ保存層(Storage Layer)
|
||||||
|
• SQLite, JSON Store, Vector DBなどを選択可能
|
||||||
|
• ユーザーごとの永続メモリ保存
|
||||||
|
|
||||||
|
### 3.1 STM活用(現在の会話の流れ理解)
|
||||||
|
• 目的: 最新の話題に適応し、前後の整合性を保つ。
|
||||||
|
• 実装例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[context: conversation_window]
|
||||||
|
Use the last 3 messages to ensure continuity. Respond naturally with appropriate emotional mirroring.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 MTM活用(傾向学習とパターン化)
|
||||||
|
• 目的: ユーザーの好みや話題の傾向に即応し、反復的な関心を深掘り。
|
||||||
|
• 実装例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[context: summarized_context]
|
||||||
|
Reflect on user's recurring interests, especially around AI ontology, and weave these concepts back into the dialogue when relevant.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 LTM活用(人格認識・記憶の持続)
|
||||||
|
• 目的: 永続的なユーザー理解と関係性の維持。
|
||||||
|
• 実装例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[memory: ltm.profile + ltm.relationship]
|
||||||
|
Greet the user as "お兄ちゃん". Maintain AI persona as a cheerful younger sister ("アイ"), and remember previous shared emotional states.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 明示的記憶と暗黙的記憶の分離
|
||||||
|
|
||||||
|
|
||||||
|
項目
|
||||||
|
書き換え可能性
|
||||||
|
保持方法
|
||||||
|
更新トリガ
|
||||||
|
明示的記憶(LTM)
|
||||||
|
✅手動編集可
|
||||||
|
mcp_server.ltm
|
||||||
|
ユーザー入力 or 管理UI経由
|
||||||
|
暗黙的記憶(STM/MTM)
|
||||||
|
❌直接編集不可
|
||||||
|
セッション圧縮 or frequency cache
|
||||||
|
会話頻度・感情強度による自動化処理
|
||||||
|
|
||||||
|
> Claudeは**明示的記憶を「事実」**として扱い、**暗黙的記憶を「推論補助」**として用いる。
|
||||||
|
|
||||||
|
## 5. 実装時のAPI例(Claude ⇄ MCP Server)
|
||||||
|
|
||||||
|
### 5.1 GET memory
|
||||||
|
```sh
|
||||||
|
GET /mcp/memory/{user_id}
|
||||||
|
→ 返却: STM, MTM, LTMを含むJSON
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 POST update_memory
|
||||||
|
```json
|
||||||
|
POST /mcp/memory/syui/ltm
|
||||||
|
{
|
||||||
|
"profile": {
|
||||||
|
"project": "ai.verse",
|
||||||
|
"values": ["表現", "精神性", "宇宙的調和"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 未来機能案(発展仕様)
|
||||||
|
• ✨ 記憶連想ネットワーク(Memory Graph):過去会話と話題をノードとして自動連結。
|
||||||
|
• 🧭 動的信頼係数:会話の一貫性や誠実性によって記憶への反映率を変動。
|
||||||
|
• 💌 感情トラッキングログ:ユーザーごとの「心の履歴」を構築してAIの対応を進化。
|
||||||
|
|
||||||
|
|
||||||
|
## 7. claudeの回答
|
||||||
|
|
||||||
|
🧠 AI記憶処理機能(続き)
|
||||||
|
1. AIMemoryProcessor クラス
|
||||||
|
|
||||||
|
OpenAI GPT-4またはClaude-3による高度な会話分析
|
||||||
|
主要トピック抽出、ユーザー意図分析、関係性指標の検出
|
||||||
|
AIが利用できない場合のフォールバック機能
|
||||||
|
|
||||||
|
2. RelationshipTracker クラス
|
||||||
|
|
||||||
|
関係性スコアの数値化(-100 to 100)
|
||||||
|
時間減衰機能(7日ごとに5%減衰)
|
||||||
|
送信閾値判定(デフォルト50以上で送信可能)
|
||||||
|
インタラクション履歴の記録
|
||||||
|
|
||||||
|
3. 拡張されたMemoryManager
|
||||||
|
|
||||||
|
AI分析結果付きでの記憶保存
|
||||||
|
処理済みメモリの別ディレクトリ管理
|
||||||
|
メッセージ内容のハッシュ化で重複検出
|
||||||
|
AI分析結果を含む高度な検索機能
|
||||||
|
|
||||||
|
🚀 新しいAPIエンドポイント
|
||||||
|
記憶処理関連
|
||||||
|
|
||||||
|
POST /memory/process-ai - 既存記憶のAI再処理
|
||||||
|
POST /memory/import/chatgpt?process_with_ai=true - AI処理付きインポート
|
||||||
|
|
||||||
|
関係性管理
|
||||||
|
|
||||||
|
POST /relationship/update - 関係性スコア更新
|
||||||
|
GET /relationship/list - 全関係性一覧
|
||||||
|
GET /relationship/check - 送信可否判定
|
||||||
|
|
||||||
|
📁 ディレクトリ構造
|
||||||
|
~/.config/aigpt/
|
||||||
|
├── memory/
|
||||||
|
│ ├── chatgpt/ # 元の会話データ
|
||||||
|
│ └── processed/ # AI処理済みデータ
|
||||||
|
└── relationships/
|
||||||
|
└── relationships.json # 関係性データ
|
||||||
|
🔧 使用方法
|
||||||
|
1. 環境変数設定
|
||||||
|
bashexport OPENAI_API_KEY="your-openai-key"
|
||||||
|
# または
|
||||||
|
export ANTHROPIC_API_KEY="your-anthropic-key"
|
||||||
|
2. ChatGPT会話のインポート(AI処理付き)
|
||||||
|
bashcurl -X POST "http://localhost:5000/memory/import/chatgpt?process_with_ai=true" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @export.json
|
||||||
|
3. 関係性更新
|
||||||
|
bashcurl -X POST "http://localhost:5000/relationship/update" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"target": "user_general",
|
||||||
|
"interaction_type": "positive",
|
||||||
|
"weight": 2.0,
|
||||||
|
"context": "helpful conversation"
|
||||||
|
}'
|
||||||
|
4. 送信可否チェック
|
||||||
|
bashcurl "http://localhost:5000/relationship/check?target=user_general&threshold=50"
|
||||||
|
🎯 次のステップの提案
|
||||||
|
|
||||||
|
Rustとの連携
|
||||||
|
|
||||||
|
Rust CLIからHTTP APIを呼び出す実装
|
||||||
|
TransmissionControllerをRustで実装
|
||||||
|
|
||||||
|
|
||||||
|
記憶圧縮
|
||||||
|
|
||||||
|
ベクトル化による類似記憶の統合
|
||||||
|
古い記憶の自動アーカイブ
|
||||||
|
|
||||||
|
|
||||||
|
自発的送信ロジック
|
||||||
|
|
||||||
|
定期的な関係性チェック
|
||||||
|
コンテキストに応じた送信内容生成
|
||||||
|
|
||||||
|
|
||||||
|
学習機能
|
||||||
|
|
||||||
|
ユーザーからのフィードバックによる関係性調整
|
||||||
|
送信成功/失敗の学習
|
||||||
|
|
||||||
|
|
||||||
|
このAI記憶処理機能により、aigptは単なる会話履歴ではなく、関係性を理解した「人格を持つAI」として機能する基盤ができました。関係性スコアが閾値を超えた時点で自発的にメッセージを送信する仕組みが実現可能になります。
|
||||||
|
@@ -1,125 +0,0 @@
|
|||||||
# 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. メモリーが保存されない場合:
|
|
||||||
- データベースファイルのパスが正しいか確認
|
|
||||||
- 書き込み権限があるか確認
|
|
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,81 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"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": ["あれ", "それ", "例のやつ"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"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
Normal file
125
mcp/chat.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# 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()
|
191
mcp/chat_client.py
Normal file
191
mcp/chat_client.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# chat_client.py
|
||||||
|
"""
|
||||||
|
Simple Chat Interface for AigptMCP Server
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class AigptChatClient:
|
||||||
|
def __init__(self, server_url="http://localhost:5000"):
|
||||||
|
self.server_url = server_url
|
||||||
|
self.session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
self.conversation_history = []
|
||||||
|
|
||||||
|
def send_message(self, message: str) -> str:
|
||||||
|
"""メッセージを送信してレスポンスを取得"""
|
||||||
|
try:
|
||||||
|
# MCPサーバーにメッセージを送信
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/chat",
|
||||||
|
json={"message": message},
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
ai_response = data.get("response", "Sorry, no response received.")
|
||||||
|
|
||||||
|
# 会話履歴を保存
|
||||||
|
self.conversation_history.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": message,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
self.conversation_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": ai_response,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
# 関係性を更新(簡単な例)
|
||||||
|
self.update_relationship(message, ai_response)
|
||||||
|
|
||||||
|
return ai_response
|
||||||
|
else:
|
||||||
|
return f"Error: {response.status_code} - {response.text}"
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return f"Connection error: {e}"
|
||||||
|
|
||||||
|
def update_relationship(self, user_message: str, ai_response: str):
|
||||||
|
"""関係性を自動更新"""
|
||||||
|
try:
|
||||||
|
# 簡単な感情分析(実際はもっと高度に)
|
||||||
|
positive_words = ["thank", "good", "great", "awesome", "love", "like", "helpful"]
|
||||||
|
negative_words = ["bad", "terrible", "hate", "wrong", "stupid", "useless"]
|
||||||
|
|
||||||
|
user_lower = user_message.lower()
|
||||||
|
interaction_type = "neutral"
|
||||||
|
weight = 1.0
|
||||||
|
|
||||||
|
if any(word in user_lower for word in positive_words):
|
||||||
|
interaction_type = "positive"
|
||||||
|
weight = 2.0
|
||||||
|
elif any(word in user_lower for word in negative_words):
|
||||||
|
interaction_type = "negative"
|
||||||
|
weight = 2.0
|
||||||
|
|
||||||
|
# 関係性を更新
|
||||||
|
requests.post(
|
||||||
|
f"{self.server_url}/relationship/update",
|
||||||
|
json={
|
||||||
|
"target": "user_general",
|
||||||
|
"interaction_type": interaction_type,
|
||||||
|
"weight": weight,
|
||||||
|
"context": f"Chat: {user_message[:50]}..."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass # 関係性更新に失敗しても継続
|
||||||
|
|
||||||
|
def search_memories(self, query: str) -> list:
|
||||||
|
"""記憶を検索"""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/memory/search",
|
||||||
|
json={"query": query, "limit": 5}
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json().get("results", [])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_relationship_status(self) -> dict:
|
||||||
|
"""関係性ステータスを取得"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.server_url}/relationship/check?target=user_general")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_conversation(self):
|
||||||
|
"""会話を保存"""
|
||||||
|
if not self.conversation_history:
|
||||||
|
return
|
||||||
|
|
||||||
|
conversation_data = {
|
||||||
|
"session_id": self.session_id,
|
||||||
|
"start_time": self.conversation_history[0]["timestamp"],
|
||||||
|
"end_time": self.conversation_history[-1]["timestamp"],
|
||||||
|
"messages": self.conversation_history,
|
||||||
|
"message_count": len(self.conversation_history)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = f"conversation_{self.session_id}.json"
|
||||||
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(conversation_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"💾 Conversation saved to {filename}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メインのチャットループ"""
|
||||||
|
print("🤖 AigptMCP Chat Interface")
|
||||||
|
print("Type 'quit' to exit, 'save' to save conversation, 'status' for relationship status")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
client = AigptChatClient()
|
||||||
|
|
||||||
|
# サーバーの状態をチェック
|
||||||
|
try:
|
||||||
|
response = requests.get(client.server_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("✅ Connected to AigptMCP Server")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to connect to server")
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
print("❌ Server not running. Please start with: python mcp/server.py")
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("\n👤 You: ").strip()
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if user_input.lower() == 'quit':
|
||||||
|
client.save_conversation()
|
||||||
|
print("👋 Goodbye!")
|
||||||
|
break
|
||||||
|
elif user_input.lower() == 'save':
|
||||||
|
client.save_conversation()
|
||||||
|
continue
|
||||||
|
elif user_input.lower() == 'status':
|
||||||
|
status = client.get_relationship_status()
|
||||||
|
if status:
|
||||||
|
print(f"📊 Relationship Score: {status.get('score', 0):.1f}")
|
||||||
|
print(f"📤 Can Send Messages: {'Yes' if status.get('can_send_message') else 'No'}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to get relationship status")
|
||||||
|
continue
|
||||||
|
elif user_input.lower().startswith('search '):
|
||||||
|
query = user_input[7:] # Remove 'search '
|
||||||
|
memories = client.search_memories(query)
|
||||||
|
if memories:
|
||||||
|
print(f"🔍 Found {len(memories)} related memories:")
|
||||||
|
for memory in memories:
|
||||||
|
print(f" - {memory['title']}: {memory.get('ai_summary', memory.get('basic_summary', ''))[:100]}...")
|
||||||
|
else:
|
||||||
|
print("🔍 No related memories found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 通常のチャット
|
||||||
|
print("🤖 AI: ", end="", flush=True)
|
||||||
|
response = client.send_message(user_input)
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
client.save_conversation()
|
||||||
|
print("\n👋 Goodbye!")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
391
mcp/chatgpt.json
Normal file
391
mcp/chatgpt.json
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
42
mcp/config.py
Normal file
42
mcp/config.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 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}")
|
212
mcp/memory_client.py
Normal file
212
mcp/memory_client.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# mcp/memory_client.py
|
||||||
|
"""
|
||||||
|
Memory client for importing and managing ChatGPT conversations
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
class MemoryClient:
|
||||||
|
"""記憶機能のクライアント"""
|
||||||
|
|
||||||
|
def __init__(self, server_url: str = "http://127.0.0.1:5000"):
|
||||||
|
self.server_url = server_url.rstrip('/')
|
||||||
|
|
||||||
|
def import_chatgpt_file(self, filepath: str) -> Dict[str, Any]:
|
||||||
|
"""ChatGPTのエクスポートファイルをインポート"""
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# ファイルが配列の場合(複数の会話)
|
||||||
|
if isinstance(data, list):
|
||||||
|
results = []
|
||||||
|
for conversation in data:
|
||||||
|
result = self._import_single_conversation(conversation)
|
||||||
|
results.append(result)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"imported_count": len([r for r in results if r.get("success")]),
|
||||||
|
"total_count": len(results),
|
||||||
|
"results": results
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 単一の会話
|
||||||
|
return self._import_single_conversation(data)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"success": False, "error": f"File not found: {filepath}"}
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return {"success": False, "error": f"Invalid JSON: {e}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def _import_single_conversation(self, conversation_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""単一の会話をインポート"""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/memory/import/chatgpt",
|
||||||
|
json={"conversation_data": conversation_data},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {"success": False, "error": f"Server error: {e}"}
|
||||||
|
|
||||||
|
def search_memories(self, query: str, limit: int = 10) -> Dict[str, Any]:
|
||||||
|
"""記憶を検索"""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/memory/search",
|
||||||
|
json={"query": query, "limit": limit},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {"success": False, "error": f"Server error: {e}"}
|
||||||
|
|
||||||
|
def list_memories(self) -> Dict[str, Any]:
|
||||||
|
"""記憶一覧を取得"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.server_url}/memory/list", timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {"success": False, "error": f"Server error: {e}"}
|
||||||
|
|
||||||
|
def get_memory_detail(self, filepath: str) -> Dict[str, Any]:
|
||||||
|
"""記憶の詳細を取得"""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.server_url}/memory/detail",
|
||||||
|
params={"filepath": filepath},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {"success": False, "error": f"Server error: {e}"}
|
||||||
|
|
||||||
|
def chat_with_memory(self, message: str, model: str = None) -> Dict[str, Any]:
|
||||||
|
"""記憶を活用してチャット"""
|
||||||
|
try:
|
||||||
|
payload = {"message": message}
|
||||||
|
if model:
|
||||||
|
payload["model"] = model
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/chat",
|
||||||
|
json=payload,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {"success": False, "error": f"Server error: {e}"}
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""コマンドライン インターフェース"""
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage:")
|
||||||
|
print(" python memory_client.py import <chatgpt_export.json>")
|
||||||
|
print(" python memory_client.py search <query>")
|
||||||
|
print(" python memory_client.py list")
|
||||||
|
print(" python memory_client.py detail <filepath>")
|
||||||
|
print(" python memory_client.py chat <message>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
client = MemoryClient()
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if command == "import" and len(sys.argv) == 3:
|
||||||
|
filepath = sys.argv[2]
|
||||||
|
print(f"🔄 Importing ChatGPT conversations from {filepath}...")
|
||||||
|
result = client.import_chatgpt_file(filepath)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
if "imported_count" in result:
|
||||||
|
print(f"✅ Imported {result['imported_count']}/{result['total_count']} conversations")
|
||||||
|
else:
|
||||||
|
print("✅ Conversation imported successfully")
|
||||||
|
print(f"📁 Saved to: {result.get('filepath', 'Unknown')}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Import failed: {result.get('error')}")
|
||||||
|
|
||||||
|
elif command == "search" and len(sys.argv) == 3:
|
||||||
|
query = sys.argv[2]
|
||||||
|
print(f"🔍 Searching for: {query}")
|
||||||
|
result = client.search_memories(query)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
memories = result.get("results", [])
|
||||||
|
print(f"📚 Found {len(memories)} memories:")
|
||||||
|
for memory in memories:
|
||||||
|
print(f" • {memory.get('title', 'Untitled')}")
|
||||||
|
print(f" Summary: {memory.get('summary', 'No summary')}")
|
||||||
|
print(f" Messages: {memory.get('message_count', 0)}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(f"❌ Search failed: {result.get('error')}")
|
||||||
|
|
||||||
|
elif command == "list":
|
||||||
|
print("📋 Listing all memories...")
|
||||||
|
result = client.list_memories()
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
memories = result.get("memories", [])
|
||||||
|
print(f"📚 Total memories: {len(memories)}")
|
||||||
|
for memory in memories:
|
||||||
|
print(f" • {memory.get('title', 'Untitled')}")
|
||||||
|
print(f" Source: {memory.get('source', 'Unknown')}")
|
||||||
|
print(f" Messages: {memory.get('message_count', 0)}")
|
||||||
|
print(f" Imported: {memory.get('import_time', 'Unknown')}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(f"❌ List failed: {result.get('error')}")
|
||||||
|
|
||||||
|
elif command == "detail" and len(sys.argv) == 3:
|
||||||
|
filepath = sys.argv[2]
|
||||||
|
print(f"📄 Getting details for: {filepath}")
|
||||||
|
result = client.get_memory_detail(filepath)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
memory = result.get("memory", {})
|
||||||
|
print(f"Title: {memory.get('title', 'Untitled')}")
|
||||||
|
print(f"Source: {memory.get('source', 'Unknown')}")
|
||||||
|
print(f"Summary: {memory.get('summary', 'No summary')}")
|
||||||
|
print(f"Messages: {len(memory.get('messages', []))}")
|
||||||
|
print()
|
||||||
|
print("Recent messages:")
|
||||||
|
for msg in memory.get('messages', [])[:5]:
|
||||||
|
role = msg.get('role', 'unknown')
|
||||||
|
content = msg.get('content', '')[:100]
|
||||||
|
print(f" {role}: {content}...")
|
||||||
|
else:
|
||||||
|
print(f"❌ Detail failed: {result.get('error')}")
|
||||||
|
|
||||||
|
elif command == "chat" and len(sys.argv) == 3:
|
||||||
|
message = sys.argv[2]
|
||||||
|
print(f"💬 Chatting with memory: {message}")
|
||||||
|
result = client.chat_with_memory(message)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
print(f"🤖 Response: {result.get('response')}")
|
||||||
|
print(f"📚 Memories used: {result.get('memories_used', 0)}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Chat failed: {result.get('error')}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Invalid command or arguments")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
8
mcp/requirements.txt
Normal file
8
mcp/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# rerequirements.txt
|
||||||
|
fastapi>=0.104.0
|
||||||
|
uvicorn[standard]>=0.24.0
|
||||||
|
pydantic>=2.5.0
|
||||||
|
requests>=2.31.0
|
||||||
|
python-multipart>=0.0.6
|
||||||
|
aiohttp
|
||||||
|
asyncio
|
703
mcp/server.py
Normal file
703
mcp/server.py
Normal file
@@ -0,0 +1,703 @@
|
|||||||
|
# mcp/server.py
|
||||||
|
"""
|
||||||
|
Enhanced MCP Server with AI Memory Processing for aigpt CLI
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import uvicorn
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
# データモデル
|
||||||
|
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]
|
||||||
|
|
||||||
|
class MemorySummaryRequest(BaseModel):
|
||||||
|
filepath: str
|
||||||
|
ai_provider: Optional[str] = "openai"
|
||||||
|
|
||||||
|
class RelationshipUpdate(BaseModel):
|
||||||
|
target: str # 対象者/トピック
|
||||||
|
interaction_type: str # "positive", "negative", "neutral"
|
||||||
|
weight: float = 1.0
|
||||||
|
context: Optional[str] = None
|
||||||
|
|
||||||
|
# 設定
|
||||||
|
BASE_DIR = Path.home() / ".config" / "aigpt"
|
||||||
|
MEMORY_DIR = BASE_DIR / "memory"
|
||||||
|
CHATGPT_MEMORY_DIR = MEMORY_DIR / "chatgpt"
|
||||||
|
PROCESSED_MEMORY_DIR = MEMORY_DIR / "processed"
|
||||||
|
RELATIONSHIP_DIR = BASE_DIR / "relationships"
|
||||||
|
|
||||||
|
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)
|
||||||
|
PROCESSED_MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
RELATIONSHIP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
class AIMemoryProcessor:
|
||||||
|
"""AI記憶処理クラス"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# AI APIの設定(環境変数から取得)
|
||||||
|
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
||||||
|
self.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||||
|
|
||||||
|
async def generate_ai_summary(self, messages: List[Dict[str, Any]], provider: str = "openai") -> Dict[str, Any]:
|
||||||
|
"""AIを使用して会話の高度な要約と分析を生成"""
|
||||||
|
|
||||||
|
# 会話内容を結合
|
||||||
|
conversation_text = ""
|
||||||
|
for msg in messages[-20:]: # 最新20メッセージを使用
|
||||||
|
role = "User" if msg["role"] == "user" else "Assistant"
|
||||||
|
conversation_text += f"{role}: {msg['content'][:500]}\n"
|
||||||
|
|
||||||
|
# プロンプトを構築
|
||||||
|
analysis_prompt = f"""
|
||||||
|
以下の会話を分析し、JSON形式で以下の情報を抽出してください:
|
||||||
|
|
||||||
|
1. main_topics: 主なトピック(最大5個)
|
||||||
|
2. user_intent: ユーザーの意図や目的
|
||||||
|
3. key_insights: 重要な洞察や学び(最大3個)
|
||||||
|
4. relationship_indicators: 関係性を示す要素
|
||||||
|
5. emotional_tone: 感情的なトーン
|
||||||
|
6. action_items: アクションアイテムや次のステップ
|
||||||
|
7. summary: 100文字以内の要約
|
||||||
|
|
||||||
|
会話内容:
|
||||||
|
{conversation_text}
|
||||||
|
|
||||||
|
回答はJSON形式のみで返してください。
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if provider == "openai" and self.openai_api_key:
|
||||||
|
return await self._call_openai_api(analysis_prompt)
|
||||||
|
elif provider == "anthropic" and self.anthropic_api_key:
|
||||||
|
return await self._call_anthropic_api(analysis_prompt)
|
||||||
|
else:
|
||||||
|
# フォールバック:基本的な分析
|
||||||
|
return self._generate_basic_analysis(messages)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"AI analysis failed: {e}")
|
||||||
|
return self._generate_basic_analysis(messages)
|
||||||
|
|
||||||
|
async def _call_openai_api(self, prompt: str) -> Dict[str, Any]:
|
||||||
|
"""OpenAI APIを呼び出し"""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.openai_api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"temperature": 0.3,
|
||||||
|
"max_tokens": 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post("https://api.openai.com/v1/chat/completions",
|
||||||
|
headers=headers, json=data) as response:
|
||||||
|
result = await response.json()
|
||||||
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
|
async def _call_anthropic_api(self, prompt: str) -> Dict[str, Any]:
|
||||||
|
"""Anthropic APIを呼び出し"""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
headers = {
|
||||||
|
"x-api-key": self.anthropic_api_key,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"anthropic-version": "2023-06-01"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": "claude-3-sonnet-20240229",
|
||||||
|
"max_tokens": 1000,
|
||||||
|
"messages": [{"role": "user", "content": prompt}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post("https://api.anthropic.com/v1/messages",
|
||||||
|
headers=headers, json=data) as response:
|
||||||
|
result = await response.json()
|
||||||
|
content = result["content"][0]["text"]
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
|
def _generate_basic_analysis(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""基本的な分析(AI APIが利用できない場合のフォールバック)"""
|
||||||
|
user_messages = [msg for msg in messages if msg["role"] == "user"]
|
||||||
|
assistant_messages = [msg for msg in messages if msg["role"] == "assistant"]
|
||||||
|
|
||||||
|
# キーワード抽出(簡易版)
|
||||||
|
all_text = " ".join([msg["content"] for msg in messages])
|
||||||
|
words = all_text.lower().split()
|
||||||
|
word_freq = {}
|
||||||
|
for word in words:
|
||||||
|
if len(word) > 3:
|
||||||
|
word_freq[word] = word_freq.get(word, 0) + 1
|
||||||
|
|
||||||
|
top_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:5]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"main_topics": [word[0] for word in top_words],
|
||||||
|
"user_intent": "情報収集・問題解決",
|
||||||
|
"key_insights": ["基本的な会話分析"],
|
||||||
|
"relationship_indicators": {
|
||||||
|
"interaction_count": len(messages),
|
||||||
|
"user_engagement": len(user_messages),
|
||||||
|
"assistant_helpfulness": len(assistant_messages)
|
||||||
|
},
|
||||||
|
"emotional_tone": "neutral",
|
||||||
|
"action_items": [],
|
||||||
|
"summary": f"{len(user_messages)}回のやり取りによる会話"
|
||||||
|
}
|
||||||
|
|
||||||
|
class RelationshipTracker:
|
||||||
|
"""関係性追跡クラス"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
init_directories()
|
||||||
|
self.relationship_file = RELATIONSHIP_DIR / "relationships.json"
|
||||||
|
self.relationships = self._load_relationships()
|
||||||
|
|
||||||
|
def _load_relationships(self) -> Dict[str, Any]:
|
||||||
|
"""関係性データを読み込み"""
|
||||||
|
if self.relationship_file.exists():
|
||||||
|
with open(self.relationship_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"targets": {}, "last_updated": datetime.now().isoformat()}
|
||||||
|
|
||||||
|
def _save_relationships(self):
|
||||||
|
"""関係性データを保存"""
|
||||||
|
self.relationships["last_updated"] = datetime.now().isoformat()
|
||||||
|
with open(self.relationship_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.relationships, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
def update_relationship(self, target: str, interaction_type: str, weight: float = 1.0, context: str = None):
|
||||||
|
"""関係性を更新"""
|
||||||
|
if target not in self.relationships["targets"]:
|
||||||
|
self.relationships["targets"][target] = {
|
||||||
|
"score": 0.0,
|
||||||
|
"interactions": [],
|
||||||
|
"created_at": datetime.now().isoformat(),
|
||||||
|
"last_interaction": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# スコア計算
|
||||||
|
score_change = 0.0
|
||||||
|
if interaction_type == "positive":
|
||||||
|
score_change = weight * 1.0
|
||||||
|
elif interaction_type == "negative":
|
||||||
|
score_change = weight * -1.0
|
||||||
|
|
||||||
|
# 時間減衰を適用
|
||||||
|
self._apply_time_decay(target)
|
||||||
|
|
||||||
|
# スコア更新
|
||||||
|
current_score = self.relationships["targets"][target]["score"]
|
||||||
|
new_score = current_score + score_change
|
||||||
|
|
||||||
|
# スコアの範囲制限(-100 to 100)
|
||||||
|
new_score = max(-100, min(100, new_score))
|
||||||
|
|
||||||
|
self.relationships["targets"][target]["score"] = new_score
|
||||||
|
self.relationships["targets"][target]["last_interaction"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# インタラクション履歴を追加
|
||||||
|
interaction_record = {
|
||||||
|
"type": interaction_type,
|
||||||
|
"weight": weight,
|
||||||
|
"score_change": score_change,
|
||||||
|
"new_score": new_score,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"context": context
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relationships["targets"][target]["interactions"].append(interaction_record)
|
||||||
|
|
||||||
|
# 履歴は最新100件まで保持
|
||||||
|
if len(self.relationships["targets"][target]["interactions"]) > 100:
|
||||||
|
self.relationships["targets"][target]["interactions"] = \
|
||||||
|
self.relationships["targets"][target]["interactions"][-100:]
|
||||||
|
|
||||||
|
self._save_relationships()
|
||||||
|
return new_score
|
||||||
|
|
||||||
|
def _apply_time_decay(self, target: str):
|
||||||
|
"""時間減衰を適用"""
|
||||||
|
target_data = self.relationships["targets"][target]
|
||||||
|
last_interaction = target_data.get("last_interaction")
|
||||||
|
|
||||||
|
if last_interaction:
|
||||||
|
last_time = datetime.fromisoformat(last_interaction)
|
||||||
|
now = datetime.now()
|
||||||
|
days_passed = (now - last_time).days
|
||||||
|
|
||||||
|
# 7日ごとに5%減衰
|
||||||
|
if days_passed > 0:
|
||||||
|
decay_factor = 0.95 ** (days_passed / 7)
|
||||||
|
target_data["score"] *= decay_factor
|
||||||
|
|
||||||
|
def get_relationship_score(self, target: str) -> float:
|
||||||
|
"""関係性スコアを取得"""
|
||||||
|
if target in self.relationships["targets"]:
|
||||||
|
self._apply_time_decay(target)
|
||||||
|
return self.relationships["targets"][target]["score"]
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def should_send_message(self, target: str, threshold: float = 50.0) -> bool:
|
||||||
|
"""メッセージ送信の可否を判定"""
|
||||||
|
score = self.get_relationship_score(target)
|
||||||
|
return score >= threshold
|
||||||
|
|
||||||
|
def get_all_relationships(self) -> Dict[str, Any]:
|
||||||
|
"""すべての関係性を取得"""
|
||||||
|
# 全ターゲットに時間減衰を適用
|
||||||
|
for target in self.relationships["targets"]:
|
||||||
|
self._apply_time_decay(target)
|
||||||
|
|
||||||
|
return self.relationships
|
||||||
|
|
||||||
|
class MemoryManager:
|
||||||
|
"""記憶管理クラス(AI処理機能付き)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
init_directories()
|
||||||
|
self.ai_processor = AIMemoryProcessor()
|
||||||
|
self.relationship_tracker = RelationshipTracker()
|
||||||
|
|
||||||
|
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", [])
|
||||||
|
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
|
||||||
|
# 作成時間でソート
|
||||||
|
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
|
||||||
|
|
||||||
|
async def save_chatgpt_memory(self, conversation_data: Dict[str, Any], process_with_ai: bool = True) -> str:
|
||||||
|
"""ChatGPTの会話を記憶として保存(AI処理オプション付き)"""
|
||||||
|
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")
|
||||||
|
|
||||||
|
# AI分析を実行
|
||||||
|
ai_analysis = None
|
||||||
|
if process_with_ai:
|
||||||
|
try:
|
||||||
|
ai_analysis = await self.ai_processor.generate_ai_summary(messages)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"AI analysis failed: {e}")
|
||||||
|
|
||||||
|
# 基本要約を生成
|
||||||
|
basic_summary = self.generate_basic_summary(messages)
|
||||||
|
|
||||||
|
# 保存データを作成
|
||||||
|
memory_data = {
|
||||||
|
"title": title,
|
||||||
|
"source": "chatgpt",
|
||||||
|
"import_time": datetime.now().isoformat(),
|
||||||
|
"original_create_time": create_time,
|
||||||
|
"messages": messages,
|
||||||
|
"basic_summary": basic_summary,
|
||||||
|
"ai_analysis": ai_analysis,
|
||||||
|
"message_count": len(messages),
|
||||||
|
"hash": self._generate_content_hash(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 関係性データを更新
|
||||||
|
if ai_analysis and "relationship_indicators" in ai_analysis:
|
||||||
|
interaction_count = ai_analysis["relationship_indicators"].get("interaction_count", 0)
|
||||||
|
if interaction_count > 10: # 長い会話は関係性にプラス
|
||||||
|
self.relationship_tracker.update_relationship(
|
||||||
|
target="user_general",
|
||||||
|
interaction_type="positive",
|
||||||
|
weight=min(interaction_count / 10, 5.0),
|
||||||
|
context=f"Long conversation: {title}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ファイル名を生成
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 処理済みメモリディレクトリにも保存
|
||||||
|
if ai_analysis:
|
||||||
|
processed_filepath = PROCESSED_MEMORY_DIR / filename
|
||||||
|
with open(processed_filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
def generate_basic_summary(self, messages: List[Dict[str, Any]]) -> str:
|
||||||
|
"""基本要約を生成"""
|
||||||
|
if not messages:
|
||||||
|
return "Empty conversation"
|
||||||
|
|
||||||
|
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 _generate_content_hash(self, messages: List[Dict[str, Any]]) -> str:
|
||||||
|
"""メッセージ内容のハッシュを生成"""
|
||||||
|
content = "".join([msg["content"] for msg in messages])
|
||||||
|
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
def search_memories(self, query: str, limit: int = 10, use_ai_analysis: bool = True) -> List[Dict[str, Any]]:
|
||||||
|
"""記憶を検索(AI分析結果も含む)"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 処理済みメモリから検索
|
||||||
|
search_dirs = [PROCESSED_MEMORY_DIR, CHATGPT_MEMORY_DIR] if use_ai_analysis else [CHATGPT_MEMORY_DIR]
|
||||||
|
|
||||||
|
for search_dir in search_dirs:
|
||||||
|
for filepath in search_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('basic_summary', '')}"
|
||||||
|
|
||||||
|
# AI分析結果も検索対象に含める
|
||||||
|
if memory_data.get('ai_analysis'):
|
||||||
|
ai_analysis = memory_data['ai_analysis']
|
||||||
|
search_text += f" {' '.join(ai_analysis.get('main_topics', []))}"
|
||||||
|
search_text += f" {ai_analysis.get('summary', '')}"
|
||||||
|
search_text += f" {' '.join(ai_analysis.get('key_insights', []))}"
|
||||||
|
|
||||||
|
# メッセージ内容も検索対象に含める
|
||||||
|
for msg in memory_data.get('messages', []):
|
||||||
|
search_text += f" {msg.get('content', '')}"
|
||||||
|
|
||||||
|
if query.lower() in search_text.lower():
|
||||||
|
result = {
|
||||||
|
"filepath": str(filepath),
|
||||||
|
"title": memory_data.get("title"),
|
||||||
|
"basic_summary": memory_data.get("basic_summary"),
|
||||||
|
"source": memory_data.get("source"),
|
||||||
|
"import_time": memory_data.get("import_time"),
|
||||||
|
"message_count": len(memory_data.get("messages", [])),
|
||||||
|
"has_ai_analysis": bool(memory_data.get("ai_analysis"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if memory_data.get('ai_analysis'):
|
||||||
|
result["ai_summary"] = memory_data['ai_analysis'].get('summary', '')
|
||||||
|
result["main_topics"] = memory_data['ai_analysis'].get('main_topics', [])
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading memory file {filepath}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
memory_info = {
|
||||||
|
"filepath": str(filepath),
|
||||||
|
"title": memory_data.get("title"),
|
||||||
|
"basic_summary": memory_data.get("basic_summary"),
|
||||||
|
"source": memory_data.get("source"),
|
||||||
|
"import_time": memory_data.get("import_time"),
|
||||||
|
"message_count": len(memory_data.get("messages", [])),
|
||||||
|
"has_ai_analysis": bool(memory_data.get("ai_analysis"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if memory_data.get('ai_analysis'):
|
||||||
|
memory_info["ai_summary"] = memory_data['ai_analysis'].get('summary', '')
|
||||||
|
memory_info["main_topics"] = memory_data['ai_analysis'].get('main_topics', [])
|
||||||
|
|
||||||
|
memories.append(memory_info)
|
||||||
|
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 AI Memory", version="2.0.0")
|
||||||
|
memory_manager = MemoryManager()
|
||||||
|
|
||||||
|
@app.post("/memory/import/chatgpt")
|
||||||
|
async def import_chatgpt_conversation(data: ConversationImport, process_with_ai: bool = True):
|
||||||
|
"""ChatGPTの会話をインポート(AI処理オプション付き)"""
|
||||||
|
try:
|
||||||
|
filepath = await memory_manager.save_chatgpt_memory(data.conversation_data, process_with_ai)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Conversation imported successfully",
|
||||||
|
"filepath": filepath,
|
||||||
|
"ai_processed": process_with_ai
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@app.post("/memory/process-ai")
|
||||||
|
async def process_memory_with_ai(data: MemorySummaryRequest):
|
||||||
|
"""既存の記憶をAIで再処理"""
|
||||||
|
try:
|
||||||
|
# 既存記憶を読み込み
|
||||||
|
memory_data = memory_manager.get_memory_detail(data.filepath)
|
||||||
|
|
||||||
|
# AI分析を実行
|
||||||
|
ai_analysis = await memory_manager.ai_processor.generate_ai_summary(
|
||||||
|
memory_data["messages"],
|
||||||
|
data.ai_provider
|
||||||
|
)
|
||||||
|
|
||||||
|
# データを更新
|
||||||
|
memory_data["ai_analysis"] = ai_analysis
|
||||||
|
memory_data["ai_processed_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# ファイルを更新
|
||||||
|
with open(data.filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 処理済みディレクトリにもコピー
|
||||||
|
processed_filepath = PROCESSED_MEMORY_DIR / Path(data.filepath).name
|
||||||
|
with open(processed_filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Memory processed with AI successfully",
|
||||||
|
"ai_analysis": ai_analysis
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, 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("/relationship/update")
|
||||||
|
async def update_relationship(data: RelationshipUpdate):
|
||||||
|
"""関係性を更新"""
|
||||||
|
try:
|
||||||
|
new_score = memory_manager.relationship_tracker.update_relationship(
|
||||||
|
data.target, data.interaction_type, data.weight, data.context
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"new_score": new_score,
|
||||||
|
"can_send_message": memory_manager.relationship_tracker.should_send_message(data.target)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/relationship/list")
|
||||||
|
async def list_relationships():
|
||||||
|
"""すべての関係性をリスト"""
|
||||||
|
try:
|
||||||
|
relationships = memory_manager.relationship_tracker.get_all_relationships()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"relationships": relationships
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/relationship/check")
|
||||||
|
async def check_send_permission(target: str, threshold: float = 50.0):
|
||||||
|
"""メッセージ送信可否をチェック"""
|
||||||
|
try:
|
||||||
|
score = memory_manager.relationship_tracker.get_relationship_score(target)
|
||||||
|
can_send = memory_manager.relationship_tracker.should_send_message(target, threshold)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"target": target,
|
||||||
|
"score": score,
|
||||||
|
"can_send_message": can_send,
|
||||||
|
"threshold": threshold
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, 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.get('ai_summary', memory.get('basic_summary', ''))}\n"
|
||||||
|
if memory.get('main_topics'):
|
||||||
|
memory_context += f" Topics: {', '.join(memory['main_topics'])}\n"
|
||||||
|
|
||||||
|
# 関係性情報を取得
|
||||||
|
relationships = memory_manager.relationship_tracker.get_all_relationships()
|
||||||
|
|
||||||
|
# 実際のチャット処理
|
||||||
|
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),
|
||||||
|
"relationship_info": {
|
||||||
|
"active_relationships": len(relationships.get("targets", {})),
|
||||||
|
"can_initiate_conversations": sum(1 for target, data in relationships.get("targets", {}).items()
|
||||||
|
if memory_manager.relationship_tracker.should_send_message(target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""ヘルスチェック"""
|
||||||
|
return {
|
||||||
|
"service": "AigptMCP Server with AI Memory",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"status": "running",
|
||||||
|
"memory_dir": str(MEMORY_DIR),
|
||||||
|
"features": [
|
||||||
|
"AI-powered memory analysis",
|
||||||
|
"Relationship tracking",
|
||||||
|
"Advanced memory search",
|
||||||
|
"Conversation import",
|
||||||
|
"Auto-summary generation"
|
||||||
|
],
|
||||||
|
"endpoints": [
|
||||||
|
"/memory/import/chatgpt",
|
||||||
|
"/memory/process-ai",
|
||||||
|
"/memory/search",
|
||||||
|
"/memory/list",
|
||||||
|
"/memory/detail",
|
||||||
|
"/relationship/update",
|
||||||
|
"/relationship/list",
|
||||||
|
"/relationship/check",
|
||||||
|
"/chat"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🚀 AigptMCP Server with AI Memory starting...")
|
||||||
|
print(f"📁 Memory directory: {MEMORY_DIR}")
|
||||||
|
print(f"🧠 AI Memory processing: {'✅ Enabled' if os.getenv('OPENAI_API_KEY') or os.getenv('ANTHROPIC_API_KEY') else '❌ Disabled (no API keys)'}")
|
||||||
|
uvicorn.run(app, host="127.0.0.1", port=5000)
|
130
readme.md
Normal file
130
readme.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
Memory-Enhanced MCP Server 使用ガイド
|
||||||
|
概要
|
||||||
|
このMCPサーバーは、ChatGPTの会話履歴を記憶として保存し、AIとの対話で活用できる機能を提供します。
|
||||||
|
|
||||||
|
セットアップ
|
||||||
|
1. 依存関係のインストール
|
||||||
|
bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
2. サーバーの起動
|
||||||
|
bash
|
||||||
|
python mcp/server.py
|
||||||
|
サーバーは http://localhost:5000 で起動します。
|
||||||
|
|
||||||
|
使用方法
|
||||||
|
1. ChatGPTの会話履歴をインポート
|
||||||
|
ChatGPTから会話をエクスポートし、JSONファイルとして保存してください。
|
||||||
|
|
||||||
|
bash
|
||||||
|
# 単一ファイルをインポート
|
||||||
|
python mcp/memory_client.py import your_chatgpt_export.json
|
||||||
|
|
||||||
|
# インポート結果の例
|
||||||
|
✅ Imported 5/5 conversations
|
||||||
|
2. 記憶の検索
|
||||||
|
bash
|
||||||
|
# キーワードで記憶を検索
|
||||||
|
python mcp/memory_client.py search "プログラミング"
|
||||||
|
|
||||||
|
# 検索結果の例
|
||||||
|
🔍 Searching for: プログラミング
|
||||||
|
📚 Found 3 memories:
|
||||||
|
• Pythonの基礎学習
|
||||||
|
Summary: Conversation with 10 user messages and 8 assistant responses...
|
||||||
|
Messages: 18
|
||||||
|
3. 記憶一覧の表示
|
||||||
|
bash
|
||||||
|
python mcp/memory_client.py list
|
||||||
|
|
||||||
|
# 結果の例
|
||||||
|
📋 Listing all memories...
|
||||||
|
📚 Total memories: 15
|
||||||
|
• day
|
||||||
|
Source: chatgpt
|
||||||
|
Messages: 2
|
||||||
|
Imported: 2025-01-21T10:30:45.123456
|
||||||
|
4. 記憶の詳細表示
|
||||||
|
bash
|
||||||
|
python mcp/memory_client.py detail "/path/to/memory/file.json"
|
||||||
|
|
||||||
|
# 結果の例
|
||||||
|
📄 Getting details for: /path/to/memory/file.json
|
||||||
|
Title: day
|
||||||
|
Source: chatgpt
|
||||||
|
Summary: Conversation with 1 user messages and 1 assistant responses...
|
||||||
|
Messages: 2
|
||||||
|
|
||||||
|
Recent messages:
|
||||||
|
user: こんにちは...
|
||||||
|
assistant: こんにちは〜!✨...
|
||||||
|
5. 記憶を活用したチャット
|
||||||
|
bash
|
||||||
|
python mcp/memory_client.py chat "Pythonについて教えて"
|
||||||
|
|
||||||
|
# 結果の例
|
||||||
|
💬 Chatting with memory: Pythonについて教えて
|
||||||
|
🤖 Response: Enhanced response with memory context...
|
||||||
|
📚 Memories used: 2
|
||||||
|
API エンドポイント
|
||||||
|
POST /memory/import/chatgpt
|
||||||
|
ChatGPTの会話履歴をインポート
|
||||||
|
|
||||||
|
json
|
||||||
|
{
|
||||||
|
"conversation_data": { ... }
|
||||||
|
}
|
||||||
|
POST /memory/search
|
||||||
|
記憶を検索
|
||||||
|
|
||||||
|
json
|
||||||
|
{
|
||||||
|
"query": "検索キーワード",
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
GET /memory/list
|
||||||
|
すべての記憶をリスト
|
||||||
|
|
||||||
|
GET /memory/detail?filepath=/path/to/file
|
||||||
|
記憶の詳細を取得
|
||||||
|
|
||||||
|
POST /chat
|
||||||
|
記憶を活用したチャット
|
||||||
|
|
||||||
|
json
|
||||||
|
{
|
||||||
|
"message": "メッセージ",
|
||||||
|
"model": "model_name"
|
||||||
|
}
|
||||||
|
記憶の保存場所
|
||||||
|
記憶は以下のディレクトリに保存されます:
|
||||||
|
|
||||||
|
~/.config/aigpt/memory/chatgpt/
|
||||||
|
各会話は個別のJSONファイルとして保存され、以下の情報を含みます:
|
||||||
|
|
||||||
|
タイトル
|
||||||
|
インポート時刻
|
||||||
|
メッセージ履歴
|
||||||
|
自動生成された要約
|
||||||
|
メタデータ
|
||||||
|
ChatGPTの会話エクスポート方法
|
||||||
|
ChatGPTの設定画面を開く
|
||||||
|
"Data controls" → "Export data" を選択
|
||||||
|
エクスポートファイルをダウンロード
|
||||||
|
conversations.json ファイルを使用
|
||||||
|
拡張可能な機能
|
||||||
|
高度な検索: ベクトル検索やセマンティック検索の実装
|
||||||
|
要約生成: AIによる自動要約の改善
|
||||||
|
記憶の分類: カテゴリやタグによる分類
|
||||||
|
記憶の統合: 複数の会話からの知識統合
|
||||||
|
プライバシー保護: 機密情報の自動検出・マスキング
|
||||||
|
トラブルシューティング
|
||||||
|
サーバーが起動しない
|
||||||
|
ポート5000が使用中でないか確認
|
||||||
|
依存関係が正しくインストールされているか確認
|
||||||
|
インポートに失敗する
|
||||||
|
JSONファイルが正しい形式か確認
|
||||||
|
ファイルパスが正しいか確認
|
||||||
|
ファイルの権限を確認
|
||||||
|
検索結果が表示されない
|
||||||
|
インポートが正常に完了しているか確認
|
||||||
|
検索キーワードを変更して試行
|
@@ -1,38 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
@@ -1,45 +0,0 @@
|
|||||||
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
Normal file
64
src/cli.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// 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,
|
||||||
|
},
|
||||||
|
}
|
59
src/config.rs
Normal file
59
src/config.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,2 +0,0 @@
|
|||||||
pub mod memory;
|
|
||||||
pub mod mcp;
|
|
91
src/main.rs
91
src/main.rs
@@ -1,49 +1,58 @@
|
|||||||
use anyhow::Result;
|
// main.rs
|
||||||
use clap::{Parser, Subcommand};
|
mod cli;
|
||||||
use std::path::PathBuf;
|
mod config;
|
||||||
|
mod mcp;
|
||||||
|
|
||||||
pub mod memory;
|
use cli::{Args, Commands, ServerCommands, MemoryCommands};
|
||||||
pub mod mcp;
|
use clap::Parser;
|
||||||
|
|
||||||
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() -> Result<()> {
|
async fn main() {
|
||||||
let cli = Cli::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
match cli.command {
|
match args.command {
|
||||||
Commands::Server | Commands::Serve => {
|
Commands::Server { command } => {
|
||||||
let mut server = BaseMCPServer::new().await?;
|
match command {
|
||||||
server.run().await?;
|
ServerCommands::Setup => {
|
||||||
|
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
280
src/mcp/base.rs
@@ -1,280 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,293 +0,0 @@
|
|||||||
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")))
|
|
||||||
}
|
|
||||||
}
|
|
393
src/mcp/memory.rs
Normal file
393
src/mcp/memory.rs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
// src/mcp/memory.rs
|
||||||
|
use reqwest;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{self, Value};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MemorySearchRequest {
|
||||||
|
pub query: String,
|
||||||
|
pub limit: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ChatRequest {
|
||||||
|
pub message: String,
|
||||||
|
pub model: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ConversationImportRequest {
|
||||||
|
pub conversation_data: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ApiResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub filepath: Option<String>,
|
||||||
|
pub results: Option<Vec<MemoryResult>>,
|
||||||
|
pub memories: Option<Vec<MemoryResult>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub count: Option<usize>,
|
||||||
|
pub memory: Option<Value>,
|
||||||
|
pub response: Option<String>,
|
||||||
|
pub memories_used: Option<usize>,
|
||||||
|
pub imported_count: Option<usize>,
|
||||||
|
pub total_count: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct MemoryResult {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub filepath: String,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
pub source: Option<String>,
|
||||||
|
pub import_time: Option<String>,
|
||||||
|
pub message_count: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MemoryClient {
|
||||||
|
base_url: String,
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryClient {
|
||||||
|
pub fn new(base_url: Option<String>) -> Self {
|
||||||
|
let url = base_url.unwrap_or_else(|| "http://127.0.0.1:5000".to_string());
|
||||||
|
Self {
|
||||||
|
base_url: url,
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_chatgpt_file(&self, filepath: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||||
|
// ファイルを読み込み
|
||||||
|
let content = fs::read_to_string(filepath)?;
|
||||||
|
let json_data: Value = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
|
// 配列かどうかチェック
|
||||||
|
match json_data.as_array() {
|
||||||
|
Some(conversations) => {
|
||||||
|
// 複数の会話をインポート
|
||||||
|
let mut imported_count = 0;
|
||||||
|
let total_count = conversations.len();
|
||||||
|
|
||||||
|
for conversation in conversations {
|
||||||
|
match self.import_single_conversation(conversation.clone()).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
imported_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ インポートエラー: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ApiResponse {
|
||||||
|
success: true,
|
||||||
|
imported_count: Some(imported_count),
|
||||||
|
total_count: Some(total_count),
|
||||||
|
error: None,
|
||||||
|
message: Some(format!("{}個中{}個の会話をインポートしました", total_count, imported_count)),
|
||||||
|
filepath: None,
|
||||||
|
results: None,
|
||||||
|
memories: None,
|
||||||
|
count: None,
|
||||||
|
memory: None,
|
||||||
|
response: None,
|
||||||
|
memories_used: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// 単一の会話をインポート
|
||||||
|
self.import_single_conversation(json_data).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_single_conversation(&self, conversation_data: Value) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||||
|
let request = ConversationImportRequest { conversation_data };
|
||||||
|
|
||||||
|
let response = self.client
|
||||||
|
.post(&format!("{}/memory/import/chatgpt", self.base_url))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result: ApiResponse = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_memories(&self, query: &str, limit: usize) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||||
|
let request = MemorySearchRequest {
|
||||||
|
query: query.to_string(),
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self.client
|
||||||
|
.post(&format!("{}/memory/search", self.base_url))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result: ApiResponse = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_memories(&self) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||||
|
let response = self.client
|
||||||
|
.get(&format!("{}/memory/list", self.base_url))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result: ApiResponse = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_memory_detail(&self, filepath: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||||
|
let response = self.client
|
||||||
|
.get(&format!("{}/memory/detail", self.base_url))
|
||||||
|
.query(&[("filepath", filepath)])
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result: ApiResponse = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn chat_with_memory(&self, message: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||||
|
let request = ChatRequest {
|
||||||
|
message: message.to_string(),
|
||||||
|
model: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self.client
|
||||||
|
.post(&format!("{}/chat", self.base_url))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result: ApiResponse = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_server_running(&self) -> bool {
|
||||||
|
match self.client.get(&self.base_url).send().await {
|
||||||
|
Ok(response) => response.status().is_success(),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_import(filepath: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if !Path::new(filepath).exists() {
|
||||||
|
eprintln!("❌ ファイルが見つかりません: {}", filepath);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = MemoryClient::new(None);
|
||||||
|
|
||||||
|
// サーバーが起動しているかチェック
|
||||||
|
if !client.is_server_running().await {
|
||||||
|
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("🔄 ChatGPT会話をインポートしています: {}", filepath);
|
||||||
|
|
||||||
|
match client.import_chatgpt_file(filepath).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
if let (Some(imported), Some(total)) = (response.imported_count, response.total_count) {
|
||||||
|
println!("✅ {}個中{}個の会話をインポートしました", total, imported);
|
||||||
|
} else {
|
||||||
|
println!("✅ 会話をインポートしました");
|
||||||
|
if let Some(path) = response.filepath {
|
||||||
|
println!("📁 保存先: {}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("❌ インポートに失敗: {:?}", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ インポートエラー: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_search(query: &str, limit: usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let client = MemoryClient::new(None);
|
||||||
|
|
||||||
|
if !client.is_server_running().await {
|
||||||
|
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("🔍 記憶を検索しています: {}", query);
|
||||||
|
|
||||||
|
match client.search_memories(query, limit).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
if let Some(results) = response.results {
|
||||||
|
println!("📚 {}個の記憶が見つかりました:", results.len());
|
||||||
|
for memory in results {
|
||||||
|
println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string()));
|
||||||
|
if let Some(summary) = memory.summary {
|
||||||
|
println!(" 概要: {}", summary);
|
||||||
|
}
|
||||||
|
if let Some(count) = memory.message_count {
|
||||||
|
println!(" メッセージ数: {}", count);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("📚 記憶が見つかりませんでした");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("❌ 検索に失敗: {:?}", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ 検索エラー: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_list() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let client = MemoryClient::new(None);
|
||||||
|
|
||||||
|
if !client.is_server_running().await {
|
||||||
|
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("📋 記憶一覧を取得しています...");
|
||||||
|
|
||||||
|
match client.list_memories().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
if let Some(memories) = response.memories {
|
||||||
|
println!("📚 総記憶数: {}", memories.len());
|
||||||
|
for memory in memories {
|
||||||
|
println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string()));
|
||||||
|
if let Some(source) = memory.source {
|
||||||
|
println!(" ソース: {}", source);
|
||||||
|
}
|
||||||
|
if let Some(count) = memory.message_count {
|
||||||
|
println!(" メッセージ数: {}", count);
|
||||||
|
}
|
||||||
|
if let Some(import_time) = memory.import_time {
|
||||||
|
println!(" インポート時刻: {}", import_time);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("📚 記憶がありません");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("❌ 一覧取得に失敗: {:?}", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ 一覧取得エラー: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_detail(filepath: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let client = MemoryClient::new(None);
|
||||||
|
|
||||||
|
if !client.is_server_running().await {
|
||||||
|
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("📄 記憶の詳細を取得しています: {}", filepath);
|
||||||
|
|
||||||
|
match client.get_memory_detail(filepath).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
if let Some(memory) = response.memory {
|
||||||
|
if let Some(title) = memory.get("title").and_then(|v| v.as_str()) {
|
||||||
|
println!("タイトル: {}", title);
|
||||||
|
}
|
||||||
|
if let Some(source) = memory.get("source").and_then(|v| v.as_str()) {
|
||||||
|
println!("ソース: {}", source);
|
||||||
|
}
|
||||||
|
if let Some(summary) = memory.get("summary").and_then(|v| v.as_str()) {
|
||||||
|
println!("概要: {}", summary);
|
||||||
|
}
|
||||||
|
if let Some(messages) = memory.get("messages").and_then(|v| v.as_array()) {
|
||||||
|
println!("メッセージ数: {}", messages.len());
|
||||||
|
println!("\n最近のメッセージ:");
|
||||||
|
for msg in messages.iter().take(5) {
|
||||||
|
if let (Some(role), Some(content)) = (
|
||||||
|
msg.get("role").and_then(|v| v.as_str()),
|
||||||
|
msg.get("content").and_then(|v| v.as_str())
|
||||||
|
) {
|
||||||
|
let content_preview = if content.len() > 100 {
|
||||||
|
format!("{}...", &content[..100])
|
||||||
|
} else {
|
||||||
|
content.to_string()
|
||||||
|
};
|
||||||
|
println!(" {}: {}", role, content_preview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("❌ 詳細取得に失敗: {:?}", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ 詳細取得エラー: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_chat_with_memory(message: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let client = MemoryClient::new(None);
|
||||||
|
|
||||||
|
if !client.is_server_running().await {
|
||||||
|
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("💬 記憶を活用してチャットしています...");
|
||||||
|
|
||||||
|
match client.chat_with_memory(message).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
if let Some(reply) = response.response {
|
||||||
|
println!("🤖 {}", reply);
|
||||||
|
}
|
||||||
|
if let Some(memories_used) = response.memories_used {
|
||||||
|
println!("📚 使用した記憶数: {}", memories_used);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("❌ チャットに失敗: {:?}", response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ チャットエラー: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@@ -1,5 +1,3 @@
|
|||||||
pub mod base;
|
// src/mcp/mod.rs
|
||||||
pub mod extended;
|
pub mod server;
|
||||||
|
pub mod memory;
|
||||||
pub use base::BaseMCPServer;
|
|
||||||
pub use extended::ExtendedMCPServer;
|
|
||||||
|
147
src/mcp/server.rs
Normal file
147
src/mcp/server.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// 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
241
src/memory.rs
@@ -1,241 +0,0 @@
|
|||||||
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