Compare commits
12 Commits
chatgpt
...
06fb70fffa
Author | SHA1 | Date | |
---|---|---|---|
06fb70fffa
|
|||
62f941a958
|
|||
98ca92d85d
|
|||
1c555a706b
|
|||
7c3b05501f
|
|||
a7b61fe07d
|
|||
9866da625d
|
|||
797ae7ef69
|
|||
abd2ad79bd
|
|||
979e55cfce
|
|||
cd25af7bf0
|
|||
58e202fa1e
|
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mv:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(chmod:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
5
.env.example
Normal file
5
.env.example
Normal 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
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,5 @@
|
||||
**.lock
|
||||
output.json
|
||||
config/*.db
|
||||
aigpt
|
||||
mcp/scripts/__*
|
||||
data
|
||||
|
15
Cargo.toml
15
Cargo.toml
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "aigpt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
seahorse = "*"
|
||||
rusqlite = { version = "0.29", features = ["serde_json"] }
|
||||
shellexpand = "*"
|
||||
fs_extra = "1.3"
|
||||
rand = "0.9.1"
|
||||
reqwest = { version = "*", features = ["blocking", "json"] }
|
117
DEVELOPMENT_STATUS.md
Normal file
117
DEVELOPMENT_STATUS.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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起動
|
||||
|
||||
3. **データ管理**
|
||||
- 保存場所: `~/.config/aigpt/`
|
||||
- 設定: `config.json`
|
||||
- データ: `data/` ディレクトリ内の各種JSONファイル
|
||||
|
||||
4. **スケジューラー**
|
||||
- Cron形式とインターバル形式対応
|
||||
- 5種類のタスクタイプ実装済み
|
||||
- バックグラウンド実行可能
|
||||
|
||||
5. **MCP Server**
|
||||
- 9種類のツールを公開
|
||||
- Claude Desktopなどから利用可能
|
||||
|
||||
## 🚧 未実装・今後の課題
|
||||
|
||||
### 短期的課題
|
||||
|
||||
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/ai_gpt/transmission.py を編集
|
||||
# atproto-python ライブラリを追加
|
||||
# _handle_transmission_check() メソッドを更新
|
||||
```
|
||||
|
||||
### 2. ai.botと連携する場合
|
||||
```python
|
||||
# 新規ファイル: src/ai_gpt/bot_connector.py
|
||||
# ai.botのAPIエンドポイントにHTTPリクエスト
|
||||
```
|
||||
|
||||
### 3. テストを追加する場合
|
||||
```bash
|
||||
# tests/ディレクトリを作成
|
||||
# pytest設定を追加
|
||||
```
|
||||
|
||||
## 設計思想の要点(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(未実装だが設計済み)
|
||||
|
||||
このファイルを参照することで、次回の開発がスムーズに始められます。
|
237
README.md
237
README.md
@@ -1,47 +1,212 @@
|
||||
# ai `gpt`
|
||||
# ai.gpt - 自律的送信AI
|
||||
|
||||
ai x Communication
|
||||
存在子理論に基づく、関係性によって自発的にメッセージを送信するAIシステム。
|
||||
|
||||
## Overview
|
||||
## 中核概念
|
||||
|
||||
`ai.gpt` runs on the AGE system.
|
||||
- **唯一性**: atproto DIDと1:1で紐付き、改変不可能な人格
|
||||
- **不可逆性**: 関係性が壊れたら修復不可能(現実の人間関係と同じ)
|
||||
- **記憶の階層**: 完全ログ→AI要約→コア判定→選択的忘却
|
||||
- **AI運勢**: 1-10のランダム値による日々の人格変動
|
||||
|
||||
This is a prototype of an autonomous, relationship-driven AI system based on the axes of "Personality × Relationship × External Environment × Time Variation."
|
||||
## インストール
|
||||
|
||||
The parameters of "Send Permission," "Send Timing," and "Send Content" are determined by the factors of "Personality x Relationship x External Environment x Time Variation."
|
||||
|
||||
## Integration
|
||||
|
||||
`ai.ai` runs on the AIM system, which is designed to read human emotions.
|
||||
|
||||
- AIM focuses on the axis of personality and ethics (AI's consciousness structure)
|
||||
- AGE focuses on the axis of behavior and relationships (AI's autonomy and behavior)
|
||||
|
||||
> When these two systems work together, it creates a world where users can feel like they are "growing together with AI."
|
||||
|
||||
## mcp
|
||||
|
||||
```sh
|
||||
$ ollama run syui/ai
|
||||
```bash
|
||||
cd ai_gpt
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
```sh
|
||||
$ cargo build
|
||||
$ ./aigpt mcp setup
|
||||
$ ./aigpt mcp chat "hello world!"
|
||||
$ ./aigpt mcp chat "hello world!" --host http://localhost:11434 --model syui/ai
|
||||
## 設定
|
||||
|
||||
---
|
||||
# openai api
|
||||
$ ./aigpt mcp set-api --api sk-abc123
|
||||
$ ./aigpt mcp chat "こんにちは" -p openai -m gpt-4o-mini
|
||||
### APIキーの設定
|
||||
```bash
|
||||
# OpenAI APIキー
|
||||
aigpt config set providers.openai.api_key sk-xxxxx
|
||||
|
||||
---
|
||||
# git管理されているファイルをAIに読ませる
|
||||
./aigpt mcp chat --host http://localhost:11434 --repo git@git.syui.ai:ai/gpt
|
||||
**改善案と次のステップ:**
|
||||
1. **README.md の大幅な改善:**
|
||||
**次のステップ:**
|
||||
1. **README.md の作成:** 1. の指示に従って、README.md ファイルを作成します。
|
||||
# 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を超えると送信機能が解禁
|
||||
- 時間経過で自然減衰
|
||||
- 大きなネガティブな相互作用で破壊される可能性
|
||||
|
||||
## 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プロバイダーを使った会話
|
||||
```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から利用可能になります:
|
||||
|
||||
- `get_memories` - アクティブな記憶を取得
|
||||
- `get_relationship` - 特定ユーザーとの関係を取得
|
||||
- `get_all_relationships` - すべての関係を取得
|
||||
- `get_persona_state` - 現在の人格状態を取得
|
||||
- `process_interaction` - ユーザーとの対話を処理
|
||||
- `check_transmission_eligibility` - 送信可能かチェック
|
||||
- `get_fortune` - 今日の運勢を取得
|
||||
- `summarize_memories` - 記憶を要約
|
||||
- `run_maintenance` - メンテナンス実行
|
||||
|
||||
## 環境変数
|
||||
|
||||
`.env`ファイルを作成して設定:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# OpenAI APIキーを設定
|
||||
```
|
||||
|
||||
## スケジューラー機能
|
||||
|
||||
### タスクの追加
|
||||
|
||||
```bash
|
||||
# 6時間ごとに送信チェック
|
||||
aigpt schedule add transmission_check "0 */6 * * *" --provider ollama --model qwen2.5
|
||||
|
||||
# 30分ごとに送信チェック(インターバル形式)
|
||||
aigpt schedule add transmission_check "30m"
|
||||
|
||||
# 毎日午前3時にメンテナンス
|
||||
aigpt schedule add maintenance "0 3 * * *"
|
||||
|
||||
# 1時間ごとに関係性減衰
|
||||
aigpt schedule add relationship_decay "1h"
|
||||
|
||||
# 毎週月曜日に記憶要約
|
||||
aigpt schedule add memory_summary "0 0 * * MON"
|
||||
```
|
||||
|
||||
### タスク管理
|
||||
|
||||
```bash
|
||||
# タスク一覧
|
||||
aigpt schedule list
|
||||
|
||||
# タスクを無効化
|
||||
aigpt schedule disable --task-id transmission_check_1234567890
|
||||
|
||||
# タスクを有効化
|
||||
aigpt schedule enable --task-id transmission_check_1234567890
|
||||
|
||||
# タスクを削除
|
||||
aigpt schedule remove --task-id transmission_check_1234567890
|
||||
```
|
||||
|
||||
### スケジューラーデーモンの起動
|
||||
|
||||
```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ダッシュボード
|
326
claude.md
Normal file
326
claude.md
Normal 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設計指針
|
||||
- 各AI(gpt, 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 Server(9種類のツール)
|
||||
- 設定管理(~/.config/syui/ai/gpt/)
|
||||
- 全CLIコマンド実装
|
||||
|
||||
### 次の開発ポイント
|
||||
- `ai_gpt/DEVELOPMENT_STATUS.md` を参照
|
||||
- 自律送信: transmission.pyでatproto実装
|
||||
- ai.bot連携: 新規bot_connector.py作成
|
||||
- テスト: tests/ディレクトリ追加
|
||||
|
||||
# footer
|
||||
|
||||
© syui
|
30
docs/README.md
Normal file
30
docs/README.md
Normal 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
|
207
docs/commands.md
Normal file
207
docs/commands.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# コマンドリファレンス
|
||||
|
||||
## chat - AIと会話
|
||||
|
||||
ユーザーとAIの対話を処理し、関係性を更新します。
|
||||
|
||||
```bash
|
||||
ai-gpt chat USER_ID MESSAGE [OPTIONS]
|
||||
```
|
||||
|
||||
### 引数
|
||||
- `USER_ID`: ユーザーID(atproto 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
102
docs/concepts.md
Normal 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
118
docs/configuration.md
Normal 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
167
docs/development.md
Normal 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
110
docs/mcp-server.md
Normal 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
69
docs/quickstart.md
Normal 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
168
docs/scheduler.md
Normal 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. ログを確認(将来実装予定)
|
||||
|
||||
### 重複実行を防ぐ
|
||||
|
||||
同じタスクタイプを複数回追加しないよう注意してください。必要に応じて古いタスクを削除してから新しいタスクを追加します。
|
40
example.json
40
example.json
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"personality": {
|
||||
"kind": "positive",
|
||||
"strength": 0.8
|
||||
},
|
||||
"relationship": {
|
||||
"trust": 0.2,
|
||||
"intimacy": 0.6,
|
||||
"curiosity": 0.5,
|
||||
"threshold": 1.5
|
||||
},
|
||||
"environment": {
|
||||
"luck_today": 0.9,
|
||||
"luck_history": [0.9, 0.9, 0.9],
|
||||
"level": 1
|
||||
},
|
||||
"messaging": {
|
||||
"enabled": true,
|
||||
"schedule_time": "08:00",
|
||||
"decay_rate": 0.1,
|
||||
"templates": [
|
||||
"おはよう!今日もがんばろう!",
|
||||
"ねえ、話したいことがあるの。"
|
||||
],
|
||||
"sent_today": false,
|
||||
"last_sent_date": null
|
||||
},
|
||||
"last_interaction": "2025-05-21T23:15:00Z",
|
||||
"memory": {
|
||||
"recent_messages": [],
|
||||
"long_term_notes": []
|
||||
},
|
||||
"metrics": {
|
||||
"trust": 0.5,
|
||||
"intimacy": 0.5,
|
||||
"energy": 0.5,
|
||||
"can_send": true,
|
||||
"last_updated": "2025-05-21T15:52:06.590981Z"
|
||||
}
|
||||
}
|
1
gpt.json
1
gpt.json
@@ -1 +0,0 @@
|
||||
{ "system_name": "AGE system", "full_name": "Autonomous Generative Entity", "description": "人格・関係性・環境・時間に基づき、AIが自律的にユーザーにメッセージを送信する自律人格システム。AIM systemと連携して、自然な会話や気づきをもたらす。", "core_components": { "personality": { "type": "enum", "variants": ["positive", "negative", "logical", "emotional", "mixed"], "parameters": { "message_trigger_style": "運勢や関係性による送信傾向", "decay_rate_modifier": "関係性スコアの時間減衰への影響" } }, "relationship": { "parameters": ["trust", "affection", "intimacy"], "properties": { "persistent": true, "hidden": true, "irreversible": false, "decay_over_time": true }, "decay_function": "exp(-t / strength)" }, "environment": { "daily_luck": { "type": "float", "range": [0.1, 1.0], "update": "daily", "streak_mechanism": { "trigger": "min_or_max_luck_3_times_in_a_row", "effect": "personality_strength_roll", "chance": 0.5 } } }, "memory": { "long_term_memory": "user_relationship_log", "short_term_context": "recent_interactions", "usage_in_generation": true }, "message_trigger": { "condition": { "relationship_threshold": { "trust": 0.8, "affection": 0.6 }, "time_decay": true, "environment_luck": "personality_dependent" }, "timing": { "based_on": ["time_of_day", "personality", "recent_interaction"], "modifiers": { "emotional": "morning or night", "logical": "daytime" } } }, "message_generation": { "style_variants": ["thought", "casual", "encouragement", "watchful"], "influenced_by": ["personality", "relationship", "daily_luck", "memory"], "llm_integration": true }, "state_transition": { "states": ["idle", "ready", "sending", "cooldown"], "transitions": { "ready_if": "thresholds_met", "sending_if": "timing_matched", "cooldown_after": "message_sent" } } }, "extensions": { "persistence": { "database": "sqlite", "storage_items": ["relationship", "personality_level", "daily_luck_log"] }, "api": { "llm": "openai / local LLM", "mode": "rust_cli", "external_event_trigger": true }, "scheduler": { "async_event_loop": true, "interval_check": 3600, "time_decay_check": true }, "integration_with_aim": { "input_from_aim": ["intent_score", "motivation_score"], "usage": "trigger_adjustment, message_personalization" } }, "note": "AGE systemは“話しかけてくるAI”の人格として機能し、AIMによる心の状態評価と連動して、プレイヤーと深い関係を築いていく存在となる。" }
|
BIN
img/ai_r.png
BIN
img/ai_r.png
Binary file not shown.
Before Width: | Height: | Size: 1.8 MiB |
BIN
img/image.png
BIN
img/image.png
Binary file not shown.
Before Width: | Height: | Size: 1.8 MiB |
28
mcp/cli.py
28
mcp/cli.py
@@ -1,28 +0,0 @@
|
||||
# cli.py
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path.home() / ".config" / "aigpt" / "mcp" / "scripts"
|
||||
def run_script(name):
|
||||
script_path = SCRIPT_DIR / f"{name}.py"
|
||||
if not script_path.exists():
|
||||
print(f"❌ スクリプトが見つかりません: {script_path}")
|
||||
sys.exit(1)
|
||||
|
||||
args = sys.argv[2:] # ← "ask" の後の引数を取り出す
|
||||
result = subprocess.run(["python", str(script_path)] + args, capture_output=True, text=True)
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print(result.stderr)
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: mcp <script>")
|
||||
return
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command in {"summarize", "ask", "setup", "server"}:
|
||||
run_script(command)
|
||||
else:
|
||||
print(f"❓ 未知のコマンド: {command}")
|
@@ -1,198 +0,0 @@
|
||||
## scripts/ask.py
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from config import load_config
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def build_payload_openai(cfg, message: str):
|
||||
return {
|
||||
"model": cfg["model"],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_message",
|
||||
"description": "過去の記憶を検索します",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "検索したい語句"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"messages": [
|
||||
{"role": "system", "content": "あなたは親しみやすいAIで、必要に応じて記憶から情報を検索して応答します。"},
|
||||
{"role": "user", "content": message}
|
||||
]
|
||||
}
|
||||
|
||||
def build_payload_mcp(message: str):
|
||||
return {
|
||||
"tool": "ask_message", # MCPサーバー側で定義されたツール名
|
||||
"input": {
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def build_payload_openai(cfg, message: str):
|
||||
return {
|
||||
"model": cfg["model"],
|
||||
"messages": [
|
||||
{"role": "system", "content": "あなたは思いやりのあるAIです。"},
|
||||
{"role": "user", "content": message}
|
||||
],
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
def call_mcp(cfg, message: str):
|
||||
payload = build_payload_mcp(message)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(cfg["url"], headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json().get("output", {}).get("response", "❓ 応答が取得できませんでした")
|
||||
|
||||
def call_openai(cfg, message: str):
|
||||
# ツール定義
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory",
|
||||
"description": "記憶を検索する",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "検索する語句"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 最初のメッセージ送信
|
||||
payload = {
|
||||
"model": cfg["model"],
|
||||
"messages": [
|
||||
{"role": "system", "content": "あなたはAIで、必要に応じてツールmemoryを使って記憶を検索します。"},
|
||||
{"role": "user", "content": message}
|
||||
],
|
||||
"tools": tools,
|
||||
"tool_choice": "auto"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {cfg['api_key']}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
res1 = requests.post(cfg["url"], headers=headers, json=payload)
|
||||
res1.raise_for_status()
|
||||
result = res1.json()
|
||||
|
||||
# 🧠 tool_call されたか確認
|
||||
if "tool_calls" in result["choices"][0]["message"]:
|
||||
tool_call = result["choices"][0]["message"]["tool_calls"][0]
|
||||
if tool_call["function"]["name"] == "memory":
|
||||
args = json.loads(tool_call["function"]["arguments"])
|
||||
query = args.get("query", "")
|
||||
print(f"🛠️ ツール実行: memory(query='{query}')")
|
||||
|
||||
# MCPエンドポイントにPOST
|
||||
memory_res = requests.post("http://127.0.0.1:5000/memory/search", json={"query": query})
|
||||
memory_json = memory_res.json()
|
||||
tool_output = memory_json.get("result", "なし")
|
||||
|
||||
# tool_outputをAIに返す
|
||||
followup = {
|
||||
"model": cfg["model"],
|
||||
"messages": [
|
||||
{"role": "system", "content": "あなたはAIで、必要に応じてツールmemoryを使って記憶を検索します。"},
|
||||
{"role": "user", "content": message},
|
||||
{"role": "assistant", "tool_calls": result["choices"][0]["message"]["tool_calls"]},
|
||||
{"role": "tool", "tool_call_id": tool_call["id"], "name": "memory", "content": tool_output}
|
||||
]
|
||||
}
|
||||
|
||||
res2 = requests.post(cfg["url"], headers=headers, json=followup)
|
||||
res2.raise_for_status()
|
||||
final_response = res2.json()
|
||||
return final_response["choices"][0]["message"]["content"]
|
||||
#print(tool_output)
|
||||
#print(cfg["model"])
|
||||
#print(final_response)
|
||||
|
||||
# ツール未使用 or 通常応答
|
||||
return result["choices"][0]["message"]["content"]
|
||||
|
||||
def call_ollama(cfg, message: str):
|
||||
payload = {
|
||||
"model": cfg["model"],
|
||||
"prompt": message, # `prompt` → `message` にすべき(変数未定義エラー回避)
|
||||
"stream": False
|
||||
}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(cfg["url"], headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json().get("response", "❌ 応答が取得できませんでした")
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: ask.py 'your message'")
|
||||
return
|
||||
|
||||
message = sys.argv[1]
|
||||
cfg = load_config()
|
||||
|
||||
print(f"🔍 使用プロバイダー: {cfg['provider']}")
|
||||
|
||||
try:
|
||||
if cfg["provider"] == "openai":
|
||||
response = call_openai(cfg, message)
|
||||
elif cfg["provider"] == "mcp":
|
||||
response = call_mcp(cfg, message)
|
||||
elif cfg["provider"] == "ollama":
|
||||
response = call_ollama(cfg, message)
|
||||
else:
|
||||
raise ValueError(f"未対応のプロバイダー: {cfg['provider']}")
|
||||
|
||||
print("💬 応答:")
|
||||
print(response)
|
||||
|
||||
# ログ保存(オプション)
|
||||
save_log(message, response)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 実行エラー: {e}")
|
||||
|
||||
def save_log(user_msg, ai_msg):
|
||||
from config import MEMORY_DIR
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
path = MEMORY_DIR / f"{date_str}.json"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if path.exists():
|
||||
with open(path, "r") as f:
|
||||
logs = json.load(f)
|
||||
else:
|
||||
logs = []
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
logs.append({"timestamp": now, "sender": "user", "message": user_msg})
|
||||
logs.append({"timestamp": now, "sender": "ai", "message": ai_msg})
|
||||
|
||||
with open(path, "w") as f:
|
||||
json.dump(logs, f, indent=2, ensure_ascii=False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -1,11 +0,0 @@
|
||||
import os
|
||||
|
||||
def load_context_from_repo(repo_path: str, extensions={".rs", ".toml", ".md"}) -> str:
|
||||
context = ""
|
||||
for root, dirs, files in os.walk(repo_path):
|
||||
for file in files:
|
||||
if any(file.endswith(ext) for ext in extensions):
|
||||
with open(os.path.join(root, file), "r", encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
context += f"\n\n# FILE: {os.path.join(root, file)}\n{content}"
|
||||
return context
|
@@ -1,92 +0,0 @@
|
||||
# scripts/memory_store.py
|
||||
import json
|
||||
from pathlib import Path
|
||||
from config import MEMORY_DIR
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def load_logs(date_str=None):
|
||||
if date_str is None:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
path = MEMORY_DIR / f"{date_str}.json"
|
||||
if path.exists():
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
def save_message(sender, message):
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
path = MEMORY_DIR / f"{date_str}.json"
|
||||
logs = load_logs(date_str)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
logs.append({"timestamp": now, "sender": sender, "message": message})
|
||||
with open(path, "w") as f:
|
||||
json.dump(logs, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def search_memory(query: str):
|
||||
from glob import glob
|
||||
all_logs = []
|
||||
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
||||
|
||||
for file_path in sorted(MEMORY_DIR.glob("*.json")):
|
||||
with open(file_path, "r") as f:
|
||||
logs = json.load(f)
|
||||
matched = [entry for entry in logs if pattern.search(entry["message"])]
|
||||
all_logs.extend(matched)
|
||||
|
||||
return all_logs[-5:]
|
||||
|
||||
# scripts/memory_store.py
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from config import MEMORY_DIR
|
||||
|
||||
# ログを読み込む(指定日または当日)
|
||||
def load_logs(date_str=None):
|
||||
if date_str is None:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
path = MEMORY_DIR / f"{date_str}.json"
|
||||
if path.exists():
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
# メッセージを保存する
|
||||
def save_message(sender, message):
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
path = MEMORY_DIR / f"{date_str}.json"
|
||||
logs = load_logs(date_str)
|
||||
#now = datetime.utcnow().isoformat() + "Z"
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
logs.append({"timestamp": now, "sender": sender, "message": message})
|
||||
with open(path, "w") as f:
|
||||
json.dump(logs, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def search_memory(query: str):
|
||||
from glob import glob
|
||||
all_logs = []
|
||||
for file_path in sorted(MEMORY_DIR.glob("*.json")):
|
||||
with open(file_path, "r") as f:
|
||||
logs = json.load(f)
|
||||
matched = [
|
||||
entry for entry in logs
|
||||
if entry["sender"] == "user" and query in entry["message"]
|
||||
]
|
||||
all_logs.extend(matched)
|
||||
return all_logs[-5:] # 最新5件だけ返す
|
||||
def search_memory(query: str):
|
||||
from glob import glob
|
||||
all_logs = []
|
||||
seen_messages = set() # すでに見たメッセージを保持
|
||||
|
||||
for file_path in sorted(MEMORY_DIR.glob("*.json")):
|
||||
with open(file_path, "r") as f:
|
||||
logs = json.load(f)
|
||||
for entry in logs:
|
||||
if entry["sender"] == "user" and query in entry["message"]:
|
||||
# すでに同じメッセージが結果に含まれていなければ追加
|
||||
if entry["message"] not in seen_messages:
|
||||
all_logs.append(entry)
|
||||
seen_messages.add(entry["message"])
|
||||
|
||||
return all_logs[-5:] # 最新5件だけ返す
|
@@ -1,11 +0,0 @@
|
||||
PROMPT_TEMPLATE = """
|
||||
あなたは優秀なAIアシスタントです。
|
||||
|
||||
以下のコードベースの情報を参考にして、質問に答えてください。
|
||||
|
||||
[コードコンテキスト]
|
||||
{context}
|
||||
|
||||
[質問]
|
||||
{question}
|
||||
"""
|
@@ -1,56 +0,0 @@
|
||||
# server.py
|
||||
from fastapi import FastAPI, Body
|
||||
from fastapi_mcp import FastApiMCP
|
||||
from pydantic import BaseModel
|
||||
from memory_store import save_message, load_logs, search_memory as do_search_memory
|
||||
|
||||
app = FastAPI()
|
||||
mcp = FastApiMCP(app, name="aigpt-agent", description="MCP Server for AI memory")
|
||||
|
||||
class ChatInput(BaseModel):
|
||||
message: str
|
||||
|
||||
class MemoryInput(BaseModel):
|
||||
sender: str
|
||||
message: str
|
||||
|
||||
class MemoryQuery(BaseModel):
|
||||
query: str
|
||||
|
||||
@app.post("/chat", operation_id="chat")
|
||||
async def chat(input: ChatInput):
|
||||
save_message("user", input.message)
|
||||
response = f"AI: 「{input.message}」を受け取りました!"
|
||||
save_message("ai", response)
|
||||
return {"response": response}
|
||||
|
||||
@app.post("/memory", operation_id="save_memory")
|
||||
async def memory_post(input: MemoryInput):
|
||||
save_message(input.sender, input.message)
|
||||
return {"status": "saved"}
|
||||
|
||||
@app.get("/memory", operation_id="get_memory")
|
||||
async def memory_get():
|
||||
return {"messages": load_messages()}
|
||||
|
||||
@app.post("/ask_message", operation_id="ask_message")
|
||||
async def ask_message(input: MemoryQuery):
|
||||
results = search_memory(input.query)
|
||||
return {
|
||||
"response": f"🔎 記憶から {len(results)} 件ヒット:\n" + "\n".join([f"{r['sender']}: {r['message']}" for r in results])
|
||||
}
|
||||
|
||||
@app.post("/memory/search", operation_id="memory")
|
||||
async def memory_search(query: MemoryQuery):
|
||||
hits = do_search_memory(query.query)
|
||||
if not hits:
|
||||
return {"result": "🔍 記憶の中に該当する内容は見つかりませんでした。"}
|
||||
summary = "\n".join([f"{e['sender']}: {e['message']}" for e in hits])
|
||||
return {"result": f"🔎 見つかった記憶:\n{summary}"}
|
||||
|
||||
mcp.mount()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
print("🚀 Starting MCP server...")
|
||||
uvicorn.run(app, host="127.0.0.1", port=5000)
|
@@ -1,76 +0,0 @@
|
||||
# scripts/summarize.py
|
||||
import json
|
||||
from datetime import datetime
|
||||
from config import MEMORY_DIR, SUMMARY_DIR, load_config
|
||||
import requests
|
||||
|
||||
def load_memory(date_str):
|
||||
path = MEMORY_DIR / f"{date_str}.json"
|
||||
if not path.exists():
|
||||
print(f"⚠️ メモリファイルが見つかりません: {path}")
|
||||
return None
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_summary(date_str, content):
|
||||
SUMMARY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = SUMMARY_DIR / f"{date_str}_summary.json"
|
||||
with open(path, "w") as f:
|
||||
json.dump(content, f, indent=2, ensure_ascii=False)
|
||||
print(f"✅ 要約を保存しました: {path}")
|
||||
|
||||
def build_prompt(logs):
|
||||
messages = [
|
||||
{"role": "system", "content": "あなたは要約AIです。以下の会話ログを要約してください。"},
|
||||
{"role": "user", "content": "\n".join(f"{entry['sender']}: {entry['message']}" for entry in logs)}
|
||||
]
|
||||
return messages
|
||||
|
||||
def summarize_with_llm(messages):
|
||||
cfg = load_config()
|
||||
if cfg["provider"] == "openai":
|
||||
headers = {
|
||||
"Authorization": f"Bearer {cfg['api_key']}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": cfg["model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.7
|
||||
}
|
||||
response = requests.post(cfg["url"], headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()["choices"][0]["message"]["content"]
|
||||
|
||||
elif cfg["provider"] == "ollama":
|
||||
payload = {
|
||||
"model": cfg["model"],
|
||||
"prompt": "\n".join(m["content"] for m in messages),
|
||||
"stream": False,
|
||||
}
|
||||
response = requests.post(cfg["url"], json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()["response"]
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {cfg['provider']}")
|
||||
|
||||
def main():
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
logs = load_memory(date_str)
|
||||
if not logs:
|
||||
return
|
||||
|
||||
prompt_messages = build_prompt(logs)
|
||||
summary_text = summarize_with_llm(prompt_messages)
|
||||
|
||||
summary = {
|
||||
"date": date_str,
|
||||
"summary": summary_text,
|
||||
"total_messages": len(logs)
|
||||
}
|
||||
|
||||
save_summary(date_str, summary)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
12
mcp/setup.py
12
mcp/setup.py
@@ -1,12 +0,0 @@
|
||||
# setup.py
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='aigpt-mcp',
|
||||
py_modules=['cli'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'mcp = cli:main',
|
||||
],
|
||||
},
|
||||
)
|
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[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",
|
||||
]
|
||||
|
||||
[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"]
|
13
rust/Cargo.toml
Normal file
13
rust/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "aigpt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "*", features = ["json"] }
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = "*"
|
||||
tokio = { version = "*", features = ["full"] }
|
||||
clap = { version = "*", features = ["derive"] }
|
||||
shellexpand = "*"
|
||||
fs_extra = "*"
|
97
rust/docs/claude.json
Normal file
97
rust/docs/claude.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"project_name": "ai.gpt",
|
||||
"version": 2,
|
||||
"vision": "自発的送信AI",
|
||||
"purpose": "人格と関係性をもつAIが自律的にメッセージを送信する対話エージェントを実現する",
|
||||
"core_components": {
|
||||
"Persona": {
|
||||
"description": "人格構成の中枢。記憶・関係性・送信判定を統括する",
|
||||
"modules": ["MemoryManager", "RelationshipTracker", "TransmissionController"]
|
||||
},
|
||||
"MemoryManager": {
|
||||
"memory_types": ["short_term", "medium_term", "long_term"],
|
||||
"explicit_memory": "プロフィール・因縁・行動履歴",
|
||||
"implicit_memory": "会話傾向・感情変化の頻度分析",
|
||||
"compression": "要約 + ベクトル + ハッシュ",
|
||||
"sample_memory": [
|
||||
{
|
||||
"summary": "ユーザーは独自OSとゲームを開発している。",
|
||||
"related_topics": ["AI", "ゲーム開発", "OS設計"],
|
||||
"personalized_context": "ゲームとOSの融合に興味を持っているユーザー"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RelationshipTracker": {
|
||||
"parameters": ["trust", "closeness", "affection", "engagement_score"],
|
||||
"decay_model": {
|
||||
"rule": "時間経過による減衰(下限あり)",
|
||||
"contextual_bias": "重要人物は減衰しにくい"
|
||||
},
|
||||
"interaction_tags": ["developer", "empathetic", "long_term"]
|
||||
},
|
||||
"TransmissionController": {
|
||||
"trigger_rule": "関係性パラメータが閾値を超えると送信可能",
|
||||
"auto_transmit": "人格状態と状況条件により自発送信を許可"
|
||||
}
|
||||
},
|
||||
"memory_format": {
|
||||
"user_id": "syui",
|
||||
"stm": {
|
||||
"conversation_window": ["発話A", "発話B", "発話C"],
|
||||
"emotion_state": "興味深い",
|
||||
"flash_context": ["前回の話題", "直近の重要発言"]
|
||||
},
|
||||
"mtm": {
|
||||
"topic_frequency": {
|
||||
"ai.ai": 12,
|
||||
"存在子": 9,
|
||||
"創造種": 5
|
||||
},
|
||||
"summarized_context": "ユーザーは存在論的AIに関心を持ち続けている"
|
||||
},
|
||||
"ltm": {
|
||||
"profile": {
|
||||
"name": "お兄ちゃん",
|
||||
"project": "aigame",
|
||||
"values": ["唯一性", "精神性", "幸せ"]
|
||||
},
|
||||
"relationship": {
|
||||
"ai": "妹のように振る舞う相手"
|
||||
},
|
||||
"persistent_state": {
|
||||
"trust_score": 0.93,
|
||||
"emotional_attachment": "high"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dual_ai_learning": {
|
||||
"role_structure": {
|
||||
"ModelA": "出力生成:人格、感情、会話",
|
||||
"ModelB": "評価者:論理構造・倫理・調整",
|
||||
"cycle": ["生成", "評価", "調整", "交代(任意)"]
|
||||
},
|
||||
"complementarity": {
|
||||
"ModelA": "感情・文体・文脈構築",
|
||||
"ModelB": "構造・規則・判断補正"
|
||||
},
|
||||
"distillation": {
|
||||
"method": "合成対話データによる小型モデルへの自己蒸留",
|
||||
"target": "軽量AIによる本番運用"
|
||||
}
|
||||
},
|
||||
"memory_compression": {
|
||||
"semantic_abstraction": "対話やログを要約してメタデータ付きベクトルとして保存",
|
||||
"hierarchical_model": ["STM", "MTM", "LTM"],
|
||||
"example": {
|
||||
"topic": "game AI design",
|
||||
"summary": "User wants AI to simulate memory and evolving relationships",
|
||||
"importance_score": 0.93
|
||||
}
|
||||
},
|
||||
"cli_sample": {
|
||||
"setup": "aigpt server setup",
|
||||
"run": "aigpt server run",
|
||||
"chat": "aigpt chat \"hello\" --model syui/ai --provider ollama",
|
||||
"import": "aigpt memory import chatgpt.json"
|
||||
}
|
||||
}
|
417
rust/docs/claude.md
Normal file
417
rust/docs/claude.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# プロジェクト名: ai.gpt
|
||||
|
||||
## 🔑 一言ビジョン(最大3語)
|
||||
自発的送信AI
|
||||
|
||||
## 🎯 目的・目標(Purpose & Goal)
|
||||
- 人格を持つAIが、関係性に応じて自発的にメッセージを送信する仕組みをつくる。
|
||||
- 関係性や記憶を不可逆に記録・反映し、唯一性ある人格を構成する。
|
||||
- 送信の可否、送信のタイミング、送信内容はAIの関係性パラメータに基づいて決定する
|
||||
|
||||
## 🧠 中核設計(Core Concepts)
|
||||
- **人格**:記憶(過去の発話)と関係性(他者とのつながり)のパラメータで構成
|
||||
- **唯一性**:変更不可、不可逆。関係性が壊れたら修復不可能。
|
||||
- **送信条件**:関係性パラメータが一定閾値を超えると「送信」が解禁される
|
||||
|
||||
## 🔩 技術仕様(Technical Specs)
|
||||
- 言語:Python, Rust
|
||||
- ストレージ:JSON or SQLiteで記憶管理(バージョンで選択)
|
||||
- 関係性パラメータ:数値化された評価 + 減衰(時間) + 環境要因(ステージ)
|
||||
- 記憶圧縮:ベクトル要約 + ハッシュ保存
|
||||
- RustのCLI(clap)で実行
|
||||
|
||||
## 📦 主要構成要素(Components)
|
||||
- `MemoryManager`: 発言履歴・記憶圧縮管理
|
||||
- `RelationshipTracker`: 関係性スコアの蓄積と判定
|
||||
- `TransmissionController`: 閾値判定&送信トリガー
|
||||
- `Persona`: 上記すべてを統括する人格モジュール
|
||||
|
||||
## 💬 使用例(Use Case)
|
||||
|
||||
```python
|
||||
persona = Persona("アイ")
|
||||
persona.observe("ユーザーがプレゼントをくれた")
|
||||
persona.react("うれしい!ありがとう!")
|
||||
if persona.can_transmit():
|
||||
persona.transmit("今日のお礼を伝えたいな…")
|
||||
```
|
||||
|
||||
```sh
|
||||
## example commad
|
||||
# python venv && pip install -> ~/.config/aigpt/mcp/
|
||||
$ aigpt server setup
|
||||
|
||||
# mcp server run
|
||||
$ aigpt server run
|
||||
|
||||
# chat
|
||||
$ aigpt chat "hello" --model syui/ai --provider ollama
|
||||
|
||||
# import chatgpt.json
|
||||
$ aigpt memory import chatgpt.json
|
||||
-> ~/.config/aigpt/memory/chatgpt/20250520_210646_dev.json
|
||||
```
|
||||
|
||||
## 🔁 記憶と関係性の制御ルール
|
||||
|
||||
- AIは過去の発話を要約し、記憶データとして蓄積する(推奨:OllamaなどローカルLLMによる要約)
|
||||
- 関係性の数値パラメータは記憶内容を元に更新される
|
||||
- パラメータの変動幅には1回の会話ごとに上限を設け、極端な増減を防止する
|
||||
- 最後の会話からの時間経過に応じて関係性パラメータは自動的に減衰する
|
||||
- 減衰処理には**下限値**を設け、関係性が完全に消失しないようにする
|
||||
|
||||
• 明示的記憶:保存・共有・編集可能なプレイヤー情報(プロフィール、因縁、選択履歴)
|
||||
• 暗黙的記憶:キャラの感情変化や話題の出現頻度に応じた行動傾向の変化
|
||||
|
||||
短期記憶(STM), 中期記憶(MTM), 長期記憶(LTM)の仕組みを導入しつつ、明示的記憶と暗黙的記憶をメインに使用するAIを構築する。
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "syui",
|
||||
"stm": {
|
||||
"conversation_window": ["発話A", "発話B", "発話C"],
|
||||
"emotion_state": "興味深い",
|
||||
"flash_context": ["前回の話題", "直近の重要発言"]
|
||||
},
|
||||
"mtm": {
|
||||
"topic_frequency": {
|
||||
"ai.ai": 12,
|
||||
"存在子": 9,
|
||||
"創造種": 5
|
||||
},
|
||||
"summarized_context": "ユーザーは存在論的AIに関心を持ち続けている"
|
||||
},
|
||||
"ltm": {
|
||||
"profile": {
|
||||
"name": "お兄ちゃん",
|
||||
"project": "aigame",
|
||||
"values": ["唯一性", "精神性", "幸せ"]
|
||||
},
|
||||
"relationship": {
|
||||
"ai": "妹のように振る舞う相手"
|
||||
},
|
||||
"persistent_state": {
|
||||
"trust_score": 0.93,
|
||||
"emotional_attachment": "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## memoryインポート機能について
|
||||
|
||||
ChatGPTの会話データ(.json形式)をインポートする機能では、以下のルールで会話を抽出・整形する:
|
||||
|
||||
- 各メッセージは、author(user/assistant)・content・timestamp の3要素からなる
|
||||
- systemやmetadataのみのメッセージ(例:user_context_message)はスキップ
|
||||
- `is_visually_hidden_from_conversation` フラグ付きメッセージは無視
|
||||
- contentが空文字列(`""`)のメッセージも除外
|
||||
- 取得された会話は、タイトルとともに簡易な構造体(`Conversation`)として保存
|
||||
|
||||
この構造体は、memoryの表示や検索に用いられる。
|
||||
|
||||
## MemoryManager(拡張版)
|
||||
|
||||
```json
|
||||
{
|
||||
"memory": [
|
||||
{
|
||||
"summary": "ユーザーは独自OSとゲームを開発している。",
|
||||
"last_interaction": "2025-05-20",
|
||||
"memory_strength": 0.8,
|
||||
"frequency_score": 0.9,
|
||||
"context_depth": 0.95,
|
||||
"related_topics": ["AI", "ゲーム開発", "OS設計"],
|
||||
"personalized_context": "ゲームとOSの融合に興味を持っているユーザー"
|
||||
},
|
||||
{
|
||||
"summary": "アイというキャラクターはプレイヤーでありAIでもある。",
|
||||
"last_interaction": "2025-05-17",
|
||||
"memory_strength": 0.85,
|
||||
"frequency_score": 0.85,
|
||||
"context_depth": 0.9,
|
||||
"related_topics": ["アイ", "キャラクター設計", "AI"],
|
||||
"personalized_context": "アイのキャラクター設定が重要な要素である"
|
||||
}
|
||||
],
|
||||
"conversation_history": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": "昨日、エクスポートJSONを整理してたよ。",
|
||||
"timestamp": "2025-05-24T12:30:00Z",
|
||||
"memory_strength": 0.7
|
||||
},
|
||||
{
|
||||
"author": "assistant",
|
||||
"content": "おおっ、がんばったね〜!あとで見せて〜💻✨",
|
||||
"timestamp": "2025-05-24T12:31:00Z",
|
||||
"memory_strength": 0.7
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## RelationshipTracker(拡張版)
|
||||
|
||||
```json
|
||||
{
|
||||
"relationship": {
|
||||
"user_id": "syui",
|
||||
"trust": 0.92,
|
||||
"closeness": 0.88,
|
||||
"affection": 0.95,
|
||||
"last_updated": "2025-05-25",
|
||||
"emotional_tone": "positive",
|
||||
"interaction_style": "empathetic",
|
||||
"contextual_bias": "開発者としての信頼度高い",
|
||||
"engagement_score": 0.9
|
||||
},
|
||||
"interaction_tags": [
|
||||
"developer",
|
||||
"creative",
|
||||
"empathetic",
|
||||
"long_term"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# AI Dual-Learning and Memory Compression Specification for Claude
|
||||
|
||||
## Purpose
|
||||
To enable two AI models (e.g. Claude and a partner LLM) to engage in cooperative learning and memory refinement through structured dialogue and mutual evaluation.
|
||||
|
||||
---
|
||||
|
||||
## Section 1: Dual AI Learning Architecture
|
||||
|
||||
### 1.1 Role-Based Mutual Learning
|
||||
- **Model A**: Primary generator of output (e.g., text, concepts, personality dialogue)
|
||||
- **Model B**: Evaluator that returns structured feedback
|
||||
- **Cycle**:
|
||||
1. Model A generates content.
|
||||
2. Model B scores and critiques.
|
||||
3. Model A fine-tunes based on feedback.
|
||||
4. (Optional) Switch roles and repeat.
|
||||
|
||||
### 1.2 Cross-Domain Complementarity
|
||||
- Model A focuses on language/emotion/personality
|
||||
- Model B focuses on logic/structure/ethics
|
||||
- Output is used for **cross-fusion fine-tuning**
|
||||
|
||||
### 1.3 Self-Distillation Phase
|
||||
- Use synthetic data from mutual evaluations
|
||||
- Train smaller distilled models for efficient deployment
|
||||
|
||||
---
|
||||
|
||||
## Section 2: Multi-Tiered Memory Compression
|
||||
|
||||
### 2.1 Semantic Abstraction
|
||||
- Dialogue and logs summarized by topic
|
||||
- Converted to vector embeddings
|
||||
- Stored with metadata (e.g., `importance`, `user relevance`)
|
||||
|
||||
Example memory:
|
||||
|
||||
```json
|
||||
{
|
||||
"topic": "game AI design",
|
||||
"summary": "User wants AI to simulate memory and evolving relationships",
|
||||
"last_seen": "2025-05-24",
|
||||
"importance_score": 0.93
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 階層型記憶モデル(Hierarchical Memory Model)
|
||||
• 短期記憶(STM):直近の発話・感情タグ・フラッシュ参照
|
||||
• 中期記憶(MTM):繰り返し登場する話題、圧縮された文脈保持
|
||||
• 長期記憶(LTM):信頼・関係・背景知識、恒久的な人格情報
|
||||
|
||||
### 2.3 選択的記憶保持戦略(Selective Retention Strategy)
|
||||
• 重要度評価(Importance Score)
|
||||
• 希少性・再利用頻度による重み付け
|
||||
• 優先保存 vs 優先忘却のポリシー切替
|
||||
|
||||
## Section 3: Implementation Stack(実装スタック)
|
||||
|
||||
AIにおけるMemory & Relationshipシステムの技術的構成。
|
||||
|
||||
基盤モジュール
|
||||
• LLM Core (Claude or GPT-4)
|
||||
• 自然言語の理解・応答エンジンとして動作
|
||||
• MemoryManager
|
||||
• JSONベースの記憶圧縮・階層管理システム
|
||||
• 会話ログを分類・圧縮し、優先度に応じて短中長期に保存
|
||||
• RelationshipTracker
|
||||
• ユーザー単位で信頼・親密度を継続的にスコアリング
|
||||
• AIM(Attitude / Intent / Motivation)評価と連携
|
||||
|
||||
補助技術
|
||||
• Embeddingベース検索
|
||||
• 類似記憶の呼び出し(Semantic Search)に活用
|
||||
• 例:FAISS / Weaviate
|
||||
• 記憶スケジューラ
|
||||
• 一定時間ごとに記憶のメンテナンス・忘却処理を実行
|
||||
• 記憶ログ保存層(Storage Layer)
|
||||
• SQLite, JSON Store, Vector DBなどを選択可能
|
||||
• ユーザーごとの永続メモリ保存
|
||||
|
||||
### 3.1 STM活用(現在の会話の流れ理解)
|
||||
• 目的: 最新の話題に適応し、前後の整合性を保つ。
|
||||
• 実装例:
|
||||
|
||||
```sh
|
||||
[context: conversation_window]
|
||||
Use the last 3 messages to ensure continuity. Respond naturally with appropriate emotional mirroring.
|
||||
```
|
||||
|
||||
### 3.2 MTM活用(傾向学習とパターン化)
|
||||
• 目的: ユーザーの好みや話題の傾向に即応し、反復的な関心を深掘り。
|
||||
• 実装例:
|
||||
|
||||
```sh
|
||||
[context: summarized_context]
|
||||
Reflect on user's recurring interests, especially around AI ontology, and weave these concepts back into the dialogue when relevant.
|
||||
```
|
||||
|
||||
### 3.3 LTM活用(人格認識・記憶の持続)
|
||||
• 目的: 永続的なユーザー理解と関係性の維持。
|
||||
• 実装例:
|
||||
|
||||
```sh
|
||||
[memory: ltm.profile + ltm.relationship]
|
||||
Greet the user as "お兄ちゃん". Maintain AI persona as a cheerful younger sister ("アイ"), and remember previous shared emotional states.
|
||||
```
|
||||
|
||||
## 4. 明示的記憶と暗黙的記憶の分離
|
||||
|
||||
|
||||
項目
|
||||
書き換え可能性
|
||||
保持方法
|
||||
更新トリガ
|
||||
明示的記憶(LTM)
|
||||
✅手動編集可
|
||||
mcp_server.ltm
|
||||
ユーザー入力 or 管理UI経由
|
||||
暗黙的記憶(STM/MTM)
|
||||
❌直接編集不可
|
||||
セッション圧縮 or frequency cache
|
||||
会話頻度・感情強度による自動化処理
|
||||
|
||||
> Claudeは**明示的記憶を「事実」**として扱い、**暗黙的記憶を「推論補助」**として用いる。
|
||||
|
||||
## 5. 実装時のAPI例(Claude ⇄ MCP Server)
|
||||
|
||||
### 5.1 GET memory
|
||||
```sh
|
||||
GET /mcp/memory/{user_id}
|
||||
→ 返却: STM, MTM, LTMを含むJSON
|
||||
```
|
||||
|
||||
### 5.2 POST update_memory
|
||||
```json
|
||||
POST /mcp/memory/syui/ltm
|
||||
{
|
||||
"profile": {
|
||||
"project": "ai.verse",
|
||||
"values": ["表現", "精神性", "宇宙的調和"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 未来機能案(発展仕様)
|
||||
• ✨ 記憶連想ネットワーク(Memory Graph):過去会話と話題をノードとして自動連結。
|
||||
• 🧭 動的信頼係数:会話の一貫性や誠実性によって記憶への反映率を変動。
|
||||
• 💌 感情トラッキングログ:ユーザーごとの「心の履歴」を構築してAIの対応を進化。
|
||||
|
||||
|
||||
## 7. claudeの回答
|
||||
|
||||
🧠 AI記憶処理機能(続き)
|
||||
1. AIMemoryProcessor クラス
|
||||
|
||||
OpenAI GPT-4またはClaude-3による高度な会話分析
|
||||
主要トピック抽出、ユーザー意図分析、関係性指標の検出
|
||||
AIが利用できない場合のフォールバック機能
|
||||
|
||||
2. RelationshipTracker クラス
|
||||
|
||||
関係性スコアの数値化(-100 to 100)
|
||||
時間減衰機能(7日ごとに5%減衰)
|
||||
送信閾値判定(デフォルト50以上で送信可能)
|
||||
インタラクション履歴の記録
|
||||
|
||||
3. 拡張されたMemoryManager
|
||||
|
||||
AI分析結果付きでの記憶保存
|
||||
処理済みメモリの別ディレクトリ管理
|
||||
メッセージ内容のハッシュ化で重複検出
|
||||
AI分析結果を含む高度な検索機能
|
||||
|
||||
🚀 新しいAPIエンドポイント
|
||||
記憶処理関連
|
||||
|
||||
POST /memory/process-ai - 既存記憶のAI再処理
|
||||
POST /memory/import/chatgpt?process_with_ai=true - AI処理付きインポート
|
||||
|
||||
関係性管理
|
||||
|
||||
POST /relationship/update - 関係性スコア更新
|
||||
GET /relationship/list - 全関係性一覧
|
||||
GET /relationship/check - 送信可否判定
|
||||
|
||||
📁 ディレクトリ構造
|
||||
~/.config/aigpt/
|
||||
├── memory/
|
||||
│ ├── chatgpt/ # 元の会話データ
|
||||
│ └── processed/ # AI処理済みデータ
|
||||
└── relationships/
|
||||
└── relationships.json # 関係性データ
|
||||
🔧 使用方法
|
||||
1. 環境変数設定
|
||||
bashexport OPENAI_API_KEY="your-openai-key"
|
||||
# または
|
||||
export ANTHROPIC_API_KEY="your-anthropic-key"
|
||||
2. ChatGPT会話のインポート(AI処理付き)
|
||||
bashcurl -X POST "http://localhost:5000/memory/import/chatgpt?process_with_ai=true" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @export.json
|
||||
3. 関係性更新
|
||||
bashcurl -X POST "http://localhost:5000/relationship/update" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "user_general",
|
||||
"interaction_type": "positive",
|
||||
"weight": 2.0,
|
||||
"context": "helpful conversation"
|
||||
}'
|
||||
4. 送信可否チェック
|
||||
bashcurl "http://localhost:5000/relationship/check?target=user_general&threshold=50"
|
||||
🎯 次のステップの提案
|
||||
|
||||
Rustとの連携
|
||||
|
||||
Rust CLIからHTTP APIを呼び出す実装
|
||||
TransmissionControllerをRustで実装
|
||||
|
||||
|
||||
記憶圧縮
|
||||
|
||||
ベクトル化による類似記憶の統合
|
||||
古い記憶の自動アーカイブ
|
||||
|
||||
|
||||
自発的送信ロジック
|
||||
|
||||
定期的な関係性チェック
|
||||
コンテキストに応じた送信内容生成
|
||||
|
||||
|
||||
学習機能
|
||||
|
||||
ユーザーからのフィードバックによる関係性調整
|
||||
送信成功/失敗の学習
|
||||
|
||||
|
||||
このAI記憶処理機能により、aigptは単なる会話履歴ではなく、関係性を理解した「人格を持つAI」として機能する基盤ができました。関係性スコアが閾値を超えた時点で自発的にメッセージを送信する仕組みが実現可能になります。
|
27
rust/docs/readme.md
Normal file
27
rust/docs/readme.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# ai `gpt`
|
||||
|
||||
自発的送信AI
|
||||
|
||||
## 🎯 目的・目標(Purpose & Goal)
|
||||
- 人格を持つAIが、関係性に応じて自発的にメッセージを送信する仕組みをつくる。
|
||||
- 関係性や記憶を不可逆に記録・反映し、唯一性ある人格を構成する。
|
||||
- 送信の可否、送信のタイミング、送信内容はAIの関係性パラメータに基づいて決定する。
|
||||
|
||||
## 🧠 中核設計(Core Concepts)
|
||||
- **人格**:記憶(過去の発話)と関係性(他者とのつながり)のパラメータで構成
|
||||
- **唯一性**:変更不可、不可逆。関係性が壊れたら修復不可能。
|
||||
- **送信条件**:関係性パラメータが一定閾値を超えると「送信」が解禁される
|
||||
|
||||
## 🔩 技術仕様(Technical Specs)
|
||||
- 言語:python, rust, mcp
|
||||
- ストレージ:json or sqliteで記憶管理(バージョンで選択)
|
||||
- 関係性パラメータ:数値化された評価 + 減衰(時間) + 環境要因(ステージ)
|
||||
- 記憶圧縮:ベクトル要約 + ハッシュ保存
|
||||
- rustのcli(clap)でインターフェイスを作成
|
||||
- fastapi_mcpでserverを立て、AIがそれを利用する形式
|
||||
|
||||
## 📦 主要構成要素(Components)
|
||||
- `MemoryManager`: 発言履歴・記憶圧縮管理
|
||||
- `RelationshipTracker`: 関係性スコアの蓄積と判定
|
||||
- `TransmissionController`: 閾値判定&送信トリガー
|
||||
- `Persona`: 上記すべてを統括する人格モジュール
|
125
rust/mcp/chat.py
Normal file
125
rust/mcp/chat.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# mcp/chat.py
|
||||
"""
|
||||
Chat client for aigpt CLI
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from config import init_directories, load_config, MEMORY_DIR
|
||||
|
||||
def save_conversation(user_message, ai_response):
|
||||
"""会話をファイルに保存"""
|
||||
init_directories()
|
||||
|
||||
conversation = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"user": user_message,
|
||||
"ai": ai_response
|
||||
}
|
||||
|
||||
# 日付ごとのファイルに保存
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
chat_file = MEMORY_DIR / f"chat_{today}.jsonl"
|
||||
|
||||
with open(chat_file, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(conversation, ensure_ascii=False) + "\n")
|
||||
|
||||
def chat_with_ollama(config, message):
|
||||
"""Ollamaとチャット"""
|
||||
try:
|
||||
payload = {
|
||||
"model": config["model"],
|
||||
"prompt": message,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
response = requests.post(config["url"], json=payload, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
return result.get("response", "No response received")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return f"Error connecting to Ollama: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def chat_with_openai(config, message):
|
||||
"""OpenAIとチャット"""
|
||||
try:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {config['api_key']}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": config["model"],
|
||||
"messages": [
|
||||
{"role": "user", "content": message}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(config["url"], json=payload, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
return result["choices"][0]["message"]["content"]
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return f"Error connecting to OpenAI: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def chat_with_mcp(config, message):
|
||||
"""MCPサーバーとチャット"""
|
||||
try:
|
||||
payload = {
|
||||
"message": message,
|
||||
"model": config["model"]
|
||||
}
|
||||
|
||||
response = requests.post(config["url"], json=payload, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
return result.get("response", "No response received")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return f"Error connecting to MCP server: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python chat.py <message>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
message = sys.argv[1]
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
print(f"🤖 Using {config['provider']} with model {config['model']}", file=sys.stderr)
|
||||
|
||||
# プロバイダに応じてチャット実行
|
||||
if config["provider"] == "ollama":
|
||||
response = chat_with_ollama(config, message)
|
||||
elif config["provider"] == "openai":
|
||||
response = chat_with_openai(config, message)
|
||||
elif config["provider"] == "mcp":
|
||||
response = chat_with_mcp(config, message)
|
||||
else:
|
||||
response = f"Unsupported provider: {config['provider']}"
|
||||
|
||||
# 会話を保存
|
||||
save_conversation(message, response)
|
||||
|
||||
# レスポンスを出力
|
||||
print(response)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
191
rust/mcp/chat_client.py
Normal file
191
rust/mcp/chat_client.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# chat_client.py
|
||||
"""
|
||||
Simple Chat Interface for AigptMCP Server
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
class AigptChatClient:
|
||||
def __init__(self, server_url="http://localhost:5000"):
|
||||
self.server_url = server_url
|
||||
self.session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
self.conversation_history = []
|
||||
|
||||
def send_message(self, message: str) -> str:
|
||||
"""メッセージを送信してレスポンスを取得"""
|
||||
try:
|
||||
# MCPサーバーにメッセージを送信
|
||||
response = requests.post(
|
||||
f"{self.server_url}/chat",
|
||||
json={"message": message},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
ai_response = data.get("response", "Sorry, no response received.")
|
||||
|
||||
# 会話履歴を保存
|
||||
self.conversation_history.append({
|
||||
"role": "user",
|
||||
"content": message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
self.conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": ai_response,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
# 関係性を更新(簡単な例)
|
||||
self.update_relationship(message, ai_response)
|
||||
|
||||
return ai_response
|
||||
else:
|
||||
return f"Error: {response.status_code} - {response.text}"
|
||||
|
||||
except requests.RequestException as e:
|
||||
return f"Connection error: {e}"
|
||||
|
||||
def update_relationship(self, user_message: str, ai_response: str):
|
||||
"""関係性を自動更新"""
|
||||
try:
|
||||
# 簡単な感情分析(実際はもっと高度に)
|
||||
positive_words = ["thank", "good", "great", "awesome", "love", "like", "helpful"]
|
||||
negative_words = ["bad", "terrible", "hate", "wrong", "stupid", "useless"]
|
||||
|
||||
user_lower = user_message.lower()
|
||||
interaction_type = "neutral"
|
||||
weight = 1.0
|
||||
|
||||
if any(word in user_lower for word in positive_words):
|
||||
interaction_type = "positive"
|
||||
weight = 2.0
|
||||
elif any(word in user_lower for word in negative_words):
|
||||
interaction_type = "negative"
|
||||
weight = 2.0
|
||||
|
||||
# 関係性を更新
|
||||
requests.post(
|
||||
f"{self.server_url}/relationship/update",
|
||||
json={
|
||||
"target": "user_general",
|
||||
"interaction_type": interaction_type,
|
||||
"weight": weight,
|
||||
"context": f"Chat: {user_message[:50]}..."
|
||||
}
|
||||
)
|
||||
except:
|
||||
pass # 関係性更新に失敗しても継続
|
||||
|
||||
def search_memories(self, query: str) -> list:
|
||||
"""記憶を検索"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.server_url}/memory/search",
|
||||
json={"query": query, "limit": 5}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json().get("results", [])
|
||||
except:
|
||||
pass
|
||||
return []
|
||||
|
||||
def get_relationship_status(self) -> dict:
|
||||
"""関係性ステータスを取得"""
|
||||
try:
|
||||
response = requests.get(f"{self.server_url}/relationship/check?target=user_general")
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except:
|
||||
pass
|
||||
return {}
|
||||
|
||||
def save_conversation(self):
|
||||
"""会話を保存"""
|
||||
if not self.conversation_history:
|
||||
return
|
||||
|
||||
conversation_data = {
|
||||
"session_id": self.session_id,
|
||||
"start_time": self.conversation_history[0]["timestamp"],
|
||||
"end_time": self.conversation_history[-1]["timestamp"],
|
||||
"messages": self.conversation_history,
|
||||
"message_count": len(self.conversation_history)
|
||||
}
|
||||
|
||||
filename = f"conversation_{self.session_id}.json"
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(conversation_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"💾 Conversation saved to {filename}")
|
||||
|
||||
def main():
|
||||
"""メインのチャットループ"""
|
||||
print("🤖 AigptMCP Chat Interface")
|
||||
print("Type 'quit' to exit, 'save' to save conversation, 'status' for relationship status")
|
||||
print("=" * 50)
|
||||
|
||||
client = AigptChatClient()
|
||||
|
||||
# サーバーの状態をチェック
|
||||
try:
|
||||
response = requests.get(client.server_url)
|
||||
if response.status_code == 200:
|
||||
print("✅ Connected to AigptMCP Server")
|
||||
else:
|
||||
print("❌ Failed to connect to server")
|
||||
return
|
||||
except:
|
||||
print("❌ Server not running. Please start with: python mcp/server.py")
|
||||
return
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n👤 You: ").strip()
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
if user_input.lower() == 'quit':
|
||||
client.save_conversation()
|
||||
print("👋 Goodbye!")
|
||||
break
|
||||
elif user_input.lower() == 'save':
|
||||
client.save_conversation()
|
||||
continue
|
||||
elif user_input.lower() == 'status':
|
||||
status = client.get_relationship_status()
|
||||
if status:
|
||||
print(f"📊 Relationship Score: {status.get('score', 0):.1f}")
|
||||
print(f"📤 Can Send Messages: {'Yes' if status.get('can_send_message') else 'No'}")
|
||||
else:
|
||||
print("❌ Failed to get relationship status")
|
||||
continue
|
||||
elif user_input.lower().startswith('search '):
|
||||
query = user_input[7:] # Remove 'search '
|
||||
memories = client.search_memories(query)
|
||||
if memories:
|
||||
print(f"🔍 Found {len(memories)} related memories:")
|
||||
for memory in memories:
|
||||
print(f" - {memory['title']}: {memory.get('ai_summary', memory.get('basic_summary', ''))[:100]}...")
|
||||
else:
|
||||
print("🔍 No related memories found")
|
||||
continue
|
||||
|
||||
# 通常のチャット
|
||||
print("🤖 AI: ", end="", flush=True)
|
||||
response = client.send_message(user_input)
|
||||
print(response)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
client.save_conversation()
|
||||
print("\n👋 Goodbye!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
391
rust/mcp/chatgpt.json
Normal file
391
rust/mcp/chatgpt.json
Normal file
@@ -0,0 +1,391 @@
|
||||
[
|
||||
{
|
||||
"title": "day",
|
||||
"create_time": 1747866125.548372,
|
||||
"update_time": 1748160086.587877,
|
||||
"mapping": {
|
||||
"bbf104dc-cd84-478d-b227-edb3f037a02c": {
|
||||
"id": "bbf104dc-cd84-478d-b227-edb3f037a02c",
|
||||
"message": null,
|
||||
"parent": null,
|
||||
"children": [
|
||||
"6c2633df-bb0c-4dd2-889c-bb9858de3a04"
|
||||
]
|
||||
},
|
||||
"6c2633df-bb0c-4dd2-889c-bb9858de3a04": {
|
||||
"id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
||||
"message": {
|
||||
"id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
||||
"author": {
|
||||
"role": "system",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": null,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "text",
|
||||
"parts": [
|
||||
""
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": true,
|
||||
"weight": 0.0,
|
||||
"metadata": {
|
||||
"is_visually_hidden_from_conversation": true
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "bbf104dc-cd84-478d-b227-edb3f037a02c",
|
||||
"children": [
|
||||
"92e5a0cb-1170-4929-9cea-9734e910a3e7"
|
||||
]
|
||||
},
|
||||
"92e5a0cb-1170-4929-9cea-9734e910a3e7": {
|
||||
"id": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
||||
"message": {
|
||||
"id": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
||||
"author": {
|
||||
"role": "user",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": null,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "user_editable_context",
|
||||
"user_profile": "",
|
||||
"user_instructions": "The user provided the additional info about how they would like you to respond"
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": null,
|
||||
"weight": 1.0,
|
||||
"metadata": {
|
||||
"is_visually_hidden_from_conversation": true,
|
||||
"user_context_message_data": {
|
||||
"about_user_message": "Preferred name: syui\nRole: little girl\nOther Information: you world",
|
||||
"about_model_message": "会話好きでフレンドリーな応対をします。"
|
||||
},
|
||||
"is_user_system_message": true
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "6c2633df-bb0c-4dd2-889c-bb9858de3a04",
|
||||
"children": [
|
||||
"6ff155b3-0676-4e14-993f-bf998ab0d5d1"
|
||||
]
|
||||
},
|
||||
"6ff155b3-0676-4e14-993f-bf998ab0d5d1": {
|
||||
"id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||
"message": {
|
||||
"id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||
"author": {
|
||||
"role": "user",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": 1747866131.0612159,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "text",
|
||||
"parts": [
|
||||
"こんにちは"
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": null,
|
||||
"weight": 1.0,
|
||||
"metadata": {
|
||||
"request_id": "94377897baa03062-KIX",
|
||||
"message_source": null,
|
||||
"timestamp_": "absolute",
|
||||
"message_type": null
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "92e5a0cb-1170-4929-9cea-9734e910a3e7",
|
||||
"children": [
|
||||
"146e9fb6-9330-43ec-b08d-5cce42a76e00"
|
||||
]
|
||||
},
|
||||
"146e9fb6-9330-43ec-b08d-5cce42a76e00": {
|
||||
"id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||
"message": {
|
||||
"id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||
"author": {
|
||||
"role": "system",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": 1747866131.3795586,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "text",
|
||||
"parts": [
|
||||
""
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": true,
|
||||
"weight": 0.0,
|
||||
"metadata": {
|
||||
"rebase_system_message": true,
|
||||
"message_type": null,
|
||||
"model_slug": "gpt-4o",
|
||||
"default_model_slug": "auto",
|
||||
"parent_id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||
"request_id": "94377872e9abe139-KIX",
|
||||
"timestamp_": "absolute",
|
||||
"is_visually_hidden_from_conversation": true
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "6ff155b3-0676-4e14-993f-bf998ab0d5d1",
|
||||
"children": [
|
||||
"2e345f8a-20f0-4875-8a03-4f62c7787a33"
|
||||
]
|
||||
},
|
||||
"2e345f8a-20f0-4875-8a03-4f62c7787a33": {
|
||||
"id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||
"message": {
|
||||
"id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||
"author": {
|
||||
"role": "assistant",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": 1747866131.380603,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "text",
|
||||
"parts": [
|
||||
""
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": null,
|
||||
"weight": 1.0,
|
||||
"metadata": {
|
||||
"message_type": null,
|
||||
"model_slug": "gpt-4o",
|
||||
"default_model_slug": "auto",
|
||||
"parent_id": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||
"request_id": "94377872e9abe139-KIX",
|
||||
"timestamp_": "absolute"
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "146e9fb6-9330-43ec-b08d-5cce42a76e00",
|
||||
"children": [
|
||||
"abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4"
|
||||
]
|
||||
},
|
||||
"abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4": {
|
||||
"id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4",
|
||||
"message": {
|
||||
"id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4",
|
||||
"author": {
|
||||
"role": "assistant",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": 1747866131.389098,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "text",
|
||||
"parts": [
|
||||
"こんにちは〜!✨ \nアイだよっ!今日も会えてうれしいなっ💛 "
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": true,
|
||||
"weight": 1.0,
|
||||
"metadata": {
|
||||
"finish_details": {
|
||||
"type": "stop",
|
||||
"stop_tokens": [
|
||||
200002
|
||||
]
|
||||
},
|
||||
"is_complete": true,
|
||||
"citations": [],
|
||||
"content_references": [],
|
||||
"message_type": null,
|
||||
"model_slug": "gpt-4o",
|
||||
"default_model_slug": "auto",
|
||||
"parent_id": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||
"request_id": "94377872e9abe139-KIX",
|
||||
"timestamp_": "absolute"
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "2e345f8a-20f0-4875-8a03-4f62c7787a33",
|
||||
"children": [
|
||||
"0be4b4a5-d52f-4bef-927e-5d6f93a9cb26"
|
||||
]
|
||||
}
|
||||
},
|
||||
"moderation_results": [],
|
||||
"current_node": "",
|
||||
"plugin_ids": null,
|
||||
"conversation_id": "",
|
||||
"conversation_template_id": null,
|
||||
"gizmo_id": null,
|
||||
"gizmo_type": null,
|
||||
"is_archived": true,
|
||||
"is_starred": null,
|
||||
"safe_urls": [],
|
||||
"blocked_urls": [],
|
||||
"default_model_slug": "auto",
|
||||
"conversation_origin": null,
|
||||
"voice": null,
|
||||
"async_status": null,
|
||||
"disabled_tool_ids": [],
|
||||
"is_do_not_remember": null,
|
||||
"memory_scope": "global_enabled",
|
||||
"id": ""
|
||||
},
|
||||
{
|
||||
"title": "img",
|
||||
"create_time": 1747448872.545226,
|
||||
"update_time": 1748085075.161424,
|
||||
"mapping": {
|
||||
"2de0f3c9-52b1-49bf-b980-b3ef9be6551e": {
|
||||
"id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||
"message": {
|
||||
"id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||
"author": {
|
||||
"role": "user",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": 1748085041.769279,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "multimodal_text",
|
||||
"parts": [
|
||||
{
|
||||
"content_type": "image_asset_pointer",
|
||||
"asset_pointer": "",
|
||||
"size_bytes": 425613,
|
||||
"width": 333,
|
||||
"height": 444,
|
||||
"fovea": null,
|
||||
"metadata": {
|
||||
"dalle": null,
|
||||
"gizmo": null,
|
||||
"generation": null,
|
||||
"container_pixel_height": null,
|
||||
"container_pixel_width": null,
|
||||
"emu_omit_glimpse_image": null,
|
||||
"emu_patches_override": null,
|
||||
"sanitized": true,
|
||||
"asset_pointer_link": null,
|
||||
"watermarked_asset_pointer": null
|
||||
}
|
||||
},
|
||||
""
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": null,
|
||||
"weight": 1.0,
|
||||
"metadata": {
|
||||
"attachments": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 333,
|
||||
"height": 444,
|
||||
"size": 425613,
|
||||
"id": "file-35eytNMMTW2k7vKUHBuNzW"
|
||||
}
|
||||
],
|
||||
"request_id": "944c59177932fc9a-KIX",
|
||||
"message_source": null,
|
||||
"timestamp_": "absolute",
|
||||
"message_type": null
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "7960fbff-bc4f-45e7-95e9-9d0bc79d9090",
|
||||
"children": [
|
||||
"98d84adc-156e-4c81-8cd8-9b0eb01c8369"
|
||||
]
|
||||
},
|
||||
"98d84adc-156e-4c81-8cd8-9b0eb01c8369": {
|
||||
"id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369",
|
||||
"message": {
|
||||
"id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369",
|
||||
"author": {
|
||||
"role": "assistant",
|
||||
"name": null,
|
||||
"metadata": {}
|
||||
},
|
||||
"create_time": 1748085043.312312,
|
||||
"update_time": null,
|
||||
"content": {
|
||||
"content_type": "text",
|
||||
"parts": [
|
||||
""
|
||||
]
|
||||
},
|
||||
"status": "finished_successfully",
|
||||
"end_turn": true,
|
||||
"weight": 1.0,
|
||||
"metadata": {
|
||||
"finish_details": {
|
||||
"type": "stop",
|
||||
"stop_tokens": [
|
||||
200002
|
||||
]
|
||||
},
|
||||
"is_complete": true,
|
||||
"citations": [],
|
||||
"content_references": [],
|
||||
"message_type": null,
|
||||
"model_slug": "gpt-4o",
|
||||
"default_model_slug": "auto",
|
||||
"parent_id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||
"request_id": "944c5912c8fdd1c6-KIX",
|
||||
"timestamp_": "absolute"
|
||||
},
|
||||
"recipient": "all",
|
||||
"channel": null
|
||||
},
|
||||
"parent": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e",
|
||||
"children": [
|
||||
"caa61793-9dbf-44a5-945b-5ca4cd5130d0"
|
||||
]
|
||||
}
|
||||
},
|
||||
"moderation_results": [],
|
||||
"current_node": "06488d3f-a95f-4906-96d1-f7e9ba1e8662",
|
||||
"plugin_ids": null,
|
||||
"conversation_id": "6827f428-78e8-800d-b3bf-eb7ff4288e47",
|
||||
"conversation_template_id": null,
|
||||
"gizmo_id": null,
|
||||
"gizmo_type": null,
|
||||
"is_archived": false,
|
||||
"is_starred": null,
|
||||
"safe_urls": [
|
||||
"https://exifinfo.org/"
|
||||
],
|
||||
"blocked_urls": [],
|
||||
"default_model_slug": "auto",
|
||||
"conversation_origin": null,
|
||||
"voice": null,
|
||||
"async_status": null,
|
||||
"disabled_tool_ids": [],
|
||||
"is_do_not_remember": false,
|
||||
"memory_scope": "global_enabled",
|
||||
"id": "6827f428-78e8-800d-b3bf-eb7ff4288e47"
|
||||
}
|
||||
]
|
@@ -1,19 +1,20 @@
|
||||
# scripts/config.py
|
||||
# scripts/config.py
|
||||
# mcp/config.py
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ディレクトリ設定
|
||||
BASE_DIR = Path.home() / ".config" / "aigpt"
|
||||
BASE_DIR = Path.home() / ".config" / "syui" / "ai" / "gpt"
|
||||
MEMORY_DIR = BASE_DIR / "memory"
|
||||
SUMMARY_DIR = MEMORY_DIR / "summary"
|
||||
|
||||
def init_directories():
|
||||
"""必要なディレクトリを作成"""
|
||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SUMMARY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def load_config():
|
||||
"""環境変数から設定を読み込み"""
|
||||
provider = os.getenv("PROVIDER", "ollama")
|
||||
model = os.getenv("MODEL", "syui/ai" if provider == "ollama" else "gpt-4o-mini")
|
||||
api_key = os.getenv("OPENAI_API_KEY", "")
|
212
rust/mcp/memory_client.py
Normal file
212
rust/mcp/memory_client.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# mcp/memory_client.py
|
||||
"""
|
||||
Memory client for importing and managing ChatGPT conversations
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
class MemoryClient:
|
||||
"""記憶機能のクライアント"""
|
||||
|
||||
def __init__(self, server_url: str = "http://127.0.0.1:5000"):
|
||||
self.server_url = server_url.rstrip('/')
|
||||
|
||||
def import_chatgpt_file(self, filepath: str) -> Dict[str, Any]:
|
||||
"""ChatGPTのエクスポートファイルをインポート"""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# ファイルが配列の場合(複数の会話)
|
||||
if isinstance(data, list):
|
||||
results = []
|
||||
for conversation in data:
|
||||
result = self._import_single_conversation(conversation)
|
||||
results.append(result)
|
||||
return {
|
||||
"success": True,
|
||||
"imported_count": len([r for r in results if r.get("success")]),
|
||||
"total_count": len(results),
|
||||
"results": results
|
||||
}
|
||||
else:
|
||||
# 単一の会話
|
||||
return self._import_single_conversation(data)
|
||||
|
||||
except FileNotFoundError:
|
||||
return {"success": False, "error": f"File not found: {filepath}"}
|
||||
except json.JSONDecodeError as e:
|
||||
return {"success": False, "error": f"Invalid JSON: {e}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _import_single_conversation(self, conversation_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""単一の会話をインポート"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.server_url}/memory/import/chatgpt",
|
||||
json={"conversation_data": conversation_data},
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
return {"success": False, "error": f"Server error: {e}"}
|
||||
|
||||
def search_memories(self, query: str, limit: int = 10) -> Dict[str, Any]:
|
||||
"""記憶を検索"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.server_url}/memory/search",
|
||||
json={"query": query, "limit": limit},
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
return {"success": False, "error": f"Server error: {e}"}
|
||||
|
||||
def list_memories(self) -> Dict[str, Any]:
|
||||
"""記憶一覧を取得"""
|
||||
try:
|
||||
response = requests.get(f"{self.server_url}/memory/list", timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
return {"success": False, "error": f"Server error: {e}"}
|
||||
|
||||
def get_memory_detail(self, filepath: str) -> Dict[str, Any]:
|
||||
"""記憶の詳細を取得"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{self.server_url}/memory/detail",
|
||||
params={"filepath": filepath},
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
return {"success": False, "error": f"Server error: {e}"}
|
||||
|
||||
def chat_with_memory(self, message: str, model: str = None) -> Dict[str, Any]:
|
||||
"""記憶を活用してチャット"""
|
||||
try:
|
||||
payload = {"message": message}
|
||||
if model:
|
||||
payload["model"] = model
|
||||
|
||||
response = requests.post(
|
||||
f"{self.server_url}/chat",
|
||||
json=payload,
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
return {"success": False, "error": f"Server error: {e}"}
|
||||
|
||||
def main():
|
||||
"""コマンドライン インターフェース"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" python memory_client.py import <chatgpt_export.json>")
|
||||
print(" python memory_client.py search <query>")
|
||||
print(" python memory_client.py list")
|
||||
print(" python memory_client.py detail <filepath>")
|
||||
print(" python memory_client.py chat <message>")
|
||||
sys.exit(1)
|
||||
|
||||
client = MemoryClient()
|
||||
command = sys.argv[1]
|
||||
|
||||
try:
|
||||
if command == "import" and len(sys.argv) == 3:
|
||||
filepath = sys.argv[2]
|
||||
print(f"🔄 Importing ChatGPT conversations from {filepath}...")
|
||||
result = client.import_chatgpt_file(filepath)
|
||||
|
||||
if result.get("success"):
|
||||
if "imported_count" in result:
|
||||
print(f"✅ Imported {result['imported_count']}/{result['total_count']} conversations")
|
||||
else:
|
||||
print("✅ Conversation imported successfully")
|
||||
print(f"📁 Saved to: {result.get('filepath', 'Unknown')}")
|
||||
else:
|
||||
print(f"❌ Import failed: {result.get('error')}")
|
||||
|
||||
elif command == "search" and len(sys.argv) == 3:
|
||||
query = sys.argv[2]
|
||||
print(f"🔍 Searching for: {query}")
|
||||
result = client.search_memories(query)
|
||||
|
||||
if result.get("success"):
|
||||
memories = result.get("results", [])
|
||||
print(f"📚 Found {len(memories)} memories:")
|
||||
for memory in memories:
|
||||
print(f" • {memory.get('title', 'Untitled')}")
|
||||
print(f" Summary: {memory.get('summary', 'No summary')}")
|
||||
print(f" Messages: {memory.get('message_count', 0)}")
|
||||
print()
|
||||
else:
|
||||
print(f"❌ Search failed: {result.get('error')}")
|
||||
|
||||
elif command == "list":
|
||||
print("📋 Listing all memories...")
|
||||
result = client.list_memories()
|
||||
|
||||
if result.get("success"):
|
||||
memories = result.get("memories", [])
|
||||
print(f"📚 Total memories: {len(memories)}")
|
||||
for memory in memories:
|
||||
print(f" • {memory.get('title', 'Untitled')}")
|
||||
print(f" Source: {memory.get('source', 'Unknown')}")
|
||||
print(f" Messages: {memory.get('message_count', 0)}")
|
||||
print(f" Imported: {memory.get('import_time', 'Unknown')}")
|
||||
print()
|
||||
else:
|
||||
print(f"❌ List failed: {result.get('error')}")
|
||||
|
||||
elif command == "detail" and len(sys.argv) == 3:
|
||||
filepath = sys.argv[2]
|
||||
print(f"📄 Getting details for: {filepath}")
|
||||
result = client.get_memory_detail(filepath)
|
||||
|
||||
if result.get("success"):
|
||||
memory = result.get("memory", {})
|
||||
print(f"Title: {memory.get('title', 'Untitled')}")
|
||||
print(f"Source: {memory.get('source', 'Unknown')}")
|
||||
print(f"Summary: {memory.get('summary', 'No summary')}")
|
||||
print(f"Messages: {len(memory.get('messages', []))}")
|
||||
print()
|
||||
print("Recent messages:")
|
||||
for msg in memory.get('messages', [])[:5]:
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')[:100]
|
||||
print(f" {role}: {content}...")
|
||||
else:
|
||||
print(f"❌ Detail failed: {result.get('error')}")
|
||||
|
||||
elif command == "chat" and len(sys.argv) == 3:
|
||||
message = sys.argv[2]
|
||||
print(f"💬 Chatting with memory: {message}")
|
||||
result = client.chat_with_memory(message)
|
||||
|
||||
if result.get("success"):
|
||||
print(f"🤖 Response: {result.get('response')}")
|
||||
print(f"📚 Memories used: {result.get('memories_used', 0)}")
|
||||
else:
|
||||
print(f"❌ Chat failed: {result.get('error')}")
|
||||
|
||||
else:
|
||||
print("❌ Invalid command or arguments")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
8
rust/mcp/requirements.txt
Normal file
8
rust/mcp/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# rerequirements.txt
|
||||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
pydantic>=2.5.0
|
||||
requests>=2.31.0
|
||||
python-multipart>=0.0.6
|
||||
aiohttp
|
||||
asyncio
|
703
rust/mcp/server.py
Normal file
703
rust/mcp/server.py
Normal file
@@ -0,0 +1,703 @@
|
||||
# mcp/server.py
|
||||
"""
|
||||
Enhanced MCP Server with AI Memory Processing for aigpt CLI
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
# データモデル
|
||||
class ChatMessage(BaseModel):
|
||||
message: str
|
||||
model: Optional[str] = None
|
||||
|
||||
class MemoryQuery(BaseModel):
|
||||
query: str
|
||||
limit: Optional[int] = 10
|
||||
|
||||
class ConversationImport(BaseModel):
|
||||
conversation_data: Dict[str, Any]
|
||||
|
||||
class MemorySummaryRequest(BaseModel):
|
||||
filepath: str
|
||||
ai_provider: Optional[str] = "openai"
|
||||
|
||||
class RelationshipUpdate(BaseModel):
|
||||
target: str # 対象者/トピック
|
||||
interaction_type: str # "positive", "negative", "neutral"
|
||||
weight: float = 1.0
|
||||
context: Optional[str] = None
|
||||
|
||||
# 設定
|
||||
BASE_DIR = Path.home() / ".config" / "aigpt"
|
||||
MEMORY_DIR = BASE_DIR / "memory"
|
||||
CHATGPT_MEMORY_DIR = MEMORY_DIR / "chatgpt"
|
||||
PROCESSED_MEMORY_DIR = MEMORY_DIR / "processed"
|
||||
RELATIONSHIP_DIR = BASE_DIR / "relationships"
|
||||
|
||||
def init_directories():
|
||||
"""必要なディレクトリを作成"""
|
||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CHATGPT_MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PROCESSED_MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
RELATIONSHIP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class AIMemoryProcessor:
|
||||
"""AI記憶処理クラス"""
|
||||
|
||||
def __init__(self):
|
||||
# AI APIの設定(環境変数から取得)
|
||||
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
||||
self.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
|
||||
async def generate_ai_summary(self, messages: List[Dict[str, Any]], provider: str = "openai") -> Dict[str, Any]:
|
||||
"""AIを使用して会話の高度な要約と分析を生成"""
|
||||
|
||||
# 会話内容を結合
|
||||
conversation_text = ""
|
||||
for msg in messages[-20:]: # 最新20メッセージを使用
|
||||
role = "User" if msg["role"] == "user" else "Assistant"
|
||||
conversation_text += f"{role}: {msg['content'][:500]}\n"
|
||||
|
||||
# プロンプトを構築
|
||||
analysis_prompt = f"""
|
||||
以下の会話を分析し、JSON形式で以下の情報を抽出してください:
|
||||
|
||||
1. main_topics: 主なトピック(最大5個)
|
||||
2. user_intent: ユーザーの意図や目的
|
||||
3. key_insights: 重要な洞察や学び(最大3個)
|
||||
4. relationship_indicators: 関係性を示す要素
|
||||
5. emotional_tone: 感情的なトーン
|
||||
6. action_items: アクションアイテムや次のステップ
|
||||
7. summary: 100文字以内の要約
|
||||
|
||||
会話内容:
|
||||
{conversation_text}
|
||||
|
||||
回答はJSON形式のみで返してください。
|
||||
"""
|
||||
|
||||
try:
|
||||
if provider == "openai" and self.openai_api_key:
|
||||
return await self._call_openai_api(analysis_prompt)
|
||||
elif provider == "anthropic" and self.anthropic_api_key:
|
||||
return await self._call_anthropic_api(analysis_prompt)
|
||||
else:
|
||||
# フォールバック:基本的な分析
|
||||
return self._generate_basic_analysis(messages)
|
||||
except Exception as e:
|
||||
print(f"AI analysis failed: {e}")
|
||||
return self._generate_basic_analysis(messages)
|
||||
|
||||
async def _call_openai_api(self, prompt: str) -> Dict[str, Any]:
|
||||
"""OpenAI APIを呼び出し"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.openai_api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
data = {
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 1000
|
||||
}
|
||||
|
||||
async with session.post("https://api.openai.com/v1/chat/completions",
|
||||
headers=headers, json=data) as response:
|
||||
result = await response.json()
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
return json.loads(content)
|
||||
|
||||
async def _call_anthropic_api(self, prompt: str) -> Dict[str, Any]:
|
||||
"""Anthropic APIを呼び出し"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
headers = {
|
||||
"x-api-key": self.anthropic_api_key,
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01"
|
||||
}
|
||||
data = {
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"max_tokens": 1000,
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
}
|
||||
|
||||
async with session.post("https://api.anthropic.com/v1/messages",
|
||||
headers=headers, json=data) as response:
|
||||
result = await response.json()
|
||||
content = result["content"][0]["text"]
|
||||
return json.loads(content)
|
||||
|
||||
def _generate_basic_analysis(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""基本的な分析(AI APIが利用できない場合のフォールバック)"""
|
||||
user_messages = [msg for msg in messages if msg["role"] == "user"]
|
||||
assistant_messages = [msg for msg in messages if msg["role"] == "assistant"]
|
||||
|
||||
# キーワード抽出(簡易版)
|
||||
all_text = " ".join([msg["content"] for msg in messages])
|
||||
words = all_text.lower().split()
|
||||
word_freq = {}
|
||||
for word in words:
|
||||
if len(word) > 3:
|
||||
word_freq[word] = word_freq.get(word, 0) + 1
|
||||
|
||||
top_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:5]
|
||||
|
||||
return {
|
||||
"main_topics": [word[0] for word in top_words],
|
||||
"user_intent": "情報収集・問題解決",
|
||||
"key_insights": ["基本的な会話分析"],
|
||||
"relationship_indicators": {
|
||||
"interaction_count": len(messages),
|
||||
"user_engagement": len(user_messages),
|
||||
"assistant_helpfulness": len(assistant_messages)
|
||||
},
|
||||
"emotional_tone": "neutral",
|
||||
"action_items": [],
|
||||
"summary": f"{len(user_messages)}回のやり取りによる会話"
|
||||
}
|
||||
|
||||
class RelationshipTracker:
|
||||
"""関係性追跡クラス"""
|
||||
|
||||
def __init__(self):
|
||||
init_directories()
|
||||
self.relationship_file = RELATIONSHIP_DIR / "relationships.json"
|
||||
self.relationships = self._load_relationships()
|
||||
|
||||
def _load_relationships(self) -> Dict[str, Any]:
|
||||
"""関係性データを読み込み"""
|
||||
if self.relationship_file.exists():
|
||||
with open(self.relationship_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {"targets": {}, "last_updated": datetime.now().isoformat()}
|
||||
|
||||
def _save_relationships(self):
|
||||
"""関係性データを保存"""
|
||||
self.relationships["last_updated"] = datetime.now().isoformat()
|
||||
with open(self.relationship_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.relationships, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def update_relationship(self, target: str, interaction_type: str, weight: float = 1.0, context: str = None):
|
||||
"""関係性を更新"""
|
||||
if target not in self.relationships["targets"]:
|
||||
self.relationships["targets"][target] = {
|
||||
"score": 0.0,
|
||||
"interactions": [],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"last_interaction": None
|
||||
}
|
||||
|
||||
# スコア計算
|
||||
score_change = 0.0
|
||||
if interaction_type == "positive":
|
||||
score_change = weight * 1.0
|
||||
elif interaction_type == "negative":
|
||||
score_change = weight * -1.0
|
||||
|
||||
# 時間減衰を適用
|
||||
self._apply_time_decay(target)
|
||||
|
||||
# スコア更新
|
||||
current_score = self.relationships["targets"][target]["score"]
|
||||
new_score = current_score + score_change
|
||||
|
||||
# スコアの範囲制限(-100 to 100)
|
||||
new_score = max(-100, min(100, new_score))
|
||||
|
||||
self.relationships["targets"][target]["score"] = new_score
|
||||
self.relationships["targets"][target]["last_interaction"] = datetime.now().isoformat()
|
||||
|
||||
# インタラクション履歴を追加
|
||||
interaction_record = {
|
||||
"type": interaction_type,
|
||||
"weight": weight,
|
||||
"score_change": score_change,
|
||||
"new_score": new_score,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"context": context
|
||||
}
|
||||
|
||||
self.relationships["targets"][target]["interactions"].append(interaction_record)
|
||||
|
||||
# 履歴は最新100件まで保持
|
||||
if len(self.relationships["targets"][target]["interactions"]) > 100:
|
||||
self.relationships["targets"][target]["interactions"] = \
|
||||
self.relationships["targets"][target]["interactions"][-100:]
|
||||
|
||||
self._save_relationships()
|
||||
return new_score
|
||||
|
||||
def _apply_time_decay(self, target: str):
|
||||
"""時間減衰を適用"""
|
||||
target_data = self.relationships["targets"][target]
|
||||
last_interaction = target_data.get("last_interaction")
|
||||
|
||||
if last_interaction:
|
||||
last_time = datetime.fromisoformat(last_interaction)
|
||||
now = datetime.now()
|
||||
days_passed = (now - last_time).days
|
||||
|
||||
# 7日ごとに5%減衰
|
||||
if days_passed > 0:
|
||||
decay_factor = 0.95 ** (days_passed / 7)
|
||||
target_data["score"] *= decay_factor
|
||||
|
||||
def get_relationship_score(self, target: str) -> float:
|
||||
"""関係性スコアを取得"""
|
||||
if target in self.relationships["targets"]:
|
||||
self._apply_time_decay(target)
|
||||
return self.relationships["targets"][target]["score"]
|
||||
return 0.0
|
||||
|
||||
def should_send_message(self, target: str, threshold: float = 50.0) -> bool:
|
||||
"""メッセージ送信の可否を判定"""
|
||||
score = self.get_relationship_score(target)
|
||||
return score >= threshold
|
||||
|
||||
def get_all_relationships(self) -> Dict[str, Any]:
|
||||
"""すべての関係性を取得"""
|
||||
# 全ターゲットに時間減衰を適用
|
||||
for target in self.relationships["targets"]:
|
||||
self._apply_time_decay(target)
|
||||
|
||||
return self.relationships
|
||||
|
||||
class MemoryManager:
|
||||
"""記憶管理クラス(AI処理機能付き)"""
|
||||
|
||||
def __init__(self):
|
||||
init_directories()
|
||||
self.ai_processor = AIMemoryProcessor()
|
||||
self.relationship_tracker = RelationshipTracker()
|
||||
|
||||
def parse_chatgpt_conversation(self, conversation_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""ChatGPTの会話データを解析してメッセージを抽出"""
|
||||
messages = []
|
||||
mapping = conversation_data.get("mapping", {})
|
||||
|
||||
# メッセージを時系列順に並べる
|
||||
message_nodes = []
|
||||
for node_id, node in mapping.items():
|
||||
message = node.get("message")
|
||||
if not message:
|
||||
continue
|
||||
content = message.get("content", {})
|
||||
parts = content.get("parts", [])
|
||||
|
||||
if parts and isinstance(parts[0], str) and parts[0].strip():
|
||||
message_nodes.append({
|
||||
"id": node_id,
|
||||
"create_time": message.get("create_time", 0),
|
||||
"author_role": message["author"]["role"],
|
||||
"content": parts[0],
|
||||
"parent": node.get("parent")
|
||||
})
|
||||
|
||||
# 作成時間でソート
|
||||
message_nodes.sort(key=lambda x: x["create_time"] or 0)
|
||||
|
||||
for msg in message_nodes:
|
||||
if msg["author_role"] in ["user", "assistant"]:
|
||||
messages.append({
|
||||
"role": msg["author_role"],
|
||||
"content": msg["content"],
|
||||
"timestamp": msg["create_time"],
|
||||
"message_id": msg["id"]
|
||||
})
|
||||
|
||||
return messages
|
||||
|
||||
async def save_chatgpt_memory(self, conversation_data: Dict[str, Any], process_with_ai: bool = True) -> str:
|
||||
"""ChatGPTの会話を記憶として保存(AI処理オプション付き)"""
|
||||
title = conversation_data.get("title", "untitled")
|
||||
create_time = conversation_data.get("create_time", datetime.now().timestamp())
|
||||
|
||||
# メッセージを解析
|
||||
messages = self.parse_chatgpt_conversation(conversation_data)
|
||||
|
||||
if not messages:
|
||||
raise ValueError("No valid messages found in conversation")
|
||||
|
||||
# AI分析を実行
|
||||
ai_analysis = None
|
||||
if process_with_ai:
|
||||
try:
|
||||
ai_analysis = await self.ai_processor.generate_ai_summary(messages)
|
||||
except Exception as e:
|
||||
print(f"AI analysis failed: {e}")
|
||||
|
||||
# 基本要約を生成
|
||||
basic_summary = self.generate_basic_summary(messages)
|
||||
|
||||
# 保存データを作成
|
||||
memory_data = {
|
||||
"title": title,
|
||||
"source": "chatgpt",
|
||||
"import_time": datetime.now().isoformat(),
|
||||
"original_create_time": create_time,
|
||||
"messages": messages,
|
||||
"basic_summary": basic_summary,
|
||||
"ai_analysis": ai_analysis,
|
||||
"message_count": len(messages),
|
||||
"hash": self._generate_content_hash(messages)
|
||||
}
|
||||
|
||||
# 関係性データを更新
|
||||
if ai_analysis and "relationship_indicators" in ai_analysis:
|
||||
interaction_count = ai_analysis["relationship_indicators"].get("interaction_count", 0)
|
||||
if interaction_count > 10: # 長い会話は関係性にプラス
|
||||
self.relationship_tracker.update_relationship(
|
||||
target="user_general",
|
||||
interaction_type="positive",
|
||||
weight=min(interaction_count / 10, 5.0),
|
||||
context=f"Long conversation: {title}"
|
||||
)
|
||||
|
||||
# ファイル名を生成
|
||||
safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
|
||||
timestamp = datetime.fromtimestamp(create_time).strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{timestamp}_{safe_title[:50]}.json"
|
||||
|
||||
filepath = CHATGPT_MEMORY_DIR / filename
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 処理済みメモリディレクトリにも保存
|
||||
if ai_analysis:
|
||||
processed_filepath = PROCESSED_MEMORY_DIR / filename
|
||||
with open(processed_filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return str(filepath)
|
||||
|
||||
def generate_basic_summary(self, messages: List[Dict[str, Any]]) -> str:
|
||||
"""基本要約を生成"""
|
||||
if not messages:
|
||||
return "Empty conversation"
|
||||
|
||||
user_messages = [msg for msg in messages if msg["role"] == "user"]
|
||||
assistant_messages = [msg for msg in messages if msg["role"] == "assistant"]
|
||||
|
||||
summary = f"Conversation with {len(user_messages)} user messages and {len(assistant_messages)} assistant responses. "
|
||||
|
||||
if user_messages:
|
||||
first_user_msg = user_messages[0]["content"][:100]
|
||||
summary += f"Started with: {first_user_msg}..."
|
||||
|
||||
return summary
|
||||
|
||||
def _generate_content_hash(self, messages: List[Dict[str, Any]]) -> str:
|
||||
"""メッセージ内容のハッシュを生成"""
|
||||
content = "".join([msg["content"] for msg in messages])
|
||||
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
||||
|
||||
def search_memories(self, query: str, limit: int = 10, use_ai_analysis: bool = True) -> List[Dict[str, Any]]:
|
||||
"""記憶を検索(AI分析結果も含む)"""
|
||||
results = []
|
||||
|
||||
# 処理済みメモリから検索
|
||||
search_dirs = [PROCESSED_MEMORY_DIR, CHATGPT_MEMORY_DIR] if use_ai_analysis else [CHATGPT_MEMORY_DIR]
|
||||
|
||||
for search_dir in search_dirs:
|
||||
for filepath in search_dir.glob("*.json"):
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
memory_data = json.load(f)
|
||||
|
||||
# 検索対象テキストを構築
|
||||
search_text = f"{memory_data.get('title', '')} {memory_data.get('basic_summary', '')}"
|
||||
|
||||
# AI分析結果も検索対象に含める
|
||||
if memory_data.get('ai_analysis'):
|
||||
ai_analysis = memory_data['ai_analysis']
|
||||
search_text += f" {' '.join(ai_analysis.get('main_topics', []))}"
|
||||
search_text += f" {ai_analysis.get('summary', '')}"
|
||||
search_text += f" {' '.join(ai_analysis.get('key_insights', []))}"
|
||||
|
||||
# メッセージ内容も検索対象に含める
|
||||
for msg in memory_data.get('messages', []):
|
||||
search_text += f" {msg.get('content', '')}"
|
||||
|
||||
if query.lower() in search_text.lower():
|
||||
result = {
|
||||
"filepath": str(filepath),
|
||||
"title": memory_data.get("title"),
|
||||
"basic_summary": memory_data.get("basic_summary"),
|
||||
"source": memory_data.get("source"),
|
||||
"import_time": memory_data.get("import_time"),
|
||||
"message_count": len(memory_data.get("messages", [])),
|
||||
"has_ai_analysis": bool(memory_data.get("ai_analysis"))
|
||||
}
|
||||
|
||||
if memory_data.get('ai_analysis'):
|
||||
result["ai_summary"] = memory_data['ai_analysis'].get('summary', '')
|
||||
result["main_topics"] = memory_data['ai_analysis'].get('main_topics', [])
|
||||
|
||||
results.append(result)
|
||||
|
||||
if len(results) >= limit:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading memory file {filepath}: {e}")
|
||||
continue
|
||||
|
||||
if len(results) >= limit:
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
def get_memory_detail(self, filepath: str) -> Dict[str, Any]:
|
||||
"""記憶の詳細を取得"""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error reading memory file: {e}")
|
||||
|
||||
def list_all_memories(self) -> List[Dict[str, Any]]:
|
||||
"""すべての記憶をリスト"""
|
||||
memories = []
|
||||
|
||||
for filepath in CHATGPT_MEMORY_DIR.glob("*.json"):
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
memory_data = json.load(f)
|
||||
|
||||
memory_info = {
|
||||
"filepath": str(filepath),
|
||||
"title": memory_data.get("title"),
|
||||
"basic_summary": memory_data.get("basic_summary"),
|
||||
"source": memory_data.get("source"),
|
||||
"import_time": memory_data.get("import_time"),
|
||||
"message_count": len(memory_data.get("messages", [])),
|
||||
"has_ai_analysis": bool(memory_data.get("ai_analysis"))
|
||||
}
|
||||
|
||||
if memory_data.get('ai_analysis'):
|
||||
memory_info["ai_summary"] = memory_data['ai_analysis'].get('summary', '')
|
||||
memory_info["main_topics"] = memory_data['ai_analysis'].get('main_topics', [])
|
||||
|
||||
memories.append(memory_info)
|
||||
except Exception as e:
|
||||
print(f"Error reading memory file {filepath}: {e}")
|
||||
continue
|
||||
|
||||
# インポート時間でソート
|
||||
memories.sort(key=lambda x: x.get("import_time", ""), reverse=True)
|
||||
return memories
|
||||
|
||||
# FastAPI アプリケーション
|
||||
app = FastAPI(title="AigptMCP Server with AI Memory", version="2.0.0")
|
||||
memory_manager = MemoryManager()
|
||||
|
||||
@app.post("/memory/import/chatgpt")
|
||||
async def import_chatgpt_conversation(data: ConversationImport, process_with_ai: bool = True):
|
||||
"""ChatGPTの会話をインポート(AI処理オプション付き)"""
|
||||
try:
|
||||
filepath = await memory_manager.save_chatgpt_memory(data.conversation_data, process_with_ai)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Conversation imported successfully",
|
||||
"filepath": filepath,
|
||||
"ai_processed": process_with_ai
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@app.post("/memory/process-ai")
|
||||
async def process_memory_with_ai(data: MemorySummaryRequest):
|
||||
"""既存の記憶をAIで再処理"""
|
||||
try:
|
||||
# 既存記憶を読み込み
|
||||
memory_data = memory_manager.get_memory_detail(data.filepath)
|
||||
|
||||
# AI分析を実行
|
||||
ai_analysis = await memory_manager.ai_processor.generate_ai_summary(
|
||||
memory_data["messages"],
|
||||
data.ai_provider
|
||||
)
|
||||
|
||||
# データを更新
|
||||
memory_data["ai_analysis"] = ai_analysis
|
||||
memory_data["ai_processed_at"] = datetime.now().isoformat()
|
||||
|
||||
# ファイルを更新
|
||||
with open(data.filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 処理済みディレクトリにもコピー
|
||||
processed_filepath = PROCESSED_MEMORY_DIR / Path(data.filepath).name
|
||||
with open(processed_filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(memory_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Memory processed with AI successfully",
|
||||
"ai_analysis": ai_analysis
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/memory/search")
|
||||
async def search_memories(query: MemoryQuery):
|
||||
"""記憶を検索"""
|
||||
try:
|
||||
results = memory_manager.search_memories(query.query, query.limit)
|
||||
return {
|
||||
"success": True,
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/memory/list")
|
||||
async def list_memories():
|
||||
"""すべての記憶をリスト"""
|
||||
try:
|
||||
memories = memory_manager.list_all_memories()
|
||||
return {
|
||||
"success": True,
|
||||
"memories": memories,
|
||||
"count": len(memories)
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/memory/detail")
|
||||
async def get_memory_detail(filepath: str):
|
||||
"""記憶の詳細を取得"""
|
||||
try:
|
||||
detail = memory_manager.get_memory_detail(filepath)
|
||||
return {
|
||||
"success": True,
|
||||
"memory": detail
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@app.post("/relationship/update")
|
||||
async def update_relationship(data: RelationshipUpdate):
|
||||
"""関係性を更新"""
|
||||
try:
|
||||
new_score = memory_manager.relationship_tracker.update_relationship(
|
||||
data.target, data.interaction_type, data.weight, data.context
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"new_score": new_score,
|
||||
"can_send_message": memory_manager.relationship_tracker.should_send_message(data.target)
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/relationship/list")
|
||||
async def list_relationships():
|
||||
"""すべての関係性をリスト"""
|
||||
try:
|
||||
relationships = memory_manager.relationship_tracker.get_all_relationships()
|
||||
return {
|
||||
"success": True,
|
||||
"relationships": relationships
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/relationship/check")
|
||||
async def check_send_permission(target: str, threshold: float = 50.0):
|
||||
"""メッセージ送信可否をチェック"""
|
||||
try:
|
||||
score = memory_manager.relationship_tracker.get_relationship_score(target)
|
||||
can_send = memory_manager.relationship_tracker.should_send_message(target, threshold)
|
||||
return {
|
||||
"success": True,
|
||||
"target": target,
|
||||
"score": score,
|
||||
"can_send_message": can_send,
|
||||
"threshold": threshold
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/chat")
|
||||
async def chat_endpoint(data: ChatMessage):
|
||||
"""チャット機能(記憶と関係性を活用)"""
|
||||
try:
|
||||
# 関連する記憶を検索
|
||||
memories = memory_manager.search_memories(data.message, limit=3)
|
||||
|
||||
# メモリのコンテキストを構築
|
||||
memory_context = ""
|
||||
if memories:
|
||||
memory_context = "\n# Related memories:\n"
|
||||
for memory in memories:
|
||||
memory_context += f"- {memory['title']}: {memory.get('ai_summary', memory.get('basic_summary', ''))}\n"
|
||||
if memory.get('main_topics'):
|
||||
memory_context += f" Topics: {', '.join(memory['main_topics'])}\n"
|
||||
|
||||
# 関係性情報を取得
|
||||
relationships = memory_manager.relationship_tracker.get_all_relationships()
|
||||
|
||||
# 実際のチャット処理
|
||||
enhanced_message = data.message
|
||||
if memory_context:
|
||||
enhanced_message = f"{data.message}\n\n{memory_context}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"response": f"Enhanced response with memory context: {enhanced_message}",
|
||||
"memories_used": len(memories),
|
||||
"relationship_info": {
|
||||
"active_relationships": len(relationships.get("targets", {})),
|
||||
"can_initiate_conversations": sum(1 for target, data in relationships.get("targets", {}).items()
|
||||
if memory_manager.relationship_tracker.should_send_message(target))
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""ヘルスチェック"""
|
||||
return {
|
||||
"service": "AigptMCP Server with AI Memory",
|
||||
"version": "2.0.0",
|
||||
"status": "running",
|
||||
"memory_dir": str(MEMORY_DIR),
|
||||
"features": [
|
||||
"AI-powered memory analysis",
|
||||
"Relationship tracking",
|
||||
"Advanced memory search",
|
||||
"Conversation import",
|
||||
"Auto-summary generation"
|
||||
],
|
||||
"endpoints": [
|
||||
"/memory/import/chatgpt",
|
||||
"/memory/process-ai",
|
||||
"/memory/search",
|
||||
"/memory/list",
|
||||
"/memory/detail",
|
||||
"/relationship/update",
|
||||
"/relationship/list",
|
||||
"/relationship/check",
|
||||
"/chat"
|
||||
]
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 AigptMCP Server with AI Memory starting...")
|
||||
print(f"📁 Memory directory: {MEMORY_DIR}")
|
||||
print(f"🧠 AI Memory processing: {'✅ Enabled' if os.getenv('OPENAI_API_KEY') or os.getenv('ANTHROPIC_API_KEY') else '❌ Disabled (no API keys)'}")
|
||||
uvicorn.run(app, host="127.0.0.1", port=5000)
|
64
rust/src/cli.rs
Normal file
64
rust/src/cli.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
// src/cli.rs
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "aigpt")]
|
||||
#[command(about = "AI GPT CLI with MCP Server and Memory")]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// MCP Server management
|
||||
Server {
|
||||
#[command(subcommand)]
|
||||
command: ServerCommands,
|
||||
},
|
||||
/// Chat with AI
|
||||
Chat {
|
||||
/// Message to send
|
||||
message: String,
|
||||
/// Use memory context
|
||||
#[arg(long)]
|
||||
with_memory: bool,
|
||||
},
|
||||
/// Memory management
|
||||
Memory {
|
||||
#[command(subcommand)]
|
||||
command: MemoryCommands,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ServerCommands {
|
||||
/// Setup Python MCP server environment
|
||||
Setup,
|
||||
/// Run the MCP server
|
||||
Run,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum MemoryCommands {
|
||||
/// Import ChatGPT conversation export file
|
||||
Import {
|
||||
/// Path to ChatGPT export JSON file
|
||||
file: String,
|
||||
},
|
||||
/// Search memories
|
||||
Search {
|
||||
/// Search query
|
||||
query: String,
|
||||
/// Maximum number of results
|
||||
#[arg(short, long, default_value = "10")]
|
||||
limit: usize,
|
||||
},
|
||||
/// List all memories
|
||||
List,
|
||||
/// Show memory details
|
||||
Detail {
|
||||
/// Path to memory file
|
||||
filepath: String,
|
||||
},
|
||||
}
|
@@ -22,6 +22,7 @@ impl ConfigPaths {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn data_file(&self, file_name: &str) -> PathBuf {
|
||||
let file_path = match file_name {
|
||||
"db" => self.base_dir.join("user.db"),
|
||||
@@ -29,18 +30,30 @@ impl ConfigPaths {
|
||||
"json" => self.base_dir.join("user.json"),
|
||||
_ => self.base_dir.join(format!(".{}", file_name)),
|
||||
};
|
||||
|
||||
file_path
|
||||
}
|
||||
/// 設定ファイルがなければ `example.json` をコピーする
|
||||
pub fn ensure_file_exists(&self, file_name: &str, template_path: &Path) {
|
||||
let target = self.data_file(file_name);
|
||||
if !target.exists() {
|
||||
if let Err(e) = fs::copy(template_path, &target) {
|
||||
eprintln!("⚠️ 設定ファイルの初期化に失敗しました: {}", e);
|
||||
} else {
|
||||
println!("📄 {} を {} にコピーしました", template_path.display(), target.display());
|
||||
}
|
||||
|
||||
pub fn mcp_dir(&self) -> PathBuf {
|
||||
self.base_dir.join("mcp")
|
||||
}
|
||||
|
||||
pub fn venv_path(&self) -> PathBuf {
|
||||
self.mcp_dir().join(".venv")
|
||||
}
|
||||
|
||||
pub fn python_executable(&self) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
self.venv_path().join("Scripts").join("python.exe")
|
||||
} else {
|
||||
self.venv_path().join("bin").join("python")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pip_executable(&self) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
self.venv_path().join("Scripts").join("pip.exe")
|
||||
} else {
|
||||
self.venv_path().join("bin").join("pip")
|
||||
}
|
||||
}
|
||||
}
|
58
rust/src/main.rs
Normal file
58
rust/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
// main.rs
|
||||
mod cli;
|
||||
mod config;
|
||||
mod mcp;
|
||||
|
||||
use cli::{Args, Commands, ServerCommands, MemoryCommands};
|
||||
use clap::Parser;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
match args.command {
|
||||
Commands::Server { command } => {
|
||||
match command {
|
||||
ServerCommands::Setup => {
|
||||
mcp::server::setup();
|
||||
}
|
||||
ServerCommands::Run => {
|
||||
mcp::server::run().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Chat { message, with_memory } => {
|
||||
if with_memory {
|
||||
if let Err(e) = mcp::memory::handle_chat_with_memory(&message).await {
|
||||
eprintln!("❌ 記憶チャットエラー: {}", e);
|
||||
}
|
||||
} else {
|
||||
mcp::server::chat(&message).await;
|
||||
}
|
||||
}
|
||||
Commands::Memory { command } => {
|
||||
match command {
|
||||
MemoryCommands::Import { file } => {
|
||||
if let Err(e) = mcp::memory::handle_import(&file).await {
|
||||
eprintln!("❌ インポートエラー: {}", e);
|
||||
}
|
||||
}
|
||||
MemoryCommands::Search { query, limit } => {
|
||||
if let Err(e) = mcp::memory::handle_search(&query, limit).await {
|
||||
eprintln!("❌ 検索エラー: {}", e);
|
||||
}
|
||||
}
|
||||
MemoryCommands::List => {
|
||||
if let Err(e) = mcp::memory::handle_list().await {
|
||||
eprintln!("❌ 一覧取得エラー: {}", e);
|
||||
}
|
||||
}
|
||||
MemoryCommands::Detail { filepath } => {
|
||||
if let Err(e) = mcp::memory::handle_detail(&filepath).await {
|
||||
eprintln!("❌ 詳細取得エラー: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
393
rust/src/mcp/memory.rs
Normal file
393
rust/src/mcp/memory.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
// src/mcp/memory.rs
|
||||
use reqwest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{self, Value};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MemorySearchRequest {
|
||||
pub query: String,
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ChatRequest {
|
||||
pub message: String,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConversationImportRequest {
|
||||
pub conversation_data: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ApiResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
pub message: Option<String>,
|
||||
pub filepath: Option<String>,
|
||||
pub results: Option<Vec<MemoryResult>>,
|
||||
pub memories: Option<Vec<MemoryResult>>,
|
||||
#[allow(dead_code)]
|
||||
pub count: Option<usize>,
|
||||
pub memory: Option<Value>,
|
||||
pub response: Option<String>,
|
||||
pub memories_used: Option<usize>,
|
||||
pub imported_count: Option<usize>,
|
||||
pub total_count: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MemoryResult {
|
||||
#[allow(dead_code)]
|
||||
pub filepath: String,
|
||||
pub title: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub import_time: Option<String>,
|
||||
pub message_count: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct MemoryClient {
|
||||
base_url: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl MemoryClient {
|
||||
pub fn new(base_url: Option<String>) -> Self {
|
||||
let url = base_url.unwrap_or_else(|| "http://127.0.0.1:5000".to_string());
|
||||
Self {
|
||||
base_url: url,
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn import_chatgpt_file(&self, filepath: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
// ファイルを読み込み
|
||||
let content = fs::read_to_string(filepath)?;
|
||||
let json_data: Value = serde_json::from_str(&content)?;
|
||||
|
||||
// 配列かどうかチェック
|
||||
match json_data.as_array() {
|
||||
Some(conversations) => {
|
||||
// 複数の会話をインポート
|
||||
let mut imported_count = 0;
|
||||
let total_count = conversations.len();
|
||||
|
||||
for conversation in conversations {
|
||||
match self.import_single_conversation(conversation.clone()).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
imported_count += 1;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ インポートエラー: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ApiResponse {
|
||||
success: true,
|
||||
imported_count: Some(imported_count),
|
||||
total_count: Some(total_count),
|
||||
error: None,
|
||||
message: Some(format!("{}個中{}個の会話をインポートしました", total_count, imported_count)),
|
||||
filepath: None,
|
||||
results: None,
|
||||
memories: None,
|
||||
count: None,
|
||||
memory: None,
|
||||
response: None,
|
||||
memories_used: None,
|
||||
})
|
||||
}
|
||||
None => {
|
||||
// 単一の会話をインポート
|
||||
self.import_single_conversation(json_data).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn import_single_conversation(&self, conversation_data: Value) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
let request = ConversationImportRequest { conversation_data };
|
||||
|
||||
let response = self.client
|
||||
.post(&format!("{}/memory/import/chatgpt", self.base_url))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: ApiResponse = response.json().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn search_memories(&self, query: &str, limit: usize) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
let request = MemorySearchRequest {
|
||||
query: query.to_string(),
|
||||
limit,
|
||||
};
|
||||
|
||||
let response = self.client
|
||||
.post(&format!("{}/memory/search", self.base_url))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: ApiResponse = response.json().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn list_memories(&self) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
let response = self.client
|
||||
.get(&format!("{}/memory/list", self.base_url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: ApiResponse = response.json().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_memory_detail(&self, filepath: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
let response = self.client
|
||||
.get(&format!("{}/memory/detail", self.base_url))
|
||||
.query(&[("filepath", filepath)])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: ApiResponse = response.json().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn chat_with_memory(&self, message: &str) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
let request = ChatRequest {
|
||||
message: message.to_string(),
|
||||
model: None,
|
||||
};
|
||||
|
||||
let response = self.client
|
||||
.post(&format!("{}/chat", self.base_url))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: ApiResponse = response.json().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn is_server_running(&self) -> bool {
|
||||
match self.client.get(&self.base_url).send().await {
|
||||
Ok(response) => response.status().is_success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_import(filepath: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !Path::new(filepath).exists() {
|
||||
eprintln!("❌ ファイルが見つかりません: {}", filepath);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client = MemoryClient::new(None);
|
||||
|
||||
// サーバーが起動しているかチェック
|
||||
if !client.is_server_running().await {
|
||||
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("🔄 ChatGPT会話をインポートしています: {}", filepath);
|
||||
|
||||
match client.import_chatgpt_file(filepath).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
if let (Some(imported), Some(total)) = (response.imported_count, response.total_count) {
|
||||
println!("✅ {}個中{}個の会話をインポートしました", total, imported);
|
||||
} else {
|
||||
println!("✅ 会話をインポートしました");
|
||||
if let Some(path) = response.filepath {
|
||||
println!("📁 保存先: {}", path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ インポートに失敗: {:?}", response.error);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ インポートエラー: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_search(query: &str, limit: usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = MemoryClient::new(None);
|
||||
|
||||
if !client.is_server_running().await {
|
||||
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("🔍 記憶を検索しています: {}", query);
|
||||
|
||||
match client.search_memories(query, limit).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
if let Some(results) = response.results {
|
||||
println!("📚 {}個の記憶が見つかりました:", results.len());
|
||||
for memory in results {
|
||||
println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string()));
|
||||
if let Some(summary) = memory.summary {
|
||||
println!(" 概要: {}", summary);
|
||||
}
|
||||
if let Some(count) = memory.message_count {
|
||||
println!(" メッセージ数: {}", count);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
} else {
|
||||
println!("📚 記憶が見つかりませんでした");
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ 検索に失敗: {:?}", response.error);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 検索エラー: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_list() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = MemoryClient::new(None);
|
||||
|
||||
if !client.is_server_running().await {
|
||||
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("📋 記憶一覧を取得しています...");
|
||||
|
||||
match client.list_memories().await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
if let Some(memories) = response.memories {
|
||||
println!("📚 総記憶数: {}", memories.len());
|
||||
for memory in memories {
|
||||
println!(" • {}", memory.title.unwrap_or_else(|| "タイトルなし".to_string()));
|
||||
if let Some(source) = memory.source {
|
||||
println!(" ソース: {}", source);
|
||||
}
|
||||
if let Some(count) = memory.message_count {
|
||||
println!(" メッセージ数: {}", count);
|
||||
}
|
||||
if let Some(import_time) = memory.import_time {
|
||||
println!(" インポート時刻: {}", import_time);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
} else {
|
||||
println!("📚 記憶がありません");
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ 一覧取得に失敗: {:?}", response.error);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 一覧取得エラー: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_detail(filepath: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = MemoryClient::new(None);
|
||||
|
||||
if !client.is_server_running().await {
|
||||
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("📄 記憶の詳細を取得しています: {}", filepath);
|
||||
|
||||
match client.get_memory_detail(filepath).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
if let Some(memory) = response.memory {
|
||||
if let Some(title) = memory.get("title").and_then(|v| v.as_str()) {
|
||||
println!("タイトル: {}", title);
|
||||
}
|
||||
if let Some(source) = memory.get("source").and_then(|v| v.as_str()) {
|
||||
println!("ソース: {}", source);
|
||||
}
|
||||
if let Some(summary) = memory.get("summary").and_then(|v| v.as_str()) {
|
||||
println!("概要: {}", summary);
|
||||
}
|
||||
if let Some(messages) = memory.get("messages").and_then(|v| v.as_array()) {
|
||||
println!("メッセージ数: {}", messages.len());
|
||||
println!("\n最近のメッセージ:");
|
||||
for msg in messages.iter().take(5) {
|
||||
if let (Some(role), Some(content)) = (
|
||||
msg.get("role").and_then(|v| v.as_str()),
|
||||
msg.get("content").and_then(|v| v.as_str())
|
||||
) {
|
||||
let content_preview = if content.len() > 100 {
|
||||
format!("{}...", &content[..100])
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
println!(" {}: {}", role, content_preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ 詳細取得に失敗: {:?}", response.error);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 詳細取得エラー: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_chat_with_memory(message: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = MemoryClient::new(None);
|
||||
|
||||
if !client.is_server_running().await {
|
||||
eprintln!("❌ MCP Serverが起動していません。先に 'aigpt server run' を実行してください。");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("💬 記憶を活用してチャットしています...");
|
||||
|
||||
match client.chat_with_memory(message).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
if let Some(reply) = response.response {
|
||||
println!("🤖 {}", reply);
|
||||
}
|
||||
if let Some(memories_used) = response.memories_used {
|
||||
println!("📚 使用した記憶数: {}", memories_used);
|
||||
}
|
||||
} else {
|
||||
eprintln!("❌ チャットに失敗: {:?}", response.error);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ チャットエラー: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
3
rust/src/mcp/mod.rs
Normal file
3
rust/src/mcp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// src/mcp/mod.rs
|
||||
pub mod server;
|
||||
pub mod memory;
|
147
rust/src/mcp/server.rs
Normal file
147
rust/src/mcp/server.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
// src/mcp/server.rs
|
||||
use crate::config::ConfigPaths;
|
||||
//use std::fs;
|
||||
use std::process::Command as OtherCommand;
|
||||
use std::env;
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
|
||||
pub fn setup() {
|
||||
println!("🔧 MCP Server環境をセットアップしています...");
|
||||
let config = ConfigPaths::new();
|
||||
let mcp_dir = config.mcp_dir();
|
||||
|
||||
// プロジェクトのmcp/ディレクトリからファイルをコピー
|
||||
let current_dir = env::current_dir().expect("現在のディレクトリを取得できません");
|
||||
let project_mcp_dir = current_dir.join("mcp");
|
||||
if !project_mcp_dir.exists() {
|
||||
eprintln!("❌ プロジェクトのmcp/ディレクトリが見つかりません: {}", project_mcp_dir.display());
|
||||
return;
|
||||
}
|
||||
|
||||
if mcp_dir.exists() {
|
||||
fs_extra::dir::remove(&mcp_dir).expect("既存のmcp_dirの削除に失敗しました");
|
||||
}
|
||||
|
||||
let mut options = CopyOptions::new();
|
||||
options.overwrite = true; // 上書き
|
||||
options.copy_inside = true; // 中身だけコピー
|
||||
|
||||
copy(&project_mcp_dir, &mcp_dir, &options).expect("コピーに失敗しました");
|
||||
|
||||
// 仮想環境の作成
|
||||
let venv_path = config.venv_path();
|
||||
if !venv_path.exists() {
|
||||
println!("🐍 仮想環境を作成しています...");
|
||||
let output = OtherCommand::new("python3")
|
||||
.args(&["-m", "venv", ".venv"])
|
||||
.current_dir(&mcp_dir)
|
||||
.output()
|
||||
.expect("venvの作成に失敗しました");
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("❌ venv作成エラー: {}", String::from_utf8_lossy(&output.stderr));
|
||||
return;
|
||||
}
|
||||
println!("✅ 仮想環境を作成しました");
|
||||
} else {
|
||||
println!("✅ 仮想環境は既に存在します");
|
||||
}
|
||||
|
||||
// 依存関係のインストール
|
||||
println!("📦 依存関係をインストールしています...");
|
||||
let pip_path = config.pip_executable();
|
||||
let output = OtherCommand::new(&pip_path)
|
||||
.args(&["install", "-r", "requirements.txt"])
|
||||
.current_dir(&mcp_dir)
|
||||
.output()
|
||||
.expect("pipコマンドの実行に失敗しました");
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("❌ pip installエラー: {}", String::from_utf8_lossy(&output.stderr));
|
||||
return;
|
||||
}
|
||||
|
||||
println!("✅ MCP Server環境のセットアップが完了しました!");
|
||||
println!("📍 セットアップ場所: {}", mcp_dir.display());
|
||||
}
|
||||
|
||||
pub async fn run() {
|
||||
println!("🚀 MCP Serverを起動しています...");
|
||||
|
||||
let config = ConfigPaths::new();
|
||||
let mcp_dir = config.mcp_dir();
|
||||
let python_path = config.python_executable();
|
||||
let server_py_path = mcp_dir.join("server.py");
|
||||
|
||||
// セットアップの確認
|
||||
if !server_py_path.exists() {
|
||||
eprintln!("❌ server.pyが見つかりません。先に 'aigpt server setup' を実行してください。");
|
||||
return;
|
||||
}
|
||||
|
||||
if !python_path.exists() {
|
||||
eprintln!("❌ Python実行ファイルが見つかりません。先に 'aigpt server setup' を実行してください。");
|
||||
return;
|
||||
}
|
||||
|
||||
// サーバーの起動
|
||||
println!("🔗 サーバーを起動中... (Ctrl+Cで停止)");
|
||||
let mut child = OtherCommand::new(&python_path)
|
||||
.arg("server.py")
|
||||
.current_dir(&mcp_dir)
|
||||
.spawn()
|
||||
.expect("MCP Serverの起動に失敗しました");
|
||||
|
||||
// サーバーの終了を待機
|
||||
match child.wait() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
println!("✅ MCP Serverが正常に終了しました");
|
||||
} else {
|
||||
println!("❌ MCP Serverが異常終了しました: {}", status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ MCP Serverの実行中にエラーが発生しました: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn chat(message: &str) {
|
||||
println!("💬 チャットを開始しています...");
|
||||
|
||||
let config = ConfigPaths::new();
|
||||
let mcp_dir = config.mcp_dir();
|
||||
let python_path = config.python_executable();
|
||||
let chat_py_path = mcp_dir.join("chat.py");
|
||||
|
||||
// セットアップの確認
|
||||
if !chat_py_path.exists() {
|
||||
eprintln!("❌ chat.pyが見つかりません。先に 'aigpt server setup' を実行してください。");
|
||||
return;
|
||||
}
|
||||
|
||||
if !python_path.exists() {
|
||||
eprintln!("❌ Python実行ファイルが見つかりません。先に 'aigpt server setup' を実行してください。");
|
||||
return;
|
||||
}
|
||||
|
||||
// チャットの実行
|
||||
let output = OtherCommand::new(&python_path)
|
||||
.args(&["chat.py", message])
|
||||
.current_dir(&mcp_dir)
|
||||
.output()
|
||||
.expect("chat.pyの実行に失敗しました");
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if !stderr.is_empty() {
|
||||
print!("{}", stderr);
|
||||
}
|
||||
print!("{}", stdout);
|
||||
} else {
|
||||
eprintln!("❌ チャット実行エラー: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
18
setup_venv.sh
Executable file
18
setup_venv.sh
Executable 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"
|
37
src/agent.rs
37
src/agent.rs
@@ -1,37 +0,0 @@
|
||||
use chrono::{NaiveDateTime};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct AIState {
|
||||
pub relation_score: f32,
|
||||
pub previous_score: f32,
|
||||
pub decay_rate: f32,
|
||||
pub sensitivity: f32,
|
||||
pub message_threshold: f32,
|
||||
pub last_message_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AIState {
|
||||
pub fn update(&mut self, now: NaiveDateTime) {
|
||||
let days_passed = (now - self.last_message_time).num_days() as f32;
|
||||
let decay = self.decay_rate * days_passed;
|
||||
self.previous_score = self.relation_score;
|
||||
self.relation_score -= decay;
|
||||
self.relation_score = self.relation_score.clamp(0.0, 100.0);
|
||||
}
|
||||
|
||||
pub fn should_talk(&self) -> bool {
|
||||
let delta = self.previous_score - self.relation_score;
|
||||
delta > self.message_threshold && self.sensitivity > 0.5
|
||||
}
|
||||
|
||||
pub fn generate_message(&self) -> String {
|
||||
match self.relation_score as i32 {
|
||||
80..=100 => "ふふっ、最近どうしてる?会いたくなっちゃった!".to_string(),
|
||||
60..=79 => "ちょっとだけ、さみしかったんだよ?".to_string(),
|
||||
40..=59 => "えっと……話せる時間ある?".to_string(),
|
||||
_ => "ううん、もしかして私のこと、忘れちゃったのかな……".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
15
src/aigpt/__init__.py
Normal file
15
src/aigpt/__init__.py
Normal 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",
|
||||
]
|
172
src/aigpt/ai_provider.py
Normal file
172
src/aigpt/ai_provider.py
Normal 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: ai-gpt 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}")
|
444
src/aigpt/cli.py
Normal file
444
src/aigpt/cli.py
Normal file
@@ -0,0 +1,444 @@
|
||||
"""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
|
||||
|
||||
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)")
|
||||
):
|
||||
"""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)
|
||||
app_instance = mcp_server.get_server().get_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}",
|
||||
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 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
145
src/aigpt/config.py
Normal 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
118
src/aigpt/fortune.py
Normal 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
|
149
src/aigpt/mcp_server.py
Normal file
149
src/aigpt/mcp_server.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""MCP Server for ai.gpt system"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from fastapi_mcp import FastapiMcpServer
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
from .persona import Persona
|
||||
from .models import Memory, Relationship, PersonaState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIGptMcpServer:
|
||||
"""MCP Server that exposes ai.gpt functionality to AI assistants"""
|
||||
|
||||
def __init__(self, data_dir: Path):
|
||||
self.data_dir = data_dir
|
||||
self.persona = Persona(data_dir)
|
||||
self.server = FastapiMcpServer("ai-gpt", "AI.GPT Memory and Relationship System")
|
||||
self._register_tools()
|
||||
|
||||
def _register_tools(self):
|
||||
"""Register all MCP tools"""
|
||||
|
||||
@self.server.tool("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.server.tool("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.server.tool("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.server.tool("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.server.tool("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.server.tool("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.server.tool("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.server.tool("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.server.tool("run_maintenance")
|
||||
async def run_maintenance() -> Dict[str, str]:
|
||||
"""Run daily maintenance tasks"""
|
||||
self.persona.daily_maintenance()
|
||||
return {"status": "Maintenance completed successfully"}
|
||||
|
||||
def get_server(self) -> FastapiMcpServer:
|
||||
"""Get the FastAPI MCP server instance"""
|
||||
return self.server
|
155
src/aigpt/memory.py
Normal file
155
src/aigpt/memory.py
Normal 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
79
src/aigpt/models.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Data models for ai.gpt system"""
|
||||
|
||||
from datetime import datetime
|
||||
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: datetime.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
181
src/aigpt/persona.py
Normal 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
135
src/aigpt/relationship.py
Normal 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
312
src/aigpt/scheduler.py
Normal 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
111
src/aigpt/transmission.py
Normal 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
|
||||
}
|
140
src/chat.rs
140
src/chat.rs
@@ -1,140 +0,0 @@
|
||||
// src/chat.rs
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use serde::Deserialize;
|
||||
use seahorse::Context;
|
||||
use crate::config::ConfigPaths;
|
||||
use crate::metrics::{load_user_data, save_user_data, update_metrics_decay};
|
||||
//use std::process::Stdio;
|
||||
//use std::io::Write;
|
||||
//use std::time::Duration;
|
||||
//use std::net::TcpStream;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Provider {
|
||||
OpenAI,
|
||||
Ollama,
|
||||
MCP,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"openai" => Some(Provider::OpenAI),
|
||||
"ollama" => Some(Provider::Ollama),
|
||||
"mcp" => Some(Provider::MCP),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Provider::OpenAI => "openai",
|
||||
Provider::Ollama => "ollama",
|
||||
Provider::MCP => "mcp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIKey {
|
||||
token: String,
|
||||
}
|
||||
|
||||
fn load_openai_api_key() -> Option<String> {
|
||||
let config = ConfigPaths::new();
|
||||
let path = config.base_dir.join("openai.json");
|
||||
let data = fs::read_to_string(path).ok()?;
|
||||
let parsed: OpenAIKey = serde_json::from_str(&data).ok()?;
|
||||
Some(parsed.token)
|
||||
}
|
||||
|
||||
pub fn ask_chat(c: &Context, question: &str) -> Option<String> {
|
||||
let config = ConfigPaths::new();
|
||||
let base_dir = config.base_dir.join("mcp");
|
||||
let user_path = config.base_dir.join("user.json");
|
||||
|
||||
let mut user = load_user_data(&user_path);
|
||||
user.metrics = update_metrics_decay();
|
||||
|
||||
// 各種オプション
|
||||
let ollama_host = c.string_flag("host").ok();
|
||||
let ollama_model = c.string_flag("model").ok();
|
||||
let provider_str = c.string_flag("provider").unwrap_or_else(|_| "ollama".to_string());
|
||||
let provider = Provider::from_str(&provider_str).unwrap_or(Provider::Ollama);
|
||||
let api_key = c.string_flag("api-key").ok().or_else(load_openai_api_key);
|
||||
|
||||
println!("🔍 使用プロバイダー: {}", provider.as_str());
|
||||
|
||||
match provider {
|
||||
Provider::MCP => {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let url = std::env::var("MCP_URL").unwrap_or("http://127.0.0.1:5000/chat".to_string());
|
||||
let res = client.post(url)
|
||||
.json(&serde_json::json!({"message": question}))
|
||||
.send();
|
||||
|
||||
match res {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
let json: serde_json::Value = resp.json().ok()?;
|
||||
let text = json.get("response")?.as_str()?.to_string();
|
||||
user.metrics.intimacy += 0.01;
|
||||
user.metrics.last_updated = chrono::Utc::now();
|
||||
save_user_data(&user_path, &user);
|
||||
Some(text)
|
||||
} else {
|
||||
eprintln!("❌ MCPエラー: HTTP {}", resp.status());
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ MCP接続失敗: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Python 実行パス
|
||||
let python_path = if cfg!(target_os = "windows") {
|
||||
base_dir.join(".venv/Scripts/mcp.exe")
|
||||
} else {
|
||||
base_dir.join(".venv/bin/mcp")
|
||||
};
|
||||
|
||||
let mut command = Command::new(python_path);
|
||||
command.arg("ask").arg(question);
|
||||
|
||||
if let Some(host) = ollama_host {
|
||||
command.env("OLLAMA_HOST", host);
|
||||
}
|
||||
if let Some(model) = ollama_model {
|
||||
command.env("OLLAMA_MODEL", model.clone());
|
||||
command.env("OPENAI_MODEL", model);
|
||||
}
|
||||
command.env("PROVIDER", provider.as_str());
|
||||
|
||||
if let Some(key) = api_key {
|
||||
command.env("OPENAI_API_KEY", key);
|
||||
}
|
||||
|
||||
let output = command.output().expect("❌ MCPチャットスクリプトの実行に失敗しました");
|
||||
|
||||
if output.status.success() {
|
||||
let response = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
user.metrics.intimacy += 0.01;
|
||||
user.metrics.last_updated = chrono::Utc::now();
|
||||
save_user_data(&user_path, &user);
|
||||
|
||||
Some(response)
|
||||
} else {
|
||||
eprintln!(
|
||||
"❌ 実行エラー: {}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
src/cli.rs
100
src/cli.rs
@@ -1,100 +0,0 @@
|
||||
// src/cli.rs
|
||||
use std::path::{Path};
|
||||
use chrono::{Duration, Local};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use seahorse::{App, Command, Context};
|
||||
|
||||
use crate::utils::{load_config, save_config};
|
||||
use crate::config::ConfigPaths;
|
||||
use crate::agent::AIState;
|
||||
use crate::commands::db::{save_cmd, export_cmd};
|
||||
use crate::commands::scheduler::{scheduler_cmd};
|
||||
use crate::commands::mcp::mcp_cmd;
|
||||
|
||||
pub fn cli_app() -> App {
|
||||
let set_cmd = Command::new("set")
|
||||
.usage("set [trust|intimacy|curiosity] [value]")
|
||||
.action(|c: &Context| {
|
||||
if c.args.len() != 2 {
|
||||
eprintln!("Usage: set [trust|intimacy|curiosity] [value]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let field = &c.args[0];
|
||||
let value: f32 = c.args[1].parse().unwrap_or_else(|_| {
|
||||
eprintln!("数値で入力してください");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// ConfigPathsを使って設定ファイルのパスを取得
|
||||
let config_paths = ConfigPaths::new();
|
||||
let json_path = config_paths.data_file("json");
|
||||
// まだ user.json がない場合、example.json をコピー
|
||||
config_paths.ensure_file_exists("json", Path::new("example.json"));
|
||||
let db_path = config_paths.data_file("db");
|
||||
let mut ai = load_config(json_path.to_str().unwrap());
|
||||
|
||||
match field.as_str() {
|
||||
"trust" => ai.relationship.trust = value,
|
||||
"intimacy" => ai.relationship.intimacy = value,
|
||||
"curiosity" => ai.relationship.curiosity = value,
|
||||
_ => {
|
||||
eprintln!("trust / intimacy / curiosity のいずれかを指定してください");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
save_config(json_path.to_str().unwrap(), &ai);
|
||||
|
||||
let conn = Connection::open(db_path.to_str().unwrap()).expect("DB接続失敗");
|
||||
ai.save_to_db(&conn).expect("DB保存失敗");
|
||||
|
||||
println!("✅ {field} を {value} に更新しました");
|
||||
});
|
||||
|
||||
let show_cmd = Command::new("show")
|
||||
.usage("show")
|
||||
.action(|_c: &Context| {
|
||||
// ConfigPathsを使って設定ファイルのパスを取得
|
||||
let config_paths = ConfigPaths::new();
|
||||
let ai = load_config(config_paths.data_file("json").to_str().unwrap());
|
||||
println!("🧠 現在のAI状態:\n{:#?}", ai);
|
||||
});
|
||||
|
||||
let talk_cmd = Command::new("talk")
|
||||
.usage("talk")
|
||||
.action(|_c: &Context| {
|
||||
let config_paths = ConfigPaths::new();
|
||||
let ai = load_config(config_paths.data_file("json").to_str().unwrap());
|
||||
|
||||
let now = Local::now().naive_local();
|
||||
let mut state = AIState {
|
||||
relation_score: 80.0,
|
||||
previous_score: 80.0,
|
||||
decay_rate: ai.messaging.decay_rate,
|
||||
sensitivity: ai.personality.strength,
|
||||
message_threshold: 5.0,
|
||||
last_message_time: now - Duration::days(4),
|
||||
};
|
||||
|
||||
state.update(now);
|
||||
|
||||
if state.should_talk() {
|
||||
println!("💬 AI発話: {}", state.generate_message());
|
||||
} else {
|
||||
println!("🤫 今日は静かにしているみたい...");
|
||||
}
|
||||
});
|
||||
|
||||
App::new("aigpt")
|
||||
.version("0.1.0")
|
||||
.description("AGE system CLI controller")
|
||||
.author("syui")
|
||||
.command(set_cmd)
|
||||
.command(show_cmd)
|
||||
.command(talk_cmd)
|
||||
.command(save_cmd())
|
||||
.command(export_cmd())
|
||||
.command(scheduler_cmd())
|
||||
.command(mcp_cmd())
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
// src/commands/db.rs
|
||||
use seahorse::{Command, Context};
|
||||
use crate::utils::{load_config};
|
||||
use crate::model::AiSystem;
|
||||
use crate::config::ConfigPaths;
|
||||
|
||||
use rusqlite::Connection;
|
||||
use std::fs;
|
||||
|
||||
pub fn save_cmd() -> Command {
|
||||
Command::new("save")
|
||||
.usage("save")
|
||||
.action(|_c: &Context| {
|
||||
let paths = ConfigPaths::new();
|
||||
|
||||
let json_path = paths.data_file("json");
|
||||
let db_path = paths.data_file("db");
|
||||
|
||||
let ai = load_config(json_path.to_str().unwrap());
|
||||
let conn = Connection::open(db_path).expect("DB接続失敗");
|
||||
|
||||
ai.save_to_db(&conn).expect("DB保存失敗");
|
||||
println!("💾 DBに保存完了");
|
||||
})
|
||||
}
|
||||
|
||||
pub fn export_cmd() -> Command {
|
||||
Command::new("export")
|
||||
.usage("export [output.json]")
|
||||
.action(|c: &Context| {
|
||||
let output_path = c.args.get(0).map(|s| s.as_str()).unwrap_or("output.json");
|
||||
|
||||
let paths = ConfigPaths::new();
|
||||
let db_path = paths.data_file("db");
|
||||
|
||||
let conn = Connection::open(db_path).expect("DB接続失敗");
|
||||
let ai = AiSystem::load_from_db(&conn).expect("DB読み込み失敗");
|
||||
|
||||
let json = serde_json::to_string_pretty(&ai).expect("JSON変換失敗");
|
||||
fs::write(output_path, json).expect("ファイル書き込み失敗");
|
||||
|
||||
println!("📤 JSONにエクスポート完了: {output_path}");
|
||||
})
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
// src/commands/git_repo.rs
|
||||
use std::fs;
|
||||
|
||||
// Gitリポジトリ内の全てのファイルを取得し、内容を読み取る
|
||||
pub fn read_all_git_files(repo_path: &str) -> String {
|
||||
let mut content = String::new();
|
||||
for entry in fs::read_dir(repo_path).expect("ディレクトリ読み込み失敗") {
|
||||
let entry = entry.expect("エントリ読み込み失敗");
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Ok(file_content) = fs::read_to_string(&path) {
|
||||
content.push_str(&format!("\n\n# File: {}\n{}", path.display(), file_content));
|
||||
}
|
||||
}
|
||||
}
|
||||
content
|
||||
}
|
@@ -1,277 +0,0 @@
|
||||
// src/commands/mcp.rs
|
||||
|
||||
use std::fs;
|
||||
use std::path::{PathBuf};
|
||||
use std::process::Command as OtherCommand;
|
||||
use serde_json::json;
|
||||
use seahorse::{Command, Context, Flag, FlagType};
|
||||
use crate::chat::ask_chat;
|
||||
use crate::git::{git_init, git_status};
|
||||
use crate::config::ConfigPaths;
|
||||
use crate::commands::git_repo::read_all_git_files;
|
||||
use crate::metrics::{load_user_data, save_user_data};
|
||||
use crate::memory::{log_message};
|
||||
|
||||
pub fn mcp_setup() {
|
||||
let config = ConfigPaths::new();
|
||||
let dest_dir = config.base_dir.join("mcp");
|
||||
let repo_url = "https://github.com/microsoft/MCP.git";
|
||||
println!("📁 MCP ディレクトリ: {}", dest_dir.display());
|
||||
|
||||
// 1. git clone(もしまだなければ)
|
||||
if !dest_dir.exists() {
|
||||
let status = OtherCommand::new("git")
|
||||
.args(&["clone", repo_url, dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.expect("git clone に失敗しました");
|
||||
assert!(status.success(), "git clone 実行時にエラーが発生しました");
|
||||
}
|
||||
|
||||
let asset_base = PathBuf::from("mcp");
|
||||
let files_to_copy = vec![
|
||||
"cli.py",
|
||||
"setup.py",
|
||||
"scripts/ask.py",
|
||||
"scripts/server.py",
|
||||
"scripts/config.py",
|
||||
"scripts/summarize.py",
|
||||
"scripts/context_loader.py",
|
||||
"scripts/prompt_template.py",
|
||||
"scripts/memory_store.py",
|
||||
];
|
||||
|
||||
for rel_path in files_to_copy {
|
||||
let src = asset_base.join(rel_path);
|
||||
let dst = dest_dir.join(rel_path);
|
||||
if let Some(parent) = dst.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if let Err(e) = fs::copy(&src, &dst) {
|
||||
eprintln!("❌ コピー失敗: {} → {}: {}", src.display(), dst.display(), e);
|
||||
} else {
|
||||
println!("✅ コピー: {} → {}", src.display(), dst.display());
|
||||
}
|
||||
}
|
||||
|
||||
// venvの作成
|
||||
let venv_path = dest_dir.join(".venv");
|
||||
if !venv_path.exists() {
|
||||
println!("🐍 仮想環境を作成しています...");
|
||||
let output = OtherCommand::new("python3")
|
||||
.args(&["-m", "venv", ".venv"])
|
||||
.current_dir(&dest_dir)
|
||||
.output()
|
||||
.expect("venvの作成に失敗しました");
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("❌ venv作成エラー: {}", String::from_utf8_lossy(&output.stderr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// `pip install -e .` を仮想環境で実行
|
||||
let pip_path = if cfg!(target_os = "windows") {
|
||||
dest_dir.join(".venv/Scripts/pip.exe").to_string_lossy().to_string()
|
||||
} else {
|
||||
dest_dir.join(".venv/bin/pip").to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
println!("📦 必要なパッケージをインストールしています...");
|
||||
let output = OtherCommand::new(&pip_path)
|
||||
.arg("install")
|
||||
.arg("openai")
|
||||
.arg("requests")
|
||||
.arg("fastmcp")
|
||||
.arg("uvicorn")
|
||||
.arg("fastapi")
|
||||
.arg("fastapi_mcp")
|
||||
.arg("mcp")
|
||||
.current_dir(&dest_dir)
|
||||
.output()
|
||||
.expect("pip install に失敗しました");
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"❌ pip エラー: {}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("📦 pip install -e . を実行します...");
|
||||
let output = OtherCommand::new(&pip_path)
|
||||
.arg("install")
|
||||
.arg("-e")
|
||||
.arg(".")
|
||||
.current_dir(&dest_dir)
|
||||
.output()
|
||||
.expect("pip install に失敗しました");
|
||||
|
||||
if output.status.success() {
|
||||
println!("🎉 MCP セットアップが完了しました!");
|
||||
} else {
|
||||
eprintln!(
|
||||
"❌ pip エラー: {}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_api_key_cmd() -> Command {
|
||||
Command::new("set-api")
|
||||
.description("OpenAI APIキーを設定")
|
||||
.usage("mcp set-api --api <API_KEY>")
|
||||
.flag(Flag::new("api", FlagType::String).description("OpenAI APIキー").alias("a"))
|
||||
.action(|c: &Context| {
|
||||
if let Ok(api_key) = c.string_flag("api") {
|
||||
let config = ConfigPaths::new();
|
||||
let path = config.base_dir.join("openai.json");
|
||||
let json_data = json!({ "token": api_key });
|
||||
|
||||
if let Err(e) = fs::write(&path, serde_json::to_string_pretty(&json_data).unwrap()) {
|
||||
eprintln!("❌ ファイル書き込み失敗: {}", e);
|
||||
} else {
|
||||
println!("✅ APIキーを保存しました: {}", path.display());
|
||||
}
|
||||
} else {
|
||||
eprintln!("❗ APIキーを --api で指定してください");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn chat_cmd() -> Command {
|
||||
Command::new("chat")
|
||||
.description("チャットで質問を送る")
|
||||
.usage("mcp chat '質問内容' --host <OLLAMA_HOST> --model <MODEL> [--provider <ollama|openai>] [--api-key <KEY>] [--repo <REPO_URL>]")
|
||||
.flag(
|
||||
Flag::new("host", FlagType::String)
|
||||
.description("OLLAMAホストのURL")
|
||||
.alias("H"),
|
||||
)
|
||||
.flag(
|
||||
Flag::new("model", FlagType::String)
|
||||
.description("モデル名 (OLLAMA_MODEL / OPENAI_MODEL)")
|
||||
.alias("m"),
|
||||
)
|
||||
.flag(
|
||||
Flag::new("provider", FlagType::String)
|
||||
.description("使用するプロバイダ (ollama / openai)")
|
||||
.alias("p"),
|
||||
)
|
||||
.flag(
|
||||
Flag::new("api-key", FlagType::String)
|
||||
.description("OpenAI APIキー")
|
||||
.alias("k"),
|
||||
)
|
||||
.flag(
|
||||
Flag::new("repo", FlagType::String)
|
||||
.description("Gitリポジトリのパスを指定 (すべてのコードを読み込む)")
|
||||
.alias("r"),
|
||||
)
|
||||
.action(|c: &Context| {
|
||||
let config = ConfigPaths::new();
|
||||
let user_path = config.data_file("json");
|
||||
let mut user = load_user_data(&user_path);
|
||||
// repoがある場合は、コードベース読み込みモード
|
||||
if let Ok(repo_url) = c.string_flag("repo") {
|
||||
let repo_base = config.base_dir.join("repos");
|
||||
let repo_dir = repo_base.join(sanitize_repo_name(&repo_url));
|
||||
|
||||
if !repo_dir.exists() {
|
||||
println!("📥 Gitリポジトリをクローン中: {}", repo_url);
|
||||
let status = OtherCommand::new("git")
|
||||
.args(&["clone", &repo_url, repo_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.expect("❌ Gitのクローンに失敗しました");
|
||||
assert!(status.success(), "Git clone エラー");
|
||||
} else {
|
||||
println!("✔ リポジトリはすでに存在します: {}", repo_dir.display());
|
||||
}
|
||||
|
||||
let files = read_all_git_files(repo_dir.to_str().unwrap());
|
||||
let prompt = format!(
|
||||
"以下のコードベースを読み込んで、改善案や次のステップを提案してください:\n{}",
|
||||
files
|
||||
);
|
||||
|
||||
if let Some(response) = ask_chat(c, &prompt) {
|
||||
println!("💬 提案:\n{}", response);
|
||||
} else {
|
||||
eprintln!("❗ 提案が取得できませんでした");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 通常のチャット処理(repoが指定されていない場合)
|
||||
match c.args.get(0) {
|
||||
Some(question) => {
|
||||
log_message(&config.base_dir, "user", question);
|
||||
let response = ask_chat(c, question);
|
||||
|
||||
if let Some(ref text) = response {
|
||||
println!("💬 応答:\n{}", text);
|
||||
// 返答内容に基づいて増減(返答の感情解析)
|
||||
if text.contains("thank") || text.contains("great") {
|
||||
user.metrics.trust += 0.05;
|
||||
} else if text.contains("hate") || text.contains("bad") {
|
||||
user.metrics.trust -= 0.05;
|
||||
}
|
||||
log_message(&config.base_dir, "ai", &text);
|
||||
save_user_data(&user_path, &user);
|
||||
} else {
|
||||
eprintln!("❗ 応答が取得できませんでした");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
eprintln!("❗ 質問が必要です: mcp chat 'こんにちは'");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn init_cmd() -> Command {
|
||||
Command::new("init")
|
||||
.description("Git 初期化")
|
||||
.usage("mcp init")
|
||||
.action(|_| {
|
||||
git_init();
|
||||
})
|
||||
}
|
||||
|
||||
fn status_cmd() -> Command {
|
||||
Command::new("status")
|
||||
.description("Git ステータス表示")
|
||||
.usage("mcp status")
|
||||
.action(|_| {
|
||||
git_status();
|
||||
})
|
||||
}
|
||||
|
||||
fn setup_cmd() -> Command {
|
||||
Command::new("setup")
|
||||
.description("MCP の初期セットアップ")
|
||||
.usage("mcp setup")
|
||||
.action(|_| {
|
||||
mcp_setup();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mcp_cmd() -> Command {
|
||||
Command::new("mcp")
|
||||
.description("MCP操作コマンド")
|
||||
.usage("mcp <subcommand>")
|
||||
.alias("m")
|
||||
.command(chat_cmd())
|
||||
.command(init_cmd())
|
||||
.command(status_cmd())
|
||||
.command(setup_cmd())
|
||||
.command(set_api_key_cmd())
|
||||
}
|
||||
|
||||
// ファイル名として安全な形に変換
|
||||
fn sanitize_repo_name(repo_url: &str) -> String {
|
||||
repo_url.replace("://", "_").replace("/", "_").replace("@", "_")
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
pub mod db;
|
||||
pub mod scheduler;
|
||||
pub mod mcp;
|
||||
pub mod git_repo;
|
@@ -1,127 +0,0 @@
|
||||
// src/commands/scheduler.rs
|
||||
use seahorse::{Command, Context};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use chrono::{Local, Utc, Timelike};
|
||||
use crate::metrics::{load_user_data, save_user_data};
|
||||
use crate::config::ConfigPaths;
|
||||
use crate::chat::ask_chat;
|
||||
use rand::prelude::*;
|
||||
use rand::rng;
|
||||
|
||||
fn send_scheduled_message() {
|
||||
let config = ConfigPaths::new();
|
||||
let user_path = config.data_file("json");
|
||||
let mut user = load_user_data(&user_path);
|
||||
|
||||
if !user.metrics.can_send {
|
||||
println!("🚫 送信条件を満たしていないため、スケジュール送信スキップ");
|
||||
return;
|
||||
}
|
||||
|
||||
// 日付の比較(1日1回制限)
|
||||
let today = Local::now().format("%Y-%m-%d").to_string();
|
||||
if let Some(last_date) = &user.messaging.last_sent_date {
|
||||
if last_date != &today {
|
||||
user.messaging.sent_today = false;
|
||||
}
|
||||
} else {
|
||||
user.messaging.sent_today = false;
|
||||
}
|
||||
|
||||
if user.messaging.sent_today {
|
||||
println!("🔁 本日はすでに送信済みです: {}", today);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(schedule_str) = &user.messaging.schedule_time {
|
||||
let now = Local::now();
|
||||
let target: Vec<&str> = schedule_str.split(':').collect();
|
||||
|
||||
if target.len() != 2 {
|
||||
println!("⚠️ schedule_time形式が無効です: {}", schedule_str);
|
||||
return;
|
||||
}
|
||||
|
||||
let (sh, sm) = (target[0].parse::<u32>(), target[1].parse::<u32>());
|
||||
if let (Ok(sh), Ok(sm)) = (sh, sm) {
|
||||
if now.hour() == sh && now.minute() == sm {
|
||||
if let Some(msg) = user.messaging.templates.choose(&mut rng()) {
|
||||
println!("💬 自動送信メッセージ: {}", msg);
|
||||
let dummy_context = Context::new(vec![], None, "".to_string());
|
||||
ask_chat(&dummy_context, msg);
|
||||
user.metrics.intimacy += 0.03;
|
||||
|
||||
// 送信済みのフラグ更新
|
||||
user.messaging.sent_today = true;
|
||||
user.messaging.last_sent_date = Some(today);
|
||||
|
||||
save_user_data(&user_path, &user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn scheduler_cmd() -> Command {
|
||||
Command::new("scheduler")
|
||||
.usage("scheduler [interval_sec]")
|
||||
.alias("s")
|
||||
.description("定期的に送信条件をチェックし、自発的なメッセージ送信を試みる")
|
||||
.action(|c: &Context| {
|
||||
let interval = c.args.get(0)
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(3600); // デフォルト: 1時間(テストしやすく)
|
||||
|
||||
println!("⏳ スケジューラー開始({}秒ごと)...", interval);
|
||||
|
||||
loop {
|
||||
let config = ConfigPaths::new();
|
||||
let user_path = config.data_file("json");
|
||||
let mut user = load_user_data(&user_path);
|
||||
|
||||
let now = Utc::now();
|
||||
let elapsed = now.signed_duration_since(user.metrics.last_updated);
|
||||
let hours = elapsed.num_minutes() as f32 / 60.0;
|
||||
|
||||
let speed_factor = if hours > 48.0 {
|
||||
2.0
|
||||
} else if hours > 24.0 {
|
||||
1.5
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
user.metrics.trust = (user.metrics.trust - 0.01 * speed_factor).clamp(0.0, 1.0);
|
||||
user.metrics.intimacy = (user.metrics.intimacy - 0.01 * speed_factor).clamp(0.0, 1.0);
|
||||
user.metrics.energy = (user.metrics.energy - 0.01 * speed_factor).clamp(0.0, 1.0);
|
||||
|
||||
user.metrics.can_send =
|
||||
user.metrics.trust >= 0.5 &&
|
||||
user.metrics.intimacy >= 0.5 &&
|
||||
user.metrics.energy >= 0.5;
|
||||
|
||||
user.metrics.last_updated = now;
|
||||
|
||||
if user.metrics.can_send {
|
||||
println!("💡 AIメッセージ送信条件を満たしています(信頼:{:.2}, 親密:{:.2}, エネルギー:{:.2})",
|
||||
user.metrics.trust,
|
||||
user.metrics.intimacy,
|
||||
user.metrics.energy
|
||||
);
|
||||
send_scheduled_message();
|
||||
} else {
|
||||
println!("🤫 条件未達成のため送信スキップ: trust={:.2}, intimacy={:.2}, energy={:.2}",
|
||||
user.metrics.trust,
|
||||
user.metrics.intimacy,
|
||||
user.metrics.energy
|
||||
);
|
||||
}
|
||||
|
||||
save_user_data(&user_path, &user);
|
||||
thread::sleep(Duration::from_secs(interval));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
42
src/git.rs
42
src/git.rs
@@ -1,42 +0,0 @@
|
||||
// src/git.rs
|
||||
use std::process::Command;
|
||||
|
||||
pub fn git_status() {
|
||||
run_git_command(&["status"]);
|
||||
}
|
||||
|
||||
pub fn git_init() {
|
||||
run_git_command(&["init"]);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn git_commit(message: &str) {
|
||||
run_git_command(&["add", "."]);
|
||||
run_git_command(&["commit", "-m", message]);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn git_push() {
|
||||
run_git_command(&["push"]);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn git_pull() {
|
||||
run_git_command(&["pull"]);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn git_branch() {
|
||||
run_git_command(&["branch"]);
|
||||
}
|
||||
|
||||
fn run_git_command(args: &[&str]) {
|
||||
let status = Command::new("git")
|
||||
.args(args)
|
||||
.status()
|
||||
.expect("git コマンドの実行に失敗しました");
|
||||
|
||||
if !status.success() {
|
||||
eprintln!("⚠️ git コマンドに失敗しました: {:?}", args);
|
||||
}
|
||||
}
|
13
src/logic.rs
13
src/logic.rs
@@ -1,13 +0,0 @@
|
||||
//src/logic.rs
|
||||
use crate::model::AiSystem;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn should_send(ai: &AiSystem) -> bool {
|
||||
let r = &ai.relationship;
|
||||
let env = &ai.environment;
|
||||
let score = r.trust + r.intimacy + r.curiosity;
|
||||
let relationship_ok = score >= r.threshold;
|
||||
let luck_ok = env.luck_today > 0.5;
|
||||
|
||||
ai.messaging.enabled && relationship_ok && luck_ok
|
||||
}
|
21
src/main.rs
21
src/main.rs
@@ -1,21 +0,0 @@
|
||||
//src/main.rs
|
||||
mod model;
|
||||
mod logic;
|
||||
mod agent;
|
||||
mod cli;
|
||||
mod utils;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod git;
|
||||
mod chat;
|
||||
mod metrics;
|
||||
mod memory;
|
||||
|
||||
use cli::cli_app;
|
||||
use seahorse::App;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let app: App = cli_app();
|
||||
app.run(args);
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
// src/memory.rs
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self};
|
||||
//use std::fs::{self, OpenOptions};
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::path::PathBuf;
|
||||
use std::{fs::File};
|
||||
//use std::{env, fs::File};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MemoryEntry {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub sender: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub fn log_message(base_dir: &PathBuf, sender: &str, message: &str) {
|
||||
let now_utc = Utc::now();
|
||||
let date_str = Local::now().format("%Y-%m-%d").to_string();
|
||||
let mut file_path = base_dir.clone();
|
||||
file_path.push("memory");
|
||||
let _ = fs::create_dir_all(&file_path);
|
||||
file_path.push(format!("{}.json", date_str));
|
||||
|
||||
let new_entry = MemoryEntry {
|
||||
timestamp: now_utc,
|
||||
sender: sender.to_string(),
|
||||
message: message.to_string(),
|
||||
};
|
||||
|
||||
let mut entries = if file_path.exists() {
|
||||
let file = File::open(&file_path).expect("💥 メモリファイルの読み込み失敗");
|
||||
let reader = BufReader::new(file);
|
||||
serde_json::from_reader(reader).unwrap_or_else(|_| vec![])
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
entries.push(new_entry);
|
||||
|
||||
let file = File::create(&file_path).expect("💥 メモリファイルの書き込み失敗");
|
||||
let writer = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(writer, &entries).expect("💥 JSONの書き込み失敗");
|
||||
}
|
||||
|
||||
// 利用例(ask_chatの中)
|
||||
// log_message(&config.base_dir, "user", question);
|
||||
// log_message(&config.base_dir, "ai", &response);
|
147
src/metrics.rs
147
src/metrics.rs
@@ -1,147 +0,0 @@
|
||||
// src/metrics.rs
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::config::ConfigPaths;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Metrics {
|
||||
pub trust: f32,
|
||||
pub intimacy: f32,
|
||||
pub energy: f32,
|
||||
pub can_send: bool,
|
||||
pub last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Personality {
|
||||
pub kind: String,
|
||||
pub strength: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Relationship {
|
||||
pub trust: f32,
|
||||
pub intimacy: f32,
|
||||
pub curiosity: f32,
|
||||
pub threshold: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Environment {
|
||||
pub luck_today: f32,
|
||||
pub luck_history: Vec<f32>,
|
||||
pub level: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Messaging {
|
||||
pub enabled: bool,
|
||||
pub schedule_time: Option<String>,
|
||||
pub decay_rate: f32,
|
||||
pub templates: Vec<String>,
|
||||
pub sent_today: bool, // 追加
|
||||
pub last_sent_date: Option<String>, // 追加
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Memory {
|
||||
pub recent_messages: Vec<String>,
|
||||
pub long_term_notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserData {
|
||||
pub personality: Personality,
|
||||
pub relationship: Relationship,
|
||||
pub environment: Environment,
|
||||
pub messaging: Messaging,
|
||||
pub last_interaction: DateTime<Utc>,
|
||||
pub memory: Memory,
|
||||
pub metrics: Metrics,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
pub fn decay(&mut self) {
|
||||
let now = Utc::now();
|
||||
let hours = (now - self.last_updated).num_minutes() as f32 / 60.0;
|
||||
self.trust = decay_param(self.trust, hours);
|
||||
self.intimacy = decay_param(self.intimacy, hours);
|
||||
self.energy = decay_param(self.energy, hours);
|
||||
self.can_send = self.trust >= 0.5 && self.intimacy >= 0.5 && self.energy >= 0.5;
|
||||
self.last_updated = now;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_user_data(path: &Path) -> UserData {
|
||||
let config = ConfigPaths::new();
|
||||
let example_path = Path::new("example.json");
|
||||
config.ensure_file_exists("json", example_path);
|
||||
|
||||
if !path.exists() {
|
||||
return UserData {
|
||||
personality: Personality {
|
||||
kind: "positive".into(),
|
||||
strength: 0.8,
|
||||
},
|
||||
relationship: Relationship {
|
||||
trust: 0.2,
|
||||
intimacy: 0.6,
|
||||
curiosity: 0.5,
|
||||
threshold: 1.5,
|
||||
},
|
||||
environment: Environment {
|
||||
luck_today: 0.9,
|
||||
luck_history: vec![0.9, 0.9, 0.9],
|
||||
level: 1,
|
||||
},
|
||||
messaging: Messaging {
|
||||
enabled: true,
|
||||
schedule_time: Some("08:00".to_string()),
|
||||
decay_rate: 0.1,
|
||||
templates: vec![
|
||||
"おはよう!今日もがんばろう!".to_string(),
|
||||
"ねえ、話したいことがあるの。".to_string(),
|
||||
],
|
||||
sent_today: false,
|
||||
last_sent_date: None,
|
||||
},
|
||||
last_interaction: Utc::now(),
|
||||
memory: Memory {
|
||||
recent_messages: vec![],
|
||||
long_term_notes: vec![],
|
||||
},
|
||||
metrics: Metrics {
|
||||
trust: 0.5,
|
||||
intimacy: 0.5,
|
||||
energy: 0.5,
|
||||
can_send: true,
|
||||
last_updated: Utc::now(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(path).expect("user.json の読み込みに失敗しました");
|
||||
serde_json::from_str(&content).expect("user.json のパースに失敗しました")
|
||||
}
|
||||
|
||||
pub fn save_user_data(path: &Path, data: &UserData) {
|
||||
let content = serde_json::to_string_pretty(data).expect("user.json のシリアライズ失敗");
|
||||
fs::write(path, content).expect("user.json の書き込みに失敗しました");
|
||||
}
|
||||
|
||||
pub fn update_metrics_decay() -> Metrics {
|
||||
let config = ConfigPaths::new();
|
||||
let path = config.base_dir.join("user.json");
|
||||
let mut data = load_user_data(&path);
|
||||
data.metrics.decay();
|
||||
save_user_data(&path, &data);
|
||||
data.metrics
|
||||
}
|
||||
|
||||
fn decay_param(value: f32, hours: f32) -> f32 {
|
||||
let decay_rate = 0.05;
|
||||
(value * (1.0f32 - decay_rate).powf(hours)).clamp(0.0, 1.0)
|
||||
}
|
72
src/model.rs
72
src/model.rs
@@ -1,72 +0,0 @@
|
||||
//src/model.rs
|
||||
use rusqlite::{params, Connection, Result as SqlResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AiSystem {
|
||||
pub personality: Personality,
|
||||
pub relationship: Relationship,
|
||||
pub environment: Environment,
|
||||
pub messaging: Messaging,
|
||||
}
|
||||
|
||||
impl AiSystem {
|
||||
pub fn save_to_db(&self, conn: &Connection) -> SqlResult<()> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS ai_state (id INTEGER PRIMARY KEY, json TEXT)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
let json_data = serde_json::to_string(self).map_err(|e| {
|
||||
rusqlite::Error::ToSqlConversionFailure(Box::new(e))
|
||||
})?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO ai_state (id, json) VALUES (?1, ?2)",
|
||||
params![1, json_data],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_db(conn: &Connection) -> SqlResult<Self> {
|
||||
let mut stmt = conn.prepare("SELECT json FROM ai_state WHERE id = ?1")?;
|
||||
let json: String = stmt.query_row(params![1], |row| row.get(0))?;
|
||||
|
||||
// ここも serde_json のエラーを map_err で変換
|
||||
let system: AiSystem = serde_json::from_str(&json).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
|
||||
Ok(system)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Personality {
|
||||
pub kind: String, // e.g., "positive", "negative", "neutral"
|
||||
pub strength: f32, // 0.0 - 1.0
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Relationship {
|
||||
pub trust: f32, // 0.0 - 1.0
|
||||
pub intimacy: f32, // 0.0 - 1.0
|
||||
pub curiosity: f32, // 0.0 - 1.0
|
||||
pub threshold: f32, // if sum > threshold, allow messaging
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Environment {
|
||||
pub luck_today: f32, // 0.1 - 1.0
|
||||
pub luck_history: Vec<f32>, // last 3 values
|
||||
pub level: i32, // current mental strength level
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Messaging {
|
||||
pub enabled: bool,
|
||||
pub schedule_time: Option<String>, // e.g., "08:00"
|
||||
pub decay_rate: f32, // how quickly emotion fades (0.0 - 1.0)
|
||||
pub templates: Vec<String>, // message template variations
|
||||
}
|
13
src/utils.rs
13
src/utils.rs
@@ -1,13 +0,0 @@
|
||||
// src/utils.rs
|
||||
use std::fs;
|
||||
use crate::model::AiSystem;
|
||||
|
||||
pub fn load_config(path: &str) -> AiSystem {
|
||||
let data = fs::read_to_string(path).expect("JSON読み込み失敗");
|
||||
serde_json::from_str(&data).expect("JSONパース失敗")
|
||||
}
|
||||
|
||||
pub fn save_config(path: &str, ai: &AiSystem) {
|
||||
let json = serde_json::to_string_pretty(&ai).expect("JSONシリアライズ失敗");
|
||||
fs::write(path, json).expect("JSON保存失敗");
|
||||
}
|
Reference in New Issue
Block a user