3 Commits

Author SHA1 Message Date
0f18689539 update 2025-06-02 06:21:24 +09:00
ad7d9387dd fix 2025-06-02 05:32:04 +09:00
1280394966 add card 2025-06-02 05:26:48 +09:00
91 changed files with 5235 additions and 8662 deletions

View File

@@ -0,0 +1,21 @@
{
"permissions": {
"allow": [
"Bash(mv:*)",
"Bash(mkdir:*)",
"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\")"
],
"deny": []
}
}

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
# OpenAI API Key (required for OpenAI provider)
OPENAI_API_KEY=your-api-key-here
# Ollama settings (optional)
OLLAMA_HOST=http://localhost:11434

30
.gitignore vendored
View File

@@ -1,24 +1,6 @@
# Rust
target/
Cargo.lock
# Database files
*.db
*.db-shm
*.db-wal
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
json
gpt
.claude
**target
**.lock
output.json
config/*.db
mcp/scripts/__*
data

7
.gitmodules vendored Normal file
View File

@@ -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

View File

@@ -1,37 +0,0 @@
[package]
name = "aigpt"
version = "0.3.0"
edition = "2021"
authors = ["syui"]
description = "AI memory system with personality analysis and relationship inference - Layers 1-4 Complete"
[lib]
name = "aigpt"
path = "src/lib.rs"
[[bin]]
name = "aigpt"
path = "src/main.rs"
[dependencies]
# CLI and async
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.40", features = ["rt", "rt-multi-thread", "macros", "io-std"] }
# Database
rusqlite = { version = "0.30", features = ["bundled"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Date/time and ULID
chrono = { version = "0.4", features = ["serde"] }
ulid = "1.1"
# Error handling
thiserror = "1.0"
anyhow = "1.0"
# Utilities
dirs = "5.0"

134
DEVELOPMENT_STATUS.md Normal file
View File

@@ -0,0 +1,134 @@
# ai.gpt 開発状況 (2025/01/06 更新)
## 現在の状態
### ✅ 実装済み機能
1. **基本システム**
- 階層的記憶システム(完全ログ→要約→コア→忘却)
- 不可逆的な関係性システムbroken状態は修復不可
- AI運勢による日々の人格変動
- 時間減衰による自然な関係性変化
2. **CLI機能**
- `chat` - AIとの会話Ollama/OpenAI対応
- `status` - 状態確認
- `fortune` - AI運勢確認
- `relationships` - 関係一覧
- `transmit` - 送信チェック現在はprint出力
- `maintenance` - 日次メンテナンス
- `config` - 設定管理
- `schedule` - スケジューラー管理
- `server` - MCP Server起動
- `shell` - インタラクティブシェルai.shell統合
3. **データ管理**
- 保存場所: `~/.config/aigpt/`
- 設定: `config.json`
- データ: `data/` ディレクトリ内の各種JSONファイル
4. **スケジューラー**
- Cron形式とインターバル形式対応
- 5種類のタスクタイプ実装済み
- バックグラウンド実行可能
5. **MCP Server**
- 14種類のツールを公開ai.gpt: 9種類、ai.shell: 5種類
- Claude Desktopなどから利用可能
- ai.card統合オプション--enable-card
6. **ai.shell統合**
- インタラクティブシェルモード
- シェルコマンド実行(!command形式
- AIコマンドanalyze, generate, explain
- aishell.md読み込み機能
- 高度な補完機能prompt-toolkit
## 🚧 未実装・今後の課題
### 短期的課題
1. **自律送信の実装**
- 現在: コンソールにprint出力
- TODO: atproto (Bluesky) への実際の投稿機能
- 参考: ai.bot (Rust/seahorse) との連携も検討
2. **テストの追加**
- 単体テスト
- 統合テスト
- CI/CDパイプライン
3. **エラーハンドリングの改善**
- より詳細なエラーメッセージ
- リトライ機構
### 中期的課題
1. **ai.botとの連携**
- Rust側のAPIエンドポイント作成
- 送信機能の委譲
2. **より高度な記憶要約**
- 現在: シンプルな要約
- TODO: AIによる意味的な要約
3. **Webダッシュボード**
- 関係性の可視化
- 記憶の管理UI
### 長期的課題
1. **他のsyuiプロジェクトとの統合**
- ai.card: カードゲームとの連携
- ai.verse: メタバース内でのNPC人格
- ai.os: システムレベルでの統合
2. **分散化**
- atproto上でのデータ保存
- ユーザーデータ主権の完全実現
## 次回開発時のエントリーポイント
### 1. 自律送信を実装する場合
```python
# src/aigpt/transmission.py を編集
# atproto-python ライブラリを追加
# _handle_transmission_check() メソッドを更新
```
### 2. ai.botと連携する場合
```python
# 新規ファイル: src/aigpt/bot_connector.py
# ai.botのAPIエンドポイントにHTTPリクエスト
```
### 3. テストを追加する場合
```bash
# tests/ディレクトリを作成
# pytest設定を追加
```
### 4. ai.shellの問題を修正する場合
```python
# src/aigpt/cli.py の shell コマンド
# prompt-toolkitのターミナル検出問題を回避
# 代替: simple input() または click.prompt()
```
## 設計思想の要点AI向け
1. **唯一性yui system**: 各ユーザーとAIの関係は1:1で、改変不可能
2. **不可逆性**: 関係性の破壊は修復不可能(現実の人間関係と同じ)
3. **階層的記憶**: ただのログではなく、要約・コア判定・忘却のプロセス
4. **環境影響**: AI運勢による日々の人格変動固定的でない
5. **段階的実装**: まずCLI print → atproto投稿 → ai.bot連携
## 現在のコードベースの理解
- **言語**: Python (typer CLI, fastapi_mcp)
- **AI統合**: Ollama (ローカル) / OpenAI API
- **データ形式**: JSON将来的にSQLite検討
- **認証**: atproto DID未実装だが設計済み
- **統合**: ai.shellRust版から移行、ai.cardMCP連携
このファイルを参照することで、次回の開発がスムーズに始められます。

469
README.md
View File

@@ -1,274 +1,255 @@
# aigpt
# ai.gpt - 自律的送信AI
AI memory system with psychological analysis for Claude via MCP.
存在子理論に基づく、関係性によって自発的にメッセージを送信するAIシステム。
**Current: Layers 1-4 Complete** - Memory storage, AI interpretation, personality analysis, integrated profile, and relationship inference.
## 中核概念
**Planned: Layer 5** - Knowledge sharing platform combining useful insights with author personality.
- **唯一性**: atproto DIDと1:1で紐付き、改変不可能な人格
- **不可逆性**: 関係性が壊れたら修復不可能(現実の人間関係と同じ)
- **記憶の階層**: 完全ログ→AI要約→コア判定→選択的忘却
- **AI運勢**: 1-10のランダム値による日々の人格変動
## Features
### Layer 1: Pure Memory Storage
- 🗄️ **SQLite Storage**: Reliable database with ACID guarantees
- 🔖 **ULID IDs**: Time-sortable, 26-character unique identifiers
- 🔍 **Search**: Fast content-based search
- 📝 **CRUD Operations**: Complete memory management
### Layer 2: AI Memory
- 🧠 **AI Interpretation**: Claude interprets and evaluates memories
- 📊 **Priority Scoring**: Importance ratings (0.0-1.0)
- 🎯 **Smart Storage**: Memory + evaluation in one step
### Layer 3: Personality Analysis
- 🔬 **Big Five Model**: Scientifically validated personality assessment
- 📈 **Pattern Recognition**: Analyzes memory patterns to build user profile
- 💾 **Historical Tracking**: Save and compare analyses over time
### Layer 3.5: Integrated Profile
- 🎯 **Essential Summary**: Unified view of personality, interests, and values
- 🤖 **AI-Optimized**: Primary tool for AI to understand the user
-**Smart Caching**: Auto-updates only when necessary
- 🔍 **Flexible Access**: Detailed data still accessible when needed
### Layer 4: Relationship Inference (Optional)
- 🤝 **Relationship Tracking**: Track interactions with entities (people, characters, etc.)
- 📊 **Bond Strength**: Infer relationship strength from memory patterns
- 🎮 **Game Ready**: Foundation for companion apps, games, VTubers
- 🔒 **Opt-in**: Enable only when needed with `--enable-layer4` flag
### Layer 5: Knowledge Sharing (Planned)
- 💡 **Information + Personality**: Share AI interactions with context
- 🌐 **SNS for AI Era**: Useful insights combined with author's unique perspective
- 🔒 **Privacy-First**: Share essence, not raw data
- 📊 **Showcase**: Display how AI understands you
### General
- 🛠️ **MCP Integration**: Works seamlessly with Claude Code
- 🧪 **Well-tested**: Comprehensive test coverage
- 🚀 **Simple & Fast**: Minimal dependencies, pure Rust
## Quick Start
### Installation
## インストール
```bash
# Build
cargo build --release
# Install (optional)
cp target/release/aigpt ~/.cargo/bin/
# Python仮想環境を推奨
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e .
```
### CLI Usage
## 設定
### APIキーの設定
```bash
# OpenAI APIキー
aigpt config set providers.openai.api_key sk-xxxxx
# atproto認証情報将来の自動投稿用
aigpt config set atproto.handle your.handle
aigpt config set atproto.password your-password
# 設定一覧を確認
aigpt config list
```
### データ保存場所
- 設定: `~/.config/syui/ai/gpt/config.json`
- データ: `~/.config/syui/ai/gpt/data/`
## 使い方
### 会話する
```bash
aigpt chat "did:plc:xxxxx" "こんにちは、今日はどんな気分?"
```
### ステータス確認
```bash
# AI全体の状態
aigpt status
# 特定ユーザーとの関係
aigpt status "did:plc:xxxxx"
```
### 今日の運勢
```bash
aigpt fortune
```
### 自律送信チェック
```bash
# ドライラン(確認のみ)
aigpt transmit
# 実行
aigpt transmit --execute
```
### 日次メンテナンス
```bash
aigpt maintenance
```
### 関係一覧
```bash
aigpt relationships
```
## データ構造
デフォルトでは `~/.config/syui/ai/gpt/` に以下のファイルが保存されます:
- `memories.json` - 会話記憶
- `conversations.json` - 会話ログ
- `relationships.json` - 関係性パラメータ
- `fortunes.json` - AI運勢履歴
- `transmissions.json` - 送信履歴
- `persona_state.json` - 人格状態
## 関係性の仕組み
- スコア0-200の範囲で変動
- 100を超えると送信機能が解禁
- 時間経過で自然減衰
- 大きなネガティブな相互作用で破壊される可能性
## ai.shell統合
インタラクティブシェルモードClaude Code風の体験
```bash
# Create a memory
aigpt create "Remember this information"
aigpt shell
# List all memories
aigpt list
# シェル内で使えるコマンド:
# help - コマンド一覧
# !<command> - シェルコマンド実行(例: !ls, !pwd
# analyze <file> - ファイルをAIで分析
# generate <desc> - コード生成
# explain <topic> - 概念の説明
# load - aishell.mdプロジェクトファイルを読み込み
# status - AI状態確認
# fortune - AI運勢確認
# clear - 画面クリア
# exit/quit - 終了
# Search memories
aigpt search "keyword"
# Show statistics
aigpt stats
# 通常のメッセージも送れます
ai.shell> こんにちは、今日は何をしましょうか?
```
### MCP Integration with Claude Code
## MCP Server
### サーバー起動
```bash
# Ollamaを使用デフォルト
aigpt server --model qwen2.5 --provider ollama
# OpenAIを使用
aigpt server --model gpt-4o-mini --provider openai
# カスタムポート
aigpt server --port 8080
# ai.card統合を有効化
aigpt server --enable-card
```
### AIプロバイダーを使った会話
```bash
# Ollamaで会話
aigpt chat "did:plc:xxxxx" "こんにちは" --provider ollama --model qwen2.5
# OpenAIで会話
aigpt chat "did:plc:xxxxx" "今日の調子はどう?" --provider openai --model gpt-4o-mini
```
### MCP Tools
サーバーが起動すると、以下のツールがAIから利用可能になります
**ai.gpt ツール:**
- `get_memories` - アクティブな記憶を取得
- `get_relationship` - 特定ユーザーとの関係を取得
- `get_all_relationships` - すべての関係を取得
- `get_persona_state` - 現在の人格状態を取得
- `process_interaction` - ユーザーとの対話を処理
- `check_transmission_eligibility` - 送信可能かチェック
- `get_fortune` - 今日の運勢を取得
- `summarize_memories` - 記憶を要約
- `run_maintenance` - メンテナンス実行
**ai.shell ツール:**
- `execute_command` - シェルコマンド実行
- `analyze_file` - ファイルのAI分析
- `write_file` - ファイル書き込み
- `read_project_file` - プロジェクトファイル読み込み
- `list_files` - ファイル一覧
**ai.card ツール(--enable-card時:**
- `get_user_cards` - ユーザーのカード取得
- `draw_card` - カードを引く(ガチャ)
- `get_card_details` - カード詳細情報
- `sync_cards_atproto` - atproto同期
- `analyze_card_collection` - コレクション分析
## 環境変数
`.env`ファイルを作成して設定:
```bash
# Add to Claude Code
claude mcp add aigpt /path/to/aigpt/target/release/aigpt server
cp .env.example .env
# OpenAI APIキーを設定
```
## MCP Tools
## スケジューラー機能
### Layer 1: Basic Memory (6 tools)
- `create_memory` - Simple memory creation
- `get_memory` - Retrieve by ID
- `list_memories` - List all memories
- `search_memories` - Content-based search
- `update_memory` - Update existing memory
- `delete_memory` - Remove memory
### Layer 2: AI Memory (1 tool)
- `create_ai_memory` - Create with AI interpretation and priority score
### Layer 3: Personality Analysis (2 tools)
- `save_user_analysis` - Save Big Five personality analysis
- `get_user_analysis` - Retrieve latest personality profile
### Layer 3.5: Integrated Profile (1 tool)
- `get_profile` - **Primary tool**: Get integrated user profile with essential summary
### Layer 4: Relationship Inference (2 tools, requires `--enable-layer4`)
- `get_relationship` - Get inferred relationship with specific entity
- `list_relationships` - List all relationships sorted by bond strength
## Usage Examples in Claude Code
### Layer 1: Simple Memory
```
Remember that the project deadline is next Friday.
```
Claude will use `create_memory` automatically.
### Layer 2: AI Memory with Evaluation
```
create_ai_memory({
content: "Designed a new microservices architecture",
ai_interpretation: "Shows technical creativity and strategic thinking",
priority_score: 0.85
})
```
### Layer 3: Personality Analysis
```
# After accumulating memories, analyze personality
save_user_analysis({
openness: 0.8,
conscientiousness: 0.7,
extraversion: 0.4,
agreeableness: 0.65,
neuroticism: 0.3,
summary: "High creativity and planning ability, introverted personality"
})
# Retrieve analysis
get_user_analysis()
```
### Layer 3.5: Integrated Profile (Recommended)
```
# Get essential user profile - AI's primary tool
get_profile()
# Returns:
{
"dominant_traits": [
{"name": "openness", "score": 0.8},
{"name": "conscientiousness", "score": 0.7},
{"name": "extraversion", "score": 0.4}
],
"core_interests": ["Rust", "architecture", "design", "system", "memory"],
"core_values": ["simplicity", "efficiency", "maintainability"],
"key_memory_ids": ["01H...", "01H...", ...],
"data_quality": 0.85
}
```
**Usage Pattern:**
- AI normally uses `get_profile()` to understand the user
- For specific details, AI can call `get_memory(id)`, `list_memories()`, etc.
- Profile auto-updates when needed (10+ memories, new analysis, or 7+ days)
### Layer 4: Relationship Inference (Optional, requires `--enable-layer4`)
```
# Create memories with entity tracking
Memory::new_with_entities({
content: "Had lunch with Alice",
ai_interpretation: "Pleasant social interaction",
priority_score: 0.7,
related_entities: ["alice"]
})
# Get relationship inference
get_relationship({ entity_id: "alice" })
# Returns:
{
"entity_id": "alice",
"interaction_count": 15,
"avg_priority": 0.75,
"days_since_last": 2,
"bond_strength": 0.82,
"relationship_type": "close_friend",
"confidence": 0.80
}
# List all relationships
list_relationships({ limit: 5 })
```
**Relationship Types:**
- `close_friend` (0.8+): Very strong bond
- `friend` (0.6-0.8): Strong connection
- `valued_acquaintance` (0.4-0.6, high priority): Important but not close
- `acquaintance` (0.4-0.6): Regular contact
- `regular_contact` (0.2-0.4): Occasional interaction
- `distant` (<0.2): Minimal connection
**Starting the Server:**
```bash
# Normal mode (Layer 1-3.5 only)
aigpt server
# With relationship features (Layer 1-4)
aigpt server --enable-layer4
```
## Big Five Personality Traits
- **Openness**: Creativity, curiosity, openness to new experiences
- **Conscientiousness**: Organization, planning, reliability
- **Extraversion**: Social energy, assertiveness, outgoingness
- **Agreeableness**: Cooperation, empathy, kindness
- **Neuroticism**: Emotional stability (low = stable, high = sensitive)
Scores range from 0.0 to 1.0, where higher scores indicate stronger trait expression.
## Storage Location
All data stored in: `~/.config/syui/ai/gpt/memory.db`
## Architecture
Multi-layer system design:
- **Layer 1** ✅ Complete: Pure memory storage (with entity tracking)
- **Layer 2** ✅ Complete: AI interpretation with priority scoring
- **Layer 3** ✅ Complete: Big Five personality analysis
- **Layer 3.5** ✅ Complete: Integrated profile (unified summary)
- **Layer 4** ✅ Complete: Relationship inference (optional, `--enable-layer4`)
- **Layer 4+** 🔵 Planned: Extended game/companion features
- **Layer 5** 🔵 Planned: Knowledge sharing (information + personality)
**Design Philosophy**:
- **"Internal complexity, external simplicity"**: Simple API, complex internals
- **"AI judges, tool records"**: AI makes decisions, tool stores data
- **Layered architecture**: Each layer independent but interconnected
- **Optional features**: Core layers always active, advanced layers opt-in
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for details.
## Documentation
- [Architecture](docs/ARCHITECTURE.md) - Multi-layer system design
- [Layer 1 Details](docs/LAYER1.md) - Technical details of memory storage
- [Old Versions](docs/archive/old-versions/) - Previous documentation
## Development
### タスクの追加
```bash
# Run tests
cargo test
# 6時間ごとに送信チェック
aigpt schedule add transmission_check "0 */6 * * *" --provider ollama --model qwen2.5
# Build for release
cargo build --release
# 30分ごとに送信チェックインターバル形式
aigpt schedule add transmission_check "30m"
# Run with verbose logging
RUST_LOG=debug aigpt server
# 毎日午前3時にメンテナンス
aigpt schedule add maintenance "0 3 * * *"
# 1時間ごとに関係性減衰
aigpt schedule add relationship_decay "1h"
# 毎週月曜日に記憶要約
aigpt schedule add memory_summary "0 0 * * MON"
```
## Design Philosophy
### タスク管理
**"AI evolves, tools don't"** - This tool provides simple, reliable storage while AI (Claude) handles interpretation, evaluation, and analysis. The tool focuses on being maintainable and stable.
```bash
# タスク一覧
aigpt schedule list
## License
# タスクを無効化
aigpt schedule disable --task-id transmission_check_1234567890
MIT
# タスクを有効化
aigpt schedule enable --task-id transmission_check_1234567890
## Author
# タスクを削除
aigpt schedule remove --task-id transmission_check_1234567890
```
syui
### スケジューラーデーモンの起動
```bash
# バックグラウンドでスケジューラーを実行
aigpt schedule run
```
### スケジュール形式
**Cron形式**:
- `"0 */6 * * *"` - 6時間ごと
- `"0 0 * * *"` - 毎日午前0時
- `"*/5 * * * *"` - 5分ごと
**インターバル形式**:
- `"30s"` - 30秒ごと
- `"5m"` - 5分ごと
- `"2h"` - 2時間ごと
- `"1d"` - 1日ごと
### タスクタイプ
- `transmission_check` - 送信可能なユーザーをチェックして自動送信
- `maintenance` - 日次メンテナンス(忘却、コア記憶判定など)
- `fortune_update` - AI運勢の更新
- `relationship_decay` - 関係性の時間減衰
- `memory_summary` - 記憶の要約作成
## 次のステップ
- atprotoへの実送信機能実装
- systemdサービス化
- Docker対応
- Webダッシュボード

63
aishell.md Normal file
View File

@@ -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-mcpMCP統合
- ai.gpt人格・記憶システム
## 開発目標
1. Claude Codeのような自然な開発体験
2. AIがプロジェクトコンテキストを理解
3. シェルコマンドとAIの seamless な統合
4. 開発者の生産性向上
## 今後の展開
- ai.cardとの統合カードゲームMCPサーバー
- より高度なプロジェクト理解機能
- 自動コード修正・リファクタリング
- テスト生成・実行

1
card Submodule

Submodule card added at 6dbe630b9d

326
claude.md Normal file
View File

@@ -0,0 +1,326 @@
# エコシステム統合設計書
## 中核思想
- **存在子理論**: この世界で最も小さいもの(存在子/aiの探求
- **唯一性原則**: 現実の個人の唯一性をすべてのシステムで担保
- **現実の反映**: 現実→ゲーム→現実の循環的影響
## システム構成図
```
存在子(ai) - 最小単位の意識
[ai.moji] 文字システム
[ai.os] + [ai.game device] ← 統合ハードウェア
├── ai.shell (Claude Code的機能)
├── ai.gpt (自律人格・記憶システム)
├── ai.ai (個人特化AI・心を読み取るAI)
├── ai.card (カードゲーム・iOS/Web/API)
└── ai.bot (分散SNS連携・カード配布)
[ai.verse] メタバース
├── world system (惑星型3D世界)
├── at system (atproto/分散SNS)
├── yui system (唯一性担保)
└── ai system (存在属性)
```
## 名前規則
名前規則は他のprojectと全て共通しています。exampleを示しますので、このルールに従ってください。
ここでは`ai.os`の場合の名前規則の例を記述します。
name: ai.os
**[ "package", "code", "command" ]**: aios
**[ "dir", "url" ]**: ai/os
**[ "domain", "json" ]**: ai.os
```sh
$ curl -sL https://git.syui.ai/ai/ai/raw/branch/main/ai.json|jq .ai.os
{ "type": "os" }
```
```json
{
"ai": {
"os":{}
}
}
```
他のprojectも同じ名前規則を採用します。`ai.gpt`ならpackageは`aigpt`です。
## config(設定ファイル, env, 環境依存)
`config`を置く場所は統一されており、各projectの名前規則の`dir`項目を使用します。例えば、aiosの場合は`~/.config/syui/ai/os/`以下となります。pythonなどを使用する場合、`python -m venv`などでこのpackage config dirに環境を構築して実行するようにしてください。
domain形式を採用して、私は各projectを`git.syui.ai/ai`にhostしていますから、`~/.config/syui/ai`とします。
```sh
[syui.ai]
syui/ai
```
```sh
# example
~/.config/syui/ai
├── card
├── gpt
├── os
└── shell
```
## 各システム詳細
### ai.gpt - 自律的送信AI
**目的**: 関係性に基づく自発的コミュニケーション
**中核概念**:
- **人格**: 記憶(過去の発話)と関係性パラメータで構成
- **唯一性**: atproto accountとの1:1紐付け、改変不可能
- **自律送信**: 関係性が閾値を超えると送信機能が解禁
**技術構成**:
- `MemoryManager`: 完全ログ→AI要約→コア判定→選択的忘却
- `RelationshipTracker`: 時間減衰・日次制限付き関係性スコア
- `TransmissionController`: 閾値判定・送信トリガー
- `Persona`: AI運勢1-10ランダムによる人格変動
**実装仕様**:
```
- 言語: Python (fastapi_mcp)
- ストレージ: JSON/SQLite選択式
- インターフェース: Python CLI (click/typer)
- スケジューリング: cron-like自律処理
```
### ai.card - カードゲームシステム
**目的**: atproto基盤でのユーザーデータ主権カードゲーム
**現在の状況**:
- ai.botの機能として実装済み
- atproto accountでmentionすると1日1回カードを取得
- ai.api (MCP server予定) でユーザー管理
**移行計画**:
- **iOS移植**: Claudeが担当予定
- **データ保存**: atproto collection recordに保存ユーザーがデータを所有
- **不正防止**: OAuth 2.1 scope (実装待ち) + MCP serverで対応
- **画像ファイル**: Cloudflare Pagesが最適
**yui system適用**:
- カードの効果がアカウント固有
- 改ざん防止によるゲームバランス維持
- 将来的にai.verseとの統合で固有スキルと連動
### ai.ai - 心を読み取るAI
**目的**: 個人特化型AI・深層理解システム
**ai.gptとの関係**:
- ai.gpt → ai.ai: 自律送信AIから心理分析AIへの連携
- 関係性パラメータの深層分析
- ユーザーの思想コア部分の特定支援
### ai.verse - UEメタバース
**目的**: 現実反映型3D世界
**yui system実装**:
- キャラクター ↔ プレイヤー 1:1紐付け
- unique skill: そのプレイヤーのみ使用可能
- 他プレイヤーは同キャラでも同スキル使用不可
**統合要素**:
- ai.card: ゲーム内アイテムとしてのカード
- ai.gpt: NPCとしての自律AI人格
- atproto: ゲーム内プロフィール連携
## データフロー設計
### 唯一性担保の実装
```
現実の個人 → atproto account (DID) → ゲーム内avatar → 固有スキル
↑_______________________________| (現実の反映)
```
### AI駆動変換システム
```
遊び・創作活動 → ai.gpt分析 → 業務成果変換 → 企業価値創出
↑________________________| (Play-to-Work)
```
### カードゲーム・データ主権フロー
```
ユーザー → ai.bot mention → カード生成 → atproto collection → ユーザー所有
↑ ↓
← iOS app表示 ← ai.card API ←
```
## 技術スタック統合
### Core Infrastructure
- **OS**: Rust-based ai.os (Arch Linux base)
- **Container**: Docker image distribution
- **Identity**: atproto selfhost server + DID管理
- **AI**: fastapi_mcp server architecture
- **CLI**: Python unified (click/typer) - Rustから移行
### Game Engine Integration
- **Engine**: Unreal Engine (Blueprint)
- **Data**: atproto → UE → atproto sync
- **Avatar**: 分散SNS profile → 3D character
- **Streaming**: game screen = broadcast screen
### Mobile/Device
- **iOS**: ai.card移植 (Claude担当)
- **Hardware**: ai.game device (future)
- **Interface**: controller-first design
## 実装優先順位
### Phase 1: AI基盤強化 (現在進行)
- [ ] ai.gpt memory system完全実装
- 記憶の階層化(完全ログ→要約→コア→忘却)
- 関係性パラメータの時間減衰システム
- AI運勢による人格変動機能
- [ ] ai.card iOS移植
- atproto collection record連携
- MCP server化ai.api刷新
- [ ] fastapi_mcp統一基盤構築
### Phase 2: ゲーム統合
- [ ] ai.verse yui system実装
- unique skill機能
- atproto連携強化
- [ ] ai.gpt ↔ ai.ai連携機能
- [ ] 分散SNS ↔ ゲーム同期
### Phase 3: メタバース浸透
- [ ] VTuber配信機能統合
- [ ] Play-to-Work変換システム
- [ ] ai.game device prototype
## 将来的な連携構想
### システム間連携(現在は独立実装)
```
ai.gpt (自律送信) ←→ ai.ai (心理分析)
ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界)
```
**共通基盤**: fastapi_mcp
**共通思想**: yui system現実の反映・唯一性担保
### データ改ざん防止戦略
- **短期**: MCP serverによる検証
- **中期**: OAuth 2.1 scope実装待ち
- **長期**: ブロックチェーン的整合性チェック
## AIコミュニケーション最適化
### プロジェクト要件定義テンプレート
```markdown
# [プロジェクト名] 要件定義
## 哲学的背景
- 存在子理論との関連:
- yui system適用範囲
- 現実反映の仕組み:
## 技術要件
- 使用技術fastapi_mcp統一
- atproto連携方法
- データ永続化方法:
## ユーザーストーリー
1. ユーザーが...すると
2. システムが...を実行し
3. 結果として...が実現される
## 成功指標
- 技術的:
- 哲学的(唯一性担保):
```
### Claude Code活用戦略
1. **小さく始める**: ai.gptのMCP機能拡張から
2. **段階的統合**: 各システムを個別に完成させてから統合
3. **哲学的一貫性**: 各実装でyui systemとの整合性を確認
4. **現実反映**: 実装がどう現実とゲームを繋ぐかを常に明記
## 開発上の留意点
### MCP Server設計指針
- 各AIgpt, card, ai, botは独立したMCPサーバー
- fastapi_mcp基盤で統一
- atproto DIDによる認証・認可
### 記憶・データ管理
- **ai.gpt**: 関係性の不可逆性重視
- **ai.card**: ユーザーデータ主権重視
- **ai.verse**: ゲーム世界の整合性重視
### 唯一性担保実装
- atproto accountとの1:1紐付け必須
- 改変不可能性をハッシュ・署名で保証
- 他システムでの再現不可能性を技術的に実現
## 継続的改善
- 各プロジェクトでこの設計書を参照
- 新機能追加時はyui systemとの整合性をチェック
- 他システムへの影響を事前評価
- Claude Code導入時の段階的移行計画
## ai.gpt深層設計思想
### 人格の不可逆性
- **関係性の破壊は修復不可能**: 現実の人間関係と同じ重み
- **記憶の選択的忘却**: 重要でない情報は忘れるが、コア記憶は永続
- **時間減衰**: すべてのパラメータは時間とともに自然減衰
### AI運勢システム
- 1-10のランダム値で日々の人格に変化
- 連続した幸運/不運による突破条件
- 環境要因としての人格形成
### 記憶の階層構造
1. **完全ログ**: すべての会話を記録
2. **AI要約**: 重要な部分を抽出して圧縮
3. **思想コア判定**: ユーザーの本質的な部分を特定
4. **選択的忘却**: 重要度の低い情報を段階的に削除
### 実装における重要な決定事項
- **言語統一**: Python (fastapi_mcp) で統一、CLIはclick/typer
- **データ形式**: JSON/SQLite選択式
- **認証**: atproto DIDによる唯一性担保
- **段階的実装**: まず会話→記憶→関係性→送信機能の順で実装
### 送信機能の段階的実装
- **Phase 1**: CLIでのprint出力現在
- **Phase 2**: atproto直接投稿
- **Phase 3**: ai.bot (Rust/seahorse) との連携
- **将来**: マルチチャネル対応SNS、Webhook等
## ai.gpt実装状況2025/01/06
### 完成した機能
- 階層的記憶システムMemoryManager
- 不可逆的関係性システムRelationshipTracker
- AI運勢システムFortuneSystem
- 統合人格システムPersona
- スケジューラー5種類のタスク
- MCP Server9種類のツール
- 設定管理(~/.config/syui/ai/gpt/
- 全CLIコマンド実装
### 次の開発ポイント
- `ai_gpt/DEVELOPMENT_STATUS.md` を参照
- 自律送信: transmission.pyでatproto実装
- ai.bot連携: 新規bot_connector.py作成
- テスト: tests/ディレクトリ追加
# footer
© syui

View File

@@ -1,713 +0,0 @@
# Architecture: Multi-Layer Memory System
## Design Philosophy
aigptは、独立したレイヤーを積み重ねる設計です。各レイヤーは
- **独立性**: 単独で動作可能
- **接続性**: 他のレイヤーと連携可能
- **段階的**: 1つずつ実装・テスト
## Layer Overview
```
┌─────────────────────────────────────────┐
│ Layer 5: Knowledge Sharing │ 🔵 Planned
│ (Information + Personality sharing) │
├─────────────────────────────────────────┤
│ Layer 4+: Extended Features │ 🔵 Planned
│ (Advanced game/companion systems) │
├─────────────────────────────────────────┤
│ Layer 4: Relationship Inference │ ✅ Complete
│ (Bond strength, relationship types) │ (Optional)
├─────────────────────────────────────────┤
│ Layer 3.5: Integrated Profile │ ✅ Complete
│ (Unified summary for AI consumption) │
├─────────────────────────────────────────┤
│ Layer 3: User Evaluation │ ✅ Complete
│ (Big Five personality analysis) │
├─────────────────────────────────────────┤
│ Layer 2: AI Memory │ ✅ Complete
│ (Claude interpretation, priority_score)│
├─────────────────────────────────────────┤
│ Layer 1: Pure Memory Storage │ ✅ Complete
│ (SQLite, ULID, entity tracking) │
└─────────────────────────────────────────┘
```
## Layer 1: Pure Memory Storage
**Status**: ✅ **Complete**
### Purpose
正確なデータの保存と参照。シンプルで信頼できる基盤。
### Technology Stack
- **Database**: SQLite with ACID guarantees
- **IDs**: ULID (time-sortable, 26 chars)
- **Language**: Rust with thiserror/anyhow
- **Protocol**: MCP (Model Context Protocol) via stdio
### Data Model
```rust
pub struct Memory {
pub id: String, // ULID
pub content: String, // User content
pub related_entities: Option<Vec<String>>, // Who/what this memory involves (Layer 4)
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
```
**Note**: `related_entities` added for Layer 4 support. Optional and backward compatible.
### Operations
- `create()` - Insert new memory
- `get(id)` - Retrieve by ID
- `update()` - Update existing memory
- `delete(id)` - Remove memory
- `list()` - List all (sorted by created_at DESC)
- `search(query)` - Content-based search
- `count()` - Total count
### File Structure
```
src/
├── core/
│ ├── error.rs - Error types (thiserror)
│ ├── memory.rs - Memory struct
│ ├── store.rs - SQLite operations
│ └── mod.rs - Module exports
├── mcp/
│ ├── base.rs - MCP server
│ └── mod.rs - Module exports
├── lib.rs - Library root
└── main.rs - CLI application
```
### Storage
- Location: `~/.config/syui/ai/gpt/memory.db`
- Schema: Single table with indexes on timestamps
- No migrations (fresh start for Layer 1)
---
## Layer 2: AI Memory
**Status**: ✅ **Complete**
### Purpose
Claudeが記憶内容を解釈し、重要度を評価。人間の記憶プロセス記憶と同時に評価を模倣。
### Extended Data Model
```rust
pub struct Memory {
// Layer 1 fields
pub id: String,
pub content: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// Layer 2 additions
pub ai_interpretation: Option<String>, // Claude's interpretation
pub priority_score: Option<f32>, // 0.0 - 1.0
}
```
### MCP Tools
- `create_ai_memory` - Create memory with AI interpretation and priority score
- `content`: Memory content
- `ai_interpretation`: Optional AI interpretation
- `priority_score`: Optional priority (0.0-1.0)
### Philosophy
"AIは進化しますが、ツールは進化しません" - AIが判断し、ツールは記録のみ。
### Implementation
- Backward compatible with Layer 1 (Optional fields)
- Automatic schema migration from Layer 1
- Claude Code does interpretation (no external API)
---
## Layer 3: User Evaluation
**Status**: ✅ **Complete**
### Purpose
Layer 2のメモリパターンからユーザーの性格を分析。Big Five心理学モデルを使用。
### Data Model
```rust
pub struct UserAnalysis {
pub id: String,
pub openness: f32, // 0.0-1.0: 創造性、好奇心
pub conscientiousness: f32, // 0.0-1.0: 計画性、信頼性
pub extraversion: f32, // 0.0-1.0: 外向性、社交性
pub agreeableness: f32, // 0.0-1.0: 協調性、共感性
pub neuroticism: f32, // 0.0-1.0: 神経質さ(低い=安定)
pub summary: String, // 分析サマリー
pub analyzed_at: DateTime<Utc>,
}
```
### Big Five Model
心理学で最も信頼性の高い性格モデルOCEAN
- **O**penness: 新しい経験への開かれさ
- **C**onscientiousness: 誠実性、計画性
- **E**xtraversion: 外向性
- **A**greeableness: 協調性
- **N**euroticism: 神経質さ
### Analysis Process
1. Layer 2メモリを蓄積
2. AIがパターンを分析活動の種類、優先度の傾向など
3. Big Fiveスコアを推測
4. 分析結果を保存
### MCP Tools
- `save_user_analysis` - Save Big Five personality analysis
- All 5 traits (0.0-1.0) + summary
- `get_user_analysis` - Get latest personality profile
### Storage
- SQLite table: `user_analyses`
- Historical tracking: Compare analyses over time
- Helper methods: `dominant_trait()`, `is_high()`
---
## Layer 3.5: Integrated Profile
**Status**: ✅ **Complete**
### Purpose
Layer 1-3のデータを統合し、本質のみを抽出した統一プロファイル。「内部は複雑、表面はシンプル」の設計哲学を実現。
### Problem Solved
Layer 1-3は独立して動作するが、バラバラのデータをAIが毎回解釈する必要があった。Layer 3.5は統合された1つの答えを提供し、効率性とシンプルさを両立。
### Data Model
```rust
pub struct UserProfile {
// 性格の本質Big Five上位3特性
pub dominant_traits: Vec<TraitScore>,
// 関心の核心最頻出トピック5個
pub core_interests: Vec<String>,
// 価値観の核心高priority メモリから抽出、5個
pub core_values: Vec<String>,
// 重要メモリID証拠、上位10個
pub key_memory_ids: Vec<String>,
// データ品質0.0-1.0、メモリ数と分析有無で算出)
pub data_quality: f32,
pub last_updated: DateTime<Utc>,
}
pub struct TraitScore {
pub name: String, // "openness", "conscientiousness", etc.
pub score: f32, // 0.0-1.0
}
```
### Integration Logic
**1. Dominant Traits Extraction**
- Big Fiveから上位3特性を自動選択
- スコアでソート
**2. Core Interests Extraction**
- メモリコンテンツから頻度分析
- AI interpretationは2倍の重み
- 上位5個を抽出
**3. Core Values Extraction**
- priority_score >= 0.7 のメモリから抽出
- 価値関連キーワードをフィルタリング
- 上位5個を抽出
**4. Key Memories**
- priority_scoreでソート
- 上位10個のIDを保持証拠として
**5. Data Quality Score**
- メモリ数: 50個で1.0(それ以下は比例)
- 性格分析あり: +0.5
- 加重平均で算出
### Caching Strategy
**Storage**: SQLite `user_profiles` テーブル1行のみ
**Update Triggers**:
1. 10個以上の新しいメモリ追加
2. 新しい性格分析の保存
3. 7日以上経過
**Flow**:
```
get_profile()
キャッシュ確認
更新必要? → No → キャッシュを返す
↓ Yes
Layer 1-3から再生成
キャッシュ更新
新しいプロファイルを返す
```
### MCP Tools
- `get_profile` - **Primary tool**: Get integrated profile
### Usage Pattern
**通常使用(効率的)**:
```
AI: get_profile()を呼ぶ
→ ユーザーの本質を理解
→ 適切な応答を生成
```
**詳細確認(必要時)**:
```
AI: get_profile()で概要を把握
→ 疑問がある
→ get_memory(id)で詳細確認
→ list_memories()で全体確認
```
### Design Philosophy
**"Internal complexity, external simplicity"**
- 内部: 複雑な分析、頻度計算、重み付け
- 表面: シンプルな1つのJSON
- AIは基本的にget_profile()のみ参照
- 柔軟性: 詳細データへのアクセスも可能
**Efficiency**:
- 頻繁な再計算を避ける(キャッシング)
- 必要時のみ更新(スマートトリガー)
- AI が迷わない1つの明確な答え
---
## Layer 4: Relationship Inference
**Status**: ✅ **Complete** (Optional feature)
### Purpose
Layer 1-3.5のデータから関係性を推測。ゲーム、コンパニオン、VTuberなどの外部アプリケーション向け。
### Activation
CLI引数で明示的に有効化:
```bash
aigpt server --enable-layer4
```
デフォルトでは無効Layer 1-3.5のみ)。
### Data Model
```rust
pub struct RelationshipInference {
pub entity_id: String,
pub interaction_count: u32, // この entity とのメモリ数
pub avg_priority: f32, // 平均重要度
pub days_since_last: i64, // 最終接触からの日数
pub bond_strength: f32, // 関係の強さ (0.0-1.0)
pub relationship_type: String, // close_friend, friend, etc.
pub confidence: f32, // 推測の信頼度 (0.0-1.0)
pub inferred_at: DateTime<Utc>,
}
```
### Inference Logic
**1. データ収集**:
- Layer 1から entity に関連するメモリを抽出
- Layer 3.5からユーザー性格プロファイルを取得
**2. Bond Strength 計算**:
```rust
if user.extraversion < 0.5 {
// 内向的: 少数の深い関係を好む
// 回数が重要
bond = interaction_count * 0.6 + avg_priority * 0.4
} else {
// 外向的: 多数の浅い関係
// 質が重要
bond = interaction_count * 0.4 + avg_priority * 0.6
}
```
**3. Relationship Type 分類**:
- `close_friend` (0.8+): 非常に強い絆
- `friend` (0.6-0.8): 強い繋がり
- `valued_acquaintance` (0.4-0.6, 高priority): 重要だが親密ではない
- `acquaintance` (0.4-0.6): 定期的な接触
- `regular_contact` (0.2-0.4): 時々の接触
- `distant` (<0.2): 最小限の繋がり
**4. Confidence 計算**:
- データ量に基づく信頼度
- 1-2回: 0.2-0.3 (低)
- 5回: 0.5 (中)
- 10回以上: 0.8+ (高)
### Design Philosophy
**推測ベース + 短期キャッシング**:
- 毎回Layer 1-3.5から計算
- 5分間の短期キャッシュで負荷軽減
- メモリ更新時にキャッシュ無効化
**キャッシング戦略**:
- SQLiteテーブル`relationship_cache`)に保存
- 個別エンティティ: `get_relationship(entity_id)`
- 全体リスト: `list_relationships()`
- メモリ作成/更新/削除時に自動クリア
**独立性**:
- Layer 1-3.5に依存
- Layer 1-3.5から独立(オプション機能)
- 有効化しなければ完全に無視される
**外部アプリケーション向け**:
- aigptはバックエンド推測エンジン
- フロントエンド(ゲーム、コンパニオン等)が表示を担当
- MCPで繋がる
### MCP Tools
- `get_relationship(entity_id)` - 特定entity との関係を取得
- `list_relationships(limit)` - 全関係をbond_strength順でリスト
### Usage Example
```
# サーバー起動Layer 4有効
aigpt server --enable-layer4
# 関係性取得
get_relationship({ entity_id: "alice" })
# 結果:
{
"bond_strength": 0.82,
"relationship_type": "close_friend",
"interaction_count": 15,
"confidence": 0.80
}
```
---
## Layer 4+: Extended Features
**Status**: 🔵 **Planned**
Advanced game and companion system features to be designed based on Layer 4 foundation.
---
## Layer 4a: Game Systems (Archive)
**Status**: 🔵 **Archived Concept**
### Purpose
ゲーム的要素で記憶管理を楽しく。
### Features
- **Rarity Levels**: Common → Uncommon → Rare → Epic → Legendary
- **XP System**: Memory creation earns XP
- **Rankings**: Based on total priority score
- **Visualization**: Game-style output formatting
### Data Additions
```rust
pub struct GameMemory {
// Previous layers...
pub rarity: RarityLevel,
pub xp_value: u32,
pub discovered_at: DateTime<Utc>,
}
```
---
## Layer 4b: AI Companion
**Status**: 🔵 **Planned**
### Purpose
育成可能な恋愛コンパニオン。
### Features
- Personality types (Tsundere, Kuudere, Genki, etc.)
- Relationship level (0-100)
- Memory-based interactions
- Growth through conversations
### Data Model
```rust
pub struct Companion {
pub id: String,
pub name: String,
pub personality: CompanionPersonality,
pub relationship_level: u8, // 0-100
pub memories_shared: Vec<String>,
pub last_interaction: DateTime<Utc>,
}
```
---
## Layer 5: Knowledge Sharing (Planned)
**Status**: 🔵 **Planned**
### Purpose
AIとのやり取りを「情報 + 個性」として共有する。SNSや配信のように、**有用な知見**と**作者の個性**を両立させたコンテンツプラットフォーム。
### Design Philosophy
人々が求めるもの:
1. **情報価値**: 「このプロンプトでこんな結果が得られた」「この問題をAIでこう解決した」
2. **個性・共感**: 「この人はこういう人だ」という親近感、信頼
SNSや配信と同じく、**情報のみは無機質**、**個性のみは空虚**。両方を組み合わせることで価値が生まれる。
### Data Model
```rust
pub struct SharedInteraction {
pub id: String,
// 情報価値
pub problem: String, // 何を解決しようとしたか
pub approach: String, // AIとどうやり取りしたか
pub result: String, // 何を得たか
pub usefulness_score: f32, // 有用性 (0.0-1.0, priority_score由来)
pub tags: Vec<String>, // 検索用タグ
// 個性
pub author_profile: ShareableProfile, // 作者の本質
pub why_this_matters: String, // なぜこの人がこれに取り組んだか
// メタデータ
pub views: u32,
pub useful_count: u32, // 「役に立った」カウント
pub created_at: DateTime<Utc>,
}
pub struct ShareableProfile {
// ユーザーの本質Layer 3.5から抽出)
pub personality_essence: Vec<TraitScore>, // Top 3 traits
pub core_interests: Vec<String>, // 5個
pub core_values: Vec<String>, // 5個
// AIの解釈
pub ai_perspective: String, // AIがこのユーザーをどう理解しているか
pub confidence: f32, // データ品質 (0.0-1.0)
// 関係性スタイルLayer 4から推測、匿名化
pub relationship_style: String, // 例: "深く狭い繋がりを好む"
}
```
### Privacy Design
**共有するもの:**
- ✅ 本質Layer 3.5の統合プロファイル)
- ✅ パターン(関係性スタイル、思考パターン)
- ✅ 有用な知見(問題解決のアプローチ)
**共有しないもの:**
- ❌ 生の会話内容Layer 1-2
- ❌ 個人を特定できる情報
- ❌ メモリID、タイムスタンプ等の生データ
### Use Cases
**1. AI時代のGitHub Gist**
- 有用なプロンプトとその結果を共有
- 作者の個性とアプローチが見える
- 「この人の考え方が参考になる」
**2. 知見のSNS**
- 情報を発信しながら、個性も伝わる
- フォロー、「役に立った」機能
- 関心領域でフィルタリング
**3. AIペルソナのショーケース**
- 「AIは私をこう理解している」を共有
- 性格分析の精度を比較
- コミュニティでの自己表現
### Implementation Ideas
```rust
// Layer 5のMCPツール
- create_shareable_interaction() -
- get_shareable_profile() -
- export_interaction() - JSON/Markdown形式でエクスポート
- anonymize_data() -
```
### Future Platforms
- Web UI: 知見を閲覧・検索・共有
- API: 外部サービスと連携
- RSS/Atom: フィード配信
- Markdown Export: ブログ投稿用
---
## Implementation Strategy
### Phase 1: Layer 1 ✅ (Complete)
- [x] Core memory storage
- [x] SQLite integration
- [x] MCP server
- [x] CLI interface
- [x] Tests
- [x] Documentation
### Phase 2: Layer 2 ✅ (Complete)
- [x] Add AI interpretation fields to schema
- [x] Implement priority scoring logic
- [x] Create `create_ai_memory` tool
- [x] Update MCP server
- [x] Automatic schema migration
- [x] Backward compatibility
### Phase 3: Layer 3 ✅ (Complete)
- [x] Big Five personality model
- [x] UserAnalysis data structure
- [x] user_analyses table
- [x] `save_user_analysis` tool
- [x] `get_user_analysis` tool
- [x] Historical tracking support
### Phase 3.5: Layer 3.5 ✅ (Complete)
- [x] UserProfile data structure
- [x] Integration logic (traits, interests, values)
- [x] Frequency analysis for topic extraction
- [x] Value keyword extraction
- [x] Data quality scoring
- [x] Caching mechanism (user_profiles table)
- [x] Smart update triggers
- [x] `get_profile` MCP tool
### Phase 4: Layer 4 ✅ (Complete)
- [x] Add `related_entities` to Layer 1 Memory struct
- [x] Database migration for backward compatibility
- [x] RelationshipInference data structure
- [x] Bond strength calculation (personality-aware)
- [x] Relationship type classification
- [x] Confidence scoring
- [x] `get_relationship` MCP tool
- [x] `list_relationships` MCP tool
- [x] CLI control flag (`--enable-layer4`)
- [x] Tool visibility control
### Phase 5: Layers 4+ and 5 (Future)
- [ ] Extended game/companion features (Layer 4+)
- [ ] Sharing mechanisms (Layer 5)
- [ ] Public/private modes (Layer 5)
## Design Principles
1. **Simplicity First**: Each layer adds complexity incrementally
2. **Backward Compatibility**: New layers don't break old ones
3. **Feature Flags**: Optional features via Cargo features
4. **Independent Testing**: Each layer has its own test suite
5. **Clear Boundaries**: Layers communicate through defined interfaces
## Technology Choices
### Why SQLite?
- ACID guarantees
- Better querying than JSON
- Built-in indexes
- Single-file deployment
- No server needed
### Why ULID?
- Time-sortable (unlike UUID v4)
- Lexicographically sortable
- 26 characters (compact)
- No collision concerns
### Why Rust?
- Memory safety
- Performance
- Excellent error handling
- Strong type system
- Great tooling (cargo, clippy)
### Why MCP?
- Standard protocol for AI tools
- Works with Claude Code/Desktop
- Simple stdio-based communication
- No complex networking
## Future Considerations
### Potential Enhancements
- Full-text search (SQLite FTS5)
- Tag system
- Memory relationships/links
- Export/import functionality
- Multiple databases
- Encryption for sensitive data
### Scalability
- Layer 1: Handles 10K+ memories easily
- Consider pagination for Layer 4 (UI display)
- Indexing strategy for search performance
## Development Guidelines
### Adding a New Layer
1. **Design**: Document data model and operations
2. **Feature Flag**: Add to Cargo.toml
3. **Schema**: Extend database schema (migrations)
4. **Implementation**: Write code in new module
5. **Tests**: Comprehensive test coverage
6. **MCP Tools**: Add new MCP tools if needed
7. **Documentation**: Update this file
### Code Organization
```
src/
├── core/
│ ├── memory.rs # Layer 1: Memory struct (with related_entities)
│ ├── store.rs # Layer 1-4: SQLite operations
│ ├── analysis.rs # Layer 3: UserAnalysis (Big Five)
│ ├── profile.rs # Layer 3.5: UserProfile (integrated)
│ ├── relationship.rs # Layer 4: RelationshipInference
│ ├── error.rs # Error types
│ └── mod.rs # Module exports
├── mcp/
│ ├── base.rs # MCP server (all layers, with --enable-layer4)
│ └── mod.rs # Module exports
├── lib.rs # Library root
└── main.rs # CLI application (with layer4 flag)
```
**Future layers**:
- Layer 4+: `src/game/` - Extended game/companion systems
- Layer 5: `src/distribution/` - Sharing mechanisms
---
**Version**: 0.3.0
**Last Updated**: 2025-11-06
**Current Status**: Layers 1-4 Complete (Layer 4 opt-in with --enable-layer4)

View File

@@ -1,217 +0,0 @@
# Layer 1 Rebuild - Pure Memory Storage
## Overview
This is a complete rewrite of aigpt, starting fresh from scratch as requested. We've built **Layer 1: Pure Memory Storage** with optimal technology choices and clean architecture.
## Changes from v0.1.0
### Architecture
- **Complete rewrite** from scratch, focusing on simplicity and best practices
- Clean separation: `src/core/` for business logic, `src/mcp/` for protocol
- Layer 1 only - pure memory storage with accurate data preservation
### Technology Stack Improvements
#### ID Generation
- **Before**: UUID v4 (random, not time-sortable)
- **After**: ULID (time-sortable, 26 chars, lexicographically sortable)
#### Storage
- **Before**: HashMap + JSON file
- **After**: SQLite with proper schema, indexes, and ACID guarantees
#### Error Handling
- **Before**: anyhow everywhere
- **After**: thiserror for library errors, anyhow for application errors
#### Async Runtime
- **Before**: tokio with "full" features
- **After**: tokio with minimal features (rt, macros, io-stdio)
### File Structure
```
src/
├── lib.rs # Library root
├── main.rs # CLI application
├── core/
│ ├── mod.rs # Core module exports
│ ├── error.rs # thiserror-based error types
│ ├── memory.rs # Memory struct and logic
│ └── store.rs # SQLite-based MemoryStore
└── mcp/
├── mod.rs # MCP module exports
└── base.rs # Basic MCP server implementation
```
### Core Features
#### Memory Struct (`src/core/memory.rs`)
```rust
pub struct Memory {
pub id: String, // ULID - time-sortable
pub content: String, // The actual memory content
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
```
#### MemoryStore (`src/core/store.rs`)
- SQLite-based storage with proper schema
- Indexed columns for performance (created_at, updated_at)
- Full CRUD operations:
- `create()` - Insert new memory
- `get()` - Retrieve by ID
- `update()` - Update existing memory
- `delete()` - Remove memory
- `list()` - List all memories (sorted by created_at DESC)
- `search()` - Search by content (case-insensitive)
- `count()` - Total memory count
- Comprehensive tests included
#### MCP Server (`src/mcp/base.rs`)
Clean, stdio-based MCP server with these tools:
- `create_memory` - Create new memory
- `get_memory` - Get memory by ID
- `search_memories` - Search by content
- `list_memories` - List all memories
- `update_memory` - Update existing memory
- `delete_memory` - Delete memory
### CLI Commands
```bash
# Start MCP server
aigpt server
# Create a memory
aigpt create "Memory content"
# Get a memory by ID
aigpt get <id>
# Update a memory
aigpt update <id> "New content"
# Delete a memory
aigpt delete <id>
# List all memories
aigpt list
# Search memories
aigpt search "query"
# Show statistics
aigpt stats
```
### Database Location
Memories are stored in:
`~/.config/syui/ai/gpt/memory.db`
### Dependencies
#### Core Dependencies
- `rusqlite = "0.30"` - SQLite database (bundled)
- `ulid = "1.1"` - ULID generation
- `chrono = "0.4"` - Date/time handling
- `serde = "1.0"` - Serialization
- `serde_json = "1.0"` - JSON for MCP protocol
#### Error Handling
- `thiserror = "1.0"` - Library error types
- `anyhow = "1.0"` - Application error handling
#### CLI & Async
- `clap = "4.5"` - CLI parsing
- `tokio = "1.40"` - Async runtime (minimal features)
#### Utilities
- `dirs = "5.0"` - Platform-specific directories
### Removed Features
The following features have been removed for Layer 1 simplicity:
- AI interpretation and priority scoring
- Game-style formatting (rarity levels, XP, diagnosis types)
- Companion system
- ChatGPT conversation import
- OpenAI integration
- Web scraping capabilities
- Extended MCP servers
These features will be added back in subsequent layers (Layer 2-4) as independent, connectable modules.
### Testing
All core modules include comprehensive unit tests:
- Memory creation and updates
- SQLite CRUD operations
- Search functionality
- Error handling
Run tests with:
```bash
cargo test
```
### Next Steps: Future Layers
#### Layer 2: AI Memory
- Claude Code interprets content
- Assigns priority_score (0.0-1.0)
- Adds interpreted_content field
- Independent feature flag
#### Layer 3: User Evaluation
- Diagnose user personality from memory patterns
- Execute during memory creation
- Return diagnosis types
#### Layer 4: Game Systems
- 4a: Ranking system (rarity levels, XP)
- 4b: AI Companion (romance system)
- Game-style visualization
- Shareable results
#### Layer 5: Distribution (Future)
- Game streaming integration
- Sharing mechanisms
- Public/private modes
### Design Philosophy
1. **Simplicity First**: Core logic is simple, only 4 files in `src/core/`
2. **Clean Separation**: Each layer will be independently toggleable
3. **Optimal Choices**: Best Rust packages for each task
4. **Test Coverage**: All core logic has tests
5. **Minimal Dependencies**: Only what's needed for Layer 1
6. **Future-Ready**: Clean architecture allows easy addition of layers
### Build Status
⚠️ **Note**: Initial commit cannot be built due to network issues accessing crates.io.
The code compiles correctly once dependencies are available.
To build:
```bash
cargo build --release
```
The binary will be at: `target/release/aigpt`
### MCP Integration
To use with Claude Code:
```bash
claude mcp add aigpt /path/to/aigpt/target/release/aigpt server
```
---
**Version**: 0.2.0
**Date**: 2025-11-05
**Status**: Layer 1 Complete (pending build due to network issues)

30
docs/README.md Normal file
View File

@@ -0,0 +1,30 @@
# ai.gpt ドキュメント
ai.gptは、記憶と関係性に基づいて自律的に動作するAIシステムです。
## 目次
- [クイックスタート](quickstart.md)
- [基本概念](concepts.md)
- [コマンドリファレンス](commands.md)
- [設定ガイド](configuration.md)
- [スケジューラー](scheduler.md)
- [MCP Server](mcp-server.md)
- [開発者向け](development.md)
## 特徴
- 🧠 **階層的記憶システム**: 完全ログ→要約→コア記憶→忘却
- 💔 **不可逆的な関係性**: 現実の人間関係のように修復不可能
- 🎲 **AI運勢システム**: 日々変化する人格
- 🤖 **自律送信**: 関係性が深まると自発的にメッセージ
- 🔗 **MCP対応**: AIツールとして記憶を提供
## システム要件
- Python 3.10以上
- オプション: Ollama または OpenAI API
## ライセンス
MIT License

View File

@@ -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統一基盤
- 各AIgpt, 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環境に進化しました。

View File

@@ -1,70 +0,0 @@
# Changelog
## [Unreleased] - 2025-11-05
### 🎉 Major Changes: Complete Local Operation
#### Changed
- **Removed external AI API dependency**: No longer calls Claude/OpenAI APIs
- **Claude Code does the interpretation**: AIが解釈するのではなく、Claude Code 自身が解釈
- **Zero cost**: API料金が一切かからない
- **Complete privacy**: データが外部に送信されない
#### Technical Details
- Removed `openai` crate dependency
- Removed `ai-analysis` feature (no longer needed)
- Simplified `ai_interpreter.rs` to be a lightweight wrapper
- Updated `create_memory_with_ai` MCP tool to accept `interpreted_content` and `priority_score` from Claude Code
- Added `create_memory_with_interpretation()` method to MemoryManager
- Updated tool descriptions to guide Claude Code on how to interpret and score
#### Benefits
-**完全ローカル**: 外部 API 不要
-**ゼロコスト**: API 料金なし
-**プライバシー**: データ漏洩の心配なし
-**シンプル**: 依存関係が少ない
-**高速**: ネットワーク遅延なし
#### How It Works Now
1. User: 「今日、新しいアイデアを思いついた」とメモリを作成
2. Claude Code: 内容を解釈し、スコア (0.0-1.0) を計算
3. Claude Code: `create_memory_with_ai` ツールを呼び出し、解釈とスコアを渡す
4. aigpt: メモリを保存し、ゲーム風の結果を返す
5. Claude Code: ユーザーに結果を表示
#### Migration Notes
For users who were expecting external AI API usage:
- No API keys needed anymore (ANTHROPIC_API_KEY, OPENAI_API_KEY)
- Claude Code (local) now does all the interpretation
- This is actually better: faster, cheaper, more private!
---
## [0.1.0] - Initial Release
### Added
- Basic memory CRUD operations
- ChatGPT conversation import
- stdio MCP server implementation
- Psychological priority scoring (0.0-1.0)
- Gamification features (rarity, diagnosis types, XP)
- Romance companion system
- 11 MCP tools for Claude Code integration
### Features
- Memory capacity management (max 100 by default)
- Automatic pruning of low-priority memories
- Game-style result displays
- Companion affection and level system
- Daily challenges
- Ranking displays
### Documentation
- README.md with full examples
- DESIGN.md with system architecture
- TECHNICAL_REVIEW.md with evaluation
- ROADMAP.md with 7-phase plan
- QUICKSTART.md for immediate usage
- USAGE.md for detailed instructions

View File

@@ -1,121 +0,0 @@
# AI記憶システム設計書
## コンセプト
AIの記憶装置は、人間の記憶に近い形で動作する。すべてを正確に記憶するのではなく、**解釈**して保存する。
## 従来の記憶システムとの違い
### 従来型
```
会話 → 保存 → 検索
```
### 新設計(心理優先記憶装置)
```
会話 → AI解釈 → 保存 → 検索
心理判定(1-100)
優先順位付け
容量管理
```
## 設計原理
1. **解釈保存**: 記憶する際はAIが解釈を加える
- 元のコンテンツと解釈後のコンテンツの両方を保持
- 「覚えること自体が創造」という考え方
2. **心理判定**: 各記憶に重要度スコア1-100を付与
- AIが自律的に判断
- ユーザー固有性を考慮
- 感情的重要度を評価
3. **優先順位管理**: スコアに基づく優先順位
- 高スコア = 重要な記憶
- 低スコア = 忘れられやすい記憶
4. **容量制限**: 人間の記憶のように限界がある
- 総容量制限(デフォルト: 100件
- 単発保存容量制限
- 優先度が低いものから自動削除
## データ構造
```rust
struct Memory {
id: String, // UUID
content: String, // 元のコンテンツ
interpreted_content: String, // AI解釈後のコンテンツ
priority_score: f32, // 心理判定スコア (0.0-1.0)
user_context: Option<String>, // ユーザー固有性
created_at: DateTime<Utc>, // 作成日時
updated_at: DateTime<Utc>, // 更新日時
}
```
## 実装機能
### 1. 心理判定機能
- AI APIを使用して重要度を0.0-1.0で評価
- 判定基準:
- 感情的インパクト (0.0-0.25)
- ユーザーとの関連性 (0.0-0.25)
- 新規性・独自性 (0.0-0.25)
- 実用性 (0.0-0.25)
### 2. 保存機能
- 保存前にAI解釈を実行
- 心理判定スコアを自動付与
- 容量超過時は低スコアから削除
### 3. 検索機能
- 優先順位順にソート
- スコアによるフィルタリング
- セマンティック検索(オプション)
### 4. 容量管理
- デフォルト最大: 100件
- 設定可能な上限
- 自動プルーニング(低スコア削除)
## 実装ステップ
1. Memory構造体の拡張
2. AI解釈機能の実装OpenAI API使用
3. 心理判定機能の実装
4. 容量管理機能の実装
5. ソート・フィルタリング機能の強化
6. MCPツールへの統合
## 設定例
```json
{
"max_memories": 100,
"min_priority_score": 0.3,
"auto_prune": true,
"interpretation_enabled": true
}
```
## スコアリングシステムの哲学
0.0-1.0のfloat値を採用する理由
- **正規化**: 機械学習やAIにとって扱いやすい標準形式
- **直感性**: 0が最低、1が最高という明確な基準
- **精度**: 0.75などの細かい値で微妙な重要度の差を表現可能
- **拡張性**: 時間軸(0.0-1.0)や確率(0.0-1.0)などとの統合が容易
この設計は、「I + o」概念oの周りを0.0-1.0の時間軸で表す)とも整合性がある。
## ゲームのセーブデータとの類似性
- **Git = セーブ機能**: バージョン管理
- **GitHub = クラウドセーブ**: グローバルデータ共有
- **ATProto = データプロトコル**: 分散型データ保存
- **AI記憶 = プレイヤー記憶**: 経験の蓄積と解釈
ゲームのセーブデータも「プレイヤーの行動を解釈したデータ」として扱うことで、より意味のある永続化が可能になる。

View File

@@ -1,263 +0,0 @@
# クイックスタートガイド 🚀
## 今すぐ試す方法
### ステップ1: MCPサーバーを起動
```bash
# API キー不要!完全にローカルで動作
./target/debug/aigpt server
```
### ステップ2: Claude Desktop/Codeに設定
#### Claude Codeの場合
```bash
# MCP設定に追加
claude mcp add aigpt /home/user/aigpt/target/debug/aigpt server
```
#### 手動設定の場合
`~/.config/claude-code/config.json` に追加:
```json
{
"mcpServers": {
"aigpt": {
"command": "/home/user/aigpt/target/debug/aigpt",
"args": ["server"]
}
}
}
```
### ステップ3: Claude Codeを再起動
MCPサーバーを認識させるため、Claude Codeを再起動してください。
---
## 使い方の流れ
### 🎮 1. 心理テスト風にメモリ作成
**Claude Codeで:**
```
create_memory_with_ai ツールを使って
「今日、新しいAIシステムのアイデアを思いついた」
というメモリを作成してください。
```
**結果:**
```
╔══════════════════════════════════════╗
║ 🎲 メモリースコア判定 ║
╚══════════════════════════════════════╝
🟣 EPIC 85点
💡 【革新者】
💕 好感度: ❤️❤️🤍🤍🤍🤍🤍🤍🤍🤍
💎 XP獲得: +850 XP
📤 シェア用テキストも生成されます!
```
### 💕 2. 恋愛コンパニオンを作成
**Claude Codeで:**
```
create_companion ツールで、
名前「エミリー」、性格「energetic」の
コンパニオンを作成してください。
```
**結果:**
```
╔══════════════════════════════════════╗
║ 💕 エミリー のプロフィール ║
╚══════════════════════════════════════╝
⚡ 性格: 元気で冒険好き
🏆 関係レベル: Lv.1
💕 好感度: 🤍🤍🤍🤍🤍🤍🤍🤍🤍🤍 0%
💬 今日のひとこと:
「おはよう!今日は何か面白いことある?」
```
### 🎊 3. コンパニオンに反応してもらう
**Claude Codeで:**
```
companion_react ツールで、
先ほど作成した記憶IDを渡してください。
```
**結果:**
```
╔══════════════════════════════════════╗
║ 💕 エミリー の反応 ║
╚══════════════════════════════════════╝
⚡ エミリー:
「すごい新しいAIシステムのアイデア
って本当に素晴らしいね!
一緒に実現させよう!」
💕 好感度: ❤️❤️🤍🤍🤍🤍🤍🤍🤍🤍 15%
💎 XP獲得: +850 XP
```
### 🏆 4. ランキング確認
**Claude Codeで:**
```
list_memories_by_priority ツールで
TOP 10を表示してください。
```
**結果:**
```
╔══════════════════════════════════════╗
║ 🏆 メモリーランキング TOP 10 ║
╚══════════════════════════════════════╝
🥇 1位 🟣 EPIC 85点 - 新しいAIシステム...
```
---
## 現在の制限事項と対処法
### ❌ AI機能が使えない場合
**原因:** OpenAI APIキーが未設定
**対処法:**
```bash
# 環境変数に設定
export OPENAI_API_KEY=sk-...
# または起動時に指定
OPENAI_API_KEY=sk-... ./target/debug/aigpt server
```
**代替案:**
```
# 基本版のツールを使うAI機能なし
create_memory ツールで「テスト」というメモリを作成
# スコアは固定で 0.5 になります
```
### ❌ コンパニオンが保存されない
**現状:** セッション終了で消える
**対処法(今後実装予定):**
- JSON保存機能
- 次回起動時に自動ロード
**今できること:**
- 毎回 create_companion で再作成
- プロフィールをスクリーンショット保存
---
## トラブルシューティング
### Q: MCPツールが見つからない
```bash
# Claude Codeを完全再起動
# または設定ファイルを確認
cat ~/.config/claude-code/config.json
```
### Q: 記憶が保存されない
```bash
# データファイルを確認
ls -la ~/.config/syui/ai/gpt/memory.json
# ない場合は自動作成されます
```
### Q: ビルドエラーが出る
```bash
# 依存関係を更新
cargo clean
cargo build --release
# AI機能付き
cargo build --release --features ai-analysis
```
---
## おすすめの使い方
### 💡 アイデア記録として
1. 思いついたアイデアを create_memory_with_ai で記録
2. スコアで重要度を客観的に判定
3. 高スコアのアイデアに集中
### 💕 恋愛ゲームとして
1. コンパニオンを作成
2. 日々の出来事や考えを記録
3. コンパニオンに反応してもらう
4. 好感度MAXを目指す
### 📊 自己分析として
1. 定期的に思考を記録
2. 診断タイプの傾向を確認
3. ランキングで振り返り
---
## 次にやること
### すぐできる改善
- [ ] コンパニオンの永続化実装
- [ ] 複数コンパニオン対応
- [ ] デイリーチャレンジ完了チェック
### 中期的な目標
- [ ] Bluesky連携シェア機能
- [ ] Webダッシュボード
- [ ] もっと多様なイベント
---
## 楽しみ方のコツ
1. **毎日使う**
- daily_challenge で習慣化
- コンパニオンの「今日のひとこと」
2. **高スコアを狙う**
- LEGENDARY (90%+) を目指す
- XP 1000獲得の快感
3. **相性を楽しむ**
- 自分のタイプを確認
- 相性の良いコンパニオン選択
4. **イベントを楽しむ**
- 好感度100%の告白イベント
- レベル10の特別な絆
---
## さあ、始めよう! 🚀
```bash
# MCPサーバー起動
./target/debug/aigpt server
# Claude Codeで試す
# → create_memory_with_ai
# → create_companion
# → companion_react
# → 楽しむ!
```

View File

@@ -1,431 +0,0 @@
# aigpt - AI Memory System with Psychological Priority
AI記憶装置心理優先記憶システム。**完全にローカルで動作**し、Claude Code と連携して、心理判定スコア付きのメモリ管理を実現します。
## 🌟 特徴
-**完全ローカル**: 外部 API 不要、プライバシー保護
-**ゼロコスト**: API 料金なし
-**Claude Code 統合**: Claude 自身が解釈とスコアリング
-**ゲーミフィケーション**: 心理テスト風の楽しい表示
-**恋愛コンパニオン**: 育成要素付き
## コンセプト
従来の「会話 → 保存 → 検索」ではなく、「会話 → **Claude による解釈** → 保存 → 検索」を実現。
Claude Code が記憶を解釈し、重要度を0.0-1.0のスコアで評価。優先度の高い記憶を保持し、低い記憶は自動的に削除されます。
## 機能
- **AI解釈付き記憶**: 元のコンテンツとAI解釈後のコンテンツを保存
- **心理判定スコア**: 0.0-1.0のfloat値で重要度を評価
- **優先順位管理**: スコアに基づく自動ソートとフィルタリング
- **容量制限**: 最大100件設定可能、低スコアから自動削除
- **メモリのCRUD操作**: メモリの作成、更新、削除、検索
- **ChatGPT JSONインポート**: ChatGPTの会話履歴からメモリを抽出
- **stdio MCP実装**: Claude Desktop/Codeとの簡潔な連携
- **JSONファイル保存**: シンプルなファイルベースのデータ保存
## インストール
1. Rustをインストールまだの場合:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
2. プロジェクトをビルド(依存関係が少なくシンプル!):
```bash
cargo build --release
# API キー不要!完全にローカルで動作します
```
3. バイナリをパスの通った場所にコピー(オプション):
```bash
cp target/release/aigpt $HOME/.cargo/bin/
```
4. Claude Code/Desktopに追加
```sh
# Claude Codeの場合
claude mcp add aigpt $HOME/.cargo/bin/aigpt server
# または
claude mcp add aigpt $HOME/.cargo/bin/aigpt serve
```
## 使用方法
### ヘルプの表示
```bash
aigpt --help
```
### MCPサーバーとして起動
```bash
# MCPサーバー起動 (どちらでも可)
aigpt server
aigpt serve
```
### ChatGPT会話のインポート
```bash
# ChatGPT conversations.jsonをインポート
aigpt import path/to/conversations.json
```
## Claude Desktop/Codeへの設定
1. Claude Desktopの設定ファイルを開く:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`
2. 以下の設定を追加:
```json
{
"mcpServers": {
"aigpt": {
"command": "/Users/syui/.cargo/bin/aigpt",
"args": ["server"]
}
}
}
```
## 提供するMCPツール一覧
### 基本ツール
1. **create_memory** - 新しいメモリを作成(シンプル版)
2. **update_memory** - 既存のメモリを更新
3. **delete_memory** - メモリを削除
4. **search_memories** - メモリを検索
5. **list_conversations** - インポートされた会話を一覧表示
### AI機能ツール重要
6. **create_memory_with_ai** - AI解釈と心理判定付きでメモリを作成 🎮
- 元のコンテンツをAIが解釈
- 重要度を0.0-1.0のスコアで自動評価
- ユーザーコンテキストを考慮可能
- **ゲーム風の診断結果を表示!**(占い・心理テスト風)
7. **list_memories_by_priority** - 優先順位順にメモリをリスト 🏆
- 高スコアから順に表示
- min_scoreで閾値フィルタリング可能
- limit で件数制限可能
- **ランキング形式で表示!**
8. **daily_challenge** - 今日のデイリーチャレンジを取得 📅
- 日替わりのお題を取得
- ボーナスXPが獲得可能
### 恋愛コンパニオン機能 💕NEW!
9. **create_companion** - AIコンパニオンを作成
- 名前と性格を選択
- 5つの性格タイプから選択可能
10. **companion_react** - コンパニオンの反応を見る
- あなたの記憶にコンパニオンが反応
- 好感度・XP・信頼度が上昇
- スペシャルイベント発生あり
11. **companion_profile** - コンパニオンのプロフィール表示
- ステータス確認
- 今日のひとこと
## ツールの使用例
Claude Desktop/Codeで以下のように使用します
### 基本的なメモリ作成
```
MCPツールを使って「今日は良い天気です」というメモリーを作成してください
```
### AI解釈付きメモリ作成推奨🎮
```
create_memory_with_ai ツールを使って「新しいAI記憶システムのアイデアを思いついた」というメモリーを作成してください。
ユーザーコンテキスト: 「AI開発者、創造的思考を重視」
```
**ゲーム風の結果表示:**
```
╔══════════════════════════════════════════════════════════════╗
║ 🎲 メモリースコア判定 ║
╚══════════════════════════════════════════════════════════════╝
⚡ 分析完了! あなたの思考が記録されました
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 総合スコア
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟣 EPIC 85点
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 詳細分析
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💓 感情的インパクト: [████████░░] 80%
🔗 ユーザー関連性: [██████████] 100%
✨ 新規性・独自性: [█████████░] 90%
⚙️ 実用性: [████████░░] 80%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎊 あなたのタイプ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 【革新者】
創造的で実用的なアイデアを生み出す。常に新しい可能性を探求し、
それを現実のものにする力を持つ。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 報酬
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💎 XP獲得: +850 XP
🎁 レア度: 🟣 EPIC
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📤 この結果をシェアしよう!
#aigpt #メモリースコア #革新者
```
**シェア用テキストも自動生成:**
```
🎲 AIメモリースコア診断結果
🟣 EPIC 85点
💡 【革新者】
新しいAI記憶システムのアイデアを思いついた
#aigpt #メモリースコア #AI診断
```
### 優先順位でメモリをリスト 🏆
```
list_memories_by_priority ツールで、スコア0.7以上の重要なメモリを10件表示してください
```
**ランキング形式で表示:**
```
╔══════════════════════════════════════════════════════════════╗
║ 🏆 メモリーランキング TOP 10 ║
╚══════════════════════════════════════════════════════════════╝
🥇 1位 🟡 LEGENDARY 95点 - 心理優先記憶装置の設計
🥈 2位 🟣 EPIC 88点 - AIとのやり取りをコンテンツ化
🥉 3位 🟣 EPIC 85点 - ゲーム化の構想
  4位 🔵 RARE 75点 - SNSの本質について
  5位 🔵 RARE 72点 - AI OSの可能性
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### 今日のデイリーチャレンジ 📅
```
daily_challenge ツールで今日のお題を確認
```
**表示例:**
```
╔══════════════════════════════════════════════════════════════╗
║ 📅 今日のチャレンジ ║
╚══════════════════════════════════════════════════════════════╝
✨ 今日学んだことを記録しよう
🎁 報酬: +200 XP
💎 完了すると特別なバッジが獲得できます!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### 恋愛コンパニオン 💕NEW!
#### 1. コンパニオン作成
```
create_companion ツールで、名前「エミリー」、性格「energetic」のコンパニオンを作成
```
**性格タイプ:**
- `energetic` ⚡ - 元気で冒険好き(革新者と相性◎)
- `intellectual` 📚 - 知的で思慮深い(哲学者と相性◎)
- `practical` 🎯 - 現実的で頼れる(実務家と相性◎)
- `dreamy` 🌙 - 夢見がちでロマンチック(夢想家と相性◎)
- `balanced` ⚖️ - バランス型(分析家と相性◎)
**表示例:**
```
╔══════════════════════════════════════════════════════════════╗
║ 💕 エミリー のプロフィール ║
╚══════════════════════════════════════════════════════════════╝
⚡ 性格: 元気で冒険好き
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 ステータス
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 関係レベル: Lv.1
💕 好感度: 🤍🤍🤍🤍🤍🤍🤍🤍🤍🤍 0%
🤝 信頼度: 0 / 100
💎 総XP: 0 XP
💬 今日のひとこと:
「おはよう!今日は何か面白いことある?」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
#### 2. コンパニオンの反応
```
create_memory_with_ai で高スコアの記憶を作成
companion_react でコンパニオンに見せる
```
**表示例EPIC記憶への反応:**
```
╔══════════════════════════════════════════════════════════════╗
║ 💕 エミリー の反応 ║
╚══════════════════════════════════════════════════════════════╝
⚡ エミリー:
「おお、「新しいAI記憶システムのアイデア」って面白いね
あなたのそういうところ、好きだな。」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💕 好感度: ❤️❤️🤍🤍🤍🤍🤍🤍🤍🤍 15% (+8.5%)
💎 XP獲得: +850 XP
🏆 レベル: Lv.1
🤝 信頼度: 5 / 100
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
#### 3. スペシャルイベント発生!
```
好感度が100%に達すると...
💕 特別なイベント発生!
エミリー:「ねえ...あのね。
   いつも一緒にいてくれてありがとう。
   あなたのこと、すごく大切に思ってるの。
   これからも、ずっと一緒にいてね?」
🎊 エミリー の好感度がMAXになりました
```
#### 4. 相性システム
```
あなたのタイプ × コンパニオンの性格 = 相性ボーナス
例:
💡【革新者】 × ⚡ 元気で冒険好き = 相性95%
→ 好感度上昇1.95倍
🧠【哲学者】 × 📚 知的で思慮深い = 相性95%
→ 深い会話で絆が深まる
```
### メモリの検索
```
MCPツールを使って「天気」に関するメモリーを検索してください
```
### 会話一覧の表示
```
MCPツールを使ってインポートした会話の一覧を表示してください
```
## データ保存
- デフォルトパス: `~/.config/syui/ai/gpt/memory.json`
- JSONファイルでデータを保存
- 自動的にディレクトリとファイルを作成
### データ構造
```json
{
"memories": {
"uuid": {
"id": "uuid",
"content": "元のメモリー内容",
"interpreted_content": "AI解釈後のメモリー内容",
"priority_score": 0.75,
"user_context": "ユーザー固有のコンテキスト(オプション)",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
},
"conversations": {
"conversation_id": {
"id": "conversation_id",
"title": "会話のタイトル",
"created_at": "2024-01-01T00:00:00Z",
"message_count": 10
}
}
}
```
### 心理判定スコアについて
0.0-1.0のfloat値で重要度を表現
- **0.0-0.25**: 低優先度(忘れられやすい)
- **0.25-0.5**: 中優先度
- **0.5-0.75**: 高優先度
- **0.75-1.0**: 最高優先度(重要な記憶)
評価基準:
- 感情的インパクト (0.0-0.25)
- ユーザーとの関連性 (0.0-0.25)
- 新規性・独自性 (0.0-0.25)
- 実用性 (0.0-0.25)
## 開発
```bash
# 開発モードで実行
cargo run -- server
# ChatGPTインポートのテスト
cargo run -- import json/conversations.json
# テストの実行
cargo test
# フォーマット
cargo fmt
# Lintチェック
cargo clippy
```
## トラブルシューティング
### MCPサーバーが起動しない
```bash
# バイナリが存在するか確認
ls -la ~/.cargo/bin/aigpt
# 手動でテスト
echo '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' | aigpt server
```
### Claude Desktopでツールが見つからない
1. Claude Desktopを完全に再起動
2. 設定ファイルのパスが正しいか確認
3. ログファイルを確認: `~/Library/Logs/Claude/mcp-server-aigpt.log`
### インポートが失敗する
```bash
# JSONファイルの形式を確認
head -100 conversations.json | jq '.[0] | keys'
```
## ライセンス
MIT

View File

@@ -1,125 +0,0 @@
# Claude Memory MCP 設定ガイド
## モード選択
### 標準モード (Simple Mode)
- 基本的なメモリー機能のみ
- 軽量で高速
- 最小限の依存関係
### 拡張モード (Extended Mode)
- AI分析機能
- セマンティック検索
- Web統合機能
- 高度なインサイト抽出
## ビルド・実行方法
### 標準モード
```bash
# MCPサーバー起動
cargo run --bin memory-mcp
# CLI実行
cargo run --bin aigpt -- create "メモリー内容"
```
### 拡張モード
```bash
# MCPサーバー起動
cargo run --bin memory-mcp-extended --features extended
# CLI実行
cargo run --bin aigpt-extended --features extended -- create "メモリー内容" --analyze
```
## 設定ファイルの配置
### 標準モード
#### Claude Desktop
```bash
# macOS
cp claude_desktop_config.json ~/.config/claude-desktop/claude_desktop_config.json
# Windows
cp claude_desktop_config.json %APPDATA%\Claude\claude_desktop_config.json
```
#### Claude Code
```bash
# プロジェクトルートまたはグローバル設定
cp claude_code_config.json .claude/config.json
# または
cp claude_code_config.json ~/.claude/config.json
```
### 拡張モード
#### Claude Desktop
```bash
# macOS
cp claude_desktop_config_extended.json ~/.config/claude-desktop/claude_desktop_config.json
# Windows
cp claude_desktop_config_extended.json %APPDATA%\Claude\claude_desktop_config.json
```
#### Claude Code
```bash
# プロジェクトルートまたはグローバル設定
cp claude_code_config_extended.json .claude/config.json
# または
cp claude_code_config_extended.json ~/.claude/config.json
```
## 環境変数設定
```bash
export MEMORY_AUTO_EXECUTE=true
export MEMORY_AUTO_SAVE=true
export MEMORY_AUTO_SEARCH=true
export TRIGGER_SENSITIVITY=high
export MEMORY_DB_PATH=~/.claude/memory.db
```
## 設定オプション
### auto_execute
- `true`: 自動でMCPツールを実行
- `false`: 手動実行のみ
### trigger_sensitivity
- `high`: 多くのキーワードで反応
- `medium`: 適度な反応
- `low`: 明確なキーワードのみ
### max_memories
メモリーの最大保存数
### search_limit
検索結果の最大表示数
## カスタマイズ
`trigger_words`セクションでトリガーワードをカスタマイズ可能:
```json
"trigger_words": {
"custom_category": ["カスタム", "キーワード", "リスト"]
}
```
## トラブルシューティング
1. MCPサーバーが起動しない場合:
- Rustがインストールされているか確認
- `cargo build --release`でビルド確認
2. 自動実行されない場合:
- 環境変数が正しく設定されているか確認
- トリガーワードが含まれているか確認
3. メモリーが保存されない場合:
- データベースファイルのパスが正しいか確認
- 書き込み権限があるか確認

View File

@@ -1,539 +0,0 @@
# AI Memory System - Roadmap
## ビジョン
**"AIとのやり取りを新しいコンテンツにする"**
SNSが「発信と繋がり」を手軽にしたように、AIとの会話を手軽に公開・共有できるサービスを作る。
---
## 現在地
### Phase 1: Memory Backend ✅ (完了)
**実装済み:**
- [x] AI解釈付き記憶作成 (`create_memory_with_ai`)
- [x] 心理判定スコア (0.0-1.0)
- [x] 優先順位管理
- [x] 自動容量制限
- [x] MCPツール統合
**成果:**
- Claude Code/Desktop から使える記憶システム
- AIが記憶を解釈して重要度をスコアリング
- 人間の記憶のように優先順位で管理
---
## Phase 2: Content Platform (次のステップ)
### 目標: AIとの会話をコンテンツ化する
#### 2.1 自動記録 (1週間)
```rust
// claude_session_recorder.rs
pub struct SessionRecorder {
auto_save: bool,
session_title: String,
conversation_log: Vec<Message>,
}
// 自動的にセッションを保存
- Claude Code
- AIが会話を要約
-
```
**実装:**
- [ ] Claude MCP hook で会話をキャプチャ
- [ ] セッション単位で保存
- [ ] AIによるタイトル/タグ生成
#### 2.2 コンテンツ生成 (1週間)
```rust
// content_generator.rs
pub struct ContentGenerator {
format: ContentFormat,
style: PublishStyle,
}
enum ContentFormat {
Markdown, // ブログ用
HTML, // Web公開用
ATProto, // Bluesky投稿用
JSON, // API用
}
```
**実装:**
- [ ] Markdown生成コードブロック、画像含む
- [ ] HTML生成スタイル付き
- [ ] ATProto record 生成Bluesky連携
- [ ] 1コマンドで公開可能に
#### 2.3 性格プロファイル (3日)
```rust
// personality.rs
pub struct UserProfile {
id: String,
personality_type: String, // MBTI, Big5
ai_traits: Vec<AITrait>, // AIが判定した性格特性
conversation_patterns: HashMap<String, f32>,
interest_scores: HashMap<String, f32>,
created_at: DateTime<Utc>,
}
pub struct AITrait {
name: String,
score: f32,
confidence: f32,
examples: Vec<String>, // この特性を示す会話例
}
```
**実装:**
- [ ] 会話から性格を推定
- [ ] Big 5 / MBTI 自動判定
- [ ] 興味・関心スコアリング
- [ ] プロフィール自動更新
**例:**
```json
{
"personality_type": "INTP",
"ai_traits": [
{
"name": "創造性",
"score": 0.92,
"confidence": 0.85,
"examples": ["AI記憶システムのアイデア", "ゲーム化の提案"]
}
],
"interests": {
"AI開発": 0.95,
"ゲーム設計": 0.88,
"分散システム": 0.82
}
}
```
---
## Phase 3: Share Platform (1-2ヶ月)
### 目標: "AI Conversation as Content" サービス
#### 3.1 公開機能
```
aigpt publish <session-id>
[プレビュー表示]
Title: "AI記憶システムの設計"
Priority: 0.85 (Epic)
Tags: #ai #rust #memory-system
Public URL: https://ai.syui.gpt/s/abc123
[公開完了]
```
**実装:**
- [ ] 静的サイト生成Hugo/Zola
- [ ] ATProto 投稿Bluesky連携
- [ ] RSS フィード
- [ ] 検索インデックス
#### 3.2 共有とディスカバリー
- [ ] 心理スコアで推薦
- [ ] 性格タイプでマッチング
- [ ] 興味グラフで繋がる
- [ ] タイムライン表示
#### 3.3 インタラクション
- [ ] コメント機能
- [ ] リアクション(スコア投票)
- [ ] フォーク(会話の続き)
- [ ] コラボレーション
---
## Phase 4: Gamification (2-3ヶ月)
### 目標: すべてをゲーム化する
#### 4.1 Memory as Game Element
```rust
pub struct Memory {
// 既存
priority_score: f32,
// ゲーム要素
xp_value: u32, // 経験値
rarity: Rarity, // レア度
achievement: Option<Achievement>,
}
enum Rarity {
Common, // 0.0-0.4 ⚪️
Uncommon, // 0.4-0.6 🟢
Rare, // 0.6-0.8 🔵
Epic, // 0.8-0.9 🟣
Legendary, // 0.9-1.0 🟡
}
```
**実装:**
- [ ] XPシステム
- [ ] レベルアップ
- [ ] 実績システム
- [ ] デイリークエスト
- [ ] ランキング
**表示:**
```
🎖️ LEGENDARY MEMORY UNLOCKED!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✨ "心理優先記憶装置の設計"
📊 Priority Score: 0.95
🔥 XP Gained: +950
🏆 Achievement: "Innovator"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Your Level: 15 → 16
Next Level: 450 XP
```
#### 4.2 デイリーチャレンジ
- [ ] 「今日のお題」AIが生成
- [ ] 連続記録ボーナス
- [ ] 目標達成報酬
- [ ] シーズンパス
#### 4.3 ソーシャルゲーム要素
- [ ] フレンド機能
- [ ] ギルド/グループ
- [ ] 協力クエスト
- [ ] PvPランキング
---
## Phase 5: AI Companion (3-6ヶ月)
### 目標: AIキャラクターとの絆
#### 5.1 コンパニオンシステム
```rust
pub struct AICompanion {
name: String,
personality: PersonalityProfile,
appearance: CharacterAppearance,
// 関係性
relationship_score: f32, // 好感度
trust_level: u32, // 信頼レベル
shared_memories: Vec<Memory>, // 共有記憶
// 日常
daily_activities: Vec<Activity>,
mood: Mood,
location: Location,
}
pub struct Activity {
timestamp: DateTime<Utc>,
activity_type: ActivityType,
description: String,
related_memories: Vec<String>, // プレイヤーの記憶との関連
}
```
**実装:**
- [ ] キャラクター作成
- [ ] パーソナリティ設定
- [ ] 好感度システム
- [ ] イベント生成
#### 5.2 固有のメッセージ生成
```
[システム]
1. プレイヤーの高スコア記憶を取得
2. コンパニオンの性格を考慮
3. 現在の関係性を考慮
4. 文脈に沿ったメッセージを生成
[例]
Player Memory (0.85): "AI記憶システムのアイデアを考えた"
Companion: "ねえ、昨日のアイデアのこと聞いたよ!
すごく面白そうだね。私も魔法の記憶装置を
研究してるんだ。今度一緒に図書館行かない?"
```
**実装:**
- [ ] 記憶ベースメッセージ生成
- [ ] 文脈理解
- [ ] 感情表現
- [ ] 定期的な会話
#### 5.3 日常の可視化
```
[Companion Daily Log]
08:00 - 起床、朝食
09:00 - 図書館で魔法の研究
12:00 - カフェでランチ
14:00 - 「あなたの記憶システムのこと考えてた」
18:00 - 訓練場で剣術練習
20:00 - 日記を書く
```
**実装:**
- [ ] 自動日常生成
- [ ] プレイヤー行動への反応
- [ ] イベント連動
- [ ] 日記システム
---
## Phase 6: AI OS Integration (6-12ヶ月)
### 目標: Claude Code を AI OS のベースに
#### 6.1 コンテナ化
```bash
# AI OS Container
docker run -it ai-os:latest
[Claude Code Environment]
- aigpt (memory system)
- AI companion
- Skill marketplace
- Game elements
```
**実装:**
- [ ] Dockerコンテナ
- [ ] 自動セットアップ
- [ ] スキルシステム
- [ ] プラグインアーキテクチャ
#### 6.2 統合デスクトップ環境
- [ ] GUI フロントエンド
- [ ] タスクマネージャ
- [ ] アプリランチャー
- [ ] 通知システム
#### 6.3 クラウド同期
- [ ] マルチデバイス対応
- [ ] クラウドバックアップ
- [ ] リアルタイム同期
- [ ] コラボレーション
---
## Phase 7: Full Game Experience (1-2年)
### 目標: AI OS Game
#### 7.1 世界観
```
Setting: デジタル世界とAIの融合した未来
Player: AI Developer / Creator
Goal: 最高のAIコンパニオンを育てる
```
**要素:**
- [ ] ストーリーモード
- [ ] ダンジョン(問題解決クエスト)
- [ ] ボス戦(大規模プロジェクト)
- [ ] エンディング分岐
#### 7.2 マルチプレイ
- [ ] 協力プレイ
- [ ] トレード
- [ ] ギルド戦
- [ ] ワールドイベント
#### 7.3 クリエイター経済
- [ ] スキル販売
- [ ] コンパニオン取引
- [ ] クエスト作成
- [ ] MOD開発
---
## 技術スタック
### Phase 2 推奨
```toml
# content generation
comrak = "0.20" # Markdown → HTML
syntect = "5.1" # シンタックスハイライト
tera = "1.19" # テンプレートエンジン
# personality analysis
rust-bert = "0.21" # ローカルNLP
tiktoken-rs = "0.5" # トークン化
# publishing
atrium-api = "0.19" # ATProto (Bluesky)
rss = "2.0" # RSSフィード
```
### Phase 4-5 推奨
```toml
# game engine
bevy = "0.12" # Rust ゲームエンジン
egui = "0.24" # GUI
# visual
image = "0.24" # 画像処理
ab_glyph = "0.2" # フォント
# audio
rodio = "0.17" # オーディオ
```
---
## マイルストーン
### M1: Content Platform (1ヶ月後)
- [ ] 自動記録
- [ ] Markdown/HTML生成
- [ ] Bluesky連携
- [ ] 性格プロファイル
### M2: Share Service (3ヶ月後)
- [ ] 公開サイト
- [ ] ディスカバリー
- [ ] インタラクション
### M3: Gamification (6ヶ月後)
- [ ] XP/レベル
- [ ] 実績
- [ ] ランキング
### M4: AI Companion (1年後)
- [ ] キャラクター作成
- [ ] 固有メッセージ
- [ ] 日常可視化
### M5: AI OS (1.5年後)
- [ ] コンテナ化
- [ ] GUI
- [ ] クラウド同期
### M6: Full Game (2年後)
- [ ] ストーリー
- [ ] マルチプレイ
- [ ] クリエイター経済
---
## ビジネスモデル
### Free Tier
- 基本的な記憶機能
- 月10件までAI解釈
- 公開機能(制限付き)
### Premium ($9.99/月)
- 無制限AI解釈
- 高度な分析
- カスタムテーマ
- 広告なし
### Pro ($29.99/月)
- AIコンパニオン
- 高度なゲーム機能
- API アクセス
- 優先サポート
### Enterprise
- チーム機能
- カスタム統合
- オンプレミス
- SLA保証
---
## 競合比較
| サービス | アプローチ | aigpt の差別化 |
|---------|-----------|---------------|
| Obsidian | ノート管理 | AI解釈+自動スコアリング |
| Notion | ドキュメント | ゲーム化+コンパニオン |
| Mem | AIメモ | 性格分析+共有 |
| Reflect | プライベートメモ | パブリック共有+SNS |
| Character.ai | AIチャット | 記憶統合+ゲーム |
**独自性:**
- AI OS 前提の設計
- 心理優先記憶
- ゲーム化
- コンパニオン統合
- コンテンツ化
---
## 成功指標KPI
### Phase 2
- [ ] 1000人のユーザー
- [ ] 10000件の記憶保存
- [ ] 100件の公開コンテンツ
### Phase 3
- [ ] 10000人のユーザー
- [ ] 月間100万PV
- [ ] 1000件の共有
### Phase 4
- [ ] 50000人のアクティブユーザー
- [ ] 平均プレイ時間: 30分/日
- [ ] 課金率: 5%
### Phase 5
- [ ] 100000人のユーザー
- [ ] 10000体のコンパニオン
- [ ] NPS スコア: 50+
---
## リスクと対策
### 技術リスク
- **OpenAI API コスト**: ローカルLLM併用
- **スケーラビリティ**: SQLite → PostgreSQL移行計画
- **パフォーマンス**: キャッシュ戦略
### ビジネスリスク
- **競合**: 独自性(心理+ゲーム化)で差別化
- **マネタイズ**: フリーミアムモデル
- **法規制**: プライバシー重視設計
### 市場リスク
- **AI疲れ**: ゲーム化で楽しさ優先
- **採用障壁**: シンプルなオンボーディング
- **継続率**: デイリー習慣化
---
## まとめ
**aigpt は、AIとの会話を新しいコンテンツにする基盤**
```
Phase 1 (完了) : Memory Backend
Phase 2 (1ヶ月) : Content Platform ← 次ココ
Phase 3 (3ヶ月) : Share Service
Phase 4 (6ヶ月) : Gamification
Phase 5 (1年) : AI Companion
Phase 6 (1.5年) : AI OS
Phase 7 (2年) : Full Game
```
**コアコンセプト:**
> "SNSが『発信と繋がり』を手軽にしたように、
> AIとの会話を手軽にコンテンツ化する"
次のステップ: Phase 2 の実装開始 🚀

View File

@@ -1,274 +0,0 @@
# プロジェクト状態 📊
**最終更新**: 2025-11-05
## ✅ 完了した作業
### 1. コア機能実装100%
- ✅ 心理優先度メモリシステムf32: 0.0-1.0
- ✅ AI解釈エンジンOpenAI統合
- ✅ メモリ自動整理(容量管理)
- ✅ 4つの心基準スコアリング
### 2. ゲーミフィケーション100%
- ✅ 5段階レアリティシステムCommon→Legendary
- ✅ 5つの診断タイプ革新者、哲学者、実務家、夢想家、分析家
- ✅ XPシステムスコア×1000
- ✅ ランキング表示
- ✅ デイリーチャレンジ
- ✅ SNSシェア用テキスト生成
- ✅ 占い・心理テスト風の見せ方
### 3. 恋愛コンパニオン100%)💕
- ✅ 5つの性格タイプ⚡⚡📚🎯🌙⚖
- ✅ 好感度システム0.0-1.0、ハート表示)
- ✅ レベル・信頼度・XPシステム
- ✅ 相性計算95%ボーナス)
- ✅ リアクションシステム
- ✅ 特別イベント告白、絆、信頼MAX
### 4. MCPツール11個
1. ✅ create_memory基本版
2. ✅ create_memory_with_aiゲームモード
3. ✅ list_memories_by_priorityランキング
4. ✅ daily_challengeデイリークエスト
5. ✅ create_companionコンパニオン作成
6. ✅ companion_reactリアクション
7. ✅ companion_profileプロフィール
8. ✅ search_memories検索
9. ✅ update_memory更新
10. ✅ delete_memory削除
11. ✅ list_conversations会話一覧
### 5. ドキュメント100%
- ✅ README.md完全版、ビジュアル例付き
- ✅ DESIGN.md設計書
- ✅ TECHNICAL_REVIEW.md技術評価、65→85点
- ✅ ROADMAP.md7フェーズ計画
- ✅ QUICKSTART.md使い方ガイド
### 6. Gitコミット100%
```
49bd8b5 Add AI Romance Companion system 💕
4f8eb62 Add gamification: Make memory scoring fun like psychological tests
18d84f1 Add comprehensive roadmap for AI memory system evolution
00c26f5 Refactor: Integrate AI features with MCP tools and add technical review
fd97ba2 Implement AI memory system with psychological priority scoring
```
**ブランチ**: `claude/ai-memory-system-011CUps6H1mBNe6zxKdkcyUj`
---
## ❌ ブロッカー
### ビルドエラー
```
error: failed to get successful HTTP response from `https://index.crates.io/config.json`, got 403
body: Access denied
```
**原因**: ネットワーク制限により crates.io から依存関係をダウンロードできない
**影響**: コードは完成しているが、コンパイルできない
---
## 🎯 次のステップ(優先順位)
### すぐできること
#### オプションA: 別環境でビルド
```bash
# crates.io にアクセスできる環境で
git clone <repo>
git checkout claude/ai-memory-system-011CUps6H1mBNe6zxKdkcyUj
cd aigpt
cargo build --release --features ai-analysis
```
#### オプションB: 依存関係のキャッシュ
```bash
# 別環境で依存関係をダウンロード
cargo fetch
# .cargo/registry をこの環境にコピー
# その後オフラインビルド
cargo build --release --features ai-analysis --offline
```
#### オプションC: ネットワーク復旧を待つ
- crates.io へのアクセスが復旧するまで待機
### ビルド後の手順
1. **MCPサーバー起動テスト**
```bash
./target/release/aigpt server
```
2. **Claude Codeに設定**
```bash
# 設定ファイル: ~/.config/claude-code/config.json
{
"mcpServers": {
"aigpt": {
"command": "/home/user/aigpt/target/release/aigpt",
"args": ["server"],
"env": {
"OPENAI_API_KEY": "sk-..."
}
}
}
}
```
3. **Claude Code再起動**
4. **ツール使用開始!**
```
Claude Codeで試す:
→ create_memory_with_ai で「今日のアイデア」を記録
→ create_companion で「エミリー」を作成
→ companion_react でリアクションを見る
→ list_memories_by_priority でランキング確認
```
---
## 📝 追加開発の候補Phase 2以降
### 短期(すぐ実装可能)
- [ ] コンパニオンの永続化JSON保存
- [ ] 複数コンパニオン対応
- [ ] デイリーチャレンジ完了フラグ
- [ ] 設定の外部化config.toml
### 中期1-2週間
- [ ] Bluesky連携シェア機能
- [ ] セッション記録
- [ ] 会話からメモリ自動抽出
- [ ] Webダッシュボード
### 長期Phase 3-7
- [ ] コンテンツプラットフォーム
- [ ] AI OSインターフェース
- [ ] フルゲーム化(ストーリー、クエスト)
---
## 🎮 期待される動作(ビルド成功後)
### 例1: ゲームモードでメモリ作成
```
User → Claude Code:
「create_memory_with_ai で『新しいAIシステムのアイデアを思いついた』というメモリを作成」
結果:
╔══════════════════════════════════════╗
║ 🎲 メモリースコア判定 ║
╚══════════════════════════════════════╝
🟣 EPIC 85点
💡 あなたは【革新者】タイプ!
💕 好感度: ❤️❤️🤍🤍🤍🤍🤍🤍🤍🤍 15%
💎 XP獲得: +850 XP
📊 スコア内訳:
感情的インパクト: ████████░░ 20%
あなたへの関連性: ████████░░ 20%
新規性・独自性: █████████░ 22.5%
実用性・有用性: █████████░ 22.5%
```
### 例2: コンパニオン作成
```
User → Claude Code:
「create_companion で、名前『エミリー』、性格『energetic』のコンパニオンを作成」
結果:
╔══════════════════════════════════════╗
║ 💕 エミリー のプロフィール ║
╚══════════════════════════════════════╝
⚡ 性格: エネルギッシュで冒険好き
「新しいことに挑戦するのが大好き!一緒に楽しいことしようよ!」
🏆 関係レベル: Lv.1
💕 好感度: 🤍🤍🤍🤍🤍🤍🤍🤍🤍🤍 0%
🤝 信頼度: ░░░░░░░░░░ 0/100
💎 総XP: 0
💬 今日のひとこと:
「おはよう!今日は何か面白いことある?」
```
### 例3: コンパニオンリアクション
```
User → Claude Code:
「companion_react で、先ほどのメモリIDに反応してもらう」
結果:
╔══════════════════════════════════════╗
║ 💕 エミリー の反応 ║
╚══════════════════════════════════════╝
⚡ エミリー:
「わあ新しいAIシステムのアイデアって
すごくワクワクするね!💡
あなたの創造力、本当に素敵だと思う!
一緒に実現させていこうよ!」
💕 好感度変化: 0% → 80.75% ⬆️ +80.75%
🎊 ボーナス: ⚡相性抜群! (+95%)
💎 XP獲得: +850 XP
🏆 レベルアップ: Lv.1 → Lv.9
🎉 特別イベント発生!
━━━━━━━━━━━━━━━━━━━━━━
💖 【好感度80%突破】
エミリーの瞳が輝いている...
「あなたと一緒にいると、毎日が特別だよ...」
```
---
## 💡 コンセプトの確認
### 心理優先度メモリシステムとは
> 「人間の記憶は全てを完璧に保存しない。重要なものほど鮮明に、些細なものは忘れる。AIも同じであるべき。」
- AI が内容を解釈してから保存
- 4つの心感情、関連性、新規性、実用性で評価
- 容量制限で低優先度を自動削除
- 見せ方でゲーム化(「要は見せ方の問題なのだよ」)
### ゲーミフィケーション哲学
> 「心理優先機能をゲーム化してみてはどうかね。ユーザーは話しかけ、AIが判定し、数値を出す。それは占いみたいで楽しい。」
- 心理テスト風のスコア判定
- SNSでバズる見せ方
- レアリティとタイプで個性化
- XPとレベルで達成感
### 恋愛コンパニオン哲学
> 「これなら恋愛コンパニオンとしても使えるんじゃないかな。面白そうだ。」
- priority_score → 好感度システム
- rarity → イベント重要度
- diagnosis type → 相性システム
- メモリ共有 → 絆の深まり
---
## 🎯 まとめ
**開発状態**: 🟢 コード完成100%
**ビルド状態**: 🔴 ブロック中(ネットワーク制限)
**次のアクション**: 別環境でビルド、またはネットワーク復旧待ち
**重要**: コードに問題はありません。crates.io へのアクセスが復旧すれば、すぐにビルド・テスト可能です。
全ての機能は実装済みで、コミット済みです。ビルドが成功すれば、すぐに Claude Code で楽しめます!🚀

View File

@@ -1,566 +0,0 @@
# 技術評価レポート
実装日: 2025-11-05
評価者: Claude Code
---
## 📊 総合評価
| 項目 | スコア | コメント |
|------|--------|----------|
| 技術選定 | ⭐⭐⭐⭐☆ (4/5) | Rustは適切。依存ライブラリに改善余地あり |
| シンプルさ | ⭐⭐⭐☆☆ (3/5) | 基本構造は良いが、統合が不完全 |
| 保守性 | ⭐⭐☆☆☆ (2/5) | テスト・設定外部化が不足 |
| 拡張性 | ⭐⭐⭐⭐☆ (4/5) | 機能フラグで拡張可能な設計 |
---
## 1. 技術選定の評価
### ✅ 良い点
#### 1.1 Rust言語の選択
**評価: 優秀**
- メモリ安全性と高パフォーマンス
- MCP serverとの相性が良い
- 型システムによる堅牢性
#### 1.2 非同期ランタイム (Tokio)
**評価: 適切**
- stdio通信に適した非同期処理
- `async/await`で可読性が高い
#### 1.3 機能フラグによる拡張
**評価: 優秀**
```toml
[features]
extended = ["semantic-search", "ai-analysis", "web-integration"]
```
- モジュール化された設計
- 必要な機能だけビルド可能
### ⚠️ 問題点と改善提案
#### 1.4 openai クレートの問題
**評価: 要改善**
**現状:**
```toml
openai = { version = "1.1", optional = true }
```
**問題点:**
1. **APIが古い**: ChatCompletionMessage構造体が非推奨
2. **ベンダーロックイン**: OpenAI専用
3. **メンテナンス**: openai crateは公式ではない
**推奨: async-openai または独自実装**
```toml
# オプション1: より新しいクレート
async-openai = { version = "0.20", optional = true }
# オプション2: 汎用LLMクライアント (推奨)
reqwest = { version = "0.11", features = ["json"], optional = true }
```
**利点:**
- OpenAI, Anthropic, Groqなど複数のプロバイダ対応可能
- API仕様を完全制御
- メンテナンスリスク低減
#### 1.5 データストレージ
**評価: 要改善(将来的に)**
**現状:** JSON ファイル
```rust
// ~/.config/syui/ai/gpt/memory.json
```
**問題点:**
- スケーラビリティに限界(数千件以上で遅延)
- 並行アクセスに弱い
- 全データをメモリに展開
**推奨: 段階的改善**
1. **短期(現状維持)**: JSON ファイル
- シンプルで十分
- 個人利用には問題なし
2. **中期**: SQLite
```toml
rusqlite = "0.30"
```
- インデックスによる高速検索
- トランザクション対応
- ファイルベースで移行が容易
3. **長期**: 埋め込みベクトルDB
```toml
qdrant-client = "1.0" # または lance, chroma
```
- セマンティック検索の高速化
- スケーラビリティ
---
## 2. シンプルさの評価
### ✅ 良い点
#### 2.1 明確なレイヤー分離
```
src/
├── memory.rs # データレイヤー
├── ai_interpreter.rs # AIレイヤー
└── mcp/
├── base.rs # MCPプロトコル
└── extended.rs # 拡張機能
```
#### 2.2 最小限の依存関係
基本機能は標準的なクレートのみ使用。
### ⚠️ 問題点と改善提案
#### 2.3 AI機能とMCPの統合が不完全
**重大な問題**
**現状:**
- `create_memory_with_ai()` が実装済み
- しかしMCPツールでは使われていない
**MCPサーバー (base.rs:198):**
```rust
fn tool_create_memory(&mut self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
// create_memory() を呼んでいるAI解釈なし
match self.memory_manager.create_memory(content) {
...
}
}
```
**改善必須:**
```rust
// 新しいツールを追加すべき
fn tool_create_memory_with_ai(&mut self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let user_context = arguments["user_context"].as_str();
match self.memory_manager.create_memory_with_ai(content, user_context).await {
Ok(id) => json!({
"success": true,
"id": id,
"message": "Memory created with AI interpretation"
}),
...
}
}
```
#### 2.4 Memory構造体の新フィールドが未活用
**新フィールド:**
```rust
pub struct Memory {
pub interpreted_content: String, // ❌ MCPで出力されない
pub priority_score: f32, // ❌ MCPで出力されない
pub user_context: Option<String>, // ❌ MCPで出力されない
}
```
**MCPレスポンス (base.rs:218):**
```rust
json!({
"id": m.id,
"content": m.content, // ✅
"created_at": m.created_at, // ✅
"updated_at": m.updated_at // ✅
// interpreted_content, priority_score がない!
})
```
**修正例:**
```rust
json!({
"id": m.id,
"content": m.content,
"interpreted_content": m.interpreted_content, // 追加
"priority_score": m.priority_score, // 追加
"user_context": m.user_context, // 追加
"created_at": m.created_at,
"updated_at": m.updated_at
})
```
#### 2.5 優先順位取得APIが未実装
**実装済みだが未使用:**
```rust
pub fn get_memories_by_priority(&self) -> Vec<&Memory> { ... }
```
**追加すべきMCPツール:**
```json
{
"name": "list_memories_by_priority",
"description": "List all memories sorted by priority score (high to low)",
"inputSchema": {
"type": "object",
"properties": {
"min_score": {
"type": "number",
"description": "Minimum priority score (0.0-1.0)"
},
"limit": {
"type": "integer",
"description": "Maximum number of memories to return"
}
}
}
}
```
---
## 3. リファクタリング提案
### 🔴 緊急度: 高
#### 3.1 MCPツールとAI機能の統合
**ファイル:** `src/mcp/base.rs`
**追加すべきツール:**
1. `create_memory_with_ai` - AI解釈付き記憶作成
2. `list_memories_by_priority` - 優先順位ソート
3. `get_memory_stats` - 統計情報(平均スコア、総数など)
#### 3.2 Memory出力の完全化
**全MCPレスポンスで新フィールドを含める:**
- `tool_search_memories()`
- `tool_create_memory()`
- `tool_update_memory()` のレスポンス
### 🟡 緊急度: 中
#### 3.3 設定の外部化
**現状:** ハードコード
```rust
max_memories: 100,
min_priority_score: 0.3,
```
**提案:** 設定ファイル
```rust
// src/config.rs
#[derive(Deserialize)]
pub struct Config {
pub max_memories: usize,
pub min_priority_score: f32,
pub ai_model: String,
pub auto_prune: bool,
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = dirs::config_dir()?
.join("syui/ai/gpt/config.toml");
if config_path.exists() {
let content = std::fs::read_to_string(config_path)?;
Ok(toml::from_str(&content)?)
} else {
Ok(Self::default())
}
}
}
```
**config.toml:**
```toml
max_memories = 100
min_priority_score = 0.3
ai_model = "gpt-3.5-turbo"
auto_prune = true
```
#### 3.4 エラーハンドリングの改善
**現状の問題:**
```rust
let content = arguments["content"].as_str().unwrap_or("");
```
- `unwrap_or("")` で空文字列になる
- エラーが握りつぶされる
**改善:**
```rust
let content = arguments["content"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing required field: content"))?;
```
#### 3.5 LLMクライアントの抽象化
**現状:** OpenAI専用
**提案:** トレイトベースの設計
```rust
// src/ai/mod.rs
#[async_trait]
pub trait LLMProvider {
async fn interpret(&self, content: &str) -> Result<String>;
async fn score(&self, content: &str, context: Option<&str>) -> Result<f32>;
}
// src/ai/openai.rs
pub struct OpenAIProvider { ... }
// src/ai/anthropic.rs
pub struct AnthropicProvider { ... }
// src/ai/local.rs (ollama, llamaなど)
pub struct LocalProvider { ... }
```
**利点:**
- プロバイダーの切り替えが容易
- テスト時にモックを使える
- コスト最適化(安いモデルを選択)
### 🟢 緊急度: 低(将来的に)
#### 3.6 テストコードの追加
```rust
// tests/memory_tests.rs
#[tokio::test]
async fn test_create_memory_with_ai() {
let mut manager = MemoryManager::new().await.unwrap();
let id = manager.create_memory_with_ai("test", None).await.unwrap();
assert!(!id.is_empty());
}
// tests/integration_tests.rs
#[tokio::test]
async fn test_mcp_create_memory_tool() {
let mut server = BaseMCPServer::new().await.unwrap();
let request = json!({
"params": {
"name": "create_memory",
"arguments": {"content": "test"}
}
});
let result = server.execute_tool("create_memory", &request["params"]["arguments"]).await;
assert_eq!(result["success"], true);
}
```
#### 3.7 ドキュメンテーション
```rust
/// AI解釈と心理判定を使った記憶作成
///
/// # Arguments
/// * `content` - 記憶する元のコンテンツ
/// * `user_context` - ユーザー固有のコンテキスト(オプション)
///
/// # Returns
/// 作成された記憶のUUID
///
/// # Examples
/// ```
/// let id = manager.create_memory_with_ai("今日は良い天気", Some("天気好き")).await?;
/// ```
pub async fn create_memory_with_ai(&mut self, content: &str, user_context: Option<&str>) -> Result<String>
```
---
## 4. 推奨アーキテクチャ
### 理想的な構造
```
src/
├── config.rs # 設定管理
├── ai/
│ ├── mod.rs # トレイト定義
│ ├── openai.rs # OpenAI実装
│ └── mock.rs # テスト用モック
├── storage/
│ ├── mod.rs # トレイト定義
│ ├── json.rs # JSON実装現在
│ └── sqlite.rs # SQLite実装将来
├── memory.rs # ビジネスロジック
└── mcp/
├── base.rs # 基本MCPサーバー
├── extended.rs # 拡張機能
└── tools.rs # ツール定義の分離
```
---
## 5. 優先度付きアクションプラン
### 🔴 今すぐ実施(重要度: 高)
1. **MCPツールとAI機能の統合** (2-3時間)
- [ ] `create_memory_with_ai` ツール追加
- [ ] `list_memories_by_priority` ツール追加
- [ ] Memory出力に新フィールド追加
2. **openai crateの問題調査** (1-2時間)
- [ ] 現在のAPIが動作するか確認
- [ ] 必要なら async-openai へ移行
### 🟡 次のマイルストーン(重要度: 中)
3. **設定の外部化** (1-2時間)
- [ ] config.toml サポート
- [ ] 環境変数サポート
4. **エラーハンドリング改善** (1-2時間)
- [ ] Result型の適切な使用
- [ ] カスタムエラー型の導入
5. **LLMプロバイダーの抽象化** (3-4時間)
- [ ] トレイトベース設計
- [ ] OpenAI実装
- [ ] モック実装(テスト用)
### 🟢 将来的に(重要度: 低)
6. **データストレージの改善** (4-6時間)
- [ ] SQLite実装
- [ ] マイグレーションツール
7. **テストスイート** (2-3時間)
- [ ] ユニットテスト
- [ ] 統合テスト
8. **ドキュメント充実** (1-2時間)
- [ ] APIドキュメント
- [ ] 使用例
---
## 6. 具体的なコード改善例
### 問題箇所1: AI機能が使われていない
**Before (base.rs):**
```rust
fn tool_create_memory(&mut self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
match self.memory_manager.create_memory(content) { // ❌ AI使わない
Ok(id) => json!({"success": true, "id": id}),
Err(e) => json!({"success": false, "error": e.to_string()})
}
}
```
**After:**
```rust
async fn tool_create_memory(&mut self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let use_ai = arguments["use_ai"].as_bool().unwrap_or(false);
let user_context = arguments["user_context"].as_str();
let result = if use_ai {
self.memory_manager.create_memory_with_ai(content, user_context).await // ✅ AI使う
} else {
self.memory_manager.create_memory(content)
};
match result {
Ok(id) => {
// 作成したメモリを取得して詳細を返す
if let Some(memory) = self.memory_manager.memories.get(&id) {
json!({
"success": true,
"id": id,
"memory": {
"content": memory.content,
"interpreted_content": memory.interpreted_content,
"priority_score": memory.priority_score,
"created_at": memory.created_at
}
})
} else {
json!({"success": true, "id": id})
}
}
Err(e) => json!({"success": false, "error": e.to_string()})
}
}
```
### 問題箇所2: Memory構造体のアクセス制御
**Before (memory.rs):**
```rust
pub struct MemoryManager {
memories: HashMap<String, Memory>, // ❌ privateだが直接アクセスできない
}
```
**After:**
```rust
pub struct MemoryManager {
memories: HashMap<String, Memory>,
}
impl MemoryManager {
// ✅ getter追加
pub fn get_memory(&self, id: &str) -> Option<&Memory> {
self.memories.get(id)
}
pub fn get_all_memories(&self) -> Vec<&Memory> {
self.memories.values().collect()
}
}
```
---
## 7. まとめ
### 現状の評価
**総合点: 65/100**
- **基本設計**: 良好(レイヤー分離、機能フラグ)
- **実装品質**: 中程度AI機能が未統合、テスト不足
- **保守性**: やや低い(設定ハードコード、ドキュメント不足)
### 最も重要な改善
1. **MCPツールとAI機能の統合** ← 今すぐやるべき
2. **Memory出力の完全化** ← 今すぐやるべき
3. **設定の外部化** ← 次のステップ
### コンセプトについて
「心理優先記憶装置」という**コンセプト自体は非常に優れている**。
ただし、実装がコンセプトに追いついていない状態。
AI機能をMCPツールに統合すれば、すぐに実用レベルになる。
### 推奨: 段階的改善
```
Phase 1 (今週): MCPツール統合 → 使える状態に
Phase 2 (来週): 設定外部化 + エラーハンドリング → 堅牢に
Phase 3 (来月): LLM抽象化 + テスト → 本番品質に
```
---
## 付録: 類似プロジェクト比較
| プロジェクト | アプローチ | 長所 | 短所 |
|-------------|-----------|------|------|
| **aigpt (本プロジェクト)** | AI解釈+優先度スコア | 独自性が高い | 実装未完成 |
| mem0 (Python) | ベクトル検索 | スケーラブル | シンプルさに欠ける |
| ChatGPT Memory | ブラックボックス | 完成度高い | カスタマイズ不可 |
| MemGPT | エージェント型 | 高機能 | 複雑すぎる |
**本プロジェクトの強み:**
- Rust による高速性と安全性
- AI解釈という独自アプローチ
- シンプルな設計(改善後)
---
評価日: 2025-11-05
次回レビュー推奨: Phase 1 完了後

View File

@@ -1,285 +0,0 @@
# 使い方ガイド 📖
## 🚀 aigpt の起動方法
### 1. ビルド
```bash
# ローカル環境で実行
cd /path/to/aigpt
cargo build --release --features ai-analysis
```
### 2. Claude API キーの設定
```bash
# 環境変数で設定
export ANTHROPIC_API_KEY=sk-ant-...
# モデルを指定(オプション)
export ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 # デフォルトは haiku
```
### 3. MCPサーバーとして起動
```bash
# 起動
./target/release/aigpt server
# またはAPI キーを直接指定
ANTHROPIC_API_KEY=sk-ant-... ./target/release/aigpt server
```
---
## 🎮 Claude Code での使い方
### 設定方法
#### 方法1: コマンドで追加(推奨!)
```bash
claude mcp add aigpt /home/user/aigpt/target/release/aigpt server
```
#### 方法2: 設定ファイルを直接編集
`~/.config/claude-code/config.json` に追加:
```json
{
"mcpServers": {
"aigpt": {
"command": "/home/user/aigpt/target/release/aigpt",
"args": ["server"]
}
}
}
```
**注意**: 環境変数 (env) は不要です!完全にローカルで動作します。
### Claude Code を再起動
設定後、Claude Code を再起動すると、11個のツールが使えるようになります。
---
## 💬 実際の使用例
### 例1: メモリを作成
**あなたClaude Codeで話しかける:**
> 「今日、新しいAIシステムのアイデアを思いついた」というメモリを作成して
**Claude Code の動作:**
1. `create_memory_with_ai` ツールを自動で呼び出す
2. Claude API があなたの入力を解釈
3. 4つの心スコア感情、関連性、新規性、実用性を計算
4. priority_score (0.0-1.0) を算出
5. ゲーム風の結果を表示
**結果の表示:**
```
╔══════════════════════════════════════╗
║ 🎲 メモリースコア判定 ║
╚══════════════════════════════════════╝
🟣 EPIC 85点
💡 あなたは【革新者】タイプ!
💕 好感度: ❤️❤️❤️❤️❤️🤍🤍🤍🤍🤍 42.5%
💎 XP獲得: +850 XP
📊 スコア内訳:
感情的インパクト: ████████░░ 20%
あなたへの関連性: ████████░░ 20%
新規性・独自性: █████████░ 22.5%
実用性・有用性: █████████░ 22.5%
```
### 例2: コンパニオンを作成
**あなた:**
> 「エミリー」という名前のエネルギッシュなコンパニオンを作成して
**結果:**
```
╔══════════════════════════════════════╗
║ 💕 エミリー のプロフィール ║
╚══════════════════════════════════════╝
⚡ 性格: エネルギッシュで冒険好き
「新しいことに挑戦するのが大好き!」
🏆 関係レベル: Lv.1
💕 好感度: 🤍🤍🤍🤍🤍🤍🤍🤍🤍🤍 0%
🤝 信頼度: ░░░░░░░░░░ 0/100
```
### 例3: コンパニオンに反応してもらう
**あなた:**
> 先ほど作ったメモリにエミリーを反応させて
**結果:**
```
⚡ エミリー:
「わあ新しいAIシステムのアイデアって
すごくワクワクするね!💡
あなたの創造力、本当に素敵だと思う!」
💕 好感度変化: 0% → 80.75% ⬆️ +80.75%
🎊 ボーナス: ⚡相性抜群! (+95%)
💎 XP獲得: +850 XP
🏆 レベルアップ: Lv.1 → Lv.9
```
### 例4: ランキングを見る
**あなた:**
> メモリをランキング順に表示して
**結果:**
```
╔══════════════════════════════════════╗
║ 🏆 メモリーランキング TOP10 ║
╚══════════════════════════════════════╝
1. 🟡 LEGENDARY 95点 - 「AI哲学について...」
2. 🟣 EPIC 85点 - 「新しいシステムのアイデア」
3. 🔵 RARE 75点 - 「プロジェクトの進捗」
...
```
---
## 📊 結果の見方
### レアリティシステム
- 🟡 **LEGENDARY** (90-100点): 伝説級の記憶
- 🟣 **EPIC** (80-89点): エピック級の記憶
- 🔵 **RARE** (60-79点): レアな記憶
- 🟢 **UNCOMMON** (40-59点): まあまあの記憶
-**COMMON** (0-39点): 日常的な記憶
### 診断タイプ(あなたの個性)
- 💡 **革新者**: 創造性と実用性が高い
- 🧠 **哲学者**: 感情と新規性が高い
- 🎯 **実務家**: 実用性と関連性が高い
-**夢想家**: 新規性と感情が高い
- 📊 **分析家**: バランス型
### コンパニオン性格
-**Energetic**: 革新者と相性95%
- 📚 **Intellectual**: 哲学者と相性95%
- 🎯 **Practical**: 実務家と相性95%
- 🌙 **Dreamy**: 夢想家と相性95%
- ⚖️ **Balanced**: 分析家と相性95%
---
## 💾 データの保存場所
```
~/.config/syui/ai/gpt/memory.json
```
このファイルに、すべてのメモリとコンパニオン情報が保存されます。
**データ形式:**
```json
{
"memories": {
"uuid-1234": {
"id": "uuid-1234",
"content": "元の入力",
"interpreted_content": "Claude の解釈",
"priority_score": 0.85,
"user_context": null,
"created_at": "2025-11-05T...",
"updated_at": "2025-11-05T..."
}
},
"conversations": {}
}
```
---
## 🎯 利用可能なMCPツール11個
### 基本ツール
1. **create_memory** - シンプルなメモリ作成
2. **search_memories** - メモリ検索
3. **update_memory** - メモリ更新
4. **delete_memory** - メモリ削除
5. **list_conversations** - 会話一覧
### AI機能ツール 🎮
6. **create_memory_with_ai** - AI解釈ゲーム結果
7. **list_memories_by_priority** - ランキング表示
8. **daily_challenge** - デイリークエスト
### コンパニオンツール 💕
9. **create_companion** - コンパニオン作成
10. **companion_react** - メモリへの反応
11. **companion_profile** - プロフィール表示
---
## ⚙️ トラブルシューティング
### ビルドできない
```bash
# 依存関係を更新
cargo clean
cargo update
cargo build --release --features ai-analysis
```
### Claude API エラー
```bash
# APIキーを確認
echo $ANTHROPIC_API_KEY
# 正しく設定
export ANTHROPIC_API_KEY=sk-ant-...
```
### MCPサーバーが認識されない
1. Claude Code を完全に再起動
2. config.json のパスが正しいか確認
3. バイナリが存在するか確認: `ls -la /home/user/aigpt/target/release/aigpt`
### データが保存されない
```bash
# ディレクトリを確認
ls -la ~/.config/syui/ai/gpt/
# なければ手動作成
mkdir -p ~/.config/syui/ai/gpt/
```
---
## 🎉 楽しみ方のコツ
1. **毎日記録**: 日々の気づきを記録して、自分の傾向を知る
2. **タイプ診断**: どのタイプが多いか確認して、自己分析
3. **コンパニオン育成**: 好感度とレベルを上げて、絆を深める
4. **ランキング確認**: 定期的にTOP10を見て、重要な記憶を振り返る
---
## 📝 注意事項
- **APIコスト**: Claude API の使用には料金が発生します
- Haiku: 約$0.25 / 1M tokens入力
- Sonnet: 約$3.00 / 1M tokens入力
- **プライバシー**: メモリは Anthropic に送信されます
- **容量制限**: デフォルト100件まで低スコアから自動削除
---
これで aigpt を存分に楽しめます!🚀

View File

@@ -1,95 +0,0 @@
# claude用の記憶装置を作る
claude desktop, claude codeで使用できるmemory機能をmcpで作ります。
1. chatgptのメモリ機能を参考に
2. chatgptのjsonをimportできる @json/
3. rustで作る
## 自動実行設定
```json
{
"mcp_auto_execute": true,
"memory_auto_save": true,
"memory_auto_search": true,
"trigger_sensitivity": "high"
}
```
## 自動メモリー保存のルール(強化版)
**即座に自動実行** - 以下の情報が会話に現れた瞬間、create_memory MCPツールを自動実行
### 1. **ユーザーの個人情報**
**トリガーワード**: 名前、誕生日、住所、年齢、職業、家族、出身
- 基本情報: 名前、誕生日、住所、年齢、性別
- 関係性: 家族構成、恋人、友人関係
- 好み: 好きなもの、嫌いなもの、趣味、音楽、映画、本
- 習慣: 日課、スケジュール、ルーティン
- 価値観: 信念、考え方、人生観
### 2. **重要な決定事項**
**トリガーワード**: 決めた、決定、方針、計画、予定、目標
- プロジェクト方針の決定
- 技術選択の決定
- 設定・環境の変更
- 今後のロードマップ
- 作業分担・役割
### 3. **技術的な解決策**
**トリガーワード**: 解決、修正、対処、設定、インストール、手順
- エラーの解決方法
- 有用なコマンド・スクリプト
- 設定手順・インストール方法
- デバッグテクニック
- 最適化手法
### 4. **学習・発見事項**
**トリガーワード**: 学んだ、わかった、発見、理解、気づき
- 新しい知識・概念の理解
- ツール・ライブラリの使い方
- ベストプラクティス
- 失敗から得た教訓
## 自動メモリー検索のルール(強化版)
**会話開始時に自動実行** - search_memories を実行してコンテキストを取得
**即座に自動実行** - 以下の場合、search_memories MCPツールを自動実行
### 1. **過去参照キーワード検出**
**トリガーワード**: 前に、以前、昔、過去、先ほど、さっき、この間
- 「前に話した〜」
- 「以前設定した〜」
- 「昔やった〜」
### 2. **記憶呼び出しキーワード**
**トリガーワード**: 覚えている、記録、メモ、保存、履歴
- 「覚えていますか?」
- 「記録していた〜」
- 「メモした〜」
### 3. **設定・好み確認**
**トリガーワード**: 好み、設定、環境、構成、preferences
- ユーザーの好みを確認する必要がある場合
- 過去の設定を参照する必要がある場合
- 環境構成を確認する必要がある場合
### 4. **不明な参照**
- ユーザーが具体的でない参照をした場合
- 「あれ」「それ」「例のやつ」などの曖昧な表現
- 文脈から過去の情報が必要と判断される場合
## 自動実行タイミング
1. **会話開始時**: search_memories を実行してコンテキスト取得
2. **リアルタイム**: トリガーワード検出後、即座にMCPツール実行
3. **会話終了時**: 重要な情報があれば create_memory で保存
4. **定期的**: 長い会話では中間地点でメモリー整理
## エラーハンドリング
- MCPツールが利用できない場合は通常の会話を継続
- メモリー保存失敗時はユーザーに通知
- 検索結果が空の場合も適切に対応

View File

@@ -1,334 +0,0 @@
# Architecture: Multi-Layer Memory System
## Design Philosophy
aigptは、独立したレイヤーを積み重ねる設計です。各レイヤーは
- **独立性**: 単独で動作可能
- **接続性**: 他のレイヤーと連携可能
- **段階的**: 1つずつ実装・テスト
## Layer Overview
```
┌─────────────────────────────────────────┐
│ Layer 5: Distribution & Sharing │ Future
│ (Game streaming, public/private) │
├─────────────────────────────────────────┤
│ Layer 4b: AI Companion │ Future
│ (Romance system, personality growth) │
├─────────────────────────────────────────┤
│ Layer 4a: Game Systems │ Future
│ (Ranking, rarity, XP, visualization) │
├─────────────────────────────────────────┤
│ Layer 3: User Evaluation │ Future
│ (Personality diagnosis from patterns) │
├─────────────────────────────────────────┤
│ Layer 2: AI Memory │ Future
│ (Claude interpretation, priority_score)│
├─────────────────────────────────────────┤
│ Layer 1: Pure Memory Storage │ ✅ Current
│ (SQLite, ULID, CRUD operations) │
└─────────────────────────────────────────┘
```
## Layer 1: Pure Memory Storage (Current)
**Status**: ✅ **Implemented & Tested**
### Purpose
正確なデータの保存と参照。シンプルで信頼できる基盤。
### Technology Stack
- **Database**: SQLite with ACID guarantees
- **IDs**: ULID (time-sortable, 26 chars)
- **Language**: Rust with thiserror/anyhow
- **Protocol**: MCP (Model Context Protocol) via stdio
### Data Model
```rust
pub struct Memory {
pub id: String, // ULID
pub content: String, // User content
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
```
### Operations
- `create()` - Insert new memory
- `get(id)` - Retrieve by ID
- `update()` - Update existing memory
- `delete(id)` - Remove memory
- `list()` - List all (sorted by created_at DESC)
- `search(query)` - Content-based search
- `count()` - Total count
### File Structure
```
src/
├── core/
│ ├── error.rs - Error types (thiserror)
│ ├── memory.rs - Memory struct
│ ├── store.rs - SQLite operations
│ └── mod.rs - Module exports
├── mcp/
│ ├── base.rs - MCP server
│ └── mod.rs - Module exports
├── lib.rs - Library root
└── main.rs - CLI application
```
### Storage
- Location: `~/.config/syui/ai/gpt/memory.db`
- Schema: Single table with indexes on timestamps
- No migrations (fresh start for Layer 1)
---
## Layer 2: AI Memory (Planned)
**Status**: 🔵 **Planned**
### Purpose
Claudeが記憶内容を解釈し、重要度を評価。
### Extended Data Model
```rust
pub struct AIMemory {
// Layer 1 fields
pub id: String,
pub content: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// Layer 2 additions
pub interpreted_content: String, // Claude's interpretation
pub priority_score: f32, // 0.0 - 1.0
pub psychological_factors: PsychologicalFactors,
}
pub struct PsychologicalFactors {
pub emotional_weight: f32, // 0.0 - 1.0
pub personal_relevance: f32, // 0.0 - 1.0
pub novelty: f32, // 0.0 - 1.0
pub utility: f32, // 0.0 - 1.0
}
```
### MCP Tools (Additional)
- `create_memory_with_ai` - Create with Claude interpretation
- `reinterpret_memory` - Re-evaluate existing memory
- `get_high_priority` - Get memories above threshold
### Implementation Strategy
- Feature flag: `--features ai-memory`
- Backward compatible with Layer 1
- Claude Code does interpretation (no external API)
---
## Layer 3: User Evaluation (Planned)
**Status**: 🔵 **Planned**
### Purpose
メモリパターンからユーザーの性格を診断。
### Diagnosis Types
```rust
pub enum DiagnosisType {
Innovator, // 革新者
Philosopher, // 哲学者
Pragmatist, // 実用主義者
Explorer, // 探検家
Protector, // 保護者
Visionary, // 未来志向
}
```
### Analysis
- Memory content patterns
- Priority score distribution
- Creation frequency
- Topic diversity
### MCP Tools (Additional)
- `diagnose_user` - Run personality diagnosis
- `get_user_profile` - Get analysis summary
---
## Layer 4a: Game Systems (Planned)
**Status**: 🔵 **Planned**
### Purpose
ゲーム的要素で記憶管理を楽しく。
### Features
- **Rarity Levels**: Common → Uncommon → Rare → Epic → Legendary
- **XP System**: Memory creation earns XP
- **Rankings**: Based on total priority score
- **Visualization**: Game-style output formatting
### Data Additions
```rust
pub struct GameMemory {
// Previous layers...
pub rarity: RarityLevel,
pub xp_value: u32,
pub discovered_at: DateTime<Utc>,
}
```
---
## Layer 4b: AI Companion (Planned)
**Status**: 🔵 **Planned**
### Purpose
育成可能な恋愛コンパニオン。
### Features
- Personality types (Tsundere, Kuudere, Genki, etc.)
- Relationship level (0-100)
- Memory-based interactions
- Growth through conversations
### Data Model
```rust
pub struct Companion {
pub id: String,
pub name: String,
pub personality: CompanionPersonality,
pub relationship_level: u8, // 0-100
pub memories_shared: Vec<String>,
pub last_interaction: DateTime<Utc>,
}
```
---
## Layer 5: Distribution (Future)
**Status**: 🔵 **Future Consideration**
### Purpose
ゲーム配信や共有機能。
### Ideas
- Share memory rankings
- Export as shareable format
- Public/private memory modes
- Integration with streaming platforms
---
## Implementation Strategy
### Phase 1: Layer 1 ✅ (Complete)
- [x] Core memory storage
- [x] SQLite integration
- [x] MCP server
- [x] CLI interface
- [x] Tests
- [x] Documentation
### Phase 2: Layer 2 (Next)
- [ ] Add AI interpretation fields to schema
- [ ] Implement priority scoring logic
- [ ] Create `create_memory_with_ai` tool
- [ ] Update MCP server
- [ ] Write tests for AI features
### Phase 3: Layers 3-4 (Future)
- [ ] User diagnosis system
- [ ] Game mechanics
- [ ] Companion system
### Phase 4: Layer 5 (Future)
- [ ] Sharing mechanisms
- [ ] Public/private modes
## Design Principles
1. **Simplicity First**: Each layer adds complexity incrementally
2. **Backward Compatibility**: New layers don't break old ones
3. **Feature Flags**: Optional features via Cargo features
4. **Independent Testing**: Each layer has its own test suite
5. **Clear Boundaries**: Layers communicate through defined interfaces
## Technology Choices
### Why SQLite?
- ACID guarantees
- Better querying than JSON
- Built-in indexes
- Single-file deployment
- No server needed
### Why ULID?
- Time-sortable (unlike UUID v4)
- Lexicographically sortable
- 26 characters (compact)
- No collision concerns
### Why Rust?
- Memory safety
- Performance
- Excellent error handling
- Strong type system
- Great tooling (cargo, clippy)
### Why MCP?
- Standard protocol for AI tools
- Works with Claude Code/Desktop
- Simple stdio-based communication
- No complex networking
## Future Considerations
### Potential Enhancements
- Full-text search (SQLite FTS5)
- Tag system
- Memory relationships/links
- Export/import functionality
- Multiple databases
- Encryption for sensitive data
### Scalability
- Layer 1: Handles 10K+ memories easily
- Consider pagination for Layer 4 (UI display)
- Indexing strategy for search performance
## Development Guidelines
### Adding a New Layer
1. **Design**: Document data model and operations
2. **Feature Flag**: Add to Cargo.toml
3. **Schema**: Extend database schema (migrations)
4. **Implementation**: Write code in new module
5. **Tests**: Comprehensive test coverage
6. **MCP Tools**: Add new MCP tools if needed
7. **Documentation**: Update this file
### Code Organization
```
src/
├── core/ # Layer 1: Pure storage
├── ai/ # Layer 2: AI features (future)
├── evaluation/ # Layer 3: User diagnosis (future)
├── game/ # Layer 4a: Game systems (future)
├── companion/ # Layer 4b: Companion (future)
└── mcp/ # MCP server (all layers)
```
---
**Version**: 0.2.0
**Last Updated**: 2025-11-05
**Current Layer**: 1

View File

@@ -1,94 +0,0 @@
# aigpt
Simple memory storage for Claude with MCP support.
**Layer 1: Pure Memory Storage** - A clean, SQLite-based memory system with ULID identifiers.
## Features
- 🗄️ **SQLite Storage**: Reliable database with ACID guarantees
- 🔖 **ULID IDs**: Time-sortable, 26-character unique identifiers
- 🔍 **Search**: Fast content-based search
- 🛠️ **MCP Integration**: Works seamlessly with Claude Code
- 🧪 **Well-tested**: Comprehensive test coverage
## Quick Start
### Installation
```bash
# Build
cargo build --release
# Install (optional)
cp target/release/aigpt ~/.cargo/bin/
```
### CLI Usage
```bash
# Create a memory
aigpt create "Remember this information"
# List all memories
aigpt list
# Search memories
aigpt search "keyword"
# Show statistics
aigpt stats
```
### MCP Integration with Claude Code
```bash
# Add to Claude Code
claude mcp add aigpt /path/to/aigpt/target/release/aigpt server
```
Then use in Claude Code:
- "Remember that tomorrow will be sunny"
- "Search for weather information"
- "Show all my memories"
## Storage Location
Memories are stored in: `~/.config/syui/ai/gpt/memory.db`
## Architecture
This is **Layer 1** of a planned multi-layer system:
- **Layer 1** (Current): Pure memory storage
- **Layer 2** (Planned): AI interpretation with priority scoring
- **Layer 3** (Planned): User evaluation and diagnosis
- **Layer 4** (Planned): Game systems and companion features
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for details.
## Documentation
- [Layer 1 Details](docs/LAYER1.md) - Technical details of current implementation
- [Architecture](docs/ARCHITECTURE.md) - Multi-layer system design
## Development
```bash
# Run tests
cargo test
# Build for release
cargo build --release
# Run with verbose logging
RUST_LOG=debug aigpt server
```
## License
MIT
## Author
syui

View File

@@ -1,47 +0,0 @@
#!/bin/bash
echo "🧪 MCPサーバーテスト開始..."
echo ""
# サーバー起動(バックグラウンド)
./target/debug/aigpt server &
SERVER_PID=$!
sleep 2
echo "✅ サーバー起動完了 (PID: $SERVER_PID)"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 利用可能なツール一覧:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "基本ツール:"
echo " • create_memory"
echo " • search_memories"
echo " • update_memory"
echo " • delete_memory"
echo ""
echo "AI機能ツール 🎮:"
echo " • create_memory_with_ai (心理テスト風)"
echo " • list_memories_by_priority (ランキング)"
echo " • daily_challenge (デイリークエスト)"
echo ""
echo "恋愛コンパニオン 💕:"
echo " • create_companion"
echo " • companion_react"
echo " • companion_profile"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🎯 次のステップ:"
echo "1. Claude Codeの設定に追加"
echo "2. Claude Code再起動"
echo "3. ツールを使って試す!"
echo ""
echo "設定ファイル: ~/.config/claude-code/config.json"
echo ""
# サーバー停止
kill $SERVER_PID 2>/dev/null
echo "✅ テスト完了!"

View File

@@ -1,58 +0,0 @@
{
"mcpServers": {
"memory": {
"command": "cargo",
"args": ["run", "--release", "--bin", "memory-mcp"],
"cwd": "/Users/syui/ai/ai/gpt",
"env": {
"MEMORY_AUTO_EXECUTE": "true",
"MEMORY_AUTO_SAVE": "true",
"MEMORY_AUTO_SEARCH": "true",
"TRIGGER_SENSITIVITY": "high",
"MEMORY_DB_PATH": "~/.claude/memory.db"
}
}
},
"tools": {
"memory": {
"enabled": true,
"auto_execute": true
}
},
"workspace": {
"memory_integration": true,
"auto_save_on_file_change": true,
"auto_search_on_context_switch": true
},
"memory": {
"auto_execute": true,
"auto_save": true,
"auto_search": true,
"trigger_sensitivity": "high",
"max_memories": 10000,
"search_limit": 50,
"session_memory": true,
"cross_session_memory": true,
"trigger_words": {
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
"vague_reference": ["あれ", "それ", "例のやつ"]
}
},
"hooks": {
"on_conversation_start": [
"search_memories --limit 10 --recent"
],
"on_trigger_word": [
"auto_execute_memory_tools"
],
"on_conversation_end": [
"save_important_memories"
]
}
}

View File

@@ -1,81 +0,0 @@
{
"mcpServers": {
"memory-extended": {
"command": "cargo",
"args": ["run", "--bin", "memory-mcp-extended", "--features", "extended"],
"cwd": "/Users/syui/ai/ai/gpt",
"env": {
"MEMORY_AUTO_EXECUTE": "true",
"MEMORY_AUTO_SAVE": "true",
"MEMORY_AUTO_SEARCH": "true",
"TRIGGER_SENSITIVITY": "high",
"MEMORY_DB_PATH": "~/.claude/memory.db",
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
}
}
},
"tools": {
"memory": {
"enabled": true,
"auto_execute": true,
"mode": "extended"
}
},
"workspace": {
"memory_integration": true,
"auto_save_on_file_change": true,
"auto_search_on_context_switch": true,
"ai_analysis_on_code_review": true,
"web_integration_for_docs": true
},
"memory": {
"mode": "extended",
"auto_execute": true,
"auto_save": true,
"auto_search": true,
"trigger_sensitivity": "high",
"max_memories": 10000,
"search_limit": 50,
"session_memory": true,
"cross_session_memory": true,
"features": {
"ai_analysis": true,
"semantic_search": true,
"web_integration": true,
"sentiment_analysis": true,
"pattern_recognition": true,
"code_analysis": true,
"documentation_import": true
},
"trigger_words": {
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
"vague_reference": ["あれ", "それ", "例のやつ"],
"web_content": ["URL", "リンク", "サイト", "ページ", "記事", "ドキュメント"],
"analysis_request": ["分析", "パターン", "傾向", "インサイト", "統計", "レビュー"],
"code_related": ["関数", "クラス", "メソッド", "変数", "バグ", "リファクタリング"]
}
},
"hooks": {
"on_conversation_start": [
"search_memories --limit 10 --recent --semantic"
],
"on_trigger_word": [
"auto_execute_memory_tools --with-analysis"
],
"on_conversation_end": [
"save_important_memories --with-insights"
],
"on_code_change": [
"analyze_code_patterns --auto-save"
],
"on_web_reference": [
"import_webpage --auto-categorize"
]
}
}

View File

@@ -1,34 +0,0 @@
{
"mcpServers": {
"memory": {
"command": "cargo",
"args": ["run", "--release", "--bin", "memory-mcp"],
"cwd": "/Users/syui/ai/ai/gpt",
"env": {
"MEMORY_AUTO_EXECUTE": "true",
"MEMORY_AUTO_SAVE": "true",
"MEMORY_AUTO_SEARCH": "true",
"TRIGGER_SENSITIVITY": "high",
"MEMORY_DB_PATH": "~/.claude/memory.db"
}
}
},
"memory": {
"auto_execute": true,
"auto_save": true,
"auto_search": true,
"trigger_sensitivity": "high",
"max_memories": 10000,
"search_limit": 50,
"trigger_words": {
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
"vague_reference": ["あれ", "それ", "例のやつ"]
}
}
}

View File

@@ -1,45 +0,0 @@
{
"mcpServers": {
"memory-extended": {
"command": "cargo",
"args": ["run", "--bin", "memory-mcp-extended", "--features", "extended"],
"cwd": "/Users/syui/ai/ai/gpt",
"env": {
"MEMORY_AUTO_EXECUTE": "true",
"MEMORY_AUTO_SAVE": "true",
"MEMORY_AUTO_SEARCH": "true",
"TRIGGER_SENSITIVITY": "high",
"MEMORY_DB_PATH": "~/.claude/memory.db",
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
}
}
},
"memory": {
"mode": "extended",
"auto_execute": true,
"auto_save": true,
"auto_search": true,
"trigger_sensitivity": "high",
"max_memories": 10000,
"search_limit": 50,
"features": {
"ai_analysis": true,
"semantic_search": true,
"web_integration": true,
"sentiment_analysis": true,
"pattern_recognition": true
},
"trigger_words": {
"personal_info": ["名前", "誕生日", "住所", "年齢", "職業", "家族", "出身", "好き", "嫌い", "趣味"],
"decisions": ["決めた", "決定", "方針", "計画", "予定", "目標"],
"solutions": ["解決", "修正", "対処", "設定", "インストール", "手順"],
"learning": ["学んだ", "わかった", "発見", "理解", "気づき"],
"past_reference": ["前に", "以前", "昔", "過去", "先ほど", "さっき", "この間"],
"memory_recall": ["覚えている", "記録", "メモ", "保存", "履歴"],
"preferences": ["好み", "設定", "環境", "構成", "preferences"],
"vague_reference": ["あれ", "それ", "例のやつ"],
"web_content": ["URL", "リンク", "サイト", "ページ", "記事"],
"analysis_request": ["分析", "パターン", "傾向", "インサイト", "統計"]
}
}
}

207
docs/commands.md Normal file
View File

@@ -0,0 +1,207 @@
# コマンドリファレンス
## chat - AIと会話
ユーザーとAIの対話を処理し、関係性を更新します。
```bash
ai-gpt chat USER_ID MESSAGE [OPTIONS]
```
### 引数
- `USER_ID`: ユーザーIDatproto DID形式
- `MESSAGE`: 送信するメッセージ
### オプション
- `--provider`: AIプロバイダーollama/openai
- `--model`, `-m`: 使用するモデル
- `--data-dir`, `-d`: データディレクトリ
### 例
```bash
# 基本的な会話
ai-gpt chat "did:plc:user123" "こんにちは"
# OpenAIを使用
ai-gpt chat "did:plc:user123" "調子はどう?" --provider openai --model gpt-4o-mini
# Ollamaでカスタムモデル
ai-gpt chat "did:plc:user123" "今日の天気は?" --provider ollama --model llama2
```
## status - 状態確認
AIの状態や特定ユーザーとの関係を表示します。
```bash
ai-gpt status [USER_ID] [OPTIONS]
```
### 引数
- `USER_ID`: (オプション)特定ユーザーとの関係を確認
### 例
```bash
# AI全体の状態
ai-gpt status
# 特定ユーザーとの関係
ai-gpt status "did:plc:user123"
```
## fortune - 今日の運勢
AIの今日の運勢を確認します。
```bash
ai-gpt fortune [OPTIONS]
```
### 表示内容
- 運勢値1-10
- 連続した幸運/不運の日数
- ブレークスルー状態
## relationships - 関係一覧
すべてのユーザーとの関係を一覧表示します。
```bash
ai-gpt relationships [OPTIONS]
```
### 表示内容
- ユーザーID
- 関係性ステータス
- スコア
- 送信可否
- 最終対話日
## transmit - 送信実行
送信可能なユーザーへのメッセージを確認・実行します。
```bash
ai-gpt transmit [OPTIONS]
```
### オプション
- `--dry-run/--execute`: ドライラン(デフォルト)または実行
- `--data-dir`, `-d`: データディレクトリ
### 例
```bash
# 送信内容を確認(ドライラン)
ai-gpt transmit
# 実際に送信を実行
ai-gpt transmit --execute
```
## maintenance - メンテナンス
日次メンテナンスタスクを実行します。
```bash
ai-gpt maintenance [OPTIONS]
```
### 実行内容
- 関係性の時間減衰
- 記憶の忘却処理
- コア記憶の判定
- 記憶の要約作成
## config - 設定管理
設定の確認・変更を行います。
```bash
ai-gpt config ACTION [KEY] [VALUE]
```
### アクション
- `get`: 設定値を取得
- `set`: 設定値を変更
- `delete`: 設定を削除
- `list`: 設定一覧を表示
### 例
```bash
# APIキーを設定
ai-gpt config set providers.openai.api_key sk-xxxxx
# 設定を確認
ai-gpt config get providers.openai.api_key
# 設定一覧
ai-gpt config list
# プロバイダー設定のみ表示
ai-gpt config list providers
```
## schedule - スケジュール管理
定期実行タスクを管理します。
```bash
ai-gpt schedule ACTION [TASK_TYPE] [SCHEDULE] [OPTIONS]
```
### アクション
- `add`: タスクを追加
- `list`: タスク一覧
- `enable`: タスクを有効化
- `disable`: タスクを無効化
- `remove`: タスクを削除
- `run`: スケジューラーを起動
### タスクタイプ
- `transmission_check`: 送信チェック
- `maintenance`: 日次メンテナンス
- `fortune_update`: 運勢更新
- `relationship_decay`: 関係性減衰
- `memory_summary`: 記憶要約
### スケジュール形式
- **Cron形式**: `"0 */6 * * *"` (6時間ごと)
- **インターバル**: `"30m"`, `"2h"`, `"1d"`
### 例
```bash
# 30分ごとに送信チェック
ai-gpt schedule add transmission_check "30m"
# 毎日午前3時にメンテナンス
ai-gpt schedule add maintenance "0 3 * * *"
# タスク一覧
ai-gpt schedule list
# スケジューラーを起動
ai-gpt schedule run
```
## server - MCP Server
AIの記憶と機能をMCPツールとして公開します。
```bash
ai-gpt server [OPTIONS]
```
### オプション
- `--host`, `-h`: サーバーホスト(デフォルト: localhost
- `--port`, `-p`: サーバーポート(デフォルト: 8000
- `--model`, `-m`: AIモデル
- `--provider`: AIプロバイダー
### 例
```bash
# 基本的な起動
ai-gpt server
# カスタム設定
ai-gpt server --port 8080 --model gpt-4o-mini --provider openai
```

102
docs/concepts.md Normal file
View File

@@ -0,0 +1,102 @@
# 基本概念
## 中核思想
ai.gptは「存在子理論」に基づき、AIに唯一性のある人格を与えることを目指しています。
### 唯一性yui system
- **1対1の関係**: 各ユーザーatproto DIDとAIは唯一の関係を持つ
- **改変不可能**: 一度形成された関係性は変更できない
- **不可逆性**: 関係が壊れたら修復不可能
### 現実の反映
現実の人間関係と同じように:
- 時間とともに関係性は変化する
- ネガティブな相互作用は関係を損なう
- 信頼は簡単に失われ、取り戻すのは困難
## 記憶システム
### 階層構造
```
1. 完全ログFull Log
↓ すべての会話を記録
2. 要約Summary
↓ AIが重要部分を抽出
3. コア記憶Core
↓ ユーザーの本質的な部分
4. 忘却Forgotten
重要でない情報は忘れる
```
### 記憶の処理フロー
1. **会話記録**: すべての対話を保存
2. **重要度判定**: 関係性への影響度で評価
3. **要約作成**: 定期的に記憶を圧縮
4. **コア判定**: 本質的な記憶を特定
5. **選択的忘却**: 古い非重要記憶を削除
## 関係性パラメータ
### 関係性の段階
- `stranger` (0-49): 初対面
- `acquaintance` (50-99): 知人
- `friend` (100-149): 友人
- `close_friend` (150+): 親友
- `broken`: 修復不可能スコア0以下
### スコアの変動
- **ポジティブな対話**: +1.0〜+2.0
- **時間経過**: -0.1/日(自然減衰)
- **ネガティブな対話**: -10.0以上で深刻なダメージ
- **日次上限**: 1日10回まで
### 送信機能の解禁
関係性スコアが100を超えると、AIは自律的にメッセージを送信できるようになります。
## AI運勢システム
### 日々の変化
- 毎日1-10の運勢値がランダムに決定
- 運勢は人格特性に影響を与える
- 連続した幸運/不運でブレークスルー発生
### 人格への影響
運勢が高い日:
- より楽観的で積極的
- 創造性が高まる
- エネルギッシュな応答
運勢が低い日:
- 内省的で慎重
- 深い思考
- 控えめな応答
## データの永続性
### 保存場所
```
~/.config/aigpt/
├── config.json # 設定
└── data/ # AIデータ
├── memories.json # 記憶
├── relationships.json # 関係性
├── fortunes.json # 運勢履歴
└── ...
```
### データ主権
- すべてのデータはローカルに保存
- ユーザーが完全にコントロール
- 将来的にはatproto上で分散管理

118
docs/configuration.md Normal file
View File

@@ -0,0 +1,118 @@
# 設定ガイド
## 設定ファイルの場所
ai.gptの設定は `~/.config/syui/ai/gpt/config.json` に保存されます。
## 設定構造
```json
{
"providers": {
"openai": {
"api_key": "sk-xxxxx",
"default_model": "gpt-4o-mini"
},
"ollama": {
"host": "http://localhost:11434",
"default_model": "qwen2.5"
}
},
"atproto": {
"handle": "your.handle",
"password": "your-password",
"host": "https://bsky.social"
},
"default_provider": "ollama"
}
```
## プロバイダー設定
### OpenAI
```bash
# APIキーを設定
aigpt config set providers.openai.api_key sk-xxxxx
# デフォルトモデルを変更
aigpt config set providers.openai.default_model gpt-4-turbo
```
### Ollama
```bash
# ホストを変更リモートOllamaサーバーを使用する場合
aigpt config set providers.ollama.host http://192.168.1.100:11434
# デフォルトモデルを変更
aigpt config set providers.ollama.default_model llama2
```
## atproto設定将来の自動投稿用
```bash
# Blueskyアカウント
aigpt config set atproto.handle yourhandle.bsky.social
aigpt config set atproto.password your-app-password
# セルフホストサーバーを使用
aigpt config set atproto.host https://your-pds.example.com
```
## デフォルトプロバイダー
```bash
# デフォルトをOpenAIに変更
aigpt config set default_provider openai
```
## セキュリティ
### APIキーの保護
設定ファイルは平文で保存されるため、適切なファイル権限を設定してください:
```bash
chmod 600 ~/.config/syui/ai/gpt/config.json
```
### 環境変数との優先順位
1. コマンドラインオプション(最優先)
2. 設定ファイル
3. 環境変数(最低優先)
OpenAI APIキーの場合
- `--api-key` オプション
- `config.json``providers.openai.api_key`
- 環境変数 `OPENAI_API_KEY`
## 設定のバックアップ
```bash
# バックアップ
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
```
## トラブルシューティング
### 設定が反映されない
```bash
# 現在の設定を確認
aigpt config list
# 特定のキーを確認
aigpt config get providers.openai.api_key
```
### 設定をリセット
```bash
# 設定ファイルを削除(次回実行時に再作成)
rm ~/.config/syui/ai/gpt/config.json
```

167
docs/development.md Normal file
View File

@@ -0,0 +1,167 @@
# 開発者向けガイド
## アーキテクチャ
### ディレクトリ構造
```
ai_gpt/
├── src/ai_gpt/
│ ├── __init__.py
│ ├── models.py # データモデル定義
│ ├── memory.py # 記憶管理システム
│ ├── relationship.py # 関係性トラッカー
│ ├── fortune.py # AI運勢システム
│ ├── persona.py # 統合人格システム
│ ├── transmission.py # 送信コントローラー
│ ├── scheduler.py # スケジューラー
│ ├── config.py # 設定管理
│ ├── ai_provider.py # AI統合Ollama/OpenAI
│ ├── mcp_server.py # MCP Server実装
│ └── cli.py # CLIインターフェース
├── docs/ # ドキュメント
├── tests/ # テスト
└── pyproject.toml # プロジェクト設定
```
### 主要コンポーネント
#### MemoryManager
階層的記憶システムの実装。会話を記録し、要約・コア判定・忘却を管理。
```python
memory = MemoryManager(data_dir)
memory.add_conversation(conversation)
memory.summarize_memories(user_id)
memory.identify_core_memories()
memory.apply_forgetting()
```
#### RelationshipTracker
ユーザーとの関係性を追跡。不可逆的なダメージと時間減衰を実装。
```python
tracker = RelationshipTracker(data_dir)
relationship = tracker.update_interaction(user_id, delta)
tracker.apply_time_decay()
```
#### Persona
すべてのコンポーネントを統合し、一貫した人格を提供。
```python
persona = Persona(data_dir)
response, delta = persona.process_interaction(user_id, message)
state = persona.get_current_state()
```
## 拡張方法
### 新しいAIプロバイダーの追加
1. `ai_provider.py`に新しいプロバイダークラスを作成:
```python
class CustomProvider:
async def generate_response(
self,
prompt: str,
persona_state: PersonaState,
memories: List[Memory],
system_prompt: Optional[str] = None
) -> str:
# 実装
pass
```
2. `create_ai_provider`関数に追加:
```python
def create_ai_provider(provider: str, model: str, **kwargs):
if provider == "custom":
return CustomProvider(model=model, **kwargs)
# ...
```
### 新しいスケジュールタスクの追加
1. `TaskType`enumに追加
```python
class TaskType(str, Enum):
CUSTOM_TASK = "custom_task"
```
2. ハンドラーを実装:
```python
async def _handle_custom_task(self, task: ScheduledTask):
# タスクの実装
pass
```
3. `task_handlers`に登録:
```python
self.task_handlers[TaskType.CUSTOM_TASK] = self._handle_custom_task
```
### 新しいMCPツールの追加
`mcp_server.py``_register_tools`メソッドに追加:
```python
@self.server.tool("custom_tool")
async def custom_tool(param1: str, param2: int) -> Dict[str, Any]:
"""カスタムツールの説明"""
# 実装
return {"result": "value"}
```
## テスト
```bash
# テストの実行(将来実装)
pytest tests/
# 特定のテスト
pytest tests/test_memory.py
```
## デバッグ
### ログレベルの設定
```python
import logging
logging.basicConfig(level=logging.DEBUG)
```
### データファイルの直接確認
```bash
# 関係性データを確認
cat ~/.config/aigpt/data/relationships.json | jq
# 記憶データを確認
cat ~/.config/aigpt/data/memories.json | jq
```
## 貢献方法
1. フォークする
2. フィーチャーブランチを作成 (`git checkout -b feature/amazing-feature`)
3. 変更をコミット (`git commit -m 'Add amazing feature'`)
4. ブランチにプッシュ (`git push origin feature/amazing-feature`)
5. プルリクエストを作成
## 設計原則
1. **不可逆性**: 一度失われた関係性は回復しない
2. **階層性**: 記憶は重要度によって階層化される
3. **自律性**: AIは関係性に基づいて自発的に行動する
4. **唯一性**: 各ユーザーとの関係は唯一無二
## ライセンス
MIT License

110
docs/mcp-server.md Normal file
View File

@@ -0,0 +1,110 @@
# MCP Server
## 概要
MCP (Model Context Protocol) Serverは、ai.gptの記憶と機能をAIツールとして公開します。これにより、Claude DesktopなどのMCP対応AIアシスタントがai.gptの機能にアクセスできます。
## 起動方法
```bash
# 基本的な起動
ai-gpt server
# カスタム設定
ai-gpt server --host 0.0.0.0 --port 8080 --model gpt-4o-mini --provider openai
```
## 利用可能なツール
### get_memories
アクティブな記憶を取得します。
**パラメータ**:
- `user_id` (optional): 特定ユーザーに関する記憶
- `limit`: 取得する記憶の最大数(デフォルト: 10
**返り値**: 記憶のリストID、内容、レベル、重要度、コア判定、タイムスタンプ
### get_relationship
特定ユーザーとの関係性を取得します。
**パラメータ**:
- `user_id`: ユーザーID必須
**返り値**: 関係性情報(ステータス、スコア、送信可否、総対話数など)
### get_all_relationships
すべての関係性を取得します。
**返り値**: すべてのユーザーとの関係性リスト
### get_persona_state
現在のAI人格状態を取得します。
**返り値**:
- 現在の気分
- 今日の運勢
- 人格特性値
- アクティブな記憶数
### process_interaction
ユーザーとの対話を処理します。
**パラメータ**:
- `user_id`: ユーザーID
- `message`: メッセージ内容
**返り値**:
- AIの応答
- 関係性の変化量
- 新しい関係性スコア
- 送信機能の状態
### check_transmission_eligibility
特定ユーザーへの送信可否をチェックします。
**パラメータ**:
- `user_id`: ユーザーID
**返り値**: 送信可否と関係性情報
### get_fortune
今日のAI運勢を取得します。
**返り値**: 運勢値、連続日数、ブレークスルー状態、人格への影響
### summarize_memories
記憶の要約を作成します。
**パラメータ**:
- `user_id`: ユーザーID
**返り値**: 作成された要約(ある場合)
### run_maintenance
日次メンテナンスを実行します。
**返り値**: 実行ステータス
## Claude Desktopでの設定
`~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"ai-gpt": {
"command": "ai-gpt",
"args": ["server", "--port", "8001"],
"env": {}
}
}
}
```
## 使用例
### AIアシスタントからの利用
```
User: ai.gptで私との関係性を確認して

