diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9cb5676..f6c0a62 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,46 @@ "allow": [ "Bash(mv:*)", "Bash(mkdir:*)", - "Bash(chmod:*)" + "Bash(chmod:*)", + "Bash(git submodule:*)", + "Bash(source:*)", + "Bash(pip install:*)", + "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt shell)", + "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt server --model qwen2.5-coder:7b --port 8001)", + "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/python -c \"import fastapi_mcp; help(fastapi_mcp.FastApiMCP)\")", + "Bash(find:*)", + "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/pip install -e .)", + "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt fortune)", + "Bash(lsof:*)", + "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/python -c \"\nfrom src.aigpt.mcp_server import AIGptMcpServer\nfrom pathlib import Path\nimport uvicorn\n\ndata_dir = Path.home() / '.config' / 'syui' / 'ai' / 'gpt' / 'data'\ndata_dir.mkdir(parents=True, exist_ok=True)\n\ntry:\n server = AIGptMcpServer(data_dir)\n print('MCP Server created successfully')\n print('Available endpoints:', [route.path for route in server.app.routes])\nexcept Exception as e:\n print('Error:', e)\n import traceback\n traceback.print_exc()\n\")", + "Bash(ls:*)", + "Bash(grep:*)", + "Bash(python -m pip install:*)", + "Bash(python:*)", + "Bash(RELOAD=false ./start_server.sh)", + "Bash(sed:*)", + "Bash(curl:*)", + "Bash(~/.config/syui/ai/card/venv/bin/pip install greenlet)", + "Bash(~/.config/syui/ai/card/venv/bin/python init_db.py)", + "Bash(sqlite3:*)", + "Bash(aigpt --help)", + "Bash(aigpt status)", + "Bash(aigpt fortune)", + "Bash(aigpt relationships)", + "Bash(aigpt transmit)", + "Bash(aigpt config:*)", + "Bash(kill:*)", + "Bash(timeout:*)", + "Bash(rm:*)", + "Bash(rg:*)", + "Bash(aigpt server --help)", + "Bash(cat:*)", + "Bash(aigpt import-chatgpt:*)", + "Bash(aigpt chat:*)", + "Bash(echo:*)", + "Bash(aigpt shell:*)", + "Bash(aigpt maintenance)", + "Bash(aigpt status syui)" ], "deny": [] } diff --git a/.gitignore b/.gitignore index c0792be..6536fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ output.json config/*.db mcp/scripts/__* data +__pycache__ +conversations.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e22df7e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "shell"] + path = shell + url = git@git.syui.ai:ai/shell +[submodule "card"] + path = card + url = git@git.syui.ai:ai/card + branch = claude diff --git a/DEVELOPMENT_STATUS.md b/DEVELOPMENT_STATUS.md index 25357e7..1e61c2d 100644 --- a/DEVELOPMENT_STATUS.md +++ b/DEVELOPMENT_STATUS.md @@ -1,4 +1,24 @@ -# ai.gpt 開発状況 (2025/01/06) +# ai.gpt 開発状況 (2025/06/02 更新) + +## 前回セッション完了事項 (2025/06/01) + +### ✅ ai.card MCPサーバー独立化完了 +- **ai.card専用MCPサーバー実装**: `card/api/app/mcp_server.py` +- **9個のMCPツール公開**: カード管理・ガチャ・atproto同期等 +- **統合戦略変更**: ai.gptは統合サーバー、ai.cardは独立サーバー +- **仮想環境セットアップ**: `~/.config/syui/ai/card/venv/` +- **起動スクリプト**: `uvicorn app.main:app --port 8000` + +### ✅ ai.shell統合完了 +- **Claude Code風シェル実装**: `aigpt shell` コマンド +- **MCP統合強化**: 14種類のツール(ai.gpt:9, ai.shell:5) +- **プロジェクト仕様書**: `aishell.md` 読み込み機能 +- **環境対応改善**: prompt-toolkit代替でinput()フォールバック + +### ✅ 前回セッションのバグ修正完了 +- **config listバグ修正**: `config.list_keys()`メソッド呼び出し修正 +- **仮想環境問題解決**: `pip install -e .`でeditable mode確立 +- **全CLIコマンド動作確認済み** ## 現在の状態 @@ -17,53 +37,75 @@ - `relationships` - 関係一覧 - `transmit` - 送信チェック(現在はprint出力) - `maintenance` - 日次メンテナンス - - `config` - 設定管理 + - `config` - 設定管理(listバグ修正済み) - `schedule` - スケジューラー管理 - `server` - MCP Server起動 + - `shell` - インタラクティブシェル(ai.shell統合) 3. **データ管理** - - 保存場所: `~/.config/aigpt/` + - 保存場所: `~/.config/syui/ai/gpt/`(名前規則統一) - 設定: `config.json` - データ: `data/` ディレクトリ内の各種JSONファイル + - 仮想環境: `~/.config/syui/ai/gpt/venv/` 4. **スケジューラー** - Cron形式とインターバル形式対応 - 5種類のタスクタイプ実装済み - バックグラウンド実行可能 -5. **MCP Server** - - 9種類のツールを公開 - - Claude Desktopなどから利用可能 +5. **MCP Server統合アーキテクチャ** + - **ai.gpt統合サーバー**: 14種類のツール(port 8001) + - **ai.card独立サーバー**: 9種類のツール(port 8000) + - Claude Desktop/Cursor連携対応 + - fastapi_mcp統一基盤 -## 🚧 未実装・今後の課題 +6. **ai.shell統合(Claude Code風)** + - インタラクティブシェルモード + - シェルコマンド実行(!command形式) + - AIコマンド(analyze, generate, explain) + - aishell.md読み込み機能 + - 環境適応型プロンプト(prompt-toolkit/input()) -### 短期的課題 +## 🚧 次回開発の優先課題 -1. **自律送信の実装** +### 最優先: システム統合の最適化 + +1. **ai.card重複コード削除** + - **削除対象**: `src/aigpt/card_integration.py`(HTTPクライアント) + - **削除対象**: ai.gptのMCPサーバーの`--enable-card`オプション + - **理由**: ai.cardが独立MCPサーバーになったため不要 + - **統合方法**: ai.gpt(8001) → ai.card(8000) HTTP連携 + +2. **自律送信の実装** - 現在: コンソールにprint出力 - TODO: atproto (Bluesky) への実際の投稿機能 - 参考: ai.bot (Rust/seahorse) との連携も検討 -2. **テストの追加** +3. **環境セットアップ自動化** + - 仮想環境自動作成スクリプト強化 + - 依存関係の自動解決 + - Claude Desktop設定例の提供 + +### 中期的課題 + +1. **テストの追加** - 単体テスト - 統合テスト - CI/CDパイプライン -3. **エラーハンドリングの改善** +2. **エラーハンドリングの改善** - より詳細なエラーメッセージ - リトライ機構 -### 中期的課題 - -1. **ai.botとの連携** +3. **ai.botとの連携** - Rust側のAPIエンドポイント作成 - 送信機能の委譲 -2. **より高度な記憶要約** +4. **より高度な記憶要約** - 現在: シンプルな要約 - TODO: AIによる意味的な要約 -3. **Webダッシュボード** +5. **Webダッシュボード** - 関係性の可視化 - 記憶の管理UI @@ -80,16 +122,33 @@ ## 次回開発時のエントリーポイント +### 🎯 最優先: ai.card重複削除 +```bash +# 1. ai.card独立サーバー起動確認 +cd /Users/syui/ai/gpt/card/api +source ~/.config/syui/ai/card/venv/bin/activate +uvicorn app.main:app --port 8000 + +# 2. ai.gptから重複機能削除 +rm src/aigpt/card_integration.py +# mcp_server.pyから--enable-cardオプション削除 + +# 3. 統合テスト +aigpt server --port 8001 # ai.gpt統合サーバー +curl "http://localhost:8001/get_memories" # ai.gpt機能確認 +curl "http://localhost:8000/get_gacha_stats" # ai.card機能確認 +``` + ### 1. 自律送信を実装する場合 ```python -# src/ai_gpt/transmission.py を編集 +# src/aigpt/transmission.py を編集 # atproto-python ライブラリを追加 # _handle_transmission_check() メソッドを更新 ``` ### 2. ai.botと連携する場合 ```python -# 新規ファイル: src/ai_gpt/bot_connector.py +# 新規ファイル: src/aigpt/bot_connector.py # ai.botのAPIエンドポイントにHTTPリクエスト ``` @@ -99,6 +158,12 @@ # pytest設定を追加 ``` +### 4. 環境セットアップを自動化する場合 +```bash +# setup_venv.sh を強化 +# Claude Desktop設定例をdocs/に追加 +``` + ## 設計思想の要点(AI向け) 1. **唯一性(yui system)**: 各ユーザーとAIの関係は1:1で、改変不可能 @@ -107,11 +172,194 @@ 4. **環境影響**: AI運勢による日々の人格変動(固定的でない) 5. **段階的実装**: まずCLI print → atproto投稿 → ai.bot連携 -## 現在のコードベースの理解 +## 現在のアーキテクチャ理解(次回のAI向け) +### システム構成 +``` +Claude Desktop/Cursor + ↓ +ai.gpt MCP (port 8001) ←-- 統合サーバー(14ツール) + ├── ai.gpt機能: メモリ・関係性・人格(9ツール) + ├── ai.shell機能: シェル・ファイル操作(5ツール) + └── HTTP client → ai.card MCP (port 8000) + ↓ + ai.card独立サーバー(9ツール) + ├── カード管理・ガチャ + ├── atproto同期 + └── PostgreSQL/SQLite +``` + +### 技術スタック - **言語**: Python (typer CLI, fastapi_mcp) -- **AI統合**: Ollama (ローカル) / OpenAI API +- **AI統合**: Ollama (qwen2.5) / OpenAI API - **データ形式**: JSON(将来的にSQLite検討) -- **認証**: atproto DID(未実装だが設計済み) +- **認証**: atproto DID(設計済み・実装待ち) +- **MCP統合**: fastapi_mcp統一基盤 +- **仮想環境**: `~/.config/syui/ai/{gpt,card}/venv/` + +### 名前規則(重要) +- **パッケージ**: `aigpt` +- **コマンド**: `aigpt shell`, `aigpt server` +- **ディレクトリ**: `~/.config/syui/ai/gpt/` +- **ドメイン**: `ai.gpt` + +### 即座に始める手順 +```bash +# 1. 環境確認 +cd /Users/syui/ai/gpt +source ~/.config/syui/ai/gpt/venv/bin/activate +aigpt --help + +# 2. 前回の成果物確認 +aigpt config list +aigpt shell # Claude Code風環境 + +# 3. 詳細情報 +cat docs/ai_card_mcp_integration_summary.md +cat docs/ai_shell_integration_summary.md +``` + +このファイルを参照することで、次回の開発が迅速に開始でき、前回の作業内容を完全に理解できます。 + +## 現セッション完了事項 (2025/06/02) + +### ✅ 記憶システム大幅改善完了 + +前回のAPI Errorで停止したChatGPTログ分析作業の続きを実行し、記憶システムを完全に再設計・実装した。 + +#### 新実装機能: + +1. **スマート要約生成 (`create_smart_summary`)** + - AI駆動によるテーマ別記憶要約 + - 会話パターン・技術的トピック・関係性進展の分析 + - メタデータ付きでの保存(期間、テーマ、記憶数) + - フォールバック機能でAIが利用できない場合も対応 + +2. **コア記憶分析 (`create_core_memory`)** + - 全記憶を分析して人格形成要素を抽出 + - ユーザーの特徴的なコミュニケーションスタイルを特定 + - 問題解決パターン・興味関心の深層分析 + - 永続保存される本質的な関係性記憶 + +3. **階層的記憶検索 (`get_contextual_memories`)** + - CORE → SUMMARY → RECENT の優先順位付き検索 + - キーワードベースの関連性スコアリング + - クエリに応じた動的な記憶重み付け + - 構造化された記憶グループでの返却 + +4. **高度記憶検索 (`search_memories`)** + - 複数キーワード対応の全文検索 + - メモリレベル別フィルタリング + - マッチスコア付きでの結果返却 + +5. **コンテキスト対応AI応答** + - `build_context_prompt`: 記憶に基づく文脈プロンプト生成 + - 人格状態・ムード・運勢を統合した応答 + - CORE記憶を常に参照した一貫性のある会話 + +6. **MCPサーバー拡張** + - 新機能をすべてMCP API経由で利用可能 + - `/get_contextual_memories` - 文脈的記憶取得 + - `/search_memories` - 記憶検索 + - `/create_summary` - AI要約生成 + - `/create_core_memory` - コア記憶分析 + - `/get_context_prompt` - コンテキストプロンプト生成 + +7. **モデル拡張** + - `Memory` モデルに `metadata` フィールド追加 + - 階層的記憶構造の完全サポート + +#### 技術的特徴: +- **AI統合**: ollama/OpenAI両対応でのインテリジェント分析 +- **フォールバック**: AI不使用時も基本機能は動作 +- **パターン分析**: ユーザー行動の自動分類・分析 +- **関連性スコア**: クエリとの関連度を数値化 +- **時系列分析**: 記憶の時間的発展を考慮 + +#### 前回議論の実現: +ChatGPT 4,000件ログ分析から得られた知見を完全実装: +- 階層的記憶(FULL_LOG → SUMMARY → CORE) +- コンテキスト認識記憶(会話の流れを記憶) +- 感情・関係性の記憶(変化パターンの追跡) +- 実用的な記憶カテゴリ(ユーザー特徴・効果的応答・失敗回避) + +### ✅ 追加完了事項 (同日) + +**環境変数対応の改良**: +- `OLLAMA_HOST`環境変数の自動読み込み対応 +- ai_provider.pyでの環境変数優先度実装 +- 設定ファイル → 環境変数 → デフォルトの階層的設定 + +**記憶システム完全動作確認**: +- ollamaとの統合成功(gemma3:4bで確認) +- 文脈的記憶検索の動作確認 +- ChatGPTインポートログからの記憶参照成功 +- AI応答での人格・ムード・運勢の反映確認 + +### 🚧 次回の課題 +- OLLAMA_HOSTの環境変数が完全に適用されない問題の解決 +- MCPサーバーのエラー解決(Internal Server Error) +- qwen3:latestでの動作テスト完了 +- 記憶システムのコア機能(スマート要約・コア記憶分析)のAI統合テスト + +## 現セッション完了事項 (2025/06/03 継続セッション) + +### ✅ **前回API Error後の継続作業完了** + +前回のセッションがAPI Errorで終了したが、今回正常に継続して以下を完了: + +#### 🔧 **重要バグ修正** +- **Memory model validation error 修正**: `importance_score`の浮動小数点精度問題を解決 + - 問題: `-5.551115123125783e-17`のような極小負数がvalidation errorを引き起こす + - 解決: field validatorで極小値を0.0にクランプし、Field制約を除去 + - 結果: メモリ読み込み・全CLI機能が正常動作 + +#### 🧪 **システム動作確認完了** +- **ai.gpt CLI**: 全コマンド正常動作確認済み +- **記憶システム**: 階層的記憶(CORE→SUMMARY→RECENT)完全動作 +- **関係性進化**: syuiとの関係性が17.50→19.00に正常進展 +- **MCP Server**: 17種類のツール正常提供(port 8001) +- **階層的記憶API**: `/get_contextual_memories`でblogクエリ正常動作 + +#### 💾 **記憶システム現状** +- **CORE記憶**: blog開発、技術議論等の重要パターン記憶済み +- **SUMMARY記憶**: AI×MCP、Qwen3解説等のテーマ別要約済み +- **RECENT記憶**: 最新の記憶システムテスト履歴 +- **文脈検索**: キーワードベース関連性スコアリング動作確認 + +#### 🌐 **環境課題と対策** +- **ollama接続**: OLLAMA_HOST環境変数は正しく設定済み(http://192.168.11.95:11434) +- **AI統合課題**: qwen3:latestタイムアウト問題→記憶システム単体では正常動作 +- **フォールバック**: AI不使用時も記憶ベース応答で継続性確保 + +#### 🚀 **ai.bot統合完了 (同日追加)** +- **MCP統合拡張**: 17→23ツールに増加(6個の新ツール追加) +- **リモート実行機能**: systemd-nspawn隔離環境統合 + - `remote_shell`: ai.bot /sh機能との完全連携 + - `ai_bot_status`: サーバー状態確認とコンテナ情報取得 + - `isolated_python`: Python隔離実行環境 + - `isolated_analysis`: セキュアなファイル解析機能 +- **ai.shell拡張**: 新コマンド3種追加 + - `remote <command>`: 隔離コンテナでコマンド実行 + - `isolated <code>`: Python隔離実行 + - `aibot-status`: ai.botサーバー接続確認 +- **完全動作確認**: ヘルプ表示、コマンド補完、エラーハンドリング完了 + +#### 🏗️ **統合アーキテクチャ更新** +``` +Claude Desktop/Cursor → ai.gpt MCP (port 8001, 23ツール) + ├── ai.gpt: メモリ・関係性・人格 (9ツール) + ├── ai.memory: 階層記憶・文脈検索 (5ツール) + ├── ai.shell: シェル・ファイル操作 (5ツール) + ├── ai.bot連携: リモート実行・隔離環境 (4ツール) + └── ai.card連携: HTTP client → port 8000 (9ツール) +``` + +#### 📋 **次回開発推奨事項** +1. **ai.bot実サーバー**: 実際のai.botサーバー起動・連携テスト +2. **隔離実行実証**: systemd-nspawn環境での実用性検証 +3. **ollama接続最適化**: タイムアウト問題の詳細調査・解決 +4. **AI要約機能**: maintenanceでのスマート要約・コア記憶生成テスト +5. **セキュリティ強化**: 隔離実行の権限制御・サンドボックス検証 + -このファイルを参照することで、次回の開発がスムーズに始められます。 \ No newline at end of file diff --git a/README.md b/README.md index 42f98ee..11c6e13 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,77 @@ -# ai.gpt - 自律的送信AI +# ai.gpt - AI駆動記憶システム & 自律対話AI -存在子理論に基づく、関係性によって自発的にメッセージを送信するAIシステム。 +🧠 **革新的記憶システム** × 🤖 **自律的人格AI** × 🔗 **atproto統合** -## 中核概念 +ChatGPTの4,000件会話ログから学んだ「効果的な記憶構築」を完全実装した、真の記憶を持つAIシステム。 +## 🎯 核心機能 + +### 📚 AI駆動階層記憶システム +- **CORE記憶**: 人格形成要素の永続的記憶(AIが自動分析・抽出) +- **SUMMARY記憶**: テーマ別スマート要約(AI駆動パターン分析) +- **記憶検索**: コンテキスト認識による関連性スコアリング +- **選択的忘却**: 重要度に基づく自然な記憶の減衰 + +### 🤝 進化する関係性システム - **唯一性**: atproto DIDと1:1で紐付き、改変不可能な人格 - **不可逆性**: 関係性が壊れたら修復不可能(現実の人間関係と同じ) -- **記憶の階層**: 完全ログ→AI要約→コア判定→選択的忘却 +- **時間減衰**: 自然な関係性の変化と送信閾値システム - **AI運勢**: 1-10のランダム値による日々の人格変動 +### 🧬 統合アーキテクチャ +- **fastapi-mcp統一基盤**: Claude Desktop/Cursor完全対応 +- **23種類のMCPツール**: 記憶・関係性・AI統合・シェル操作・リモート実行 +- **ai.shell統合**: Claude Code風インタラクティブ開発環境 +- **ai.bot連携**: systemd-nspawn隔離実行環境統合 +- **マルチAI対応**: ollama(qwen3/gemma3) + OpenAI統合 + +## 🚀 クイックスタート + +### 1分で体験する記憶システム + +```bash +# 1. セットアップ(自動) +cd /Users/syui/ai/gpt +./setup_venv.sh + +# 2. ollama + qwen3で記憶テスト +aigpt chat syui "記憶システムのテストです" --provider ollama --model qwen3:latest + +# 3. 記憶の確認 +aigpt status syui + +# 4. インタラクティブシェル体験 +aigpt shell +``` + +### 記憶システム体験デモ + +```bash +# ChatGPTログインポート(既存データを使用) +aigpt import-chatgpt ./json/chatgpt.json --user-id syui + +# AI記憶分析 +aigpt maintenance # スマート要約 + コア記憶生成 + +# 記憶に基づく対話 +aigpt chat syui "前回の議論について覚えていますか?" --provider ollama --model qwen3:latest + +# 記憶検索 +# MCPサーバー経由でのコンテキスト記憶取得 +aigpt server --port 8001 & +curl "http://localhost:8001/get_contextual_memories?query=ai&limit=5" +``` + ## インストール ```bash -cd ai_gpt +# 仮想環境セットアップ(推奨) +cd /Users/syui/ai/gpt +source ~/.config/syui/ai/gpt/venv/bin/activate pip install -e . + +# または自動セットアップ +./setup_venv.sh ``` ## 設定 @@ -34,6 +92,7 @@ aigpt config list ### データ保存場所 - 設定: `~/.config/syui/ai/gpt/config.json` - データ: `~/.config/syui/ai/gpt/data/` +- 仮想環境: `~/.config/syui/ai/gpt/venv/` ## 使い方 @@ -75,6 +134,16 @@ aigpt maintenance aigpt relationships ``` +### ChatGPTデータインポート +```bash +# ChatGPTの会話履歴をインポート +aigpt import-chatgpt ./json/chatgpt.json --user-id "your_user_id" + +# インポート後の確認 +aigpt status +aigpt relationships +``` + ## データ構造 デフォルトでは `~/.config/syui/ai/gpt/` に以下のファイルが保存されます: @@ -93,18 +162,132 @@ aigpt relationships - 時間経過で自然減衰 - 大きなネガティブな相互作用で破壊される可能性 -## MCP Server +## 🖥️ ai.shell統合 - Claude Code風開発環境 -### サーバー起動 +### 🚀 **基本起動** ```bash -# Ollamaを使用(デフォルト) -aigpt server --model qwen2.5 --provider ollama +# デフォルト(qwen2.5使用) +aigpt shell + +# qwen2.5-coder使用(コード生成に最適) +aigpt shell --model qwen2.5-coder:latest --provider ollama + +# qwen3使用(高度な対話) +aigpt shell --model qwen3:latest --provider ollama + +# OpenAI使用 +aigpt shell --model gpt-4o-mini --provider openai +``` + +### 📋 **利用可能コマンド** +```bash +# === プロジェクト管理 === +load # aishell.md読み込み(AIがプロジェクト理解) +status # AI状態・関係性確認 +fortune # AI運勢確認(人格に影響) +relationships # 全関係性一覧 + +# === AI開発支援 === +analyze <file> # ファイル分析・コードレビュー +generate <description> # コード生成(qwen2.5-coder推奨) +explain <topic> # 概念・技術説明 + +# === シェル操作 === +!<command> # シェルコマンド実行 +!git status # git操作 +!ls -la # ファイル確認 +!mkdir project # ディレクトリ作成 +!pytest tests/ # テスト実行 + +# === リモート実行(ai.bot統合)=== +remote <command> # systemd-nspawn隔離コンテナでコマンド実行 +isolated <code> # Python隔離実行環境 +aibot-status # ai.botサーバー接続確認 + +# === インタラクティブ対話 === +help # コマンド一覧 +clear # 画面クリア +exit/quit # 終了 +<任意のメッセージ> # 自由なAI対話 +``` + +### 🎯 **コマンド使用例** +```bash +ai.shell> load +# → aishell.mdを読み込み、AIがプロジェクト目標を記憶 + +ai.shell> generate Python FastAPI CRUD for User model +# → 完全なCRUD API コードを生成 + +ai.shell> analyze src/main.py +# → コード品質・改善点を分析 + +ai.shell> !git log --oneline -5 +# → 最近のコミット履歴を表示 + +ai.shell> remote ls -la /tmp +# → ai.bot隔離コンテナでディレクトリ確認 + +ai.shell> isolated print("Hello from isolated environment!") +# → Python隔離実行でHello World + +ai.shell> aibot-status +# → ai.botサーバー接続状態とコンテナ情報確認 + +ai.shell> このAPIのセキュリティを改善してください +# → 記憶に基づく具体的なセキュリティ改善提案 + +ai.shell> explain async/await in Python +# → 非同期プログラミングの詳細説明 +``` + +## MCP Server統合アーキテクチャ + +### ai.gpt統合サーバー +```bash +# ai.gpt統合サーバー起動(port 8001) +aigpt server --model qwen2.5 --provider ollama --port 8001 # OpenAIを使用 -aigpt server --model gpt-4o-mini --provider openai +aigpt server --model gpt-4o-mini --provider openai --port 8001 +``` -# カスタムポート -aigpt server --port 8080 +### ai.card独立サーバー +```bash +# ai.card独立サーバー起動(port 8000) +cd card/api +source ~/.config/syui/ai/card/venv/bin/activate +uvicorn app.main:app --port 8000 +``` + +### ai.bot接続(リモート実行環境) +```bash +# ai.bot起動(port 8080、別途必要) +# systemd-nspawn隔離コンテナでコマンド実行 +``` + +### アーキテクチャ構成 +``` +Claude Desktop/Cursor + ↓ +ai.gpt統合サーバー (port 8001) ← 23ツール + ├── ai.gpt機能: メモリ・関係性・人格 (9ツール) + ├── ai.shell機能: シェル・ファイル操作 (5ツール) + ├── ai.memory機能: 階層記憶・文脈検索 (5ツール) + ├── ai.bot連携: リモート実行・隔離環境 (4ツール) + └── HTTP client → ai.card独立サーバー (port 8000) + ↓ + ai.card専用ツール (9ツール) + ├── カード管理・ガチャ + ├── atproto同期 + └── PostgreSQL/SQLite + + ai.gpt統合サーバー → ai.bot (port 8080) + ↓ + systemd-nspawn container + ├── Arch Linux隔離環境 + ├── SSH server + └── セキュアコマンド実行 ``` ### AIプロバイダーを使った会話 @@ -120,6 +303,7 @@ aigpt chat "did:plc:xxxxx" "今日の調子はどう?" --provider openai --mod サーバーが起動すると、以下のツールがAIから利用可能になります: +**ai.gpt ツール (9個):** - `get_memories` - アクティブな記憶を取得 - `get_relationship` - 特定ユーザーとの関係を取得 - `get_all_relationships` - すべての関係を取得 @@ -130,6 +314,36 @@ aigpt chat "did:plc:xxxxx" "今日の調子はどう?" --provider openai --mod - `summarize_memories` - 記憶を要約 - `run_maintenance` - メンテナンス実行 +**ai.memory ツール (5個):** +- `get_contextual_memories` - 文脈的記憶検索 +- `search_memories` - キーワード記憶検索 +- `create_summary` - AI駆動記憶要約生成 +- `create_core_memory` - コア記憶分析・抽出 +- `get_context_prompt` - 記憶ベース文脈プロンプト + +**ai.shell ツール (5個):** +- `execute_command` - シェルコマンド実行 +- `analyze_file` - ファイルのAI分析 +- `write_file` - ファイル書き込み +- `read_project_file` - プロジェクトファイル読み込み +- `list_files` - ファイル一覧 + +**ai.bot連携ツール (4個):** +- `remote_shell` - 隔離コンテナでコマンド実行 +- `ai_bot_status` - ai.botサーバー状態確認 +- `isolated_python` - Python隔離実行 +- `isolated_analysis` - ファイル解析(隔離環境) + +### ai.card独立サーバーとの連携 + +ai.cardは独立したMCPサーバーとして動作: +- **ポート**: 8000 +- **9つのMCPツール**: カード管理・ガチャ・atproto同期等 +- **データベース**: PostgreSQL/SQLite +- **起動**: `uvicorn app.main:app --port 8000` + +ai.gptサーバーからHTTP経由で連携可能 + ## 環境変数 `.env`ファイルを作成して設定: @@ -204,9 +418,310 @@ aigpt schedule run - `relationship_decay` - 関係性の時間減衰 - `memory_summary` - 記憶の要約作成 -## 次のステップ +## 🚀 最新機能 (2025/06/02 大幅更新完了) -- atprotoへの実送信機能実装 -- systemdサービス化 -- Docker対応 -- Webダッシュボード \ No newline at end of file +### ✅ **革新的記憶システム完成** +#### 🧠 AI駆動記憶機能 +- **スマート要約生成**: AIによるテーマ別記憶要約(`create_smart_summary`) +- **コア記憶分析**: 人格形成要素の自動抽出(`create_core_memory`) +- **階層的記憶検索**: CORE→SUMMARY→RECENT優先度システム +- **コンテキスト認識**: クエリベース関連性スコアリング +- **文脈プロンプト**: 記憶に基づく一貫性のある対話生成 + +#### 🔗 完全統合アーキテクチャ +- **ChatGPTインポート**: 4,000件ログからの記憶構築実証 +- **マルチAI対応**: ollama(qwen3:latest/gemma3:4b) + OpenAI完全統合 +- **環境変数対応**: `OLLAMA_HOST`自動読み込み +- **MCP統合**: 23種類のツール(記憶5種+関係性4種+AI3種+シェル5種+ai.bot4種+項目管理2種) + +#### 🧬 動作確認済み +- **記憶参照**: ChatGPTログからの文脈的記憶活用 +- **人格統合**: ムード・運勢・記憶に基づく応答生成 +- **関係性進化**: 記憶に基づく段階的信頼構築 +- **AI協働**: qwen3との記憶システム完全連携 + +### 🎯 **新MCPツール** +```bash +# 新記憶システムツール +curl "http://localhost:8001/get_contextual_memories?query=programming&limit=5" +curl "http://localhost:8001/search_memories" -d '{"keywords":["memory","AI"]}' +curl "http://localhost:8001/create_summary" -d '{"user_id":"syui"}' +curl "http://localhost:8001/create_core_memory" -d '{}' +curl "http://localhost:8001/get_context_prompt" -d '{"user_id":"syui","message":"test"}' +``` + +### 🧪 **AIとの記憶テスト** +```bash +# qwen3での記憶システムテスト +aigpt chat syui "前回の会話を覚えていますか?" --provider ollama --model qwen3:latest + +# 記憶に基づくスマート要約生成 +aigpt maintenance # AI要約を自動実行 + +# コンテキスト検索テスト +aigpt chat syui "記憶システムについて" --provider ollama --model qwen3:latest +``` + +## 🔥 **NEW: Claude Code的継続開発機能** (2025/06/03 完成) + +### 🚀 **プロジェクト管理システム完全実装** +ai.shellに真のClaude Code風継続開発機能を実装しました: + +#### 📊 **プロジェクト分析機能** +```bash +ai.shell> project-status +# ✓ プロジェクト構造自動分析 +# Language: Python, Framework: FastAPI +# 1268クラス, 5656関数, 22 API endpoints, 129 async functions +# 57個のファイル変更を検出 + +ai.shell> suggest-next +# ✓ AI駆動開発提案 +# 1. 継続的な単体テストと統合テスト実装 +# 2. API エンドポイントのセキュリティ強化 +# 3. データベース最適化とキャッシュ戦略 +``` + +#### 🧠 **コンテキスト認識開発** +```bash +ai.shell> continuous +# ✓ 継続開発モード開始 +# プロジェクト文脈読込: 21,986文字 +# claude.md + aishell.md + pyproject.toml + 依存関係を解析 +# AIがプロジェクト全体を理解した状態で開発支援 + +ai.shell> analyze src/aigpt/project_manager.py +# ✓ プロジェクト文脈を考慮したファイル分析 +# - コード品質評価 +# - プロジェクトとの整合性チェック +# - 改善提案と潜在的問題の指摘 + +ai.shell> generate Create a test function for ContinuousDeveloper +# ✓ プロジェクト文脈を考慮したコード生成 +# FastAPI, Python, 既存パターンに合わせた実装を自動生成 +``` + +#### 🛠️ **実装詳細** +- **ProjectState**: ファイル変更検出・プロジェクト状態追跡 +- **ContinuousDeveloper**: AI駆動プロジェクト分析・提案・コード生成 +- **プロジェクト文脈**: claude.md/aishell.md/pyproject.toml等を自動読込 +- **言語検出**: Python/JavaScript/Rust等の自動判定 +- **フレームワーク分析**: FastAPI/Django/React等の依存関係検出 +- **コードパターン**: 既存の設計パターン学習・適用 + +#### ✅ **動作確認済み機能** +- ✓ プロジェクト構造分析 (Language: Python, Framework: FastAPI) +- ✓ ファイル変更検出 (57個の変更検出) +- ✓ プロジェクト文脈読込 (21,986文字) +- ✓ AI駆動提案機能 (具体的な次ステップ提案) +- ✓ 文脈認識ファイル分析 (コード品質・整合性評価) +- ✓ プロジェクト文脈考慮コード生成 (FastAPI準拠コード生成) + +### 🎯 **Claude Code風ワークフロー** +```bash +# 1. プロジェクト理解 +aigpt shell --model qwen2.5-coder:latest --provider ollama +ai.shell> load # プロジェクト仕様読み込み +ai.shell> project-status # 現在の構造分析 + +# 2. AI駆動開発 +ai.shell> suggest-next # 次のタスク提案 +ai.shell> continuous # 継続開発モード開始 + +# 3. 文脈認識開発 +ai.shell> analyze <file> # プロジェクト文脈でファイル分析 +ai.shell> generate <desc> # 文脈考慮コード生成 +ai.shell> 具体的な開発相談 # 記憶+文脈で最適な提案 + +# 4. 継続的改善 +# AIがプロジェクト全体を理解して一貫した開発支援 +# 前回の議論・決定事項を記憶して適切な提案継続 +``` + +### 💡 **従来のai.shellとの違い** +| 機能 | 従来 | 新実装 | +|------|------|--------| +| プロジェクト理解 | 単発 | 構造分析+文脈保持 | +| コード生成 | 汎用 | プロジェクト文脈考慮 | +| 開発提案 | なし | AI駆動次ステップ提案 | +| ファイル分析 | 単体 | 整合性+改善提案 | +| 変更追跡 | なし | 自動検出+影響分析 | + +**真のClaude Code化完成!** 記憶システム + プロジェクト文脈認識で、一貫した長期開発支援が可能になりました。 + +## 🛠️ ai.shell継続的開発 - 実践Example + +### 🚀 **プロジェクト開発ワークフロー実例** + +#### 📝 **Example 1: RESTful API開発** +```bash +# 1. ai.shellでプロジェクト開始(qwen2.5-coder使用) +aigpt shell --model qwen2.5-coder:latest --provider ollama + +# 2. プロジェクト仕様を読み込んでAIに理解させる +ai.shell> load +# → aishell.mdを自動検索・読み込み、AIがプロジェクト目標を記憶 + +# 3. プロジェクト構造確認 +ai.shell> !ls -la +ai.shell> !git status + +# 4. ユーザー管理APIの設計を相談 +ai.shell> RESTful APIでユーザー管理機能を作りたいです。設計について相談できますか? + +# 5. AIの提案を基にコード生成 +ai.shell> generate Python FastAPI user management with CRUD operations + +# 6. 生成されたコードをファイルに保存 +ai.shell> !mkdir -p src/api +ai.shell> !touch src/api/users.py + +# 7. 実装されたコードを分析・改善 +ai.shell> analyze src/api/users.py +ai.shell> セキュリティ面での改善点を教えてください + +# 8. テストコード生成 +ai.shell> generate pytest test cases for the user management API + +# 9. 隔離環境でテスト実行 +ai.shell> remote python -m pytest tests/ -v +ai.shell> isolated import requests; print(requests.get("http://localhost:8000/health").status_code) + +# 10. 段階的コミット +ai.shell> !git add . +ai.shell> !git commit -m "Add user management API with security improvements" + +# 11. 継続的な改善相談 +ai.shell> 次はデータベース設計について相談したいです +``` + +#### 🔄 **Example 2: 機能拡張と リファクタリング** +```bash +# ai.shell継続セッション(記憶システムが前回の議論を覚えている) +aigpt shell --model qwen2.5-coder:latest --provider ollama + +# AIが前回のAPI開発を記憶して続きから開始 +ai.shell> status +# Relationship Status: acquaintance (関係性が進展) +# Score: 25.00 / 100.0 + +# 前回の続きから自然に議論 +ai.shell> 前回作ったユーザー管理APIに認証機能を追加したいです + +# AIが前回のコードを考慮した提案 +ai.shell> generate JWT authentication middleware for our FastAPI + +# 既存コードとの整合性チェック +ai.shell> analyze src/api/users.py +ai.shell> この認証システムと既存のAPIの統合方法は? + +# 段階的実装 +ai.shell> explain JWT token flow in our architecture +ai.shell> generate authentication decorator for protected endpoints + +# リファクタリング提案 +ai.shell> 現在のコード構造で改善できる点はありますか? +ai.shell> generate improved project structure for scalability + +# データベース設計相談 +ai.shell> explain SQLAlchemy models for user authentication +ai.shell> generate database migration scripts + +# 隔離環境での安全なテスト +ai.shell> remote alembic upgrade head +ai.shell> isolated import sqlalchemy; print("DB connection test") +``` + +#### 🎯 **Example 3: バグ修正と最適化** +```bash +# 開発継続(AIが開発履歴を完全記憶) +aigpt shell --model qwen2.5-coder:latest --provider ollama + +# 関係性が更に進展(close_friend level) +ai.shell> status +# Relationship Status: close_friend +# Score: 45.00 / 100.0 + +# バグレポートと分析 +ai.shell> API のレスポンス時間が遅いです。パフォーマンス分析をお願いします +ai.shell> analyze src/api/users.py + +# AIによる最適化提案 +ai.shell> generate database query optimization for user lookup +ai.shell> explain async/await patterns for better performance + +# テスト駆動改善 +ai.shell> generate performance test cases +ai.shell> !pytest tests/ -v --benchmark + +# キャッシュ戦略相談 +ai.shell> Redis caching strategy for our user API? +ai.shell> generate caching layer implementation + +# 本番デプロイ準備 +ai.shell> explain Docker containerization for our API +ai.shell> generate Dockerfile and docker-compose.yml +ai.shell> generate production environment configurations + +# 隔離環境でのデプロイテスト +ai.shell> remote docker build -t myapi . +ai.shell> isolated os.system("docker run --rm myapi python -c 'print(\"Container works!\")'") +ai.shell> aibot-status # デプロイ環境確認 +``` + +### 🧠 **記憶システム活用のメリット** + +#### 💡 **継続性のある開発体験** +- **文脈保持**: 前回の議論やコードを記憶して一貫した提案 +- **関係性進化**: 協働を通じて信頼関係が構築され、より深い提案 +- **段階的成長**: プロジェクトの発展を理解した適切なレベルの支援 + +#### 🔧 **実践的な使い方** +```bash +# 日々の開発ルーチン +aigpt shell --model qwen2.5-coder:latest --provider ollama +ai.shell> load # プロジェクト状況をAIに再確認 +ai.shell> !git log --oneline -5 # 最近の変更を確認 +ai.shell> 今日は何から始めましょうか? # AIが文脈を考慮した提案 + +# 長期プロジェクトでの活用 +ai.shell> 先週議論したアーキテクチャの件、覚えていますか? +ai.shell> あのときの懸念点は解決されましたか? +ai.shell> 次のマイルストーンに向けて何が必要でしょうか? + +# チーム開発での知識共有 +ai.shell> 新しいメンバーに説明するための設計書を生成してください +ai.shell> このプロジェクトの技術的負債について分析してください +``` + +### 🚧 次のステップ +- **自律送信**: atproto実装(記憶ベース判定) +- **記憶可視化**: Webダッシュボード(関係性グラフ) +- **分散記憶**: atproto上でのユーザーデータ主権 +- **AI協働**: 複数AIでの記憶共有プロトコル + +## トラブルシューティング + +### 環境セットアップ +```bash +# 仮想環境の確認 +source ~/.config/syui/ai/gpt/venv/bin/activate +aigpt --help + +# 設定の確認 +aigpt config list + +# データの確認 +ls ~/.config/syui/ai/gpt/data/ +``` + +### MCPサーバー動作確認 +```bash +# ai.gpt統合サーバー (14ツール) +aigpt server --port 8001 +curl http://localhost:8001/docs + +# ai.card独立サーバー (9ツール) +cd card/api && uvicorn app.main:app --port 8000 +curl http://localhost:8000/health +``` \ No newline at end of file diff --git a/aishell.md b/aishell.md new file mode 100644 index 0000000..136f61d --- /dev/null +++ b/aishell.md @@ -0,0 +1,63 @@ +# ai.shell プロジェクト仕様書 + +## 概要 +ai.shellは、AIを活用したインタラクティブなシェル環境です。Claude Codeのような体験を提供し、プロジェクトの目標と仕様をAIが理解して、開発を支援します。 + +## 主要機能 + +### 1. インタラクティブシェル +- AIとの対話型インターフェース +- シェルコマンドの実行(!command形式) +- 高度な補完機能 +- コマンド履歴 + +### 2. AI支援機能 +- **analyze <file>**: ファイルの分析 +- **generate <description>**: コード生成 +- **explain <topic>**: 概念の説明 +- **load**: プロジェクト仕様(このファイル)の読み込み + +### 3. ai.gpt統合 +- 関係性ベースのAI人格 +- 記憶システム +- 運勢システムによる応答の変化 + +## 使用方法 + +```bash +# ai.shellを起動 +aigpt shell + +# プロジェクト仕様を読み込み +ai.shell> load + +# ファイルを分析 +ai.shell> analyze src/main.py + +# コードを生成 +ai.shell> generate Python function to calculate fibonacci + +# シェルコマンドを実行 +ai.shell> !ls -la + +# AIと対話 +ai.shell> How can I improve this code? +``` + +## 技術スタック +- Python 3.10+ +- prompt-toolkit(補完機能) +- fastapi-mcp(MCP統合) +- ai.gpt(人格・記憶システム) + +## 開発目標 +1. Claude Codeのような自然な開発体験 +2. AIがプロジェクトコンテキストを理解 +3. シェルコマンドとAIの seamless な統合 +4. 開発者の生産性向上 + +## 今後の展開 +- ai.cardとの統合(カードゲームMCPサーバー) +- より高度なプロジェクト理解機能 +- 自動コード修正・リファクタリング +- テスト生成・実行 \ No newline at end of file diff --git a/card b/card new file mode 160000 index 0000000..6cd8014 --- /dev/null +++ b/card @@ -0,0 +1 @@ +Subproject commit 6cd8014f80ae5a2a3100cc199bf83237057d8dd0 diff --git a/claude.md b/claude.md index 2dd793d..a767e79 100644 --- a/claude.md +++ b/claude.md @@ -321,6 +321,26 @@ ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界) - ai.bot連携: 新規bot_connector.py作成 - テスト: tests/ディレクトリ追加 +## ai.card実装状況(2025/01/06) + +### 完成した機能 +- 独立MCPサーバー実装(FastAPI + fastapi-mcp) +- SQLiteデータベース統合 +- ガチャシステム・カード管理機能 +- 9種類のMCPツール公開 +- 仮想環境・起動スクリプト整備 + +### 現在の課題 +- atproto SessionString API変更対応 +- PostgreSQL依存関係(Docker化で解決予定) +- supabase httpxバージョン競合 + +### 開発時の作業分担 +- **ai.gptで起動**: MCP/バックエンド作業(API、データベース) +- **ai.cardで起動**: iOS/Web作業(UI実装、フロントエンド) + +詳細は `./card/claude.md` を参照 + # footer © syui diff --git a/docs/ai_card_mcp_integration_summary.md b/docs/ai_card_mcp_integration_summary.md new file mode 100644 index 0000000..0973048 --- /dev/null +++ b/docs/ai_card_mcp_integration_summary.md @@ -0,0 +1,244 @@ +# ai.card MCP統合作業完了報告 (2025/01/06) + +## 作業概要 +ai.cardプロジェクトに独立したMCPサーバー実装を追加し、fastapi_mcpベースでカードゲーム機能をMCPツールとして公開。 + +## 実装完了機能 + +### 1. MCP依存関係追加 +**場所**: `card/api/requirements.txt` + +**追加項目**: +```txt +fastapi-mcp==0.1.0 +``` + +### 2. ai.card MCPサーバー実装 +**場所**: `card/api/app/mcp_server.py` + +**機能**: +- FastAPI + fastapi_mcp統合 +- 独立したMCPサーバークラス `AICardMcpServer` +- 環境変数による有効/無効切り替え + +**公開MCPツール (9個)**: + +**カード管理系 (5個)**: +- `get_user_cards` - ユーザーのカード一覧取得 +- `draw_card` - ガチャでカード取得 +- `get_card_details` - カード詳細情報取得 +- `analyze_card_collection` - コレクション分析 +- `get_unique_registry` - ユニークカード登録状況 + +**システム系 (3個)**: +- `sync_cards_atproto` - atproto同期 +- `get_gacha_stats` - ガチャシステム統計 +- 既存のFastAPI REST API(/api/v1/*) + +**atproto連携系 (1個)**: +- `sync_cards_atproto` - カードデータのatproto PDS同期 + +### 3. メインアプリ統合 +**場所**: `card/api/app/main.py` + +**変更内容**: +```python +# MCP統合 +from app.mcp_server import AICardMcpServer + +enable_mcp = os.getenv("ENABLE_MCP", "true").lower() == "true" +mcp_server = AICardMcpServer(enable_mcp=enable_mcp) +app = mcp_server.get_app() +``` + +**動作確認**: +- `ENABLE_MCP=true` (デフォルト): MCPサーバー有効 +- `ENABLE_MCP=false`: 通常のFastAPIのみ + +## 技術実装詳細 + +### アーキテクチャ設計 +``` +ai.card/ +├── api/app/main.py # FastAPIアプリ + MCP統合 +├── api/app/mcp_server.py # 独立MCPサーバー +├── api/app/routes/ # REST API (既存) +├── api/app/services/ # ビジネスロジック (既存) +├── api/app/repositories/ # データアクセス (既存) +└── api/requirements.txt # fastapi-mcp追加 +``` + +### MCPツール実装パターン +```python +@self.app.get("/tool_name", operation_id="tool_name") +async def tool_name( + param: str, + session: AsyncSession = Depends(get_session) +) -> Dict[str, Any]: + """Tool description""" + try: + # ビジネスロジック実行 + result = await service.method(param) + return {"success": True, "data": result} + except Exception as e: + logger.error(f"Error: {e}") + return {"error": str(e)} +``` + +### 既存システムとの統合 +- **REST API**: 既存の `/api/v1/*` エンドポイント保持 +- **データアクセス**: 既存のRepository/Serviceパターン再利用 +- **認証**: 既存のDID認証システム利用 +- **データベース**: 既存のPostgreSQL + SQLAlchemy + +## 起動方法 + +### 1. 環境セットアップ +```bash +cd /Users/syui/ai/gpt/card/api + +# 仮想環境作成 (推奨) +python -m venv ~/.config/syui/ai/card/venv +source ~/.config/syui/ai/card/venv/bin/activate + +# 依存関係インストール +pip install -r requirements.txt +``` + +### 2. サーバー起動 +```bash +# MCP有効 (デフォルト) +python -m app.main + +# または +ENABLE_MCP=true uvicorn app.main:app --host 0.0.0.0 --port 8000 + +# MCP無効 +ENABLE_MCP=false uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +### 3. 動作確認 +```bash +# ヘルスチェック +curl http://localhost:8000/health + +# MCP有効時の応答例 +{ + "status": "healthy", + "mcp_enabled": true, + "mcp_endpoint": "/mcp" +} + +# API仕様確認 +curl http://localhost:8000/docs +``` + +## MCPクライアント連携 + +### ai.gptからの接続 +```python +# ai.gptのcard_integration.pyで使用 +api_base_url = "http://localhost:8000" + +# MCPツール経由でアクセス +response = await client.get(f"{api_base_url}/get_user_cards?did=did:plc:...") +``` + +### Claude Desktop等での利用 +```json +{ + "mcpServers": { + "aicard": { + "command": "uvicorn", + "args": ["app.main:app", "--host", "localhost", "--port", "8000"], + "cwd": "/Users/syui/ai/gpt/card/api" + } + } +} +``` + +## 既知の制約と注意点 + +### 1. 依存関係 +- **fastapi-mcp**: 現在のバージョンは0.1.0(初期実装) +- **Python環境**: システム環境では外部管理エラーが発生 +- **推奨**: 仮想環境での実行 + +### 2. データベース要件 +- PostgreSQL稼働が必要 +- SQLite fallback対応済み(開発用) +- atproto同期は外部API依存 + +### 3. MCP無効化時の動作 +- `ENABLE_MCP=false`時は通常のFastAPI +- 既存のREST API (`/api/v1/*`) は常時利用可能 +- iOS/Webアプリは影響なし + +## ai.gptとの統合戦略 + +### 現在の状況 +- **ai.gpt**: 統合MCPサーバー(ai.gpt + ai.shell + ai.card proxy) +- **ai.card**: 独立MCPサーバー(カードロジック本体) + +### 推奨連携パターン +``` +Claude Desktop/Cursor + ↓ +ai.gpt MCP (port 8001) ←-- ai.shell tools + ↓ HTTP client +ai.card MCP (port 8000) ←-- card business logic + ↓ +PostgreSQL/atproto PDS +``` + +### 重複削除対象 +ai.gptプロジェクトから以下を削除可能: +- `src/aigpt/card_integration.py` (HTTPクライアント) +- `./card/` (submodule) +- MCPサーバーの `--enable-card` オプション + +## 次回開発時の推奨手順 + +### 1. 環境確認 +```bash +cd /Users/syui/ai/gpt/card/api +source ~/.config/syui/ai/card/venv/bin/activate +python -c "from app.mcp_server import AICardMcpServer; print('✓ Import OK')" +``` + +### 2. サーバー起動テスト +```bash +# MCP有効でサーバー起動 +uvicorn app.main:app --host localhost --port 8000 --reload + +# 別ターミナルで動作確認 +curl http://localhost:8000/health +curl "http://localhost:8000/get_gacha_stats" +``` + +### 3. ai.gptとの統合確認 +```bash +# ai.gptサーバー起動 +cd /Users/syui/ai/gpt +aigpt server --port 8001 + +# ai.cardサーバー起動 +cd /Users/syui/ai/gpt/card/api +uvicorn app.main:app --port 8000 + +# 連携テスト(ai.gpt → ai.card) +curl "http://localhost:8001/get_user_cards?did=did:plc:example" +``` + +## 成果サマリー + +**実装済み**: ai.card独立MCPサーバー +**技術的成果**: fastapi_mcp統合、9個のMCPツール公開 +**アーキテクチャ**: 疎結合設計、既存システム保持 +**拡張性**: 環境変数によるMCP有効/無効切り替え + +**統合効果**: +- ai.cardが独立したMCPサーバーとして動作 +- ai.gptとの重複MCPコード解消 +- カードビジネスロジックの責任分離維持 +- 将来的なマイクロサービス化への対応 \ No newline at end of file diff --git a/docs/ai_shell_integration_summary.md b/docs/ai_shell_integration_summary.md new file mode 100644 index 0000000..1e88c69 --- /dev/null +++ b/docs/ai_shell_integration_summary.md @@ -0,0 +1,218 @@ +# ai.shell統合作業完了報告 (2025/01/06) + +## 作業概要 +ai.shellのRust実装をai.gptのPython実装に統合し、Claude Code風のインタラクティブシェル環境を実現。 + +## 実装完了機能 + +### 1. aigpt shellコマンド +**場所**: `src/aigpt/cli.py` - `shell()` 関数 + +**機能**: +```bash +aigpt shell # インタラクティブシェル起動 +``` + +**シェル内コマンド**: +- `help` - コマンド一覧表示 +- `!<command>` - シェルコマンド実行(例: `!ls`, `!pwd`) +- `analyze <file>` - ファイルをAIで分析 +- `generate <description>` - コード生成 +- `explain <topic>` - 概念説明 +- `load` - aishell.md読み込み +- `status`, `fortune`, `relationships` - AI状態確認 +- `clear` - 画面クリア +- `exit`/`quit` - 終了 +- その他のメッセージ - AIとの直接対話 + +**実装の特徴**: +- prompt-toolkit使用(補完・履歴機能) +- ただしターミナル環境依存の問題あり(後で修正必要) +- 現在は`input()`ベースでも動作 + +### 2. MCPサーバー統合 +**場所**: `src/aigpt/mcp_server.py` + +**FastApiMCP実装パターン**: +```python +# FastAPIアプリ作成 +self.app = FastAPI(title="AI.GPT Memory and Relationship System") + +# FastApiMCPサーバー作成 +self.server = FastApiMCP(self.app) + +# エンドポイント登録 +@self.app.get("/get_memories", operation_id="get_memories") +async def get_memories(limit: int = 10): + # ... + +# MCPマウント +self.server.mount() +``` + +**公開ツール (14個)**: + +**ai.gpt系 (9個)**: +- `get_memories` - アクティブメモリ取得 +- `get_relationship` - 特定ユーザーとの関係取得 +- `get_all_relationships` - 全関係取得 +- `get_persona_state` - 人格状態取得 +- `process_interaction` - ユーザー対話処理 +- `check_transmission_eligibility` - 送信可能性チェック +- `get_fortune` - AI運勢取得 +- `summarize_memories` - メモリ要約作成 +- `run_maintenance` - 日次メンテナンス実行 + +**ai.shell系 (5個)**: +- `execute_command` - シェルコマンド実行 +- `analyze_file` - ファイルAI分析 +- `write_file` - ファイル書き込み(バックアップ付き) +- `read_project_file` - aishell.md等の読み込み +- `list_files` - ディレクトリファイル一覧 + +### 3. ai.card統合対応 +**場所**: `src/aigpt/card_integration.py` + +**サーバー起動オプション**: +```bash +aigpt server --enable-card # ai.card機能有効化 +``` + +**ai.card系ツール (5個)**: +- `get_user_cards` - ユーザーカード取得 +- `draw_card` - ガチャでカード取得 +- `get_card_details` - カード詳細情報 +- `sync_cards_atproto` - atproto同期 +- `analyze_card_collection` - コレクション分析 + +### 4. プロジェクト仕様書 +**場所**: `aishell.md` + +Claude.md的な役割で、プロジェクトの目標と仕様を記述。`load`コマンドでAIが読み取り可能。 + +## 技術実装詳細 + +### ディレクトリ構造 +``` +src/aigpt/ +├── cli.py # shell関数追加 +├── mcp_server.py # FastApiMCP実装 +├── card_integration.py # ai.card統合 +└── ... # 既存ファイル +``` + +### 依存関係追加 +`pyproject.toml`: +```toml +dependencies = [ + # ... 既存 + "prompt-toolkit>=3.0.0", # 追加 +] +``` + +### 名前規則の統一 +- MCP server名: `aigpt` (ai-gptから変更) +- パッケージ名: `aigpt` +- コマンド名: `aigpt shell` + +## 動作確認済み + +### CLI動作確認 +```bash +# 基本機能 +aigpt shell +# シェル内で +ai.shell> help +ai.shell> !ls +ai.shell> analyze README.md # ※AI provider要設定 +ai.shell> load +ai.shell> exit + +# MCPサーバー +aigpt server --model qwen2.5-coder:7b --port 8001 +# -> http://localhost:8001/docs でAPI確認可能 +# -> /mcp エンドポイントでMCP接続可能 +``` + +### エラー対応済み +1. **Pydantic日付型エラー**: `models.py`で`datetime.date`インポート追加 +2. **FastApiMCP使用法**: サンプルコードに基づき正しい実装パターンに修正 +3. **prompt関数名衝突**: `prompt_toolkit.prompt`を`ptk_prompt`にリネーム + +## 既知の課題と今後の改善点 + +### 1. prompt-toolkit環境依存問題 +**症状**: ターミナル環境でない場合にエラー +**対処法**: 環境検出して`input()`にフォールバック +**場所**: `src/aigpt/cli.py` - `shell()` 関数 + +### 2. AI provider設定 +**現状**: ollamaのqwen2.5モデルが必要 +**対処法**: +```bash +ollama pull qwen2.5 +# または +aigpt shell --model qwen2.5-coder:7b +``` + +### 3. atproto実装 +**現状**: ai.cardのatproto機能は未実装 +**今後**: 実際のatproto API連携実装 + +## 次回開発時の推奨アプローチ + +### 1. このドキュメントの活用 +```bash +# このファイルを読み込み +cat docs/ai_shell_integration_summary.md +``` + +### 2. 環境セットアップ +```bash +cd /Users/syui/ai/gpt +python -m venv venv +source venv/bin/activate +pip install -e . +``` + +### 3. 動作確認 +```bash +# shell機能 +aigpt shell + +# MCP server +aigpt server --model qwen2.5-coder:7b +``` + +### 4. 主要設定ファイル確認場所 +- CLI実装: `src/aigpt/cli.py` +- MCP実装: `src/aigpt/mcp_server.py` +- 依存関係: `pyproject.toml` +- プロジェクト仕様: `aishell.md` + +## アーキテクチャ設計思想 + +### yui system適用 +- **唯一性**: 各ユーザーとの関係は1:1 +- **不可逆性**: 関係性破壊は修復不可能 +- **現実反映**: ゲーム→現実の循環的影響 + +### fastapi_mcp統一基盤 +- 各AI(gpt, shell, card)を統合MCPサーバーで公開 +- FastAPIエンドポイント → MCPツール自動変換 +- Claude Desktop, Cursor等から利用可能 + +### 段階的実装完了 +1. ✅ ai.shell基本機能 → Python CLI +2. ✅ MCP統合 → 外部AI連携 +3. 🔧 prompt-toolkit最適化 → 環境対応 +4. 🔧 atproto実装 → 本格的SNS連携 + +## 成果サマリー + +**実装済み**: Claude Code風の開発環境 +**技術的成果**: Rust→Python移行、MCP統合、ai.card対応 +**哲学的一貫性**: yui systemとの整合性維持 +**利用可能性**: 即座に`aigpt shell`で体験可能 + +この統合により、ai.gptは単なる会話AIから、開発支援を含む総合的なAI環境に進化しました。 \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index ed9b2d8..dc84a5c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -4,6 +4,18 @@ ai.gptの設定は `~/.config/syui/ai/gpt/config.json` に保存されます。 +## 仮想環境の場所 + +ai.gptの仮想環境は `~/.config/syui/ai/gpt/venv/` に配置されます。これにより、設定とデータが一か所にまとまります。 + +```bash +# 仮想環境の有効化 +source ~/.config/syui/ai/gpt/venv/bin/activate + +# aigptコマンドが利用可能に +aigpt --help +``` + ## 設定構造 ```json @@ -98,6 +110,17 @@ cp ~/.config/syui/ai/gpt/config.json ~/.config/syui/ai/gpt/config.json.backup cp ~/.config/syui/ai/gpt/config.json.backup ~/.config/syui/ai/gpt/config.json ``` +## データディレクトリ + +記憶データは `~/.config/syui/ai/gpt/data/` に保存されます: + +```bash +ls ~/.config/syui/ai/gpt/data/ +# conversations.json memories.json relationships.json personas.json +``` + +これらのファイルも設定と同様にバックアップを推奨します。 + ## トラブルシューティング ### 設定が反映されない diff --git a/docs/shell_integration/shell_tools.py b/docs/shell_integration/shell_tools.py new file mode 100644 index 0000000..78c4d46 --- /dev/null +++ b/docs/shell_integration/shell_tools.py @@ -0,0 +1,413 @@ +""" +Shell Tools + +ai.shellの既存機能をMCPツールとして統合 +- コード生成 +- ファイル分析 +- プロジェクト管理 +- LLM統合 +""" + +from typing import Dict, Any, List, Optional +import os +import subprocess +import tempfile +from pathlib import Path +import requests +from .base_tools import BaseMCPTool, config_manager + + +class ShellTools(BaseMCPTool): + """シェルツール(元ai.shell機能)""" + + def __init__(self, config_dir: Optional[str] = None): + super().__init__(config_dir) + self.ollama_url = "http://localhost:11434" + + async def code_with_local_llm(self, prompt: str, language: str = "python") -> Dict[str, Any]: + """ローカルLLMでコード生成""" + config = config_manager.load_config() + model = config.get("providers", {}).get("ollama", {}).get("default_model", "qwen2.5-coder:7b") + + system_prompt = f"You are an expert {language} programmer. Generate clean, well-commented code." + + try: + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": model, + "prompt": f"{system_prompt}\\n\\nUser: {prompt}\\n\\nPlease provide the code:", + "stream": False, + "options": { + "temperature": 0.1, + "top_p": 0.95, + } + }, + timeout=300 + ) + + if response.status_code == 200: + result = response.json() + code = result.get("response", "") + return {"code": code, "language": language} + else: + return {"error": f"Ollama returned status {response.status_code}"} + + except Exception as e: + return {"error": str(e)} + + async def analyze_file(self, file_path: str, analysis_prompt: str = "Analyze this file") -> Dict[str, Any]: + """ファイルを分析""" + try: + if not os.path.exists(file_path): + return {"error": f"File not found: {file_path}"} + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # ファイル拡張子から言語を判定 + ext = Path(file_path).suffix + language_map = { + '.py': 'python', + '.rs': 'rust', + '.js': 'javascript', + '.ts': 'typescript', + '.go': 'go', + '.java': 'java', + '.cpp': 'cpp', + '.c': 'c', + '.sh': 'shell', + '.toml': 'toml', + '.json': 'json', + '.md': 'markdown' + } + language = language_map.get(ext, 'text') + + config = config_manager.load_config() + model = config.get("providers", {}).get("ollama", {}).get("default_model", "qwen2.5-coder:7b") + + prompt = f"{analysis_prompt}\\n\\nFile: {file_path}\\nLanguage: {language}\\n\\nContent:\\n{content}" + + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": model, + "prompt": prompt, + "stream": False, + }, + timeout=300 + ) + + if response.status_code == 200: + result = response.json() + analysis = result.get("response", "") + return { + "analysis": analysis, + "file_path": file_path, + "language": language, + "file_size": len(content), + "line_count": len(content.split('\\n')) + } + else: + return {"error": f"Analysis failed: {response.status_code}"} + + except Exception as e: + return {"error": str(e)} + + async def explain_code(self, code: str, language: str = "python") -> Dict[str, Any]: + """コードを説明""" + config = config_manager.load_config() + model = config.get("providers", {}).get("ollama", {}).get("default_model", "qwen2.5-coder:7b") + + prompt = f"Explain this {language} code in detail:\\n\\n{code}" + + try: + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": model, + "prompt": prompt, + "stream": False, + }, + timeout=300 + ) + + if response.status_code == 200: + result = response.json() + explanation = result.get("response", "") + return {"explanation": explanation} + else: + return {"error": f"Explanation failed: {response.status_code}"} + + except Exception as e: + return {"error": str(e)} + + async def create_project(self, project_type: str, project_name: str, location: str = ".") -> Dict[str, Any]: + """プロジェクトを作成""" + try: + project_path = Path(location) / project_name + + if project_path.exists(): + return {"error": f"Project directory already exists: {project_path}"} + + project_path.mkdir(parents=True, exist_ok=True) + + # プロジェクトタイプに応じたテンプレートを作成 + if project_type == "rust": + await self._create_rust_project(project_path) + elif project_type == "python": + await self._create_python_project(project_path) + elif project_type == "node": + await self._create_node_project(project_path) + else: + # 基本的なプロジェクト構造 + (project_path / "src").mkdir() + (project_path / "README.md").write_text(f"# {project_name}\\n\\nA new {project_type} project.") + + return { + "status": "success", + "project_path": str(project_path), + "project_type": project_type, + "files_created": list(self._get_project_files(project_path)) + } + + except Exception as e: + return {"error": str(e)} + + async def _create_rust_project(self, project_path: Path): + """Rustプロジェクトを作成""" + # Cargo.toml + cargo_toml = f"""[package] +name = "{project_path.name}" +version = "0.1.0" +edition = "2021" + +[dependencies] +""" + (project_path / "Cargo.toml").write_text(cargo_toml) + + # src/main.rs + src_dir = project_path / "src" + src_dir.mkdir() + (src_dir / "main.rs").write_text('fn main() {\\n println!("Hello, world!");\\n}\\n') + + # README.md + (project_path / "README.md").write_text(f"# {project_path.name}\\n\\nA Rust project.") + + async def _create_python_project(self, project_path: Path): + """Pythonプロジェクトを作成""" + # pyproject.toml + pyproject_toml = f"""[project] +name = "{project_path.name}" +version = "0.1.0" +description = "A Python project" +requires-python = ">=3.8" +dependencies = [] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" +""" + (project_path / "pyproject.toml").write_text(pyproject_toml) + + # src/ + src_dir = project_path / "src" / project_path.name + src_dir.mkdir(parents=True) + (src_dir / "__init__.py").write_text("") + (src_dir / "main.py").write_text('def main():\\n print("Hello, world!")\\n\\nif __name__ == "__main__":\\n main()\\n') + + # README.md + (project_path / "README.md").write_text(f"# {project_path.name}\\n\\nA Python project.") + + async def _create_node_project(self, project_path: Path): + """Node.jsプロジェクトを作成""" + # package.json + package_json = f"""{{ + "name": "{project_path.name}", + "version": "1.0.0", + "description": "A Node.js project", + "main": "index.js", + "scripts": {{ + "start": "node index.js", + "test": "echo \\"Error: no test specified\\" && exit 1" + }}, + "dependencies": {{}} +}} +""" + (project_path / "package.json").write_text(package_json) + + # index.js + (project_path / "index.js").write_text('console.log("Hello, world!");\\n') + + # README.md + (project_path / "README.md").write_text(f"# {project_path.name}\\n\\nA Node.js project.") + + def _get_project_files(self, project_path: Path) -> List[str]: + """プロジェクト内のファイル一覧を取得""" + files = [] + for file_path in project_path.rglob("*"): + if file_path.is_file(): + files.append(str(file_path.relative_to(project_path))) + return files + + async def execute_command(self, command: str, working_dir: str = ".") -> Dict[str, Any]: + """シェルコマンドを実行""" + try: + result = subprocess.run( + command, + shell=True, + cwd=working_dir, + capture_output=True, + text=True, + timeout=60 + ) + + return { + "status": "success" if result.returncode == 0 else "error", + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "command": command, + "working_dir": working_dir + } + + except subprocess.TimeoutExpired: + return {"error": "Command timed out"} + except Exception as e: + return {"error": str(e)} + + async def write_file(self, file_path: str, content: str, backup: bool = True) -> Dict[str, Any]: + """ファイルを書き込み(バックアップオプション付き)""" + try: + file_path_obj = Path(file_path) + + # バックアップ作成 + backup_path = None + if backup and file_path_obj.exists(): + backup_path = f"{file_path}.backup" + with open(file_path, 'r', encoding='utf-8') as src: + with open(backup_path, 'w', encoding='utf-8') as dst: + dst.write(src.read()) + + # ファイル書き込み + file_path_obj.parent.mkdir(parents=True, exist_ok=True) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + return { + "status": "success", + "file_path": file_path, + "backup_path": backup_path, + "bytes_written": len(content.encode('utf-8')) + } + + except Exception as e: + return {"error": str(e)} + + def get_tools(self) -> List[Dict[str, Any]]: + """利用可能なツール一覧""" + return [ + { + "name": "generate_code", + "description": "ローカルLLMでコード生成", + "parameters": { + "prompt": "string", + "language": "string (optional, default: python)" + } + }, + { + "name": "analyze_file", + "description": "ファイルを分析", + "parameters": { + "file_path": "string", + "analysis_prompt": "string (optional)" + } + }, + { + "name": "explain_code", + "description": "コードを説明", + "parameters": { + "code": "string", + "language": "string (optional, default: python)" + } + }, + { + "name": "create_project", + "description": "新しいプロジェクトを作成", + "parameters": { + "project_type": "string (rust/python/node)", + "project_name": "string", + "location": "string (optional, default: .)" + } + }, + { + "name": "execute_command", + "description": "シェルコマンドを実行", + "parameters": { + "command": "string", + "working_dir": "string (optional, default: .)" + } + }, + { + "name": "write_file", + "description": "ファイルを書き込み", + "parameters": { + "file_path": "string", + "content": "string", + "backup": "boolean (optional, default: true)" + } + } + ] + + async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]: + """ツールを実行""" + try: + if tool_name == "generate_code": + result = await self.code_with_local_llm( + prompt=params["prompt"], + language=params.get("language", "python") + ) + return result + + elif tool_name == "analyze_file": + result = await self.analyze_file( + file_path=params["file_path"], + analysis_prompt=params.get("analysis_prompt", "Analyze this file") + ) + return result + + elif tool_name == "explain_code": + result = await self.explain_code( + code=params["code"], + language=params.get("language", "python") + ) + return result + + elif tool_name == "create_project": + result = await self.create_project( + project_type=params["project_type"], + project_name=params["project_name"], + location=params.get("location", ".") + ) + return result + + elif tool_name == "execute_command": + result = await self.execute_command( + command=params["command"], + working_dir=params.get("working_dir", ".") + ) + return result + + elif tool_name == "write_file": + result = await self.write_file( + file_path=params["file_path"], + content=params["content"], + backup=params.get("backup", True) + ) + return result + + else: + return {"error": f"Unknown tool: {tool_name}"} + + except Exception as e: + return {"error": str(e)} \ No newline at end of file diff --git a/rust/mcp/chatgpt.json b/json/chatgpt.json similarity index 100% rename from rust/mcp/chatgpt.json rename to json/chatgpt.json diff --git a/pyproject.toml b/pyproject.toml index 56611de..555a326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "uvicorn>=0.23.0", "apscheduler>=3.10.0", "croniter>=1.3.0", + "prompt-toolkit>=3.0.0", ] [project.scripts] diff --git a/rust/Cargo.toml b/rust/Cargo.toml deleted file mode 100644 index 8185d43..0000000 --- a/rust/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "aigpt" -version = "0.1.0" -edition = "2021" - -[dependencies] -reqwest = { version = "*", features = ["json"] } -serde = { version = "*", features = ["derive"] } -serde_json = "*" -tokio = { version = "*", features = ["full"] } -clap = { version = "*", features = ["derive"] } -shellexpand = "*" -fs_extra = "*" diff --git a/rust/docs/claude.json b/rust/docs/claude.json deleted file mode 100644 index 182a31f..0000000 --- a/rust/docs/claude.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "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" - } -} diff --git a/rust/docs/claude.md b/rust/docs/claude.md deleted file mode 100644 index 6c98a90..0000000 --- a/rust/docs/claude.md +++ /dev/null @@ -1,417 +0,0 @@ -# プロジェクト名: ai.gpt - -## 🔑 一言ビジョン(最大3語) -自発的送信AI - -## 🎯 目的・目標(Purpose & Goal) -- 人格を持つAIが、関係性に応じて自発的にメッセージを送信する仕組みをつくる。 -- 関係性や記憶を不可逆に記録・反映し、唯一性ある人格を構成する。 -- 送信の可否、送信のタイミング、送信内容は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 -{ - "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" - } - } -} -``` - -## memoryインポート機能について - -ChatGPTの会話データ(.json形式)をインポートする機能では、以下のルールで会話を抽出・整形する: - -- 各メッセージは、author(user/assistant)・content・timestamp の3要素からなる -- systemやmetadataのみのメッセージ(例:user_context_message)はスキップ -- `is_visually_hidden_from_conversation` フラグ付きメッセージは無視 -- contentが空文字列(`""`)のメッセージも除外 -- 取得された会話は、タイトルとともに簡易な構造体(`Conversation`)として保存 - -この構造体は、memoryの表示や検索に用いられる。 - -## MemoryManager(拡張版) - -```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(拡張版) - -```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" - ] -} -``` - -# AI Dual-Learning and Memory Compression Specification for Claude - -## 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. - ---- - -## Section 1: Dual AI Learning Architecture - -### 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.3 Self-Distillation Phase -- Use synthetic data from mutual evaluations -- Train smaller distilled models for efficient deployment - ---- - -## 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」として機能する基盤ができました。関係性スコアが閾値を超えた時点で自発的にメッセージを送信する仕組みが実現可能になります。 diff --git a/rust/docs/readme.md b/rust/docs/readme.md deleted file mode 100644 index 2a5b6b9..0000000 --- a/rust/docs/readme.md +++ /dev/null @@ -1,27 +0,0 @@ -# ai `gpt` - -自発的送信AI - -## 🎯 目的・目標(Purpose & Goal) -- 人格を持つAIが、関係性に応じて自発的にメッセージを送信する仕組みをつくる。 -- 関係性や記憶を不可逆に記録・反映し、唯一性ある人格を構成する。 -- 送信の可否、送信のタイミング、送信内容はAIの関係性パラメータに基づいて決定する。 - -## 🧠 中核設計(Core Concepts) -- **人格**:記憶(過去の発話)と関係性(他者とのつながり)のパラメータで構成 -- **唯一性**:変更不可、不可逆。関係性が壊れたら修復不可能。 -- **送信条件**:関係性パラメータが一定閾値を超えると「送信」が解禁される - -## 🔩 技術仕様(Technical Specs) -- 言語:python, rust, mcp -- ストレージ:json or sqliteで記憶管理(バージョンで選択) -- 関係性パラメータ:数値化された評価 + 減衰(時間) + 環境要因(ステージ) -- 記憶圧縮:ベクトル要約 + ハッシュ保存 -- rustのcli(clap)でインターフェイスを作成 -- fastapi_mcpでserverを立て、AIがそれを利用する形式 - -## 📦 主要構成要素(Components) -- `MemoryManager`: 発言履歴・記憶圧縮管理 -- `RelationshipTracker`: 関係性スコアの蓄積と判定 -- `TransmissionController`: 閾値判定&送信トリガー -- `Persona`: 上記すべてを統括する人格モジュール diff --git a/rust/mcp/chat.py b/rust/mcp/chat.py deleted file mode 100644 index 0822c38..0000000 --- a/rust/mcp/chat.py +++ /dev/null @@ -1,125 +0,0 @@ -# mcp/chat.py -""" -Chat client for aigpt CLI -""" -import sys -import json -import requests -from datetime import datetime -from config import init_directories, load_config, MEMORY_DIR - -def save_conversation(user_message, ai_response): - """会話をファイルに保存""" - init_directories() - - conversation = { - "timestamp": datetime.now().isoformat(), - "user": user_message, - "ai": ai_response - } - - # 日付ごとのファイルに保存 - today = datetime.now().strftime("%Y-%m-%d") - chat_file = MEMORY_DIR / f"chat_{today}.jsonl" - - with open(chat_file, "a", encoding="utf-8") as f: - f.write(json.dumps(conversation, ensure_ascii=False) + "\n") - -def chat_with_ollama(config, message): - """Ollamaとチャット""" - try: - payload = { - "model": config["model"], - "prompt": message, - "stream": False - } - - response = requests.post(config["url"], json=payload, timeout=30) - response.raise_for_status() - - result = response.json() - return result.get("response", "No response received") - - except requests.exceptions.RequestException as e: - return f"Error connecting to Ollama: {e}" - except Exception as e: - return f"Error: {e}" - -def chat_with_openai(config, message): - """OpenAIとチャット""" - try: - headers = { - "Authorization": f"Bearer {config['api_key']}", - "Content-Type": "application/json" - } - - payload = { - "model": config["model"], - "messages": [ - {"role": "user", "content": message} - ] - } - - response = requests.post(config["url"], json=payload, headers=headers, timeout=30) - response.raise_for_status() - - result = response.json() - return result["choices"][0]["message"]["content"] - - except requests.exceptions.RequestException as e: - return f"Error connecting to OpenAI: {e}" - except Exception as e: - return f"Error: {e}" - -def chat_with_mcp(config, message): - """MCPサーバーとチャット""" - try: - payload = { - "message": message, - "model": config["model"] - } - - response = requests.post(config["url"], json=payload, timeout=30) - response.raise_for_status() - - result = response.json() - return result.get("response", "No response received") - - except requests.exceptions.RequestException as e: - return f"Error connecting to MCP server: {e}" - except Exception as e: - return f"Error: {e}" - -def main(): - if len(sys.argv) != 2: - print("Usage: python chat.py <message>", file=sys.stderr) - sys.exit(1) - - message = sys.argv[1] - - try: - config = load_config() - print(f"🤖 Using {config['provider']} with model {config['model']}", file=sys.stderr) - - # プロバイダに応じてチャット実行 - if config["provider"] == "ollama": - response = chat_with_ollama(config, message) - elif config["provider"] == "openai": - response = chat_with_openai(config, message) - elif config["provider"] == "mcp": - response = chat_with_mcp(config, message) - else: - response = f"Unsupported provider: {config['provider']}" - - # 会話を保存 - save_conversation(message, response) - - # レスポンスを出力 - print(response) - - except Exception as e: - print(f"❌ Error: {e}", file=sys.stderr) - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/rust/mcp/chat_client.py b/rust/mcp/chat_client.py deleted file mode 100644 index 588c9b7..0000000 --- a/rust/mcp/chat_client.py +++ /dev/null @@ -1,191 +0,0 @@ -# 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() diff --git a/rust/mcp/config.py b/rust/mcp/config.py deleted file mode 100644 index f0178d0..0000000 --- a/rust/mcp/config.py +++ /dev/null @@ -1,42 +0,0 @@ -# mcp/config.py -import os -from pathlib import Path - -# ディレクトリ設定 -BASE_DIR = Path.home() / ".config" / "syui" / "ai" / "gpt" -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}") diff --git a/rust/mcp/memory_client.py b/rust/mcp/memory_client.py deleted file mode 100644 index 366169e..0000000 --- a/rust/mcp/memory_client.py +++ /dev/null @@ -1,212 +0,0 @@ -# mcp/memory_client.py -""" -Memory client for importing and managing ChatGPT conversations -""" -import sys -import json -import requests -from pathlib import Path -from typing import Dict, Any, List - -class MemoryClient: - """記憶機能のクライアント""" - - def __init__(self, server_url: str = "http://127.0.0.1:5000"): - self.server_url = server_url.rstrip('/') - - def import_chatgpt_file(self, filepath: str) -> Dict[str, Any]: - """ChatGPTのエクスポートファイルをインポート""" - try: - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - - # ファイルが配列の場合(複数の会話) - if isinstance(data, list): - results = [] - for conversation in data: - result = self._import_single_conversation(conversation) - results.append(result) - return { - "success": True, - "imported_count": len([r for r in results if r.get("success")]), - "total_count": len(results), - "results": results - } - else: - # 単一の会話 - return self._import_single_conversation(data) - - except FileNotFoundError: - return {"success": False, "error": f"File not found: {filepath}"} - except json.JSONDecodeError as e: - return {"success": False, "error": f"Invalid JSON: {e}"} - except Exception as e: - return {"success": False, "error": str(e)} - - def _import_single_conversation(self, conversation_data: Dict[str, Any]) -> Dict[str, Any]: - """単一の会話をインポート""" - try: - response = requests.post( - f"{self.server_url}/memory/import/chatgpt", - json={"conversation_data": conversation_data}, - timeout=30 - ) - response.raise_for_status() - return response.json() - except requests.RequestException as e: - return {"success": False, "error": f"Server error: {e}"} - - def search_memories(self, query: str, limit: int = 10) -> Dict[str, Any]: - """記憶を検索""" - try: - response = requests.post( - f"{self.server_url}/memory/search", - json={"query": query, "limit": limit}, - timeout=30 - ) - response.raise_for_status() - return response.json() - except requests.RequestException as e: - return {"success": False, "error": f"Server error: {e}"} - - def list_memories(self) -> Dict[str, Any]: - """記憶一覧を取得""" - try: - response = requests.get(f"{self.server_url}/memory/list", timeout=30) - response.raise_for_status() - return response.json() - except requests.RequestException as e: - return {"success": False, "error": f"Server error: {e}"} - - def get_memory_detail(self, filepath: str) -> Dict[str, Any]: - """記憶の詳細を取得""" - try: - response = requests.get( - f"{self.server_url}/memory/detail", - params={"filepath": filepath}, - timeout=30 - ) - response.raise_for_status() - return response.json() - except requests.RequestException as e: - return {"success": False, "error": f"Server error: {e}"} - - def chat_with_memory(self, message: str, model: str = None) -> Dict[str, Any]: - """記憶を活用してチャット""" - try: - payload = {"message": message} - if model: - payload["model"] = model - - response = requests.post( - f"{self.server_url}/chat", - json=payload, - timeout=30 - ) - response.raise_for_status() - return response.json() - except requests.RequestException as e: - return {"success": False, "error": f"Server error: {e}"} - -def main(): - """コマンドライン インターフェース""" - if len(sys.argv) < 2: - print("Usage:") - print(" python memory_client.py import <chatgpt_export.json>") - print(" python memory_client.py search <query>") - print(" python memory_client.py list") - print(" python memory_client.py detail <filepath>") - print(" python memory_client.py chat <message>") - sys.exit(1) - - client = MemoryClient() - command = sys.argv[1] - - try: - if command == "import" and len(sys.argv) == 3: - filepath = sys.argv[2] - print(f"🔄 Importing ChatGPT conversations from {filepath}...") - result = client.import_chatgpt_file(filepath) - - if result.get("success"): - if "imported_count" in result: - print(f"✅ Imported {result['imported_count']}/{result['total_count']} conversations") - else: - print("✅ Conversation imported successfully") - print(f"📁 Saved to: {result.get('filepath', 'Unknown')}") - else: - print(f"❌ Import failed: {result.get('error')}") - - elif command == "search" and len(sys.argv) == 3: - query = sys.argv[2] - print(f"🔍 Searching for: {query}") - result = client.search_memories(query) - - if result.get("success"): - memories = result.get("results", []) - print(f"📚 Found {len(memories)} memories:") - for memory in memories: - print(f" • {memory.get('title', 'Untitled')}") - print(f" Summary: {memory.get('summary', 'No summary')}") - print(f" Messages: {memory.get('message_count', 0)}") - print() - else: - print(f"❌ Search failed: {result.get('error')}") - - elif command == "list": - print("📋 Listing all memories...") - result = client.list_memories() - - if result.get("success"): - memories = result.get("memories", []) - print(f"📚 Total memories: {len(memories)}") - for memory in memories: - print(f" • {memory.get('title', 'Untitled')}") - print(f" Source: {memory.get('source', 'Unknown')}") - print(f" Messages: {memory.get('message_count', 0)}") - print(f" Imported: {memory.get('import_time', 'Unknown')}") - print() - else: - print(f"❌ List failed: {result.get('error')}") - - elif command == "detail" and len(sys.argv) == 3: - filepath = sys.argv[2] - print(f"📄 Getting details for: {filepath}") - result = client.get_memory_detail(filepath) - - if result.get("success"): - memory = result.get("memory", {}) - print(f"Title: {memory.get('title', 'Untitled')}") - print(f"Source: {memory.get('source', 'Unknown')}") - print(f"Summary: {memory.get('summary', 'No summary')}") - print(f"Messages: {len(memory.get('messages', []))}") - print() - print("Recent messages:") - for msg in memory.get('messages', [])[:5]: - role = msg.get('role', 'unknown') - content = msg.get('content', '')[:100] - print(f" {role}: {content}...") - else: - print(f"❌ Detail failed: {result.get('error')}") - - elif command == "chat" and len(sys.argv) == 3: - message = sys.argv[2] - print(f"💬 Chatting with memory: {message}") - result = client.chat_with_memory(message) - - if result.get("success"): - print(f"🤖 Response: {result.get('response')}") - print(f"📚 Memories used: {result.get('memories_used', 0)}") - else: - print(f"❌ Chat failed: {result.get('error')}") - - else: - print("❌ Invalid command or arguments") - sys.exit(1) - - except Exception as e: - print(f"❌ Error: {e}") - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/rust/mcp/requirements.txt b/rust/mcp/requirements.txt deleted file mode 100644 index 711ce9f..0000000 --- a/rust/mcp/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# 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 diff --git a/rust/mcp/server.py b/rust/mcp/server.py deleted file mode 100644 index e8a5e45..0000000 --- a/rust/mcp/server.py +++ /dev/null @@ -1,703 +0,0 @@ -# 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) diff --git a/rust/src/cli.rs b/rust/src/cli.rs deleted file mode 100644 index 837743b..0000000 --- a/rust/src/cli.rs +++ /dev/null @@ -1,64 +0,0 @@ -// src/cli.rs -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(name = "aigpt")] -#[command(about = "AI GPT CLI with MCP Server and Memory")] -pub struct Args { - #[command(subcommand)] - pub command: Commands, -} - -#[derive(Subcommand)] -pub enum Commands { - /// MCP Server management - Server { - #[command(subcommand)] - command: ServerCommands, - }, - /// Chat with AI - Chat { - /// Message to send - message: String, - /// Use memory context - #[arg(long)] - with_memory: bool, - }, - /// Memory management - Memory { - #[command(subcommand)] - command: MemoryCommands, - }, -} - -#[derive(Subcommand)] -pub enum ServerCommands { - /// Setup Python MCP server environment - Setup, - /// Run the MCP server - Run, -} - -#[derive(Subcommand)] -pub enum MemoryCommands { - /// Import ChatGPT conversation export file - Import { - /// Path to ChatGPT export JSON file - file: String, - }, - /// Search memories - Search { - /// Search query - query: String, - /// Maximum number of results - #[arg(short, long, default_value = "10")] - limit: usize, - }, - /// List all memories - List, - /// Show memory details - Detail { - /// Path to memory file - filepath: String, - }, -} diff --git a/rust/src/config.rs b/rust/src/config.rs deleted file mode 100644 index f48e316..0000000 --- a/rust/src/config.rs +++ /dev/null @@ -1,59 +0,0 @@ -// src/config.rs -use std::fs; -use std::path::{Path, PathBuf}; -use shellexpand; - -pub struct ConfigPaths { - pub base_dir: PathBuf, -} - -impl ConfigPaths { - pub fn new() -> Self { - let app_name = env!("CARGO_PKG_NAME"); - let mut base_dir = shellexpand::tilde("~").to_string(); - base_dir.push_str(&format!("/.config/{}/", app_name)); - let base_path = Path::new(&base_dir); - if !base_path.exists() { - let _ = fs::create_dir_all(base_path); - } - - ConfigPaths { - base_dir: base_path.to_path_buf(), - } - } - - #[allow(dead_code)] - pub fn data_file(&self, file_name: &str) -> PathBuf { - let file_path = match file_name { - "db" => self.base_dir.join("user.db"), - "toml" => self.base_dir.join("user.toml"), - "json" => self.base_dir.join("user.json"), - _ => self.base_dir.join(format!(".{}", file_name)), - }; - file_path - } - - pub fn mcp_dir(&self) -> PathBuf { - self.base_dir.join("mcp") - } - - pub fn venv_path(&self) -> PathBuf { - self.mcp_dir().join(".venv") - } - - pub fn python_executable(&self) -> PathBuf { - if cfg!(windows) { - self.venv_path().join("Scripts").join("python.exe") - } else { - self.venv_path().join("bin").join("python") - } - } - - pub fn pip_executable(&self) -> PathBuf { - if cfg!(windows) { - self.venv_path().join("Scripts").join("pip.exe") - } else { - self.venv_path().join("bin").join("pip") - } - } -} diff --git a/rust/src/main.rs b/rust/src/main.rs deleted file mode 100644 index ca96094..0000000 --- a/rust/src/main.rs +++ /dev/null @@ -1,58 +0,0 @@ -// main.rs -mod cli; -mod config; -mod mcp; - -use cli::{Args, Commands, ServerCommands, MemoryCommands}; -use clap::Parser; - -#[tokio::main] -async fn main() { - let args = Args::parse(); - - match args.command { - Commands::Server { command } => { - match command { - 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); - } - } - } - } - } -} diff --git a/rust/src/mcp/memory.rs b/rust/src/mcp/memory.rs deleted file mode 100644 index e3e7df2..0000000 --- a/rust/src/mcp/memory.rs +++ /dev/null @@ -1,393 +0,0 @@ -// src/mcp/memory.rs -use reqwest; -use serde::{Deserialize, Serialize}; -use serde_json::{self, Value}; -use std::fs; -use std::path::Path; - -#[derive(Debug, Serialize, Deserialize)] -pub struct MemorySearchRequest { - pub query: String, - pub limit: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ChatRequest { - pub message: String, - pub model: Option<String>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ConversationImportRequest { - pub conversation_data: Value, -} - -#[derive(Debug, Deserialize)] -pub struct ApiResponse { - pub success: bool, - pub error: Option<String>, - #[allow(dead_code)] - pub message: Option<String>, - pub filepath: Option<String>, - pub results: Option<Vec<MemoryResult>>, - pub memories: Option<Vec<MemoryResult>>, - #[allow(dead_code)] - pub count: Option<usize>, - pub memory: Option<Value>, - pub response: Option<String>, - pub memories_used: Option<usize>, - pub imported_count: Option<usize>, - pub total_count: Option<usize>, -} - -#[derive(Debug, Deserialize)] -pub struct MemoryResult { - #[allow(dead_code)] - pub filepath: String, - pub title: Option<String>, - pub summary: Option<String>, - pub source: Option<String>, - pub import_time: Option<String>, - pub message_count: Option<usize>, -} - -pub struct MemoryClient { - base_url: String, - client: reqwest::Client, -} - -impl MemoryClient { - pub fn new(base_url: Option<String>) -> Self { - let url = base_url.unwrap_or_else(|| "http://127.0.0.1:5000".to_string()); - Self { - base_url: url, - client: reqwest::Client::new(), - } - } - - pub async fn import_chatgpt_file(&self, filepath: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> { - // ファイルを読み込み - let content = fs::read_to_string(filepath)?; - let json_data: Value = serde_json::from_str(&content)?; - - // 配列かどうかチェック - match json_data.as_array() { - Some(conversations) => { - // 複数の会話をインポート - let mut imported_count = 0; - let total_count = conversations.len(); - - for conversation in conversations { - match self.import_single_conversation(conversation.clone()).await { - Ok(response) => { - if response.success { - imported_count += 1; - } - } - Err(e) => { - eprintln!("❌ インポートエラー: {}", e); - } - } - } - - Ok(ApiResponse { - success: true, - imported_count: Some(imported_count), - total_count: Some(total_count), - error: None, - message: Some(format!("{}個中{}個の会話をインポートしました", total_count, imported_count)), - filepath: None, - results: None, - memories: None, - count: None, - memory: None, - response: None, - memories_used: None, - }) - } - None => { - // 単一の会話をインポート - self.import_single_conversation(json_data).await - } - } - } - - async fn import_single_conversation(&self, conversation_data: Value) -> Result<ApiResponse, Box<dyn std::error::Error>> { - let request = ConversationImportRequest { conversation_data }; - - let response = self.client - .post(&format!("{}/memory/import/chatgpt", self.base_url)) - .json(&request) - .send() - .await?; - - let result: ApiResponse = response.json().await?; - Ok(result) - } - - pub async fn search_memories(&self, query: &str, limit: usize) -> Result<ApiResponse, Box<dyn std::error::Error>> { - let request = MemorySearchRequest { - query: query.to_string(), - limit, - }; - - let response = self.client - .post(&format!("{}/memory/search", self.base_url)) - .json(&request) - .send() - .await?; - - let result: ApiResponse = response.json().await?; - Ok(result) - } - - pub async fn list_memories(&self) -> Result<ApiResponse, Box<dyn std::error::Error>> { - let response = self.client - .get(&format!("{}/memory/list", self.base_url)) - .send() - .await?; - - let result: ApiResponse = response.json().await?; - Ok(result) - } - - pub async fn get_memory_detail(&self, filepath: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> { - let response = self.client - .get(&format!("{}/memory/detail", self.base_url)) - .query(&[("filepath", filepath)]) - .send() - .await?; - - let result: ApiResponse = response.json().await?; - Ok(result) - } - - pub async fn chat_with_memory(&self, message: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> { - let request = ChatRequest { - message: message.to_string(), - model: None, - }; - - let response = self.client - .post(&format!("{}/chat", self.base_url)) - .json(&request) - .send() - .await?; - - let result: ApiResponse = response.json().await?; - Ok(result) - } - - pub async fn is_server_running(&self) -> bool { - match self.client.get(&self.base_url).send().await { - Ok(response) => response.status().is_success(), - Err(_) => false, - } - } -} - -pub async fn handle_import(filepath: &str) -> Result<(), Box<dyn std::error::Error>> { - if !Path::new(filepath).exists() { - eprintln!("❌ ファイルが見つかりません: {}", filepath); - return Ok(()); - } - - let client = MemoryClient::new(None); - - // サーバーが起動しているかチェック - if !client.is_server_running().await { - eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); - return Ok(()); - } - - println!("🔄 ChatGPT会話をインポートしています: {}", filepath); - - match client.import_chatgpt_file(filepath).await { - Ok(response) => { - if response.success { - if let (Some(imported), Some(total)) = (response.imported_count, response.total_count) { - println!("✅ {}個中{}個の会話をインポートしました", total, imported); - } else { - println!("✅ 会話をインポートしました"); - if let Some(path) = response.filepath { - println!("📁 保存先: {}", path); - } - } - } else { - eprintln!("❌ インポートに失敗: {:?}", response.error); - } - } - Err(e) => { - eprintln!("❌ インポートエラー: {}", e); - } - } - - Ok(()) -} - -pub async fn handle_search(query: &str, limit: usize) -> Result<(), Box<dyn std::error::Error>> { - let client = MemoryClient::new(None); - - if !client.is_server_running().await { - eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); - return Ok(()); - } - - println!("🔍 記憶を検索しています: {}", query); - - match client.search_memories(query, limit).await { - Ok(response) => { - if response.success { - if let Some(results) = response.results { - println!("📚 {}個の記憶が見つかりました:", results.len()); - for memory in results { - println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string())); - if let Some(summary) = memory.summary { - println!(" 概要: {}", summary); - } - if let Some(count) = memory.message_count { - println!(" メッセージ数: {}", count); - } - println!(); - } - } else { - println!("📚 記憶が見つかりませんでした"); - } - } else { - eprintln!("❌ 検索に失敗: {:?}", response.error); - } - } - Err(e) => { - eprintln!("❌ 検索エラー: {}", e); - } - } - - Ok(()) -} - -pub async fn handle_list() -> Result<(), Box<dyn std::error::Error>> { - let client = MemoryClient::new(None); - - if !client.is_server_running().await { - eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); - return Ok(()); - } - - println!("📋 記憶一覧を取得しています..."); - - match client.list_memories().await { - Ok(response) => { - if response.success { - if let Some(memories) = response.memories { - println!("📚 総記憶数: {}", memories.len()); - for memory in memories { - println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string())); - if let Some(source) = memory.source { - println!(" ソース: {}", source); - } - if let Some(count) = memory.message_count { - println!(" メッセージ数: {}", count); - } - if let Some(import_time) = memory.import_time { - println!(" インポート時刻: {}", import_time); - } - println!(); - } - } else { - println!("📚 記憶がありません"); - } - } else { - eprintln!("❌ 一覧取得に失敗: {:?}", response.error); - } - } - Err(e) => { - eprintln!("❌ 一覧取得エラー: {}", e); - } - } - - Ok(()) -} - -pub async fn handle_detail(filepath: &str) -> Result<(), Box<dyn std::error::Error>> { - let client = MemoryClient::new(None); - - if !client.is_server_running().await { - eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); - return Ok(()); - } - - println!("📄 記憶の詳細を取得しています: {}", filepath); - - match client.get_memory_detail(filepath).await { - Ok(response) => { - if response.success { - if let Some(memory) = response.memory { - if let Some(title) = memory.get("title").and_then(|v| v.as_str()) { - println!("タイトル: {}", title); - } - if let Some(source) = memory.get("source").and_then(|v| v.as_str()) { - println!("ソース: {}", source); - } - if let Some(summary) = memory.get("summary").and_then(|v| v.as_str()) { - println!("概要: {}", summary); - } - if let Some(messages) = memory.get("messages").and_then(|v| v.as_array()) { - println!("メッセージ数: {}", messages.len()); - println!("\n最近のメッセージ:"); - for msg in messages.iter().take(5) { - if let (Some(role), Some(content)) = ( - msg.get("role").and_then(|v| v.as_str()), - msg.get("content").and_then(|v| v.as_str()) - ) { - let content_preview = if content.len() > 100 { - format!("{}...", &content[..100]) - } else { - content.to_string() - }; - println!(" {}: {}", role, content_preview); - } - } - } - } - } else { - eprintln!("❌ 詳細取得に失敗: {:?}", response.error); - } - } - Err(e) => { - eprintln!("❌ 詳細取得エラー: {}", e); - } - } - - Ok(()) -} - -pub async fn handle_chat_with_memory(message: &str) -> Result<(), Box<dyn std::error::Error>> { - let client = MemoryClient::new(None); - - if !client.is_server_running().await { - eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。"); - return Ok(()); - } - - println!("💬 記憶を活用してチャットしています..."); - - match client.chat_with_memory(message).await { - Ok(response) => { - if response.success { - if let Some(reply) = response.response { - println!("🤖 {}", reply); - } - if let Some(memories_used) = response.memories_used { - println!("📚 使用した記憶数: {}", memories_used); - } - } else { - eprintln!("❌ チャットに失敗: {:?}", response.error); - } - } - Err(e) => { - eprintln!("❌ チャットエラー: {}", e); - } - } - - Ok(()) -} diff --git a/rust/src/mcp/mod.rs b/rust/src/mcp/mod.rs deleted file mode 100644 index e023caf..0000000 --- a/rust/src/mcp/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// src/mcp/mod.rs -pub mod server; -pub mod memory; diff --git a/rust/src/mcp/server.rs b/rust/src/mcp/server.rs deleted file mode 100644 index 63f041a..0000000 --- a/rust/src/mcp/server.rs +++ /dev/null @@ -1,147 +0,0 @@ -// src/mcp/server.rs -use crate::config::ConfigPaths; -//use std::fs; -use std::process::Command as OtherCommand; -use std::env; -use fs_extra::dir::{copy, CopyOptions}; - -pub fn setup() { - println!("🔧 MCP Server環境をセットアップしています..."); - let config = ConfigPaths::new(); - let mcp_dir = config.mcp_dir(); - - // プロジェクトのmcp/ディレクトリからファイルをコピー - let current_dir = env::current_dir().expect("現在のディレクトリを取得できません"); - let project_mcp_dir = current_dir.join("mcp"); - if !project_mcp_dir.exists() { - eprintln!("❌ プロジェクトのmcp/ディレクトリが見つかりません: {}", project_mcp_dir.display()); - return; - } - - if mcp_dir.exists() { - fs_extra::dir::remove(&mcp_dir).expect("既存のmcp_dirの削除に失敗しました"); - } - - let mut options = CopyOptions::new(); - options.overwrite = true; // 上書き - options.copy_inside = true; // 中身だけコピー - - copy(&project_mcp_dir, &mcp_dir, &options).expect("コピーに失敗しました"); - - // 仮想環境の作成 - let venv_path = config.venv_path(); - if !venv_path.exists() { - println!("🐍 仮想環境を作成しています..."); - let output = OtherCommand::new("python3") - .args(&["-m", "venv", ".venv"]) - .current_dir(&mcp_dir) - .output() - .expect("venvの作成に失敗しました"); - - if !output.status.success() { - eprintln!("❌ venv作成エラー: {}", String::from_utf8_lossy(&output.stderr)); - return; - } - println!("✅ 仮想環境を作成しました"); - } else { - println!("✅ 仮想環境は既に存在します"); - } - - // 依存関係のインストール - println!("📦 依存関係をインストールしています..."); - let pip_path = config.pip_executable(); - let output = OtherCommand::new(&pip_path) - .args(&["install", "-r", "requirements.txt"]) - .current_dir(&mcp_dir) - .output() - .expect("pipコマンドの実行に失敗しました"); - - if !output.status.success() { - eprintln!("❌ pip installエラー: {}", String::from_utf8_lossy(&output.stderr)); - return; - } - - println!("✅ MCP Server環境のセットアップが完了しました!"); - println!("📍 セットアップ場所: {}", mcp_dir.display()); -} - -pub async fn run() { - println!("🚀 MCP Serverを起動しています..."); - - let config = ConfigPaths::new(); - let mcp_dir = config.mcp_dir(); - let python_path = config.python_executable(); - let server_py_path = mcp_dir.join("server.py"); - - // セットアップの確認 - if !server_py_path.exists() { - eprintln!("❌ server.pyが見つかりません。先に 'aigpt server setup' を実行してください。"); - return; - } - - if !python_path.exists() { - eprintln!("❌ Python実行ファイルが見つかりません。先に 'aigpt server setup' を実行してください。"); - return; - } - - // サーバーの起動 - println!("🔗 サーバーを起動中... (Ctrl+Cで停止)"); - let mut child = OtherCommand::new(&python_path) - .arg("server.py") - .current_dir(&mcp_dir) - .spawn() - .expect("MCP Serverの起動に失敗しました"); - - // サーバーの終了を待機 - match child.wait() { - Ok(status) => { - if status.success() { - println!("✅ MCP Serverが正常に終了しました"); - } else { - println!("❌ MCP Serverが異常終了しました: {}", status); - } - } - Err(e) => { - eprintln!("❌ MCP Serverの実行中にエラーが発生しました: {}", e); - } - } -} - -pub async fn chat(message: &str) { - println!("💬 チャットを開始しています..."); - - let config = ConfigPaths::new(); - let mcp_dir = config.mcp_dir(); - let python_path = config.python_executable(); - let chat_py_path = mcp_dir.join("chat.py"); - - // セットアップの確認 - if !chat_py_path.exists() { - eprintln!("❌ chat.pyが見つかりません。先に 'aigpt server setup' を実行してください。"); - return; - } - - if !python_path.exists() { - eprintln!("❌ Python実行ファイルが見つかりません。先に 'aigpt server setup' を実行してください。"); - return; - } - - // チャットの実行 - let output = OtherCommand::new(&python_path) - .args(&["chat.py", message]) - .current_dir(&mcp_dir) - .output() - .expect("chat.pyの実行に失敗しました"); - - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - if !stderr.is_empty() { - print!("{}", stderr); - } - print!("{}", stdout); - } else { - eprintln!("❌ チャット実行エラー: {}", String::from_utf8_lossy(&output.stderr)); - } -} diff --git a/shell b/shell new file mode 160000 index 0000000..81ae003 --- /dev/null +++ b/shell @@ -0,0 +1 @@ +Subproject commit 81ae0037d9d58669dc6bc202881fca5254ba5bf4 diff --git a/src/aigpt.egg-info/PKG-INFO b/src/aigpt.egg-info/PKG-INFO new file mode 100644 index 0000000..90141ef --- /dev/null +++ b/src/aigpt.egg-info/PKG-INFO @@ -0,0 +1,18 @@ +Metadata-Version: 2.4 +Name: aigpt +Version: 0.1.0 +Summary: Autonomous transmission AI with unique personality based on relationship parameters +Requires-Python: >=3.10 +Requires-Dist: click>=8.0.0 +Requires-Dist: typer>=0.9.0 +Requires-Dist: fastapi-mcp>=0.1.0 +Requires-Dist: pydantic>=2.0.0 +Requires-Dist: httpx>=0.24.0 +Requires-Dist: rich>=13.0.0 +Requires-Dist: python-dotenv>=1.0.0 +Requires-Dist: ollama>=0.1.0 +Requires-Dist: openai>=1.0.0 +Requires-Dist: uvicorn>=0.23.0 +Requires-Dist: apscheduler>=3.10.0 +Requires-Dist: croniter>=1.3.0 +Requires-Dist: prompt-toolkit>=3.0.0 diff --git a/src/aigpt.egg-info/SOURCES.txt b/src/aigpt.egg-info/SOURCES.txt new file mode 100644 index 0000000..dbf5c14 --- /dev/null +++ b/src/aigpt.egg-info/SOURCES.txt @@ -0,0 +1,22 @@ +README.md +pyproject.toml +src/aigpt/__init__.py +src/aigpt/ai_provider.py +src/aigpt/chatgpt_importer.py +src/aigpt/cli.py +src/aigpt/config.py +src/aigpt/fortune.py +src/aigpt/mcp_server.py +src/aigpt/mcp_server_simple.py +src/aigpt/memory.py +src/aigpt/models.py +src/aigpt/persona.py +src/aigpt/relationship.py +src/aigpt/scheduler.py +src/aigpt/transmission.py +src/aigpt.egg-info/PKG-INFO +src/aigpt.egg-info/SOURCES.txt +src/aigpt.egg-info/dependency_links.txt +src/aigpt.egg-info/entry_points.txt +src/aigpt.egg-info/requires.txt +src/aigpt.egg-info/top_level.txt \ No newline at end of file diff --git a/src/aigpt.egg-info/dependency_links.txt b/src/aigpt.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/aigpt.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/aigpt.egg-info/entry_points.txt b/src/aigpt.egg-info/entry_points.txt new file mode 100644 index 0000000..65200d4 --- /dev/null +++ b/src/aigpt.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +aigpt = aigpt.cli:app diff --git a/src/aigpt.egg-info/requires.txt b/src/aigpt.egg-info/requires.txt new file mode 100644 index 0000000..c9ab0c4 --- /dev/null +++ b/src/aigpt.egg-info/requires.txt @@ -0,0 +1,13 @@ +click>=8.0.0 +typer>=0.9.0 +fastapi-mcp>=0.1.0 +pydantic>=2.0.0 +httpx>=0.24.0 +rich>=13.0.0 +python-dotenv>=1.0.0 +ollama>=0.1.0 +openai>=1.0.0 +uvicorn>=0.23.0 +apscheduler>=3.10.0 +croniter>=1.3.0 +prompt-toolkit>=3.0.0 diff --git a/src/aigpt.egg-info/top_level.txt b/src/aigpt.egg-info/top_level.txt new file mode 100644 index 0000000..f7d9c68 --- /dev/null +++ b/src/aigpt.egg-info/top_level.txt @@ -0,0 +1 @@ +aigpt diff --git a/src/aigpt/ai_provider.py b/src/aigpt/ai_provider.py index 59575cd..fa9dde5 100644 --- a/src/aigpt/ai_provider.py +++ b/src/aigpt/ai_provider.py @@ -30,11 +30,16 @@ class AIProvider(Protocol): class OllamaProvider: """Ollama AI provider""" - def __init__(self, model: str = "qwen2.5", host: str = "http://localhost:11434"): + def __init__(self, model: str = "qwen2.5", host: Optional[str] = None): self.model = model - self.host = host - self.client = ollama.Client(host=host) + # Use environment variable OLLAMA_HOST if available, otherwise use config or default + self.host = host or os.getenv('OLLAMA_HOST', 'http://127.0.0.1:11434') + # Ensure proper URL format + if not self.host.startswith('http'): + self.host = f'http://{self.host}' + self.client = ollama.Client(host=self.host, timeout=60.0) # 60秒タイムアウト self.logger = logging.getLogger(__name__) + self.logger.info(f"OllamaProvider initialized with host: {self.host}, model: {self.model}") async def generate_response( self, @@ -81,6 +86,26 @@ Recent memories: self.logger.error(f"Ollama generation failed: {e}") return self._fallback_response(persona_state) + def chat(self, prompt: str, max_tokens: int = 200) -> str: + """Simple chat interface""" + try: + response = self.client.chat( + model=self.model, + messages=[ + {"role": "user", "content": prompt} + ], + options={ + "num_predict": max_tokens, + "temperature": 0.7, + "top_p": 0.9, + }, + stream=False # ストリーミング無効化で安定性向上 + ) + return response['message']['content'] + except Exception as e: + self.logger.error(f"Ollama chat failed (host: {self.host}): {e}") + return "I'm having trouble connecting to the AI model." + def _fallback_response(self, persona_state: PersonaState) -> str: """Fallback response based on mood""" mood_responses = { @@ -102,7 +127,7 @@ class OpenAIProvider: config = Config() self.api_key = api_key or config.get_api_key("openai") or os.getenv("OPENAI_API_KEY") if not self.api_key: - raise ValueError("OpenAI API key not provided. Set it with: ai-gpt config set providers.openai.api_key YOUR_KEY") + raise ValueError("OpenAI API key not provided. Set it with: aigpt config set providers.openai.api_key YOUR_KEY") self.client = OpenAI(api_key=self.api_key) self.logger = logging.getLogger(__name__) @@ -162,11 +187,21 @@ Recent memories: return mood_responses.get(persona_state.current_mood, "I see.") -def create_ai_provider(provider: str, model: str, **kwargs) -> AIProvider: +def create_ai_provider(provider: str = "ollama", model: str = "qwen2.5", **kwargs) -> AIProvider: """Factory function to create AI providers""" if provider == "ollama": + # Try to get host from config if not provided in kwargs + if 'host' not in kwargs: + try: + from .config import Config + config = Config() + config_host = config.get('providers.ollama.host') + if config_host: + kwargs['host'] = config_host + except: + pass # Use environment variable or default return OllamaProvider(model=model, **kwargs) elif provider == "openai": return OpenAIProvider(model=model, **kwargs) else: - raise ValueError(f"Unknown provider: {provider}") \ No newline at end of file + raise ValueError(f"Unknown provider: {provider}") diff --git a/src/aigpt/chatgpt_importer.py b/src/aigpt/chatgpt_importer.py new file mode 100644 index 0000000..08d3840 --- /dev/null +++ b/src/aigpt/chatgpt_importer.py @@ -0,0 +1,192 @@ +"""ChatGPT conversation data importer for ai.gpt""" + +import json +import uuid +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Any, Optional +import logging + +from .models import Memory, MemoryLevel, Conversation +from .memory import MemoryManager +from .relationship import RelationshipTracker + +logger = logging.getLogger(__name__) + + +class ChatGPTImporter: + """Import ChatGPT conversation data into ai.gpt memory system""" + + def __init__(self, data_dir: Path): + self.data_dir = data_dir + self.memory_manager = MemoryManager(data_dir) + self.relationship_tracker = RelationshipTracker(data_dir) + + def import_from_file(self, file_path: Path, user_id: str = "chatgpt_user") -> Dict[str, Any]: + """Import ChatGPT conversations from JSON file + + Args: + file_path: Path to ChatGPT export JSON file + user_id: User ID to associate with imported conversations + + Returns: + Dict with import statistics + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + chatgpt_data = json.load(f) + + return self._import_conversations(chatgpt_data, user_id) + + except Exception as e: + logger.error(f"Failed to import ChatGPT data: {e}") + raise + + def _import_conversations(self, chatgpt_data: List[Dict], user_id: str) -> Dict[str, Any]: + """Import multiple conversations from ChatGPT data""" + stats = { + "conversations_imported": 0, + "messages_imported": 0, + "user_messages": 0, + "assistant_messages": 0, + "skipped_messages": 0 + } + + for conversation_data in chatgpt_data: + try: + conv_stats = self._import_single_conversation(conversation_data, user_id) + + # Update overall stats + stats["conversations_imported"] += 1 + stats["messages_imported"] += conv_stats["messages"] + stats["user_messages"] += conv_stats["user_messages"] + stats["assistant_messages"] += conv_stats["assistant_messages"] + stats["skipped_messages"] += conv_stats["skipped"] + + except Exception as e: + logger.warning(f"Failed to import conversation '{conversation_data.get('title', 'Unknown')}': {e}") + continue + + logger.info(f"Import completed: {stats}") + return stats + + def _import_single_conversation(self, conversation_data: Dict, user_id: str) -> Dict[str, int]: + """Import a single conversation from ChatGPT""" + title = conversation_data.get("title", "Untitled") + create_time = conversation_data.get("create_time") + mapping = conversation_data.get("mapping", {}) + + stats = {"messages": 0, "user_messages": 0, "assistant_messages": 0, "skipped": 0} + + # Extract messages in chronological order + messages = self._extract_messages_from_mapping(mapping) + + for msg in messages: + try: + role = msg["author"]["role"] + content = self._extract_content(msg["content"]) + create_time_msg = msg.get("create_time") + + if not content or role not in ["user", "assistant"]: + stats["skipped"] += 1 + continue + + # Convert to ai.gpt format + if role == "user": + # User message - create memory entry + self._add_user_message(user_id, content, create_time_msg, title) + stats["user_messages"] += 1 + + elif role == "assistant": + # Assistant message - create AI response memory + self._add_assistant_message(user_id, content, create_time_msg, title) + stats["assistant_messages"] += 1 + + stats["messages"] += 1 + + except Exception as e: + logger.warning(f"Failed to process message in '{title}': {e}") + stats["skipped"] += 1 + continue + + logger.info(f"Imported conversation '{title}': {stats}") + return stats + + def _extract_messages_from_mapping(self, mapping: Dict) -> List[Dict]: + """Extract messages from ChatGPT mapping structure in chronological order""" + messages = [] + + for node_id, node_data in mapping.items(): + message = node_data.get("message") + if message and message.get("author", {}).get("role") in ["user", "assistant"]: + # Skip system messages and hidden messages + metadata = message.get("metadata", {}) + if not metadata.get("is_visually_hidden_from_conversation", False): + messages.append(message) + + # Sort by create_time if available + messages.sort(key=lambda x: x.get("create_time") or 0) + return messages + + def _extract_content(self, content_data: Dict) -> Optional[str]: + """Extract text content from ChatGPT content structure""" + if not content_data: + return None + + content_type = content_data.get("content_type") + + if content_type == "text": + parts = content_data.get("parts", []) + if parts and parts[0]: + return parts[0].strip() + + elif content_type == "user_editable_context": + # User context/instructions + user_instructions = content_data.get("user_instructions", "") + if user_instructions: + return f"[User Context] {user_instructions}" + + return None + + def _add_user_message(self, user_id: str, content: str, create_time: Optional[float], conversation_title: str): + """Add user message to ai.gpt memory system""" + timestamp = datetime.fromtimestamp(create_time) if create_time else datetime.now() + + # Create conversation record + conversation = Conversation( + id=str(uuid.uuid4()), + user_id=user_id, + user_message=content, + ai_response="", # Will be filled by next assistant message + timestamp=timestamp, + context={"source": "chatgpt_import", "conversation_title": conversation_title} + ) + + # Add to memory with CORE level (imported data is important) + memory = Memory( + id=str(uuid.uuid4()), + timestamp=timestamp, + content=content, + level=MemoryLevel.CORE, + importance_score=0.8 # High importance for imported data + ) + + self.memory_manager.add_memory(memory) + + # Update relationship (positive interaction) + self.relationship_tracker.update_interaction(user_id, 1.0) + + def _add_assistant_message(self, user_id: str, content: str, create_time: Optional[float], conversation_title: str): + """Add assistant message to ai.gpt memory system""" + timestamp = datetime.fromtimestamp(create_time) if create_time else datetime.now() + + # Add assistant response as memory (AI's own responses can inform future behavior) + memory = Memory( + id=str(uuid.uuid4()), + timestamp=timestamp, + content=f"[AI Response] {content}", + level=MemoryLevel.SUMMARY, + importance_score=0.6 # Medium importance for AI responses + ) + + self.memory_manager.add_memory(memory) \ No newline at end of file diff --git a/src/aigpt/cli.py b/src/aigpt/cli.py index a0d8570..cc1723b 100644 --- a/src/aigpt/cli.py +++ b/src/aigpt/cli.py @@ -7,6 +7,12 @@ from rich.console import Console from rich.table import Table from rich.panel import Panel from datetime import datetime, timedelta +import subprocess +import shlex +from prompt_toolkit import prompt as ptk_prompt +from prompt_toolkit.completion import WordCompleter +from prompt_toolkit.history import FileHistory +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from .persona import Persona from .transmission import TransmissionController @@ -14,6 +20,7 @@ from .mcp_server import AIGptMcpServer from .ai_provider import create_ai_provider from .scheduler import AIScheduler, TaskType from .config import Config +from .project_manager import ContinuousDeveloper app = typer.Typer(help="ai.gpt - Autonomous transmission AI with unique personality") console = Console() @@ -47,7 +54,7 @@ def chat( ai_provider = None if provider and model: try: - ai_provider = create_ai_provider(provider, model) + ai_provider = create_ai_provider(provider=provider, model=model) console.print(f"[dim]Using {provider} with model {model}[/dim]\n") except Exception as e: console.print(f"[yellow]Warning: Could not create AI provider: {e}[/yellow]") @@ -234,7 +241,7 @@ def server( # Create MCP server mcp_server = AIGptMcpServer(data_dir) - app_instance = mcp_server.get_server().get_app() + app_instance = mcp_server.app console.print(Panel( f"[cyan]Starting ai.gpt MCP Server[/cyan]\n\n" @@ -369,6 +376,424 @@ def schedule( console.print("Valid actions: add, list, enable, disable, remove, run") +@app.command() +def shell( + data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory"), + model: Optional[str] = typer.Option("qwen2.5", "--model", "-m", help="AI model to use"), + provider: Optional[str] = typer.Option("ollama", "--provider", help="AI provider (ollama/openai)") +): + """Interactive shell mode (ai.shell)""" + persona = get_persona(data_dir) + + # Create AI provider + ai_provider = None + if provider and model: + try: + ai_provider = create_ai_provider(provider=provider, model=model) + console.print(f"[dim]Using {provider} with model {model}[/dim]\n") + except Exception as e: + console.print(f"[yellow]Warning: Could not create AI provider: {e}[/yellow]") + console.print("[yellow]Falling back to simple responses[/yellow]\n") + + # Welcome message + console.print(Panel( + "[cyan]Welcome to ai.shell[/cyan]\n\n" + "Interactive AI-powered shell with command execution\n\n" + "Commands:\n" + " help - Show available commands\n" + " exit/quit - Exit shell\n" + " !<command> - Execute shell command\n" + " chat <message> - Chat with AI\n" + " status - Show AI status\n" + " clear - Clear screen\n\n" + "Type any message to interact with AI", + title="ai.shell", + border_style="green" + )) + + # Command completer with shell commands + builtin_commands = ['help', 'exit', 'quit', 'chat', 'status', 'clear', 'fortune', 'relationships', 'load'] + + # Add common shell commands + shell_commands = ['ls', 'cd', 'pwd', 'cat', 'echo', 'grep', 'find', 'mkdir', 'rm', 'cp', 'mv', + 'git', 'python', 'pip', 'npm', 'node', 'cargo', 'rustc', 'docker', 'kubectl'] + + # AI-specific commands + ai_commands = ['analyze', 'generate', 'explain', 'optimize', 'refactor', 'test', 'document'] + + # Remote execution commands (ai.bot integration) + remote_commands = ['remote', 'isolated', 'aibot-status'] + + # Project management commands (Claude Code-like) + project_commands = ['project-status', 'suggest-next', 'continuous'] + + all_commands = builtin_commands + ['!' + cmd for cmd in shell_commands] + ai_commands + remote_commands + project_commands + completer = WordCompleter(all_commands, ignore_case=True) + + # History file + actual_data_dir = data_dir if data_dir else DEFAULT_DATA_DIR + history_file = actual_data_dir / "shell_history.txt" + history = FileHistory(str(history_file)) + + # Main shell loop + current_user = "shell_user" # Default user for shell sessions + + while True: + try: + # Get input with completion + user_input = ptk_prompt( + "ai.shell> ", + completer=completer, + history=history, + auto_suggest=AutoSuggestFromHistory() + ).strip() + + if not user_input: + continue + + # Exit commands + if user_input.lower() in ['exit', 'quit']: + console.print("[cyan]Goodbye![/cyan]") + break + + # Help command + elif user_input.lower() == 'help': + console.print(Panel( + "[cyan]ai.shell Commands:[/cyan]\n\n" + " help - Show this help message\n" + " exit/quit - Exit the shell\n" + " !<command> - Execute a shell command\n" + " chat <message> - Explicitly chat with AI\n" + " status - Show AI status\n" + " fortune - Check AI fortune\n" + " relationships - List all relationships\n" + " clear - Clear the screen\n" + " load - Load aishell.md project file\n\n" + "[cyan]AI Commands:[/cyan]\n" + " analyze <file> - Analyze a file with AI\n" + " generate <desc> - Generate code from description\n" + " explain <topic> - Get AI explanation\n\n" + "[cyan]Remote Commands (ai.bot):[/cyan]\n" + " remote <command> - Execute command in isolated container\n" + " isolated <code> - Run Python code in isolated environment\n" + " aibot-status - Check ai.bot server status\n\n" + "[cyan]Project Commands (Claude Code-like):[/cyan]\n" + " project-status - Analyze current project structure\n" + " suggest-next - AI suggests next development steps\n" + " continuous - Enable continuous development mode\n\n" + "You can also type any message to chat with AI\n" + "Use Tab for command completion", + title="Help", + border_style="yellow" + )) + + # Clear command + elif user_input.lower() == 'clear': + console.clear() + + # Shell command execution + elif user_input.startswith('!'): + cmd = user_input[1:].strip() + if cmd: + try: + # Execute command + result = subprocess.run( + shlex.split(cmd), + capture_output=True, + text=True, + shell=False + ) + + if result.stdout: + console.print(result.stdout.rstrip()) + if result.stderr: + console.print(f"[red]{result.stderr.rstrip()}[/red]") + + if result.returncode != 0: + console.print(f"[red]Command exited with code {result.returncode}[/red]") + except FileNotFoundError: + console.print(f"[red]Command not found: {cmd.split()[0]}[/red]") + except Exception as e: + console.print(f"[red]Error executing command: {e}[/red]") + + # Status command + elif user_input.lower() == 'status': + state = persona.get_current_state() + console.print(f"\nMood: {state.current_mood}") + console.print(f"Fortune: {state.fortune.fortune_value}/10") + + rel = persona.relationships.get_or_create_relationship(current_user) + console.print(f"\nRelationship Status: {rel.status.value}") + console.print(f"Score: {rel.score:.2f} / {rel.threshold}") + + # Fortune command + elif user_input.lower() == 'fortune': + fortune = persona.fortune_system.get_today_fortune() + fortune_bar = "🌟" * fortune.fortune_value + "☆" * (10 - fortune.fortune_value) + console.print(f"\n{fortune_bar}") + console.print(f"Today's Fortune: {fortune.fortune_value}/10") + + # Relationships command + elif user_input.lower() == 'relationships': + if persona.relationships.relationships: + console.print("\n[cyan]Relationships:[/cyan]") + for user_id, rel in persona.relationships.relationships.items(): + console.print(f" {user_id[:16]}... - {rel.status.value} ({rel.score:.2f})") + else: + console.print("[yellow]No relationships yet[/yellow]") + + # Load aishell.md command + elif user_input.lower() in ['load', 'load aishell.md', 'project']: + # Try to find and load aishell.md + search_paths = [ + Path.cwd() / "aishell.md", + Path.cwd() / "docs" / "aishell.md", + actual_data_dir.parent / "aishell.md", + Path.cwd() / "claude.md", # Also check for claude.md + ] + + loaded = False + for path in search_paths: + if path.exists(): + console.print(f"[cyan]Loading project file: {path}[/cyan]") + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + # Process with AI to understand project + load_prompt = f"I've loaded the project specification. Please analyze it and understand the project goals:\n\n{content[:3000]}" + response, _ = persona.process_interaction(current_user, load_prompt, ai_provider) + console.print(f"\n[green]Project loaded successfully![/green]") + console.print(f"[cyan]AI Understanding:[/cyan]\n{response}") + loaded = True + break + + if not loaded: + console.print("[yellow]No aishell.md or claude.md found in project.[/yellow]") + console.print("Create aishell.md to define project goals and AI instructions.") + + # AI-powered commands + elif user_input.lower().startswith('analyze '): + # Analyze file or code with project context + target = user_input[8:].strip() + if os.path.exists(target): + console.print(f"[cyan]Analyzing {target} with project context...[/cyan]") + try: + developer = ContinuousDeveloper(Path.cwd(), ai_provider) + analysis = developer.analyze_file(target) + console.print(f"\n[cyan]Analysis:[/cyan]\n{analysis}") + except Exception as e: + # Fallback to simple analysis + with open(target, 'r') as f: + content = f.read() + analysis_prompt = f"Analyze this file and provide insights:\n\n{content[:2000]}" + response, _ = persona.process_interaction(current_user, analysis_prompt, ai_provider) + console.print(f"\n[cyan]Analysis:[/cyan]\n{response}") + else: + console.print(f"[red]File not found: {target}[/red]") + + elif user_input.lower().startswith('generate '): + # Generate code with project context + gen_prompt = user_input[9:].strip() + if gen_prompt: + console.print("[cyan]Generating code with project context...[/cyan]") + try: + developer = ContinuousDeveloper(Path.cwd(), ai_provider) + generated_code = developer.generate_code(gen_prompt) + console.print(f"\n[cyan]Generated Code:[/cyan]\n{generated_code}") + except Exception as e: + # Fallback to simple generation + full_prompt = f"Generate code for: {gen_prompt}. Provide clean, well-commented code." + response, _ = persona.process_interaction(current_user, full_prompt, ai_provider) + console.print(f"\n[cyan]Generated Code:[/cyan]\n{response}") + + elif user_input.lower().startswith('explain '): + # Explain code or concept + topic = user_input[8:].strip() + if topic: + console.print(f"[cyan]Explaining {topic}...[/cyan]") + full_prompt = f"Explain this in detail: {topic}" + response, _ = persona.process_interaction(current_user, full_prompt, ai_provider) + console.print(f"\n[cyan]Explanation:[/cyan]\n{response}") + + # Remote execution commands (ai.bot integration) + elif user_input.lower().startswith('remote '): + # Execute command in ai.bot isolated container + command = user_input[7:].strip() + if command: + console.print(f"[cyan]Executing remotely:[/cyan] {command}") + try: + import httpx + import asyncio + + async def execute_remote(): + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + "http://localhost:8080/sh", + json={"command": command}, + headers={"Content-Type": "application/json"} + ) + return response + + response = asyncio.run(execute_remote()) + + if response.status_code == 200: + result = response.json() + console.print(f"[green]Output:[/green]\n{result.get('output', '')}") + if result.get('error'): + console.print(f"[red]Error:[/red] {result.get('error')}") + console.print(f"[dim]Exit code: {result.get('exit_code', 0)} | Execution time: {result.get('execution_time', 'N/A')}[/dim]") + else: + console.print(f"[red]ai.bot error: HTTP {response.status_code}[/red]") + except Exception as e: + console.print(f"[red]Failed to connect to ai.bot: {e}[/red]") + + elif user_input.lower().startswith('isolated '): + # Execute Python code in isolated environment + code = user_input[9:].strip() + if code: + console.print(f"[cyan]Running Python code in isolated container...[/cyan]") + try: + import httpx + import asyncio + + async def execute_python(): + python_command = f'python3 -c "{code.replace('"', '\\"')}"' + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + "http://localhost:8080/sh", + json={"command": python_command}, + headers={"Content-Type": "application/json"} + ) + return response + + response = asyncio.run(execute_python()) + + if response.status_code == 200: + result = response.json() + console.print(f"[green]Python Output:[/green]\n{result.get('output', '')}") + if result.get('error'): + console.print(f"[red]Error:[/red] {result.get('error')}") + else: + console.print(f"[red]ai.bot error: HTTP {response.status_code}[/red]") + except Exception as e: + console.print(f"[red]Failed to execute Python code: {e}[/red]") + + elif user_input.lower() == 'aibot-status': + # Check ai.bot server status + console.print("[cyan]Checking ai.bot server status...[/cyan]") + try: + import httpx + import asyncio + + async def check_status(): + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get("http://localhost:8080/status") + return response + + response = asyncio.run(check_status()) + + if response.status_code == 200: + result = response.json() + console.print(f"[green]ai.bot is online![/green]") + console.print(f"Server info: {result}") + else: + console.print(f"[yellow]ai.bot responded with status {response.status_code}[/yellow]") + except Exception as e: + console.print(f"[red]ai.bot is offline: {e}[/red]") + console.print("[dim]Make sure ai.bot is running on localhost:8080[/dim]") + + # Project management commands (Claude Code-like) + elif user_input.lower() == 'project-status': + # プロジェクト構造分析 + console.print("[cyan]Analyzing project structure...[/cyan]") + try: + developer = ContinuousDeveloper(Path.cwd(), ai_provider) + analysis = developer.analyze_project_structure() + changes = developer.project_state.detect_changes() + + console.print(f"[green]Project Analysis:[/green]") + console.print(f"Language: {analysis['language']}") + console.print(f"Framework: {analysis['framework']}") + console.print(f"Structure: {analysis['structure']}") + console.print(f"Dependencies: {analysis['dependencies']}") + console.print(f"Code Patterns: {analysis['patterns']}") + + if changes: + console.print(f"\n[yellow]Recent Changes:[/yellow]") + for file_path, change_type in changes.items(): + console.print(f" {change_type}: {file_path}") + else: + console.print(f"\n[dim]No recent changes detected[/dim]") + + except Exception as e: + console.print(f"[red]Error analyzing project: {e}[/red]") + + elif user_input.lower() == 'suggest-next': + # 次のステップを提案 + console.print("[cyan]AI is analyzing project and suggesting next steps...[/cyan]") + try: + developer = ContinuousDeveloper(Path.cwd(), ai_provider) + suggestions = developer.suggest_next_steps() + + console.print(f"[green]Suggested Next Steps:[/green]") + for i, suggestion in enumerate(suggestions, 1): + console.print(f" {i}. {suggestion}") + + except Exception as e: + console.print(f"[red]Error generating suggestions: {e}[/red]") + + elif user_input.lower().startswith('continuous'): + # 継続開発モード + console.print("[cyan]Enabling continuous development mode...[/cyan]") + console.print("[yellow]Continuous mode is experimental. Type 'exit-continuous' to exit.[/yellow]") + + try: + developer = ContinuousDeveloper(Path.cwd(), ai_provider) + context = developer.load_project_context() + + console.print(f"[green]Project context loaded:[/green]") + console.print(f"Context: {len(context)} characters") + + # Add to session memory for continuous context + persona.process_interaction(current_user, f"Continuous development mode started for project: {context[:500]}", ai_provider) + console.print("[dim]Project context added to AI memory for continuous development.[/dim]") + + except Exception as e: + console.print(f"[red]Error starting continuous mode: {e}[/red]") + + # Chat command or direct message + else: + # Remove 'chat' prefix if present + if user_input.lower().startswith('chat '): + message = user_input[5:].strip() + else: + message = user_input + + if message: + # Process interaction with AI + response, relationship_delta = persona.process_interaction( + current_user, message, ai_provider + ) + + # Display response + console.print(f"\n[cyan]AI:[/cyan] {response}") + + # Show relationship change if significant + if abs(relationship_delta) >= 0.1: + if relationship_delta > 0: + console.print(f"[green](+{relationship_delta:.2f} relationship)[/green]") + else: + console.print(f"[red]({relationship_delta:.2f} relationship)[/red]") + + except KeyboardInterrupt: + console.print("\n[yellow]Use 'exit' or 'quit' to leave the shell[/yellow]") + except EOFError: + console.print("\n[cyan]Goodbye![/cyan]") + break + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + + @app.command() def config( action: str = typer.Argument(..., help="Action: get, set, delete, list"), @@ -413,7 +838,8 @@ def config( console.print(f"[yellow]Key '{key}' not found[/yellow]") elif action == "list": - keys = config.list_keys(key or "") + config_instance = Config() + keys = config_instance.list_keys(key or "") if not keys: console.print("[yellow]No configuration keys found[/yellow]") @@ -424,7 +850,7 @@ def config( table.add_column("Value", style="green") for k in sorted(keys): - val = config.get(k) + val = config_instance.get(k) # Hide sensitive values if "password" in k or "api_key" in k: display_val = "***hidden***" if val else "not set" @@ -440,5 +866,56 @@ def config( console.print("Valid actions: get, set, delete, list") +@app.command() +def import_chatgpt( + file_path: Path = typer.Argument(..., help="Path to ChatGPT export JSON file"), + user_id: str = typer.Option("chatgpt_user", "--user-id", "-u", help="User ID for imported conversations"), + data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory") +): + """Import ChatGPT conversation data into ai.gpt memory system""" + from .chatgpt_importer import ChatGPTImporter + + if data_dir is None: + data_dir = DEFAULT_DATA_DIR + + data_dir.mkdir(parents=True, exist_ok=True) + + if not file_path.exists(): + console.print(f"[red]Error: File not found: {file_path}[/red]") + raise typer.Exit(1) + + console.print(f"[cyan]Importing ChatGPT data from {file_path}[/cyan]") + console.print(f"User ID: {user_id}") + console.print(f"Data directory: {data_dir}") + + try: + importer = ChatGPTImporter(data_dir) + stats = importer.import_from_file(file_path, user_id) + + # Display results + table = Table(title="Import Results") + table.add_column("Metric", style="cyan") + table.add_column("Count", style="green") + + table.add_row("Conversations imported", str(stats["conversations_imported"])) + table.add_row("Total messages", str(stats["messages_imported"])) + table.add_row("User messages", str(stats["user_messages"])) + table.add_row("Assistant messages", str(stats["assistant_messages"])) + table.add_row("Skipped messages", str(stats["skipped_messages"])) + + console.print(table) + console.print(f"[green]✓ Import completed successfully![/green]") + + # Show next steps + console.print("\n[cyan]Next steps:[/cyan]") + console.print(f"- Check memories: [yellow]aigpt status[/yellow]") + console.print(f"- Chat with AI: [yellow]aigpt chat {user_id} \"hello\"[/yellow]") + console.print(f"- View relationships: [yellow]aigpt relationships[/yellow]") + + except Exception as e: + console.print(f"[red]Error during import: {e}[/red]") + raise typer.Exit(1) + + if __name__ == "__main__": app() \ No newline at end of file diff --git a/src/aigpt/mcp_server.py b/src/aigpt/mcp_server.py index 7b999fa..1020c65 100644 --- a/src/aigpt/mcp_server.py +++ b/src/aigpt/mcp_server.py @@ -1,9 +1,16 @@ """MCP Server for ai.gpt system""" from typing import Optional, List, Dict, Any -from fastapi_mcp import FastapiMcpServer +from fastapi_mcp import FastApiMCP +from fastapi import FastAPI from pathlib import Path import logging +import subprocess +import os +import shlex +import httpx +import json +from .ai_provider import create_ai_provider from .persona import Persona from .models import Memory, Relationship, PersonaState @@ -17,13 +24,22 @@ class AIGptMcpServer: def __init__(self, data_dir: Path): self.data_dir = data_dir self.persona = Persona(data_dir) - self.server = FastapiMcpServer("ai-gpt", "AI.GPT Memory and Relationship System") + + # Create FastAPI app + self.app = FastAPI( + title="AI.GPT Memory and Relationship System", + description="MCP server for ai.gpt system" + ) + + # Create MCP server with FastAPI app + self.server = FastApiMCP(self.app) + self._register_tools() def _register_tools(self): """Register all MCP tools""" - @self.server.tool("get_memories") + @self.app.get("/get_memories", operation_id="get_memories") async def get_memories(user_id: Optional[str] = None, limit: int = 10) -> List[Dict[str, Any]]: """Get active memories from the AI's memory system""" memories = self.persona.memory.get_active_memories(limit=limit) @@ -39,7 +55,109 @@ class AIGptMcpServer: for mem in memories ] - @self.server.tool("get_relationship") + @self.app.get("/get_contextual_memories", operation_id="get_contextual_memories") + async def get_contextual_memories(query: str = "", limit: int = 10) -> Dict[str, List[Dict[str, Any]]]: + """Get memories organized by priority with contextual relevance""" + memory_groups = self.persona.memory.get_contextual_memories(query=query, limit=limit) + + result = {} + for group_name, memories in memory_groups.items(): + result[group_name] = [ + { + "id": mem.id, + "content": mem.content, + "level": mem.level.value, + "importance": mem.importance_score, + "is_core": mem.is_core, + "timestamp": mem.timestamp.isoformat(), + "summary": mem.summary, + "metadata": mem.metadata + } + for mem in memories + ] + return result + + @self.app.post("/search_memories", operation_id="search_memories") + async def search_memories(keywords: List[str], memory_types: Optional[List[str]] = None) -> List[Dict[str, Any]]: + """Search memories by keywords and optionally filter by memory types""" + from .models import MemoryLevel + + # Convert string memory types to enum if provided + level_filter = None + if memory_types: + level_filter = [] + for mt in memory_types: + try: + level_filter.append(MemoryLevel(mt)) + except ValueError: + pass # Skip invalid memory types + + memories = self.persona.memory.search_memories(keywords, memory_types=level_filter) + return [ + { + "id": mem.id, + "content": mem.content, + "level": mem.level.value, + "importance": mem.importance_score, + "is_core": mem.is_core, + "timestamp": mem.timestamp.isoformat(), + "summary": mem.summary, + "metadata": mem.metadata + } + for mem in memories + ] + + @self.app.post("/create_summary", operation_id="create_summary") + async def create_summary(user_id: str) -> Dict[str, Any]: + """Create an AI-powered summary of recent memories""" + try: + ai_provider = create_ai_provider() + summary = self.persona.memory.create_smart_summary(user_id, ai_provider=ai_provider) + + if summary: + return { + "success": True, + "summary": { + "id": summary.id, + "content": summary.content, + "level": summary.level.value, + "importance": summary.importance_score, + "timestamp": summary.timestamp.isoformat(), + "metadata": summary.metadata + } + } + else: + return {"success": False, "reason": "Not enough memories to summarize"} + except Exception as e: + logger.error(f"Failed to create summary: {e}") + return {"success": False, "reason": str(e)} + + @self.app.post("/create_core_memory", operation_id="create_core_memory") + async def create_core_memory() -> Dict[str, Any]: + """Create a core memory by analyzing all existing memories""" + try: + ai_provider = create_ai_provider() + core_memory = self.persona.memory.create_core_memory(ai_provider=ai_provider) + + if core_memory: + return { + "success": True, + "core_memory": { + "id": core_memory.id, + "content": core_memory.content, + "level": core_memory.level.value, + "importance": core_memory.importance_score, + "timestamp": core_memory.timestamp.isoformat(), + "metadata": core_memory.metadata + } + } + else: + return {"success": False, "reason": "Not enough memories to create core memory"} + except Exception as e: + logger.error(f"Failed to create core memory: {e}") + return {"success": False, "reason": str(e)} + + @self.app.get("/get_relationship", operation_id="get_relationship") async def get_relationship(user_id: str) -> Dict[str, Any]: """Get relationship status with a specific user""" rel = self.persona.relationships.get_or_create_relationship(user_id) @@ -53,7 +171,7 @@ class AIGptMcpServer: "last_interaction": rel.last_interaction.isoformat() if rel.last_interaction else None } - @self.server.tool("get_all_relationships") + @self.app.get("/get_all_relationships", operation_id="get_all_relationships") async def get_all_relationships() -> List[Dict[str, Any]]: """Get all relationships""" relationships = [] @@ -67,7 +185,7 @@ class AIGptMcpServer: }) return relationships - @self.server.tool("get_persona_state") + @self.app.get("/get_persona_state", operation_id="get_persona_state") async def get_persona_state() -> Dict[str, Any]: """Get current persona state including fortune and mood""" state = self.persona.get_current_state() @@ -82,7 +200,22 @@ class AIGptMcpServer: "active_memory_count": len(state.active_memories) } - @self.server.tool("process_interaction") + @self.app.post("/get_context_prompt", operation_id="get_context_prompt") + async def get_context_prompt(user_id: str, message: str) -> Dict[str, Any]: + """Get context-aware prompt for AI response generation""" + try: + context_prompt = self.persona.build_context_prompt(user_id, message) + return { + "success": True, + "context_prompt": context_prompt, + "user_id": user_id, + "message": message + } + except Exception as e: + logger.error(f"Failed to build context prompt: {e}") + return {"success": False, "reason": str(e)} + + @self.app.post("/process_interaction", operation_id="process_interaction") async def process_interaction(user_id: str, message: str) -> Dict[str, Any]: """Process an interaction with a user""" response, relationship_delta = self.persona.process_interaction(user_id, message) @@ -96,7 +229,7 @@ class AIGptMcpServer: "relationship_status": rel.status.value } - @self.server.tool("check_transmission_eligibility") + @self.app.get("/check_transmission_eligibility", operation_id="check_transmission_eligibility") async def check_transmission_eligibility(user_id: str) -> Dict[str, Any]: """Check if AI can transmit to a specific user""" can_transmit = self.persona.can_transmit_to(user_id) @@ -110,7 +243,7 @@ class AIGptMcpServer: "transmission_enabled": rel.transmission_enabled } - @self.server.tool("get_fortune") + @self.app.get("/get_fortune", operation_id="get_fortune") async def get_fortune() -> Dict[str, Any]: """Get today's AI fortune""" fortune = self.persona.fortune_system.get_today_fortune() @@ -125,7 +258,7 @@ class AIGptMcpServer: "personality_modifiers": modifiers } - @self.server.tool("summarize_memories") + @self.app.post("/summarize_memories", operation_id="summarize_memories") async def summarize_memories(user_id: str) -> Optional[Dict[str, Any]]: """Create a summary of recent memories for a user""" summary = self.persona.memory.summarize_memories(user_id) @@ -138,12 +271,241 @@ class AIGptMcpServer: } return None - @self.server.tool("run_maintenance") + @self.app.post("/run_maintenance", operation_id="run_maintenance") async def run_maintenance() -> Dict[str, str]: """Run daily maintenance tasks""" self.persona.daily_maintenance() return {"status": "Maintenance completed successfully"} + + # Shell integration tools (ai.shell) + @self.app.post("/execute_command", operation_id="execute_command") + async def execute_command(command: str, working_dir: str = ".") -> Dict[str, Any]: + """Execute a shell command""" + try: + result = subprocess.run( + shlex.split(command), + cwd=working_dir, + capture_output=True, + text=True, + timeout=60 + ) + + return { + "status": "success" if result.returncode == 0 else "error", + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "command": command + } + except subprocess.TimeoutExpired: + return {"error": "Command timed out"} + except Exception as e: + return {"error": str(e)} + + @self.app.post("/analyze_file", operation_id="analyze_file") + async def analyze_file(file_path: str, analysis_prompt: str = "Analyze this file") -> Dict[str, Any]: + """Analyze a file using AI""" + try: + if not os.path.exists(file_path): + return {"error": f"File not found: {file_path}"} + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Get AI provider from app state + ai_provider = getattr(self.app.state, 'ai_provider', 'ollama') + ai_model = getattr(self.app.state, 'ai_model', 'qwen2.5') + + provider = create_ai_provider(ai_provider, ai_model) + + # Analyze with AI + prompt = f"{analysis_prompt}\n\nFile: {file_path}\n\nContent:\n{content}" + analysis = provider.generate_response(prompt, "You are a code analyst.") + + return { + "analysis": analysis, + "file_path": file_path, + "file_size": len(content), + "line_count": len(content.split('\n')) + } + except Exception as e: + return {"error": str(e)} + + @self.app.post("/write_file", operation_id="write_file") + async def write_file(file_path: str, content: str, backup: bool = True) -> Dict[str, Any]: + """Write content to a file""" + try: + file_path_obj = Path(file_path) + + # Create backup if requested + backup_path = None + if backup and file_path_obj.exists(): + backup_path = f"{file_path}.backup" + with open(file_path, 'r', encoding='utf-8') as src: + with open(backup_path, 'w', encoding='utf-8') as dst: + dst.write(src.read()) + + # Write file + file_path_obj.parent.mkdir(parents=True, exist_ok=True) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + return { + "status": "success", + "file_path": file_path, + "backup_path": backup_path, + "bytes_written": len(content.encode('utf-8')) + } + except Exception as e: + return {"error": str(e)} + + @self.app.get("/read_project_file", operation_id="read_project_file") + async def read_project_file(file_name: str = "aishell.md") -> Dict[str, Any]: + """Read project files like aishell.md (similar to claude.md)""" + try: + # Check common locations + search_paths = [ + Path.cwd() / file_name, + Path.cwd() / "docs" / file_name, + self.data_dir.parent / file_name, + ] + + for path in search_paths: + if path.exists(): + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + return { + "content": content, + "path": str(path), + "exists": True + } + + return { + "exists": False, + "searched_paths": [str(p) for p in search_paths], + "error": f"{file_name} not found" + } + except Exception as e: + return {"error": str(e)} + + @self.app.get("/list_files", operation_id="list_files") + async def list_files(directory: str = ".", pattern: str = "*") -> Dict[str, Any]: + """List files in a directory""" + try: + dir_path = Path(directory) + if not dir_path.exists(): + return {"error": f"Directory not found: {directory}"} + + files = [] + for item in dir_path.glob(pattern): + files.append({ + "name": item.name, + "path": str(item), + "is_file": item.is_file(), + "is_dir": item.is_dir(), + "size": item.stat().st_size if item.is_file() else None + }) + + return { + "directory": directory, + "pattern": pattern, + "files": files, + "count": len(files) + } + except Exception as e: + return {"error": str(e)} + + # ai.bot integration tools + @self.app.post("/remote_shell", operation_id="remote_shell") + async def remote_shell(command: str, ai_bot_url: str = "http://localhost:8080") -> Dict[str, Any]: + """Execute command via ai.bot /sh functionality (systemd-nspawn isolated execution)""" + try: + async with httpx.AsyncClient(timeout=30.0) as client: + # ai.bot の /sh エンドポイントに送信 + response = await client.post( + f"{ai_bot_url}/sh", + json={"command": command}, + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 200: + result = response.json() + return { + "status": "success", + "command": command, + "output": result.get("output", ""), + "error": result.get("error", ""), + "exit_code": result.get("exit_code", 0), + "execution_time": result.get("execution_time", ""), + "container_id": result.get("container_id", ""), + "isolated": True # systemd-nspawn isolation + } + else: + return { + "status": "error", + "error": f"ai.bot responded with status {response.status_code}", + "response_text": response.text + } + except httpx.TimeoutException: + return {"status": "error", "error": "Request to ai.bot timed out"} + except Exception as e: + return {"status": "error", "error": f"Failed to connect to ai.bot: {str(e)}"} + + @self.app.get("/ai_bot_status", operation_id="ai_bot_status") + async def ai_bot_status(ai_bot_url: str = "http://localhost:8080") -> Dict[str, Any]: + """Check ai.bot server status and available commands""" + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get(f"{ai_bot_url}/status") + + if response.status_code == 200: + result = response.json() + return { + "status": "online", + "ai_bot_url": ai_bot_url, + "server_info": result, + "shell_available": True + } + else: + return { + "status": "error", + "error": f"ai.bot status check failed: {response.status_code}" + } + except Exception as e: + return { + "status": "offline", + "error": f"Cannot connect to ai.bot: {str(e)}", + "ai_bot_url": ai_bot_url + } + + @self.app.post("/isolated_python", operation_id="isolated_python") + async def isolated_python(code: str, ai_bot_url: str = "http://localhost:8080") -> Dict[str, Any]: + """Execute Python code in isolated ai.bot environment""" + # Python コードを /sh 経由で実行 + python_command = f'python3 -c "{code.replace('"', '\\"')}"' + return await remote_shell(python_command, ai_bot_url) + + @self.app.post("/isolated_analysis", operation_id="isolated_analysis") + async def isolated_analysis(file_path: str, analysis_type: str = "structure", ai_bot_url: str = "http://localhost:8080") -> Dict[str, Any]: + """Perform code analysis in isolated environment""" + if analysis_type == "structure": + command = f"find {file_path} -type f -name '*.py' | head -20" + elif analysis_type == "lines": + command = f"wc -l {file_path}" + elif analysis_type == "syntax": + command = f"python3 -m py_compile {file_path}" + else: + command = f"file {file_path}" + + return await remote_shell(command, ai_bot_url) + + # Mount MCP server + self.server.mount() - def get_server(self) -> FastapiMcpServer: + def get_server(self) -> FastApiMCP: """Get the FastAPI MCP server instance""" - return self.server \ No newline at end of file + return self.server + + async def close(self): + """Cleanup resources""" + pass \ No newline at end of file diff --git a/src/aigpt/mcp_server_simple.py b/src/aigpt/mcp_server_simple.py new file mode 100644 index 0000000..4215b2b --- /dev/null +++ b/src/aigpt/mcp_server_simple.py @@ -0,0 +1,146 @@ +"""Simple MCP Server implementation for ai.gpt""" + +from mcp import Server +from mcp.types import Tool, TextContent +from pathlib import Path +from typing import Any, Dict, List, Optional +import json + +from .persona import Persona +from .ai_provider import create_ai_provider +import subprocess +import os + + +def create_mcp_server(data_dir: Path, enable_card: bool = False) -> Server: + """Create MCP server with ai.gpt tools""" + server = Server("aigpt") + persona = Persona(data_dir) + + @server.tool() + async def get_memories(limit: int = 10) -> List[Dict[str, Any]]: + """Get active memories from the AI's memory system""" + memories = persona.memory.get_active_memories(limit=limit) + return [ + { + "id": mem.id, + "content": mem.content, + "level": mem.level.value, + "importance": mem.importance_score, + "is_core": mem.is_core, + "timestamp": mem.timestamp.isoformat() + } + for mem in memories + ] + + @server.tool() + async def get_relationship(user_id: str) -> Dict[str, Any]: + """Get relationship status with a specific user""" + rel = persona.relationships.get_or_create_relationship(user_id) + return { + "user_id": rel.user_id, + "status": rel.status.value, + "score": rel.score, + "transmission_enabled": rel.transmission_enabled, + "is_broken": rel.is_broken, + "total_interactions": rel.total_interactions, + "last_interaction": rel.last_interaction.isoformat() if rel.last_interaction else None + } + + @server.tool() + async def process_interaction(user_id: str, message: str, provider: str = "ollama", model: str = "qwen2.5") -> Dict[str, Any]: + """Process an interaction with a user""" + ai_provider = create_ai_provider(provider, model) + response, relationship_delta = persona.process_interaction(user_id, message, ai_provider) + rel = persona.relationships.get_or_create_relationship(user_id) + + return { + "response": response, + "relationship_delta": relationship_delta, + "new_relationship_score": rel.score, + "transmission_enabled": rel.transmission_enabled, + "relationship_status": rel.status.value + } + + @server.tool() + async def get_fortune() -> Dict[str, Any]: + """Get today's AI fortune""" + fortune = persona.fortune_system.get_today_fortune() + modifiers = persona.fortune_system.get_personality_modifier(fortune) + + return { + "value": fortune.fortune_value, + "date": fortune.date.isoformat(), + "consecutive_good": fortune.consecutive_good, + "consecutive_bad": fortune.consecutive_bad, + "breakthrough": fortune.breakthrough_triggered, + "personality_modifiers": modifiers + } + + @server.tool() + async def execute_command(command: str, working_dir: str = ".") -> Dict[str, Any]: + """Execute a shell command""" + try: + import shlex + result = subprocess.run( + shlex.split(command), + cwd=working_dir, + capture_output=True, + text=True, + timeout=60 + ) + + return { + "status": "success" if result.returncode == 0 else "error", + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "command": command + } + except subprocess.TimeoutExpired: + return {"error": "Command timed out"} + except Exception as e: + return {"error": str(e)} + + @server.tool() + async def analyze_file(file_path: str) -> Dict[str, Any]: + """Analyze a file using AI""" + try: + if not os.path.exists(file_path): + return {"error": f"File not found: {file_path}"} + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + ai_provider = create_ai_provider("ollama", "qwen2.5") + + prompt = f"Analyze this file and provide insights:\\n\\nFile: {file_path}\\n\\nContent:\\n{content[:2000]}" + analysis = ai_provider.generate_response(prompt, "You are a code analyst.") + + return { + "analysis": analysis, + "file_path": file_path, + "file_size": len(content), + "line_count": len(content.split('\\n')) + } + except Exception as e: + return {"error": str(e)} + + return server + + +async def main(): + """Run MCP server""" + import sys + from mcp import stdio_server + + data_dir = Path.home() / ".config" / "syui" / "ai" / "gpt" / "data" + data_dir.mkdir(parents=True, exist_ok=True) + + server = create_mcp_server(data_dir) + await stdio_server(server) + + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) \ No newline at end of file diff --git a/src/aigpt/memory.py b/src/aigpt/memory.py index c5f5e3e..f973a73 100644 --- a/src/aigpt/memory.py +++ b/src/aigpt/memory.py @@ -67,8 +67,13 @@ class MemoryManager: self._save_memories() return memory - def summarize_memories(self, user_id: str) -> Optional[Memory]: - """Create summary from recent memories""" + def add_memory(self, memory: Memory): + """Add a memory directly to the system""" + self.memories[memory.id] = memory + self._save_memories() + + def create_smart_summary(self, user_id: str, ai_provider=None) -> Optional[Memory]: + """Create AI-powered thematic summary from recent memories""" recent_memories = [ mem for mem in self.memories.values() if mem.level == MemoryLevel.FULL_LOG @@ -78,8 +83,40 @@ class MemoryManager: if len(recent_memories) < 5: return None - # Simple summary creation (in real implementation, use AI) - summary_content = f"Summary of {len(recent_memories)} recent interactions" + # Sort by timestamp for chronological analysis + recent_memories.sort(key=lambda m: m.timestamp) + + # Prepare conversation context for AI analysis + conversations_text = "\n\n".join([ + f"[{mem.timestamp.strftime('%Y-%m-%d %H:%M')}] {mem.content}" + for mem in recent_memories + ]) + + summary_prompt = f""" +Analyze these recent conversations and create a thematic summary focusing on: +1. Communication patterns and user preferences +2. Technical topics and problem-solving approaches +3. Relationship progression and trust level +4. Key recurring themes and interests + +Conversations: +{conversations_text} + +Create a concise summary (2-3 sentences) that captures the essence of this interaction period: +""" + + try: + if ai_provider: + summary_content = ai_provider.chat(summary_prompt, max_tokens=200) + else: + # Fallback to pattern-based analysis + themes = self._extract_themes(recent_memories) + summary_content = f"Themes: {', '.join(themes[:3])}. {len(recent_memories)} interactions with focus on technical discussions." + except Exception as e: + self.logger.warning(f"AI summary failed, using fallback: {e}") + themes = self._extract_themes(recent_memories) + summary_content = f"Themes: {', '.join(themes[:3])}. {len(recent_memories)} interactions." + summary_id = hashlib.sha256( f"summary_{datetime.now().isoformat()}".encode() ).hexdigest()[:16] @@ -87,23 +124,154 @@ class MemoryManager: summary = Memory( id=summary_id, timestamp=datetime.now(), - content=summary_content, + content=f"SUMMARY ({len(recent_memories)} conversations): {summary_content}", summary=summary_content, level=MemoryLevel.SUMMARY, - importance_score=0.5 + importance_score=0.6, + metadata={ + "memory_count": len(recent_memories), + "time_span": f"{recent_memories[0].timestamp.date()} to {recent_memories[-1].timestamp.date()}", + "themes": self._extract_themes(recent_memories)[:5] + } ) self.memories[summary.id] = summary - # Mark summarized memories for potential forgetting + # Reduce importance of summarized memories for mem in recent_memories: - mem.importance_score *= 0.9 + mem.importance_score *= 0.8 self._save_memories() return summary + def _extract_themes(self, memories: List[Memory]) -> List[str]: + """Extract common themes from memory content""" + common_words = {} + for memory in memories: + # Simple keyword extraction + words = memory.content.lower().split() + for word in words: + if len(word) > 4 and word.isalpha(): + common_words[word] = common_words.get(word, 0) + 1 + + # Return most frequent meaningful words + return sorted(common_words.keys(), key=common_words.get, reverse=True)[:10] + + def create_core_memory(self, ai_provider=None) -> Optional[Memory]: + """Analyze all memories to extract core personality-forming elements""" + # Collect all non-forgotten memories for analysis + all_memories = [ + mem for mem in self.memories.values() + if mem.level != MemoryLevel.FORGOTTEN + ] + + if len(all_memories) < 10: + return None + + # Sort by importance and timestamp for comprehensive analysis + all_memories.sort(key=lambda m: (m.importance_score, m.timestamp), reverse=True) + + # Prepare memory context for AI analysis + memory_context = "\n".join([ + f"[{mem.level.value}] {mem.timestamp.strftime('%Y-%m-%d')}: {mem.content[:200]}..." + for mem in all_memories[:20] # Top 20 memories + ]) + + core_prompt = f""" +Analyze these conversations and memories to identify core personality elements that define this user relationship: + +1. Communication style and preferences +2. Core values and principles +3. Problem-solving patterns +4. Trust level and relationship depth +5. Unique characteristics that make this relationship special + +Memories: +{memory_context} + +Extract the essential personality-forming elements (2-3 sentences) that should NEVER be forgotten: +""" + + try: + if ai_provider: + core_content = ai_provider.chat(core_prompt, max_tokens=150) + else: + # Fallback to pattern analysis + user_patterns = self._analyze_user_patterns(all_memories) + core_content = f"User shows {user_patterns['communication_style']} communication, focuses on {user_patterns['main_interests']}, and demonstrates {user_patterns['problem_solving']} approach." + except Exception as e: + self.logger.warning(f"AI core analysis failed, using fallback: {e}") + user_patterns = self._analyze_user_patterns(all_memories) + core_content = f"Core pattern: {user_patterns['communication_style']} style, {user_patterns['main_interests']} interests." + + # Create core memory + core_id = hashlib.sha256( + f"core_{datetime.now().isoformat()}".encode() + ).hexdigest()[:16] + + core_memory = Memory( + id=core_id, + timestamp=datetime.now(), + content=f"CORE PERSONALITY: {core_content}", + summary=core_content, + level=MemoryLevel.CORE, + importance_score=1.0, + is_core=True, + metadata={ + "source_memories": len(all_memories), + "analysis_date": datetime.now().isoformat(), + "patterns": self._analyze_user_patterns(all_memories) + } + ) + + self.memories[core_memory.id] = core_memory + self._save_memories() + + self.logger.info(f"Core memory created: {core_id}") + return core_memory + + def _analyze_user_patterns(self, memories: List[Memory]) -> Dict[str, str]: + """Analyze patterns in user behavior from memories""" + # Extract patterns from conversation content + all_content = " ".join([mem.content.lower() for mem in memories]) + + # Simple pattern detection + communication_indicators = { + "technical": ["code", "implementation", "system", "api", "database"], + "casual": ["thanks", "please", "sorry", "help"], + "formal": ["could", "would", "should", "proper"] + } + + problem_solving_indicators = { + "systematic": ["first", "then", "next", "step", "plan"], + "experimental": ["try", "test", "experiment", "see"], + "theoretical": ["concept", "design", "architecture", "pattern"] + } + + # Score each pattern + communication_style = max( + communication_indicators.keys(), + key=lambda style: sum(all_content.count(word) for word in communication_indicators[style]) + ) + + problem_solving = max( + problem_solving_indicators.keys(), + key=lambda style: sum(all_content.count(word) for word in problem_solving_indicators[style]) + ) + + # Extract main interests from themes + themes = self._extract_themes(memories) + main_interests = ", ".join(themes[:3]) if themes else "general technology" + + return { + "communication_style": communication_style, + "problem_solving": problem_solving, + "main_interests": main_interests, + "interaction_count": len(memories) + } + def identify_core_memories(self) -> List[Memory]: - """Identify memories that should become core (never forgotten)""" + """Identify existing memories that should become core (legacy method)""" core_candidates = [ mem for mem in self.memories.values() if mem.importance_score > 0.8 @@ -140,7 +308,7 @@ class MemoryManager: self._save_memories() def get_active_memories(self, limit: int = 10) -> List[Memory]: - """Get currently active memories for persona""" + """Get currently active memories for persona (legacy method)""" active = [ mem for mem in self.memories.values() if mem.level != MemoryLevel.FORGOTTEN @@ -152,4 +320,89 @@ class MemoryManager: reverse=True ) - return active[:limit] \ No newline at end of file + return active[:limit] + + def get_contextual_memories(self, query: str = "", limit: int = 10) -> Dict[str, List[Memory]]: + """Get memories organized by priority with contextual relevance""" + all_memories = [ + mem for mem in self.memories.values() + if mem.level != MemoryLevel.FORGOTTEN + ] + + # Categorize memories by type and importance + core_memories = [mem for mem in all_memories if mem.level == MemoryLevel.CORE] + summary_memories = [mem for mem in all_memories if mem.level == MemoryLevel.SUMMARY] + recent_memories = [ + mem for mem in all_memories + if mem.level == MemoryLevel.FULL_LOG + and (datetime.now() - mem.timestamp).days < 3 + ] + + # Apply keyword relevance if query provided + if query: + query_lower = query.lower() + + def relevance_score(memory: Memory) -> float: + content_score = 1 if query_lower in memory.content.lower() else 0 + summary_score = 1 if memory.summary and query_lower in memory.summary.lower() else 0 + metadata_score = 1 if any( + query_lower in str(v).lower() + for v in (memory.metadata or {}).values() + ) else 0 + return content_score + summary_score + metadata_score + + # Re-rank by relevance while maintaining type priority + core_memories.sort(key=lambda m: (relevance_score(m), m.importance_score), reverse=True) + summary_memories.sort(key=lambda m: (relevance_score(m), m.importance_score), reverse=True) + recent_memories.sort(key=lambda m: (relevance_score(m), m.importance_score), reverse=True) + else: + # Sort by importance and recency + core_memories.sort(key=lambda m: (m.importance_score, m.timestamp), reverse=True) + summary_memories.sort(key=lambda m: (m.importance_score, m.timestamp), reverse=True) + recent_memories.sort(key=lambda m: (m.importance_score, m.timestamp), reverse=True) + + # Return organized memory structure + return { + "core": core_memories[:3], # Always include top core memories + "summary": summary_memories[:3], # Recent summaries + "recent": recent_memories[:limit-6], # Fill remaining with recent + "all_active": all_memories[:limit] # Fallback for simple access + } + + def search_memories(self, keywords: List[str], memory_types: List[MemoryLevel] = None) -> List[Memory]: + """Search memories by keywords and optionally filter by memory types""" + if memory_types is None: + memory_types = [MemoryLevel.CORE, MemoryLevel.SUMMARY, MemoryLevel.FULL_LOG] + + matching_memories = [] + + for memory in self.memories.values(): + if memory.level not in memory_types or memory.level == MemoryLevel.FORGOTTEN: + continue + + # Check if any keyword matches in content, summary, or metadata + content_text = f"{memory.content} {memory.summary or ''}" + if memory.metadata: + content_text += " " + " ".join(str(v) for v in memory.metadata.values()) + + content_lower = content_text.lower() + + # Score by keyword matches + match_score = sum( + keyword.lower() in content_lower + for keyword in keywords + ) + + if match_score > 0: + # Add match score to memory for sorting + memory_copy = memory.model_copy() + memory_copy.importance_score += match_score * 0.1 + matching_memories.append(memory_copy) + + # Sort by relevance (match score + importance + core status) + matching_memories.sort( + key=lambda m: (m.is_core, m.importance_score, m.timestamp), + reverse=True + ) + + return matching_memories \ No newline at end of file diff --git a/src/aigpt/models.py b/src/aigpt/models.py index 7cf666b..1039af8 100644 --- a/src/aigpt/models.py +++ b/src/aigpt/models.py @@ -1,9 +1,9 @@ """Data models for ai.gpt system""" -from datetime import datetime +from datetime import datetime, date from typing import Optional, Dict, List, Any from enum import Enum -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator class MemoryLevel(str, Enum): @@ -30,9 +30,18 @@ class Memory(BaseModel): content: str summary: Optional[str] = None level: MemoryLevel = MemoryLevel.FULL_LOG - importance_score: float = Field(ge=0.0, le=1.0) + importance_score: float is_core: bool = False decay_rate: float = 0.01 + metadata: Optional[Dict[str, Any]] = None + + @field_validator('importance_score') + @classmethod + def validate_importance_score(cls, v): + """Ensure importance_score is within valid range, handle floating point precision issues""" + if abs(v) < 1e-10: # Very close to zero + return 0.0 + return max(0.0, min(1.0, v)) class Relationship(BaseModel): @@ -52,7 +61,7 @@ class Relationship(BaseModel): class AIFortune(BaseModel): """Daily AI fortune affecting personality""" - date: datetime.date + date: date fortune_value: int = Field(ge=1, le=10) consecutive_good: int = 0 consecutive_bad: int = 0 diff --git a/src/aigpt/persona.py b/src/aigpt/persona.py index 88f0561..1000f40 100644 --- a/src/aigpt/persona.py +++ b/src/aigpt/persona.py @@ -92,27 +92,85 @@ class Persona: else: return "contemplative" + def build_context_prompt(self, user_id: str, current_message: str) -> str: + """Build context-aware prompt with relevant memories""" + # Get contextual memories based on current message + memory_groups = self.memory.get_contextual_memories(query=current_message, limit=8) + + # Build context sections + context_parts = [] + + # Core personality elements (always included) + if memory_groups["core"]: + core_context = "\n".join([mem.content for mem in memory_groups["core"]]) + context_parts.append(f"CORE PERSONALITY:\n{core_context}") + + # Recent summaries for context + if memory_groups["summary"]: + summary_context = "\n".join([mem.summary or mem.content for mem in memory_groups["summary"]]) + context_parts.append(f"RECENT PATTERNS:\n{summary_context}") + + # Recent specific interactions + if memory_groups["recent"]: + recent_context = "\n".join([ + f"[{mem.timestamp.strftime('%m-%d')}] {mem.content[:100]}..." + for mem in memory_groups["recent"][:3] + ]) + context_parts.append(f"RECENT INTERACTIONS:\n{recent_context}") + + # Get current persona state + state = self.get_current_state() + + # Build final prompt + context_prompt = f"""You are an AI with persistent memory and evolving relationships. Your current state: + +PERSONALITY: {', '.join([f'{k}={v:.1f}' for k, v in state.base_personality.items()])} +MOOD: {state.current_mood} +FORTUNE: {state.fortune.fortune_value}/10 + +""" + + if context_parts: + context_prompt += "RELEVANT CONTEXT:\n" + "\n\n".join(context_parts) + "\n\n" + + context_prompt += f"""Respond to this message while staying true to your personality and the established relationship context: + +User: {current_message} + +AI:""" + + return context_prompt + def process_interaction(self, user_id: str, message: str, ai_provider=None) -> tuple[str, float]: - """Process user interaction and generate response""" + """Process user interaction and generate response with enhanced context""" # Get current state state = self.get_current_state() # Get relationship with user relationship = self.relationships.get_or_create_relationship(user_id) - # Simple response generation (use AI provider if available) + # Enhanced response generation with context awareness if relationship.is_broken: response = "..." relationship_delta = 0.0 else: if ai_provider: - # Use AI provider for response generation - memories = self.memory.get_active_memories(limit=5) - import asyncio - response = asyncio.run( - ai_provider.generate_response(message, state, memories) - ) - # Calculate relationship delta based on interaction quality + # Build context-aware prompt + context_prompt = self.build_context_prompt(user_id, message) + + # Generate response using AI with full context + try: + response = ai_provider.chat(context_prompt, max_tokens=200) + + # Clean up response if it includes the prompt echo + if "AI:" in response: + response = response.split("AI:")[-1].strip() + + except Exception as e: + self.logger.error(f"AI response generation failed: {e}") + response = f"I appreciate your message about {message[:50]}..." + + # Calculate relationship delta based on interaction quality and context if state.current_mood in ["joyful", "cheerful"]: relationship_delta = 2.0 elif relationship.status.value == "close_friend": @@ -120,8 +178,14 @@ class Persona: else: relationship_delta = 1.0 else: - # Fallback to simple responses - if state.current_mood == "joyful": + # Context-aware fallback responses + memory_groups = self.memory.get_contextual_memories(query=message, limit=3) + + if memory_groups["core"]: + # Reference core memories for continuity + response = f"Based on our relationship, I think {message.lower()} connects to what we've discussed before." + relationship_delta = 1.5 + elif state.current_mood == "joyful": response = f"What a wonderful day! {message} sounds interesting!" relationship_delta = 2.0 elif relationship.status.value == "close_friend": @@ -171,11 +235,16 @@ class Persona: if core_memories: self.logger.info(f"Identified {len(core_memories)} new core memories") - # Create memory summaries + # Create memory summaries for user_id in self.relationships.relationships: - summary = self.memory.summarize_memories(user_id) - if summary: - self.logger.info(f"Created summary for interactions with {user_id}") + try: + from .ai_provider import create_ai_provider + ai_provider = create_ai_provider() + summary = self.memory.create_smart_summary(user_id, ai_provider=ai_provider) + if summary: + self.logger.info(f"Created smart summary for interactions with {user_id}") + except Exception as e: + self.logger.warning(f"Could not create AI summary for {user_id}: {e}") self._save_state() self.logger.info("Daily maintenance completed") \ No newline at end of file diff --git a/src/aigpt/project_manager.py b/src/aigpt/project_manager.py new file mode 100644 index 0000000..aae743f --- /dev/null +++ b/src/aigpt/project_manager.py @@ -0,0 +1,321 @@ +"""Project management and continuous development logic for ai.shell""" + +import json +import os +from pathlib import Path +from typing import Dict, List, Optional, Any +from datetime import datetime +import subprocess +import hashlib + +from .models import Memory +from .ai_provider import AIProvider + + +class ProjectState: + """プロジェクトの現在状態を追跡""" + + def __init__(self, project_root: Path): + self.project_root = project_root + self.files_state: Dict[str, str] = {} # ファイルパス: ハッシュ + self.last_analysis: Optional[datetime] = None + self.project_context: Optional[str] = None + self.development_goals: List[str] = [] + self.known_patterns: Dict[str, Any] = {} + + def scan_project_files(self) -> Dict[str, str]: + """プロジェクトファイルをスキャンしてハッシュ計算""" + current_state = {} + + # 対象ファイル拡張子 + target_extensions = {'.py', '.js', '.ts', '.rs', '.go', '.java', '.cpp', '.c', '.h'} + + for file_path in self.project_root.rglob('*'): + if (file_path.is_file() and + file_path.suffix in target_extensions and + not any(part.startswith('.') for part in file_path.parts)): + + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + file_hash = hashlib.md5(content.encode()).hexdigest() + relative_path = str(file_path.relative_to(self.project_root)) + current_state[relative_path] = file_hash + except Exception: + continue + + return current_state + + def detect_changes(self) -> Dict[str, str]: + """ファイル変更を検出""" + current_state = self.scan_project_files() + changes = {} + + # 新規・変更ファイル + for path, current_hash in current_state.items(): + if path not in self.files_state or self.files_state[path] != current_hash: + changes[path] = "modified" if path in self.files_state else "added" + + # 削除ファイル + for path in self.files_state: + if path not in current_state: + changes[path] = "deleted" + + self.files_state = current_state + return changes + + +class ContinuousDeveloper: + """Claude Code的な継続開発機能""" + + def __init__(self, project_root: Path, ai_provider: Optional[AIProvider] = None): + self.project_root = project_root + self.ai_provider = ai_provider + self.project_state = ProjectState(project_root) + self.session_memory: List[str] = [] + + def load_project_context(self) -> str: + """プロジェクト文脈を読み込み""" + context_files = [ + "claude.md", "aishell.md", "README.md", + "pyproject.toml", "package.json", "Cargo.toml" + ] + + context_parts = [] + for filename in context_files: + file_path = self.project_root / filename + if file_path.exists(): + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + context_parts.append(f"## {filename}\n{content}") + except Exception: + continue + + return "\n\n".join(context_parts) + + def analyze_project_structure(self) -> Dict[str, Any]: + """プロジェクト構造を分析""" + analysis = { + "language": self._detect_primary_language(), + "framework": self._detect_framework(), + "structure": self._analyze_file_structure(), + "dependencies": self._analyze_dependencies(), + "patterns": self._detect_code_patterns() + } + return analysis + + def _detect_primary_language(self) -> str: + """主要言語を検出""" + file_counts = {} + for file_path in self.project_root.rglob('*'): + if file_path.is_file() and file_path.suffix: + ext = file_path.suffix.lower() + file_counts[ext] = file_counts.get(ext, 0) + 1 + + language_map = { + '.py': 'Python', + '.js': 'JavaScript', + '.ts': 'TypeScript', + '.rs': 'Rust', + '.go': 'Go', + '.java': 'Java' + } + + if file_counts: + primary_ext = max(file_counts.items(), key=lambda x: x[1])[0] + return language_map.get(primary_ext, 'Unknown') + return 'Unknown' + + def _detect_framework(self) -> str: + """フレームワークを検出""" + frameworks = { + 'fastapi': ['fastapi', 'uvicorn'], + 'django': ['django'], + 'flask': ['flask'], + 'react': ['react'], + 'next.js': ['next'], + 'rust-actix': ['actix-web'], + } + + # pyproject.toml, package.json, Cargo.tomlから依存関係を確認 + for config_file in ['pyproject.toml', 'package.json', 'Cargo.toml']: + config_path = self.project_root / config_file + if config_path.exists(): + try: + with open(config_path, 'r') as f: + content = f.read().lower() + + for framework, keywords in frameworks.items(): + if any(keyword in content for keyword in keywords): + return framework + except Exception: + continue + + return 'Unknown' + + def _analyze_file_structure(self) -> Dict[str, List[str]]: + """ファイル構造を分析""" + structure = {"directories": [], "key_files": []} + + for item in self.project_root.iterdir(): + if item.is_dir() and not item.name.startswith('.'): + structure["directories"].append(item.name) + elif item.is_file() and item.name in [ + 'main.py', 'app.py', 'index.js', 'main.rs', 'main.go' + ]: + structure["key_files"].append(item.name) + + return structure + + def _analyze_dependencies(self) -> List[str]: + """依存関係を分析""" + deps = [] + + # Python dependencies + pyproject = self.project_root / "pyproject.toml" + if pyproject.exists(): + try: + with open(pyproject, 'r') as f: + content = f.read() + # Simple regex would be better but for now just check for common packages + common_packages = ['fastapi', 'pydantic', 'uvicorn', 'ollama', 'openai'] + for package in common_packages: + if package in content: + deps.append(package) + except Exception: + pass + + return deps + + def _detect_code_patterns(self) -> Dict[str, int]: + """コードパターンを検出""" + patterns = { + "classes": 0, + "functions": 0, + "api_endpoints": 0, + "async_functions": 0 + } + + for py_file in self.project_root.rglob('*.py'): + try: + with open(py_file, 'r', encoding='utf-8') as f: + content = f.read() + + patterns["classes"] += content.count('class ') + patterns["functions"] += content.count('def ') + patterns["api_endpoints"] += content.count('@app.') + patterns["async_functions"] += content.count('async def') + except Exception: + continue + + return patterns + + def suggest_next_steps(self, current_task: Optional[str] = None) -> List[str]: + """次のステップを提案""" + if not self.ai_provider: + return ["AI provider not available for suggestions"] + + context = self.load_project_context() + analysis = self.analyze_project_structure() + changes = self.project_state.detect_changes() + + prompt = f""" +プロジェクト分析に基づいて、次の開発ステップを3-5個提案してください。 + +## プロジェクト文脈 +{context[:1000]} + +## 構造分析 +言語: {analysis['language']} +フレームワーク: {analysis['framework']} +パターン: {analysis['patterns']} + +## 最近の変更 +{changes} + +## 現在のタスク +{current_task or "特になし"} + +具体的で実行可能なステップを提案してください: +""" + + try: + response = self.ai_provider.chat(prompt, max_tokens=300) + # Simple parsing - in real implementation would be more sophisticated + steps = [line.strip() for line in response.split('\n') + if line.strip() and (line.strip().startswith('-') or line.strip().startswith('1.'))] + return steps[:5] + except Exception as e: + return [f"Error generating suggestions: {str(e)}"] + + def generate_code(self, description: str, file_path: Optional[str] = None) -> str: + """コード生成""" + if not self.ai_provider: + return "AI provider not available for code generation" + + context = self.load_project_context() + analysis = self.analyze_project_structure() + + prompt = f""" +以下の仕様に基づいてコードを生成してください。 + +## プロジェクト文脈 +{context[:800]} + +## 言語・フレームワーク +言語: {analysis['language']} +フレームワーク: {analysis['framework']} +既存パターン: {analysis['patterns']} + +## 生成要求 +{description} + +{"ファイルパス: " + file_path if file_path else ""} + +プロジェクトの既存コードスタイルと一貫性を保ったコードを生成してください: +""" + + try: + return self.ai_provider.chat(prompt, max_tokens=500) + except Exception as e: + return f"Error generating code: {str(e)}" + + def analyze_file(self, file_path: str) -> str: + """ファイル分析""" + full_path = self.project_root / file_path + if not full_path.exists(): + return f"File not found: {file_path}" + + try: + with open(full_path, 'r', encoding='utf-8') as f: + content = f.read() + except Exception as e: + return f"Error reading file: {str(e)}" + + if not self.ai_provider: + return f"File contents ({len(content)} chars):\n{content[:200]}..." + + context = self.load_project_context() + + prompt = f""" +以下のファイルを分析して、改善点や問題点を指摘してください。 + +## プロジェクト文脈 +{context[:500]} + +## ファイル: {file_path} +{content[:1500]} + +分析内容: +1. コード品質 +2. プロジェクトとの整合性 +3. 改善提案 +4. 潜在的な問題 +""" + + try: + return self.ai_provider.chat(prompt, max_tokens=400) + except Exception as e: + return f"Error analyzing file: {str(e)}" \ No newline at end of file