fix config
This commit is contained in:
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": []
|
||||||
|
}
|
||||||
|
}
|
60
README.md
60
README.md
@ -21,63 +21,63 @@ pip install -e .
|
|||||||
### APIキーの設定
|
### APIキーの設定
|
||||||
```bash
|
```bash
|
||||||
# OpenAI APIキー
|
# OpenAI APIキー
|
||||||
ai-gpt config set providers.openai.api_key sk-xxxxx
|
aigpt config set providers.openai.api_key sk-xxxxx
|
||||||
|
|
||||||
# atproto認証情報(将来の自動投稿用)
|
# atproto認証情報(将来の自動投稿用)
|
||||||
ai-gpt config set atproto.handle your.handle
|
aigpt config set atproto.handle your.handle
|
||||||
ai-gpt config set atproto.password your-password
|
aigpt config set atproto.password your-password
|
||||||
|
|
||||||
# 設定一覧を確認
|
# 設定一覧を確認
|
||||||
ai-gpt config list
|
aigpt config list
|
||||||
```
|
```
|
||||||
|
|
||||||
### データ保存場所
|
### データ保存場所
|
||||||
- 設定: `~/.config/aigpt/config.json`
|
- 設定: `~/.config/syui/ai/gpt/config.json`
|
||||||
- データ: `~/.config/aigpt/data/`
|
- データ: `~/.config/syui/ai/gpt/data/`
|
||||||
|
|
||||||
## 使い方
|
## 使い方
|
||||||
|
|
||||||
### 会話する
|
### 会話する
|
||||||
```bash
|
```bash
|
||||||
ai-gpt chat "did:plc:xxxxx" "こんにちは、今日はどんな気分?"
|
aigpt chat "did:plc:xxxxx" "こんにちは、今日はどんな気分?"
|
||||||
```
|
```
|
||||||
|
|
||||||
### ステータス確認
|
### ステータス確認
|
||||||
```bash
|
```bash
|
||||||
# AI全体の状態
|
# AI全体の状態
|
||||||
ai-gpt status
|
aigpt status
|
||||||
|
|
||||||
# 特定ユーザーとの関係
|
# 特定ユーザーとの関係
|
||||||
ai-gpt status "did:plc:xxxxx"
|
aigpt status "did:plc:xxxxx"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 今日の運勢
|
### 今日の運勢
|
||||||
```bash
|
```bash
|
||||||
ai-gpt fortune
|
aigpt fortune
|
||||||
```
|
```
|
||||||
|
|
||||||
### 自律送信チェック
|
### 自律送信チェック
|
||||||
```bash
|
```bash
|
||||||
# ドライラン(確認のみ)
|
# ドライラン(確認のみ)
|
||||||
ai-gpt transmit
|
aigpt transmit
|
||||||
|
|
||||||
# 実行
|
# 実行
|
||||||
ai-gpt transmit --execute
|
aigpt transmit --execute
|
||||||
```
|
```
|
||||||
|
|
||||||
### 日次メンテナンス
|
### 日次メンテナンス
|
||||||
```bash
|
```bash
|
||||||
ai-gpt maintenance
|
aigpt maintenance
|
||||||
```
|
```
|
||||||
|
|
||||||
### 関係一覧
|
### 関係一覧
|
||||||
```bash
|
```bash
|
||||||
ai-gpt relationships
|
aigpt relationships
|
||||||
```
|
```
|
||||||
|
|
||||||
## データ構造
|
## データ構造
|
||||||
|
|
||||||
デフォルトでは `~/.ai_gpt/` に以下のファイルが保存されます:
|
デフォルトでは `~/.config/syui/ai/gpt/` に以下のファイルが保存されます:
|
||||||
|
|
||||||
- `memories.json` - 会話記憶
|
- `memories.json` - 会話記憶
|
||||||
- `conversations.json` - 会話ログ
|
- `conversations.json` - 会話ログ
|
||||||
@ -98,22 +98,22 @@ ai-gpt relationships
|
|||||||
### サーバー起動
|
### サーバー起動
|
||||||
```bash
|
```bash
|
||||||
# Ollamaを使用(デフォルト)
|
# Ollamaを使用(デフォルト)
|
||||||
ai-gpt server --model qwen2.5 --provider ollama
|
aigpt server --model qwen2.5 --provider ollama
|
||||||
|
|
||||||
# OpenAIを使用
|
# OpenAIを使用
|
||||||
ai-gpt server --model gpt-4o-mini --provider openai
|
aigpt server --model gpt-4o-mini --provider openai
|
||||||
|
|
||||||
# カスタムポート
|
# カスタムポート
|
||||||
ai-gpt server --port 8080
|
aigpt server --port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### AIプロバイダーを使った会話
|
### AIプロバイダーを使った会話
|
||||||
```bash
|
```bash
|
||||||
# Ollamaで会話
|
# Ollamaで会話
|
||||||
ai-gpt chat "did:plc:xxxxx" "こんにちは" --provider ollama --model qwen2.5
|
aigpt chat "did:plc:xxxxx" "こんにちは" --provider ollama --model qwen2.5
|
||||||
|
|
||||||
# OpenAIで会話
|
# OpenAIで会話
|
||||||
ai-gpt chat "did:plc:xxxxx" "今日の調子はどう?" --provider openai --model gpt-4o-mini
|
aigpt chat "did:plc:xxxxx" "今日の調子はどう?" --provider openai --model gpt-4o-mini
|
||||||
```
|
```
|
||||||
|
|
||||||
### MCP Tools
|
### MCP Tools
|
||||||
@ -145,42 +145,42 @@ cp .env.example .env
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 6時間ごとに送信チェック
|
# 6時間ごとに送信チェック
|
||||||
ai-gpt schedule add transmission_check "0 */6 * * *" --provider ollama --model qwen2.5
|
aigpt schedule add transmission_check "0 */6 * * *" --provider ollama --model qwen2.5
|
||||||
|
|
||||||
# 30分ごとに送信チェック(インターバル形式)
|
# 30分ごとに送信チェック(インターバル形式)
|
||||||
ai-gpt schedule add transmission_check "30m"
|
aigpt schedule add transmission_check "30m"
|
||||||
|
|
||||||
# 毎日午前3時にメンテナンス
|
# 毎日午前3時にメンテナンス
|
||||||
ai-gpt schedule add maintenance "0 3 * * *"
|
aigpt schedule add maintenance "0 3 * * *"
|
||||||
|
|
||||||
# 1時間ごとに関係性減衰
|
# 1時間ごとに関係性減衰
|
||||||
ai-gpt schedule add relationship_decay "1h"
|
aigpt schedule add relationship_decay "1h"
|
||||||
|
|
||||||
# 毎週月曜日に記憶要約
|
# 毎週月曜日に記憶要約
|
||||||
ai-gpt schedule add memory_summary "0 0 * * MON"
|
aigpt schedule add memory_summary "0 0 * * MON"
|
||||||
```
|
```
|
||||||
|
|
||||||
### タスク管理
|
### タスク管理
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# タスク一覧
|
# タスク一覧
|
||||||
ai-gpt schedule list
|
aigpt schedule list
|
||||||
|
|
||||||
# タスクを無効化
|
# タスクを無効化
|
||||||
ai-gpt schedule disable --task-id transmission_check_1234567890
|
aigpt schedule disable --task-id transmission_check_1234567890
|
||||||
|
|
||||||
# タスクを有効化
|
# タスクを有効化
|
||||||
ai-gpt schedule enable --task-id transmission_check_1234567890
|
aigpt schedule enable --task-id transmission_check_1234567890
|
||||||
|
|
||||||
# タスクを削除
|
# タスクを削除
|
||||||
ai-gpt schedule remove --task-id transmission_check_1234567890
|
aigpt schedule remove --task-id transmission_check_1234567890
|
||||||
```
|
```
|
||||||
|
|
||||||
### スケジューラーデーモンの起動
|
### スケジューラーデーモンの起動
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# バックグラウンドでスケジューラーを実行
|
# バックグラウンドでスケジューラーを実行
|
||||||
ai-gpt schedule run
|
aigpt schedule run
|
||||||
```
|
```
|
||||||
|
|
||||||
### スケジュール形式
|
### スケジュール形式
|
||||||
|
55
claude.md
55
claude.md
@ -1,4 +1,4 @@
|
|||||||
# syuiエコシステム統合設計書
|
# エコシステム統合設計書
|
||||||
|
|
||||||
## 中核思想
|
## 中核思想
|
||||||
- **存在子理論**: この世界で最も小さいもの(存在子/ai)の探求
|
- **存在子理論**: この世界で最も小さいもの(存在子/ai)の探求
|
||||||
@ -26,6 +26,53 @@
|
|||||||
└── ai 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
|
### ai.gpt - 自律的送信AI
|
||||||
@ -265,7 +312,7 @@ ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界)
|
|||||||
- 統合人格システム(Persona)
|
- 統合人格システム(Persona)
|
||||||
- スケジューラー(5種類のタスク)
|
- スケジューラー(5種類のタスク)
|
||||||
- MCP Server(9種類のツール)
|
- MCP Server(9種類のツール)
|
||||||
- 設定管理(~/.config/aigpt/)
|
- 設定管理(~/.config/syui/ai/gpt/)
|
||||||
- 全CLIコマンド実装
|
- 全CLIコマンド実装
|
||||||
|
|
||||||
### 次の開発ポイント
|
### 次の開発ポイント
|
||||||
@ -273,3 +320,7 @@ ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界)
|
|||||||
- 自律送信: transmission.pyでatproto実装
|
- 自律送信: transmission.pyでatproto実装
|
||||||
- ai.bot連携: 新規bot_connector.py作成
|
- ai.bot連携: 新規bot_connector.py作成
|
||||||
- テスト: tests/ディレクトリ追加
|
- テスト: tests/ディレクトリ追加
|
||||||
|
|
||||||
|
# footer
|
||||||
|
|
||||||
|
© syui
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 設定ファイルの場所
|
## 設定ファイルの場所
|
||||||
|
|
||||||
ai.gptの設定は `~/.config/aigpt/config.json` に保存されます。
|
ai.gptの設定は `~/.config/syui/ai/gpt/config.json` に保存されます。
|
||||||
|
|
||||||
## 設定構造
|
## 設定構造
|
||||||
|
|
||||||
@ -33,38 +33,38 @@ ai.gptの設定は `~/.config/aigpt/config.json` に保存されます。
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# APIキーを設定
|
# APIキーを設定
|
||||||
ai-gpt config set providers.openai.api_key sk-xxxxx
|
aigpt config set providers.openai.api_key sk-xxxxx
|
||||||
|
|
||||||
# デフォルトモデルを変更
|
# デフォルトモデルを変更
|
||||||
ai-gpt config set providers.openai.default_model gpt-4-turbo
|
aigpt config set providers.openai.default_model gpt-4-turbo
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ollama
|
### Ollama
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ホストを変更(リモートOllamaサーバーを使用する場合)
|
# ホストを変更(リモートOllamaサーバーを使用する場合)
|
||||||
ai-gpt config set providers.ollama.host http://192.168.1.100:11434
|
aigpt config set providers.ollama.host http://192.168.1.100:11434
|
||||||
|
|
||||||
# デフォルトモデルを変更
|
# デフォルトモデルを変更
|
||||||
ai-gpt config set providers.ollama.default_model llama2
|
aigpt config set providers.ollama.default_model llama2
|
||||||
```
|
```
|
||||||
|
|
||||||
## atproto設定(将来の自動投稿用)
|
## atproto設定(将来の自動投稿用)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Blueskyアカウント
|
# Blueskyアカウント
|
||||||
ai-gpt config set atproto.handle yourhandle.bsky.social
|
aigpt config set atproto.handle yourhandle.bsky.social
|
||||||
ai-gpt config set atproto.password your-app-password
|
aigpt config set atproto.password your-app-password
|
||||||
|
|
||||||
# セルフホストサーバーを使用
|
# セルフホストサーバーを使用
|
||||||
ai-gpt config set atproto.host https://your-pds.example.com
|
aigpt config set atproto.host https://your-pds.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## デフォルトプロバイダー
|
## デフォルトプロバイダー
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# デフォルトをOpenAIに変更
|
# デフォルトをOpenAIに変更
|
||||||
ai-gpt config set default_provider openai
|
aigpt config set default_provider openai
|
||||||
```
|
```
|
||||||
|
|
||||||
## セキュリティ
|
## セキュリティ
|
||||||
@ -74,7 +74,7 @@ ai-gpt config set default_provider openai
|
|||||||
設定ファイルは平文で保存されるため、適切なファイル権限を設定してください:
|
設定ファイルは平文で保存されるため、適切なファイル権限を設定してください:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod 600 ~/.config/aigpt/config.json
|
chmod 600 ~/.config/syui/ai/gpt/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### 環境変数との優先順位
|
### 環境変数との優先順位
|
||||||
@ -92,10 +92,10 @@ chmod 600 ~/.config/aigpt/config.json
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# バックアップ
|
# バックアップ
|
||||||
cp ~/.config/aigpt/config.json ~/.config/aigpt/config.json.backup
|
cp ~/.config/syui/ai/gpt/config.json ~/.config/syui/ai/gpt/config.json.backup
|
||||||
|
|
||||||
# リストア
|
# リストア
|
||||||
cp ~/.config/aigpt/config.json.backup ~/.config/aigpt/config.json
|
cp ~/.config/syui/ai/gpt/config.json.backup ~/.config/syui/ai/gpt/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## トラブルシューティング
|
## トラブルシューティング
|
||||||
@ -104,15 +104,15 @@ cp ~/.config/aigpt/config.json.backup ~/.config/aigpt/config.json
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 現在の設定を確認
|
# 現在の設定を確認
|
||||||
ai-gpt config list
|
aigpt config list
|
||||||
|
|
||||||
# 特定のキーを確認
|
# 特定のキーを確認
|
||||||
ai-gpt config get providers.openai.api_key
|
aigpt config get providers.openai.api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
### 設定をリセット
|
### 設定をリセット
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 設定ファイルを削除(次回実行時に再作成)
|
# 設定ファイルを削除(次回実行時に再作成)
|
||||||
rm ~/.config/aigpt/config.json
|
rm ~/.config/syui/ai/gpt/config.json
|
||||||
```
|
```
|
@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ai-gpt"
|
name = "aigpt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Autonomous transmission AI with unique personality based on relationship parameters"
|
description = "Autonomous transmission AI with unique personality based on relationship parameters"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@ -19,7 +19,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
ai-gpt = "ai_gpt.cli:app"
|
aigpt = "aigpt.cli:app"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0", "wheel"]
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
@ -29,4 +29,4 @@ build-backend = "setuptools.build_meta"
|
|||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
ai_gpt = ["data/*.json"]
|
aigpt = ["data/*.json"]
|
@ -3,7 +3,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ディレクトリ設定
|
# ディレクトリ設定
|
||||||
BASE_DIR = Path.home() / ".config" / "aigpt"
|
BASE_DIR = Path.home() / ".config" / "syui" / "ai" / "gpt"
|
||||||
MEMORY_DIR = BASE_DIR / "memory"
|
MEMORY_DIR = BASE_DIR / "memory"
|
||||||
SUMMARY_DIR = MEMORY_DIR / "summary"
|
SUMMARY_DIR = MEMORY_DIR / "summary"
|
||||||
|
|
||||||
|
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"
|
@ -1,15 +0,0 @@
|
|||||||
"""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",
|
|
||||||
]
|
|
@ -1,172 +0,0 @@
|
|||||||
"""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}")
|
|
@ -1,444 +0,0 @@
|
|||||||
"""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()
|
|
@ -1,145 +0,0 @@
|
|||||||
"""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" / "aigpt"
|
|
||||||
|
|
||||||
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}", {})
|
|
@ -1,118 +0,0 @@
|
|||||||
"""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
|
|
@ -1,149 +0,0 @@
|
|||||||
"""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
|
|
@ -1,155 +0,0 @@
|
|||||||
"""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]
|
|
@ -1,79 +0,0 @@
|
|||||||
"""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
|
|
@ -1,181 +0,0 @@
|
|||||||
"""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")
|
|
@ -1,135 +0,0 @@
|
|||||||
"""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
|
|
||||||
}
|
|
@ -1,312 +0,0 @@
|
|||||||
"""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
|
|
@ -1,111 +0,0 @@
|
|||||||
"""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
|
|
||||||
}
|
|
Reference in New Issue
Block a user