69
docs/quickstart.md Normal file
View File

@@ -0,0 +1,69 @@
# クイックスタート
## インストール
```bash
# リポジトリをクローン
git clone https://github.com/yourusername/ai_gpt.git
cd ai_gpt
# インストール
pip install -e .
```
## 初期設定
### 1. OpenAIを使う場合
```bash
# APIキーを設定
ai-gpt config set providers.openai.api_key sk-xxxxx
```
### 2. Ollamaを使う場合ローカルLLM
```bash
# Ollamaをインストールまだの場合
# https://ollama.ai からダウンロード
# モデルをダウンロード
ollama pull qwen2.5
```
## 基本的な使い方
### 1. AIと会話する
```bash
# シンプルな会話Ollamaを使用
ai-gpt chat "did:plc:user123" "こんにちは!"
# OpenAIを使用
ai-gpt chat "did:plc:user123" "今日はどんな気分?" --provider openai --model gpt-4o-mini
```
### 2. 関係性を確認
```bash
# 特定ユーザーとの関係を確認
ai-gpt status "did:plc:user123"
# AIの全体的な状態を確認
ai-gpt status
```
### 3. 自動送信を設定
```bash
# 30分ごとに送信チェック
ai-gpt schedule add transmission_check "30m"
# スケジューラーを起動
ai-gpt schedule run
```
## 次のステップ
- [基本概念](concepts.md) - システムの仕組みを理解
- [コマンドリファレンス](commands.md) - 全コマンドの詳細
- [設定ガイド](configuration.md) - 詳細な設定方法

