diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9cb5676 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(mv:*)", + "Bash(mkdir:*)", + "Bash(chmod:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/README.md b/README.md index c4c74fe..42f98ee 100644 --- a/README.md +++ b/README.md @@ -21,63 +21,63 @@ pip install -e . ### APIキーの設定 ```bash # OpenAI APIキー -ai-gpt config set providers.openai.api_key sk-xxxxx +aigpt config set providers.openai.api_key sk-xxxxx # atproto認証情報(将来の自動投稿用) -ai-gpt config set atproto.handle your.handle -ai-gpt config set atproto.password your-password +aigpt config set atproto.handle your.handle +aigpt config set atproto.password your-password # 設定一覧を確認 -ai-gpt config list +aigpt config list ``` ### データ保存場所 -- 設定: `~/.config/aigpt/config.json` -- データ: `~/.config/aigpt/data/` +- 設定: `~/.config/syui/ai/gpt/config.json` +- データ: `~/.config/syui/ai/gpt/data/` ## 使い方 ### 会話する ```bash -ai-gpt chat "did:plc:xxxxx" "こんにちは、今日はどんな気分?" +aigpt chat "did:plc:xxxxx" "こんにちは、今日はどんな気分?" ``` ### ステータス確認 ```bash # AI全体の状態 -ai-gpt status +aigpt status # 特定ユーザーとの関係 -ai-gpt status "did:plc:xxxxx" +aigpt status "did:plc:xxxxx" ``` ### 今日の運勢 ```bash -ai-gpt fortune +aigpt fortune ``` ### 自律送信チェック ```bash # ドライラン(確認のみ) -ai-gpt transmit +aigpt transmit # 実行 -ai-gpt transmit --execute +aigpt transmit --execute ``` ### 日次メンテナンス ```bash -ai-gpt maintenance +aigpt maintenance ``` ### 関係一覧 ```bash -ai-gpt relationships +aigpt relationships ``` ## データ構造 -デフォルトでは `~/.ai_gpt/` に以下のファイルが保存されます: +デフォルトでは `~/.config/syui/ai/gpt/` に以下のファイルが保存されます: - `memories.json` - 会話記憶 - `conversations.json` - 会話ログ @@ -98,22 +98,22 @@ ai-gpt relationships ### サーバー起動 ```bash # Ollamaを使用(デフォルト) -ai-gpt server --model qwen2.5 --provider ollama +aigpt server --model qwen2.5 --provider ollama # 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プロバイダーを使った会話 ```bash # Ollamaで会話 -ai-gpt chat "did:plc:xxxxx" "こんにちは" --provider ollama --model qwen2.5 +aigpt chat "did:plc:xxxxx" "こんにちは" --provider ollama --model qwen2.5 # 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 @@ -145,42 +145,42 @@ cp .env.example .env ```bash # 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分ごとに送信チェック(インターバル形式) -ai-gpt schedule add transmission_check "30m" +aigpt schedule add transmission_check "30m" # 毎日午前3時にメンテナンス -ai-gpt schedule add maintenance "0 3 * * *" +aigpt schedule add maintenance "0 3 * * *" # 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 # タスク一覧 -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 # バックグラウンドでスケジューラーを実行 -ai-gpt schedule run +aigpt schedule run ``` ### スケジュール形式 diff --git a/claude.md b/claude.md index 67bedc0..2dd793d 100644 --- a/claude.md +++ b/claude.md @@ -1,4 +1,4 @@ -# syuiエコシステム統合設計書 +# エコシステム統合設計書 ## 中核思想 - **存在子理論**: この世界で最も小さいもの(存在子/ai)の探求 @@ -26,6 +26,53 @@ └── 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 @@ -265,7 +312,7 @@ ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界) - 統合人格システム(Persona) - スケジューラー(5種類のタスク) - MCP Server(9種類のツール) -- 設定管理(~/.config/aigpt/) +- 設定管理(~/.config/syui/ai/gpt/) - 全CLIコマンド実装 ### 次の開発ポイント @@ -273,3 +320,7 @@ ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界) - 自律送信: transmission.pyでatproto実装 - ai.bot連携: 新規bot_connector.py作成 - テスト: tests/ディレクトリ追加 + +# footer + +© syui diff --git a/docs/configuration.md b/docs/configuration.md index e7d3f13..ed9b2d8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 # 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 ```bash # ホストを変更(リモート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設定(将来の自動投稿用) ```bash # Blueskyアカウント -ai-gpt config set atproto.handle yourhandle.bsky.social -ai-gpt config set atproto.password your-app-password +aigpt config set atproto.handle yourhandle.bsky.social +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 # デフォルトを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 -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 # バックアップ -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 # 現在の設定を確認 -ai-gpt config list +aigpt config list # 特定のキーを確認 -ai-gpt config get providers.openai.api_key +aigpt config get providers.openai.api_key ``` ### 設定をリセット ```bash # 設定ファイルを削除(次回実行時に再作成) -rm ~/.config/aigpt/config.json +rm ~/.config/syui/ai/gpt/config.json ``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8938f8d..56611de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "ai-gpt" +name = "aigpt" version = "0.1.0" description = "Autonomous transmission AI with unique personality based on relationship parameters" requires-python = ">=3.10" @@ -19,7 +19,7 @@ dependencies = [ ] [project.scripts] -ai-gpt = "ai_gpt.cli:app" +aigpt = "aigpt.cli:app" [build-system] requires = ["setuptools>=61.0", "wheel"] @@ -29,4 +29,4 @@ build-backend = "setuptools.build_meta" where = ["src"] [tool.setuptools.package-data] -ai_gpt = ["data/*.json"] \ No newline at end of file +aigpt = ["data/*.json"] \ No newline at end of file diff --git a/rust/mcp/config.py b/rust/mcp/config.py index ca05728..f0178d0 100644 --- a/rust/mcp/config.py +++ b/rust/mcp/config.py @@ -3,7 +3,7 @@ import os from pathlib import Path # ディレクトリ設定 -BASE_DIR = Path.home() / ".config" / "aigpt" +BASE_DIR = Path.home() / ".config" / "syui" / "ai" / "gpt" MEMORY_DIR = BASE_DIR / "memory" SUMMARY_DIR = MEMORY_DIR / "summary" diff --git a/setup_venv.sh b/setup_venv.sh new file mode 100755 index 0000000..de4d636 --- /dev/null +++ b/setup_venv.sh @@ -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" \ No newline at end of file diff --git a/src/ai_gpt/__init__.py b/src/ai_gpt/__init__.py deleted file mode 100644 index c29231b..0000000 --- a/src/ai_gpt/__init__.py +++ /dev/null @@ -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", -] \ No newline at end of file diff --git a/src/ai_gpt/ai_provider.py b/src/ai_gpt/ai_provider.py deleted file mode 100644 index 59575cd..0000000 --- a/src/ai_gpt/ai_provider.py +++ /dev/null @@ -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}") \ No newline at end of file diff --git a/src/ai_gpt/cli.py b/src/ai_gpt/cli.py deleted file mode 100644 index a0d8570..0000000 --- a/src/ai_gpt/cli.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/src/ai_gpt/config.py b/src/ai_gpt/config.py deleted file mode 100644 index 3fedd01..0000000 --- a/src/ai_gpt/config.py +++ /dev/null @@ -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}", {}) \ No newline at end of file diff --git a/src/ai_gpt/fortune.py b/src/ai_gpt/fortune.py deleted file mode 100644 index 0bb1e40..0000000 --- a/src/ai_gpt/fortune.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/ai_gpt/mcp_server.py b/src/ai_gpt/mcp_server.py deleted file mode 100644 index 7b999fa..0000000 --- a/src/ai_gpt/mcp_server.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/ai_gpt/memory.py b/src/ai_gpt/memory.py deleted file mode 100644 index c5f5e3e..0000000 --- a/src/ai_gpt/memory.py +++ /dev/null @@ -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] \ No newline at end of file diff --git a/src/ai_gpt/models.py b/src/ai_gpt/models.py deleted file mode 100644 index 7cf666b..0000000 --- a/src/ai_gpt/models.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/ai_gpt/persona.py b/src/ai_gpt/persona.py deleted file mode 100644 index 88f0561..0000000 --- a/src/ai_gpt/persona.py +++ /dev/null @@ -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") \ No newline at end of file diff --git a/src/ai_gpt/relationship.py b/src/ai_gpt/relationship.py deleted file mode 100644 index 31dac43..0000000 --- a/src/ai_gpt/relationship.py +++ /dev/null @@ -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 - } \ No newline at end of file diff --git a/src/ai_gpt/scheduler.py b/src/ai_gpt/scheduler.py deleted file mode 100644 index df26cf4..0000000 --- a/src/ai_gpt/scheduler.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/ai_gpt/transmission.py b/src/ai_gpt/transmission.py deleted file mode 100644 index 6eba250..0000000 --- a/src/ai_gpt/transmission.py +++ /dev/null @@ -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 - } \ No newline at end of file