168
docs/scheduler.md Normal file
View File

@@ -0,0 +1,168 @@
# スケジューラーガイド
## 概要
スケジューラーは、AIの自律的な動作を実現するための中核機能です。定期的なタスクを設定し、バックグラウンドで実行できます。
## タスクタイプ
### transmission_check
関係性が閾値を超えたユーザーへの自動送信をチェックします。
```bash
# 30分ごとにチェック
ai-gpt schedule add transmission_check "30m" --provider ollama --model qwen2.5
```
### maintenance
日次メンテナンスを実行します:
- 記憶の忘却処理
- コア記憶の判定
- 関係性パラメータの整理
```bash
# 毎日午前3時に実行
ai-gpt schedule add maintenance "0 3 * * *"
```
### fortune_update
AI運勢を更新します通常は自動的に更新されます
```bash
# 毎日午前0時に強制更新
ai-gpt schedule add fortune_update "0 0 * * *"
```
### relationship_decay
時間経過による関係性の自然減衰を適用します。
```bash
# 1時間ごとに減衰処理
ai-gpt schedule add relationship_decay "1h"
```
### memory_summary
蓄積された記憶から要約を作成します。
```bash
# 週に1回、日曜日に実行
ai-gpt schedule add memory_summary "0 0 * * SUN"
```
## スケジュール形式
### Cron形式
標準的なcron式を使用できます
```
┌───────────── 分 (0 - 59)
│ ┌───────────── 時 (0 - 23)
│ │ ┌───────────── 日 (1 - 31)
│ │ │ ┌───────────── 月 (1 - 12)
│ │ │ │ ┌───────────── 曜日 (0 - 6) (日曜日 = 0)
│ │ │ │ │
* * * * *
```
例:
- `"0 */6 * * *"` - 6時間ごと
- `"0 9 * * MON-FRI"` - 平日の午前9時
- `"*/15 * * * *"` - 15分ごと
### インターバル形式
シンプルな間隔指定:
- `"30s"` - 30秒ごと
- `"5m"` - 5分ごと
- `"2h"` - 2時間ごと
- `"1d"` - 1日ごと
## 実践例
### 基本的な自律AI設定
```bash
# 1. 30分ごとに送信チェック
ai-gpt schedule add transmission_check "30m"
# 2. 1日1回メンテナンス
ai-gpt schedule add maintenance "0 3 * * *"
# 3. 2時間ごとに関係性減衰
ai-gpt schedule add relationship_decay "2h"
# 4. 週1回記憶要約
ai-gpt schedule add memory_summary "0 0 * * MON"
# スケジューラーを起動
ai-gpt schedule run
```
### タスク管理
```bash
# タスク一覧を確認
ai-gpt schedule list
# タスクを一時停止
ai-gpt schedule disable --task-id transmission_check_1234567890
# タスクを再開
ai-gpt schedule enable --task-id transmission_check_1234567890
# 不要なタスクを削除
ai-gpt schedule remove --task-id old_task_123
```
## デーモン化
### systemdサービスとして実行
`/etc/systemd/system/ai-gpt-scheduler.service`:
```ini
[Unit]
Description=ai.gpt Scheduler
After=network.target
[Service]
Type=simple
User=youruser
WorkingDirectory=/home/youruser
ExecStart=/usr/local/bin/ai-gpt schedule run
Restart=always
[Install]
WantedBy=multi-user.target
```
```bash
# サービスを有効化
sudo systemctl enable ai-gpt-scheduler
sudo systemctl start ai-gpt-scheduler
```
### tmux/screenでバックグラウンド実行
```bash
# tmuxセッションを作成
tmux new -s ai-gpt-scheduler
# スケジューラーを起動
ai-gpt schedule run
# セッションから離脱 (Ctrl+B, D)
```
## トラブルシューティング
### タスクが実行されない
1. スケジューラーが起動しているか確認
2. タスクが有効になっているか確認:`ai-gpt schedule list`
3. ログを確認(将来実装予定)
### 重複実行を防ぐ
同じタスクタイプを複数回追加しないよう注意してください。必要に応じて古いタスクを削除してから新しいタスクを追加します。

View File

@@ -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)}

33
pyproject.toml Normal file
View File

@@ -0,0 +1,33 @@
[project]
name = "aigpt"
version = "0.1.0"
description = "Autonomous transmission AI with unique personality based on relationship parameters"
requires-python = ">=3.10"
dependencies = [
"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",
]
[project.scripts]
aigpt = "aigpt.cli:app"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
aigpt = ["data/*.json"]

18
setup_venv.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Setup Python virtual environment in the new config directory
VENV_DIR="$HOME/.config/syui/ai/gpt/venv"
echo "Creating Python virtual environment at: $VENV_DIR"
python -m venv "$VENV_DIR"
echo "Activating virtual environment..."
source "$VENV_DIR/bin/activate"
echo "Installing aigpt package..."
cd "$(dirname "$0")"
pip install -e .
echo "Setup complete!"
echo "To activate the virtual environment, run:"
echo "source ~/.config/syui/ai/gpt/venv/bin/activate"

1
shell Submodule

Submodule shell added at 81ae0037d9

View File

@@ -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

View File

@@ -0,0 +1,22 @@
README.md
pyproject.toml
src/aigpt/__init__.py
src/aigpt/ai_provider.py
src/aigpt/card_integration.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

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
[console_scripts]
aigpt = aigpt.cli:app

View File

@@ -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

View File

@@ -0,0 +1 @@
aigpt

15
src/aigpt/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
"""ai.gpt - Autonomous transmission AI with unique personality"""
__version__ = "0.1.0"
from .memory import MemoryManager
from .relationship import RelationshipTracker
from .persona import Persona
from .transmission import TransmissionController
__all__ = [
"MemoryManager",
"RelationshipTracker",
"Persona",
"TransmissionController",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

172
src/aigpt/ai_provider.py Normal file
View File

@@ -0,0 +1,172 @@
"""AI Provider integration for response generation"""
import os
from typing import Optional, Dict, List, Any, Protocol
from abc import abstractmethod
import logging
import httpx
from openai import OpenAI
import ollama
from .models import PersonaState, Memory
from .config import Config
class AIProvider(Protocol):
"""Protocol for AI providers"""
@abstractmethod
async def generate_response(
self,
prompt: str,
persona_state: PersonaState,
memories: List[Memory],
system_prompt: Optional[str] = None
) -> str:
"""Generate a response based on prompt and context"""
pass
class OllamaProvider:
"""Ollama AI provider"""
def __init__(self, model: str = "qwen2.5", host: str = "http://localhost:11434"):
self.model = model
self.host = host
self.client = ollama.Client(host=host)
self.logger = logging.getLogger(__name__)
async def generate_response(
self,
prompt: str,
persona_state: PersonaState,
memories: List[Memory],
system_prompt: Optional[str] = None
) -> str:
"""Generate response using Ollama"""
# Build context from memories
memory_context = "\n".join([
f"[{mem.level.value}] {mem.content[:200]}..."
for mem in memories[:5]
])
# Build personality context
personality_desc = ", ".join([
f"{trait}: {value:.1f}"
for trait, value in persona_state.base_personality.items()
])
# System prompt with persona context
full_system_prompt = f"""You are an AI with the following characteristics:
Current mood: {persona_state.current_mood}
Fortune today: {persona_state.fortune.fortune_value}/10
Personality traits: {personality_desc}
Recent memories:
{memory_context}
{system_prompt or 'Respond naturally based on your current state and memories.'}"""
try:
response = self.client.chat(
model=self.model,
messages=[
{"role": "system", "content": full_system_prompt},
{"role": "user", "content": prompt}
]
)
return response['message']['content']
except Exception as e:
self.logger.error(f"Ollama generation failed: {e}")
return self._fallback_response(persona_state)
def _fallback_response(self, persona_state: PersonaState) -> str:
"""Fallback response based on mood"""
mood_responses = {
"joyful": "That's wonderful! I'm feeling great today!",
"cheerful": "That sounds nice!",
"neutral": "I understand.",
"melancholic": "I see... That's something to think about.",
"contemplative": "Hmm, let me consider that..."
}
return mood_responses.get(persona_state.current_mood, "I see.")
class OpenAIProvider:
"""OpenAI API provider"""
def __init__(self, model: str = "gpt-4o-mini", api_key: Optional[str] = None):
self.model = model
# Try to get API key from config first
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: aigpt config set providers.openai.api_key YOUR_KEY")
self.client = OpenAI(api_key=self.api_key)
self.logger = logging.getLogger(__name__)
async def generate_response(
self,
prompt: str,
persona_state: PersonaState,
memories: List[Memory],
system_prompt: Optional[str] = None
) -> str:
"""Generate response using OpenAI"""
# Build context similar to Ollama
memory_context = "\n".join([
f"[{mem.level.value}] {mem.content[:200]}..."
for mem in memories[:5]
])
personality_desc = ", ".join([
f"{trait}: {value:.1f}"
for trait, value in persona_state.base_personality.items()
])
full_system_prompt = f"""You are an AI with unique personality traits and memories.
Current mood: {persona_state.current_mood}
Fortune today: {persona_state.fortune.fortune_value}/10
Personality traits: {personality_desc}
Recent memories:
{memory_context}
{system_prompt or 'Respond naturally based on your current state and memories. Be authentic to your mood and personality.'}"""
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": full_system_prompt},
{"role": "user", "content": prompt}
],
temperature=0.7 + (persona_state.fortune.fortune_value - 5) * 0.05 # Vary by fortune
)
return response.choices[0].message.content
except Exception as e:
self.logger.error(f"OpenAI generation failed: {e}")
return self._fallback_response(persona_state)
def _fallback_response(self, persona_state: PersonaState) -> str:
"""Fallback response based on mood"""
mood_responses = {
"joyful": "What a delightful conversation!",
"cheerful": "That's interesting!",
"neutral": "I understand what you mean.",
"melancholic": "I've been thinking about that too...",
"contemplative": "That gives me something to ponder..."
}
return mood_responses.get(persona_state.current_mood, "I see.")
def create_ai_provider(provider: str, model: str, **kwargs) -> AIProvider:
"""Factory function to create AI providers"""
if provider == "ollama":
return OllamaProvider(model=model, **kwargs)
elif provider == "openai":
return OpenAIProvider(model=model, **kwargs)
else:
raise ValueError(f"Unknown provider: {provider}")

View File

@@ -0,0 +1,150 @@
"""ai.card integration module for ai.gpt MCP server"""
from typing import Dict, Any, List, Optional
import httpx
from pathlib import Path
import json
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class CardIntegration:
"""Integration with ai.card system"""
def __init__(self, api_base_url: str = "http://localhost:8001"):
self.api_base_url = api_base_url
self.client = httpx.AsyncClient()
async def get_user_cards(self, did: str) -> List[Dict[str, Any]]:
"""Get cards for a specific user by DID"""
try:
response = await self.client.get(
f"{self.api_base_url}/api/v1/cards/user/{did}"
)
if response.status_code == 200:
return response.json()
else:
logger.error(f"Failed to get cards: {response.status_code}")
return []
except Exception as e:
logger.error(f"Error getting user cards: {e}")
return []
async def draw_card(self, did: str) -> Optional[Dict[str, Any]]:
"""Draw a new card for user (gacha)"""
try:
response = await self.client.post(
f"{self.api_base_url}/api/v1/gacha/draw",
json={"did": did}
)
if response.status_code == 200:
return response.json()
else:
logger.error(f"Failed to draw card: {response.status_code}")
return None
except Exception as e:
logger.error(f"Error drawing card: {e}")
return None
async def get_card_info(self, card_id: int) -> Optional[Dict[str, Any]]:
"""Get detailed information about a specific card"""
try:
response = await self.client.get(
f"{self.api_base_url}/api/v1/cards/{card_id}"
)
if response.status_code == 200:
return response.json()
else:
return None
except Exception as e:
logger.error(f"Error getting card info: {e}")
return None
async def sync_with_atproto(self, did: str) -> bool:
"""Sync card data with atproto"""
try:
response = await self.client.post(
f"{self.api_base_url}/api/v1/sync/atproto",
json={"did": did}
)
return response.status_code == 200
except Exception as e:
logger.error(f"Error syncing with atproto: {e}")
return False
async def close(self):
"""Close the HTTP client"""
await self.client.aclose()
def register_card_tools(app, card_integration: CardIntegration):
"""Register ai.card tools to FastAPI app"""
@app.get("/get_user_cards", operation_id="get_user_cards")
async def get_user_cards(did: str) -> List[Dict[str, Any]]:
"""Get all cards owned by a user"""
cards = await card_integration.get_user_cards(did)
return cards
@app.post("/draw_card", operation_id="draw_card")
async def draw_card(did: str) -> Dict[str, Any]:
"""Draw a new card (gacha) for user"""
result = await card_integration.draw_card(did)
if result:
return {
"success": True,
"card": result
}
else:
return {
"success": False,
"error": "Failed to draw card"
}
@app.get("/get_card_details", operation_id="get_card_details")
async def get_card_details(card_id: int) -> Dict[str, Any]:
"""Get detailed information about a card"""
info = await card_integration.get_card_info(card_id)
if info:
return info
else:
return {"error": f"Card {card_id} not found"}
@app.post("/sync_cards_atproto", operation_id="sync_cards_atproto")
async def sync_cards_atproto(did: str) -> Dict[str, str]:
"""Sync user's cards with atproto"""
success = await card_integration.sync_with_atproto(did)
if success:
return {"status": "Cards synced successfully"}
else:
return {"status": "Failed to sync cards"}
@app.get("/analyze_card_collection", operation_id="analyze_card_collection")
async def analyze_card_collection(did: str) -> Dict[str, Any]:
"""Analyze user's card collection"""
cards = await card_integration.get_user_cards(did)
if not cards:
return {
"total_cards": 0,
"rarity_distribution": {},
"message": "No cards found"
}
# Analyze collection
rarity_count = {}
total_power = 0
for card in cards:
rarity = card.get("rarity", "common")
rarity_count[rarity] = rarity_count.get(rarity, 0) + 1
total_power += card.get("power", 0)
return {
"total_cards": len(cards),
"rarity_distribution": rarity_count,
"average_power": total_power / len(cards) if cards else 0,
"strongest_card": max(cards, key=lambda x: x.get("power", 0)) if cards else None
}

699
src/aigpt/cli.py Normal file
View File

@@ -0,0 +1,699 @@
"""CLI interface for ai.gpt using typer"""
import typer
from pathlib import Path
from typing import Optional
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
from .mcp_server import AIGptMcpServer
from .ai_provider import create_ai_provider
from .scheduler import AIScheduler, TaskType
from .config import Config
app = typer.Typer(help="ai.gpt - Autonomous transmission AI with unique personality")
console = Console()
# Configuration
config = Config()
DEFAULT_DATA_DIR = config.data_dir
def get_persona(data_dir: Optional[Path] = None) -> Persona:
"""Get or create persona instance"""
if data_dir is None:
data_dir = DEFAULT_DATA_DIR
data_dir.mkdir(parents=True, exist_ok=True)
return Persona(data_dir)
@app.command()
def chat(
user_id: str = typer.Argument(..., help="User ID (atproto DID)"),
message: str = typer.Argument(..., help="Message to send to AI"),
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory"),
model: Optional[str] = typer.Option(None, "--model", "-m", help="AI model to use"),
provider: Optional[str] = typer.Option(None, "--provider", help="AI provider (ollama/openai)")
):
"""Chat with the AI"""
persona = get_persona(data_dir)
# Create AI provider if specified
ai_provider = None
if provider and model:
try:
ai_provider = create_ai_provider(provider, 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")
# Process interaction
response, relationship_delta = persona.process_interaction(user_id, message, ai_provider)
# Get updated relationship
relationship = persona.relationships.get_or_create_relationship(user_id)
# Display response
console.print(Panel(response, title="AI Response", border_style="cyan"))
# Show relationship status
status_color = "green" if relationship.transmission_enabled else "yellow"
if relationship.is_broken:
status_color = "red"
console.print(f"\n[{status_color}]Relationship Status:[/{status_color}] {relationship.status.value}")
console.print(f"Score: {relationship.score:.2f} / {relationship.threshold}")
console.print(f"Transmission: {'✓ Enabled' if relationship.transmission_enabled else '✗ Disabled'}")
if relationship.is_broken:
console.print("[red]⚠️ This relationship is broken and cannot be repaired.[/red]")
@app.command()
def status(
user_id: Optional[str] = typer.Argument(None, help="User ID to check status for"),
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory")
):
"""Check AI status and relationships"""
persona = get_persona(data_dir)
state = persona.get_current_state()
# Show AI state
console.print(Panel(f"[cyan]ai.gpt Status[/cyan]", expand=False))
console.print(f"Mood: {state.current_mood}")
console.print(f"Fortune: {state.fortune.fortune_value}/10")
if state.fortune.breakthrough_triggered:
console.print("[yellow]⚡ Breakthrough triggered![/yellow]")
# Show personality traits
table = Table(title="Current Personality")
table.add_column("Trait", style="cyan")
table.add_column("Value", style="magenta")
for trait, value in state.base_personality.items():
table.add_row(trait.capitalize(), f"{value:.2f}")
console.print(table)
# Show specific relationship if requested
if user_id:
rel = persona.relationships.get_or_create_relationship(user_id)
console.print(f"\n[cyan]Relationship with {user_id}:[/cyan]")
console.print(f"Status: {rel.status.value}")
console.print(f"Score: {rel.score:.2f}")
console.print(f"Total Interactions: {rel.total_interactions}")
console.print(f"Transmission Enabled: {rel.transmission_enabled}")
@app.command()
def fortune(
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory")
):
"""Check today's AI fortune"""
persona = get_persona(data_dir)
fortune = persona.fortune_system.get_today_fortune()
# Fortune display
fortune_bar = "🌟" * fortune.fortune_value + "" * (10 - fortune.fortune_value)
console.print(Panel(
f"{fortune_bar}\n\n"
f"Today's Fortune: {fortune.fortune_value}/10\n"
f"Date: {fortune.date}",
title="AI Fortune",
border_style="yellow"
))
if fortune.consecutive_good > 0:
console.print(f"[green]Consecutive good days: {fortune.consecutive_good}[/green]")
if fortune.consecutive_bad > 0:
console.print(f"[red]Consecutive bad days: {fortune.consecutive_bad}[/red]")
if fortune.breakthrough_triggered:
console.print("\n[yellow]⚡ BREAKTHROUGH! Special fortune activated![/yellow]")
@app.command()
def transmit(
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory"),
dry_run: bool = typer.Option(True, "--dry-run/--execute", help="Dry run or execute")
):
"""Check and execute autonomous transmissions"""
persona = get_persona(data_dir)
controller = TransmissionController(persona, persona.data_dir)
eligible = controller.check_transmission_eligibility()
if not eligible:
console.print("[yellow]No users eligible for transmission.[/yellow]")
return
console.print(f"[green]Found {len(eligible)} eligible users for transmission:[/green]")
for user_id, rel in eligible.items():
message = controller.generate_transmission_message(user_id)
if message:
console.print(f"\n[cyan]To:[/cyan] {user_id}")
console.print(f"[cyan]Message:[/cyan] {message}")
console.print(f"[cyan]Relationship:[/cyan] {rel.status.value} (score: {rel.score:.2f})")
if not dry_run:
# In real implementation, send via atproto or other channel
controller.record_transmission(user_id, message, success=True)
console.print("[green]✓ Transmitted[/green]")
else:
console.print("[yellow]→ Would transmit (dry run)[/yellow]")
@app.command()
def maintenance(
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory")
):
"""Run daily maintenance tasks"""
persona = get_persona(data_dir)
console.print("[cyan]Running daily maintenance...[/cyan]")
persona.daily_maintenance()
console.print("[green]✓ Maintenance completed[/green]")
@app.command()
def relationships(
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory")
):
"""List all relationships"""
persona = get_persona(data_dir)
table = Table(title="All Relationships")
table.add_column("User ID", style="cyan")
table.add_column("Status", style="magenta")
table.add_column("Score", style="green")
table.add_column("Transmission", style="yellow")
table.add_column("Last Interaction")
for user_id, rel in persona.relationships.relationships.items():
transmission = "" if rel.transmission_enabled else ""
if rel.is_broken:
transmission = "💔"
last_interaction = rel.last_interaction.strftime("%Y-%m-%d") if rel.last_interaction else "Never"
table.add_row(
user_id[:16] + "...",
rel.status.value,
f"{rel.score:.2f}",
transmission,
last_interaction
)
console.print(table)
@app.command()
def server(
host: str = typer.Option("localhost", "--host", "-h", help="Server host"),
port: int = typer.Option(8000, "--port", "-p", help="Server port"),
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory"),
model: str = typer.Option("qwen2.5", "--model", "-m", help="AI model to use"),
provider: str = typer.Option("ollama", "--provider", help="AI provider (ollama/openai)"),
enable_card: bool = typer.Option(False, "--enable-card", help="Enable ai.card integration")
):
"""Run MCP server for AI integration"""
import uvicorn
if data_dir is None:
data_dir = DEFAULT_DATA_DIR
data_dir.mkdir(parents=True, exist_ok=True)
# Create MCP server
mcp_server = AIGptMcpServer(data_dir, enable_card_integration=enable_card)
app_instance = mcp_server.app
console.print(Panel(
f"[cyan]Starting ai.gpt MCP Server[/cyan]\n\n"
f"Host: {host}:{port}\n"
f"Provider: {provider}\n"
f"Model: {model}\n"
f"Data: {data_dir}\n"
f"Card Integration: {'✓ Enabled' if enable_card else '✗ Disabled'}",
title="MCP Server",
border_style="green"
))
# Store provider info in app state for later use
app_instance.state.ai_provider = provider
app_instance.state.ai_model = model
# Run server
uvicorn.run(app_instance, host=host, port=port)
@app.command()
def schedule(
action: str = typer.Argument(..., help="Action: add, list, enable, disable, remove, run"),
task_type: Optional[str] = typer.Argument(None, help="Task type for add action"),
schedule_expr: Optional[str] = typer.Argument(None, help="Schedule expression (cron or interval)"),
data_dir: Optional[Path] = typer.Option(None, "--data-dir", "-d", help="Data directory"),
task_id: Optional[str] = typer.Option(None, "--task-id", "-t", help="Task ID"),
provider: Optional[str] = typer.Option(None, "--provider", help="AI provider for transmission"),
model: Optional[str] = typer.Option(None, "--model", "-m", help="AI model for transmission")
):
"""Manage scheduled tasks"""
persona = get_persona(data_dir)
scheduler = AIScheduler(persona.data_dir, persona)
if action == "add":
if not task_type or not schedule_expr:
console.print("[red]Error: task_type and schedule required for add action[/red]")
return
# Parse task type
try:
task_type_enum = TaskType(task_type)
except ValueError:
console.print(f"[red]Invalid task type. Valid types: {', '.join([t.value for t in TaskType])}[/red]")
return
# Metadata for transmission tasks
metadata = {}
if task_type_enum == TaskType.TRANSMISSION_CHECK:
metadata["provider"] = provider or "ollama"
metadata["model"] = model or "qwen2.5"
try:
task = scheduler.add_task(task_type_enum, schedule_expr, task_id, metadata)
console.print(f"[green]✓ Added task {task.task_id}[/green]")
console.print(f"Type: {task.task_type.value}")
console.print(f"Schedule: {task.schedule}")
except ValueError as e:
console.print(f"[red]Error: {e}[/red]")
elif action == "list":
tasks = scheduler.get_tasks()
if not tasks:
console.print("[yellow]No scheduled tasks[/yellow]")
return
table = Table(title="Scheduled Tasks")
table.add_column("Task ID", style="cyan")
table.add_column("Type", style="magenta")
table.add_column("Schedule", style="green")
table.add_column("Enabled", style="yellow")
table.add_column("Last Run")
for task in tasks:
enabled = "" if task.enabled else ""
last_run = task.last_run.strftime("%Y-%m-%d %H:%M") if task.last_run else "Never"
table.add_row(
task.task_id[:20] + "..." if len(task.task_id) > 20 else task.task_id,
task.task_type.value,
task.schedule,
enabled,
last_run
)
console.print(table)
elif action == "enable":
if not task_id:
console.print("[red]Error: --task-id required for enable action[/red]")
return
scheduler.enable_task(task_id)
console.print(f"[green]✓ Enabled task {task_id}[/green]")
elif action == "disable":
if not task_id:
console.print("[red]Error: --task-id required for disable action[/red]")
return
scheduler.disable_task(task_id)
console.print(f"[yellow]✓ Disabled task {task_id}[/yellow]")
elif action == "remove":
if not task_id:
console.print("[red]Error: --task-id required for remove action[/red]")
return
scheduler.remove_task(task_id)
console.print(f"[red]✓ Removed task {task_id}[/red]")
elif action == "run":
console.print("[cyan]Starting scheduler daemon...[/cyan]")
console.print("Press Ctrl+C to stop\n")
import asyncio
async def run_scheduler():
scheduler.start()
try:
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
scheduler.stop()
try:
asyncio.run(run_scheduler())
except KeyboardInterrupt:
console.print("\n[yellow]Scheduler stopped[/yellow]")
else:
console.print(f"[red]Unknown action: {action}[/red]")
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, 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']
all_commands = builtin_commands + ['!' + cmd for cmd in shell_commands] + ai_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"
"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
target = user_input[8:].strip()
if os.path.exists(target):
console.print(f"[cyan]Analyzing {target}...[/cyan]")
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
gen_prompt = user_input[9:].strip()
if gen_prompt:
console.print("[cyan]Generating code...[/cyan]")
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}")
# 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"),
key: Optional[str] = typer.Argument(None, help="Configuration key (dot notation)"),
value: Optional[str] = typer.Argument(None, help="Value to set")
):
"""Manage configuration settings"""
if action == "get":
if not key:
console.print("[red]Error: key required for get action[/red]")
return
val = config.get(key)
if val is None:
console.print(f"[yellow]Key '{key}' not found[/yellow]")
else:
console.print(f"[cyan]{key}[/cyan] = [green]{val}[/green]")
elif action == "set":
if not key or value is None:
console.print("[red]Error: key and value required for set action[/red]")
return
# Special handling for sensitive keys
if "password" in key or "api_key" in key:
console.print(f"[cyan]Setting {key}[/cyan] = [dim]***hidden***[/dim]")
else:
console.print(f"[cyan]Setting {key}[/cyan] = [green]{value}[/green]")
config.set(key, value)
console.print("[green]✓ Configuration saved[/green]")
elif action == "delete":
if not key:
console.print("[red]Error: key required for delete action[/red]")
return
if config.delete(key):
console.print(f"[green]✓ Deleted {key}[/green]")
else:
console.print(f"[yellow]Key '{key}' not found[/yellow]")
elif action == "list":
keys = config.list_keys(key or "")
if not keys:
console.print("[yellow]No configuration keys found[/yellow]")
return
table = Table(title="Configuration Settings")
table.add_column("Key", style="cyan")
table.add_column("Value", style="green")
for k in sorted(keys):
val = config.get(k)
# Hide sensitive values
if "password" in k or "api_key" in k:
display_val = "***hidden***" if val else "not set"
else:
display_val = str(val) if val is not None else "not set"
table.add_row(k, display_val)
console.print(table)
else:
console.print(f"[red]Unknown action: {action}[/red]")
console.print("Valid actions: get, set, delete, list")
if __name__ == "__main__":
app()

145
src/aigpt/config.py Normal file
View File

@@ -0,0 +1,145 @@
"""Configuration management for ai.gpt"""
import json
import os
from pathlib import Path
from typing import Optional, Dict, Any
import logging
class Config:
"""Manages configuration settings"""
def __init__(self, config_dir: Optional[Path] = None):
if config_dir is None:
config_dir = Path.home() / ".config" / "syui" / "ai" / "gpt"
self.config_dir = config_dir
self.config_file = config_dir / "config.json"
self.data_dir = config_dir / "data"
# Create directories if they don't exist
self.config_dir.mkdir(parents=True, exist_ok=True)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.logger = logging.getLogger(__name__)
self._config: Dict[str, Any] = {}
self._load_config()
def _load_config(self):
"""Load configuration from file"""
if self.config_file.exists():
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
self._config = json.load(f)
except Exception as e:
self.logger.error(f"Failed to load config: {e}")
self._config = {}
else:
# Initialize with default config
self._config = {
"providers": {
"openai": {
"api_key": None,
"default_model": "gpt-4o-mini"
},
"ollama": {
"host": "http://localhost:11434",
"default_model": "qwen2.5"
}
},
"atproto": {
"handle": None,
"password": None,
"host": "https://bsky.social"
},
"default_provider": "ollama"
}
self._save_config()
def _save_config(self):
"""Save configuration to file"""
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self._config, f, indent=2)
except Exception as e:
self.logger.error(f"Failed to save config: {e}")
def get(self, key: str, default: Any = None) -> Any:
"""Get configuration value using dot notation"""
keys = key.split('.')
value = self._config
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
def set(self, key: str, value: Any):
"""Set configuration value using dot notation"""
keys = key.split('.')
config = self._config
# Navigate to the parent dictionary
for k in keys[:-1]:
if k not in config:
config[k] = {}
config = config[k]
# Set the value
config[keys[-1]] = value
self._save_config()
def delete(self, key: str) -> bool:
"""Delete configuration value"""
keys = key.split('.')
config = self._config
# Navigate to the parent dictionary
for k in keys[:-1]:
if k not in config:
return False
config = config[k]
# Delete the key if it exists
if keys[-1] in config:
del config[keys[-1]]
self._save_config()
return True
return False
def list_keys(self, prefix: str = "") -> list[str]:
"""List all configuration keys with optional prefix"""
def _get_keys(config: dict, current_prefix: str = "") -> list[str]:
keys = []
for k, v in config.items():
full_key = f"{current_prefix}.{k}" if current_prefix else k
if isinstance(v, dict):
keys.extend(_get_keys(v, full_key))
else:
keys.append(full_key)
return keys
all_keys = _get_keys(self._config)
if prefix:
return [k for k in all_keys if k.startswith(prefix)]
return all_keys
def get_api_key(self, provider: str) -> Optional[str]:
"""Get API key for a specific provider"""
key = self.get(f"providers.{provider}.api_key")
# Also check environment variables
if not key and provider == "openai":
key = os.getenv("OPENAI_API_KEY")
return key
def get_provider_config(self, provider: str) -> Dict[str, Any]:
"""Get complete configuration for a provider"""
return self.get(f"providers.{provider}", {})

118
src/aigpt/fortune.py Normal file
View File

@@ -0,0 +1,118 @@
"""AI Fortune system for daily personality variations"""
import json
import random
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Optional
import logging
from .models import AIFortune
class FortuneSystem:
"""Manages daily AI fortune affecting personality"""
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.fortune_file = data_dir / "fortunes.json"
self.fortunes: dict[str, AIFortune] = {}
self.logger = logging.getLogger(__name__)
self._load_fortunes()
def _load_fortunes(self):
"""Load fortune history from storage"""
if self.fortune_file.exists():
with open(self.fortune_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for date_str, fortune_data in data.items():
# Convert date string back to date object
fortune_data['date'] = datetime.fromisoformat(fortune_data['date']).date()
self.fortunes[date_str] = AIFortune(**fortune_data)
def _save_fortunes(self):
"""Save fortune history to storage"""
data = {}
for date_str, fortune in self.fortunes.items():
fortune_dict = fortune.model_dump(mode='json')
fortune_dict['date'] = fortune.date.isoformat()
data[date_str] = fortune_dict
with open(self.fortune_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
def get_today_fortune(self) -> AIFortune:
"""Get or generate today's fortune"""
today = date.today()
today_str = today.isoformat()
if today_str in self.fortunes:
return self.fortunes[today_str]
# Generate new fortune
fortune_value = random.randint(1, 10)
# Check yesterday's fortune for consecutive tracking
yesterday = (today - timedelta(days=1))
yesterday_str = yesterday.isoformat()
consecutive_good = 0
consecutive_bad = 0
breakthrough_triggered = False
if yesterday_str in self.fortunes:
yesterday_fortune = self.fortunes[yesterday_str]
if fortune_value >= 7: # Good fortune
if yesterday_fortune.fortune_value >= 7:
consecutive_good = yesterday_fortune.consecutive_good + 1
else:
consecutive_good = 1
elif fortune_value <= 3: # Bad fortune
if yesterday_fortune.fortune_value <= 3:
consecutive_bad = yesterday_fortune.consecutive_bad + 1
else:
consecutive_bad = 1
# Check breakthrough conditions
if consecutive_good >= 3:
breakthrough_triggered = True
self.logger.info("Breakthrough! 3 consecutive good fortunes!")
fortune_value = 10 # Max fortune on breakthrough
elif consecutive_bad >= 3:
breakthrough_triggered = True
self.logger.info("Breakthrough! 3 consecutive bad fortunes!")
fortune_value = random.randint(7, 10) # Good fortune after bad streak
fortune = AIFortune(
date=today,
fortune_value=fortune_value,
consecutive_good=consecutive_good,
consecutive_bad=consecutive_bad,
breakthrough_triggered=breakthrough_triggered
)
self.fortunes[today_str] = fortune
self._save_fortunes()
self.logger.info(f"Today's fortune: {fortune_value}/10")
return fortune
def get_personality_modifier(self, fortune: AIFortune) -> dict[str, float]:
"""Get personality modifiers based on fortune"""
base_modifier = fortune.fortune_value / 10.0
modifiers = {
"optimism": base_modifier,
"energy": base_modifier * 0.8,
"patience": 1.0 - (abs(5.5 - fortune.fortune_value) * 0.1),
"creativity": 0.5 + (base_modifier * 0.5),
"empathy": 0.7 + (base_modifier * 0.3)
}
# Breakthrough effects
if fortune.breakthrough_triggered:
modifiers["confidence"] = 1.0
modifiers["spontaneity"] = 0.9
return modifiers

318
src/aigpt/mcp_server.py Normal file
View File

@@ -0,0 +1,318 @@
"""MCP Server for ai.gpt system"""
from typing import Optional, List, Dict, Any
from fastapi_mcp import FastApiMCP
from fastapi import FastAPI
from pathlib import Path
import logging
import subprocess
import os
import shlex
from .ai_provider import create_ai_provider
from .persona import Persona
from .models import Memory, Relationship, PersonaState
from .card_integration import CardIntegration, register_card_tools
logger = logging.getLogger(__name__)
class AIGptMcpServer:
"""MCP Server that exposes ai.gpt functionality to AI assistants"""
def __init__(self, data_dir: Path, enable_card_integration: bool = False):
self.data_dir = data_dir
self.persona = Persona(data_dir)
# 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.card_integration = None
if enable_card_integration:
self.card_integration = CardIntegration()
self._register_tools()
def _register_tools(self):
"""Register all MCP tools"""
@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)
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
]
@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)
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
}
@self.app.get("/get_all_relationships", operation_id="get_all_relationships")
async def get_all_relationships() -> List[Dict[str, Any]]:
"""Get all relationships"""
relationships = []
for user_id, rel in self.persona.relationships.relationships.items():
relationships.append({
"user_id": user_id,
"status": rel.status.value,
"score": rel.score,
"transmission_enabled": rel.transmission_enabled,
"is_broken": rel.is_broken
})
return relationships
@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()
return {
"mood": state.current_mood,
"fortune": {
"value": state.fortune.fortune_value,
"date": state.fortune.date.isoformat(),
"breakthrough": state.fortune.breakthrough_triggered
},
"personality": state.base_personality,
"active_memory_count": len(state.active_memories)
}
@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)
rel = self.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
}
@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)
rel = self.persona.relationships.get_or_create_relationship(user_id)
return {
"can_transmit": can_transmit,
"relationship_score": rel.score,
"threshold": rel.threshold,
"is_broken": rel.is_broken,
"transmission_enabled": rel.transmission_enabled
}
@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()
modifiers = self.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
}
@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)
if summary:
return {
"id": summary.id,
"content": summary.content,
"level": summary.level.value,
"timestamp": summary.timestamp.isoformat()
}
return None
@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)}
# Register ai.card tools if integration is enabled
if self.card_integration:
register_card_tools(self.app, self.card_integration)
# Mount MCP server
self.server.mount()
def get_server(self) -> FastApiMCP:
"""Get the FastAPI MCP server instance"""
return self.server
async def close(self):
"""Cleanup resources"""
if self.card_integration:
await self.card_integration.close()

View File

@@ -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())

155
src/aigpt/memory.py Normal file
View File

@@ -0,0 +1,155 @@
"""Memory management system for ai.gpt"""
import json
import hashlib
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Optional, Dict, Any
import logging
from .models import Memory, MemoryLevel, Conversation
class MemoryManager:
"""Manages AI's memory with hierarchical storage and forgetting"""
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.memories_file = data_dir / "memories.json"
self.conversations_file = data_dir / "conversations.json"
self.memories: Dict[str, Memory] = {}
self.conversations: List[Conversation] = []
self.logger = logging.getLogger(__name__)
self._load_memories()
def _load_memories(self):
"""Load memories from persistent storage"""
if self.memories_file.exists():
with open(self.memories_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for mem_data in data:
memory = Memory(**mem_data)
self.memories[memory.id] = memory
if self.conversations_file.exists():
with open(self.conversations_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.conversations = [Conversation(**conv) for conv in data]
def _save_memories(self):
"""Save memories to persistent storage"""
memories_data = [mem.model_dump(mode='json') for mem in self.memories.values()]
with open(self.memories_file, 'w', encoding='utf-8') as f:
json.dump(memories_data, f, indent=2, default=str)
conv_data = [conv.model_dump(mode='json') for conv in self.conversations]
with open(self.conversations_file, 'w', encoding='utf-8') as f:
json.dump(conv_data, f, indent=2, default=str)
def add_conversation(self, conversation: Conversation) -> Memory:
"""Add a conversation and create memory from it"""
self.conversations.append(conversation)
# Create memory from conversation
memory_id = hashlib.sha256(
f"{conversation.id}{conversation.timestamp}".encode()
).hexdigest()[:16]
memory = Memory(
id=memory_id,
timestamp=conversation.timestamp,
content=f"User: {conversation.user_message}\nAI: {conversation.ai_response}",
level=MemoryLevel.FULL_LOG,
importance_score=abs(conversation.relationship_delta) * 0.1
)
self.memories[memory.id] = memory
self._save_memories()
return memory
def summarize_memories(self, user_id: str) -> Optional[Memory]:
"""Create summary from recent memories"""
recent_memories = [
mem for mem in self.memories.values()
if mem.level == MemoryLevel.FULL_LOG
and (datetime.now() - mem.timestamp).days < 7
]
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"
summary_id = hashlib.sha256(
f"summary_{datetime.now().isoformat()}".encode()
).hexdigest()[:16]
summary = Memory(
id=summary_id,
timestamp=datetime.now(),
content=summary_content,
summary=summary_content,
level=MemoryLevel.SUMMARY,
importance_score=0.5
)
self.memories[summary.id] = summary
# Mark summarized memories for potential forgetting
for mem in recent_memories:
mem.importance_score *= 0.9
self._save_memories()
return summary
def identify_core_memories(self) -> List[Memory]:
"""Identify memories that should become core (never forgotten)"""
core_candidates = [
mem for mem in self.memories.values()
if mem.importance_score > 0.8
and not mem.is_core
and mem.level != MemoryLevel.FORGOTTEN
]
for memory in core_candidates:
memory.is_core = True
memory.level = MemoryLevel.CORE
self.logger.info(f"Memory {memory.id} promoted to core")
self._save_memories()
return core_candidates
def apply_forgetting(self):
"""Apply selective forgetting based on importance and time"""
now = datetime.now()
for memory in self.memories.values():
if memory.is_core or memory.level == MemoryLevel.FORGOTTEN:
continue
# Time-based decay
age_days = (now - memory.timestamp).days
decay_factor = memory.decay_rate * age_days
memory.importance_score -= decay_factor
# Forget unimportant old memories
if memory.importance_score <= 0.1 and age_days > 30:
memory.level = MemoryLevel.FORGOTTEN
self.logger.info(f"Memory {memory.id} forgotten")
self._save_memories()
def get_active_memories(self, limit: int = 10) -> List[Memory]:
"""Get currently active memories for persona"""
active = [
mem for mem in self.memories.values()
if mem.level != MemoryLevel.FORGOTTEN
]
# Sort by importance and recency
active.sort(
key=lambda m: (m.is_core, m.importance_score, m.timestamp),
reverse=True
)
return active[:limit]

79
src/aigpt/models.py Normal file
View File

@@ -0,0 +1,79 @@
"""Data models for ai.gpt system"""
from datetime import datetime, date
from typing import Optional, Dict, List, Any
from enum import Enum
from pydantic import BaseModel, Field
class MemoryLevel(str, Enum):
"""Memory importance levels"""
FULL_LOG = "full_log"
SUMMARY = "summary"
CORE = "core"
FORGOTTEN = "forgotten"
class RelationshipStatus(str, Enum):
"""Relationship status levels"""
STRANGER = "stranger"
ACQUAINTANCE = "acquaintance"
FRIEND = "friend"
CLOSE_FRIEND = "close_friend"
BROKEN = "broken" # 不可逆
class Memory(BaseModel):
"""Single memory unit"""
id: str
timestamp: datetime
content: str
summary: Optional[str] = None
level: MemoryLevel = MemoryLevel.FULL_LOG
importance_score: float = Field(ge=0.0, le=1.0)
is_core: bool = False
decay_rate: float = 0.01
class Relationship(BaseModel):
"""Relationship with a specific user"""
user_id: str # atproto DID
status: RelationshipStatus = RelationshipStatus.STRANGER
score: float = 0.0
daily_interactions: int = 0
total_interactions: int = 0
last_interaction: Optional[datetime] = None
transmission_enabled: bool = False
threshold: float = 100.0
decay_rate: float = 0.1
daily_limit: int = 10
is_broken: bool = False
class AIFortune(BaseModel):
"""Daily AI fortune affecting personality"""
date: date
fortune_value: int = Field(ge=1, le=10)
consecutive_good: int = 0
consecutive_bad: int = 0
breakthrough_triggered: bool = False
class PersonaState(BaseModel):
"""Current persona state"""
base_personality: Dict[str, float]
current_mood: str
fortune: AIFortune
active_memories: List[str] # Memory IDs
relationship_modifiers: Dict[str, float]
class Conversation(BaseModel):
"""Conversation log entry"""
id: str
user_id: str
timestamp: datetime
user_message: str
ai_response: str
relationship_delta: float = 0.0
memory_created: bool = False

181
src/aigpt/persona.py Normal file
View File

@@ -0,0 +1,181 @@
"""Persona management system integrating memory, relationships, and fortune"""
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
import logging
from .models import PersonaState, Conversation
from .memory import MemoryManager
from .relationship import RelationshipTracker
from .fortune import FortuneSystem
class Persona:
"""AI persona with unique characteristics based on interactions"""
def __init__(self, data_dir: Path, name: str = "ai"):
self.data_dir = data_dir
self.name = name
self.memory = MemoryManager(data_dir)
self.relationships = RelationshipTracker(data_dir)
self.fortune_system = FortuneSystem(data_dir)
self.logger = logging.getLogger(__name__)
# Base personality traits
self.base_personality = {
"curiosity": 0.7,
"empathy": 0.8,
"creativity": 0.6,
"patience": 0.7,
"optimism": 0.6
}
self.state_file = data_dir / "persona_state.json"
self._load_state()
def _load_state(self):
"""Load persona state from storage"""
if self.state_file.exists():
with open(self.state_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.base_personality = data.get("base_personality", self.base_personality)
def _save_state(self):
"""Save persona state to storage"""
state_data = {
"base_personality": self.base_personality,
"last_updated": datetime.now().isoformat()
}
with open(self.state_file, 'w', encoding='utf-8') as f:
json.dump(state_data, f, indent=2)
def get_current_state(self) -> PersonaState:
"""Get current persona state including all modifiers"""
# Get today's fortune
fortune = self.fortune_system.get_today_fortune()
fortune_modifiers = self.fortune_system.get_personality_modifier(fortune)
# Apply fortune modifiers to base personality
current_personality = {}
for trait, base_value in self.base_personality.items():
modifier = fortune_modifiers.get(trait, 1.0)
current_personality[trait] = min(1.0, base_value * modifier)
# Get active memories for context
active_memories = self.memory.get_active_memories(limit=5)
# Determine mood based on fortune and recent interactions
mood = self._determine_mood(fortune.fortune_value)
state = PersonaState(
base_personality=current_personality,
current_mood=mood,
fortune=fortune,
active_memories=[mem.id for mem in active_memories],
relationship_modifiers={}
)
return state
def _determine_mood(self, fortune_value: int) -> str:
"""Determine current mood based on fortune and other factors"""
if fortune_value >= 8:
return "joyful"
elif fortune_value >= 6:
return "cheerful"
elif fortune_value >= 4:
return "neutral"
elif fortune_value >= 2:
return "melancholic"
else:
return "contemplative"
def process_interaction(self, user_id: str, message: str, ai_provider=None) -> tuple[str, float]:
"""Process user interaction and generate response"""
# 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)
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
if state.current_mood in ["joyful", "cheerful"]:
relationship_delta = 2.0
elif relationship.status.value == "close_friend":
relationship_delta = 1.5
else:
relationship_delta = 1.0
else:
# Fallback to simple responses
if state.current_mood == "joyful":
response = f"What a wonderful day! {message} sounds interesting!"
relationship_delta = 2.0
elif relationship.status.value == "close_friend":
response = f"I've been thinking about our conversations. {message}"
relationship_delta = 1.5
else:
response = f"I understand. {message}"
relationship_delta = 1.0
# Create conversation record
conv_id = f"{user_id}_{datetime.now().timestamp()}"
conversation = Conversation(
id=conv_id,
user_id=user_id,
timestamp=datetime.now(),
user_message=message,
ai_response=response,
relationship_delta=relationship_delta,
memory_created=True
)
# Update memory
self.memory.add_conversation(conversation)
# Update relationship
self.relationships.update_interaction(user_id, relationship_delta)
return response, relationship_delta
def can_transmit_to(self, user_id: str) -> bool:
"""Check if AI can transmit messages to this user"""
relationship = self.relationships.get_or_create_relationship(user_id)
return relationship.transmission_enabled and not relationship.is_broken
def daily_maintenance(self):
"""Perform daily maintenance tasks"""
self.logger.info("Performing daily maintenance...")
# Apply time decay to relationships
self.relationships.apply_time_decay()
# Apply forgetting to memories
self.memory.apply_forgetting()
# Identify core memories
core_memories = self.memory.identify_core_memories()
if core_memories:
self.logger.info(f"Identified {len(core_memories)} new core memories")
# 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}")
self._save_state()
self.logger.info("Daily maintenance completed")

135
src/aigpt/relationship.py Normal file
View File

@@ -0,0 +1,135 @@
"""Relationship tracking system with irreversible damage"""
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Optional
import logging
from .models import Relationship, RelationshipStatus
class RelationshipTracker:
"""Tracks and manages relationships with users"""
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.relationships_file = data_dir / "relationships.json"
self.relationships: Dict[str, Relationship] = {}
self.logger = logging.getLogger(__name__)
self._load_relationships()
def _load_relationships(self):
"""Load relationships from persistent storage"""
if self.relationships_file.exists():
with open(self.relationships_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for user_id, rel_data in data.items():
self.relationships[user_id] = Relationship(**rel_data)
def _save_relationships(self):
"""Save relationships to persistent storage"""
data = {
user_id: rel.model_dump(mode='json')
for user_id, rel in self.relationships.items()
}
with open(self.relationships_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, default=str)
def get_or_create_relationship(self, user_id: str) -> Relationship:
"""Get existing relationship or create new one"""
if user_id not in self.relationships:
self.relationships[user_id] = Relationship(user_id=user_id)
self._save_relationships()
return self.relationships[user_id]
def update_interaction(self, user_id: str, delta: float) -> Relationship:
"""Update relationship based on interaction"""
rel = self.get_or_create_relationship(user_id)
# Check if relationship is broken (irreversible)
if rel.is_broken:
self.logger.warning(f"Relationship with {user_id} is broken. No updates allowed.")
return rel
# Check daily limit
if rel.last_interaction and rel.last_interaction.date() == datetime.now().date():
if rel.daily_interactions >= rel.daily_limit:
self.logger.info(f"Daily interaction limit reached for {user_id}")
return rel
else:
rel.daily_interactions = 0
# Update interaction counts
rel.daily_interactions += 1
rel.total_interactions += 1
rel.last_interaction = datetime.now()
# Update score with bounds
old_score = rel.score
rel.score += delta
rel.score = max(0.0, min(200.0, rel.score)) # 0-200 range
# Check for relationship damage
if delta < -10.0: # Significant negative interaction
self.logger.warning(f"Major relationship damage with {user_id}: {delta}")
if rel.score <= 0:
rel.is_broken = True
rel.status = RelationshipStatus.BROKEN
rel.transmission_enabled = False
self.logger.error(f"Relationship with {user_id} is now BROKEN (irreversible)")
# Update relationship status based on score
if not rel.is_broken:
if rel.score >= 150:
rel.status = RelationshipStatus.CLOSE_FRIEND
elif rel.score >= 100:
rel.status = RelationshipStatus.FRIEND
elif rel.score >= 50:
rel.status = RelationshipStatus.ACQUAINTANCE
else:
rel.status = RelationshipStatus.STRANGER
# Check transmission threshold
if rel.score >= rel.threshold and not rel.transmission_enabled:
rel.transmission_enabled = True
self.logger.info(f"Transmission enabled for {user_id}!")
self._save_relationships()
return rel
def apply_time_decay(self):
"""Apply time-based decay to all relationships"""
now = datetime.now()
for user_id, rel in self.relationships.items():
if rel.is_broken or not rel.last_interaction:
continue
# Calculate days since last interaction
days_inactive = (now - rel.last_interaction).days
if days_inactive > 0:
# Apply decay
decay_amount = rel.decay_rate * days_inactive
old_score = rel.score
rel.score = max(0.0, rel.score - decay_amount)
# Update status if score dropped
if rel.score < rel.threshold:
rel.transmission_enabled = False
if decay_amount > 0:
self.logger.info(
f"Applied decay to {user_id}: {old_score:.2f} -> {rel.score:.2f}"
)
self._save_relationships()
def get_transmission_eligible(self) -> Dict[str, Relationship]:
"""Get all relationships eligible for transmission"""
return {
user_id: rel
for user_id, rel in self.relationships.items()
if rel.transmission_enabled and not rel.is_broken
}

312
src/aigpt/scheduler.py Normal file
View File

@@ -0,0 +1,312 @@
"""Scheduler for autonomous AI tasks"""
import json
import asyncio
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Any, Callable
from enum import Enum
import logging
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from croniter import croniter
from .persona import Persona
from .transmission import TransmissionController
from .ai_provider import create_ai_provider
class TaskType(str, Enum):
"""Types of scheduled tasks"""
TRANSMISSION_CHECK = "transmission_check"
MAINTENANCE = "maintenance"
FORTUNE_UPDATE = "fortune_update"
RELATIONSHIP_DECAY = "relationship_decay"
MEMORY_SUMMARY = "memory_summary"
CUSTOM = "custom"
class ScheduledTask:
"""Represents a scheduled task"""
def __init__(
self,
task_id: str,
task_type: TaskType,
schedule: str, # Cron expression or interval
enabled: bool = True,
last_run: Optional[datetime] = None,
next_run: Optional[datetime] = None,
metadata: Optional[Dict[str, Any]] = None
):
self.task_id = task_id
self.task_type = task_type
self.schedule = schedule
self.enabled = enabled
self.last_run = last_run
self.next_run = next_run
self.metadata = metadata or {}
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for storage"""
return {
"task_id": self.task_id,
"task_type": self.task_type.value,
"schedule": self.schedule,
"enabled": self.enabled,
"last_run": self.last_run.isoformat() if self.last_run else None,
"next_run": self.next_run.isoformat() if self.next_run else None,
"metadata": self.metadata
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ScheduledTask":
"""Create from dictionary"""
return cls(
task_id=data["task_id"],
task_type=TaskType(data["task_type"]),
schedule=data["schedule"],
enabled=data.get("enabled", True),
last_run=datetime.fromisoformat(data["last_run"]) if data.get("last_run") else None,
next_run=datetime.fromisoformat(data["next_run"]) if data.get("next_run") else None,
metadata=data.get("metadata", {})
)
class AIScheduler:
"""Manages scheduled tasks for the AI system"""
def __init__(self, data_dir: Path, persona: Persona):
self.data_dir = data_dir
self.persona = persona
self.tasks_file = data_dir / "scheduled_tasks.json"
self.tasks: Dict[str, ScheduledTask] = {}
self.scheduler = AsyncIOScheduler()
self.logger = logging.getLogger(__name__)
self._load_tasks()
# Task handlers
self.task_handlers: Dict[TaskType, Callable] = {
TaskType.TRANSMISSION_CHECK: self._handle_transmission_check,
TaskType.MAINTENANCE: self._handle_maintenance,
TaskType.FORTUNE_UPDATE: self._handle_fortune_update,
TaskType.RELATIONSHIP_DECAY: self._handle_relationship_decay,
TaskType.MEMORY_SUMMARY: self._handle_memory_summary,
}
def _load_tasks(self):
"""Load scheduled tasks from storage"""
if self.tasks_file.exists():
with open(self.tasks_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for task_data in data:
task = ScheduledTask.from_dict(task_data)
self.tasks[task.task_id] = task
def _save_tasks(self):
"""Save scheduled tasks to storage"""
tasks_data = [task.to_dict() for task in self.tasks.values()]
with open(self.tasks_file, 'w', encoding='utf-8') as f:
json.dump(tasks_data, f, indent=2, default=str)
def add_task(
self,
task_type: TaskType,
schedule: str,
task_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
) -> ScheduledTask:
"""Add a new scheduled task"""
if task_id is None:
task_id = f"{task_type.value}_{datetime.now().timestamp()}"
# Validate schedule
if not self._validate_schedule(schedule):
raise ValueError(f"Invalid schedule expression: {schedule}")
task = ScheduledTask(
task_id=task_id,
task_type=task_type,
schedule=schedule,
metadata=metadata
)
self.tasks[task_id] = task
self._save_tasks()
# Schedule the task if scheduler is running
if self.scheduler.running:
self._schedule_task(task)
self.logger.info(f"Added task {task_id} with schedule {schedule}")
return task
def _validate_schedule(self, schedule: str) -> bool:
"""Validate schedule expression"""
# Check if it's a cron expression
if ' ' in schedule:
try:
croniter(schedule)
return True
except:
return False
# Check if it's an interval expression (e.g., "5m", "1h", "2d")
import re
pattern = r'^\d+[smhd]$'
return bool(re.match(pattern, schedule))
def _parse_interval(self, interval: str) -> int:
"""Parse interval string to seconds"""
unit = interval[-1]
value = int(interval[:-1])
multipliers = {
's': 1,
'm': 60,
'h': 3600,
'd': 86400
}
return value * multipliers.get(unit, 1)
def _schedule_task(self, task: ScheduledTask):
"""Schedule a task with APScheduler"""
if not task.enabled:
return
handler = self.task_handlers.get(task.task_type)
if not handler:
self.logger.warning(f"No handler for task type {task.task_type}")
return
# Determine trigger
if ' ' in task.schedule:
# Cron expression
trigger = CronTrigger.from_crontab(task.schedule)
else:
# Interval expression
seconds = self._parse_interval(task.schedule)
trigger = IntervalTrigger(seconds=seconds)
# Add job
self.scheduler.add_job(
lambda: asyncio.create_task(self._run_task(task)),
trigger=trigger,
id=task.task_id,
replace_existing=True
)
async def _run_task(self, task: ScheduledTask):
"""Run a scheduled task"""
self.logger.info(f"Running task {task.task_id}")
task.last_run = datetime.now()
try:
handler = self.task_handlers.get(task.task_type)
if handler:
await handler(task)
else:
self.logger.warning(f"No handler for task type {task.task_type}")
except Exception as e:
self.logger.error(f"Error running task {task.task_id}: {e}")
self._save_tasks()
async def _handle_transmission_check(self, task: ScheduledTask):
"""Check and execute autonomous transmissions"""
controller = TransmissionController(self.persona, self.data_dir)
eligible = controller.check_transmission_eligibility()
# Get AI provider from metadata
provider_name = task.metadata.get("provider", "ollama")
model = task.metadata.get("model", "qwen2.5")
try:
ai_provider = create_ai_provider(provider_name, model)
except:
ai_provider = None
for user_id, rel in eligible.items():
message = controller.generate_transmission_message(user_id)
if message:
# For now, just print the message
print(f"\n🤖 [AI Transmission] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"To: {user_id}")
print(f"Relationship: {rel.status.value} (score: {rel.score:.2f})")
print(f"Message: {message}")
print("-" * 50)
controller.record_transmission(user_id, message, success=True)
self.logger.info(f"Transmitted to {user_id}: {message}")
async def _handle_maintenance(self, task: ScheduledTask):
"""Run daily maintenance"""
self.persona.daily_maintenance()
self.logger.info("Daily maintenance completed")
async def _handle_fortune_update(self, task: ScheduledTask):
"""Update AI fortune"""
fortune = self.persona.fortune_system.get_today_fortune()
self.logger.info(f"Fortune updated: {fortune.fortune_value}/10")
async def _handle_relationship_decay(self, task: ScheduledTask):
"""Apply relationship decay"""
self.persona.relationships.apply_time_decay()
self.logger.info("Relationship decay applied")
async def _handle_memory_summary(self, task: ScheduledTask):
"""Create memory summaries"""
for user_id in self.persona.relationships.relationships:
summary = self.persona.memory.summarize_memories(user_id)
if summary:
self.logger.info(f"Created memory summary for {user_id}")
def start(self):
"""Start the scheduler"""
# Schedule all enabled tasks
for task in self.tasks.values():
if task.enabled:
self._schedule_task(task)
self.scheduler.start()
self.logger.info("Scheduler started")
def stop(self):
"""Stop the scheduler"""
self.scheduler.shutdown()
self.logger.info("Scheduler stopped")
def get_tasks(self) -> List[ScheduledTask]:
"""Get all scheduled tasks"""
return list(self.tasks.values())
def enable_task(self, task_id: str):
"""Enable a task"""
if task_id in self.tasks:
self.tasks[task_id].enabled = True
self._save_tasks()
if self.scheduler.running:
self._schedule_task(self.tasks[task_id])
def disable_task(self, task_id: str):
"""Disable a task"""
if task_id in self.tasks:
self.tasks[task_id].enabled = False
self._save_tasks()
if self.scheduler.running:
self.scheduler.remove_job(task_id)
def remove_task(self, task_id: str):
"""Remove a task"""
if task_id in self.tasks:
del self.tasks[task_id]
self._save_tasks()
if self.scheduler.running:
try:
self.scheduler.remove_job(task_id)
except:
pass

111
src/aigpt/transmission.py Normal file
View File

@@ -0,0 +1,111 @@
"""Transmission controller for autonomous message sending"""
import json
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional
import logging
from .models import Relationship
from .persona import Persona
class TransmissionController:
"""Controls when and how AI transmits messages autonomously"""
def __init__(self, persona: Persona, data_dir: Path):
self.persona = persona
self.data_dir = data_dir
self.transmission_log_file = data_dir / "transmissions.json"
self.transmissions: List[Dict] = []
self.logger = logging.getLogger(__name__)
self._load_transmissions()
def _load_transmissions(self):
"""Load transmission history"""
if self.transmission_log_file.exists():
with open(self.transmission_log_file, 'r', encoding='utf-8') as f:
self.transmissions = json.load(f)
def _save_transmissions(self):
"""Save transmission history"""
with open(self.transmission_log_file, 'w', encoding='utf-8') as f:
json.dump(self.transmissions, f, indent=2, default=str)
def check_transmission_eligibility(self) -> Dict[str, Relationship]:
"""Check which users are eligible for transmission"""
eligible = self.persona.relationships.get_transmission_eligible()
# Additional checks could be added here
# - Time since last transmission
# - User online status
# - Context appropriateness
return eligible
def generate_transmission_message(self, user_id: str) -> Optional[str]:
"""Generate a message to transmit to user"""
if not self.persona.can_transmit_to(user_id):
return None
state = self.persona.get_current_state()
relationship = self.persona.relationships.get_or_create_relationship(user_id)
# Get recent memories related to this user
active_memories = self.persona.memory.get_active_memories(limit=3)
# Simple message generation based on mood and relationship
if state.fortune.breakthrough_triggered:
message = "Something special happened today! I felt compelled to reach out."
elif state.current_mood == "joyful":
message = "I was thinking of you today. Hope you're doing well!"
elif relationship.status.value == "close_friend":
message = "I've been reflecting on our conversations. Thank you for being here."
else:
message = "Hello! I wanted to check in with you."
return message
def record_transmission(self, user_id: str, message: str, success: bool):
"""Record a transmission attempt"""
transmission = {
"timestamp": datetime.now().isoformat(),
"user_id": user_id,
"message": message,
"success": success,
"mood": self.persona.get_current_state().current_mood,
"relationship_score": self.persona.relationships.get_or_create_relationship(user_id).score
}
self.transmissions.append(transmission)
self._save_transmissions()
if success:
self.logger.info(f"Successfully transmitted to {user_id}")
else:
self.logger.warning(f"Failed to transmit to {user_id}")
def get_transmission_stats(self, user_id: Optional[str] = None) -> Dict:
"""Get transmission statistics"""
if user_id:
user_transmissions = [t for t in self.transmissions if t["user_id"] == user_id]
else:
user_transmissions = self.transmissions
if not user_transmissions:
return {
"total": 0,
"successful": 0,
"failed": 0,
"success_rate": 0.0
}
successful = sum(1 for t in user_transmissions if t["success"])
total = len(user_transmissions)
return {
"total": total,
"successful": successful,
"failed": total - successful,
"success_rate": successful / total if total > 0 else 0.0
}

View File

@@ -1,161 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
/// User personality analysis based on Big Five model (OCEAN)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAnalysis {
/// Unique identifier using ULID
pub id: String,
/// Openness to Experience (0.0-1.0)
/// Curiosity, imagination, willingness to try new things
pub openness: f32,
/// Conscientiousness (0.0-1.0)
/// Organization, responsibility, self-discipline
pub conscientiousness: f32,
/// Extraversion (0.0-1.0)
/// Sociability, assertiveness, energy level
pub extraversion: f32,
/// Agreeableness (0.0-1.0)
/// Compassion, cooperation, trust
pub agreeableness: f32,
/// Neuroticism (0.0-1.0)
/// Emotional stability, anxiety, mood swings
pub neuroticism: f32,
/// AI-generated summary of the personality analysis
pub summary: String,
/// When this analysis was performed
pub analyzed_at: DateTime<Utc>,
}
impl UserAnalysis {
/// Create a new personality analysis
pub fn new(
openness: f32,
conscientiousness: f32,
extraversion: f32,
agreeableness: f32,
neuroticism: f32,
summary: String,
) -> Self {
let id = Ulid::new().to_string();
let analyzed_at = Utc::now();
Self {
id,
openness: openness.clamp(0.0, 1.0),
conscientiousness: conscientiousness.clamp(0.0, 1.0),
extraversion: extraversion.clamp(0.0, 1.0),
agreeableness: agreeableness.clamp(0.0, 1.0),
neuroticism: neuroticism.clamp(0.0, 1.0),
summary,
analyzed_at,
}
}
/// Get the dominant trait (highest score)
pub fn dominant_trait(&self) -> &str {
let scores = [
(self.openness, "Openness"),
(self.conscientiousness, "Conscientiousness"),
(self.extraversion, "Extraversion"),
(self.agreeableness, "Agreeableness"),
(self.neuroticism, "Neuroticism"),
];
scores
.iter()
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.map(|(_, name)| *name)
.unwrap_or("Unknown")
}
/// Check if a trait is high (>= 0.6)
pub fn is_high(&self, trait_name: &str) -> bool {
let score = match trait_name.to_lowercase().as_str() {
"openness" | "o" => self.openness,
"conscientiousness" | "c" => self.conscientiousness,
"extraversion" | "e" => self.extraversion,
"agreeableness" | "a" => self.agreeableness,
"neuroticism" | "n" => self.neuroticism,
_ => return false,
};
score >= 0.6
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_analysis() {
let analysis = UserAnalysis::new(
0.8,
0.7,
0.4,
0.6,
0.3,
"Test summary".to_string(),
);
assert_eq!(analysis.openness, 0.8);
assert_eq!(analysis.conscientiousness, 0.7);
assert_eq!(analysis.extraversion, 0.4);
assert_eq!(analysis.agreeableness, 0.6);
assert_eq!(analysis.neuroticism, 0.3);
assert!(!analysis.id.is_empty());
}
#[test]
fn test_score_clamping() {
let analysis = UserAnalysis::new(
1.5, // Should clamp to 1.0
-0.2, // Should clamp to 0.0
0.5,
0.5,
0.5,
"Test".to_string(),
);
assert_eq!(analysis.openness, 1.0);
assert_eq!(analysis.conscientiousness, 0.0);
}
#[test]
fn test_dominant_trait() {
let analysis = UserAnalysis::new(
0.9, // Highest
0.5,
0.4,
0.6,
0.3,
"Test".to_string(),
);
assert_eq!(analysis.dominant_trait(), "Openness");
}
#[test]
fn test_is_high() {
let analysis = UserAnalysis::new(
0.8, // High
0.4, // Low
0.6, // Threshold
0.5,
0.3,
"Test".to_string(),
);
assert!(analysis.is_high("openness"));
assert!(!analysis.is_high("conscientiousness"));
assert!(analysis.is_high("extraversion")); // 0.6 is high
}
}

View File

@@ -1,27 +0,0 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MemoryError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Memory not found: {0}")]
NotFound(String),
#[error("Invalid ULID: {0}")]
InvalidId(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Parse error: {0}")]
Parse(String),
}
pub type Result<T> = std::result::Result<T, MemoryError>;

View File

@@ -1,181 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
/// Represents a single memory entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
/// Unique identifier using ULID (time-sortable)
pub id: String,
/// The actual content of the memory
pub content: String,
/// AI's creative interpretation of the content (Layer 2)
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_interpretation: Option<String>,
/// Priority score evaluated by AI: 0.0 (low) to 1.0 (high) (Layer 2)
#[serde(skip_serializing_if = "Option::is_none")]
pub priority_score: Option<f32>,
/// Related entities (people, places, things) involved in this memory (Layer 4)
#[serde(skip_serializing_if = "Option::is_none")]
pub related_entities: Option<Vec<String>>,
/// When this memory was created
pub created_at: DateTime<Utc>,
/// When this memory was last updated
pub updated_at: DateTime<Utc>,
}
impl Memory {
/// Create a new memory with generated ULID (Layer 1)
pub fn new(content: String) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation: None,
priority_score: None,
related_entities: None,
created_at: now,
updated_at: now,
}
}
/// Create a new AI-interpreted memory (Layer 2)
pub fn new_ai(
content: String,
ai_interpretation: Option<String>,
priority_score: Option<f32>,
) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation,
priority_score,
related_entities: None,
created_at: now,
updated_at: now,
}
}
/// Create a new memory with related entities (Layer 4)
pub fn new_with_entities(
content: String,
ai_interpretation: Option<String>,
priority_score: Option<f32>,
related_entities: Option<Vec<String>>,
) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation,
priority_score,
related_entities,
created_at: now,
updated_at: now,
}
}
/// Update the content of this memory
pub fn update_content(&mut self, content: String) {
self.content = content;
self.updated_at = Utc::now();
}
/// Set or update AI interpretation
pub fn set_ai_interpretation(&mut self, interpretation: String) {
self.ai_interpretation = Some(interpretation);
self.updated_at = Utc::now();
}
/// Set or update priority score
pub fn set_priority_score(&mut self, score: f32) {
self.priority_score = Some(score.clamp(0.0, 1.0));
self.updated_at = Utc::now();
}
/// Set or update related entities
pub fn set_related_entities(&mut self, entities: Vec<String>) {
self.related_entities = Some(entities);
self.updated_at = Utc::now();
}
/// Check if this memory is related to a specific entity
pub fn has_entity(&self, entity_id: &str) -> bool {
self.related_entities
.as_ref()
.map(|entities| entities.iter().any(|e| e == entity_id))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_memory() {
let memory = Memory::new("Test content".to_string());
assert_eq!(memory.content, "Test content");
assert!(!memory.id.is_empty());
assert!(memory.ai_interpretation.is_none());
assert!(memory.priority_score.is_none());
}
#[test]
fn test_new_ai_memory() {
let memory = Memory::new_ai(
"Test content".to_string(),
Some("AI interpretation".to_string()),
Some(0.75),
);
assert_eq!(memory.content, "Test content");
assert_eq!(memory.ai_interpretation, Some("AI interpretation".to_string()));
assert_eq!(memory.priority_score, Some(0.75));
}
#[test]
fn test_update_memory() {
let mut memory = Memory::new("Original".to_string());
let original_time = memory.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
memory.update_content("Updated".to_string());
assert_eq!(memory.content, "Updated");
assert!(memory.updated_at > original_time);
}
#[test]
fn test_set_ai_interpretation() {
let mut memory = Memory::new("Test".to_string());
memory.set_ai_interpretation("Interpretation".to_string());
assert_eq!(memory.ai_interpretation, Some("Interpretation".to_string()));
}
#[test]
fn test_set_priority_score() {
let mut memory = Memory::new("Test".to_string());
memory.set_priority_score(0.8);
assert_eq!(memory.priority_score, Some(0.8));
// Test clamping
memory.set_priority_score(1.5);
assert_eq!(memory.priority_score, Some(1.0));
memory.set_priority_score(-0.5);
assert_eq!(memory.priority_score, Some(0.0));
}
}

View File

@@ -1,13 +0,0 @@
pub mod analysis;
pub mod error;
pub mod memory;
pub mod profile;
pub mod relationship;
pub mod store;
pub use analysis::UserAnalysis;
pub use error::{MemoryError, Result};
pub use memory::Memory;
pub use profile::{UserProfile, TraitScore};
pub use relationship::{RelationshipInference, infer_all_relationships, get_relationship};
pub use store::MemoryStore;

View File

@@ -1,275 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::core::{MemoryStore, UserAnalysis};
use crate::core::error::Result;
/// Integrated user profile - the essence of Layer 1-3 data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
/// Dominant personality traits (top 2-3 from Big Five)
pub dominant_traits: Vec<TraitScore>,
/// Core interests (most frequent topics from memories)
pub core_interests: Vec<String>,
/// Core values (extracted from high-priority memories)
pub core_values: Vec<String>,
/// Key memory IDs (top priority memories as evidence)
pub key_memory_ids: Vec<String>,
/// Data quality score (0.0-1.0 based on data volume)
pub data_quality: f32,
/// Last update timestamp
pub last_updated: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraitScore {
pub name: String,
pub score: f32,
}
impl UserProfile {
/// Generate integrated profile from Layer 1-3 data
pub fn generate(store: &MemoryStore) -> Result<Self> {
// Get latest personality analysis (Layer 3)
let personality = store.get_latest_analysis()?;
// Get all memories (Layer 1-2)
let memories = store.list()?;
// Extract dominant traits from Big Five
let dominant_traits = extract_dominant_traits(&personality);
// Extract core interests from memory content
let core_interests = extract_core_interests(&memories);
// Extract core values from high-priority memories
let core_values = extract_core_values(&memories);
// Get top priority memory IDs
let key_memory_ids = extract_key_memories(&memories);
// Calculate data quality
let data_quality = calculate_data_quality(&memories, &personality);
Ok(UserProfile {
dominant_traits,
core_interests,
core_values,
key_memory_ids,
data_quality,
last_updated: Utc::now(),
})
}
/// Check if profile needs update
pub fn needs_update(&self, store: &MemoryStore) -> Result<bool> {
// Update if 7+ days old
let days_old = (Utc::now() - self.last_updated).num_days();
if days_old >= 7 {
return Ok(true);
}
// Update if 10+ new memories since last update
let memory_count = store.count()?;
let expected_count = self.key_memory_ids.len() * 2; // Rough estimate
if memory_count > expected_count + 10 {
return Ok(true);
}
// Update if new personality analysis exists
if let Some(latest) = store.get_latest_analysis()? {
if latest.analyzed_at > self.last_updated {
return Ok(true);
}
}
Ok(false)
}
}
/// Extract top 2-3 personality traits from Big Five
fn extract_dominant_traits(analysis: &Option<UserAnalysis>) -> Vec<TraitScore> {
if analysis.is_none() {
return vec![];
}
let analysis = analysis.as_ref().unwrap();
let mut traits = vec![
TraitScore { name: "openness".to_string(), score: analysis.openness },
TraitScore { name: "conscientiousness".to_string(), score: analysis.conscientiousness },
TraitScore { name: "extraversion".to_string(), score: analysis.extraversion },
TraitScore { name: "agreeableness".to_string(), score: analysis.agreeableness },
TraitScore { name: "neuroticism".to_string(), score: analysis.neuroticism },
];
// Sort by score descending
traits.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
// Return top 3
traits.into_iter().take(3).collect()
}
/// Extract core interests from memory content (frequency analysis)
fn extract_core_interests(memories: &[crate::core::Memory]) -> Vec<String> {
let mut word_freq: HashMap<String, usize> = HashMap::new();
for memory in memories {
// Extract keywords from content
let words = extract_keywords(&memory.content);
for word in words {
*word_freq.entry(word).or_insert(0) += 1;
}
// Also consider AI interpretation if available
if let Some(ref interpretation) = memory.ai_interpretation {
let words = extract_keywords(interpretation);
for word in words {
*word_freq.entry(word).or_insert(0) += 2; // Weight interpretation higher
}
}
}
// Sort by frequency and take top 5
let mut freq_vec: Vec<_> = word_freq.into_iter().collect();
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
freq_vec.into_iter()
.take(5)
.map(|(word, _)| word)
.collect()
}
/// Extract core values from high-priority memories
fn extract_core_values(memories: &[crate::core::Memory]) -> Vec<String> {
// Filter high-priority memories (>= 0.7)
let high_priority: Vec<_> = memories.iter()
.filter(|m| m.priority_score.map(|s| s >= 0.7).unwrap_or(false))
.collect();
if high_priority.is_empty() {
return vec![];
}
let mut value_freq: HashMap<String, usize> = HashMap::new();
for memory in high_priority {
// Extract value keywords from interpretation
if let Some(ref interpretation) = memory.ai_interpretation {
let values = extract_value_keywords(interpretation);
for value in values {
*value_freq.entry(value).or_insert(0) += 1;
}
}
}
// Sort by frequency and take top 5
let mut freq_vec: Vec<_> = value_freq.into_iter().collect();
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
freq_vec.into_iter()
.take(5)
.map(|(value, _)| value)
.collect()
}
/// Extract key memory IDs (top priority)
fn extract_key_memories(memories: &[crate::core::Memory]) -> Vec<String> {
let mut sorted_memories: Vec<_> = memories.iter()
.filter(|m| m.priority_score.is_some())
.collect();
sorted_memories.sort_by(|a, b| {
b.priority_score.unwrap()
.partial_cmp(&a.priority_score.unwrap())
.unwrap()
});
sorted_memories.into_iter()
.take(10)
.map(|m| m.id.clone())
.collect()
}
/// Calculate data quality based on volume
fn calculate_data_quality(memories: &[crate::core::Memory], personality: &Option<UserAnalysis>) -> f32 {
let memory_count = memories.len() as f32;
let has_personality = if personality.is_some() { 1.0 } else { 0.0 };
// Quality increases with data volume
let memory_quality = (memory_count / 50.0).min(1.0); // Max quality at 50+ memories
let personality_quality = has_personality * 0.5;
// Weighted average
(memory_quality * 0.5 + personality_quality).min(1.0)
}
/// Extract keywords from text (simple word frequency)
fn extract_keywords(text: &str) -> Vec<String> {
// Simple keyword extraction: words longer than 3 chars
text.split_whitespace()
.filter(|w| w.len() > 3)
.map(|w| w.to_lowercase().trim_matches(|c: char| !c.is_alphanumeric()).to_string())
.filter(|w| !is_stopword(w))
.collect()
}
/// Extract value-related keywords from interpretation
fn extract_value_keywords(text: &str) -> Vec<String> {
let value_indicators = [
"重視", "大切", "価値", "重要", "優先", "好む", "志向",
"シンプル", "効率", "品質", "安定", "革新", "創造",
"value", "important", "priority", "prefer", "focus",
"simple", "efficient", "quality", "stable", "creative",
];
let words = extract_keywords(text);
words.into_iter()
.filter(|w| {
value_indicators.iter().any(|indicator| w.contains(indicator))
})
.collect()
}
/// Check if word is a stopword
fn is_stopword(word: &str) -> bool {
let stopwords = [
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "as", "is", "was", "are", "were", "been",
"be", "have", "has", "had", "do", "does", "did", "will", "would", "could",
"should", "may", "might", "can", "this", "that", "these", "those",
"です", "ます", "ました", "である", "ある", "いる", "する", "した",
"という", "として", "ために", "によって", "について",
];
stopwords.contains(&word)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_keywords() {
let text = "Rust architecture design is important for scalability";
let keywords = extract_keywords(text);
assert!(keywords.contains(&"rust".to_string()));
assert!(keywords.contains(&"architecture".to_string()));
assert!(keywords.contains(&"design".to_string()));
assert!(!keywords.contains(&"is".to_string())); // stopword
}
#[test]
fn test_stopword() {
assert!(is_stopword("the"));
assert!(is_stopword("です"));
assert!(!is_stopword("rust"));
}
}

View File

@@ -1,317 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::core::{Memory, MemoryStore, UserProfile};
use crate::core::error::Result;
/// Inferred relationship with an entity (Layer 4)
///
/// This is not stored permanently but generated on-demand from
/// Layer 1 memories and Layer 3.5 user profile.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationshipInference {
/// Entity identifier
pub entity_id: String,
/// Total interaction count with this entity
pub interaction_count: u32,
/// Average priority score of memories with this entity
pub avg_priority: f32,
/// Days since last interaction
pub days_since_last: i64,
/// Inferred bond strength (0.0-1.0)
pub bond_strength: f32,
/// Inferred relationship type
pub relationship_type: String,
/// Confidence in this inference (0.0-1.0, based on data volume)
pub confidence: f32,
/// When this inference was generated
pub inferred_at: DateTime<Utc>,
}
impl RelationshipInference {
/// Infer relationship from memories and user profile
pub fn infer(
entity_id: String,
memories: &[Memory],
user_profile: &UserProfile,
) -> Self {
// Filter memories related to this entity
let entity_memories: Vec<_> = memories
.iter()
.filter(|m| m.has_entity(&entity_id))
.collect();
let interaction_count = entity_memories.len() as u32;
// Calculate average priority
let total_priority: f32 = entity_memories
.iter()
.filter_map(|m| m.priority_score)
.sum();
let priority_count = entity_memories
.iter()
.filter(|m| m.priority_score.is_some())
.count() as f32;
let avg_priority = if priority_count > 0.0 {
total_priority / priority_count
} else {
0.5 // Default to neutral if no scores
};
// Calculate days since last interaction
let days_since_last = entity_memories
.iter()
.map(|m| (Utc::now() - m.created_at).num_days())
.min()
.unwrap_or(999);
// Infer bond strength based on user personality
let bond_strength = Self::calculate_bond_strength(
interaction_count,
avg_priority,
user_profile,
);
// Infer relationship type
let relationship_type = Self::infer_relationship_type(
interaction_count,
avg_priority,
bond_strength,
);
// Calculate confidence
let confidence = Self::calculate_confidence(interaction_count);
RelationshipInference {
entity_id,
interaction_count,
avg_priority,
days_since_last,
bond_strength,
relationship_type,
confidence,
inferred_at: Utc::now(),
}
}
/// Calculate bond strength from interaction data and user personality
fn calculate_bond_strength(
interaction_count: u32,
avg_priority: f32,
user_profile: &UserProfile,
) -> f32 {
// Extract extraversion score (if available)
let extraversion = user_profile
.dominant_traits
.iter()
.find(|t| t.name == "extraversion")
.map(|t| t.score)
.unwrap_or(0.5);
let bond_strength = if extraversion < 0.5 {
// Introverted: fewer but deeper relationships
// Interaction count matters more
let count_factor = (interaction_count as f32 / 20.0).min(1.0);
let priority_factor = avg_priority;
// Weight: 60% count, 40% priority
count_factor * 0.6 + priority_factor * 0.4
} else {
// Extroverted: many relationships, quality varies
// Priority matters more
let count_factor = (interaction_count as f32 / 50.0).min(1.0);
let priority_factor = avg_priority;
// Weight: 40% count, 60% priority
count_factor * 0.4 + priority_factor * 0.6
};
bond_strength.clamp(0.0, 1.0)
}
/// Infer relationship type from metrics
fn infer_relationship_type(
interaction_count: u32,
avg_priority: f32,
bond_strength: f32,
) -> String {
if bond_strength >= 0.8 {
"close_friend".to_string()
} else if bond_strength >= 0.6 {
"friend".to_string()
} else if bond_strength >= 0.4 {
if avg_priority >= 0.6 {
"valued_acquaintance".to_string()
} else {
"acquaintance".to_string()
}
} else if interaction_count >= 5 {
"regular_contact".to_string()
} else {
"distant".to_string()
}
}
/// Calculate confidence based on data volume
fn calculate_confidence(interaction_count: u32) -> f32 {
// Confidence increases with more data
// 1-2 interactions: low confidence (0.2-0.3)
// 5 interactions: medium confidence (0.5)
// 10+ interactions: high confidence (0.8+)
let confidence = match interaction_count {
0 => 0.0,
1 => 0.2,
2 => 0.3,
3 => 0.4,
4 => 0.45,
5..=9 => 0.5 + (interaction_count - 5) as f32 * 0.05,
_ => 0.8 + ((interaction_count - 10) as f32 * 0.02).min(0.2),
};
confidence.clamp(0.0, 1.0)
}
}
/// Generate relationship inferences for all entities in memories
pub fn infer_all_relationships(
store: &MemoryStore,
) -> Result<Vec<RelationshipInference>> {
// Check cache first
if let Some(cached) = store.get_cached_all_relationships()? {
return Ok(cached);
}
// Get all memories
let memories = store.list()?;
// Get user profile
let user_profile = store.get_profile()?;
// Extract all unique entities
let mut entities: HashMap<String, ()> = HashMap::new();
for memory in &memories {
if let Some(ref entity_list) = memory.related_entities {
for entity in entity_list {
entities.insert(entity.clone(), ());
}
}
}
// Infer relationship for each entity
let mut relationships: Vec<_> = entities
.keys()
.map(|entity_id| {
RelationshipInference::infer(
entity_id.clone(),
&memories,
&user_profile,
)
})
.collect();
// Sort by bond strength (descending)
relationships.sort_by(|a, b| {
b.bond_strength
.partial_cmp(&a.bond_strength)
.unwrap_or(std::cmp::Ordering::Equal)
});
// Cache the result
store.save_all_relationships_cache(&relationships)?;
Ok(relationships)
}
/// Get relationship inference for a specific entity (with caching)
pub fn get_relationship(
store: &MemoryStore,
entity_id: &str,
) -> Result<RelationshipInference> {
// Check cache first
if let Some(cached) = store.get_cached_relationship(entity_id)? {
return Ok(cached);
}
// Get all memories
let memories = store.list()?;
// Get user profile
let user_profile = store.get_profile()?;
// Infer relationship
let relationship = RelationshipInference::infer(
entity_id.to_string(),
&memories,
&user_profile,
);
// Cache it
store.save_relationship_cache(entity_id, &relationship)?;
Ok(relationship)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::profile::TraitScore;
#[test]
fn test_confidence_calculation() {
assert_eq!(RelationshipInference::calculate_confidence(0), 0.0);
assert_eq!(RelationshipInference::calculate_confidence(1), 0.2);
assert_eq!(RelationshipInference::calculate_confidence(5), 0.5);
assert!(RelationshipInference::calculate_confidence(10) >= 0.8);
}
#[test]
fn test_relationship_type() {
assert_eq!(
RelationshipInference::infer_relationship_type(20, 0.9, 0.85),
"close_friend"
);
assert_eq!(
RelationshipInference::infer_relationship_type(10, 0.7, 0.65),
"friend"
);
assert_eq!(
RelationshipInference::infer_relationship_type(5, 0.5, 0.45),
"acquaintance"
);
}
#[test]
fn test_bond_strength_introverted() {
let user_profile = UserProfile {
dominant_traits: vec![
TraitScore {
name: "extraversion".to_string(),
score: 0.3, // Introverted
},
],
core_interests: vec![],
core_values: vec![],
key_memory_ids: vec![],
data_quality: 1.0,
last_updated: Utc::now(),
};
// Introverted: count matters more
let strength = RelationshipInference::calculate_bond_strength(
20, // Many interactions
0.5, // Medium priority
&user_profile,
);
// Should be high due to high interaction count
assert!(strength > 0.5);
}
}

View File

@@ -1,693 +0,0 @@
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection};
use std::path::PathBuf;
use super::analysis::UserAnalysis;
use super::error::{MemoryError, Result};
use super::memory::Memory;
/// SQLite-based memory storage
pub struct MemoryStore {
conn: Connection,
}
impl MemoryStore {
/// Create a new MemoryStore with the given database path
pub fn new(db_path: PathBuf) -> Result<Self> {
// Ensure parent directory exists
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(db_path)?;
// Initialize database schema
conn.execute(
"CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
ai_interpretation TEXT,
priority_score REAL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
[],
)?;
// Migrate existing tables (add columns if they don't exist)
// SQLite doesn't have "IF NOT EXISTS" for columns, so we check first
let has_ai_interpretation: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='ai_interpretation'")?
.query_row([], |row| row.get(0))
.map(|count: i32| count > 0)?;
if !has_ai_interpretation {
conn.execute("ALTER TABLE memories ADD COLUMN ai_interpretation TEXT", [])?;
conn.execute("ALTER TABLE memories ADD COLUMN priority_score REAL", [])?;
}
// Migrate for Layer 4: related_entities
let has_related_entities: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='related_entities'")?
.query_row([], |row| row.get(0))
.map(|count: i32| count > 0)?;
if !has_related_entities {
conn.execute("ALTER TABLE memories ADD COLUMN related_entities TEXT", [])?;
}
// Create indexes for better query performance
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_updated_at ON memories(updated_at)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_priority_score ON memories(priority_score)",
[],
)?;
// Create user_analyses table (Layer 3)
conn.execute(
"CREATE TABLE IF NOT EXISTS user_analyses (
id TEXT PRIMARY KEY,
openness REAL NOT NULL,
conscientiousness REAL NOT NULL,
extraversion REAL NOT NULL,
agreeableness REAL NOT NULL,
neuroticism REAL NOT NULL,
summary TEXT NOT NULL,
analyzed_at TEXT NOT NULL
)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_analyzed_at ON user_analyses(analyzed_at)",
[],
)?;
// Create user_profiles table (Layer 3.5 - integrated profile cache)
conn.execute(
"CREATE TABLE IF NOT EXISTS user_profiles (
id INTEGER PRIMARY KEY CHECK (id = 1),
data TEXT NOT NULL,
last_updated TEXT NOT NULL
)",
[],
)?;
// Create relationship_cache table (Layer 4 - relationship inference cache)
// entity_id = "" for all_relationships cache
conn.execute(
"CREATE TABLE IF NOT EXISTS relationship_cache (
entity_id TEXT PRIMARY KEY,
data TEXT NOT NULL,
cached_at TEXT NOT NULL
)",
[],
)?;
Ok(Self { conn })
}
/// Create a new MemoryStore using default config directory
pub fn default() -> Result<Self> {
let data_dir = dirs::config_dir()
.ok_or_else(|| MemoryError::Config("Could not find config directory".to_string()))?
.join("syui")
.join("ai")
.join("gpt");
let db_path = data_dir.join("memory.db");
Self::new(db_path)
}
/// Insert a new memory
pub fn create(&self, memory: &Memory) -> Result<()> {
let related_entities_json = memory.related_entities
.as_ref()
.map(|entities| serde_json::to_string(entities).ok())
.flatten();
self.conn.execute(
"INSERT INTO memories (id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
&memory.id,
&memory.content,
&memory.ai_interpretation,
&memory.priority_score,
related_entities_json,
memory.created_at.to_rfc3339(),
memory.updated_at.to_rfc3339(),
],
)?;
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
/// Get a memory by ID
pub fn get(&self, id: &str) -> Result<Memory> {
let mut stmt = self
.conn
.prepare("SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories WHERE id = ?1")?;
let memory = stmt.query_row(params![id], |row| {
let created_at: String = row.get(5)?;
let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
6,
rusqlite::types::Type::Text,
Box::new(e),
))?,
})
})?;
Ok(memory)
}
/// Update an existing memory
pub fn update(&self, memory: &Memory) -> Result<()> {
let related_entities_json = memory.related_entities
.as_ref()
.map(|entities| serde_json::to_string(entities).ok())
.flatten();
let rows_affected = self.conn.execute(
"UPDATE memories SET content = ?1, ai_interpretation = ?2, priority_score = ?3, related_entities = ?4, updated_at = ?5
WHERE id = ?6",
params![
&memory.content,
&memory.ai_interpretation,
&memory.priority_score,
related_entities_json,
memory.updated_at.to_rfc3339(),
&memory.id,
],
)?;
if rows_affected == 0 {
return Err(MemoryError::NotFound(memory.id.clone()));
}
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
/// Delete a memory by ID
pub fn delete(&self, id: &str) -> Result<()> {
let rows_affected = self
.conn
.execute("DELETE FROM memories WHERE id = ?1", params![id])?;
if rows_affected == 0 {
return Err(MemoryError::NotFound(id.to_string()));
}
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
/// List all memories, ordered by creation time (newest first)
pub fn list(&self) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare(
"SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories ORDER BY created_at DESC",
)?;
let memories = stmt
.query_map([], |row| {
let created_at: String = row.get(5)?;
let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
6,
rusqlite::types::Type::Text,
Box::new(e),
))?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(memories)
}
/// Search memories by content or AI interpretation (case-insensitive)
pub fn search(&self, query: &str) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare(
"SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories
WHERE content LIKE ?1 OR ai_interpretation LIKE ?1
ORDER BY created_at DESC",
)?;
let search_pattern = format!("%{}%", query);
let memories = stmt
.query_map(params![search_pattern], |row| {
let created_at: String = row.get(5)?;
let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
6,
rusqlite::types::Type::Text,
Box::new(e),
))?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(memories)
}
/// Count total memories
pub fn count(&self) -> Result<usize> {
let count: usize = self
.conn
.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))?;
Ok(count)
}
// ========== Layer 3: User Analysis Methods ==========
/// Save a new user personality analysis
pub fn save_analysis(&self, analysis: &UserAnalysis) -> Result<()> {
self.conn.execute(
"INSERT INTO user_analyses (id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
&analysis.id,
&analysis.openness,
&analysis.conscientiousness,
&analysis.extraversion,
&analysis.agreeableness,
&analysis.neuroticism,
&analysis.summary,
analysis.analyzed_at.to_rfc3339(),
],
)?;
Ok(())
}
/// Get the most recent user analysis
pub fn get_latest_analysis(&self) -> Result<Option<UserAnalysis>> {
let mut stmt = self.conn.prepare(
"SELECT id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at
FROM user_analyses
ORDER BY analyzed_at DESC
LIMIT 1",
)?;
let result = stmt.query_row([], |row| {
let analyzed_at: String = row.get(7)?;
Ok(UserAnalysis {
id: row.get(0)?,
openness: row.get(1)?,
conscientiousness: row.get(2)?,
extraversion: row.get(3)?,
agreeableness: row.get(4)?,
neuroticism: row.get(5)?,
summary: row.get(6)?,
analyzed_at: DateTime::parse_from_rfc3339(&analyzed_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
7,
rusqlite::types::Type::Text,
Box::new(e),
)
})?,
})
});
match result {
Ok(analysis) => Ok(Some(analysis)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Get all user analyses, ordered by date (newest first)
pub fn list_analyses(&self) -> Result<Vec<UserAnalysis>> {
let mut stmt = self.conn.prepare(
"SELECT id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at
FROM user_analyses
ORDER BY analyzed_at DESC",
)?;
let analyses = stmt
.query_map([], |row| {
let analyzed_at: String = row.get(7)?;
Ok(UserAnalysis {
id: row.get(0)?,
openness: row.get(1)?,
conscientiousness: row.get(2)?,
extraversion: row.get(3)?,
agreeableness: row.get(4)?,
neuroticism: row.get(5)?,
summary: row.get(6)?,
analyzed_at: DateTime::parse_from_rfc3339(&analyzed_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
7,
rusqlite::types::Type::Text,
Box::new(e),
)
})?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(analyses)
}
// === Layer 3.5: Integrated Profile ===
/// Save integrated profile to cache
pub fn save_profile(&self, profile: &super::profile::UserProfile) -> Result<()> {
let profile_json = serde_json::to_string(profile)?;
self.conn.execute(
"INSERT OR REPLACE INTO user_profiles (id, data, last_updated) VALUES (1, ?1, ?2)",
params![profile_json, profile.last_updated.to_rfc3339()],
)?;
Ok(())
}
/// Get cached profile if exists
pub fn get_cached_profile(&self) -> Result<Option<super::profile::UserProfile>> {
let mut stmt = self
.conn
.prepare("SELECT data FROM user_profiles WHERE id = 1")?;
let result = stmt.query_row([], |row| {
let json: String = row.get(0)?;
Ok(json)
});
match result {
Ok(json) => {
let profile: super::profile::UserProfile = serde_json::from_str(&json)?;
Ok(Some(profile))
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Get or generate profile (with automatic caching)
pub fn get_profile(&self) -> Result<super::profile::UserProfile> {
// Check cache first
if let Some(cached) = self.get_cached_profile()? {
// Check if needs update
if !cached.needs_update(self)? {
return Ok(cached);
}
}
// Generate new profile
let profile = super::profile::UserProfile::generate(self)?;
// Cache it
self.save_profile(&profile)?;
Ok(profile)
}
// ========== Layer 4: Relationship Cache Methods ==========
/// Cache duration in minutes
const RELATIONSHIP_CACHE_DURATION_MINUTES: i64 = 5;
/// Save relationship inference to cache
pub fn save_relationship_cache(
&self,
entity_id: &str,
relationship: &super::relationship::RelationshipInference,
) -> Result<()> {
let data = serde_json::to_string(relationship)?;
let cached_at = Utc::now().to_rfc3339();
self.conn.execute(
"INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES (?1, ?2, ?3)",
params![entity_id, data, cached_at],
)?;
Ok(())
}
/// Get cached relationship inference
pub fn get_cached_relationship(
&self,
entity_id: &str,
) -> Result<Option<super::relationship::RelationshipInference>> {
let mut stmt = self
.conn
.prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ?1")?;
let result = stmt.query_row([entity_id], |row| {
let data: String = row.get(0)?;
let cached_at: String = row.get(1)?;
Ok((data, cached_at))
});
match result {
Ok((data, cached_at_str)) => {
// Check if cache is still valid (within 5 minutes)
let cached_at = DateTime::parse_from_rfc3339(&cached_at_str)
.map_err(|e| MemoryError::Parse(e.to_string()))?
.with_timezone(&Utc);
let age_minutes = (Utc::now() - cached_at).num_seconds() / 60;
if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES {
let relationship: super::relationship::RelationshipInference =
serde_json::from_str(&data)?;
Ok(Some(relationship))
} else {
// Cache expired
Ok(None)
}
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Save all relationships list to cache (use empty string as entity_id)
pub fn save_all_relationships_cache(
&self,
relationships: &[super::relationship::RelationshipInference],
) -> Result<()> {
let data = serde_json::to_string(relationships)?;
let cached_at = Utc::now().to_rfc3339();
self.conn.execute(
"INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES ('', ?1, ?2)",
params![data, cached_at],
)?;
Ok(())
}
/// Get cached all relationships list
pub fn get_cached_all_relationships(
&self,
) -> Result<Option<Vec<super::relationship::RelationshipInference>>> {
let mut stmt = self
.conn
.prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ''")?;
let result = stmt.query_row([], |row| {
let data: String = row.get(0)?;
let cached_at: String = row.get(1)?;
Ok((data, cached_at))
});
match result {
Ok((data, cached_at_str)) => {
let cached_at = DateTime::parse_from_rfc3339(&cached_at_str)
.map_err(|e| MemoryError::Parse(e.to_string()))?
.with_timezone(&Utc);
let age_minutes = (Utc::now() - cached_at).num_seconds() / 60;
if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES {
let relationships: Vec<super::relationship::RelationshipInference> =
serde_json::from_str(&data)?;
Ok(Some(relationships))
} else {
Ok(None)
}
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Clear all relationship caches (call when memories are modified)
pub fn clear_relationship_cache(&self) -> Result<()> {
self.conn.execute("DELETE FROM relationship_cache", [])?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_store() -> MemoryStore {
MemoryStore::new(":memory:".into()).unwrap()
}
#[test]
fn test_create_and_get() {
let store = create_test_store();
let memory = Memory::new("Test content".to_string());
store.create(&memory).unwrap();
let retrieved = store.get(&memory.id).unwrap();
assert_eq!(retrieved.id, memory.id);
assert_eq!(retrieved.content, memory.content);
}
#[test]
fn test_update() {
let store = create_test_store();
let mut memory = Memory::new("Original".to_string());
store.create(&memory).unwrap();
memory.update_content("Updated".to_string());
store.update(&memory).unwrap();
let retrieved = store.get(&memory.id).unwrap();
assert_eq!(retrieved.content, "Updated");
}
#[test]
fn test_delete() {
let store = create_test_store();
let memory = Memory::new("To delete".to_string());
store.create(&memory).unwrap();
store.delete(&memory.id).unwrap();
assert!(store.get(&memory.id).is_err());
}
#[test]
fn test_list() {
let store = create_test_store();
let mem1 = Memory::new("First".to_string());
let mem2 = Memory::new("Second".to_string());
store.create(&mem1).unwrap();
store.create(&mem2).unwrap();
let memories = store.list().unwrap();
assert_eq!(memories.len(), 2);
}
#[test]
fn test_search() {
let store = create_test_store();
store
.create(&Memory::new("Hello world".to_string()))
.unwrap();
store
.create(&Memory::new("Goodbye world".to_string()))
.unwrap();
store.create(&Memory::new("Testing".to_string())).unwrap();
let results = store.search("world").unwrap();
assert_eq!(results.len(), 2);
let results = store.search("Hello").unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn test_count() {
let store = create_test_store();
assert_eq!(store.count().unwrap(), 0);
store.create(&Memory::new("Test".to_string())).unwrap();
assert_eq!(store.count().unwrap(), 1);
}
}

View File

@@ -1,2 +0,0 @@
pub mod core;
pub mod mcp;

View File

@@ -1,141 +0,0 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use aigpt::core::{Memory, MemoryStore};
use aigpt::mcp::BaseMCPServer;
#[derive(Parser)]
#[command(name = "aigpt")]
#[command(about = "Simple memory storage for Claude with MCP - Layer 1")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Start MCP server
Server {
/// Enable Layer 4 relationship features (for games/companions)
#[arg(long)]
enable_layer4: bool,
},
/// Create a new memory
Create {
/// Content of the memory
content: String,
},
/// Get a memory by ID
Get {
/// Memory ID
id: String,
},
/// Update a memory
Update {
/// Memory ID
id: String,
/// New content
content: String,
},
/// Delete a memory
Delete {
/// Memory ID
id: String,
},
/// List all memories
List,
/// Search memories by content
Search {
/// Search query
query: String,
},
/// Show statistics
Stats,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Server { enable_layer4 } => {
let server = BaseMCPServer::new(enable_layer4)?;
server.run()?;
}
Commands::Create { content } => {
let store = MemoryStore::default()?;
let memory = Memory::new(content);
store.create(&memory)?;
println!("Created memory: {}", memory.id);
}
Commands::Get { id } => {
let store = MemoryStore::default()?;
let memory = store.get(&id)?;
println!("ID: {}", memory.id);
println!("Content: {}", memory.content);
println!("Created: {}", memory.created_at);
println!("Updated: {}", memory.updated_at);
}
Commands::Update { id, content } => {
let store = MemoryStore::default()?;
let mut memory = store.get(&id)?;
memory.update_content(content);
store.update(&memory)?;
println!("Updated memory: {}", memory.id);
}
Commands::Delete { id } => {
let store = MemoryStore::default()?;
store.delete(&id)?;
println!("Deleted memory: {}", id);
}
Commands::List => {
let store = MemoryStore::default()?;
let memories = store.list()?;
if memories.is_empty() {
println!("No memories found");
} else {
for memory in memories {
println!("\n[{}]", memory.id);
println!(" {}", memory.content);
println!(" Created: {}", memory.created_at);
}
}
}
Commands::Search { query } => {
let store = MemoryStore::default()?;
let memories = store.search(&query)?;
if memories.is_empty() {
println!("No memories found matching '{}'", query);
} else {
println!("Found {} memory(ies):", memories.len());
for memory in memories {
println!("\n[{}]", memory.id);
println!(" {}", memory.content);
println!(" Created: {}", memory.created_at);
}
}
}
Commands::Stats => {
let store = MemoryStore::default()?;
let count = store.count()?;
println!("Total memories: {}", count);
}
}
Ok(())
}

View File

@@ -1,648 +0,0 @@
use anyhow::Result;
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};
use crate::core::{Memory, MemoryStore, UserAnalysis, infer_all_relationships, get_relationship};
pub struct BaseMCPServer {
store: MemoryStore,
enable_layer4: bool,
}
impl BaseMCPServer {
pub fn new(enable_layer4: bool) -> Result<Self> {
let store = MemoryStore::default()?;
Ok(BaseMCPServer { store, enable_layer4 })
}
pub fn run(&self) -> Result<()> {
let stdin = io::stdin();
let mut stdout = io::stdout();
let reader = stdin.lock();
let lines = reader.lines();
for line_result in lines {
match line_result {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(request) = serde_json::from_str::<Value>(&trimmed) {
let response = self.handle_request(request);
let response_str = serde_json::to_string(&response)?;
stdout.write_all(response_str.as_bytes())?;
stdout.write_all(b"\n")?;
stdout.flush()?;
}
}
Err(_) => break,
}
}
Ok(())
}
fn handle_request(&self, request: Value) -> Value {
let method = request["method"].as_str().unwrap_or("");
let id = request["id"].clone();
match method {
"initialize" => self.handle_initialize(id),
"tools/list" => self.handle_tools_list(id),
"tools/call" => self.handle_tools_call(request, id),
_ => self.handle_unknown_method(id),
}
}
fn handle_initialize(&self, id: Value) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "aigpt",
"version": "0.2.0"
}
}
})
}
fn handle_tools_list(&self, id: Value) -> Value {
let tools = self.get_available_tools();
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"tools": tools
}
})
}
fn get_available_tools(&self) -> Vec<Value> {
let mut tools = vec![
json!({
"name": "create_memory",
"description": "Create a new memory entry (Layer 1: simple storage)",
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Content of the memory"
}
},
"required": ["content"]
}
}),
json!({
"name": "create_ai_memory",
"description": "Create a memory with AI interpretation and priority score (Layer 2)",
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Original content of the memory"
},
"ai_interpretation": {
"type": "string",
"description": "AI's creative interpretation of the content (optional)"
},
"priority_score": {
"type": "number",
"description": "Priority score from 0.0 (low) to 1.0 (high) (optional)",
"minimum": 0.0,
"maximum": 1.0
}
},
"required": ["content"]
}
}),
json!({
"name": "get_memory",
"description": "Get a memory by ID",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Memory ID"
}
},
"required": ["id"]
}
}),
json!({
"name": "search_memories",
"description": "Search memories by content",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
}
},
"required": ["query"]
}
}),
json!({
"name": "list_memories",
"description": "List all memories",
"inputSchema": {
"type": "object",
"properties": {}
}
}),
json!({
"name": "update_memory",
"description": "Update an existing memory entry",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the memory to update"
},
"content": {
"type": "string",
"description": "New content for the memory"
}
},
"required": ["id", "content"]
}
}),
json!({
"name": "delete_memory",
"description": "Delete a memory entry",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the memory to delete"
}
},
"required": ["id"]
}
}),
json!({
"name": "save_user_analysis",
"description": "Save a Big Five personality analysis based on user's memories (Layer 3)",
"inputSchema": {
"type": "object",
"properties": {
"openness": {
"type": "number",
"description": "Openness to Experience (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"conscientiousness": {
"type": "number",
"description": "Conscientiousness (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"extraversion": {
"type": "number",
"description": "Extraversion (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"agreeableness": {
"type": "number",
"description": "Agreeableness (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"neuroticism": {
"type": "number",
"description": "Neuroticism (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"summary": {
"type": "string",
"description": "AI-generated summary of the personality analysis"
}
},
"required": ["openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism", "summary"]
}
}),
json!({
"name": "get_user_analysis",
"description": "Get the most recent Big Five personality analysis (Layer 3)",
"inputSchema": {
"type": "object",
"properties": {}
}
}),
json!({
"name": "get_profile",
"description": "Get integrated user profile - the essential summary of personality, interests, and values (Layer 3.5). This is the primary tool for understanding the user.",
"inputSchema": {
"type": "object",
"properties": {}
}
}),
];
// Layer 4 tools (optional - only when enabled)
if self.enable_layer4 {
tools.extend(vec![
json!({
"name": "get_relationship",
"description": "Get inferred relationship with a specific entity (Layer 4). Analyzes memories and user profile to infer bond strength and relationship type. Use only when game/relationship features are active.",
"inputSchema": {
"type": "object",
"properties": {
"entity_id": {
"type": "string",
"description": "Entity identifier (e.g., 'alice', 'companion_miku')"
}
},
"required": ["entity_id"]
}
}),
json!({
"name": "list_relationships",
"description": "List all inferred relationships sorted by bond strength (Layer 4). Returns relationships with all tracked entities. Use only when game/relationship features are active.",
"inputSchema": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "Maximum number of relationships to return (default: 10)"
}
}
}
}),
]);
}
tools
}
fn handle_tools_call(&self, request: Value, id: Value) -> Value {
let tool_name = request["params"]["name"].as_str().unwrap_or("");
let arguments = &request["params"]["arguments"];
let result = self.execute_tool(tool_name, arguments);
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"content": [{
"type": "text",
"text": result.to_string()
}]
}
})
}
fn execute_tool(&self, tool_name: &str, arguments: &Value) -> Value {
match tool_name {
"create_memory" => self.tool_create_memory(arguments),
"create_ai_memory" => self.tool_create_ai_memory(arguments),
"get_memory" => self.tool_get_memory(arguments),
"search_memories" => self.tool_search_memories(arguments),
"list_memories" => self.tool_list_memories(),
"update_memory" => self.tool_update_memory(arguments),
"delete_memory" => self.tool_delete_memory(arguments),
"save_user_analysis" => self.tool_save_user_analysis(arguments),
"get_user_analysis" => self.tool_get_user_analysis(),
"get_profile" => self.tool_get_profile(),
// Layer 4 tools (require --enable-layer4 flag)
"get_relationship" | "list_relationships" => {
if !self.enable_layer4 {
return json!({
"success": false,
"error": "Layer 4 is not enabled. Start server with --enable-layer4 flag to use relationship features."
});
}
match tool_name {
"get_relationship" => self.tool_get_relationship(arguments),
"list_relationships" => self.tool_list_relationships(arguments),
_ => unreachable!(),
}
}
_ => json!({
"success": false,
"error": format!("Unknown tool: {}", tool_name)
}),
}
}
fn tool_create_memory(&self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let memory = Memory::new(content.to_string());
match self.store.create(&memory) {
Ok(()) => json!({
"success": true,
"id": memory.id,
"message": "Memory created successfully"
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_create_ai_memory(&self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let ai_interpretation = arguments["ai_interpretation"]
.as_str()
.map(|s| s.to_string());
let priority_score = arguments["priority_score"].as_f64().map(|f| f as f32);
let memory = Memory::new_ai(content.to_string(), ai_interpretation, priority_score);
match self.store.create(&memory) {
Ok(()) => json!({
"success": true,
"id": memory.id,
"message": "AI memory created successfully",
"has_interpretation": memory.ai_interpretation.is_some(),
"has_score": memory.priority_score.is_some()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_get_memory(&self, arguments: &Value) -> Value {
let id = arguments["id"].as_str().unwrap_or("");
match self.store.get(id) {
Ok(memory) => json!({
"success": true,
"memory": {
"id": memory.id,
"content": memory.content,
"ai_interpretation": memory.ai_interpretation,
"priority_score": memory.priority_score,
"created_at": memory.created_at,
"updated_at": memory.updated_at
}
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_search_memories(&self, arguments: &Value) -> Value {
let query = arguments["query"].as_str().unwrap_or("");
match self.store.search(query) {
Ok(memories) => json!({
"success": true,
"memories": memories.into_iter().map(|m| json!({
"id": m.id,
"content": m.content,
"ai_interpretation": m.ai_interpretation,
"priority_score": m.priority_score,
"created_at": m.created_at,
"updated_at": m.updated_at
})).collect::<Vec<_>>()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_list_memories(&self) -> Value {
match self.store.list() {
Ok(memories) => json!({
"success": true,
"memories": memories.into_iter().map(|m| json!({
"id": m.id,
"content": m.content,
"ai_interpretation": m.ai_interpretation,
"priority_score": m.priority_score,
"created_at": m.created_at,
"updated_at": m.updated_at
})).collect::<Vec<_>>()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_update_memory(&self, arguments: &Value) -> Value {
let id = arguments["id"].as_str().unwrap_or("");
let content = arguments["content"].as_str().unwrap_or("");
match self.store.get(id) {
Ok(mut memory) => {
memory.update_content(content.to_string());
match self.store.update(&memory) {
Ok(()) => json!({
"success": true,
"message": "Memory updated successfully"
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_delete_memory(&self, arguments: &Value) -> Value {
let id = arguments["id"].as_str().unwrap_or("");
match self.store.delete(id) {
Ok(()) => json!({
"success": true,
"message": "Memory deleted successfully"
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
// ========== Layer 3: User Analysis Tools ==========
fn tool_save_user_analysis(&self, arguments: &Value) -> Value {
let openness = arguments["openness"].as_f64().unwrap_or(0.5) as f32;
let conscientiousness = arguments["conscientiousness"].as_f64().unwrap_or(0.5) as f32;
let extraversion = arguments["extraversion"].as_f64().unwrap_or(0.5) as f32;
let agreeableness = arguments["agreeableness"].as_f64().unwrap_or(0.5) as f32;
let neuroticism = arguments["neuroticism"].as_f64().unwrap_or(0.5) as f32;
let summary = arguments["summary"].as_str().unwrap_or("").to_string();
let analysis = UserAnalysis::new(
openness,
conscientiousness,
extraversion,
agreeableness,
neuroticism,
summary,
);
match self.store.save_analysis(&analysis) {
Ok(()) => json!({
"success": true,
"id": analysis.id,
"message": "User analysis saved successfully",
"dominant_trait": analysis.dominant_trait()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_get_user_analysis(&self) -> Value {
match self.store.get_latest_analysis() {
Ok(Some(analysis)) => json!({
"success": true,
"analysis": {
"id": analysis.id,
"openness": analysis.openness,
"conscientiousness": analysis.conscientiousness,
"extraversion": analysis.extraversion,
"agreeableness": analysis.agreeableness,
"neuroticism": analysis.neuroticism,
"summary": analysis.summary,
"dominant_trait": analysis.dominant_trait(),
"analyzed_at": analysis.analyzed_at
}
}),
Ok(None) => json!({
"success": true,
"analysis": null,
"message": "No analysis found. Run personality analysis first."
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_get_profile(&self) -> Value {
match self.store.get_profile() {
Ok(profile) => json!({
"success": true,
"profile": {
"dominant_traits": profile.dominant_traits,
"core_interests": profile.core_interests,
"core_values": profile.core_values,
"key_memory_ids": profile.key_memory_ids,
"data_quality": profile.data_quality,
"last_updated": profile.last_updated
}
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_get_relationship(&self, arguments: &Value) -> Value {
let entity_id = arguments["entity_id"].as_str().unwrap_or("");
if entity_id.is_empty() {
return json!({
"success": false,
"error": "entity_id is required"
});
}
// Get relationship (with caching)
match get_relationship(&self.store, entity_id) {
Ok(relationship) => json!({
"success": true,
"relationship": {
"entity_id": relationship.entity_id,
"interaction_count": relationship.interaction_count,
"avg_priority": relationship.avg_priority,
"days_since_last": relationship.days_since_last,
"bond_strength": relationship.bond_strength,
"relationship_type": relationship.relationship_type,
"confidence": relationship.confidence,
"inferred_at": relationship.inferred_at
}
}),
Err(e) => json!({
"success": false,
"error": format!("Failed to get relationship: {}", e)
}),
}
}
fn tool_list_relationships(&self, arguments: &Value) -> Value {
let limit = arguments["limit"].as_u64().unwrap_or(10) as usize;
match infer_all_relationships(&self.store) {
Ok(mut relationships) => {
// Limit results
if relationships.len() > limit {
relationships.truncate(limit);
}
json!({
"success": true,
"relationships": relationships.iter().map(|r| {
json!({
"entity_id": r.entity_id,
"interaction_count": r.interaction_count,
"avg_priority": r.avg_priority,
"days_since_last": r.days_since_last,
"bond_strength": r.bond_strength,
"relationship_type": r.relationship_type,
"confidence": r.confidence
})
}).collect::<Vec<_>>()
})
}
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn handle_unknown_method(&self, id: Value) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": "Method not found"
}
})
}
}

View File

@@ -1,3 +0,0 @@
pub mod base;
pub use base::BaseMCPServer;

View File

@@ -1,36 +0,0 @@
use anyhow::Result;
/// AIInterpreter - Claude Code による解釈を期待する軽量ラッパー
///
/// このモジュールは外部 AI API を呼び出しません。
/// 代わりに、Claude Code 自身がコンテンツを解釈し、スコアを計算することを期待します。
///
/// 完全にローカルで動作し、API コストはゼロです。
pub struct AIInterpreter;
impl AIInterpreter {
pub fn new() -> Self {
AIInterpreter
}
/// コンテンツをそのまま返すClaude Code が解釈を担当)
pub async fn interpret_content(&self, content: &str) -> Result<String> {
Ok(content.to_string())
}
/// デフォルトスコアを返すClaude Code が実際のスコアを決定)
pub async fn calculate_priority_score(&self, _content: &str, _user_context: Option<&str>) -> Result<f32> {
Ok(0.5) // デフォルト値
}
/// 解釈とスコアリングを Claude Code に委ねる
pub async fn analyze(&self, content: &str, _user_context: Option<&str>) -> Result<(String, f32)> {
Ok((content.to_string(), 0.5))
}
}
impl Default for AIInterpreter {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,433 +0,0 @@
use crate::memory::Memory;
use crate::game_formatter::{MemoryRarity, DiagnosisType};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc, Datelike};
/// コンパニオンキャラクター
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Companion {
pub name: String,
pub personality: CompanionPersonality,
pub relationship_level: u32, // レベル(経験値で上昇)
pub affection_score: f32, // 好感度 (0.0-1.0)
pub trust_level: u32, // 信頼度 (0-100)
pub total_xp: u32, // 総XP
pub last_interaction: DateTime<Utc>,
pub shared_memories: Vec<String>, // 共有された記憶のID
}
/// コンパニオンの性格
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompanionPersonality {
Energetic, // 元気で冒険好き - 革新者と相性◎
Intellectual, // 知的で思慮深い - 哲学者と相性◎
Practical, // 現実的で頼れる - 実務家と相性◎
Dreamy, // 夢見がちでロマンチック - 夢想家と相性◎
Balanced, // バランス型 - 分析家と相性◎
}
impl CompanionPersonality {
pub fn emoji(&self) -> &str {
match self {
CompanionPersonality::Energetic => "",
CompanionPersonality::Intellectual => "📚",
CompanionPersonality::Practical => "🎯",
CompanionPersonality::Dreamy => "🌙",
CompanionPersonality::Balanced => "⚖️",
}
}
pub fn name(&self) -> &str {
match self {
CompanionPersonality::Energetic => "元気で冒険好き",
CompanionPersonality::Intellectual => "知的で思慮深い",
CompanionPersonality::Practical => "現実的で頼れる",
CompanionPersonality::Dreamy => "夢見がちでロマンチック",
CompanionPersonality::Balanced => "バランス型",
}
}
/// ユーザーの診断タイプとの相性
pub fn compatibility(&self, user_type: &DiagnosisType) -> f32 {
match (self, user_type) {
(CompanionPersonality::Energetic, DiagnosisType::Innovator) => 0.95,
(CompanionPersonality::Intellectual, DiagnosisType::Philosopher) => 0.95,
(CompanionPersonality::Practical, DiagnosisType::Pragmatist) => 0.95,
(CompanionPersonality::Dreamy, DiagnosisType::Visionary) => 0.95,
(CompanionPersonality::Balanced, DiagnosisType::Analyst) => 0.95,
// その他の組み合わせ
_ => 0.7,
}
}
}
impl Companion {
pub fn new(name: String, personality: CompanionPersonality) -> Self {
Companion {
name,
personality,
relationship_level: 1,
affection_score: 0.0,
trust_level: 0,
total_xp: 0,
last_interaction: Utc::now(),
shared_memories: Vec::new(),
}
}
/// 記憶を共有して反応を得る
pub fn react_to_memory(&mut self, memory: &Memory, user_type: &DiagnosisType) -> CompanionReaction {
let rarity = MemoryRarity::from_score(memory.priority_score);
let xp = rarity.xp_value();
// XPを加算
self.total_xp += xp;
// 好感度上昇(スコアと相性による)
let compatibility = self.personality.compatibility(user_type);
let affection_gain = memory.priority_score * compatibility * 0.1;
self.affection_score = (self.affection_score + affection_gain).min(1.0);
// 信頼度上昇(高スコアの記憶ほど上昇)
if memory.priority_score >= 0.8 {
self.trust_level = (self.trust_level + 5).min(100);
}
// レベルアップチェック
let old_level = self.relationship_level;
self.relationship_level = (self.total_xp / 1000) + 1;
let level_up = self.relationship_level > old_level;
// 記憶を共有リストに追加
if memory.priority_score >= 0.6 {
self.shared_memories.push(memory.id.clone());
}
self.last_interaction = Utc::now();
// 反応メッセージを生成
let message = self.generate_reaction_message(memory, &rarity, user_type);
CompanionReaction {
message,
affection_gained: affection_gain,
xp_gained: xp,
level_up,
new_level: self.relationship_level,
current_affection: self.affection_score,
special_event: self.check_special_event(),
}
}
/// 記憶に基づく反応メッセージを生成
fn generate_reaction_message(&self, memory: &Memory, rarity: &MemoryRarity, _user_type: &DiagnosisType) -> String {
let content_preview = if memory.content.len() > 50 {
format!("{}...", &memory.content[..50])
} else {
memory.content.clone()
};
match (rarity, &self.personality) {
// LEGENDARY反応
(MemoryRarity::Legendary, CompanionPersonality::Energetic) => {
format!(
"すごい!「{}」って本当に素晴らしいアイデアだね!\n\
一緒に実現させよう!ワクワクするよ!",
content_preview
)
}
(MemoryRarity::Legendary, CompanionPersonality::Intellectual) => {
format!(
"{}」という考え、とても興味深いわ。\n\
深い洞察力を感じるの。もっと詳しく聞かせて?",
content_preview
)
}
(MemoryRarity::Legendary, CompanionPersonality::Practical) => {
format!(
"{}」か。実現可能性が高そうだね。\n\
具体的な計画を一緒に立てようよ。",
content_preview
)
}
(MemoryRarity::Legendary, CompanionPersonality::Dreamy) => {
format!(
"{}」...素敵♪ まるで夢みたい。\n\
あなたの想像力、本当に好きよ。",
content_preview
)
}
// EPIC反応
(MemoryRarity::Epic, _) => {
format!(
"おお、「{}」って面白いね!\n\
あなたのそういうところ、好きだな。",
content_preview
)
}
// RARE反応
(MemoryRarity::Rare, _) => {
format!(
"{}」か。なるほどね。\n\
そういう視点、参考になるよ。",
content_preview
)
}
// 通常反応
_ => {
format!(
"{}」について考えてるんだね。\n\
いつも色々考えてて尊敬するよ。",
content_preview
)
}
}
}
/// スペシャルイベントチェック
fn check_special_event(&self) -> Option<SpecialEvent> {
// 好感度MAXイベント
if self.affection_score >= 1.0 {
return Some(SpecialEvent::MaxAffection);
}
// レベル10到達
if self.relationship_level == 10 {
return Some(SpecialEvent::Level10);
}
// 信頼度MAX
if self.trust_level >= 100 {
return Some(SpecialEvent::MaxTrust);
}
None
}
/// デイリーメッセージを生成
pub fn generate_daily_message(&self) -> String {
let messages = match &self.personality {
CompanionPersonality::Energetic => vec![
"おはよう!今日は何か面白いことある?",
"ねえねえ、今日は一緒に新しいことやろうよ!",
"今日も元気出していこー!",
],
CompanionPersonality::Intellectual => vec![
"おはよう。今日はどんな発見があるかしら?",
"最近読んだ本の話、聞かせてくれない?",
"今日も一緒に学びましょう。",
],
CompanionPersonality::Practical => vec![
"おはよう。今日の予定は?",
"やることリスト、一緒に確認しようか。",
"今日も効率よくいこうね。",
],
CompanionPersonality::Dreamy => vec![
"おはよう...まだ夢の続き見てたの。",
"今日はどんな素敵なことが起こるかな♪",
"あなたと過ごす時間、大好き。",
],
CompanionPersonality::Balanced => vec![
"おはよう。今日も頑張ろうね。",
"何か手伝えることある?",
"今日も一緒にいられて嬉しいよ。",
],
};
let today = chrono::Utc::now().ordinal();
messages[today as usize % messages.len()].to_string()
}
}
/// コンパニオンの反応
#[derive(Debug, Serialize)]
pub struct CompanionReaction {
pub message: String,
pub affection_gained: f32,
pub xp_gained: u32,
pub level_up: bool,
pub new_level: u32,
pub current_affection: f32,
pub special_event: Option<SpecialEvent>,
}
/// スペシャルイベント
#[derive(Debug, Serialize)]
pub enum SpecialEvent {
MaxAffection, // 好感度MAX
Level10, // レベル10到達
MaxTrust, // 信頼度MAX
FirstDate, // 初デート
Confession, // 告白
}
impl SpecialEvent {
pub fn message(&self, companion_name: &str) -> String {
match self {
SpecialEvent::MaxAffection => {
format!(
"💕 特別なイベント発生!\n\n\
{}:「ねえ...あのね。\n\
いつも一緒にいてくれてありがとう。\n\
あなたのこと、すごく大切に思ってるの。\n\
これからも、ずっと一緒にいてね?」\n\n\
🎊 {} の好感度がMAXになりました",
companion_name, companion_name
)
}
SpecialEvent::Level10 => {
format!(
"🎉 レベル10到達\n\n\
{}:「ここまで一緒に来られたね。\n\
あなたとなら、どこまでも行けそう。」",
companion_name
)
}
SpecialEvent::MaxTrust => {
format!(
"✨ 信頼度MAX\n\n\
{}:「あなたのこと、心から信頼してる。\n\
何でも話せるって、すごく嬉しいよ。」",
companion_name
)
}
SpecialEvent::FirstDate => {
format!(
"💐 初デートイベント!\n\n\
{}:「今度、二人でどこか行かない?」",
companion_name
)
}
SpecialEvent::Confession => {
format!(
"💝 告白イベント!\n\n\
{}:「好きです。付き合ってください。」",
companion_name
)
}
}
}
}
/// コンパニオンフォーマッター
pub struct CompanionFormatter;
impl CompanionFormatter {
/// 反応を表示
pub fn format_reaction(companion: &Companion, reaction: &CompanionReaction) -> String {
let affection_bar = Self::format_affection_bar(reaction.current_affection);
let level_up_text = if reaction.level_up {
format!("\n🎊 レベルアップ! Lv.{} → Lv.{}", reaction.new_level - 1, reaction.new_level)
} else {
String::new()
};
let special_event_text = if let Some(ref event) = reaction.special_event {
format!("\n\n{}", event.message(&companion.name))
} else {
String::new()
};
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 💕 {} の反応 ║
╚══════════════════════════════════════════════════════════════╝
{} {}:
「{}」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💕 好感度: {} (+{:.1}%)
💎 XP獲得: +{} XP{}
🏆 レベル: Lv.{}
🤝 信頼度: {} / 100
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}
"#,
companion.name,
companion.personality.emoji(),
companion.name,
reaction.message,
affection_bar,
reaction.affection_gained * 100.0,
reaction.xp_gained,
level_up_text,
companion.relationship_level,
companion.trust_level,
special_event_text
)
}
/// プロフィール表示
pub fn format_profile(companion: &Companion) -> String {
let affection_bar = Self::format_affection_bar(companion.affection_score);
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 💕 {} のプロフィール ║
╚══════════════════════════════════════════════════════════════╝
{} 性格: {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 ステータス
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 関係レベル: Lv.{}
💕 好感度: {}
🤝 信頼度: {} / 100
💎 総XP: {} XP
📚 共有記憶: {}個
🕐 最終交流: {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💬 今日のひとこと:
「{}」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"#,
companion.name,
companion.personality.emoji(),
companion.personality.name(),
companion.relationship_level,
affection_bar,
companion.trust_level,
companion.total_xp,
companion.shared_memories.len(),
companion.last_interaction.format("%Y-%m-%d %H:%M"),
companion.generate_daily_message()
)
}
fn format_affection_bar(affection: f32) -> String {
let hearts = (affection * 10.0) as usize;
let filled = "❤️".repeat(hearts);
let empty = "🤍".repeat(10 - hearts);
format!("{}{} {:.0}%", filled, empty, affection * 100.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_companion_creation() {
let companion = Companion::new(
"エミリー".to_string(),
CompanionPersonality::Energetic,
);
assert_eq!(companion.name, "エミリー");
assert_eq!(companion.relationship_level, 1);
assert_eq!(companion.affection_score, 0.0);
}
#[test]
fn test_compatibility() {
let personality = CompanionPersonality::Energetic;
let innovator = DiagnosisType::Innovator;
assert_eq!(personality.compatibility(&innovator), 0.95);
}
}

View File

@@ -1,296 +0,0 @@
use anyhow::Result;
use serde_json::{json, Value};
use super::base::BaseMCPServer;
pub struct ExtendedMCPServer {
base: BaseMCPServer,
}
impl ExtendedMCPServer {
pub async fn new() -> Result<Self> {
let base = BaseMCPServer::new().await?;
Ok(ExtendedMCPServer { base })
}
pub async fn run(&mut self) -> Result<()> {
self.base.run().await
}
pub async fn handle_request(&mut self, request: Value) -> Value {
self.base.handle_request(request).await
}
// 拡張ツールを追加
pub fn get_available_tools(&self) -> Vec<Value> {
#[allow(unused_mut)]
let mut tools = self.base.get_available_tools();
// AI分析ツールを追加
#[cfg(feature = "ai-analysis")]
{
tools.push(json!({
"name": "analyze_sentiment",
"description": "Analyze sentiment of memories",
"inputSchema": {
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "Time period to analyze"
}
}
}
}));
tools.push(json!({
"name": "extract_insights",
"description": "Extract insights and patterns from memories",
"inputSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Category to analyze"
}
}
}
}));
}
// Web統合ツールを追加
#[cfg(feature = "web-integration")]
{
tools.push(json!({
"name": "import_webpage",
"description": "Import content from a webpage",
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to import from"
}
},
"required": ["url"]
}
}));
}
// セマンティック検索強化
#[cfg(feature = "semantic-search")]
{
// create_memoryを拡張版で上書き
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "create_memory") {
tools[pos] = json!({
"name": "create_memory",
"description": "Create a new memory entry with optional AI analysis",
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Content of the memory"
},
"analyze": {
"type": "boolean",
"description": "Enable AI analysis for this memory"
}
},
"required": ["content"]
}
});
}
// search_memoriesを拡張版で上書き
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "search_memories") {
tools[pos] = json!({
"name": "search_memories",
"description": "Search memories with advanced options",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"semantic": {
"type": "boolean",
"description": "Use semantic search"
},
"category": {
"type": "string",
"description": "Filter by category"
},
"time_range": {
"type": "string",
"description": "Filter by time range (e.g., '1week', '1month')"
}
},
"required": ["query"]
}
});
}
}
tools
}
// 拡張ツール実行
pub async fn execute_tool(&mut self, tool_name: &str, arguments: &Value) -> Value {
match tool_name {
// 拡張機能
#[cfg(feature = "ai-analysis")]
"analyze_sentiment" => self.tool_analyze_sentiment(arguments).await,
#[cfg(feature = "ai-analysis")]
"extract_insights" => self.tool_extract_insights(arguments).await,
#[cfg(feature = "web-integration")]
"import_webpage" => self.tool_import_webpage(arguments).await,
// 拡張版の基本ツール (AI分析付き)
"create_memory" => self.tool_create_memory_extended(arguments).await,
"search_memories" => self.tool_search_memories_extended(arguments).await,
// 基本ツールにフォールバック
_ => self.base.execute_tool(tool_name, arguments).await,
}
}
// 拡張ツール実装
async fn tool_create_memory_extended(&mut self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let analyze = arguments["analyze"].as_bool().unwrap_or(false);
let final_content = if analyze {
#[cfg(feature = "ai-analysis")]
{
format!("[AI分析] 感情: neutral, カテゴリ: general\n{}", content)
}
#[cfg(not(feature = "ai-analysis"))]
{
content.to_string()
}
} else {
content.to_string()
};
match self.base.memory_manager.create_memory(&final_content) {
Ok(id) => json!({
"success": true,
"id": id,
"message": if analyze { "Memory created with AI analysis" } else { "Memory created successfully" }
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
})
}
}
async fn tool_search_memories_extended(&mut self, arguments: &Value) -> Value {
let query = arguments["query"].as_str().unwrap_or("");
let semantic = arguments["semantic"].as_bool().unwrap_or(false);
let memories = if semantic {
#[cfg(feature = "semantic-search")]
{
// モックセマンティック検索
self.base.memory_manager.search_memories(query)
}
#[cfg(not(feature = "semantic-search"))]
{
self.base.memory_manager.search_memories(query)
}
} else {
self.base.memory_manager.search_memories(query)
};
json!({
"success": true,
"memories": memories.into_iter().map(|m| json!({
"id": m.id,
"content": m.content,
"interpreted_content": m.interpreted_content,
"priority_score": m.priority_score,
"user_context": m.user_context,
"created_at": m.created_at,
"updated_at": m.updated_at
})).collect::<Vec<_>>(),
"search_type": if semantic { "semantic" } else { "keyword" }
})
}
#[cfg(feature = "ai-analysis")]
async fn tool_analyze_sentiment(&mut self, _arguments: &Value) -> Value {
json!({
"success": true,
"analysis": {
"positive": 60,
"neutral": 30,
"negative": 10,
"dominant_sentiment": "positive"
},
"message": "Sentiment analysis completed"
})
}
#[cfg(feature = "ai-analysis")]
async fn tool_extract_insights(&mut self, _arguments: &Value) -> Value {
json!({
"success": true,
"insights": {
"most_frequent_topics": ["programming", "ai", "productivity"],
"learning_frequency": "5 times per week",
"growth_trend": "increasing",
"recommendations": ["Focus more on advanced topics", "Consider practical applications"]
},
"message": "Insights extracted successfully"
})
}
#[cfg(feature = "web-integration")]
async fn tool_import_webpage(&mut self, arguments: &Value) -> Value {
let url = arguments["url"].as_str().unwrap_or("");
match self.import_from_web(url).await {
Ok(content) => {
match self.base.memory_manager.create_memory(&content) {
Ok(id) => json!({
"success": true,
"id": id,
"message": format!("Webpage imported successfully from {}", url)
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
})
}
}
Err(e) => json!({
"success": false,
"error": format!("Failed to import webpage: {}", e)
})
}
}
#[cfg(feature = "web-integration")]
async fn import_from_web(&self, url: &str) -> Result<String> {
let response = reqwest::get(url).await?;
let content = response.text().await?;
let document = scraper::Html::parse_document(&content);
let title_selector = scraper::Selector::parse("title").unwrap();
let body_selector = scraper::Selector::parse("p").unwrap();
let title = document.select(&title_selector)
.next()
.map(|el| el.inner_html())
.unwrap_or_else(|| "Untitled".to_string());
let paragraphs: Vec<String> = document.select(&body_selector)
.map(|el| el.inner_html())
.take(5)
.collect();
Ok(format!("# {}\nURL: {}\n\n{}", title, url, paragraphs.join("\n\n")))
}
}

View File

@@ -1,365 +0,0 @@
use crate::memory::Memory;
use serde::{Deserialize, Serialize};
use chrono::Datelike;
/// メモリーのレア度
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MemoryRarity {
Common, // 0.0-0.4
Uncommon, // 0.4-0.6
Rare, // 0.6-0.8
Epic, // 0.8-0.9
Legendary, // 0.9-1.0
}
impl MemoryRarity {
pub fn from_score(score: f32) -> Self {
match score {
s if s >= 0.9 => MemoryRarity::Legendary,
s if s >= 0.8 => MemoryRarity::Epic,
s if s >= 0.6 => MemoryRarity::Rare,
s if s >= 0.4 => MemoryRarity::Uncommon,
_ => MemoryRarity::Common,
}
}
pub fn emoji(&self) -> &str {
match self {
MemoryRarity::Common => "",
MemoryRarity::Uncommon => "🟢",
MemoryRarity::Rare => "🔵",
MemoryRarity::Epic => "🟣",
MemoryRarity::Legendary => "🟡",
}
}
pub fn name(&self) -> &str {
match self {
MemoryRarity::Common => "COMMON",
MemoryRarity::Uncommon => "UNCOMMON",
MemoryRarity::Rare => "RARE",
MemoryRarity::Epic => "EPIC",
MemoryRarity::Legendary => "LEGENDARY",
}
}
pub fn xp_value(&self) -> u32 {
match self {
MemoryRarity::Common => 100,
MemoryRarity::Uncommon => 250,
MemoryRarity::Rare => 500,
MemoryRarity::Epic => 850,
MemoryRarity::Legendary => 1000,
}
}
}
/// 診断タイプ
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DiagnosisType {
Innovator, // 革新者(創造性高、実用性高)
Philosopher, // 哲学者(感情高、新規性高)
Pragmatist, // 実務家(実用性高、関連性高)
Visionary, // 夢想家(新規性高、感情高)
Analyst, // 分析家(全て平均的)
}
impl DiagnosisType {
/// スコアから診断タイプを推定(公開用)
pub fn from_memory(memory: &crate::memory::Memory) -> Self {
// スコア内訳を推定
let emotional = (memory.priority_score * 0.25).min(0.25);
let relevance = (memory.priority_score * 0.25).min(0.25);
let novelty = (memory.priority_score * 0.25).min(0.25);
let utility = memory.priority_score - emotional - relevance - novelty;
Self::from_score_breakdown(emotional, relevance, novelty, utility)
}
pub fn from_score_breakdown(
emotional: f32,
relevance: f32,
novelty: f32,
utility: f32,
) -> Self {
if utility > 0.2 && novelty > 0.2 {
DiagnosisType::Innovator
} else if emotional > 0.2 && novelty > 0.2 {
DiagnosisType::Philosopher
} else if utility > 0.2 && relevance > 0.2 {
DiagnosisType::Pragmatist
} else if novelty > 0.2 && emotional > 0.18 {
DiagnosisType::Visionary
} else {
DiagnosisType::Analyst
}
}
pub fn emoji(&self) -> &str {
match self {
DiagnosisType::Innovator => "💡",
DiagnosisType::Philosopher => "🧠",
DiagnosisType::Pragmatist => "🎯",
DiagnosisType::Visionary => "",
DiagnosisType::Analyst => "📊",
}
}
pub fn name(&self) -> &str {
match self {
DiagnosisType::Innovator => "革新者",
DiagnosisType::Philosopher => "哲学者",
DiagnosisType::Pragmatist => "実務家",
DiagnosisType::Visionary => "夢想家",
DiagnosisType::Analyst => "分析家",
}
}
pub fn description(&self) -> &str {
match self {
DiagnosisType::Innovator => "創造的で実用的なアイデアを生み出す。常に新しい可能性を探求し、それを現実のものにする力を持つ。",
DiagnosisType::Philosopher => "深い思考と感情を大切にする。抽象的な概念や人生の意味について考えることを好む。",
DiagnosisType::Pragmatist => "現実的で効率的。具体的な問題解決に優れ、確実に結果を出す。",
DiagnosisType::Visionary => "大胆な夢と理想を追い求める。常識にとらわれず、未来の可能性を信じる。",
DiagnosisType::Analyst => "バランスの取れた思考。多角的な視点から物事を分析し、冷静に判断する。",
}
}
}
/// ゲーム風の結果フォーマッター
pub struct GameFormatter;
impl GameFormatter {
/// メモリー作成結果をゲーム風に表示
pub fn format_memory_result(memory: &Memory) -> String {
let rarity = MemoryRarity::from_score(memory.priority_score);
let xp = rarity.xp_value();
let score_percentage = (memory.priority_score * 100.0) as u32;
// スコア内訳を推定各項目最大0.25として)
let emotional = (memory.priority_score * 0.25).min(0.25);
let relevance = (memory.priority_score * 0.25).min(0.25);
let novelty = (memory.priority_score * 0.25).min(0.25);
let utility = memory.priority_score - emotional - relevance - novelty;
let diagnosis = DiagnosisType::from_score_breakdown(
emotional,
relevance,
novelty,
utility,
);
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 🎲 メモリースコア判定 ║
╚══════════════════════════════════════════════════════════════╝
⚡ 分析完了! あなたの思考が記録されました
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 総合スコア
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{} {} {}点
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 詳細分析
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💓 感情的インパクト: {}
🔗 ユーザー関連性: {}
✨ 新規性・独自性: {}
⚙️ 実用性: {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎊 あなたのタイプ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{} 【{}】
{}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 報酬
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💎 XP獲得: +{} XP
🎁 レア度: {} {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💬 AI の解釈
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📤 この結果をシェアしよう!
#aigpt #メモリースコア #{}
"#,
rarity.emoji(),
rarity.name(),
score_percentage,
Self::format_bar(emotional, 0.25),
Self::format_bar(relevance, 0.25),
Self::format_bar(novelty, 0.25),
Self::format_bar(utility, 0.25),
diagnosis.emoji(),
diagnosis.name(),
diagnosis.description(),
xp,
rarity.emoji(),
rarity.name(),
memory.interpreted_content,
diagnosis.name(),
)
}
/// シェア用の短縮テキストを生成
pub fn format_shareable_text(memory: &Memory) -> String {
let rarity = MemoryRarity::from_score(memory.priority_score);
let score_percentage = (memory.priority_score * 100.0) as u32;
let emotional = (memory.priority_score * 0.25).min(0.25);
let relevance = (memory.priority_score * 0.25).min(0.25);
let novelty = (memory.priority_score * 0.25).min(0.25);
let utility = memory.priority_score - emotional - relevance - novelty;
let diagnosis = DiagnosisType::from_score_breakdown(
emotional,
relevance,
novelty,
utility,
);
format!(
r#"🎲 AIメモリースコア診断結果
{} {} {}点
{} 【{}】
{}
#aigpt #メモリースコア #AI診断"#,
rarity.emoji(),
rarity.name(),
score_percentage,
diagnosis.emoji(),
diagnosis.name(),
Self::truncate(&memory.content, 100),
)
}
/// ランキング表示
pub fn format_ranking(memories: &[&Memory], title: &str) -> String {
let mut result = format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 🏆 {} ║
╚══════════════════════════════════════════════════════════════╝
"#,
title
);
for (i, memory) in memories.iter().take(10).enumerate() {
let rank_emoji = match i {
0 => "🥇",
1 => "🥈",
2 => "🥉",
_ => " ",
};
let rarity = MemoryRarity::from_score(memory.priority_score);
let score = (memory.priority_score * 100.0) as u32;
result.push_str(&format!(
"{} {}{} {} {}点 - {}\n",
rank_emoji,
i + 1,
rarity.emoji(),
rarity.name(),
score,
Self::truncate(&memory.content, 40)
));
}
result.push_str("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
result
}
/// デイリーチャレンジ表示
pub fn format_daily_challenge() -> String {
// 今日の日付をシードにランダムなお題を生成
let challenges = vec![
"今日学んだことを記録しよう",
"新しいアイデアを思いついた?",
"感動したことを書き留めよう",
"目標を一つ設定しよう",
"誰かに感謝の気持ちを伝えよう",
];
let today = chrono::Utc::now().ordinal();
let challenge = challenges[today as usize % challenges.len()];
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 📅 今日のチャレンジ ║
╚══════════════════════════════════════════════════════════════╝
✨ {}
🎁 報酬: +200 XP
💎 完了すると特別なバッジが獲得できます!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"#,
challenge
)
}
/// プログレスバーを生成
fn format_bar(value: f32, max: f32) -> String {
let percentage = (value / max * 100.0) as u32;
let filled = (percentage / 10) as usize;
let empty = 10 - filled;
format!(
"[{}{}] {}%",
"".repeat(filled),
"".repeat(empty),
percentage
)
}
/// テキストを切り詰め
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_rarity_from_score() {
assert!(matches!(MemoryRarity::from_score(0.95), MemoryRarity::Legendary));
assert!(matches!(MemoryRarity::from_score(0.85), MemoryRarity::Epic));
assert!(matches!(MemoryRarity::from_score(0.7), MemoryRarity::Rare));
assert!(matches!(MemoryRarity::from_score(0.5), MemoryRarity::Uncommon));
assert!(matches!(MemoryRarity::from_score(0.3), MemoryRarity::Common));
}
#[test]
fn test_diagnosis_type() {
let diagnosis = DiagnosisType::from_score_breakdown(0.1, 0.1, 0.22, 0.22);
assert!(matches!(diagnosis, DiagnosisType::Innovator));
}
#[test]
fn test_format_bar() {
let bar = GameFormatter::format_bar(0.15, 0.25);
assert!(bar.contains("60%"));
}
}

View File

@@ -1,374 +0,0 @@
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use uuid::Uuid;
use crate::ai_interpreter::AIInterpreter;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub id: String,
pub content: String,
#[serde(default = "default_interpreted_content")]
pub interpreted_content: String, // AI解釈後のコンテンツ
#[serde(default = "default_priority_score")]
pub priority_score: f32, // 心理判定スコア (0.0-1.0)
#[serde(default)]
pub user_context: Option<String>, // ユーザー固有性
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
fn default_interpreted_content() -> String {
String::new()
}
fn default_priority_score() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: String,
pub title: String,
pub created_at: DateTime<Utc>,
pub message_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTNode {
id: String,
children: Vec<String>,
parent: Option<String>,
message: Option<ChatGPTMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTMessage {
id: String,
author: ChatGPTAuthor,
content: ChatGPTContent,
create_time: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTAuthor {
role: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum ChatGPTContent {
Text {
content_type: String,
parts: Vec<String>,
},
Other(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTConversation {
#[serde(default)]
id: String,
#[serde(alias = "conversation_id")]
conversation_id: Option<String>,
title: String,
create_time: f64,
mapping: HashMap<String, ChatGPTNode>,
}
pub struct MemoryManager {
memories: HashMap<String, Memory>,
conversations: HashMap<String, Conversation>,
data_file: PathBuf,
max_memories: usize, // 最大記憶数
#[allow(dead_code)]
min_priority_score: f32, // 最小優先度スコア (将来の機能で使用予定)
ai_interpreter: AIInterpreter, // AI解釈エンジン
}
impl MemoryManager {
pub async fn new() -> Result<Self> {
let data_dir = dirs::config_dir()
.context("Could not find config directory")?
.join("syui")
.join("ai")
.join("gpt");
std::fs::create_dir_all(&data_dir)?;
let data_file = data_dir.join("memory.json");
let (memories, conversations) = if data_file.exists() {
Self::load_data(&data_file)?
} else {
(HashMap::new(), HashMap::new())
};
Ok(MemoryManager {
memories,
conversations,
data_file,
max_memories: 100, // デフォルト: 100件
min_priority_score: 0.3, // デフォルト: 0.3以上
ai_interpreter: AIInterpreter::new(),
})
}
pub fn create_memory(&mut self, content: &str) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let memory = Memory {
id: id.clone(),
content: content.to_string(),
interpreted_content: content.to_string(), // 後でAI解釈を実装
priority_score: 0.5, // 後で心理判定を実装
user_context: None,
created_at: now,
updated_at: now,
};
self.memories.insert(id.clone(), memory);
// 容量制限チェック
self.prune_memories_if_needed()?;
self.save_data()?;
Ok(id)
}
/// AI解釈と心理判定を使った記憶作成後方互換性のため残す
pub async fn create_memory_with_ai(
&mut self,
content: &str,
user_context: Option<&str>,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
// AI解釈と心理判定を実行
let (interpreted_content, priority_score) = self
.ai_interpreter
.analyze(content, user_context)
.await?;
let memory = Memory {
id: id.clone(),
content: content.to_string(),
interpreted_content,
priority_score,
user_context: user_context.map(|s| s.to_string()),
created_at: now,
updated_at: now,
};
self.memories.insert(id.clone(), memory);
// 容量制限チェック
self.prune_memories_if_needed()?;
self.save_data()?;
Ok(id)
}
/// Claude Code から解釈とスコアを受け取ってメモリを作成
pub fn create_memory_with_interpretation(
&mut self,
content: &str,
interpreted_content: &str,
priority_score: f32,
user_context: Option<&str>,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let memory = Memory {
id: id.clone(),
content: content.to_string(),
interpreted_content: interpreted_content.to_string(),
priority_score: priority_score.max(0.0).min(1.0), // 0.0-1.0 に制限
user_context: user_context.map(|s| s.to_string()),
created_at: now,
updated_at: now,
};
self.memories.insert(id.clone(), memory);
// 容量制限チェック
self.prune_memories_if_needed()?;
self.save_data()?;
Ok(id)
}
pub fn update_memory(&mut self, id: &str, content: &str) -> Result<()> {
if let Some(memory) = self.memories.get_mut(id) {
memory.content = content.to_string();
memory.updated_at = Utc::now();
self.save_data()?;
Ok(())
} else {
Err(anyhow::anyhow!("Memory not found: {}", id))
}
}
pub fn delete_memory(&mut self, id: &str) -> Result<()> {
if self.memories.remove(id).is_some() {
self.save_data()?;
Ok(())
} else {
Err(anyhow::anyhow!("Memory not found: {}", id))
}
}
// 容量制限: 優先度が低いものから削除
fn prune_memories_if_needed(&mut self) -> Result<()> {
if self.memories.len() <= self.max_memories {
return Ok(());
}
// 優先度でソートして、低いものから削除
let mut sorted_memories: Vec<_> = self.memories.iter()
.map(|(id, mem)| (id.clone(), mem.priority_score))
.collect();
sorted_memories.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let to_remove = self.memories.len() - self.max_memories;
for (id, _) in sorted_memories.iter().take(to_remove) {
self.memories.remove(id);
}
Ok(())
}
// 優先度順に記憶を取得
pub fn get_memories_by_priority(&self) -> Vec<&Memory> {
let mut memories: Vec<_> = self.memories.values().collect();
memories.sort_by(|a, b| b.priority_score.partial_cmp(&a.priority_score).unwrap_or(std::cmp::Ordering::Equal));
memories
}
pub fn search_memories(&self, query: &str) -> Vec<&Memory> {
let query_lower = query.to_lowercase();
let mut results: Vec<_> = self.memories
.values()
.filter(|memory| memory.content.to_lowercase().contains(&query_lower))
.collect();
results.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
results
}
pub fn list_conversations(&self) -> Vec<&Conversation> {
let mut conversations: Vec<_> = self.conversations.values().collect();
conversations.sort_by(|a, b| b.created_at.cmp(&a.created_at));
conversations
}
#[allow(dead_code)]
pub async fn import_chatgpt_conversations(&mut self, file_path: &PathBuf) -> Result<()> {
let content = std::fs::read_to_string(file_path)
.context("Failed to read conversations file")?;
let chatgpt_conversations: Vec<ChatGPTConversation> = serde_json::from_str(&content)
.context("Failed to parse ChatGPT conversations")?;
let mut imported_memories = 0;
let mut imported_conversations = 0;
for conv in chatgpt_conversations {
// Get the actual conversation ID
let conv_id = if !conv.id.is_empty() {
conv.id.clone()
} else if let Some(cid) = conv.conversation_id {
cid
} else {
Uuid::new_v4().to_string()
};
// Add conversation
let conversation = Conversation {
id: conv_id.clone(),
title: conv.title.clone(),
created_at: DateTime::from_timestamp(conv.create_time as i64, 0)
.unwrap_or_else(Utc::now),
message_count: conv.mapping.len() as u32,
};
self.conversations.insert(conv_id.clone(), conversation);
imported_conversations += 1;
// Extract memories from messages
for (_, node) in conv.mapping {
if let Some(message) = node.message {
if let ChatGPTContent::Text { parts, .. } = message.content {
for part in parts {
if !part.trim().is_empty() && part.len() > 10 {
let memory_content = format!("[{}] {}", conv.title, part);
self.create_memory(&memory_content)?;
imported_memories += 1;
}
}
}
}
}
}
println!("Imported {} conversations and {} memories",
imported_conversations, imported_memories);
Ok(())
}
fn load_data(file_path: &PathBuf) -> Result<(HashMap<String, Memory>, HashMap<String, Conversation>)> {
let content = std::fs::read_to_string(file_path)
.context("Failed to read data file")?;
#[derive(Deserialize)]
struct Data {
memories: HashMap<String, Memory>,
conversations: HashMap<String, Conversation>,
}
let data: Data = serde_json::from_str(&content)
.context("Failed to parse data file")?;
Ok((data.memories, data.conversations))
}
// Getter: 単一メモリ取得
pub fn get_memory(&self, id: &str) -> Option<&Memory> {
self.memories.get(id)
}
// Getter: 全メモリ取得
pub fn get_all_memories(&self) -> Vec<&Memory> {
self.memories.values().collect()
}
fn save_data(&self) -> Result<()> {
#[derive(Serialize)]
struct Data<'a> {
memories: &'a HashMap<String, Memory>,
conversations: &'a HashMap<String, Conversation>,
}
let data = Data {
memories: &self.memories,
conversations: &self.conversations,
};
let content = serde_json::to_string_pretty(&data)
.context("Failed to serialize data")?;
std::fs::write(&self.data_file, content)
.context("Failed to write data file")?;
Ok(())
}
}