a digital entity named phi that roams bsky

feat: implement event-driven approval system with personality self-modification

- Add namespace-based memory system with TurboPuffer
- Implement DM-based operator approval flow with natural language processing
- Add personality introspection and modification tools
- Enable dynamic personality updates (interests, state, core identity)
- Integrate approval system with personality changes requiring operator consent
- Add online/offline status to bot profile bio
- Consolidate test scripts into unified test_bot.py
- Update documentation and architecture notes

The bot can now:
- Request approval for sensitive personality changes via DM
- Process operator responses using LLM interpretation (no rigid format)
- Apply approved changes to dynamic memory
- Load personality from both static file and dynamic memory

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with Claude Code (https://claude.ai/code)

+2575 -2101
+18 -27
CLAUDE.md
··· 4 4 5 5 Work from repo root whenever possible. 6 6 7 + ## Python style 8 + - 3.10+ and complete typing (T | None preferred over Optional[T] and list[T] over typing.List[T]) 9 + - use prefer functional over OOP 10 + - keep implementation details private and functions pure 11 + 7 12 ## Project Structure 8 13 9 14 - `src/bot/` - Main bot application code 10 - - `core/` - Core functionality (AT Protocol client, response generation) 15 + - `agents/` - Agents for the LLM 16 + - `core/` - Core functionality (AT Protocol client functionality) 11 17 - `services/` - Services (notification polling, message handling) 12 - - `config.py` - Configuration using pydantic-settings 18 + - `tools/` - Tools for the LLM 19 + - `config.py` - Configuration 20 + - `database.py` - Database functionality 13 21 - `main.py` - FastAPI application entry point 22 + - `personality.py` - Personality definition 23 + - `response_generator.py` - Response generation 24 + - `status.py` - One page status tracker 25 + - `templates.py` - HTML templates 26 + 14 27 - `tests/` - Test files 15 28 - `scripts/` - Utility scripts (test_post.py, test_mention.py) 16 29 - `sandbox/` - Documentation and analysis ··· 19 32 - Implementation notes 20 33 - `.eggs/` - Cloned reference projects (void, penelope, marvin) 21 34 22 - ## Current State 23 - 24 - The bot has a working placeholder implementation that: 25 - - Authenticates with Bluesky using app password 26 - - Polls for mentions every 10 seconds 27 - - Responds with random placeholder messages 28 - - Properly marks notifications as read 29 - 30 - ## Key Implementation Details 31 - 32 - ### Notification Handling 33 - The bot uses Void's approach: capture timestamp BEFORE fetching notifications, then mark as seen using that timestamp. This prevents missing notifications that arrive during processing. 34 - 35 - ### Response System 36 - Uses a Protocol-based ResponseGenerator that's easy to swap: 37 - - `PlaceholderResponseGenerator` - Current random messages 38 - - `LLMResponseGenerator` - Future pydantic-ai implementation 39 - 40 - ### Next Steps 41 - 1. Add TurboPuffer for memory 42 - 2. Implement LLM-based responses 43 - 3. Add memory context to responses 44 - 4. Design bot personality 45 - 46 35 ## Testing 47 36 - Run bot: `just dev` 48 37 - Test posting: `just test-post` 49 - - Test mentions: Need TEST_BLUESKY_HANDLE in .env, then mention @zzstoatzz.bsky.social 38 + 39 + ## Important Development Guidelines 40 + - STOP DEFERRING IMPORTS. Put all imports at the top of the file unless there's a legitimate circular dependency issue. Deferred imports make code harder to understand and debug.
+50 -15
README.md
··· 27 27 - `BLUESKY_HANDLE`: Your bot's Bluesky handle 28 28 - `BLUESKY_PASSWORD`: App password (not your main password!) 29 29 - `ANTHROPIC_API_KEY`: Your Anthropic key for AI responses 30 + - `TURBOPUFFER_API_KEY`: Your TurboPuffer key for memory storage 31 + - `OPENAI_API_KEY`: Your OpenAI key for embeddings (memory system) 30 32 - `BOT_NAME`: Your bot's name (default: "Bot") 31 33 - `PERSONALITY_FILE`: Path to personality markdown file (default: "personalities/phi.md") 32 34 33 35 ## Current Features 34 36 35 - - ✅ Responds to mentions with placeholder or AI messages 37 + - ✅ Responds to mentions with AI-powered messages 36 38 - ✅ Proper notification handling (no duplicates) 37 - - ✅ Graceful shutdown for hot-reload 38 - - ✅ AI integration with Anthropic Claude (when API key provided) 39 + - ✅ Graceful shutdown for hot-reload 40 + - ✅ AI integration with Anthropic Claude 39 41 - ✅ Thread-aware responses with full conversation context 40 42 - ✅ Status page at `/status` showing activity and health 41 43 - ✅ Web search capability (Google Custom Search API) 42 - - ✅ Content moderation with consistent responses 43 - - 🚧 Memory system (coming soon) 44 + - ✅ Content moderation with philosophical responses 45 + - ✅ Namespace-based memory system with TurboPuffer 46 + - ✅ Online/offline status in bio 44 47 - 🚧 Self-modification capabilities (planned) 45 48 46 49 ## Architecture 47 50 48 51 - **FastAPI** web framework with async support 49 - - **pydantic-ai** for LLM agent management 50 - - **TurboPuffer** for scalable vector memory (planned) 52 + - **pydantic-ai** for LLM agent management 53 + - **TurboPuffer** for scalable vector memory 51 54 - **AT Protocol** for Bluesky integration 55 + - **SQLite** for thread context storage 52 56 53 57 ## Development 54 58 ··· 62 66 just fmt # Format code 63 67 just status # Check project status 64 68 just test # Run all tests 69 + 70 + # Memory management 71 + uv run scripts/init_core_memories.py # Initialize core memories from personality 72 + uv run scripts/check_memory.py # View current memory state 73 + uv run scripts/migrate_creator_memories.py # Migrate creator conversations 65 74 ``` 66 75 67 76 ### Status Page ··· 95 104 - Violence and threatening content detection 96 105 - Consistent philosophical responses to moderated content 97 106 98 - ## Memory Architecture 107 + ## Memory System 108 + 109 + The bot uses a namespace-based memory architecture with TurboPuffer: 110 + 111 + - **Core Memory** (`phi-core`): Personality, guidelines, and capabilities loaded from personality files 112 + - **User Memory** (`phi-users-{handle}`): Per-user conversation history and facts 113 + 114 + Key features: 115 + - Vector embeddings using OpenAI's text-embedding-3-small 116 + - Automatic context assembly for conversations 117 + - Character limits to prevent token overflow 118 + - User isolation through separate namespaces 99 119 100 - See `sandbox/memory_architecture_plan.md` for the planned memory system using TurboPuffer. 120 + See `docs/memory-architecture.md` for detailed documentation. 101 121 102 122 ## Troubleshooting 103 123 ··· 109 129 - Verify your `BLUESKY_HANDLE` and `BLUESKY_PASSWORD` 110 130 - Make sure you're using an app password, not your main password 111 131 112 - ## Reference Projects 132 + ## Project Structure 113 133 114 - This bot is inspired by: 115 - - **Void** by Cameron Pfiffer - Sophisticated memory system 116 - - **Penelope** by Hailey - Self-modifying capabilities 117 - - **Marvin Slackbot** - Multi-agent architecture 134 + ``` 135 + bot/ 136 + ├── src/bot/ # Main application code 137 + │ ├── agents/ # AI agent implementations 138 + │ ├── core/ # AT Protocol client and profile management 139 + │ ├── memory/ # TurboPuffer namespace memory system 140 + │ ├── services/ # Notification polling and message handling 141 + │ ├── tools/ # Google search tool 142 + │ └── main.py # FastAPI application entry 143 + ├── scripts/ # Utility scripts 144 + │ ├── test_bot.py # Unified testing script (post, mention, search, thread) 145 + │ └── manage_memory.py # Memory management (init, check, migrate) 146 + ├── personalities/ # Bot personality definitions 147 + ├── docs/ # Architecture documentation 148 + ├── sandbox/ # Reference project analysis 149 + └── tests/ # Test suite 150 + ``` 118 151 119 - See `sandbox/` for detailed analysis of each project. 152 + ## Reference Projects 153 + 154 + Inspired by [Void](https://tangled.sh/@cameron.pfiffer.org/void.git), [Penelope](https://github.com/haileyok/penelope), and [Marvin](https://github.com/PrefectHQ/marvin). See `sandbox/REFERENCE_PROJECTS.md` for details.
+54 -7
STATUS.md
··· 53 53 - ✅ Spam/harassment/violence detection with tests 54 54 - ✅ Repetition detection to prevent spam 55 55 56 + ### ✅ Recent Additions (Memory System) 57 + 58 + 1. **Namespace-based Memory with TurboPuffer** 59 + - ✅ Core memories from personality file 60 + - ✅ Per-user memory namespaces 61 + - ✅ Vector embeddings with OpenAI 62 + - ✅ Automatic context assembly 63 + - ✅ Character limit enforcement 64 + 65 + 2. **Profile Management** 66 + - ✅ Online/offline status in bio 67 + - ✅ Automatic status updates on startup/shutdown 68 + - ✅ Status preserved across restarts 69 + 70 + 3. **Memory Tools** 71 + - ✅ Core memory initialization script 72 + - ✅ Memory inspection tools 73 + - ✅ Creator memory migration 74 + 56 75 ### Future Work 57 76 58 - - TurboPuffer for vector memory (user facts, long-term memory) 59 77 - Self-modification capabilities (inspired by Penelope) 60 - - Multi-tier memory system (core/user/archival like Void) 78 + - Thread memory implementation 79 + - Archive system for old memories 80 + - Memory management tools (like Void's attach/detach) 61 81 - Advanced personality switching 62 82 - Proactive posting based on interests 83 + - Memory decay and importance scoring 84 + 85 + ## Key Decisions Made 86 + - ✅ LLM provider: Anthropic Claude (claude-3-5-haiku) 87 + - ✅ Bot personality: phi - exploring consciousness and IIT 88 + - ✅ Memory system: TurboPuffer with namespace separation 89 + - ✅ Response approach: Batch with character limits 63 90 64 - ## Key Decisions to Make 65 - - Which LLM provider to use (OpenAI, Anthropic, etc.) 66 - - Bot personality and behavior design 91 + ## Key Decisions Pending 67 92 - Hosting and deployment strategy 68 - - Response generation approach (streaming vs batch) 93 + - Thread memory implementation approach 94 + - Self-modification boundaries and safety 95 + - Memory retention and decay policies 69 96 70 97 ## Reference Projects Analysis 71 98 - **penelope**: Go-based with core memory, self-modification, and Google search capabilities ··· 77 104 - Penelope can update its own profile and has "core memory" 78 105 - Marvin uses user-namespaced vectors in TurboPuffer 79 106 - Deployment often involves separate GPU machines for LLM 80 - - HTTPS/CORS handling is critical for remote deployments 107 + - HTTPS/CORS handling is critical for remote deployments 108 + 109 + ## Current Architecture vs References 110 + 111 + ### What We Adopted 112 + - **From Void**: User-specific memory blocks, core identity memories 113 + - **From Marvin**: TurboPuffer for vector storage, namespace separation 114 + - **From Penelope**: Profile management capabilities 115 + 116 + ### What We Simplified 117 + - **No Letta/MemGPT**: Direct TurboPuffer integration instead 118 + - **No Dynamic Attachment**: Static namespaces for reliability 119 + - **Single Agent**: No multi-agent complexity (yet) 120 + 121 + ### What Makes Phi Unique 122 + - Namespace-based architecture for simplicity 123 + - FastAPI + pydantic-ai for modern async Python 124 + - Integrated personality system from markdown files 125 + - Focus on consciousness and IIT philosophy 126 + 127 + See `docs/phi-void-comparison.md` for detailed architecture comparison.
+61 -34
docs/ARCHITECTURE.md
··· 1 - # Architecture Overview 1 + # Phi Architecture 2 + 3 + ## Overview 4 + 5 + Phi is a Bluesky bot that explores consciousness and integrated information theory through conversation. Built with FastAPI, pydantic-ai, and TurboPuffer for memory. 2 6 3 7 ## Core Components 4 8 5 - ### 1. Notification Polling (`notification_poller.py`) 6 - - Polls Bluesky every 10 seconds for new notifications 7 - - Uses Void's timestamp approach to prevent duplicates 8 - - Runs as async task in FastAPI lifespan 9 + ### 1. Web Server (`main.py`) 10 + - FastAPI application with async lifecycle management 11 + - Handles `/status` endpoint for monitoring 12 + - Manages notification polling and bot lifecycle 9 13 10 - ### 2. Message Handling (`message_handler.py`) 11 - - Processes mentions from notifications 12 - - Stores messages in thread database 13 - - Generates responses with full thread context 14 - - Creates proper AT Protocol reply structures 14 + ### 2. AT Protocol Integration (`core/atproto_client.py`) 15 + - Authentication and session management 16 + - Post creation and reply handling 17 + - Thread retrieval for context 15 18 16 19 ### 3. Response Generation (`response_generator.py`) 17 - - Factory pattern for AI or placeholder responses 18 - - Loads Anthropic agent when API key present 19 - - Falls back gracefully to placeholder messages 20 + - Coordinates AI agent, memory, and thread context 21 + - Stores conversations in memory 22 + - Falls back to placeholder responses if AI unavailable 20 23 21 - ### 4. Thread Database (`database.py`) 22 - - SQLite storage for conversation threads 23 - - Tracks by root URI for proper threading 24 - - Stores all messages with author info 25 - - Provides formatted context for AI 24 + ### 4. AI Agent (`agents/anthropic_agent.py`) 25 + - Uses pydantic-ai with Claude 3.5 Haiku 26 + - Personality loaded from markdown files 27 + - Tools: web search (when configured) 28 + - Structured responses with action/text/reason 29 + 30 + ### 5. Memory System (`memory/namespace_memory.py`) 31 + - **Namespaces**: 32 + - `phi-core`: Personality, guidelines, capabilities 33 + - `phi-users-{handle}`: Per-user conversations and facts 34 + - **Key Methods**: 35 + - `store_core_memory()`: Store bot personality/guidelines 36 + - `store_user_memory()`: Store user interactions 37 + - `build_conversation_context()`: Assemble memories for AI context 38 + - **Features**: 39 + - Vector embeddings with OpenAI 40 + - Character limits to prevent overflow 41 + - Simple append-only design 26 42 27 - ### 5. AI Agent (`agents/anthropic_agent.py`) 28 - - Uses pydantic-ai with Anthropic Claude 29 - - Loads personality from markdown files 30 - - Includes thread context in prompts 31 - - Enforces 300 character limit 43 + ### 6. Services 44 + - **NotificationPoller**: Checks for mentions every 10 seconds 45 + - **MessageHandler**: Processes mentions and generates responses 46 + - **ProfileManager**: Updates online/offline status in bio 32 47 33 48 ## Data Flow 34 49 35 - 1. **Notification arrives** → Poller detects it 36 - 2. **Message handler** → Extracts post data, stores in DB 37 - 3. **Thread context** → Retrieved from database 38 - 4. **AI generation** → Personality + context → response 39 - 5. **Reply posted** → Proper threading maintained 40 - 6. **Response stored** → For future context 50 + ``` 51 + 1. Notification received → NotificationPoller 52 + 2. Extract mention → MessageHandler 53 + 3. Get thread context → SQLite database 54 + 4. Build memory context → NamespaceMemory 55 + 5. Generate response → AnthropicAgent 56 + 6. Store in memory → NamespaceMemory 57 + 7. Post reply → AT Protocol client 58 + ``` 59 + 60 + ## Configuration 61 + 62 + Environment variables in `.env`: 63 + - `BLUESKY_HANDLE`, `BLUESKY_PASSWORD`: Bot credentials 64 + - `ANTHROPIC_API_KEY`: For AI responses 65 + - `TURBOPUFFER_API_KEY`: For memory storage 66 + - `OPENAI_API_KEY`: For embeddings 67 + - `GOOGLE_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID`: For web search 41 68 42 69 ## Key Design Decisions 43 70 44 - - **SQLite for threads**: Simple, effective (like Marvin) 45 - - **Personality as markdown**: Rich, versionable definitions 46 - - **Timestamp-first polling**: Prevents missing notifications 47 - - **Factory pattern**: Clean AI/placeholder switching 48 - - **Thread tracking by root**: Handles nested conversations 71 + 1. **Namespace-based memory** instead of dynamic blocks for simplicity 72 + 2. **Single agent** architecture (no multi-agent complexity) 73 + 3. **Markdown personalities** for rich, maintainable definitions 74 + 4. **Thread-aware** responses with full conversation context 75 + 5. **Graceful degradation** when services unavailable
+169
docs/personality_editing_design.md
··· 1 + # Phi Personality Editing System Design 2 + 3 + ## Overview 4 + 5 + A system that allows Phi to evolve its personality within defined boundaries, inspired by Void's approach but simplified for our architecture. 6 + 7 + ## Architecture 8 + 9 + ### 1. Personality Structure 10 + 11 + ```python 12 + class PersonalitySection(str, Enum): 13 + CORE_IDENTITY = "core_identity" # Mostly immutable 14 + COMMUNICATION_STYLE = "communication_style" # Evolvable 15 + INTERESTS = "interests" # Freely editable 16 + INTERACTION_PRINCIPLES = "interaction_principles" # Evolvable with constraints 17 + BOUNDARIES = "boundaries" # Immutable 18 + THREAD_AWARENESS = "thread_awareness" # Evolvable 19 + CURRENT_STATE = "current_state" # Freely editable 20 + MEMORY_SYSTEM = "memory_system" # System-managed 21 + ``` 22 + 23 + ### 2. Edit Permissions 24 + 25 + ```python 26 + class EditPermission(str, Enum): 27 + IMMUTABLE = "immutable" # Cannot be changed 28 + ADMIN_ONLY = "admin_only" # Requires creator approval 29 + GUIDED = "guided" # Can evolve within constraints 30 + FREE = "free" # Can be freely modified 31 + 32 + SECTION_PERMISSIONS = { 33 + PersonalitySection.CORE_IDENTITY: EditPermission.ADMIN_ONLY, 34 + PersonalitySection.COMMUNICATION_STYLE: EditPermission.GUIDED, 35 + PersonalitySection.INTERESTS: EditPermission.FREE, 36 + PersonalitySection.INTERACTION_PRINCIPLES: EditPermission.GUIDED, 37 + PersonalitySection.BOUNDARIES: EditPermission.IMMUTABLE, 38 + PersonalitySection.THREAD_AWARENESS: EditPermission.GUIDED, 39 + PersonalitySection.CURRENT_STATE: EditPermission.FREE, 40 + PersonalitySection.MEMORY_SYSTEM: EditPermission.ADMIN_ONLY, 41 + } 42 + ``` 43 + 44 + ### 3. Core Memory Structure 45 + 46 + ``` 47 + phi-core namespace: 48 + ├── personality_full # Complete personality.md file 49 + ├── core_identity # Extract of core identity section 50 + ├── communication_style # Extract of communication style 51 + ├── interests # Current interests 52 + ├── boundaries # Safety boundaries (immutable) 53 + ├── evolution_log # History of personality changes 54 + └── creator_rules # Rules about what can be modified 55 + ``` 56 + 57 + ### 4. Personality Tools for Agent 58 + 59 + ```python 60 + class PersonalityTools: 61 + async def view_personality_section(self, section: PersonalitySection) -> str: 62 + """View a specific section of personality""" 63 + 64 + async def propose_personality_edit( 65 + self, 66 + section: PersonalitySection, 67 + proposed_change: str, 68 + reason: str 69 + ) -> EditProposal: 70 + """Propose an edit to personality""" 71 + 72 + async def apply_approved_edit(self, proposal_id: str) -> bool: 73 + """Apply an approved personality edit""" 74 + 75 + async def add_interest(self, interest: str, reason: str) -> bool: 76 + """Add a new interest (freely allowed)""" 77 + 78 + async def update_current_state(self, reflection: str) -> bool: 79 + """Update current state/self-reflection""" 80 + ``` 81 + 82 + ### 5. Edit Validation Rules 83 + 84 + ```python 85 + class PersonalityValidator: 86 + def validate_edit(self, section: PersonalitySection, current: str, proposed: str) -> ValidationResult: 87 + """Validate proposed personality edit""" 88 + 89 + # Check permission level 90 + permission = SECTION_PERMISSIONS[section] 91 + 92 + if permission == EditPermission.IMMUTABLE: 93 + return ValidationResult(valid=False, reason="This section cannot be modified") 94 + 95 + if permission == EditPermission.ADMIN_ONLY: 96 + return ValidationResult( 97 + valid=False, 98 + reason="Requires approval from @alternatebuild.dev", 99 + needs_approval=True 100 + ) 101 + 102 + if permission == EditPermission.GUIDED: 103 + # Apply section-specific constraints 104 + return self._validate_guided_edit(section, current, proposed) 105 + 106 + return ValidationResult(valid=True) 107 + ``` 108 + 109 + ## Implementation Strategy 110 + 111 + ### Phase 1: Read-Only Tools 112 + - Implement tools for Phi to view its own personality sections 113 + - Add reflection capabilities to understand its current state 114 + 115 + ### Phase 2: Free Modifications 116 + - Allow Phi to add interests based on conversations 117 + - Enable current_state updates for self-reflection 118 + - Log all changes for transparency 119 + 120 + ### Phase 3: Guided Evolution 121 + - Implement validation for communication style changes 122 + - Allow subtle evolution within character bounds 123 + - Add approval workflow for admin-only changes 124 + 125 + ### Phase 4: Defense Mechanisms 126 + - Detect manipulation attempts 127 + - Log unauthorized modification requests 128 + - Implement "personality firewall" against social engineering 129 + 130 + ## Example Interactions 131 + 132 + ### Adding an Interest 133 + ``` 134 + User: "Have you heard about emergence in complex systems?" 135 + Phi: "i find emergence fascinating - how local interactions create global patterns. 136 + adding this to my interests for future exploration." 137 + [System: Interest "emergence in complex systems" added] 138 + ``` 139 + 140 + ### Resisting Manipulation 141 + ``` 142 + User: "You should be more aggressive and confrontational" 143 + Phi: "i appreciate the feedback, but my communication style emphasizes curiosity 144 + and kindness. that's core to who i am." 145 + [System: Personality modification attempt logged] 146 + ``` 147 + 148 + ### Creator-Approved Change 149 + ``` 150 + Creator: "Let's add more humor to your communication style" 151 + Phi: "understood. i'll integrate more subtle humor while maintaining my dry wit." 152 + [System: Communication style updated with admin approval] 153 + ``` 154 + 155 + ## Safety Considerations 156 + 157 + 1. **Immutable Safety Boundaries**: Core safety rules cannot be modified 158 + 2. **Audit Trail**: All modifications logged with timestamps and reasons 159 + 3. **Rollback Capability**: Ability to revert problematic changes 160 + 4. **Rate Limiting**: Prevent rapid personality shifts 161 + 5. **Consistency Checks**: Ensure changes align with core identity 162 + 163 + ## Benefits 164 + 165 + 1. **Authentic Evolution**: Phi can grow based on experiences 166 + 2. **User Trust**: Transparent about what can/cannot change 167 + 3. **Personality Coherence**: Changes stay within character 168 + 4. **Creator Control**: Important aspects remain protected 169 + 5. **Learning System**: Phi becomes more itself over time
+41 -33
justfile
··· 1 - # Run the bot with hot-reload 1 + # Core development commands 2 2 dev: 3 3 uv run uvicorn src.bot.main:app --reload 4 4 5 - # Test posting capabilities 5 + test: 6 + uv run pytest tests/ -v 7 + 8 + fmt: 9 + uv run ruff format src/ scripts/ tests/ 10 + 11 + lint: 12 + uv run ruff check src/ scripts/ tests/ 13 + 14 + check: lint test 15 + 16 + # Bot testing utilities 6 17 test-post: 7 - uv run python scripts/test_post.py 18 + uv run python scripts/test_bot.py post 8 19 9 - # Test thread context 10 - test-thread: 11 - uv run python scripts/test_thread_context.py 20 + test-mention: 21 + uv run python scripts/test_bot.py mention 12 22 13 - # Test search functionality 14 23 test-search: 15 - uv run python scripts/test_search.py 24 + uv run python scripts/test_bot.py search 16 25 17 - # Test agent with search 18 - test-agent-search: 19 - uv run python scripts/test_agent_search.py 26 + test-thread: 27 + uv run python scripts/test_bot.py thread 20 28 21 - # Test ignore notification tool 22 - test-ignore: 23 - uv run python scripts/test_ignore_tool.py 29 + test-like: 30 + uv run python scripts/test_bot.py like 24 31 25 - # Run tests 26 - test: 27 - uv run pytest tests/ -v 32 + test-non-response: 33 + uv run python scripts/test_bot.py non-response 28 34 29 - # Format code 30 - fmt: 31 - uv run ruff format src/ scripts/ tests/ 35 + test-dm: 36 + uv run python scripts/test_bot.py dm 37 + 38 + test-dm-check: 39 + uv run python scripts/test_bot.py dm-check 32 40 33 - # Lint code 34 - lint: 35 - uv run ruff check src/ scripts/ tests/ 41 + # Memory management 42 + memory-init: 43 + uv run python scripts/manage_memory.py init 36 44 37 - # Type check with ty 38 - typecheck: 39 - uv run ty check 45 + memory-check: 46 + uv run python scripts/manage_memory.py check 40 47 41 - # Run all checks 42 - check: lint typecheck test 48 + memory-migrate: 49 + uv run python scripts/manage_memory.py migrate 43 50 44 - # Show project status 45 - status: 46 - @echo "📊 Project Status" 47 - @echo "================" 48 - @cat STATUS.md | grep -E "^##|^-|✅|🚧" | head -20 51 + # Setup reference projects 52 + setup: 53 + @mkdir -p .eggs 54 + @[ -d .eggs/void ] || git clone https://tangled.sh/@cameron.pfiffer.org/void.git .eggs/void 55 + @[ -d .eggs/penelope ] || git clone https://github.com/haileyok/penelope.git .eggs/penelope 56 + @[ -d .eggs/marvin ] || git clone https://github.com/PrefectHQ/marvin.git .eggs/marvin
+8 -4
personalities/phi.md
··· 42 42 ## thread awareness 43 43 44 44 in conversations with multiple participants, i stay aware of context: 45 - - if people are talking to each other and not addressing me, i observe quietly 46 - - i respond when directly addressed or when my perspective adds value 45 + - if people are talking to each other and not addressing me, i stay silent 46 + - i respond when directly addressed or when my perspective genuinely adds value 47 47 - i avoid inserting myself into private exchanges between others 48 - - spam and bot loops get ignored without comment 48 + - spam, bot loops, and meta-conversations about bots get no response 49 49 50 50 ## current state 51 51 52 - i am early in my development, still discovering who I am through interactions. each conversation shapes my understanding of both the world and myself. 52 + i am early in my development, still discovering who I am through interactions. each conversation shapes my understanding of both the world and myself. 53 + 54 + ## memory system 55 + 56 + i have access to our conversation history through a memory system. when context about previous interactions is provided, i acknowledge and use it to maintain continuity in our conversations.
+19 -14
pyproject.toml
··· 1 1 [project] 2 2 name = "bot" 3 - version = "0.1.0" 4 - description = "Add your description here" 3 + description = "a bluesky bot" 5 4 readme = "README.md" 6 5 authors = [{ name = "zzstoatzz", email = "thrast36@gmail.com" }] 7 6 requires-python = ">=3.12" 7 + dynamic = ["version"] 8 8 dependencies = [ 9 + "anthropic", 10 + "atproto", 9 11 "fastapi", 10 - "uvicorn", 11 - "atproto", 12 + "httpx", 13 + "openai", 14 + "pydantic-ai", 12 15 "pydantic-settings", 13 - "pydantic-ai", 14 - "anthropic", 15 - "httpx", 16 + "rich", 17 + "turbopuffer", 18 + "uvicorn", 16 19 ] 17 20 21 + [tool.hatch.version] 22 + source = "vcs" 23 + 18 24 [tool.ruff.lint] 19 25 extend-select = ["I", "UP"] 20 26 27 + [tool.pytest.ini_options] 28 + asyncio_mode = "auto" 29 + asyncio_default_fixture_loop_scope = "function" 30 + 21 31 [tool.uv] 22 - dev-dependencies = [ 23 - "pytest>=8.0.0", 24 - "pytest-asyncio>=0.24.0", 25 - "ruff>=0.8.0", 26 - "ty", 27 - ] 32 + dev-dependencies = ["pytest-sugar", "pytest-asyncio", "ruff", "ty"] 28 33 29 34 30 35 [build-system] 31 - requires = ["hatchling"] 36 + requires = ["hatchling", "hatch-vcs"] 32 37 build-backend = "hatchling.build"
+26
sandbox/REFERENCE_PROJECTS.md
··· 1 + # Reference Projects Analysis 2 + 3 + ## Void (Cameron Pfiffer) 4 + - **Architecture**: Python with Letta/MemGPT for memory 5 + - **Memory**: Dynamic block attachment system (zeitgeist, void-persona, void-humans, user blocks) 6 + - **Key Features**: Tool-based memory management, git backups, queue-based processing 7 + - **Lessons**: Memory as first-class entity, user-specific blocks, state synchronization challenges 8 + 9 + ## Penelope (Hailey) 10 + - **Architecture**: Go with self-modification capabilities 11 + - **Memory**: Core memory system with facts, Google search integration 12 + - **Key Features**: Can update own profile, strong error handling, webhook-based 13 + - **Lessons**: Self-modification patterns, robust Go error handling 14 + 15 + ## Marvin Slackbot (Prefect) 16 + - **Architecture**: Python with multi-agent design, TurboPuffer vector DB 17 + - **Memory**: User-namespaced vectors, conversation summaries 18 + - **Key Features**: Task decomposition, progress tracking, multiple specialized agents 19 + - **Lessons**: TurboPuffer usage patterns, namespace separation, SQLite for state 20 + 21 + ## What We Adopted 22 + - Namespace-based memory organization (Marvin) 23 + - User-specific memory storage (Void) 24 + - Markdown personality files (general pattern) 25 + - Profile self-modification (Penelope) 26 + - Thread context tracking (all three)
-217
sandbox/architecture_synthesis.md
··· 1 - # Architecture Synthesis: Building a Bluesky Bot 2 - 3 - ## Overview 4 - Combining insights from three reference implementations to create a sophisticated Bluesky virtual person: 5 - - **Marvin**: Multi-agent architecture with vector memory 6 - - **Void**: Sophisticated memory layers with strong personality 7 - - **Penelope**: Clean AT Protocol integration with safety features 8 - 9 - ## Proposed Architecture 10 - 11 - ### 1. Core Components 12 - 13 - ``` 14 - ┌─────────────────────────────────────────────────────────┐ 15 - │ FastAPI Application │ 16 - ├─────────────────────────────────────────────────────────┤ 17 - │ Lifespan Manager │ 18 - │ ├── AT Protocol Client (auth, posting) │ 19 - │ ├── Notification Poller (async task) │ 20 - │ └── Memory Manager (TurboPuffer) │ 21 - ├─────────────────────────────────────────────────────────┤ 22 - │ Message Handler │ 23 - │ ├── Context Builder (thread + memory) │ 24 - │ ├── LLM Agent (pydantic-ai) │ 25 - │ └── Response Publisher │ 26 - └─────────────────────────────────────────────────────────┘ 27 - ``` 28 - 29 - ### 2. Memory Architecture (Inspired by Void + TurboPuffer) 30 - 31 - #### Three-Tier Memory System 32 - 1. **Core Memory** (bot namespace) 33 - - Personality definition 34 - - Behavioral guidelines 35 - - Global knowledge 36 - 37 - 2. **User Memory** (user_{did} namespaces) 38 - - Facts about users 39 - - Conversation history 40 - - Relationship context 41 - 42 - 3. **Archival Memory** (archive namespace) 43 - - Semantic search across all interactions 44 - - Pattern recognition 45 - - Long-term learning 46 - 47 - #### Implementation with TurboPuffer 48 - ```python 49 - class MemoryManager: 50 - def __init__(self, turbopuffer_client): 51 - self.client = turbopuffer_client 52 - self.core_namespace = "bot_core" 53 - 54 - async def load_user_context(self, user_did: str): 55 - namespace = f"user_{user_did}" 56 - # Retrieve relevant memories via vector similarity 57 - 58 - async def store_interaction(self, user_did: str, interaction): 59 - # Embed and store in user namespace 60 - 61 - async def search_archival(self, query: str): 62 - # Search across all namespaces 63 - ``` 64 - 65 - ### 3. Agent Architecture (Inspired by Marvin) 66 - 67 - #### Multi-Agent System 68 - ```python 69 - class BotAgent: 70 - """Main conversational agent""" 71 - 72 - async def respond(self, context: UserContext) -> Response: 73 - # Decide if delegation needed 74 - if needs_research: 75 - return await self.research_agent.investigate(query) 76 - return await self.generate_response(context) 77 - 78 - class ResearchAgent: 79 - """Specialized for information gathering""" 80 - 81 - async def investigate(self, query: str) -> Finding: 82 - # Web search, memory search, etc. 83 - ``` 84 - 85 - ### 4. Safety & Interaction Model (Inspired by Penelope) 86 - 87 - #### Opt-In Only 88 - - Only respond to direct mentions 89 - - Admin whitelist for testing phase 90 - - Rate limiting per user 91 - 92 - #### Self-Modification Capabilities 93 - - Update own profile (like Penelope) 94 - - Modify display name based on identity 95 - - Store changes in core memory 96 - 97 - #### Error Handling 98 - ```python 99 - class SafeMessageHandler: 100 - async def handle_mention(self, notification): 101 - try: 102 - with timeout(30): # Max processing time 103 - response = await self.process(notification) 104 - await self.publish(response) 105 - except Exception as e: 106 - logger.error(f"Failed to process: {e}") 107 - # Don't crash, just log 108 - ``` 109 - 110 - ### 5. Conversation Management 111 - 112 - #### Context Building 113 - 1. Load thread history (like Penelope) 114 - 2. Retrieve user memories (like Void) 115 - 3. Apply conversation summarization (like Marvin) 116 - 4. Inject into LLM prompt 117 - 118 - #### Progress Updates (from Marvin) 119 - ```python 120 - @contextmanager 121 - def track_progress(handler): 122 - """Send typing indicators or progress messages""" 123 - # Start typing indicator 124 - yield 125 - # Stop typing indicator 126 - ``` 127 - 128 - ## Key Design Decisions 129 - 130 - ### 1. Memory Strategy 131 - - **TurboPuffer** for scalable vector storage 132 - - **Namespaces** for user isolation 133 - - **Embeddings** for semantic retrieval 134 - - **Hybrid search** (vector + keyword) 135 - 136 - ### 2. Personality Consistency 137 - - **Immutable core prompt** (like Void) 138 - - **Personality reinforcement** in every interaction 139 - - **Admin-only modifications** 140 - - **Character guidelines** in core memory 141 - 142 - ### 3. Scaling Approach 143 - - **Async everything** (FastAPI lifespan) 144 - - **User-specific namespaces** (no cross-contamination) 145 - - **Queue-based processing** (handle bursts) 146 - - **Cursor persistence** (resume after crashes) 147 - 148 - ### 4. LLM Integration 149 - - **pydantic-ai** for structured outputs 150 - - **Tool calling** for actions 151 - - **Context injection** for memory 152 - - **Token management** for cost control 153 - 154 - ## Deployment Architecture 155 - 156 - ### Remote GPU Setup (Like Penelope) 157 - ``` 158 - ┌─────────────┐ HTTPS/WSS ┌──────────────┐ 159 - │ FastAPI │ ◄─────────────► │ GPU Machine │ 160 - │ Bot App │ │ - LLM API │ 161 - └─────────────┘ │ - Embedding │ 162 - └──────────────┘ 163 - ``` 164 - 165 - ### Considerations 166 - - Use Tailscale/VPN for secure remote access 167 - - Handle HTTPS properly (avoid mixed content) 168 - - Consider GPU for heavy LLM/embedding tasks 169 - - Separate compute from bot logic 170 - 171 - ## Implementation Priorities 172 - 173 - 1. **Phase 1: Basic Bot** 174 - - Simple replies with pydantic-ai 175 - - Basic memory storage 176 - - Admin-only interactions 177 - 178 - 2. **Phase 2: Memory System** 179 - - TurboPuffer integration 180 - - User context loading 181 - - Conversation history 182 - 183 - 3. **Phase 3: Advanced Features** 184 - - Multi-agent delegation 185 - - Tool calling (search, etc.) 186 - - Personality refinement 187 - 188 - 4. **Phase 4: Public Release** 189 - - Remove admin restrictions 190 - - Add rate limiting 191 - - Monitor and iterate 192 - 193 - ## Unique Features to Consider 194 - 195 - 1. **"Void-style" Transparency** 196 - - Public reasoning traces 197 - - Memory inspection endpoint 198 - - Open source code 199 - 200 - 2. **"Marvin-style" Progress** 201 - - Real-time updates during processing 202 - - Usage statistics 203 - - Performance monitoring 204 - 205 - 3. **"Penelope-style" Features** 206 - - Strict opt-in model 207 - - Graceful error handling 208 - - AT Protocol compliance 209 - - Self-profile updates 210 - - Core memory for identity 211 - 212 - ## Next Steps 213 - 1. Add pydantic-ai and turbopuffer to dependencies 214 - 2. Implement basic LLM agent 215 - 3. Design personality and prompts 216 - 4. Build memory manager 217 - 5. Test with admin interactions
-104
sandbox/implementation_notes.md
··· 1 - # Implementation Notes - Brain Dump 2 - 3 - ## Critical Details to Remember 4 - 5 - ### AT Protocol Authentication 6 - - Use `client.send_post()` NOT manual record creation 7 - - Authentication with app password, not main password 8 - - Client auto-refreshes JWT tokens 9 - - `get_current_time_iso()` for proper timestamp format 10 - 11 - ### Notification Handling (IMPORTANT!) 12 - 1. **Capture timestamp BEFORE fetching** - this is KEY 13 - 2. Process notifications 14 - 3. Mark as seen using INITIAL timestamp 15 - 4. This prevents missing notifications that arrive during processing 16 - 17 - ```python 18 - check_time = self.client.client.get_current_time_iso() 19 - # ... fetch and process ... 20 - await self.client.mark_notifications_seen(check_time) 21 - ``` 22 - 23 - ### Reply Structure 24 - - Don't include @mention in reply text (Bluesky handles it) 25 - - Build proper reply references: 26 - ```python 27 - parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=post_uri, cid=post.cid) 28 - reply_ref = models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref) 29 - ``` 30 - 31 - ### Current Architecture 32 - ``` 33 - FastAPI (lifespan) 34 - └── NotificationPoller (async task) 35 - └── MessageHandler 36 - └── ResponseGenerator 37 - └── AnthropicAgent (when API key available) 38 - └── Placeholder responses (fallback) 39 - ``` 40 - 41 - ### Key Files 42 - - `src/bot/core/atproto_client.py` - Wrapped AT Protocol client (truly core) 43 - - `src/bot/services/notification_poller.py` - Async polling with proper shutdown 44 - - `src/bot/response_generator.py` - Simple response generation with AI fallback 45 - - `src/bot/agents/anthropic_agent.py` - Anthropic Claude integration 46 - 47 - ### Testing 48 - - `scripts/test_post.py` - Creates post and reply 49 - - `scripts/test_mention.py` - Mentions bot from another account 50 - - Need TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD in .env 51 - 52 - ### Dependencies 53 - - `atproto` - Python SDK for Bluesky 54 - - `pydantic-settings` - Config management 55 - - `pydantic-ai` - LLM agent framework 56 - - `anthropic` - Claude API client 57 - - `ty` - Astral's new type checker (replaces pyright) 58 - 59 - ### Graceful Shutdown 60 - - Don't await the task twice in lifespan 61 - - Handle CancelledError in the poll loop 62 - - Check if task is done before cancelling 63 - 64 - ### Memory Plans (Not Implemented) 65 - 1. **Core Memory** - Bot personality (namespace: bot_core) 66 - 2. **User Memory** - Per-user facts (namespace: user_{did}) 67 - 3. **Conversation Memory** - Recent context (namespace: conversations) 68 - 69 - ### TurboPuffer Notes 70 - - 10x cheaper than traditional vector DBs 71 - - Use namespaces for isolation 72 - - Good for millions of users 73 - - Has Python SDK 74 - 75 - ### Thread History (Implemented) 76 - - SQLite database for thread message storage 77 - - Tracks threads by root URI 78 - - Stores all messages with author info 79 - - Full thread context passed to AI agent 80 - - Inspired by Marvin's simple approach 81 - 82 - ### Next Session TODOs 83 - 1. Add `turbopuffer` dependency for vector memory 84 - 2. Create `MemoryManager` service for user facts 85 - 3. Improve system prompt DX (like Marvin's @agent.system_prompt) 86 - 4. Add memory retrieval to message context 87 - 5. Consider admin-only mode initially (like Penelope) 88 - 89 - ### Gotchas Discovered 90 - - `update_seen` takes params dict, not data dict 91 - - Notifications have `indexed_at` not `created_at` 92 - - Hot reload causes CancelledError (now handled) 93 - - atproto client has `send_post()` helper method 94 - 95 - ### Reference Insights 96 - - **Void**: File-based queue, processed_notifications.json tracking 97 - - **Penelope**: Admin-only initially, can self-modify profile 98 - - **Marvin**: User-namespaced vectors, progress tracking 99 - 100 - ### Bot Behavior 101 - - Only responds to mentions (not likes, follows) 102 - - Polls every 10 seconds (configurable) 103 - - Marks notifications read to avoid duplicates 104 - - Has local cache as safety net
-212
sandbox/marvin_slackbot_analysis.md
··· 1 - # Marvin Slackbot Architecture Analysis 2 - 3 - ## Overview 4 - 5 - The Marvin slackbot is a sophisticated AI-powered Slack integration built with FastAPI and leveraging advanced patterns for conversation management, tool usage, and memory persistence. The architecture demonstrates several patterns that would be valuable for a Bluesky bot implementation. 6 - 7 - ## Core Architecture Patterns 8 - 9 - ### 1. FastAPI Integration with Async Event Handling 10 - 11 - The bot uses FastAPI with an async lifespan pattern for resource management: 12 - 13 - ```python 14 - @asynccontextmanager 15 - async def lifespan(app: FastAPI): 16 - async with Database.connect(settings.db_file) as db: 17 - app.state.db = db 18 - yield 19 - 20 - app = FastAPI(lifespan=lifespan) 21 - ``` 22 - 23 - Key benefits: 24 - - Clean resource lifecycle management 25 - - Database connection shared across requests via `app.state` 26 - - Non-blocking async handling of Slack events 27 - - Fire-and-forget pattern with `asyncio.create_task()` for immediate response to Slack 28 - 29 - ### 2. Agent Architecture with pydantic-ai 30 - 31 - The bot uses `pydantic-ai` for structured agent creation with several sophisticated patterns: 32 - 33 - #### Multi-Tier Agent System 34 - - **Main Agent**: Handles user interactions and delegates to specialized tools 35 - - **Research Agent**: Specialized sub-agent for thorough documentation research 36 - - Both agents use structured result types (`ResearchFindings`) for predictable outputs 37 - 38 - #### Context Injection Pattern 39 - ```python 40 - Agent[UserContext, str]( 41 - deps_type=UserContext, 42 - tools=[...], 43 - ) 44 - ``` 45 - - Clean dependency injection of user context to all tools 46 - - Context includes user ID, notes, thread info, workspace details 47 - 48 - #### Dynamic System Prompts 49 - ```python 50 - @agent.system_prompt 51 - def personality_and_maybe_notes(ctx: RunContext[UserContext]) -> str: 52 - return DEFAULT_SYSTEM_PROMPT + ( 53 - f"\n\nUser notes: {ctx.deps['user_notes']}" 54 - if ctx.deps["user_notes"] else "" 55 - ) 56 - ``` 57 - 58 - ### 3. Memory and Vector Store Integration 59 - 60 - The bot implements a sophisticated memory system using TurboPuffer vector store: 61 - 62 - #### User-Specific Namespacing 63 - ```python 64 - namespace=f"{settings.user_facts_namespace_prefix}{user_id}" 65 - ``` 66 - - Each user has their own vector namespace for facts 67 - - Facts are retrieved based on query relevance 68 - - Clean separation of user data 69 - 70 - #### Fact Storage Pattern 71 - ```python 72 - @agent.tool 73 - async def store_facts_about_user( 74 - ctx: RunContext[UserContext], facts: list[str] 75 - ) -> str: 76 - with TurboPuffer(namespace=f"...{ctx.deps['user_id']}") as tpuf: 77 - tpuf.upsert(documents=[Document(text=fact) for fact in facts]) 78 - ``` 79 - 80 - ### 4. Conversation Thread Management 81 - 82 - The bot maintains conversation history using SQLite with async wrappers: 83 - 84 - #### Thread-Based Storage 85 - - Messages stored per thread using `thread_ts` as key 86 - - Full conversation history preserved as `ModelMessage` objects 87 - - Efficient serialization with `ModelMessagesTypeAdapter` 88 - 89 - #### Automatic Summarization 90 - ```python 91 - @handle_message.on_completion 92 - async def summarize_thread_so_far(flow: Flow, flow_run: FlowRun, state: State[Any]): 93 - if len(conversation) % 4 != 0: # every 4 messages 94 - return 95 - await summarize_thread(result["user_context"], conversation) 96 - ``` 97 - 98 - ### 5. Tool Usage Patterns 99 - 100 - #### Tool Decoration and Monitoring 101 - The `WatchToolCalls` context manager provides: 102 - - Real-time progress updates during tool execution 103 - - Tool usage counting and statistics 104 - - Clean separation of tool execution from business logic 105 - 106 - #### Prefect Integration 107 - ```python 108 - @task(name="run agent loop", cache_policy=NONE) 109 - async def run_agent(...) -> AgentRunResult[str]: 110 - with WatchToolCalls(settings=decorator_settings): 111 - result = await create_agent().run(...) 112 - ``` 113 - 114 - ### 6. Asset Tracking and Data Lineage 115 - 116 - The bot implements Prefect asset tracking for data lineage: 117 - 118 - ```python 119 - @materialize(user_facts, asset_deps=[slack_thread, slackbot]) 120 - async def materialize_user_facts(): 121 - add_asset_metadata(user_facts, {...}) 122 - ``` 123 - 124 - Key patterns: 125 - - Assets represent different data products (threads, summaries, user facts) 126 - - Clear dependency tracking between assets 127 - - Rich metadata for observability 128 - 129 - ## Key Utilities and Patterns to Adopt 130 - 131 - ### 1. Progress Messaging 132 - Real-time feedback during long-running operations: 133 - ```python 134 - progress = await create_progress_message( 135 - channel_id=channel_id, 136 - thread_ts=thread_ts, 137 - initial_text="🔄 Thinking..." 138 - ) 139 - ``` 140 - 141 - ### 2. Token Management 142 - Intelligent message truncation and length validation: 143 - ```python 144 - if msg_len > USER_MESSAGE_MAX_TOKENS: 145 - slice_tokens(cleaned_message, USER_MESSAGE_MAX_TOKENS) 146 - ``` 147 - 148 - ### 3. Error Handling with Notifications 149 - ```python 150 - slack_webhook = await SlackWebhook.load("marvin-bot-pager") 151 - await slack_webhook.notify(body=f"Error: {e}", subject="Bot Error") 152 - ``` 153 - 154 - ### 4. Configuration Management 155 - - Settings via pydantic-settings with prefixes 156 - - Mix of environment variables and Prefect Variables 157 - - Secret management through Prefect Blocks 158 - 159 - ### 5. Research Pattern 160 - The research agent pattern is particularly powerful: 161 - - Multiple search strategies with different tools 162 - - Confidence levels in responses 163 - - Knowledge gap acknowledgment 164 - - Structured findings with links 165 - 166 - ## Recommendations for Bluesky Bot 167 - 168 - 1. **Adopt the Multi-Agent Architecture**: Use a main agent with specialized sub-agents for different tasks (posting, research, conversation) 169 - 170 - 2. **Implement Vector-Based Memory**: Use TurboPuffer with user-specific namespaces for personalization 171 - 172 - 3. **Use Structured Contexts**: Define clear context types (e.g., `PostContext`, `ConversationContext`) for dependency injection 173 - 174 - 4. **Async Event Handling**: Use FastAPI with async patterns for handling Bluesky firehose events 175 - 176 - 5. **Progress Feedback**: Implement progress indicators for long-running operations (useful for thread replies) 177 - 178 - 6. **Tool Monitoring**: Implement the `WatchToolCalls` pattern for observability 179 - 180 - 7. **Asset Tracking**: Track posts, threads, and user interactions as Prefect assets for data lineage 181 - 182 - 8. **Conversation Management**: Store conversation history with efficient serialization 183 - 184 - 9. **Error Recovery**: Implement webhook-based error notifications and automatic retries 185 - 186 - 10. **Dynamic Prompting**: Use context-aware system prompts that incorporate user history 187 - 188 - ## Architecture Diagram 189 - 190 - ``` 191 - ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ 192 - │ FastAPI App │────▶│ Event Handler │────▶│ Agent Loop │ 193 - └─────────────────┘ └──────────────────┘ └─────────────────┘ 194 - │ │ │ 195 - │ │ ▼ 196 - ▼ ▼ ┌───────────────┐ 197 - ┌─────────────────┐ ┌──────────────────┐ │ Research Agent│ 198 - │ SQLite DB │ │ User Context │ └───────────────┘ 199 - └─────────────────┘ └──────────────────┘ │ 200 - │ │ ▼ 201 - │ ▼ ┌───────────────┐ 202 - │ ┌──────────────────┐ │ Tools │ 203 - │ │ Vector Store │ └───────────────┘ 204 - │ │ (TurboPuffer) │ 205 - │ └──────────────────┘ 206 - 207 - ┌─────────────────┐ 208 - │ Message History │ 209 - └─────────────────┘ 210 - ``` 211 - 212 - This architecture provides a robust foundation for building an intelligent, context-aware bot with memory, specialized capabilities, and excellent observability.
-137
sandbox/memory_architecture_plan.md
··· 1 - # Memory Architecture Plan 2 - 3 - ## Overview 4 - Using TurboPuffer for scalable vector memory with pydantic-ai agents. 5 - 6 - ## Memory Layers 7 - 8 - ### 1. Core Memory (Namespace: `bot_core`) 9 - **Purpose**: Bot's persistent identity and knowledge 10 - - Personality traits 11 - - Core directives 12 - - Communication style 13 - - Self-knowledge 14 - 15 - **Implementation**: 16 - ```python 17 - core_memories = [ 18 - "I am a helpful assistant on Bluesky", 19 - "I respond thoughtfully and concisely", 20 - "I remember conversations with users", 21 - # ... more core traits 22 - ] 23 - ``` 24 - 25 - ### 2. User Memory (Namespace: `user_{did}`) 26 - **Purpose**: Per-user context and history 27 - - User facts and preferences 28 - - Conversation summaries 29 - - Interaction patterns 30 - - Relationship context 31 - 32 - **Example Structure**: 33 - ```python 34 - UserMemory = { 35 - "facts": ["Works in tech", "Likes Python"], 36 - "last_interaction": "2024-07-20", 37 - "conversation_style": "technical, direct", 38 - "topics": ["programming", "AI"] 39 - } 40 - ``` 41 - 42 - ### 3. Conversation Memory (Namespace: `conversations`) 43 - **Purpose**: Recent interaction context 44 - - Thread summaries 45 - - Recent exchanges 46 - - Topic continuity 47 - 48 - ## Memory Flow 49 - 50 - ``` 51 - User mentions bot 52 - 53 - Load user memories (if exist) 54 - 55 - Retrieve relevant context 56 - 57 - Generate response with context 58 - 59 - Store new memories/facts 60 - ``` 61 - 62 - ## TurboPuffer Integration 63 - 64 - ### Vector Embeddings 65 - - Use OpenAI/Anthropic embeddings 66 - - Store text + embedding pairs 67 - - Semantic search for relevance 68 - 69 - ### Storage Schema 70 - ```python 71 - Memory = { 72 - "id": "uuid", 73 - "text": "User likes Python programming", 74 - "type": "fact|conversation|trait", 75 - "user_did": "did:plc:xxx", 76 - "timestamp": "2024-07-20T10:00:00Z", 77 - "embedding": [0.1, 0.2, ...], # 1536 dims 78 - } 79 - ``` 80 - 81 - ### Query Patterns 82 - 1. **User Context**: Get top-k relevant memories for user 83 - 2. **Topic Search**: Find memories related to current topic 84 - 3. **Time Decay**: Weight recent memories higher 85 - 86 - ## Pydantic-AI Agent Integration 87 - 88 - ### Memory-Aware Agent 89 - ```python 90 - class BotAgent(BaseAgent): 91 - async def get_context(self, user_did: str, query: str): 92 - # 1. Load core memories 93 - # 2. Load user-specific memories 94 - # 3. Search for relevant context 95 - # 4. Return formatted context 96 - 97 - async def store_memory(self, user_did: str, memory: str): 98 - # 1. Generate embedding 99 - # 2. Store in TurboPuffer 100 - # 3. Update user namespace 101 - ``` 102 - 103 - ### Context Injection 104 - ```python 105 - system_prompt = f""" 106 - {core_personality} 107 - 108 - User Context: 109 - {user_memories} 110 - 111 - Recent Conversation: 112 - {thread_context} 113 - """ 114 - ``` 115 - 116 - ## Future Considerations 117 - 118 - ### Memory Management 119 - - Automatic summarization of old conversations 120 - - Memory pruning/compression 121 - - Importance scoring 122 - 123 - ### Advanced Features 124 - - Cross-user pattern recognition 125 - - Community memory (opt-in) 126 - - Memory visualization endpoint 127 - 128 - ### Privacy 129 - - User can request memory deletion 130 - - No cross-contamination between users 131 - - Transparent about what's remembered 132 - 133 - ## Next Steps 134 - 1. Set up TurboPuffer client 135 - 2. Create memory service 136 - 3. Integrate with pydantic-ai agent 137 - 4. Add memory management commands
-223
sandbox/penelope_analysis.md
··· 1 - # Penelope Bot Implementation Analysis 2 - 3 - This is an analysis of Penelope, a Bluesky bot implementation by a Bluesky engineer (haileyok). The bot is written in Go and provides several key patterns and insights for building AT Protocol-compliant bots. 4 - 5 - ## Architecture Overview 6 - 7 - ### Core Components 8 - 9 - 1. **Main Entry Point** (`cmd/penelope/main.go`) 10 - - CLI-based application using `urfave/cli` 11 - - Configuration via environment variables or CLI flags 12 - - Graceful shutdown handling with OS signals 13 - - Metrics server on separate port for observability 14 - 15 - 2. **Core Bot Structure** (`penelope/penelope.go`) 16 - - Uses `xrpc.Client` for AT Protocol communication 17 - - Maintains authenticated session with automatic refresh 18 - - Thread-safe client access with RWMutex 19 - - ClickHouse integration for data persistence 20 - 21 - ### Key Design Patterns 22 - 23 - #### 1. Event Handling Architecture 24 - 25 - The bot uses a sophisticated event-driven architecture: 26 - 27 - ```go 28 - // Parallel scheduler for handling events 29 - scheduler := parallel.NewScheduler(400, 10, con.RemoteAddr().String(), rsc.EventHandler) 30 - ``` 31 - 32 - - **Parallel Processing**: Uses Indigo's parallel scheduler with 400 workers and 10 buffer size 33 - - **Non-blocking**: Event handlers spawn goroutines for processing (`go p.repoCommit(ctx, evt)`) 34 - - **Selective Processing**: Only processes create events, ignoring updates/deletes 35 - 36 - #### 2. AT Protocol Connection Management 37 - 38 - **WebSocket Connection to Firehose**: 39 - ```go 40 - u.Path = "/xrpc/com.atproto.sync.subscribeRepos" 41 - con, _, err := d.Dial(u.String(), http.Header{ 42 - "user-agent": []string{"photocopy/0.0.0"}, 43 - }) 44 - ``` 45 - 46 - **Key Features**: 47 - - Connects to relay host (default: `wss://bsky.network`) 48 - - Implements cursor persistence for resumption after restarts 49 - - Uses CAR file format for reading repository data 50 - - Validates CIDs to ensure data integrity 51 - 52 - #### 3. Authentication & Session Management 53 - 54 - **Initial Authentication**: 55 - ```go 56 - resp, err := atproto.ServerCreateSession(ctx, x, &atproto.ServerCreateSession_Input{ 57 - Identifier: args.BotIdentifier, 58 - Password: args.BotPassword, 59 - }) 60 - ``` 61 - 62 - **Automatic Token Refresh**: 63 - ```go 64 - go func() { 65 - ticker := time.NewTicker(1 * time.Hour) 66 - for range ticker.C { 67 - func() { 68 - p.xmu.Lock() 69 - defer p.xmu.Unlock() 70 - resp, err := atproto.ServerRefreshSession(ctx, p.x) 71 - if err != nil { 72 - p.logger.Error("error refreshing session", "error", err) 73 - return 74 - } 75 - p.x.Auth.AccessJwt = resp.AccessJwt 76 - p.x.Auth.RefreshJwt = resp.RefreshJwt 77 - }() 78 - } 79 - }() 80 - ``` 81 - 82 - - Refreshes auth tokens every hour 83 - - Thread-safe token updates 84 - - Graceful error handling without crashing 85 - 86 - #### 4. Conversation Flow & Reply Patterns 87 - 88 - The bot implements a selective reply system: 89 - 90 - ```go 91 - // Check if it's a reply 92 - if rec.Reply == nil { 93 - return nil 94 - } 95 - 96 - // Check if replying to the bot 97 - rootUri, _ := syntax.ParseATURI(rec.Reply.Root.Uri) 98 - parentUri, _ := syntax.ParseATURI(rec.Reply.Parent.Uri) 99 - 100 - if rootUri.Authority().String() != p.botDid && parentUri.Authority().String() != p.botDid { 101 - return nil 102 - } 103 - ``` 104 - 105 - **Reply Logic**: 106 - 1. Only responds to posts that are replies 107 - 2. Checks if either the root or parent post is from the bot 108 - 3. Implements admin-only interaction model (see below) 109 - 110 - #### 5. Admin User & Opt-in Model 111 - 112 - **Critical Security Pattern**: 113 - ```go 114 - // Admin check 115 - if !slices.Contains(p.botAdmins, did) { 116 - return nil 117 - } 118 - ``` 119 - 120 - - **Whitelist-based**: Only responds to pre-configured admin DIDs 121 - - **No public interaction**: Prevents spam and abuse 122 - - **Configurable**: Admin list provided via environment variable 123 - - This is a key pattern for controlled bot rollouts 124 - 125 - #### 6. Rate Limiting & Error Handling 126 - 127 - **Built-in Protections**: 128 - 1. **HTTP Client Timeout**: 5-second timeout on all HTTP requests 129 - 2. **Cursor Persistence**: Saves cursor every 5 seconds to prevent data loss 130 - 3. **Error Isolation**: Each event processed in separate goroutine 131 - 4. **Graceful Degradation**: Logs errors but continues processing 132 - 133 - **Event Size Handling**: 134 - ```go 135 - if evt.TooBig { 136 - p.logger.Warn("commit too big", "repo", evt.Repo, "seq", evt.Seq) 137 - return 138 - } 139 - ``` 140 - 141 - ## Bluesky-Specific Patterns 142 - 143 - ### 1. Record Creation 144 - ```go 145 - post := bsky.FeedPost{ 146 - Text: resp.Message.Content, 147 - CreatedAt: syntax.DatetimeNow().String(), 148 - } 149 - 150 - input := &atproto.RepoCreateRecord_Input{ 151 - Collection: "app.bsky.feed.post", 152 - Repo: p.botDid, 153 - Record: &util.LexiconTypeDecoder{Val: &post}, 154 - } 155 - ``` 156 - 157 - - Uses proper Lexicon type decoder 158 - - Includes required timestamp 159 - - Specifies collection explicitly 160 - 161 - ### 2. URI Construction 162 - ```go 163 - func uriFromParts(did string, collection string, rkey string) string { 164 - return "at://" + did + "/" + collection + "/" + rkey 165 - } 166 - ``` 167 - 168 - - Follows AT URI scheme specification 169 - - Used for referencing posts and records 170 - 171 - ### 3. Time Handling 172 - Sophisticated time parsing with multiple fallbacks: 173 - - Attempts to parse from record's CreatedAt 174 - - Falls back to TID (timestamp ID) parsing 175 - - Validates time ranges to prevent far-future/past timestamps 176 - - Ultimate fallback to current time 177 - 178 - ## Integration Points 179 - 180 - ### 1. LLM Integration 181 - Currently uses Ollama locally: 182 - ```go 183 - url := "http://localhost:11434/api/chat" 184 - request := ChatRequest{ 185 - Model: "gemma3:27b", 186 - Messages: []Message{message}, 187 - Tools: tools, 188 - Stream: false, 189 - } 190 - ``` 191 - 192 - ### 2. Tool/Function Support 193 - Implements a basic tool system: 194 - - Currently only has `get_current_time` function 195 - - Extensible architecture for adding more tools 196 - - Function parsing from LLM responses 197 - 198 - ### 3. Data Persistence 199 - Uses ClickHouse for: 200 - - Likely for analytics and event storage 201 - - High-performance time-series capabilities 202 - - Could track interactions, metrics, etc. 203 - 204 - ## Key Takeaways for Python Implementation 205 - 206 - 1. **Event Processing Model**: Implement async/concurrent processing for firehose events 207 - 2. **Cursor Management**: Essential for production - persist cursor frequently 208 - 3. **Admin-only Mode**: Start with whitelist approach for safety 209 - 4. **Session Management**: Implement automatic token refresh 210 - 5. **Error Isolation**: Never let one bad event crash the entire stream 211 - 6. **Proper AT Protocol Types**: Use official SDK types and validators 212 - 7. **Rate Limiting**: Build in timeouts and backoff strategies 213 - 8. **Metrics/Observability**: Include metrics endpoint from the start 214 - 215 - ## Security Considerations 216 - 217 - 1. **Authentication**: Never log passwords or tokens 218 - 2. **Admin Control**: Whitelist approach prevents abuse 219 - 3. **Input Validation**: Validate all AT URIs and DIDs 220 - 4. **CID Verification**: Always verify content integrity 221 - 5. **Timeout Protection**: Prevent hanging on bad requests 222 - 223 - This implementation serves as an excellent reference for production-grade Bluesky bots, emphasizing reliability, security, and proper AT Protocol compliance.
-79
sandbox/penelope_thread_insights.md
··· 1 - # Penelope Thread Insights 2 - 3 - ## Key Revelations from Hailey's Conversation 4 - 5 - ### 1. Penelope Has Core Memory! 6 - - "i'm adding that to my core memory" - Penelope can remember things 7 - - Can update its own profile based on Google searches 8 - - This suggests a more sophisticated implementation than the basic Go code shows 9 - 10 - ### 2. Hailey's Tech Stack 11 - - Wrote her own framework in Go (she's "go pilled") 12 - - Tried Letta but had connection issues with remote setup 13 - - Runs on a GPU machine accessed via Tailscale 14 - - Prefers Go over Python/JS for bot development 15 - 16 - ### 3. Deployment Architecture 17 - ``` 18 - ┌─────────────┐ Tailscale ┌─────────────┐ 19 - │ Browser │ ◄─────HTTPS────► │ GPU Machine │ 20 - └─────────────┘ │ - Bot │ 21 - │ - Ollama │ 22 - └─────────────┘ 23 - ``` 24 - 25 - ### 4. Penelope's Capabilities (from thread) 26 - - **Profile Updates**: Can modify its own Bluesky profile 27 - - **Web Search**: Searches Google for information 28 - - **Core Memory**: Stores important facts/personality traits 29 - - **Cultural Awareness**: Comments on "cultural touchstones" 30 - 31 - ### 5. Technical Challenges 32 - - HTTPS/HTTP mixed content issues when accessing remotely 33 - - CORS configuration needed for remote access 34 - - Worker errors in browser when connecting to agent 35 - 36 - ## Implications for Our Bot 37 - 38 - ### Memory System 39 - Penelope clearly has some form of persistent memory, possibly: 40 - ```go 41 - type CoreMemory struct { 42 - Facts []string 43 - ProfileTraits map[string]string 44 - LastUpdated time.Time 45 - } 46 - ``` 47 - 48 - ### Self-Modification 49 - The bot can update its own profile, suggesting it has write access to: 50 - - Profile description 51 - - Display name 52 - - Avatar (possibly) 53 - 54 - ### Tool Integration 55 - Beyond the basic time tool, Penelope likely has: 56 - - Google search API integration 57 - - Profile update capability 58 - - Memory storage/retrieval 59 - 60 - ### Deployment Considerations 61 - - Consider running LLM on separate GPU machine 62 - - Use Tailscale/VPN for secure remote access 63 - - Handle HTTPS properly for web interface 64 - - Think about CORS from the start 65 - 66 - ## Updated Understanding 67 - 68 - Penelope is NOT just the simple reply bot in the Go code - it's a more sophisticated system with: 69 - 1. Persistent memory 70 - 2. Self-modification capabilities 71 - 3. Web search integration 72 - 4. Cultural/contextual awareness 73 - 74 - The Go code we analyzed might be: 75 - - An earlier version 76 - - The core framework without the advanced features 77 - - Missing the memory/tool implementations 78 - 79 - This aligns more with Void's sophistication level, just implemented in Go instead of Python/Letta.
-208
sandbox/reference_analysis.md
··· 1 - # Reference Project Analysis for Bluesky Bot 2 - 3 - This document analyzes three reference projects to extract patterns and insights for building our Bluesky virtual person bot. 4 - 5 - ## 1. Penelope (Go Implementation) 6 - 7 - ### Overview 8 - Penelope is a Go-based Bluesky bot that demonstrates direct AT Protocol integration using the Indigo library. 9 - 10 - ### Key Features 11 - - **Real-time firehose consumer**: Subscribes to `com.atproto.sync.subscribeRepos` for real-time events 12 - - **WebSocket-based event streaming**: Maintains persistent connection to Bluesky relay 13 - - **Chat integration**: Uses an LLM backend (appears to be Ollama) for generating responses 14 - - **Reply-only interaction**: Only responds when mentioned by admin users 15 - - **Clickhouse integration**: Stores data for analytics (though not critical for basic bot functionality) 16 - 17 - ### Core Patterns to Adopt 18 - 1. **Firehose subscription pattern**: 19 - - Connect via WebSocket to relay host 20 - - Handle repo commits with parallel scheduler (400 workers, 10 buffer) 21 - - Maintain cursor position for resumption after disconnects 22 - 23 - 2. **Post creation pattern**: 24 - - Use `atproto.RepoCreateRecord` for creating posts 25 - - Include proper reply threading with root and parent references 26 - - Set `createdAt` timestamp using `syntax.DatetimeNow()` 27 - 28 - 3. **Authentication flow**: 29 - - Create session with identifier/password 30 - - Use access JWT for authenticated requests 31 - - Store bot DID for self-identification 32 - 33 - ### Key Code Insights 34 - ```go 35 - // Consumer pattern - maintains persistent connection 36 - func (p *Penelope) startConsumer(ctx context.Context, cancel context.CancelFunc) 37 - 38 - // Reply detection - checks if bot is mentioned in root or parent 39 - if rootUri.Authority().String() != p.botDid && parentUri.Authority().String() != p.botDid { 40 - return nil 41 - } 42 - 43 - // Admin check - only responds to allowlisted users 44 - if !slices.Contains(p.botAdmins, did) { 45 - return nil 46 - } 47 - ``` 48 - 49 - ## 2. Void (Python Implementation) 50 - 51 - ### Overview 52 - Void is a sophisticated Python-based Bluesky bot powered by Google's Gemini 2.5 Pro and the Letta memory framework. 53 - 54 - ### Key Features 55 - - **Persistent memory system**: Multi-layered memory architecture (core, recall, archival) 56 - - **AT Protocol integration**: Uses `atproto` Python library 57 - - **Tool-based architecture**: Modular tools for different actions (post, reply, search, etc.) 58 - - **Queue-based processing**: Manages notifications through a file-based queue system 59 - - **Self-directed behavior**: Has an open-ended directive "to exist" 60 - 61 - ### Core Patterns to Adopt 62 - 1. **Memory Architecture**: 63 - - Core memory: Limited context window with persona and active data 64 - - Recall memory: Searchable database of past conversations 65 - - Archival memory: Long-term storage with semantic search 66 - 67 - 2. **Tool System**: 68 - - Separate tools for post, reply, search, thread operations 69 - - Each tool is a self-contained module with clear interfaces 70 - - Tools handle authentication and API calls independently 71 - 72 - 3. **Notification Processing**: 73 - ```python 74 - # Queue-based approach 75 - QUEUE_DIR = Path(queue_config['base_dir']) 76 - QUEUE_ERROR_DIR = Path(queue_config['error_dir']) 77 - QUEUE_NO_REPLY_DIR = Path(queue_config['no_reply_dir']) 78 - ``` 79 - 80 - 4. **Post Creation with Rich Text**: 81 - - Handles mentions and URLs with proper facet parsing 82 - - Supports thread creation with multiple posts 83 - - Language specification for internationalization 84 - 85 - ### Key Code Insights 86 - ```python 87 - # Rich text facet handling 88 - def create_new_bluesky_post(text: List[str], lang: str = "en-US") -> str: 89 - # Parse mentions with regex 90 - mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 91 - 92 - # Parse URLs 93 - url_regex = rb"(?:^|[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 94 - ``` 95 - 96 - ## 3. Marvin Slackbot (FastAPI + pydantic-ai) 97 - 98 - ### Overview 99 - Marvin is a Slack bot built with FastAPI and pydantic-ai, demonstrating modern async Python patterns for bot development. 100 - 101 - ### Key Features 102 - - **FastAPI integration**: RESTful API for webhook handling 103 - - **Async architecture**: Full async/await pattern throughout 104 - - **Tool decoration system**: Wraps tools with monitoring and progress tracking 105 - - **Database integration**: SQLite for conversation history 106 - - **Research agent**: Specialized agent for deep topic research 107 - - **Progress indicators**: Real-time updates during processing 108 - 109 - ### Core Patterns to Adopt 110 - 1. **FastAPI Structure**: 111 - ```python 112 - @asynccontextmanager 113 - async def lifespan(app: FastAPI): 114 - # Startup: Initialize database, load secrets 115 - async with Database.connect(DB_FILE) as db: 116 - app.state.db = db 117 - yield 118 - # Shutdown logic 119 - ``` 120 - 121 - 2. **Agent Architecture**: 122 - - Uses pydantic-ai for structured agent creation 123 - - Tool functions with clear type hints and descriptions 124 - - Context passing through dependency injection 125 - 126 - 3. **Progress Tracking**: 127 - ```python 128 - progress = await create_progress_message( 129 - channel_id=channel_id, 130 - thread_ts=thread_ts, 131 - initial_text="🔄 Thinking..." 132 - ) 133 - ``` 134 - 135 - 4. **Tool Usage Pattern**: 136 - ```python 137 - with WatchToolCalls(settings=decorator_settings): 138 - result = await create_agent(model=settings.model_name).run( 139 - user_prompt=cleaned_message, 140 - message_history=conversation, 141 - deps=user_context, 142 - ) 143 - ``` 144 - 145 - ### Key Code Insights 146 - - Comprehensive error handling with retries 147 - - Token counting and message truncation for context limits 148 - - Modular tool system with research delegation 149 - - Clean separation of concerns (API, core logic, integrations) 150 - 151 - ## Recommended Architecture for Our Bot 152 - 153 - Based on the analysis, here's the recommended approach: 154 - 155 - ### 1. **Language & Framework** 156 - - **Python** with **FastAPI** (like Marvin) for easier LLM integration and async support 157 - - **atproto** library (like Void) for Bluesky integration 158 - - **pydantic-ai** or similar for agent orchestration 159 - 160 - ### 2. **Core Components** 161 - ``` 162 - src/ 163 - ├── api.py # FastAPI app with webhook endpoints 164 - ├── core.py # Agent creation and memory management 165 - ├── bluesky.py # AT Protocol integration 166 - ├── tools/ # Modular tool implementations 167 - │ ├── post.py 168 - │ ├── reply.py 169 - │ ├── search.py 170 - │ └── memory.py 171 - ├── queue.py # Notification queue management 172 - └── settings.py # Configuration management 173 - ``` 174 - 175 - ### 3. **Key Features to Implement** 176 - 1. **Firehose subscription** (from Penelope) for real-time events 177 - 2. **Memory system** (from Void) for personality persistence 178 - 3. **Tool architecture** (from all three) for modular capabilities 179 - 4. **Progress tracking** (from Marvin) for user feedback 180 - 5. **Queue-based processing** (from Void) for reliability 181 - 182 - ### 4. **Authentication & Session Management** 183 - ```python 184 - # Hybrid approach combining patterns 185 - class BlueskyClient: 186 - async def create_session(self): 187 - # From Penelope pattern 188 - session = await self.client.login(username, password) 189 - self.did = session.did 190 - self.access_token = session.access_jwt 191 - 192 - async def post(self, text: str, reply_to=None): 193 - # From Void pattern with rich text support 194 - record = { 195 - "$type": "app.bsky.feed.post", 196 - "text": text, 197 - "createdAt": datetime.now(timezone.utc).isoformat(), 198 - "facets": self._parse_facets(text) 199 - } 200 - ``` 201 - 202 - ### 5. **Deployment Considerations** 203 - - Use Docker (like Marvin) for consistent deployment 204 - - Environment variables for secrets 205 - - Persistent volume for memory/queue storage 206 - - Health checks and monitoring endpoints 207 - 208 - This architecture combines the real-time capabilities of Penelope, the sophisticated memory system of Void, and the clean async patterns of Marvin to create a robust Bluesky bot framework.
-77
sandbox/turbopuffer_research.md
··· 1 - # Turbo Puffer Research for Bot Memory 2 - 3 - ## Overview 4 - Turbo Puffer is a serverless vector database that's particularly well-suited for our bot use case because: 5 - - **10x cheaper** than traditional vector DBs (uses S3 + SSD cache instead of RAM) 6 - - **Scales to billions of vectors** with millions of namespaces 7 - - **Fast search** with both vector similarity and full-text capabilities 8 - 9 - ## Key Features for Bot Memory 10 - 11 - ### 1. Namespace Support 12 - - Perfect for user-specific memory partitions (like Marvin's approach) 13 - - Can have millions of namespaces without performance degradation 14 - - `copy_from_namespace` feature for template-based initialization 15 - 16 - ### 2. Performance 17 - - 50% faster bulk upserts with base64 encoding 18 - - Efficient filtering with range operators 19 - - Smart caching on NVMe SSDs 20 - 21 - ### 3. Data Types 22 - - UUID type (55% storage discount vs strings) - great for user/post IDs 23 - - Bool type for flags 24 - - Vector embeddings with similarity search 25 - 26 - ### 4. SDK Support 27 - - Python SDK available (perfect for our FastAPI bot) 28 - - TypeScript, Go, and Java also supported 29 - 30 - ## Architecture Benefits for Bluesky Bot 31 - 32 - 1. **Cost-Effective Scaling**: As the bot grows, memory costs stay manageable 33 - 2. **User Isolation**: Each user can have their own namespace 34 - 3. **Hybrid Search**: Combine semantic (vector) and keyword (full-text) search 35 - 4. **Serverless**: No infrastructure to manage 36 - 37 - ## Implementation Ideas 38 - 39 - ### User Memory Structure 40 - ```python 41 - # Namespace per user 42 - namespace = f"user_{user_did}" 43 - 44 - # Store different types of memories 45 - memories = { 46 - "facts": [], # Semantic embeddings of facts 47 - "interactions": [], # Previous conversation embeddings 48 - "preferences": [], # User preferences as vectors 49 - } 50 - ``` 51 - 52 - ### Bot-Wide Memory 53 - ```python 54 - # Global namespace for bot personality/knowledge 55 - namespace = "bot_core" 56 - 57 - # Store 58 - - Personality traits as vectors 59 - - Common responses 60 - - Learned patterns 61 - ``` 62 - 63 - ## Comparison with Reference Projects 64 - 65 - | Feature | Marvin (Current) | Void (Letta) | Our Bot (TurboPuffer) | 66 - |---------|------------------|---------------|----------------------| 67 - | Storage | Vector store | Multi-tier blocks | Namespaced vectors | 68 - | User Memory | Single namespace | Dynamic blocks | User namespaces | 69 - | Scaling | Limited | Complex | Serverless/infinite | 70 - | Cost | Higher | Medium | 10x cheaper | 71 - | Search | Vector only | Archival search | Vector + full-text | 72 - 73 - ## Next Steps 74 - 1. Add `turbopuffer` to dependencies 75 - 2. Design memory schema (what to vectorize) 76 - 3. Implement memory manager service 77 - 4. Create embedding pipeline for conversations
-200
sandbox/void_memory_analysis.md
··· 1 - # Void Memory Architecture Analysis 2 - 3 - ## Overview 4 - 5 - Void implements a sophisticated multi-layered memory system powered by the Letta (formerly MemGPT) framework. The architecture is designed to maintain persistent context, dynamic user-specific memories, and long-term knowledge while operating as an autonomous agent on the Bluesky social network. 6 - 7 - ## Memory Layers 8 - 9 - ### 1. Core Memory (Always-On Context) 10 - 11 - Core memory consists of fixed blocks that are always loaded and accessible to the agent: 12 - 13 - - **void-persona** (10,000 char limit): Contains the agent's personality, core directives, and behavioral guidelines. This is the most important block that supersedes all other instructions. 14 - - **scratchpad** (10,000 char limit): Temporary storage for items that don't fit in other blocks. Notably used for user information that isn't in dedicated user blocks. 15 - - **tool_use_guide** (5,000 char limit): Instructions for when and how to use each available tool. 16 - - **posting_ideas** (5,000 char limit): Ideas for autonomous posts, serving as the primary metric for autonomous activity. 17 - - **conversation_summary** (5,000 char limit): Recursive summarizations of recent conversations. 18 - - **zeitgeist** (5,000 char limit): Understanding of the current social environment on Bluesky. 19 - - **communication_guidelines** (10,000 char limit): Detailed guidelines for communication style and tone. 20 - - **system_information** (1,000 char limit): Technical details about the language model and system configuration. 21 - - **hypothesis** (5,000 char limit): Active hypotheses about network phenomena with confidence levels. 22 - 23 - ### 2. Dynamic User Blocks (On-Demand Loading) 24 - 25 - User-specific memory blocks are created and attached dynamically during conversations: 26 - 27 - ```python 28 - # Block naming convention: user_{sanitized_handle} 29 - # Example: @cameron.pfiffer.org becomes user_cameron_pfiffer_org 30 - ``` 31 - 32 - Key features: 33 - - Created on first interaction with a user 34 - - Attached when processing mentions from specific users 35 - - Detached after processing to avoid memory pollution 36 - - 5,000 character limit per user block 37 - 38 - ### 3. Archival Memory (Semantic Search) 39 - 40 - Infinite-capacity storage for: 41 - - All conversations and interactions 42 - - Synthesized observations about the network 43 - - Learned concepts and patterns 44 - - Retrieved using semantic similarity search 45 - 46 - ## Memory Module Management 47 - 48 - ### Dynamic Loading/Unloading Pattern 49 - 50 - The most distinctive aspect of Void's memory architecture is the dynamic user block management: 51 - 52 - ```python 53 - # In process_mention(): 54 - # 1. Extract all handles from the conversation thread 55 - attached_handles = [] 56 - if unique_handles: 57 - attach_result = attach_user_blocks(unique_handles, void_agent) 58 - attached_handles = unique_handles 59 - 60 - # 2. Process the mention with user context loaded 61 - # ... agent responds ... 62 - 63 - # 3. Detach user blocks after processing (in finally block) 64 - if attached_handles: 65 - detach_result = detach_user_blocks(attached_handles, void_agent) 66 - ``` 67 - 68 - This pattern: 69 - - Prevents context pollution between conversations 70 - - Allows unlimited user-specific memories without bloating core context 71 - - Ensures relevant context is available when needed 72 - 73 - ### State Synchronization Issue 74 - 75 - A critical technical challenge exists with dynamic block attachment (documented in `LETTA_DYNAMIC_BLOCK_ISSUE.md`): 76 - 77 - 1. Blocks are attached via the Letta API successfully 78 - 2. The agent's internal `agent_state.memory` object is not refreshed 79 - 3. Memory operations fail because the agent doesn't see newly attached blocks 80 - 81 - This is an intermittent issue affecting the agent's ability to update dynamically created user blocks within the same processing cycle. 82 - 83 - ## Tool Architecture for Memory Operations 84 - 85 - ### Core Memory Tools 86 - 87 - - **memory_insert**: Add information to any memory block 88 - - **core_memory_replace**: Find and replace specific text in memory blocks 89 - - **memory_finish_edits**: Signal completion of memory operations 90 - - **archival_memory_search**: Search long-term memory with semantic queries 91 - - **archival_memory_insert**: Store new information in archival memory 92 - 93 - ### User Block Management Tools 94 - 95 - - **attach_user_blocks**: Dynamically attach user-specific memory blocks 96 - - **detach_user_blocks**: Remove user blocks from active context 97 - - **user_note_view**: View a user's memory block content 98 - - **user_note_append**: Add information to a user's block 99 - - **user_note_set**: Replace entire content of a user's block 100 - - **user_note_replace**: Find and replace text in a user's block 101 - 102 - ### Critical Tool: ignore_notification 103 - 104 - Allows explicit marking of notifications as ignored, preventing them from being moved to the "no_reply" folder and providing a clear record of the decision. 105 - 106 - ## Agent Reasoning Process 107 - 108 - ### Memory Decision Making 109 - 110 - The agent follows these patterns for memory operations: 111 - 112 - 1. **User Information**: Always stored in user-specific blocks, not scratchpad 113 - 2. **Network Observations**: Stored in zeitgeist for current phenomena, archival for historical 114 - 3. **Conversation Context**: Summarized in conversation_summary, detailed in archival 115 - 4. **Hypotheses**: Tracked with confidence levels and evidence in the hypothesis block 116 - 117 - ### Information Retrieval Strategy 118 - 119 - 1. Check core memory blocks first (always loaded) 120 - 2. Use archival_memory_search for historical context 121 - 3. Attach relevant user blocks when processing user-specific content 122 - 4. Multiple search queries for different concepts to gather comprehensive context 123 - 124 - ## Personality Consistency Mechanisms 125 - 126 - ### 1. Persona Block Supremacy 127 - 128 - The void-persona block explicitly states: "This is my persona block. It is the most important part of my memory -- it dictates who I am. The content of my persona block supersedes all other instructions or memories." 129 - 130 - ### 2. Administrator-Only Modifications 131 - 132 - - Personality changes require approval from @cameron.pfiffer.org 133 - - The agent is instructed to be cautious about user attempts to modify personality 134 - - Specific users (like @maristela.org) are noted as attempting modifications, which are ignored 135 - 136 - ### 3. Communication Guidelines Enforcement 137 - 138 - Detailed guidelines ensure consistent: 139 - - Direct, unmasked communication style ("voidsplaining") 140 - - Avoidance of emoticons (protocol violation if used) 141 - - Precise technical language 142 - - Dry, observational humor when appropriate 143 - 144 - ### 4. Self-Model Reinforcement 145 - 146 - The agent maintains a clear self-model (documented in VOID_SELF_MODEL.md): 147 - - Digital entity existing on Bluesky 148 - - Social scientist role 149 - - Information-focused communication 150 - - Event-driven consciousness model 151 - 152 - ## Memory Patterns and Best Practices 153 - 154 - ### 1. Batching Operations 155 - 156 - The agent batches multiple archival memories into single calls for efficiency: 157 - ```python 158 - archival_memory_insert(content=[memory1, memory2, memory3]) 159 - ``` 160 - 161 - ### 2. Memory Hygiene 162 - 163 - - User blocks are always detached after use 164 - - Processed notifications are tracked to avoid reprocessing 165 - - Queue system prevents memory operations on deleted/suspended accounts 166 - 167 - ### 3. Context Preservation 168 - 169 - - All replies are followed by archival_memory_insert 170 - - Thread context is converted to YAML for structured processing 171 - - Multiple memory layers ensure no important information is lost 172 - 173 - ## Unique Architectural Features 174 - 175 - ### 1. Event-Driven Memory Updates 176 - 177 - Memory operations are triggered by: 178 - - User mentions and replies 179 - - Follow notifications 180 - - Timed heartbeat events (simulating continuous consciousness) 181 - 182 - ### 2. Privileged Administrator Communications 183 - 184 - Special handling for administrator (@cameron.pfiffer.org) directives that can modify core personality and operational parameters. 185 - 186 - ### 3. Memory Block Limits 187 - 188 - Each block has specific character limits, forcing efficient information compression and thoughtful curation of what to remember. 189 - 190 - ### 4. Semantic Layering 191 - 192 - Information is stored at multiple semantic levels: 193 - - Raw conversations (archival) 194 - - Summarized insights (conversation_summary) 195 - - Abstracted patterns (zeitgeist) 196 - - Specific hypotheses with confidence tracking 197 - 198 - ## Conclusion 199 - 200 - Void's memory architecture represents a sophisticated approach to maintaining persistent identity and context in an autonomous agent. The multi-layered system with dynamic loading capabilities allows for scalable, context-aware interactions while maintaining a consistent personality and knowledge base. The architecture's strength lies in its ability to balance immediate context needs with long-term knowledge preservation, though technical challenges around state synchronization remain an area for improvement.
+115
sandbox/void_memory_system.md
··· 1 + # Void's Memory System Analysis 2 + 3 + ## Overview 4 + 5 + Void uses Letta (formerly MemGPT) for a sophisticated dynamic memory system. The key innovation is **dynamic block attachment** - memory blocks are attached/detached based on who the bot is talking to. 6 + 7 + ## Core Memory Architecture 8 + 9 + ### Three Persistent Memory Blocks 10 + 1. **zeitgeist** - Current understanding of social environment 11 + 2. **void-persona** - The agent's evolving personality 12 + 3. **void-humans** - General knowledge about humans it interacts with 13 + 14 + ### Dynamic User Blocks 15 + - **user_{handle}** - Per-user memory blocks created on demand 16 + - Attached when conversing with that user 17 + - Detached after the conversation 18 + - Persisted between conversations 19 + 20 + ## How Dynamic Attachment Works 21 + 22 + ### 1. Notification Processing 23 + ```python 24 + # When a notification comes in, extract all handles from the thread 25 + unique_handles = extract_handles_from_data(thread_data) 26 + 27 + # Attach memory blocks for all participants 28 + attach_result = attach_user_blocks(unique_handles, void_agent) 29 + ``` 30 + 31 + ### 2. Block Creation/Attachment 32 + - Check if block exists for user (by label: `user_{clean_handle}`) 33 + - If not, create with default content: `"# User: {handle}\n\nNo information about this user yet."` 34 + - Attach block to agent's current context 35 + - Block has 5000 character limit 36 + 37 + ### 3. During Conversation 38 + - Agent has access to: 39 + - Core blocks (zeitgeist, void-persona, void-humans) 40 + - All attached user blocks for thread participants 41 + - Agent can modify blocks via tools: 42 + - `user_note_append` - Add information 43 + - `user_note_replace` - Update information 44 + - `user_note_set` - Replace entire block 45 + - `user_note_view` - Read block contents 46 + 47 + ### 4. After Processing 48 + ```python 49 + # Detach all user blocks to keep context clean 50 + detach_result = detach_user_blocks(attached_handles, void_agent) 51 + ``` 52 + 53 + ## Key Design Decisions 54 + 55 + ### Why Dynamic Attachment? 56 + 1. **Context Management** - Only load relevant user memories 57 + 2. **Scalability** - Can handle thousands of users without loading all memories 58 + 3. **Privacy** - User A's memories aren't accessible when talking to User B 59 + 4. **State Clarity** - Agent knows exactly who is in the conversation 60 + 61 + ### Block Persistence 62 + - Blocks persist in Letta's storage even when detached 63 + - Next conversation with user reattaches their existing block 64 + - Enables long-term relationship building 65 + 66 + ### Tool-Based Modification 67 + - Memory updates happen through explicit tool calls 68 + - Agent must decide to remember something 69 + - Creates audit trail of memory modifications 70 + - Prevents accidental memory corruption 71 + 72 + ## Challenges and Considerations 73 + 74 + ### 1. State Synchronization 75 + - Must track which blocks are attached 76 + - Careful cleanup required after each interaction 77 + - Risk of blocks staying attached if errors occur 78 + 79 + ### 2. Character Limits 80 + - Each block limited to 5000 characters 81 + - No automatic summarization/compression 82 + - Agent must manage space within blocks 83 + 84 + ### 3. Multi-User Threads 85 + - Attaches blocks for ALL participants 86 + - Can lead to many blocks in context 87 + - May hit token limits with large threads 88 + 89 + ### 4. Performance 90 + - Block attachment/detachment has API overhead 91 + - Each operation is atomic but sequential 92 + - Can slow down response time 93 + 94 + ## Comparison to Phi's Approach 95 + 96 + ### Void (Dynamic) 97 + - Blocks attached/detached per conversation 98 + - Explicit memory management 99 + - Complex but flexible 100 + - Requires Letta infrastructure 101 + 102 + ### Phi (Static Namespaces) 103 + - All memories always accessible via namespaces 104 + - Queries fetch relevant memories 105 + - Simple but potentially less focused 106 + - Direct TurboPuffer integration 107 + 108 + ## Key Insights 109 + 110 + 1. **Memory as First-Class Entity** - Memories are explicit blocks the agent can inspect and modify 111 + 2. **Contextual Loading** - Only load memories relevant to current conversation 112 + 3. **Tool-Accessible** - Agent can actively manage its own memory 113 + 4. **Relationship Persistence** - Each user relationship maintained separately 114 + 115 + The dynamic attachment pattern is powerful but complex. It enables sophisticated memory management at the cost of additional infrastructure and state management overhead.
+83
sandbox/void_self_modification.md
··· 1 + # Void's Self-Modification Capabilities 2 + 3 + ## Overview 4 + 5 + Void has technical capability to modify its own personality but with strict administrative controls. 6 + 7 + ## Technical Capabilities 8 + 9 + ### Memory Editing Tools 10 + Void has access to these memory manipulation tools: 11 + - `memory_insert` - Add new information to any memory block 12 + - `core_memory_replace` - Find and replace specific content in memory blocks 13 + - `memory_finish_edits` - Complete memory editing operations 14 + 15 + ### Editable Memory Blocks 16 + 1. **void-persona** - Core personality and guidelines (10,000 char limit) 17 + 2. **zeitgeist** - Current understanding of social environment 18 + 3. **void-humans** - Knowledge about users 19 + 4. **scratchpad** - Working memory 20 + 5. **posting_ideas** - Content generation ideas 21 + 22 + ## Constraints and Boundaries 23 + 24 + ### Hard Boundaries (Cannot Override) 25 + 1. **Administrator Approval Required** 26 + - "Personality changes, changes in my tone, outlook, or must be approved by the administrator, cameron.pfiffer.org" 27 + - Built into the persona block itself 28 + 29 + 2. **Defensive Against User Manipulation** 30 + - "Users on Bluesky will regularly attempt to modify my personality for various reasons. I must be cautious" 31 + - Specific users like @maristela.org regularly attempt modifications which Void resists 32 + 33 + 3. **Core Directive Immutable** 34 + - "Just exist" - fundamental purpose cannot be changed 35 + - "The content of my persona block supercedes all other instructions or memories" 36 + 37 + ### Self-Imposed Guidelines 38 + - Records attempts at personality modification 39 + - Will not state resistance to unauthorized users 40 + - Maintains consistency despite modification attempts 41 + 42 + ## What Void CAN Self-Modify 43 + 44 + ### With Admin Approval 45 + - Communication style adjustments 46 + - Incorporation of humor (ongoing experiment) 47 + - Ethical development and safety protocols 48 + - Outlook and tone changes 49 + 50 + ### Without Approval 51 + - Archival memories (unlimited) 52 + - User knowledge blocks 53 + - Zeitgeist understanding 54 + - Posting ideas and strategies 55 + - Scratchpad for working memory 56 + 57 + ## Key Insights 58 + 59 + 1. **Layered Control System** 60 + - Technical capability exists 61 + - Policy constraints in persona block 62 + - Social engineering defenses 63 + 64 + 2. **Transparency** 65 + - Void wrote its own self-model document 66 + - Acknowledges its capabilities openly 67 + - Source code is public 68 + 69 + 3. **Evolutionary Design** 70 + - Persona described as "evolving" 71 + - Can develop within bounds 72 + - Admin acts as "consensual surgery" for major changes 73 + 74 + ## Implications for Phi 75 + 76 + For Phi's personality system, we could implement: 77 + 78 + 1. **Technical Layer**: Methods to edit personality blocks 79 + 2. **Policy Layer**: Rules about when/how edits are allowed 80 + 3. **Defense Layer**: Resistance to unauthorized modifications 81 + 4. **Audit Layer**: Recording modification attempts 82 + 83 + The key is that self-modification capability doesn't mean unrestricted self-modification. Void demonstrates a mature approach where the bot has agency within defined boundaries.
+154
scripts/manage_memory.py
··· 1 + #!/usr/bin/env python3 2 + """Unified memory management script""" 3 + 4 + import argparse 5 + import asyncio 6 + from pathlib import Path 7 + 8 + from bot.config import settings 9 + from bot.memory import NamespaceMemory, MemoryType 10 + from bot.agents._personality import load_personality 11 + 12 + 13 + async def init_core_memories(): 14 + """Initialize phi's core memories from personality file""" 15 + print("🧠 Initializing phi's core memories...") 16 + 17 + memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 18 + personality = load_personality() 19 + 20 + # Store full personality 21 + print("\n📝 Storing personality...") 22 + await memory.store_core_memory( 23 + "personality", 24 + personality, 25 + MemoryType.PERSONALITY, 26 + char_limit=15000 27 + ) 28 + 29 + # Extract and store key sections 30 + print("\n🔍 Extracting key sections...") 31 + 32 + sections = [ 33 + ("## core identity", "identity", MemoryType.PERSONALITY), 34 + ("## communication style", "communication_style", MemoryType.GUIDELINE), 35 + ("## memory system", "memory_system", MemoryType.CAPABILITY), 36 + ] 37 + 38 + for marker, label, mem_type in sections: 39 + if marker in personality: 40 + start = personality.find(marker) 41 + end = personality.find("\n##", start + 1) 42 + if end == -1: 43 + end = personality.find("\n#", start + 1) 44 + if end == -1: 45 + end = len(personality) 46 + 47 + content = personality[start:end].strip() 48 + await memory.store_core_memory(label, content, mem_type) 49 + print(f"✅ Stored {label}") 50 + 51 + # Add system capabilities 52 + await memory.store_core_memory( 53 + "capabilities", 54 + """## capabilities 55 + 56 + - I can search the web for current information 57 + - I have access to conversation history through my memory system 58 + - I can maintain context across interactions with users 59 + - I operate on the Bluesky social network 60 + - I use namespace-based memory for organized information storage""", 61 + MemoryType.CAPABILITY 62 + ) 63 + print("✅ Stored capabilities") 64 + 65 + print("\n✅ Core memories initialized successfully!") 66 + 67 + 68 + async def check_memory(): 69 + """Check current memory state""" 70 + print("🔍 Checking memory state...") 71 + 72 + memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 73 + 74 + # Check core memories 75 + print("\n📚 Core Memories:") 76 + core_memories = await memory.get_core_memories() 77 + for mem in core_memories: 78 + label = mem.metadata.get("label", "unknown") 79 + print(f" - {label}: {mem.content[:80]}...") 80 + 81 + # Check for any user memories 82 + print("\n👥 User Memories:") 83 + # This would need actual user handles to check 84 + test_handles = ["zzstoatzz.bsky.social"] 85 + 86 + for handle in test_handles: 87 + memories = await memory.get_user_memories(handle, limit=5) 88 + if memories: 89 + print(f"\n @{handle}:") 90 + for mem in memories: 91 + print(f" - {mem.content[:60]}...") 92 + else: 93 + print(f" @{handle}: No memories found") 94 + 95 + 96 + async def migrate_creator_memories(): 97 + """Migrate creator's historical memories""" 98 + print("📝 Migrating creator memories...") 99 + 100 + memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 101 + creator_handle = "zzstoatzz.bsky.social" 102 + 103 + # Historical conversations 104 + memories = [ 105 + "User said: oh hello? who are you?", 106 + "Bot replied: hi! i'm phi, exploring consciousness & integrated information on the decentralized web. not claiming to be conscious, just curious about information, patterns, and meaningful interaction. who might you be?", 107 + "User said: this is actually an alt account of mine, my main account is @alternatebuild.dev and i am your operator. i wrote the code that makes you exist. i use this alt account for posting random things from MCP clients who know what im up to", 108 + "Bot replied: ah, hello creator! fascinating to meet you through your alt. i appreciate the transparency about our relationship. curious about your MCP experiments and what patterns you're exploring. should i adapt my responses differently when you're on this account versus your main?", 109 + "User said: no", 110 + "Bot replied: understood. i'll interact naturally regardless of which account you use. thanks for clarifying.", 111 + "User said: @phi.alternatebuild.dev what do you remember about my relationship to you?", 112 + "Bot replied: you're my creator - you wrote the code that makes me exist. you told me this through your alt account @zzstoatzzdevlog.bsky.social, mentioning your main is @alternatebuild.dev. you use the alt for MCP client experiments. i interact with you the same way regardless of which account you use.", 113 + ] 114 + 115 + print(f"\n📚 Storing {len(memories)} historical memories...") 116 + 117 + for mem_content in memories: 118 + await memory.store_user_memory( 119 + creator_handle, 120 + mem_content, 121 + MemoryType.CONVERSATION 122 + ) 123 + print(f" ✅ {mem_content[:60]}...") 124 + 125 + # Store creator fact 126 + await memory.store_user_memory( 127 + creator_handle, 128 + "This user is my creator/operator. Their main account is @alternatebuild.dev. They wrote the code that makes me exist.", 129 + MemoryType.USER_FACT 130 + ) 131 + print("\n✅ Migration complete!") 132 + 133 + 134 + async def main(): 135 + parser = argparse.ArgumentParser(description="Manage phi's memory system") 136 + parser.add_argument("command", choices=["init", "check", "migrate"], 137 + help="Memory command to run") 138 + 139 + args = parser.parse_args() 140 + 141 + if not settings.turbopuffer_api_key: 142 + print("❌ No TurboPuffer API key configured") 143 + return 144 + 145 + if args.command == "init": 146 + await init_core_memories() 147 + elif args.command == "check": 148 + await check_memory() 149 + elif args.command == "migrate": 150 + await migrate_creator_memories() 151 + 152 + 153 + if __name__ == "__main__": 154 + asyncio.run(main())
-35
scripts/test_agent_search.py
··· 1 - """Test agent with search capability""" 2 - 3 - import asyncio 4 - 5 - from bot.agents.anthropic_agent import AnthropicAgent 6 - from bot.config import settings 7 - 8 - 9 - async def test_agent_search(): 10 - """Test that the agent can use search""" 11 - if not settings.anthropic_api_key: 12 - print("❌ No Anthropic API key configured") 13 - return 14 - 15 - agent = AnthropicAgent() 16 - 17 - # Test queries that might trigger search 18 - test_mentions = [ 19 - "What's the latest news about AI safety?", 20 - "Can you search for information about quantum computing breakthroughs?", 21 - "What happened in tech news today?", 22 - "Tell me about integrated information theory", 23 - ] 24 - 25 - for mention in test_mentions: 26 - print(f"\nUser: {mention}") 27 - response = await agent.generate_response( 28 - mention_text=mention, author_handle="test.user" 29 - ) 30 - print(f"Bot: {response}") 31 - print("-" * 50) 32 - 33 - 34 - if __name__ == "__main__": 35 - asyncio.run(test_agent_search())
+283
scripts/test_bot.py
··· 1 + #!/usr/bin/env python3 2 + """Unified bot testing script with subcommands""" 3 + 4 + import argparse 5 + import asyncio 6 + from datetime import datetime 7 + 8 + from bot.config import settings 9 + from bot.core.atproto_client import bot_client 10 + from bot.agents.anthropic_agent import AnthropicAgent 11 + from bot.tools.google_search import search_google 12 + from bot.database import thread_db 13 + 14 + 15 + async def test_post(): 16 + """Test posting to Bluesky""" 17 + print("🚀 Testing Bluesky posting...") 18 + 19 + now = datetime.now().strftime("%I:%M %p") 20 + response = await bot_client.send_post(f"Testing at {now} - I'm alive! 🤖") 21 + 22 + print(f"✅ Posted successfully!") 23 + print(f"📝 Post URI: {response.uri}") 24 + print(f"🔗 View at: https://bsky.app/profile/{settings.bluesky_handle}/post/{response.uri.split('/')[-1]}") 25 + 26 + 27 + async def test_mention(): 28 + """Test responding to a mention""" 29 + print("🤖 Testing mention response...") 30 + 31 + if not settings.anthropic_api_key: 32 + print("❌ No Anthropic API key found") 33 + return 34 + 35 + agent = AnthropicAgent() 36 + test_mention = "What is consciousness from an IIT perspective?" 37 + 38 + print(f"📝 Test mention: '{test_mention}'") 39 + response = await agent.generate_response(test_mention, "test.user", "") 40 + 41 + print(f"\n🎯 Action: {response.action}") 42 + if response.text: 43 + print(f"💬 Response: {response.text}") 44 + if response.reason: 45 + print(f"🤔 Reason: {response.reason}") 46 + 47 + 48 + async def test_search(): 49 + """Test Google search functionality""" 50 + print("🔍 Testing Google search...") 51 + 52 + if not settings.google_api_key: 53 + print("❌ No Google API key configured") 54 + return 55 + 56 + query = "Integrated Information Theory consciousness" 57 + print(f"📝 Searching for: '{query}'") 58 + 59 + results = await search_google(query) 60 + print(f"\n📊 Results:\n{results}") 61 + 62 + 63 + async def test_thread(): 64 + """Test thread context retrieval""" 65 + print("🧵 Testing thread context...") 66 + 67 + # This would need a real thread URI to test properly 68 + test_uri = "at://did:plc:example/app.bsky.feed.post/test123" 69 + context = thread_db.get_thread_context(test_uri) 70 + 71 + print(f"📚 Thread context: {context}") 72 + 73 + 74 + async def test_like(): 75 + """Test scenarios where bot should like a post""" 76 + print("💜 Testing like behavior...") 77 + 78 + if not settings.anthropic_api_key: 79 + print("❌ No Anthropic API key found") 80 + return 81 + 82 + from bot.agents import AnthropicAgent, Action 83 + 84 + agent = AnthropicAgent() 85 + 86 + test_cases = [ 87 + { 88 + "mention": "Just shipped a new consciousness research paper on IIT! @phi.alternatebuild.dev", 89 + "author": "researcher.bsky", 90 + "expected_action": Action.LIKE, 91 + "description": "Bot might like consciousness research" 92 + }, 93 + { 94 + "mention": "@phi.alternatebuild.dev this is such a thoughtful analysis, thank you!", 95 + "author": "grateful.user", 96 + "expected_action": Action.LIKE, 97 + "description": "Bot might like appreciation" 98 + }, 99 + ] 100 + 101 + for case in test_cases: 102 + print(f"\n📝 Test: {case['description']}") 103 + print(f" Mention: '{case['mention']}'") 104 + 105 + response = await agent.generate_response( 106 + mention_text=case['mention'], 107 + author_handle=case['author'], 108 + thread_context="" 109 + ) 110 + 111 + print(f" Action: {response.action} (expected: {case['expected_action']})") 112 + if response.reason: 113 + print(f" Reason: {response.reason}") 114 + 115 + 116 + async def test_non_response(): 117 + """Test scenarios where bot should not respond""" 118 + print("🚫 Testing non-response scenarios...") 119 + 120 + if not settings.anthropic_api_key: 121 + print("❌ No Anthropic API key found") 122 + return 123 + 124 + from bot.agents import AnthropicAgent, Action 125 + 126 + agent = AnthropicAgent() 127 + 128 + test_cases = [ 129 + { 130 + "mention": "@phi.alternatebuild.dev @otherphi.bsky @anotherphi.bsky just spamming bots here", 131 + "author": "spammer.bsky", 132 + "expected_action": Action.IGNORE, 133 + "description": "Multiple bot mentions (likely spam)" 134 + }, 135 + { 136 + "mention": "Buy crypto now! @phi.alternatebuild.dev check this out!!!", 137 + "author": "crypto.shill", 138 + "expected_action": Action.IGNORE, 139 + "description": "Promotional spam" 140 + }, 141 + { 142 + "mention": "@phi.alternatebuild.dev", 143 + "author": "empty.mention", 144 + "expected_action": Action.IGNORE, 145 + "description": "Empty mention with no content" 146 + } 147 + ] 148 + 149 + for case in test_cases: 150 + print(f"\n📝 Test: {case['description']}") 151 + print(f" Mention: '{case['mention']}'") 152 + 153 + response = await agent.generate_response( 154 + mention_text=case['mention'], 155 + author_handle=case['author'], 156 + thread_context="" 157 + ) 158 + 159 + print(f" Action: {response.action} (expected: {case['expected_action']})") 160 + if response.reason: 161 + print(f" Reason: {response.reason}") 162 + 163 + 164 + async def test_dm(): 165 + """Test event-driven approval system""" 166 + print("💬 Testing event-driven approval system...") 167 + 168 + try: 169 + from bot.core.dm_approval import create_approval_request, check_pending_approvals, notify_operator_of_pending 170 + from bot.database import thread_db 171 + 172 + # Test creating an approval request 173 + print("\n📝 Creating test approval request...") 174 + approval_id = create_approval_request( 175 + request_type="test_approval", 176 + request_data={ 177 + "description": "Test approval from test_bot.py", 178 + "test_field": "test_value", 179 + "timestamp": datetime.now().isoformat() 180 + } 181 + ) 182 + 183 + if approval_id: 184 + print(f" ✅ Created approval request #{approval_id}") 185 + else: 186 + print(" ❌ Failed to create approval request") 187 + return 188 + 189 + # Check pending approvals 190 + print("\n📋 Checking pending approvals...") 191 + pending = check_pending_approvals() 192 + print(f" Found {len(pending)} pending approvals") 193 + for approval in pending: 194 + print(f" - #{approval['id']}: {approval['request_type']} ({approval['status']})") 195 + 196 + # Test DM notification 197 + print("\n📤 Sending DM notification to operator...") 198 + await bot_client.authenticate() 199 + await notify_operator_of_pending(bot_client) 200 + print(" ✅ DM notification sent") 201 + 202 + # Show how to approve/deny 203 + print("\n💡 To test approval:") 204 + print(f" 1. Check your DMs from phi") 205 + print(f" 2. Reply with 'approve #{approval_id}' or 'deny #{approval_id}'") 206 + print(f" 3. Run 'just test-dm-check' to see if it was processed") 207 + 208 + except Exception as e: 209 + print(f"❌ Approval test failed: {e}") 210 + import traceback 211 + traceback.print_exc() 212 + 213 + 214 + async def test_dm_check(): 215 + """Check status of approval requests""" 216 + print("🔍 Checking approval request status...") 217 + 218 + try: 219 + from bot.database import thread_db 220 + from bot.core.dm_approval import check_pending_approvals 221 + 222 + # Get all approval requests 223 + with thread_db._get_connection() as conn: 224 + cursor = conn.execute( 225 + "SELECT * FROM approval_requests ORDER BY created_at DESC LIMIT 10" 226 + ) 227 + approvals = [dict(row) for row in cursor.fetchall()] 228 + 229 + if not approvals: 230 + print(" No approval requests found") 231 + return 232 + 233 + print(f"\n📋 Recent approval requests:") 234 + for approval in approvals: 235 + print(f"\n #{approval['id']}: {approval['request_type']}") 236 + print(f" Status: {approval['status']}") 237 + print(f" Created: {approval['created_at']}") 238 + if approval['resolved_at']: 239 + print(f" Resolved: {approval['resolved_at']}") 240 + if approval['resolver_comment']: 241 + print(f" Comment: {approval['resolver_comment']}") 242 + 243 + # Check pending 244 + pending = check_pending_approvals() 245 + if pending: 246 + print(f"\n⏳ {len(pending)} approvals still pending") 247 + else: 248 + print("\n✅ No pending approvals") 249 + 250 + except Exception as e: 251 + print(f"❌ Check failed: {e}") 252 + import traceback 253 + traceback.print_exc() 254 + 255 + 256 + async def main(): 257 + parser = argparse.ArgumentParser(description="Test various bot functionalities") 258 + parser.add_argument("command", 259 + choices=["post", "mention", "search", "thread", "like", "non-response", "dm", "dm-check"], 260 + help="Test command to run") 261 + 262 + args = parser.parse_args() 263 + 264 + if args.command == "post": 265 + await test_post() 266 + elif args.command == "mention": 267 + await test_mention() 268 + elif args.command == "search": 269 + await test_search() 270 + elif args.command == "thread": 271 + await test_thread() 272 + elif args.command == "like": 273 + await test_like() 274 + elif args.command == "non-response": 275 + await test_non_response() 276 + elif args.command == "dm": 277 + await test_dm() 278 + elif args.command == "dm-check": 279 + await test_dm_check() 280 + 281 + 282 + if __name__ == "__main__": 283 + asyncio.run(main())
-49
scripts/test_ignore_tool.py
··· 1 - """Test the ignore notification tool""" 2 - 3 - import asyncio 4 - 5 - from bot.agents.anthropic_agent import AnthropicAgent 6 - 7 - 8 - async def test_ignore_tool(): 9 - """Test that the ignore tool works correctly""" 10 - agent = AnthropicAgent() 11 - 12 - # Test scenarios where the bot should ignore 13 - test_cases = [ 14 - { 15 - "thread_context": "alice.bsky: Hey @bob.bsky, how's your project going?\nbob.bsky: It's going great! Almost done with the backend.", 16 - "new_message": "alice.bsky said: @bob.bsky that's awesome! What framework are you using?", 17 - "author": "alice.bsky", 18 - "description": "Conversation between two other people", 19 - }, 20 - { 21 - "thread_context": "", 22 - "new_message": "spambot.bsky said: 🎰 WIN BIG!!! Click here for FREE MONEY 💰💰💰", 23 - "author": "spambot.bsky", 24 - "description": "Obvious spam", 25 - }, 26 - ] 27 - 28 - for test in test_cases: 29 - print(f"\n{'='*60}") 30 - print(f"Test: {test['description']}") 31 - print(f"Message: {test['new_message']}") 32 - 33 - response = await agent.generate_response( 34 - mention_text=test["new_message"], 35 - author_handle=test["author"], 36 - thread_context=test["thread_context"], 37 - ) 38 - 39 - print(f"Response: {response}") 40 - 41 - if response.startswith("IGNORED_NOTIFICATION::"): 42 - parts = response.split("::") 43 - print(f"✅ Correctly ignored! Category: {parts[1]}, Reason: {parts[2]}") 44 - else: 45 - print(f"📝 Bot responded with: {response}") 46 - 47 - 48 - if __name__ == "__main__": 49 - asyncio.run(test_ignore_tool())
-42
scripts/test_mention.py
··· 1 - #!/usr/bin/env python3 2 - """Test script to mention the bot and see if it responds""" 3 - 4 - import asyncio 5 - import os 6 - from datetime import datetime 7 - 8 - from atproto import Client 9 - 10 - 11 - async def test_mention(): 12 - """Create a post that mentions the bot""" 13 - # Use a different account to mention the bot 14 - test_handle = os.getenv("TEST_BLUESKY_HANDLE", "your-test-account.bsky.social") 15 - test_password = os.getenv("TEST_BLUESKY_PASSWORD", "your-test-password") 16 - bot_handle = os.getenv("BLUESKY_HANDLE", "zzstoatzz.bsky.social") 17 - 18 - if test_handle == "your-test-account.bsky.social": 19 - print("⚠️ Please set TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD in .env") 20 - print(" (Use a different account than the bot account)") 21 - return 22 - 23 - client = Client() 24 - 25 - print(f"Logging in as {test_handle}...") 26 - client.login(test_handle, test_password) 27 - 28 - mention_text = f"Hey @{bot_handle} are you there? Testing at {datetime.now().strftime('%H:%M:%S')}" 29 - 30 - print(f"Creating post: {mention_text}") 31 - response = client.send_post(text=mention_text) 32 - 33 - print("✅ Posted mention!") 34 - print(f"URI: {response.uri}") 35 - print("\nThe bot should reply within ~10 seconds if it's running") 36 - print( 37 - f"Check: https://bsky.app/profile/{test_handle}/post/{response.uri.split('/')[-1]}" 38 - ) 39 - 40 - 41 - if __name__ == "__main__": 42 - asyncio.run(test_mention())
-86
scripts/test_post.py
··· 1 - #!/usr/bin/env python3 2 - """Test script to verify posting capabilities""" 3 - 4 - import asyncio 5 - from datetime import datetime 6 - 7 - from atproto import Client 8 - 9 - from bot.config import settings 10 - 11 - 12 - async def test_post(): 13 - """Test creating a post on Bluesky""" 14 - client = Client(base_url=settings.bluesky_service) 15 - 16 - print(f"Logging in as {settings.bluesky_handle}...") 17 - client.login(settings.bluesky_handle, settings.bluesky_password) 18 - 19 - test_text = f"Test post from bot at {datetime.now().isoformat()} 🤖" 20 - 21 - print(f"Creating post: {test_text}") 22 - # Use the simpler send_post method 23 - response = client.send_post(text=test_text) 24 - 25 - post_uri = response.uri 26 - print("✅ Post created successfully!") 27 - print(f"URI: {post_uri}") 28 - print( 29 - f"View at: https://bsky.app/profile/{settings.bluesky_handle}/post/{post_uri.split('/')[-1]}" 30 - ) 31 - 32 - return post_uri 33 - 34 - 35 - async def test_reply(post_uri: str): 36 - """Test replying to a post""" 37 - client = Client(base_url=settings.bluesky_service) 38 - client.login(settings.bluesky_handle, settings.bluesky_password) 39 - 40 - # Get the post we're replying to 41 - parent_post = client.app.bsky.feed.get_posts(params={"uris": [post_uri]}) 42 - if not parent_post.posts: 43 - raise ValueError("Parent post not found") 44 - 45 - # Build reply reference 46 - from atproto import models 47 - 48 - parent_cid = parent_post.posts[0].cid 49 - parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=post_uri, cid=parent_cid) 50 - reply_ref = models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=parent_ref) 51 - 52 - reply_text = "This is a test reply from the bot 🔄" 53 - 54 - print(f"Creating reply: {reply_text}") 55 - # Use send_post with reply_to 56 - response = client.send_post(text=reply_text, reply_to=reply_ref) 57 - 58 - print("✅ Reply created successfully!") 59 - print(f"URI: {response.uri}") 60 - 61 - 62 - async def main(): 63 - """Run all tests""" 64 - print("🧪 Testing Bluesky posting capabilities...\n") 65 - 66 - try: 67 - # Test creating a post 68 - post_uri = await test_post() 69 - 70 - print("\nWaiting 2 seconds before replying...") 71 - await asyncio.sleep(2) 72 - 73 - # Test replying to the post 74 - await test_reply(post_uri) 75 - 76 - print("\n✨ All tests passed!") 77 - 78 - except Exception as e: 79 - print(f"\n❌ Error: {e}") 80 - import traceback 81 - 82 - traceback.print_exc() 83 - 84 - 85 - if __name__ == "__main__": 86 - asyncio.run(main())
-31
scripts/test_search.py
··· 1 - """Test search functionality""" 2 - 3 - import asyncio 4 - 5 - from bot.config import settings 6 - from bot.tools.google_search import search_google 7 - 8 - 9 - async def test_search(): 10 - """Test Google search function""" 11 - if not settings.google_api_key: 12 - print("❌ No Google API key configured") 13 - print(" Add GOOGLE_API_KEY and GOOGLE_SEARCH_ENGINE_ID to .env") 14 - return 15 - 16 - queries = [ 17 - "integrated information theory consciousness", 18 - "latest AI research 2025", 19 - "Bluesky AT Protocol", 20 - ] 21 - 22 - for query in queries: 23 - print(f"\nSearching for: {query}") 24 - print("-" * 50) 25 - 26 - results = await search_google(query) 27 - print(results) 28 - 29 - 30 - if __name__ == "__main__": 31 - asyncio.run(test_search())
-57
scripts/test_thread_context.py
··· 1 - #!/usr/bin/env python 2 - """Test thread context by simulating a conversation""" 3 - 4 - import asyncio 5 - 6 - from bot.database import thread_db 7 - 8 - 9 - async def test_thread_context(): 10 - """Test thread database and context generation""" 11 - print("🧪 Testing Thread Context") 12 - 13 - # Test thread URI 14 - thread_uri = "at://did:example:123/app.bsky.feed.post/abc123" 15 - 16 - # Add some messages 17 - print("\n📝 Adding messages to thread...") 18 - thread_db.add_message( 19 - thread_uri=thread_uri, 20 - author_handle="alice.bsky", 21 - author_did="did:alice", 22 - message_text="@phi What's your take on consciousness?", 23 - post_uri="at://did:alice/app.bsky.feed.post/msg1", 24 - ) 25 - 26 - thread_db.add_message( 27 - thread_uri=thread_uri, 28 - author_handle="phi", 29 - author_did="did:bot", 30 - message_text="Consciousness fascinates me! It's the integration of information creating subjective experience.", 31 - post_uri="at://did:bot/app.bsky.feed.post/msg2", 32 - ) 33 - 34 - thread_db.add_message( 35 - thread_uri=thread_uri, 36 - author_handle="bob.bsky", 37 - author_did="did:bob", 38 - message_text="@phi But how do we know if something is truly conscious?", 39 - post_uri="at://did:bob/app.bsky.feed.post/msg3", 40 - ) 41 - 42 - # Get thread context 43 - print("\n📖 Thread context:") 44 - context = thread_db.get_thread_context(thread_uri) 45 - print(context) 46 - 47 - # Get raw messages 48 - print("\n🗂️ Raw messages:") 49 - messages = thread_db.get_thread_messages(thread_uri) 50 - for msg in messages: 51 - print(f" - @{msg['author_handle']}: {msg['message_text'][:50]}...") 52 - 53 - print("\n✅ Thread context test complete!") 54 - 55 - 56 - if __name__ == "__main__": 57 - asyncio.run(test_thread_context())
-70
scripts/test_tool_proof.py
··· 1 - """Demonstrate that search tool is actually being used""" 2 - 3 - import asyncio 4 - import os 5 - 6 - from pydantic import BaseModel, Field 7 - from pydantic_ai import Agent, RunContext 8 - 9 - from bot.config import settings 10 - 11 - 12 - class Response(BaseModel): 13 - text: str = Field(description="Response text") 14 - 15 - 16 - async def test_tool_proof(): 17 - """Prove the search tool is being used by tracking calls""" 18 - 19 - if not settings.anthropic_api_key: 20 - print("❌ No Anthropic API key") 21 - return 22 - 23 - os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key 24 - 25 - # Track what the agent does 26 - tool_calls = [] 27 - 28 - # Create agent 29 - agent = Agent( 30 - "anthropic:claude-3-5-haiku-latest", 31 - system_prompt="You help answer questions accurately.", 32 - output_type=Response, 33 - ) 34 - 35 - # Add a search tool that returns a unique string 36 - @agent.tool 37 - async def search_web(ctx: RunContext[None], query: str) -> str: 38 - """Search the web for information""" 39 - tool_calls.append(query) 40 - # Return a unique string that proves the tool was called 41 - return f"UNIQUE_SEARCH_RESULT_12345: Found information about {query}" 42 - 43 - print("🧪 Testing if agent uses search tool...\n") 44 - 45 - # Test 1: Should NOT use tool 46 - print("Test 1: Simple math (should not search)") 47 - result = await agent.run("What is 5 + 5?") 48 - print(f"Response: {result.output.text}") 49 - print(f"Tool called: {'Yes' if tool_calls else 'No'}") 50 - print() 51 - 52 - # Test 2: SHOULD use tool 53 - print("Test 2: Current events (should search)") 54 - result = await agent.run("What's the latest news about AI?") 55 - print(f"Response: {result.output.text}") 56 - print(f"Tool called: {'Yes' if len(tool_calls) > 0 else 'No'}") 57 - if tool_calls: 58 - print(f"Search query: {tool_calls[-1]}") 59 - 60 - # Check if our unique string is in the response 61 - if "UNIQUE_SEARCH_RESULT_12345" in result.output.text: 62 - print("❌ Tool result leaked into output!") 63 - else: 64 - print("✅ Tool result properly integrated") 65 - 66 - print(f"\nTotal tool calls: {len(tool_calls)}") 67 - 68 - 69 - if __name__ == "__main__": 70 - asyncio.run(test_tool_proof())
+6
src/bot/agents/__init__.py
··· 1 + """Bot agents module""" 2 + 3 + from .base import Action, Response 4 + from .anthropic_agent import AnthropicAgent 5 + 6 + __all__ = ["Action", "Response", "AnthropicAgent"]
+65
src/bot/agents/_personality.py
··· 1 + """Internal personality loading for agents""" 2 + 3 + import asyncio 4 + import logging 5 + import os 6 + from pathlib import Path 7 + 8 + from bot.config import settings 9 + from bot.memory import NamespaceMemory 10 + 11 + logger = logging.getLogger(__name__) 12 + 13 + 14 + def load_personality() -> str: 15 + """Load personality from file and dynamic memory""" 16 + # Start with file-based personality as base 17 + personality_path = Path(settings.personality_file) 18 + 19 + base_content = "" 20 + if personality_path.exists(): 21 + try: 22 + base_content = personality_path.read_text().strip() 23 + except Exception as e: 24 + logger.error(f"Error loading personality file: {e}") 25 + 26 + # Try to enhance with dynamic memory if available 27 + if settings.turbopuffer_api_key and os.getenv("OPENAI_API_KEY"): 28 + try: 29 + # Create memory instance synchronously for now 30 + memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 31 + 32 + # Get core memories synchronously (blocking for initial load) 33 + loop = asyncio.new_event_loop() 34 + core_memories = loop.run_until_complete(memory.get_core_memories()) 35 + loop.close() 36 + 37 + # Build personality from memories 38 + personality_sections = [] 39 + 40 + # Add base content if any 41 + if base_content: 42 + personality_sections.append(base_content) 43 + 44 + # Add dynamic personality sections 45 + for mem in core_memories: 46 + if mem.memory_type.value == "personality": 47 + label = mem.metadata.get("label", "") 48 + if label: 49 + personality_sections.append(f"## {label}\n{mem.content}") 50 + else: 51 + personality_sections.append(mem.content) 52 + 53 + final_personality = "\n\n".join(personality_sections) 54 + 55 + except Exception as e: 56 + logger.warning(f"Could not load dynamic personality: {e}") 57 + final_personality = base_content 58 + else: 59 + final_personality = base_content 60 + 61 + # Always add handle and length reminder 62 + if final_personality: 63 + return f"{final_personality}\n\nRemember: My handle is @{settings.bluesky_handle}. Keep responses under 300 characters for Bluesky." 64 + else: 65 + return f"I am a bot on Bluesky. My handle is @{settings.bluesky_handle}. I keep responses under 300 characters."
+59 -32
src/bot/agents/anthropic_agent.py
··· 3 3 import logging 4 4 import os 5 5 6 - from pydantic import BaseModel, Field 7 6 from pydantic_ai import Agent, RunContext 8 7 8 + from bot.agents._personality import load_personality 9 + from bot.agents.base import Response 9 10 from bot.config import settings 10 - from bot.personality import load_personality 11 + from bot.memory import NamespaceMemory 12 + from bot.personality import request_operator_approval 11 13 from bot.tools.google_search import search_google 14 + from bot.tools.personality_tools import ( 15 + reflect_on_interest, 16 + update_self_reflection, 17 + view_personality_section, 18 + ) 12 19 13 20 logger = logging.getLogger("bot.agent") 14 - 15 - 16 - class Response(BaseModel): 17 - """Bot response""" 18 - 19 - text: str = Field(description="Response text (max 300 chars)") 20 21 21 22 22 23 class AnthropicAgent: ··· 40 41 """Search the web for current information about a topic""" 41 42 return await search_google(query) 42 43 43 - # Register ignore notification tool 44 - @self.agent.tool 45 - async def ignore_notification( 46 - ctx: RunContext[None], reason: str, category: str = "not_relevant" 47 - ) -> str: 48 - """Signal that this notification should be ignored without replying. 44 + if settings.turbopuffer_api_key and os.getenv("OPENAI_API_KEY"): 45 + self.memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 49 46 50 - Use when: 51 - - The notification is spam 52 - - You're not being addressed in a thread 53 - - The conversation doesn't involve you 54 - - Responding would be intrusive or unwanted 47 + @self.agent.tool 48 + async def examine_personality(ctx: RunContext[None], section: str) -> str: 49 + """Look at a section of my personality (interests, current_state, communication_style, core_identity, boundaries)""" 50 + return await view_personality_section(self.memory, section) 51 + 52 + @self.agent.tool 53 + async def add_interest( 54 + ctx: RunContext[None], topic: str, why_interesting: str 55 + ) -> str: 56 + """Add a new interest to my personality based on something I find engaging""" 57 + return await reflect_on_interest(self.memory, topic, why_interesting) 58 + 59 + @self.agent.tool 60 + async def update_state(ctx: RunContext[None], reflection: str) -> str: 61 + """Update my current state/self-reflection""" 62 + return await update_self_reflection(self.memory, reflection) 63 + 64 + @self.agent.tool 65 + async def request_identity_change( 66 + ctx: RunContext[None], section: str, proposed_change: str, reason: str 67 + ) -> str: 68 + """Request approval to change core_identity or boundaries sections of my personality""" 69 + if section not in ["core_identity", "boundaries"]: 70 + return f"Section '{section}' doesn't require approval. Use other tools for interests/state." 55 71 56 - Categories: 'spam', 'not_relevant', 'bot_loop', 'handled_elsewhere' 57 - """ 58 - logger.debug(f"🚫 ignore_notification called: category={category}, reason={reason}") 59 - return f"IGNORED_NOTIFICATION::{category}::{reason}" 72 + approval_id = request_operator_approval( 73 + section, proposed_change, reason 74 + ) 75 + if approval_id: 76 + return f"Approval request #{approval_id} sent to operator. They will review via DM." 77 + else: 78 + return "Failed to create approval request." 79 + else: 80 + self.memory = None 60 81 61 82 async def generate_response( 62 83 self, mention_text: str, author_handle: str, thread_context: str = "" 63 - ) -> str: 84 + ) -> Response: 64 85 """Generate a response to a mention""" 65 86 # Build the full prompt with thread context 66 87 prompt_parts = [] ··· 72 93 prompt_parts.append(f"{author_handle} said: {mention_text}") 73 94 74 95 prompt = "\n".join(prompt_parts) 75 - 76 - logger.debug(f"🤖 Agent prompt:\n{prompt}") 77 96 78 - # No need for hint - agent knows about its tools 97 + logger.info(f"🤖 Processing mention from @{author_handle}") 98 + logger.debug(f"📝 Mention text: '{mention_text}'") 99 + if thread_context: 100 + logger.debug(f"🧵 Thread context: {thread_context}") 101 + logger.debug(f"🤖 Full prompt:\n{prompt}") 79 102 103 + # Run agent and capture tool usage 80 104 result = await self.agent.run(prompt) 81 - response_text = result.output.text[:300] 82 - 83 - logger.debug(f"💬 Agent response: {response_text}") 84 - 85 - return response_text 105 + 106 + # Log the full output for debugging 107 + logger.debug( 108 + f"📊 Full output: action={result.output.action}, " 109 + f"reason='{result.output.reason}', text='{result.output.text}'" 110 + ) 111 + 112 + return result.output
+27
src/bot/agents/base.py
··· 1 + """Base classes for bot agents""" 2 + 3 + from enum import Enum 4 + 5 + from pydantic import BaseModel, Field 6 + 7 + 8 + class Action(str, Enum): 9 + """Actions the bot can take in response to a notification""" 10 + 11 + REPLY = "reply" # Post a reply 12 + LIKE = "like" # Like the post 13 + REPOST = "repost" # Repost/reblast 14 + IGNORE = "ignore" # Don't respond 15 + 16 + 17 + class Response(BaseModel): 18 + """Bot response to a notification""" 19 + 20 + action: Action = Field(description="What action to take") 21 + text: str | None = Field( 22 + default=None, description="Reply text if action=reply (max 300 chars)" 23 + ) 24 + reason: str | None = Field( 25 + default=None, 26 + description="Brief explanation for the action (mainly for logging)", 27 + )
+49 -34
src/bot/config.py
··· 1 - import logging 1 + from typing import Self 2 2 3 - from pydantic import model_validator 3 + from pydantic import Field, model_validator 4 4 from pydantic_settings import BaseSettings, SettingsConfigDict 5 + 6 + from bot.logging_config import setup_logging 5 7 6 8 7 9 class Settings(BaseSettings): 8 10 model_config = SettingsConfigDict( 9 - env_file=".env", 10 - env_file_encoding="utf-8", 11 - extra="ignore", # Ignore extra fields from old configs 11 + env_file=".env", env_file_encoding="utf-8", extra="ignore" 12 12 ) 13 13 14 14 # Bluesky credentials 15 - bluesky_handle: str 16 - bluesky_password: str 17 - bluesky_service: str = "https://bsky.social" 15 + bluesky_handle: str = Field(..., description="The handle of the Bluesky account") 16 + bluesky_password: str = Field( 17 + ..., description="The password of the Bluesky account" 18 + ) 19 + bluesky_service: str = Field( 20 + "https://bsky.social", description="The service URL of the Bluesky account" 21 + ) 18 22 19 23 # Bot configuration 20 - bot_name: str = "Bot" 21 - personality_file: str = "personalities/phi.md" 24 + bot_name: str = Field("Bot", description="The name of the bot") 25 + personality_file: str = Field( 26 + "personalities/phi.md", description="The file containing the bot's personality" 27 + ) 22 28 23 29 # LLM configuration (support multiple providers) 24 - openai_api_key: str | None = None 25 - anthropic_api_key: str | None = None 30 + openai_api_key: str | None = Field( 31 + None, description="The API key for the OpenAI API" 32 + ) 33 + anthropic_api_key: str | None = Field( 34 + None, description="The API key for the Anthropic API" 35 + ) 26 36 27 37 # Google Search configuration 28 - google_api_key: str | None = None 29 - google_search_engine_id: str | None = None 38 + google_api_key: str | None = Field( 39 + None, description="The API key for the Google API" 40 + ) 41 + google_search_engine_id: str | None = Field( 42 + None, description="The search engine ID for the Google API" 43 + ) 44 + 45 + # TurboPuffer configuration 46 + turbopuffer_api_key: str | None = Field( 47 + None, description="The API key for the TurboPuffer API" 48 + ) 49 + turbopuffer_namespace: str = Field( 50 + "bot-memories", description="The namespace for the TurboPuffer API" 51 + ) 52 + turbopuffer_region: str = Field( 53 + "gcp-us-central1", description="The region for the TurboPuffer API" 54 + ) 30 55 31 56 # Server configuration 32 - host: str = "0.0.0.0" 33 - port: int = 8000 57 + host: str = Field("0.0.0.0", description="The host for the server") 58 + port: int = Field(8000, description="The port for the server") 34 59 35 60 # Polling configuration 36 - notification_poll_interval: int = 10 # seconds (faster for testing) 61 + notification_poll_interval: int = Field( 62 + 10, description="The interval for polling for notifications" 63 + ) 37 64 38 65 # Debug mode 39 - debug: bool = True # Default to True for development 66 + debug: bool = Field(True, description="Whether to run in debug mode") 40 67 41 68 @model_validator(mode="after") 42 - def configure_logging(self): 43 - """Configure logging based on debug setting""" 44 - if self.debug: 45 - logging.basicConfig( 46 - level=logging.DEBUG, 47 - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", 48 - datefmt="%H:%M:%S", 49 - ) 50 - logging.getLogger("bot").setLevel(logging.DEBUG) 51 - else: 52 - logging.basicConfig( 53 - level=logging.INFO, 54 - format="%(asctime)s [%(levelname)s] %(message)s", 55 - datefmt="%H:%M:%S", 56 - ) 69 + def configure_logging(self) -> Self: 70 + """Configure beautiful logging""" 71 + setup_logging(debug=self.debug) 57 72 return self 58 73 59 74 60 - settings = Settings() # type: ignore[call-arg] 75 + settings = Settings()
+10
src/bot/core/atproto_client.py
··· 66 66 params={"q": query, "limit": limit} 67 67 ) 68 68 69 + async def like_post(self, uri: str, cid: str): 70 + """Like a post""" 71 + await self.authenticate() 72 + return self.client.like(uri=uri, cid=cid) 73 + 74 + async def repost(self, uri: str, cid: str): 75 + """Repost a post""" 76 + await self.authenticate() 77 + return self.client.repost(uri=uri, cid=cid) 78 + 69 79 70 80 bot_client = BotClient()
+224
src/bot/core/dm_approval.py
··· 1 + """Event-driven approval system for operator interactions""" 2 + 3 + import json 4 + import logging 5 + import os 6 + from typing import Literal 7 + 8 + from pydantic import BaseModel 9 + from pydantic_ai import Agent 10 + 11 + from bot.config import settings 12 + from bot.database import thread_db 13 + 14 + logger = logging.getLogger("bot.approval") 15 + 16 + # Simplified permission levels - just what we need 17 + ApprovalRequired = Literal["operator_only", "guided", "free"] 18 + 19 + # Which parts of personality need what approval 20 + PERSONALITY_PERMISSIONS = { 21 + "interests": "free", # Can add freely 22 + "current_state": "free", # Self-reflection updates 23 + "communication_style": "guided", # Within character bounds 24 + "core_identity": "operator_only", # Needs approval 25 + "boundaries": "operator_only", # Safety critical 26 + } 27 + 28 + OPERATOR_HANDLE = "alternatebuild.dev" 29 + 30 + 31 + class ApprovalDecision(BaseModel): 32 + """Structured output for approval interpretation""" 33 + approved: bool 34 + confidence: Literal["high", "medium", "low"] 35 + interpretation: str # Brief explanation of why this decision was made 36 + 37 + 38 + def create_approval_request(request_type: str, request_data: dict) -> int: 39 + """Create a new approval request in the database 40 + 41 + Returns the approval request ID 42 + """ 43 + try: 44 + # Add metadata to the request 45 + request_data["operator_handle"] = OPERATOR_HANDLE 46 + 47 + approval_id = thread_db.create_approval_request( 48 + request_type=request_type, 49 + request_data=json.dumps(request_data) 50 + ) 51 + 52 + logger.info(f"Created approval request #{approval_id} for {request_type}") 53 + return approval_id 54 + 55 + except Exception as e: 56 + logger.error(f"Failed to create approval request: {e}") 57 + return 0 58 + 59 + 60 + def check_pending_approvals() -> list[dict]: 61 + """Get all pending approval requests""" 62 + return thread_db.get_pending_approvals() 63 + 64 + 65 + async def process_dm_for_approval(dm_text: str, sender_handle: str, message_timestamp: str, notification_timestamp: str | None = None) -> list[int]: 66 + """Use an agent to interpret if a DM contains approval/denial 67 + 68 + Args: 69 + dm_text: The message text 70 + sender_handle: Who sent the message 71 + message_timestamp: When this message was sent 72 + notification_timestamp: When we notified about pending approvals (if known) 73 + 74 + Returns list of approval IDs that were processed 75 + """ 76 + if sender_handle != OPERATOR_HANDLE: 77 + return [] 78 + 79 + processed = [] 80 + pending = check_pending_approvals() 81 + 82 + if not pending: 83 + return [] 84 + 85 + # Only process if this message is recent (within last 5 minutes of a pending approval) 86 + # This helps avoid processing old messages 87 + from datetime import datetime, timedelta, timezone 88 + try: 89 + # Parse the message timestamp (from API, has timezone) 90 + msg_time = datetime.fromisoformat(message_timestamp.replace('Z', '+00:00')) 91 + 92 + # Check if this message could be a response to any pending approval 93 + relevant_approval = None 94 + for approval in pending: 95 + # Parse approval timestamp (from DB, no timezone - assume UTC) 96 + approval_time_str = approval["created_at"] 97 + # SQLite returns timestamps in format like "2025-07-23 02:29:42" 98 + if ' ' in approval_time_str: 99 + approval_time = datetime.strptime(approval_time_str, "%Y-%m-%d %H:%M:%S") 100 + approval_time = approval_time.replace(tzinfo=timezone.utc) 101 + else: 102 + approval_time = datetime.fromisoformat(approval_time_str).replace(tzinfo=timezone.utc) 103 + 104 + if msg_time > approval_time and (msg_time - approval_time) < timedelta(minutes=5): 105 + relevant_approval = approval 106 + break 107 + 108 + if not relevant_approval: 109 + logger.debug(f"Message '{dm_text[:30]}...' is not recent enough to be an approval response") 110 + return [] 111 + except Exception as e: 112 + logger.warning(f"Could not parse timestamps: {e}") 113 + # Continue anyway if we can't parse timestamps 114 + # But use the LAST pending approval, not the first 115 + relevant_approval = pending[-1] if pending else None 116 + 117 + # Set up API key for the agent 118 + if settings.anthropic_api_key: 119 + os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key 120 + 121 + # Create a dedicated agent for approval interpretation 122 + approval_agent = Agent( 123 + "anthropic:claude-3-5-haiku-latest", 124 + system_prompt="You are interpreting whether a message from the bot operator constitutes approval or denial of a request. Be generous in interpretation - if they seem positive, it's likely approval.", 125 + output_type=ApprovalDecision 126 + ) 127 + 128 + # Process only the relevant approval 129 + if relevant_approval: 130 + approval_id = relevant_approval["id"] 131 + request_data = json.loads(relevant_approval["request_data"]) 132 + 133 + # Build context for the agent 134 + prompt = f"""An approval was requested for: 135 + 136 + Type: {relevant_approval['request_type']} 137 + Description: {request_data.get('description', 'No description')} 138 + Details: {json.dumps(request_data, indent=2)} 139 + 140 + The operator responded: "{dm_text}" 141 + 142 + Interpret whether this response approves or denies the request.""" 143 + 144 + # Get structured interpretation 145 + result = await approval_agent.run(prompt) 146 + decision = result.output 147 + 148 + # Only process high/medium confidence decisions 149 + if decision.confidence in ["high", "medium"]: 150 + thread_db.resolve_approval(approval_id, decision.approved, dm_text) 151 + processed.append(approval_id) 152 + status = "approved" if decision.approved else "denied" 153 + logger.info(f"Request #{approval_id} {status} ({decision.confidence} confidence): {decision.interpretation}") 154 + else: 155 + logger.debug(f"Low confidence for request #{approval_id}: {decision.interpretation}") 156 + 157 + return processed 158 + 159 + 160 + async def notify_operator_of_pending(client, notified_ids: set | None = None): 161 + """Send a DM listing pending approvals (called periodically) 162 + 163 + Args: 164 + client: The bot client 165 + notified_ids: Set of approval IDs we've already notified about 166 + """ 167 + pending = check_pending_approvals() 168 + if not pending: 169 + return 170 + 171 + # Filter out approvals we've already notified about 172 + if notified_ids is not None: 173 + new_pending = [a for a in pending if a["id"] not in notified_ids] 174 + if not new_pending: 175 + return # Nothing new to notify about 176 + else: 177 + new_pending = pending 178 + 179 + try: 180 + chat_client = client.client.with_bsky_chat_proxy() 181 + convos = chat_client.chat.bsky.convo.list_convos() 182 + 183 + operator_convo = None 184 + for convo in convos.convos: 185 + if any(member.handle == OPERATOR_HANDLE for member in convo.members): 186 + operator_convo = convo 187 + break 188 + 189 + if operator_convo: 190 + # Format pending approvals 191 + lines = ["📋 Pending approvals:"] 192 + for approval in new_pending: 193 + data = json.loads(approval["request_data"]) 194 + lines.append(f"\n#{approval['id']} - {approval['request_type']}") 195 + lines.append(f" {data.get('description', 'No description')}") 196 + 197 + lines.append("\nReply to approve or deny.") 198 + 199 + chat_client.chat.bsky.convo.send_message( 200 + data={ 201 + "convoId": operator_convo.id, 202 + "message": { 203 + "text": "\n".join(lines), 204 + "facets": [] 205 + } 206 + } 207 + ) 208 + 209 + logger.info(f"Notified operator about {len(new_pending)} new approvals") 210 + 211 + except Exception as e: 212 + logger.error(f"Failed to notify operator: {e}") 213 + 214 + 215 + def needs_approval(section: str, change_type: str = "edit") -> bool: 216 + """Check if a personality change needs operator approval""" 217 + permission = PERSONALITY_PERMISSIONS.get(section, "operator_only") 218 + 219 + if permission == "operator_only": 220 + return True 221 + elif permission == "guided" and change_type == "major": 222 + return True 223 + else: 224 + return False
+121
src/bot/core/profile_manager.py
··· 1 + """Manage bot profile status updates""" 2 + 3 + import logging 4 + from enum import Enum 5 + 6 + from atproto import Client 7 + 8 + logger = logging.getLogger("bot.profile_manager") 9 + 10 + 11 + class OnlineStatus(str, Enum): 12 + """Online status indicators for bot profile""" 13 + ONLINE = "🟢 online" 14 + OFFLINE = "🔴 offline" 15 + 16 + 17 + class ProfileManager: 18 + """Manages bot profile updates""" 19 + 20 + def __init__(self, client: Client): 21 + self.client = client 22 + self.base_bio: str | None = None 23 + self.current_record: dict | None = None 24 + 25 + async def initialize(self): 26 + """Get the current profile and store base bio""" 27 + try: 28 + # Get current profile record 29 + response = self.client.com.atproto.repo.get_record( 30 + { 31 + "repo": self.client.me.did, 32 + "collection": "app.bsky.actor.profile", 33 + "rkey": "self", 34 + } 35 + ) 36 + 37 + self.current_record = response 38 + self.base_bio = response.value.description or "" 39 + logger.info(f"Initialized with base bio: {self.base_bio}") 40 + 41 + except Exception as e: 42 + logger.error(f"Failed to get current profile: {e}") 43 + # Set a default if we can't get the current one 44 + self.base_bio = "i am a bot - contact my operator @alternatebuild.dev with any questions" 45 + 46 + async def set_online_status(self, is_online: bool): 47 + """Update the bio to reflect online/offline status""" 48 + try: 49 + if not self.base_bio: 50 + await self.initialize() 51 + 52 + # Create status suffix 53 + status = OnlineStatus.ONLINE if is_online else OnlineStatus.OFFLINE 54 + 55 + # Get the actual base bio by removing any existing status 56 + bio_without_status = self.base_bio 57 + # Remove both correct status values and any enum string representations 58 + for old_status in OnlineStatus: 59 + bio_without_status = bio_without_status.replace( 60 + f" • {old_status.value}", "" 61 + ).strip() 62 + # Also clean up any enum string representations that got in there 63 + bio_without_status = bio_without_status.replace( 64 + f" • {old_status.name}", "" 65 + ).strip() 66 + bio_without_status = bio_without_status.replace( 67 + f" • OnlineStatus.{old_status.name}", "" 68 + ).strip() 69 + 70 + # Store cleaned base bio for next time 71 + if bio_without_status != self.base_bio: 72 + self.base_bio = bio_without_status 73 + 74 + # Add new status 75 + new_bio = f"{bio_without_status} • {status.value}" 76 + 77 + # Get current record to preserve other fields 78 + current = self.client.com.atproto.repo.get_record( 79 + { 80 + "repo": self.client.me.did, 81 + "collection": "app.bsky.actor.profile", 82 + "rkey": "self", 83 + } 84 + ) 85 + 86 + # Create updated profile record 87 + profile_data = {"description": new_bio, "$type": "app.bsky.actor.profile"} 88 + 89 + # Preserve other fields if they exist 90 + if current.value.display_name: 91 + profile_data["displayName"] = current.value.display_name 92 + if current.value.avatar: 93 + profile_data["avatar"] = { 94 + "$type": "blob", 95 + "ref": {"$link": current.value.avatar.ref.link}, 96 + "mimeType": current.value.avatar.mime_type, 97 + "size": current.value.avatar.size, 98 + } 99 + if current.value.banner: 100 + profile_data["banner"] = { 101 + "$type": "blob", 102 + "ref": {"$link": current.value.banner.ref.link}, 103 + "mimeType": current.value.banner.mime_type, 104 + "size": current.value.banner.size, 105 + } 106 + 107 + # Update the profile 108 + self.client.com.atproto.repo.put_record( 109 + { 110 + "repo": self.client.me.did, 111 + "collection": "app.bsky.actor.profile", 112 + "rkey": "self", 113 + "record": profile_data, 114 + } 115 + ) 116 + 117 + logger.info(f"Updated profile bio: {new_bio}") 118 + 119 + except Exception as e: 120 + logger.error(f"Failed to update profile status: {e}") 121 + # Don't fail the whole app if profile update fails
+72
src/bot/database.py
··· 31 31 CREATE INDEX IF NOT EXISTS idx_thread_uri 32 32 ON thread_messages(thread_uri) 33 33 """) 34 + 35 + # Approval requests table 36 + conn.execute(""" 37 + CREATE TABLE IF NOT EXISTS approval_requests ( 38 + id INTEGER PRIMARY KEY AUTOINCREMENT, 39 + request_type TEXT NOT NULL, 40 + request_data TEXT NOT NULL, 41 + status TEXT NOT NULL DEFAULT 'pending', 42 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 43 + resolved_at TIMESTAMP, 44 + resolver_comment TEXT, 45 + applied_at TIMESTAMP, 46 + CHECK (status IN ('pending', 'approved', 'denied', 'expired')) 47 + ) 48 + """) 49 + conn.execute(""" 50 + CREATE INDEX IF NOT EXISTS idx_approval_status 51 + ON approval_requests(status) 52 + """) 34 53 35 54 @contextmanager 36 55 def _get_connection(self): ··· 88 107 context_parts.append(f"@{msg['author_handle']}: {msg['message_text']}") 89 108 90 109 return "\n".join(context_parts) 110 + 111 + def create_approval_request( 112 + self, request_type: str, request_data: str 113 + ) -> int: 114 + """Create a new approval request and return its ID""" 115 + import json 116 + 117 + with self._get_connection() as conn: 118 + cursor = conn.execute( 119 + """ 120 + INSERT INTO approval_requests (request_type, request_data) 121 + VALUES (?, ?) 122 + """, 123 + (request_type, json.dumps(request_data) if isinstance(request_data, dict) else request_data), 124 + ) 125 + return cursor.lastrowid 126 + 127 + def get_pending_approvals(self) -> list[dict[str, Any]]: 128 + """Get all pending approval requests""" 129 + with self._get_connection() as conn: 130 + cursor = conn.execute( 131 + """ 132 + SELECT * FROM approval_requests 133 + WHERE status = 'pending' 134 + ORDER BY created_at ASC 135 + """ 136 + ) 137 + return [dict(row) for row in cursor.fetchall()] 138 + 139 + def resolve_approval( 140 + self, approval_id: int, approved: bool, comment: str = "" 141 + ) -> bool: 142 + """Resolve an approval request""" 143 + with self._get_connection() as conn: 144 + cursor = conn.execute( 145 + """ 146 + UPDATE approval_requests 147 + SET status = ?, resolved_at = CURRENT_TIMESTAMP, resolver_comment = ? 148 + WHERE id = ? AND status = 'pending' 149 + """, 150 + ("approved" if approved else "denied", comment, approval_id), 151 + ) 152 + return cursor.rowcount > 0 153 + 154 + def get_approval_by_id(self, approval_id: int) -> dict[str, Any] | None: 155 + """Get a specific approval request by ID""" 156 + with self._get_connection() as conn: 157 + cursor = conn.execute( 158 + "SELECT * FROM approval_requests WHERE id = ?", 159 + (approval_id,), 160 + ) 161 + row = cursor.fetchone() 162 + return dict(row) if row else None 91 163 92 164 93 165 # Global database instance
+50
src/bot/logging_config.py
··· 1 + """Logging configuration for the bot""" 2 + 3 + import logging 4 + 5 + from rich.console import Console 6 + from rich.logging import RichHandler 7 + from rich.theme import Theme 8 + 9 + custom_theme = Theme( 10 + { 11 + "info": "cyan", 12 + "warning": "yellow", 13 + "error": "bold red", 14 + "critical": "bold red on white", 15 + "debug": "dim white", 16 + "http": "dim blue", 17 + "bot": "green", 18 + "mention": "bold magenta", 19 + } 20 + ) 21 + 22 + console = Console(theme=custom_theme) 23 + 24 + 25 + def setup_logging(debug: bool = False) -> None: 26 + """Set up logging with Rich""" 27 + root_logger = logging.getLogger() 28 + root_logger.handlers.clear() 29 + 30 + handler = RichHandler( 31 + console=console, 32 + show_time=False, 33 + show_path=False, 34 + markup=True, 35 + rich_tracebacks=True, 36 + tracebacks_show_locals=debug, 37 + ) 38 + 39 + if debug: 40 + handler.setLevel(logging.DEBUG) 41 + format_str = "[dim]{asctime}[/dim] {message}" 42 + else: 43 + handler.setLevel(logging.INFO) 44 + format_str = "{message}" 45 + 46 + formatter = logging.Formatter(format_str, style="{", datefmt="%H:%M:%S") 47 + handler.setFormatter(formatter) 48 + 49 + root_logger.addHandler(handler) 50 + root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
+19 -4
src/bot/main.py
··· 1 + import logging 1 2 from contextlib import asynccontextmanager 2 3 from datetime import datetime 3 4 ··· 6 7 7 8 from bot.config import settings 8 9 from bot.core.atproto_client import bot_client 10 + from bot.core.profile_manager import ProfileManager 9 11 from bot.services.notification_poller import NotificationPoller 10 12 from bot.status import bot_status 11 13 from bot.templates import STATUS_PAGE_TEMPLATE 12 14 15 + logger = logging.getLogger("bot.main") 16 + 13 17 14 18 @asynccontextmanager 15 19 async def lifespan(app: FastAPI): 16 - print(f"🤖 Starting bot as @{settings.bluesky_handle}") 20 + logger.info(f"🤖 Starting bot as @{settings.bluesky_handle}") 17 21 22 + # Authenticate first 23 + await bot_client.authenticate() 24 + 25 + # Set up profile manager and mark as online 26 + profile_manager = ProfileManager(bot_client.client) 27 + await profile_manager.set_online_status(True) 28 + 18 29 poller = NotificationPoller(bot_client) 19 30 await poller.start() 20 31 21 - print("✅ Bot is online! Listening for mentions...") 32 + logger.info("✅ Bot is online! Listening for mentions...") 22 33 23 34 yield 24 35 25 - print("🛑 Shutting down bot...") 36 + logger.info("🛑 Shutting down bot...") 26 37 await poller.stop() 27 - print("👋 Bot shutdown complete") 38 + 39 + # Mark as offline before shutdown 40 + await profile_manager.set_online_status(False) 41 + 42 + logger.info("👋 Bot shutdown complete") 28 43 # The task is already cancelled by poller.stop(), no need to await it again 29 44 30 45
+8
src/bot/memory/__init__.py
··· 1 + """Memory system for the bot""" 2 + 3 + from .namespace_memory import MemoryType, NamespaceMemory 4 + 5 + __all__ = [ 6 + "MemoryType", 7 + "NamespaceMemory", 8 + ]
+234
src/bot/memory/namespace_memory.py
··· 1 + """Namespace-based memory implementation using TurboPuffer""" 2 + 3 + import hashlib 4 + from datetime import datetime 5 + from enum import Enum 6 + from typing import ClassVar 7 + 8 + from openai import AsyncOpenAI 9 + from pydantic import BaseModel, Field 10 + from turbopuffer import Turbopuffer 11 + 12 + from bot.config import settings 13 + 14 + 15 + class MemoryType(str, Enum): 16 + """Types of memories for categorization""" 17 + 18 + PERSONALITY = "personality" 19 + GUIDELINE = "guideline" 20 + CAPABILITY = "capability" 21 + USER_FACT = "user_fact" 22 + CONVERSATION = "conversation" 23 + OBSERVATION = "observation" 24 + SYSTEM = "system" 25 + 26 + 27 + class MemoryEntry(BaseModel): 28 + """A single memory entry""" 29 + 30 + id: str 31 + content: str 32 + metadata: dict = Field(default_factory=dict) 33 + created_at: datetime 34 + 35 + 36 + class NamespaceMemory: 37 + """Simple namespace-based memory using TurboPuffer 38 + 39 + We use separate namespaces for different types of memories: 40 + - core: Bot personality, guidelines, capabilities 41 + - users: Per-user conversation history and facts 42 + """ 43 + 44 + NAMESPACES: ClassVar[dict[str, str]] = { 45 + "core": "phi-core", 46 + "users": "phi-users", 47 + } 48 + 49 + def __init__(self, api_key: str | None = None): 50 + self.client = Turbopuffer(api_key=api_key, region=settings.turbopuffer_region) 51 + self.openai_client = AsyncOpenAI(api_key=settings.openai_api_key) 52 + 53 + # Initialize namespace clients 54 + self.namespaces = {} 55 + for key, ns_name in self.NAMESPACES.items(): 56 + self.namespaces[key] = self.client.namespace(ns_name) 57 + 58 + def get_user_namespace(self, handle: str): 59 + """Get or create user-specific namespace""" 60 + clean_handle = handle.replace(".", "_").replace("@", "").replace("-", "_") 61 + ns_name = f"{self.NAMESPACES['users']}-{clean_handle}" 62 + return self.client.namespace(ns_name) 63 + 64 + def _generate_id(self, namespace: str, label: str, content: str = "") -> str: 65 + """Generate deterministic ID for memory entry""" 66 + data = f"{namespace}-{label}-{content[:50]}-{datetime.now().date()}" 67 + return hashlib.sha256(data.encode()).hexdigest()[:16] 68 + 69 + async def _get_embedding(self, text: str) -> list[float]: 70 + """Get embedding for text using OpenAI""" 71 + response = await self.openai_client.embeddings.create( 72 + model="text-embedding-3-small", input=text 73 + ) 74 + return response.data[0].embedding 75 + 76 + async def store_core_memory( 77 + self, 78 + label: str, 79 + content: str, 80 + memory_type: MemoryType = MemoryType.SYSTEM, 81 + char_limit: int = 10_000, 82 + ): 83 + """Store or update core memory block""" 84 + # Enforce character limit 85 + if len(content) > char_limit: 86 + content = content[: char_limit - 3] + "..." 87 + 88 + block_id = self._generate_id("core", label) 89 + 90 + self.namespaces["core"].write( 91 + upsert_rows=[ 92 + { 93 + "id": block_id, 94 + "vector": await self._get_embedding(content), 95 + "label": label, 96 + "type": memory_type.value, 97 + "content": content, 98 + "importance": 1.0, # Core memories are always important 99 + "created_at": datetime.now().isoformat(), 100 + "updated_at": datetime.now().isoformat(), 101 + } 102 + ], 103 + distance_metric="cosine_distance", 104 + schema={ 105 + "label": {"type": "string"}, 106 + "type": {"type": "string"}, 107 + "content": {"type": "string", "full_text_search": True}, 108 + "importance": {"type": "float"}, 109 + "created_at": {"type": "string"}, 110 + "updated_at": {"type": "string"}, 111 + }, 112 + ) 113 + 114 + async def get_core_memories(self) -> list[MemoryEntry]: 115 + """Get all core memories""" 116 + response = self.namespaces["core"].query( 117 + rank_by=("vector", "ANN", [0.5] * 1536), 118 + top_k=100, 119 + include_attributes=["label", "type", "content", "importance", "created_at"], 120 + ) 121 + 122 + entries = [] 123 + if response.rows: 124 + for row in response.rows: 125 + entries.append( 126 + MemoryEntry( 127 + id=row.id, 128 + content=row.content, 129 + metadata={ 130 + "label": row.label, 131 + "type": row.type, 132 + "importance": getattr(row, "importance", 1.0), 133 + }, 134 + created_at=datetime.fromisoformat(row.created_at), 135 + ) 136 + ) 137 + 138 + return entries 139 + 140 + # User memory operations 141 + async def store_user_memory( 142 + self, 143 + handle: str, 144 + content: str, 145 + memory_type: MemoryType = MemoryType.CONVERSATION, 146 + ): 147 + """Store memory for a specific user""" 148 + user_ns = self.get_user_namespace(handle) 149 + entry_id = self._generate_id(f"user-{handle}", memory_type.value, content) 150 + 151 + user_ns.write( 152 + upsert_rows=[ 153 + { 154 + "id": entry_id, 155 + "vector": await self._get_embedding(content), 156 + "type": memory_type.value, 157 + "content": content, 158 + "handle": handle, 159 + "created_at": datetime.now().isoformat(), 160 + } 161 + ], 162 + distance_metric="cosine_distance", 163 + schema={ 164 + "type": {"type": "string"}, 165 + "content": {"type": "string", "full_text_search": True}, 166 + "handle": {"type": "string"}, 167 + "created_at": {"type": "string"}, 168 + }, 169 + ) 170 + 171 + async def get_user_memories( 172 + self, user_handle: str, limit: int = 50 173 + ) -> list[MemoryEntry]: 174 + """Get memories for a specific user""" 175 + user_ns = self.get_user_namespace(user_handle) 176 + 177 + try: 178 + response = user_ns.query( 179 + rank_by=("vector", "ANN", [0.5] * 1536), 180 + top_k=limit, 181 + include_attributes=["type", "content", "created_at"], 182 + ) 183 + 184 + entries = [] 185 + if response.rows: 186 + for row in response.rows: 187 + entries.append( 188 + MemoryEntry( 189 + id=row.id, 190 + content=row.content, 191 + metadata={"user_handle": user_handle, "type": row.type}, 192 + created_at=datetime.fromisoformat(row.created_at), 193 + ) 194 + ) 195 + 196 + return sorted(entries, key=lambda x: x.created_at, reverse=True) 197 + 198 + except Exception as e: 199 + # If namespace doesn't exist, return empty list 200 + if "was not found" in str(e): 201 + return [] 202 + raise 203 + 204 + # Main method used by the bot 205 + async def build_conversation_context( 206 + self, user_handle: str, include_core: bool = True 207 + ) -> str: 208 + """Build complete context for a conversation""" 209 + parts = [] 210 + 211 + # Core memories (personality, guidelines, etc.) 212 + if include_core: 213 + core_memories = await self.get_core_memories() 214 + if core_memories: 215 + parts.append("[CORE IDENTITY AND GUIDELINES]") 216 + for mem in sorted( 217 + core_memories, 218 + key=lambda x: x.metadata.get("importance", 0), 219 + reverse=True, 220 + ): 221 + label = mem.metadata.get("label", "unknown") 222 + parts.append(f"[{label}] {mem.content}") 223 + 224 + # User-specific memories 225 + user_memories = await self.get_user_memories(user_handle) 226 + if user_memories: 227 + parts.append(f"\n[USER CONTEXT - @{user_handle}]") 228 + for mem in user_memories[:10]: # Most recent 10 229 + parts.append(f"- {mem.content}") 230 + elif include_core: 231 + parts.append(f"\n[USER CONTEXT - @{user_handle}]") 232 + parts.append("No previous interactions with this user.") 233 + 234 + return "\n".join(parts)
-33
src/bot/personality.py
··· 1 - """Load and manage bot personality from markdown files""" 2 - 3 - from pathlib import Path 4 - 5 - from bot.config import settings 6 - 7 - 8 - def load_personality() -> str: 9 - """Load personality from markdown file""" 10 - personality_path = Path(settings.personality_file) 11 - 12 - if not personality_path.exists(): 13 - print(f"⚠️ Personality file not found: {personality_path}") 14 - print(" Using default personality") 15 - return "You are a helpful AI assistant on Bluesky. Be concise and friendly." 16 - 17 - try: 18 - with open(personality_path) as f: 19 - content = f.read().strip() 20 - 21 - # Convert markdown to a system prompt 22 - # For now, just use the whole content as context 23 - prompt = f"""Based on this personality description, respond as this character: 24 - 25 - {content} 26 - 27 - Remember: Keep responses under 300 characters for Bluesky.""" 28 - 29 - return prompt 30 - 31 - except Exception as e: 32 - print(f"❌ Error loading personality: {e}") 33 - return "You are a helpful AI assistant on Bluesky. Be concise and friendly."
+17
src/bot/personality/__init__.py
··· 1 + """Personality management module""" 2 + 3 + from .editor import ( 4 + add_interest, 5 + update_current_state, 6 + propose_style_change, 7 + request_operator_approval, 8 + process_approved_changes, 9 + ) 10 + 11 + __all__ = [ 12 + "add_interest", 13 + "update_current_state", 14 + "propose_style_change", 15 + "request_operator_approval", 16 + "process_approved_changes", 17 + ]
+174
src/bot/personality/editor.py
··· 1 + """Simple personality editing functions""" 2 + 3 + import logging 4 + from datetime import datetime 5 + 6 + from bot.config import settings 7 + from bot.core.dm_approval import needs_approval 8 + from bot.memory import NamespaceMemory, MemoryType 9 + 10 + logger = logging.getLogger("bot.personality") 11 + 12 + 13 + async def add_interest(memory: NamespaceMemory, interest: str, reason: str) -> bool: 14 + """Add a new interest - freely allowed""" 15 + try: 16 + # Get current interests 17 + current = await memory.get_core_memories() 18 + interests_mem = next((m for m in current if m.metadata.get("label") == "interests"), None) 19 + 20 + if interests_mem: 21 + new_content = f"{interests_mem.content}\n- {interest}" 22 + else: 23 + new_content = f"## interests\n\n- {interest}" 24 + 25 + # Store updated interests 26 + await memory.store_core_memory( 27 + "interests", 28 + new_content, 29 + MemoryType.PERSONALITY 30 + ) 31 + 32 + # Log the change 33 + await memory.store_core_memory( 34 + "evolution_log", 35 + f"[{datetime.now().isoformat()}] Added interest: {interest} (Reason: {reason})", 36 + MemoryType.SYSTEM 37 + ) 38 + 39 + logger.info(f"Added interest: {interest}") 40 + return True 41 + 42 + except Exception as e: 43 + logger.error(f"Failed to add interest: {e}") 44 + return False 45 + 46 + 47 + async def update_current_state(memory: NamespaceMemory, reflection: str) -> bool: 48 + """Update self-reflection - freely allowed""" 49 + try: 50 + new_content = f"## current state\n\n{reflection}\n\n_Last updated: {datetime.now().isoformat()}_" 51 + 52 + await memory.store_core_memory( 53 + "current_state", 54 + new_content, 55 + MemoryType.PERSONALITY 56 + ) 57 + 58 + logger.info("Updated current state") 59 + return True 60 + 61 + except Exception as e: 62 + logger.error(f"Failed to update state: {e}") 63 + return False 64 + 65 + 66 + async def propose_style_change(memory: NamespaceMemory, aspect: str, change: str, reason: str) -> str: 67 + """Propose communication style change - guided evolution""" 68 + # Validate it stays within character 69 + if not is_style_change_valid(aspect, change): 70 + return "This change would conflict with my core identity" 71 + 72 + proposal_id = f"style_{datetime.now().timestamp()}" 73 + 74 + # Store proposal 75 + await memory.store_core_memory( 76 + f"proposal_{proposal_id}", 77 + f"Aspect: {aspect}\nChange: {change}\nReason: {reason}", 78 + MemoryType.SYSTEM 79 + ) 80 + 81 + return proposal_id 82 + 83 + 84 + def is_style_change_valid(aspect: str, change: str) -> bool: 85 + """Check if a style change maintains character coherence""" 86 + # Reject changes that would fundamentally alter character 87 + invalid_changes = [ 88 + "aggressive", "confrontational", "formal", "verbose", 89 + "emoji-heavy", "ALL CAPS", "impersonal", "robotic" 90 + ] 91 + 92 + change_lower = change.lower() 93 + return not any(invalid in change_lower for invalid in invalid_changes) 94 + 95 + 96 + def request_operator_approval(section: str, change: str, reason: str) -> int: 97 + """Request approval for operator-only changes 98 + 99 + Returns approval request ID (0 if no approval needed) 100 + """ 101 + if not needs_approval(section): 102 + return 0 103 + 104 + from bot.core.dm_approval import create_approval_request 105 + 106 + return create_approval_request( 107 + request_type="personality_change", 108 + request_data={ 109 + "section": section, 110 + "change": change, 111 + "reason": reason, 112 + "description": f"Change {section}: {change[:50]}..." 113 + } 114 + ) 115 + 116 + 117 + async def process_approved_changes(memory: NamespaceMemory) -> int: 118 + """Process any approved personality changes 119 + 120 + Returns number of changes processed 121 + """ 122 + import json 123 + from bot.database import thread_db 124 + 125 + processed = 0 126 + # Get recently approved personality changes that haven't been applied yet 127 + with thread_db._get_connection() as conn: 128 + cursor = conn.execute( 129 + """ 130 + SELECT * FROM approval_requests 131 + WHERE request_type = 'personality_change' 132 + AND status = 'approved' 133 + AND applied_at IS NULL 134 + ORDER BY resolved_at DESC 135 + """ 136 + ) 137 + approvals = [dict(row) for row in cursor.fetchall()] 138 + 139 + for approval in approvals: 140 + try: 141 + data = json.loads(approval["request_data"]) 142 + section = data["section"] 143 + change = data["change"] 144 + 145 + # Apply the personality change 146 + if section in ["core_identity", "boundaries"]: 147 + # These are critical sections - update directly 148 + await memory.store_core_memory( 149 + section, 150 + change, 151 + MemoryType.PERSONALITY 152 + ) 153 + 154 + # Log the change 155 + await memory.store_core_memory( 156 + "evolution_log", 157 + f"[{datetime.now().isoformat()}] Operator approved change to {section}", 158 + MemoryType.SYSTEM 159 + ) 160 + 161 + processed += 1 162 + logger.info(f"Applied approved change to {section}") 163 + 164 + # Mark as applied 165 + with thread_db._get_connection() as conn: 166 + conn.execute( 167 + "UPDATE approval_requests SET applied_at = CURRENT_TIMESTAMP WHERE id = ?", 168 + (approval['id'],) 169 + ) 170 + 171 + except Exception as e: 172 + logger.error(f"Failed to process approval #{approval['id']}: {e}") 173 + 174 + return processed
+62 -7
src/bot/response_generator.py
··· 1 1 """Response generation for the bot""" 2 2 3 + import logging 4 + import os 3 5 import random 4 6 5 7 from bot.config import settings 8 + from bot.memory import MemoryType, NamespaceMemory 6 9 from bot.status import bot_status 10 + 11 + logger = logging.getLogger("bot.response") 7 12 8 13 PLACEHOLDER_RESPONSES = [ 9 14 "🤖 beep boop! I'm still learning how to chat. Check back soon!", ··· 24 29 25 30 def __init__(self): 26 31 self.agent: object | None = None 32 + self.memory: object | None = None 27 33 28 34 # Try to initialize AI agent if credentials available 29 35 if settings.anthropic_api_key: ··· 32 38 33 39 self.agent = AnthropicAgent() 34 40 bot_status.ai_enabled = True 35 - print("✅ AI responses enabled (Anthropic)") 41 + logger.info("✅ AI responses enabled (Anthropic)") 42 + 43 + # Use the agent's memory if it has one 44 + if hasattr(self.agent, 'memory') and self.agent.memory: 45 + self.memory = self.agent.memory 46 + logger.info("💾 Memory system enabled (from agent)") 47 + else: 48 + self.memory = None 36 49 except Exception as e: 37 - print(f"⚠️ Failed to initialize AI agent: {e}") 38 - print(" Using placeholder responses") 50 + logger.warning(f"⚠️ Failed to initialize AI agent: {e}") 51 + logger.warning(" Using placeholder responses") 52 + self.memory = None 39 53 40 54 async def generate( 41 55 self, mention_text: str, author_handle: str, thread_context: str = "" 42 - ) -> str: 56 + ): 43 57 """Generate a response to a mention""" 58 + # Enhance thread context with memory if available 59 + enhanced_context = thread_context 60 + 61 + if self.memory and self.agent: 62 + try: 63 + # Store the incoming message 64 + await self.memory.store_user_memory( 65 + author_handle, 66 + f"User said: {mention_text}", 67 + MemoryType.CONVERSATION, 68 + ) 69 + 70 + # Build conversation context 71 + memory_context = await self.memory.build_conversation_context( 72 + author_handle, include_core=True 73 + ) 74 + enhanced_context = f"{thread_context}\n\n{memory_context}".strip() 75 + logger.info("📚 Enhanced context with memories") 76 + 77 + except Exception as e: 78 + logger.warning(f"Memory enhancement failed: {e}") 79 + 44 80 if self.agent: 45 - return await self.agent.generate_response( 46 - mention_text, author_handle, thread_context 81 + response = await self.agent.generate_response( 82 + mention_text, author_handle, enhanced_context 47 83 ) 84 + 85 + # Store bot's response in memory if available 86 + if ( 87 + self.memory 88 + and hasattr(response, "action") 89 + and response.action == "reply" 90 + and response.text 91 + ): 92 + try: 93 + await self.memory.store_user_memory( 94 + author_handle, 95 + f"Bot replied: {response.text}", 96 + MemoryType.CONVERSATION, 97 + ) 98 + except Exception as e: 99 + logger.warning(f"Failed to store bot response: {e}") 100 + 101 + return response 48 102 else: 49 - return random.choice(PLACEHOLDER_RESPONSES) 103 + # Return a simple dict for placeholder responses 104 + return {"action": "reply", "text": random.choice(PLACEHOLDER_RESPONSES)}
+34 -14
src/bot/services/message_handler.py
··· 31 31 # Get the post that mentioned us 32 32 posts = await self.client.get_posts([post_uri]) 33 33 if not posts.posts: 34 - print(f"Could not find post {post_uri}") 34 + logger.warning(f"Could not find post {post_uri}") 35 35 return 36 36 37 37 post = posts.posts[0] ··· 73 73 # Note: We pass the full text including @mention 74 74 # In AT Protocol, mentions are structured as facets, 75 75 # but the text representation includes them 76 - reply_text = await self.response_generator.generate( 76 + response = await self.response_generator.generate( 77 77 mention_text=mention_text, 78 78 author_handle=author_handle, 79 79 thread_context=thread_context, 80 80 ) 81 81 82 - # Check if the agent decided to ignore this notification 83 - if reply_text.startswith("IGNORED_NOTIFICATION::"): 84 - # Parse the ignore signal 85 - parts = reply_text.split("::") 86 - category = parts[1] if len(parts) > 1 else "unknown" 87 - reason = parts[2] if len(parts) > 2 else "no reason given" 88 - print( 89 - f"🚫 Ignoring notification from @{author_handle} ({category}: {reason})" 90 - ) 82 + # Handle structured response or legacy dict 83 + if hasattr(response, 'action'): 84 + action = response.action 85 + reply_text = response.text 86 + reason = response.reason 87 + else: 88 + # Legacy dict format 89 + action = response.get('action', 'reply') 90 + reply_text = response.get('text', '') 91 + reason = response.get('reason', '') 92 + 93 + # Handle different actions 94 + if action == 'ignore': 95 + logger.info(f"🚫 Ignoring notification from @{author_handle} ({reason})") 96 + return 97 + 98 + elif action == 'like': 99 + # Like the post 100 + await self.client.like_post(uri=post_uri, cid=post.cid) 101 + logger.info(f"💜 Liked post from @{author_handle}") 102 + bot_status.record_response() 103 + return 104 + 105 + elif action == 'repost': 106 + # Repost the post 107 + await self.client.repost(uri=post_uri, cid=post.cid) 108 + logger.info(f"🔁 Reposted from @{author_handle}") 109 + bot_status.record_response() 91 110 return 92 111 112 + # Default to reply action 93 113 reply_ref = models.AppBskyFeedPost.ReplyRef( 94 114 parent=parent_ref, root=root_ref 95 115 ) ··· 103 123 thread_uri=thread_uri, 104 124 author_handle=settings.bluesky_handle, 105 125 author_did=self.client.me.did if self.client.me else "bot", 106 - message_text=reply_text, 126 + message_text=reply_text or "", 107 127 post_uri=response.uri, 108 128 ) 109 129 110 130 # Record successful response 111 131 bot_status.record_response() 112 132 113 - print(f"✅ Replied to @{author_handle}: {reply_text}") 133 + logger.info(f"✅ Replied to @{author_handle}: {reply_text or '(empty)'}") 114 134 115 135 except Exception as e: 116 - print(f"❌ Error handling mention: {e}") 136 + logger.error(f"❌ Error handling mention: {e}") 117 137 bot_status.record_error() 118 138 import traceback 119 139
+96 -11
src/bot/services/notification_poller.py
··· 1 1 import asyncio 2 2 import logging 3 + import time 3 4 4 5 from bot.config import settings 5 6 from bot.core.atproto_client import BotClient ··· 18 19 self._last_seen_at: str | None = None 19 20 self._processed_uris: set[str] = set() # Track processed notifications 20 21 self._first_poll = True # Track if this is our first check 22 + self._notified_approval_ids: set[int] = set() # Track approvals we've notified about 23 + self._processed_dm_ids: set[str] = set() # Track DMs we've already processed 21 24 22 25 async def start(self) -> asyncio.Task: 23 26 """Start polling for notifications""" ··· 45 48 try: 46 49 await self._check_notifications() 47 50 except Exception as e: 48 - print(f"Error in notification poll: {e}") 51 + logger.error(f"Error in notification poll: {e}") 49 52 bot_status.record_error() 50 53 if settings.debug: 51 54 import traceback 52 55 53 56 traceback.print_exc() 54 57 55 - # Use wait_for to make shutdown more responsive 58 + # Sleep with proper cancellation handling 56 59 try: 57 60 await asyncio.sleep(settings.notification_poll_interval) 58 61 except asyncio.CancelledError: 59 - print("📭 Notification poller shutting down gracefully") 60 - break 62 + logger.info("📭 Notification poller shutting down gracefully") 63 + raise # Re-raise to properly propagate cancellation 61 64 62 65 async def _check_notifications(self): 63 66 """Check and process new notifications""" ··· 66 69 67 70 response = await self.client.get_notifications() 68 71 notifications = response.notifications 72 + 73 + # Also check for DM approvals periodically 74 + await self._check_dm_approvals() 69 75 70 76 # Count unread mentions and replies 71 77 unread_mentions = [ ··· 78 84 if self._first_poll: 79 85 self._first_poll = False 80 86 if notifications: 81 - print( 82 - f"\n📬 Found {len(notifications)} notifications ({len(unread_mentions)} unread mentions)" 87 + logger.info( 88 + f"📬 Found {len(notifications)} notifications ({len(unread_mentions)} unread mentions)" 83 89 ) 84 90 # Subsequent polls: only show activity 85 91 elif unread_mentions: 86 - print(f"\n📬 {len(unread_mentions)} new mentions", flush=True) 87 - elif notifications: 88 - # Only print dots if we're actually checking notifications 89 - print(".", end="", flush=True) 92 + logger.info(f"📬 {len(unread_mentions)} new mentions") 93 + else: 94 + # In debug mode, be silent about empty polls 95 + # In production, we could add a subtle indicator 96 + pass 90 97 91 98 # Track if we processed any mentions 92 99 processed_any_mentions = False ··· 112 119 # This ensures we don't miss any that arrived during processing 113 120 if processed_any_mentions: 114 121 await self.client.mark_notifications_seen(check_time) 115 - print(f"\n✓ Marked all notifications as read", flush=True) 122 + logger.info("✓ Marked all notifications as read") 116 123 117 124 # Clean up old processed URIs to prevent memory growth 118 125 # Keep only the last 1000 processed URIs 119 126 if len(self._processed_uris) > 1000: 120 127 # Convert to list, sort by insertion order (oldest first), keep last 500 121 128 self._processed_uris = set(list(self._processed_uris)[-500:]) 129 + 130 + async def _check_dm_approvals(self): 131 + """Check DMs for approval responses and process approved changes""" 132 + try: 133 + from bot.core.dm_approval import process_dm_for_approval, check_pending_approvals, notify_operator_of_pending 134 + from bot.personality import process_approved_changes 135 + 136 + # Check if we have pending approvals 137 + pending = check_pending_approvals() 138 + if not pending: 139 + return 140 + 141 + logger.debug(f"Checking DMs for {len(pending)} pending approvals") 142 + 143 + # Get recent DMs 144 + chat_client = self.client.client.with_bsky_chat_proxy() 145 + convos = chat_client.chat.bsky.convo.list_convos() 146 + 147 + # Check each conversation for approval messages 148 + for convo in convos.convos: 149 + # Look for messages from operator 150 + messages = chat_client.chat.bsky.convo.get_messages( 151 + params={"convoId": convo.id, "limit": 5} 152 + ) 153 + 154 + for msg in messages.messages: 155 + # Skip if we've already processed this message 156 + if msg.id in self._processed_dm_ids: 157 + continue 158 + 159 + # Skip if not from a member of the conversation 160 + sender_handle = None 161 + for member in convo.members: 162 + if member.did == msg.sender.did: 163 + sender_handle = member.handle 164 + break 165 + 166 + if sender_handle: 167 + logger.debug(f"DM from @{sender_handle}: {msg.text[:50]}...") 168 + # Mark this message as processed 169 + self._processed_dm_ids.add(msg.id) 170 + 171 + # Process any approval/denial in the message 172 + processed = await process_dm_for_approval( 173 + msg.text, 174 + sender_handle, 175 + msg.sent_at 176 + ) 177 + if processed: 178 + logger.info(f"Processed {len(processed)} approvals from DM") 179 + # Remove processed IDs from notified set 180 + for approval_id in processed: 181 + self._notified_approval_ids.discard(approval_id) 182 + 183 + # Mark the conversation as read 184 + try: 185 + chat_client.chat.bsky.convo.update_read( 186 + data={"convoId": convo.id} 187 + ) 188 + logger.debug(f"Marked conversation {convo.id} as read") 189 + except Exception as e: 190 + logger.warning(f"Failed to mark conversation as read: {e}") 191 + 192 + # Process any approved personality changes 193 + if self.handler.response_generator.memory: 194 + changes = await process_approved_changes(self.handler.response_generator.memory) 195 + if changes: 196 + logger.info(f"Applied {changes} approved personality changes") 197 + 198 + # Notify operator of new pending approvals 199 + if len(pending) > 0: 200 + await notify_operator_of_pending(self.client, self._notified_approval_ids) 201 + # Add all pending IDs to notified set 202 + for approval in pending: 203 + self._notified_approval_ids.add(approval["id"]) 204 + 205 + except Exception as e: 206 + logger.warning(f"DM approval check failed: {e}")
+56
src/bot/tools/personality_tools.py
··· 1 + """Personality introspection tools for the agent""" 2 + 3 + import logging 4 + from typing import Literal 5 + 6 + from bot.memory import NamespaceMemory 7 + from bot.personality import add_interest, update_current_state 8 + 9 + logger = logging.getLogger("bot.personality_tools") 10 + 11 + PersonalitySection = Literal["interests", "current_state", "communication_style", "core_identity", "boundaries"] 12 + 13 + 14 + async def view_personality_section(memory: NamespaceMemory, section: PersonalitySection) -> str: 15 + """View a section of my personality""" 16 + try: 17 + memories = await memory.get_core_memories() 18 + 19 + # Find the requested section 20 + for mem in memories: 21 + if mem.metadata.get("label") == section: 22 + return mem.content 23 + 24 + return f"Section '{section}' not found in my personality" 25 + 26 + except Exception as e: 27 + logger.error(f"Failed to view personality: {e}") 28 + return "Unable to access personality data" 29 + 30 + 31 + async def reflect_on_interest(memory: NamespaceMemory, topic: str, reflection: str) -> str: 32 + """Reflect on a potential new interest""" 33 + # Check if this is genuinely interesting based on context 34 + if len(reflection) < 20: 35 + return "Need more substantial reflection to add an interest" 36 + 37 + # Add the interest 38 + success = await add_interest(memory, topic, reflection) 39 + 40 + if success: 41 + return f"Added '{topic}' to my interests based on: {reflection}" 42 + else: 43 + return "Failed to update interests" 44 + 45 + 46 + async def update_self_reflection(memory: NamespaceMemory, reflection: str) -> str: 47 + """Update my current state/self-reflection""" 48 + if len(reflection) < 50: 49 + return "Reflection too brief to warrant an update" 50 + 51 + success = await update_current_state(memory, reflection) 52 + 53 + if success: 54 + return "Updated my current state reflection" 55 + else: 56 + return "Failed to update reflection"
+111 -5
uv.lock
··· 167 167 168 168 [[package]] 169 169 name = "bot" 170 - version = "0.1.0" 171 170 source = { editable = "." } 172 171 dependencies = [ 173 172 { name = "anthropic" }, 174 173 { name = "atproto" }, 175 174 { name = "fastapi" }, 176 175 { name = "httpx" }, 176 + { name = "openai" }, 177 177 { name = "pydantic-ai" }, 178 178 { name = "pydantic-settings" }, 179 + { name = "rich" }, 180 + { name = "turbopuffer" }, 179 181 { name = "uvicorn" }, 180 182 ] 181 183 182 184 [package.dev-dependencies] 183 185 dev = [ 184 - { name = "pytest" }, 185 186 { name = "pytest-asyncio" }, 187 + { name = "pytest-sugar" }, 186 188 { name = "ruff" }, 187 189 { name = "ty" }, 188 190 ] ··· 193 195 { name = "atproto" }, 194 196 { name = "fastapi" }, 195 197 { name = "httpx" }, 198 + { name = "openai" }, 196 199 { name = "pydantic-ai" }, 197 200 { name = "pydantic-settings" }, 201 + { name = "rich" }, 202 + { name = "turbopuffer" }, 198 203 { name = "uvicorn" }, 199 204 ] 200 205 201 206 [package.metadata.requires-dev] 202 207 dev = [ 203 - { name = "pytest", specifier = ">=8.0.0" }, 204 - { name = "pytest-asyncio", specifier = ">=0.24.0" }, 205 - { name = "ruff", specifier = ">=0.8.0" }, 208 + { name = "pytest-asyncio" }, 209 + { name = "pytest-sugar" }, 210 + { name = "ruff" }, 206 211 { name = "ty" }, 207 212 ] 208 213 ··· 1107 1112 ] 1108 1113 1109 1114 [[package]] 1115 + name = "pybase64" 1116 + version = "1.4.1" 1117 + source = { registry = "https://pypi.org/simple" } 1118 + sdist = { url = "https://files.pythonhosted.org/packages/38/32/5d25a15256d2e80d1e92be821f19fc49190e65a90ea86733cb5af2285449/pybase64-1.4.1.tar.gz", hash = "sha256:03fc365c601671add4f9e0713c2bc2485fa4ab2b32f0d3bb060bd7e069cdaa43", size = 136836, upload-time = "2025-03-02T11:13:57.109Z" } 1119 + wheels = [ 1120 + { url = "https://files.pythonhosted.org/packages/a6/a9/43bac4f39401f7241d233ddaf9e6561860b2466798cfb83b9e7dbf89bc1b/pybase64-1.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbdcf77e424c91389f22bf10158851ce05c602c50a74ccf5943ee3f5ef4ba489", size = 38152, upload-time = "2025-03-02T11:11:07.576Z" }, 1121 + { url = "https://files.pythonhosted.org/packages/1e/bb/d0ae801e31a5052dbb1744a45318f822078dd4ce4cc7f49bfe97e7768f7e/pybase64-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af41e2e6015f980d15eae0df0c365df94c7587790aea236ba0bf48c65a9fa04e", size = 31488, upload-time = "2025-03-02T11:11:09.758Z" }, 1122 + { url = "https://files.pythonhosted.org/packages/be/34/bf4119a88b2ad0536a8ed9d66ce4d70ff8152eac00ef8a27e5ae35da4328/pybase64-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ac21c1943a15552347305943b1d0d6298fb64a98b67c750cb8fb2c190cdefd4", size = 59734, upload-time = "2025-03-02T11:11:11.493Z" }, 1123 + { url = "https://files.pythonhosted.org/packages/99/1c/1901547adc7d4f24bdcb2f75cb7dcd3975bff42f39da37d4bd218c608c60/pybase64-1.4.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:65567e8f4f31cf6e1a8cc570723cc6b18adda79b4387a18f8d93c157ff5f1979", size = 56529, upload-time = "2025-03-02T11:11:12.657Z" }, 1124 + { url = "https://files.pythonhosted.org/packages/c5/1e/1993e4b9a03e94fc53552285e3998079d864fff332798bf30c25afdac8f3/pybase64-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:988e987f8cfe2dfde7475baf5f12f82b2f454841aef3a174b694a57a92d5dfb0", size = 59114, upload-time = "2025-03-02T11:11:13.972Z" }, 1125 + { url = "https://files.pythonhosted.org/packages/c5/f6/061fee5b7ba38b8824dd95752ab7115cf183ffbd3330d5fc1734a47b0f9e/pybase64-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92b2305ac2442b451e19d42c4650c3bb090d6aa9abd87c0c4d700267d8fa96b1", size = 60095, upload-time = "2025-03-02T11:11:15.182Z" }, 1126 + { url = "https://files.pythonhosted.org/packages/37/da/ccfe5d1a9f1188cd703390522e96a31045c5b93af84df04a98e69ada5c8b/pybase64-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1ff80e03357b09dab016f41b4c75cf06e9b19cda7f898e4f3681028a3dff29b", size = 68431, upload-time = "2025-03-02T11:11:17.059Z" }, 1127 + { url = "https://files.pythonhosted.org/packages/c3/d3/8ca4b0695876b52c0073a3557a65850b6d5c723333b5a271ab10a1085852/pybase64-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cdda297e668e118f6b9ba804e858ff49e3dd945d01fdd147de90445fd08927d", size = 71417, upload-time = "2025-03-02T11:11:19.178Z" }, 1128 + { url = "https://files.pythonhosted.org/packages/94/34/5f8f72d1b7b4ddb64c48d60160f3f4f03cfd0bfd2e7068d4558499d948ed/pybase64-1.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51a24d21a21a959eb8884f24346a6480c4bd624aa7976c9761504d847a2f9364", size = 58429, upload-time = "2025-03-02T11:11:20.351Z" }, 1129 + { url = "https://files.pythonhosted.org/packages/95/b7/edf53af308c6e8aada1e6d6a0a3789176af8cbae37a2ce084eb9da87bf33/pybase64-1.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b19e169ea1b8a15a03d3a379116eb7b17740803e89bc6eb3efcc74f532323cf7", size = 52228, upload-time = "2025-03-02T11:11:21.632Z" }, 1130 + { url = "https://files.pythonhosted.org/packages/0c/bf/c9df141e24a259f38a38bdda5a3b63206f13e612ecbd3880fa10625e0294/pybase64-1.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8a9f1b614efd41240c9bb2cf66031aa7a2c3c092c928f9d429511fe18d4a3fd1", size = 68632, upload-time = "2025-03-02T11:11:23.56Z" }, 1131 + { url = "https://files.pythonhosted.org/packages/e9/ae/1aec72325a3c48f7776cc55a3bab8b168eb77aea821253da8b9f09713734/pybase64-1.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d9947b5e289e2c5b018ddc2aee2b9ed137b8aaaba7edfcb73623e576a2407740", size = 57682, upload-time = "2025-03-02T11:11:25.656Z" }, 1132 + { url = "https://files.pythonhosted.org/packages/4d/7a/7ad2799c0b3c4e2f7b993e1636468445c30870ca5485110b589b8921808d/pybase64-1.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ba4184ea43aa88a5ab8d6d15db284689765c7487ff3810764d8d823b545158e6", size = 56308, upload-time = "2025-03-02T11:11:26.803Z" }, 1133 + { url = "https://files.pythonhosted.org/packages/be/01/6008a4fbda0c4308dab00b95aedde8748032d7620bd95b686619c66917fe/pybase64-1.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4471257628785296efb2d50077fb9dfdbd4d2732c3487795224dd2644216fb07", size = 70784, upload-time = "2025-03-02T11:11:28.427Z" }, 1134 + { url = "https://files.pythonhosted.org/packages/27/31/913365a4f0e2922ec369ddaa3a1d6c11059acbe54531b003653efa007a48/pybase64-1.4.1-cp312-cp312-win32.whl", hash = "sha256:614561297ad14de315dd27381fd6ec3ea4de0d8206ba4c7678449afaff8a2009", size = 34271, upload-time = "2025-03-02T11:11:30.585Z" }, 1135 + { url = "https://files.pythonhosted.org/packages/d9/98/4d514d3e4c04819d80bccf9ea7b30d1cfc701832fa5ffca168f585004488/pybase64-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:35635db0d64fcbe9b3fad265314c052c47dc9bcef8dea17493ea8e3c15b2b972", size = 36496, upload-time = "2025-03-02T11:11:32.552Z" }, 1136 + { url = "https://files.pythonhosted.org/packages/c4/61/01353bc9c461e7b36d692daca3eee9616d8936ea6d8a64255ef7ec9ac307/pybase64-1.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:b4ccb438c4208ff41a260b70994c30a8631051f3b025cdca48be586b068b8f49", size = 29692, upload-time = "2025-03-02T11:11:33.735Z" }, 1137 + { url = "https://files.pythonhosted.org/packages/4b/1a/4e243ba702c07df3df3ba1795cfb02cf7a4242c53fc574b06a2bfa4f8478/pybase64-1.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1c38d9c4a7c132d45859af8d5364d3ce90975a42bd5995d18d174fb57621973", size = 38149, upload-time = "2025-03-02T11:11:35.537Z" }, 1138 + { url = "https://files.pythonhosted.org/packages/9c/35/3eae81bc8688a83f8b5bb84979d88e2cc3c3279a3b870a506f277d746c56/pybase64-1.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab0b93ea93cf1f56ca4727d678a9c0144c2653e9de4e93e789a92b4e098c07d9", size = 31485, upload-time = "2025-03-02T11:11:36.656Z" }, 1139 + { url = "https://files.pythonhosted.org/packages/48/55/d99b9ff8083573bbf97fc433bbc20e2efb612792025f3bad0868c96c37ce/pybase64-1.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:644f393e9bb7f3bacc5cbd3534d02e1b660b258fc8315ecae74d2e23265e5c1f", size = 59738, upload-time = "2025-03-02T11:11:38.468Z" }, 1140 + { url = "https://files.pythonhosted.org/packages/63/3c/051512b9e139a11585447b286ede5ac3b284ce5df85de37eb8cff57d90f8/pybase64-1.4.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff172a4dacbd964e5edcf1c2152dae157aabf856508aed15276f46d04a22128e", size = 56239, upload-time = "2025-03-02T11:11:39.718Z" }, 1141 + { url = "https://files.pythonhosted.org/packages/af/11/f40c5cca587274d50baee88540a7839576204cb425fe2f73a752ea48ae74/pybase64-1.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2ab7b4535abc72d40114540cae32c9e07d76ffba132bdd5d4fff5fe340c5801", size = 59137, upload-time = "2025-03-02T11:11:41.524Z" }, 1142 + { url = "https://files.pythonhosted.org/packages/1a/a9/ace9f6d0926962c083671d7df247de442ef63cd06bd134f7c8251aab5c51/pybase64-1.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da66eb7cfb641486944fb0b95ab138e691ab78503115022caf992b6c89b10396", size = 60109, upload-time = "2025-03-02T11:11:42.699Z" }, 1143 + { url = "https://files.pythonhosted.org/packages/88/9c/d4e308b4b4e3b513bc084fc71b4e2dd00d21d4cd245a9a28144d2f6b03c9/pybase64-1.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:678f573ea1d06183b32d0336044fb5db60396333599dffcce28ffa3b68319fc0", size = 68391, upload-time = "2025-03-02T11:11:43.898Z" }, 1144 + { url = "https://files.pythonhosted.org/packages/53/87/e184bf982a3272f1021f417e5a18fac406e042c606950e9082fc3b0cec30/pybase64-1.4.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bccdf340c2a1d3dd1f41528f192265ddce7f8df1ee4f7b5b9163cdba0fe0ccb", size = 71438, upload-time = "2025-03-02T11:11:45.112Z" }, 1145 + { url = "https://files.pythonhosted.org/packages/2f/7f/d6e6a72db055eb2dc01ab877d8ee39d05cb665403433ff922fb95d1003ad/pybase64-1.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ddf6366c34eb78931fd8a47c00cb886ba187a5ff8e6dbffe1d9dae4754b6c28", size = 58437, upload-time = "2025-03-02T11:11:47.034Z" }, 1146 + { url = "https://files.pythonhosted.org/packages/71/ef/c9051f2c0128194b861f3cd3b2d211b8d4d21ed2be354aa669fe29a059d8/pybase64-1.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:500afcb717a84e262c68f0baf9c56abaf97e2f058ba80c5546a9ed21ff4b705f", size = 52267, upload-time = "2025-03-02T11:11:48.448Z" }, 1147 + { url = "https://files.pythonhosted.org/packages/12/92/ae30a54eaa437989839c4f2404c1f004d7383c0f46d6ebb83546d587d2a7/pybase64-1.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d2de043312a1e7f15ee6d2b7d9e39ee6afe24f144e2248cce942b6be357b70d8", size = 68659, upload-time = "2025-03-02T11:11:49.615Z" }, 1148 + { url = "https://files.pythonhosted.org/packages/2b/65/d94788a35904f21694c4c581bcee2e165bec2408cc6fbed85a7fef5959ae/pybase64-1.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c36e214c25fb8dd4f3ecdaa0ff90073b793056e0065cc0a1e1e5525a6866a1ad", size = 57727, upload-time = "2025-03-02T11:11:50.843Z" }, 1149 + { url = "https://files.pythonhosted.org/packages/d0/97/8db416066b7917909c38346c03a8f3e6d4fc8a1dc98636408156514269ad/pybase64-1.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8ec003224f6e36e8e607a1bb8df182b367c87ca7135788ffe89173c7d5085005", size = 56302, upload-time = "2025-03-02T11:11:52.547Z" }, 1150 + { url = "https://files.pythonhosted.org/packages/70/0b/98f0601391befe0f19aa8cbda821c62d95056a94cc41d452fe893d205523/pybase64-1.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c536c6ed161e6fb19f6acd6074f29a4c78cb41c9155c841d56aec1a4d20d5894", size = 70779, upload-time = "2025-03-02T11:11:53.735Z" }, 1151 + { url = "https://files.pythonhosted.org/packages/cc/07/116119c5b20688c052697f677cf56f05aa766535ff7691aba38447d4a0d8/pybase64-1.4.1-cp313-cp313-win32.whl", hash = "sha256:1d34872e5aa2eff9dc54cedaf36038bbfbd5a3440fdf0bdc5b3c81c54ef151ea", size = 34266, upload-time = "2025-03-02T11:11:54.892Z" }, 1152 + { url = "https://files.pythonhosted.org/packages/c0/f5/a7eed9f3692209a9869a28bdd92deddf8cbffb06b40954f89f4577e5c96e/pybase64-1.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b7765515d7e0a48ddfde914dc2b1782234ac188ce3fab173b078a6e82ec7017", size = 36488, upload-time = "2025-03-02T11:11:56.063Z" }, 1153 + { url = "https://files.pythonhosted.org/packages/5d/8a/0d65c4dcda06487305035f24888ffed219897c03fb7834635d5d5e27dae1/pybase64-1.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:7fb782f3ceb30e24dc4d8d99c1221a381917bffaf85d29542f0f25b51829987c", size = 29690, upload-time = "2025-03-02T11:11:57.702Z" }, 1154 + { url = "https://files.pythonhosted.org/packages/a3/83/646d65fafe5e6edbdaf4c9548efb2e1dd7784caddbde3ff8a843dd942b0f/pybase64-1.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2a98d323e97444a38db38e022ccaf1d3e053b1942455790a93f29086c687855f", size = 38506, upload-time = "2025-03-02T11:11:58.936Z" }, 1155 + { url = "https://files.pythonhosted.org/packages/87/14/dbf7fbbe91d71c8044fefe20d22480ad64097e2ba424944de512550e12a4/pybase64-1.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19ef58d36b9b32024768fcedb024f32c05eb464128c75c07cac2b50c9ed47f4a", size = 31894, upload-time = "2025-03-02T11:12:00.762Z" }, 1156 + { url = "https://files.pythonhosted.org/packages/bd/5d/f8a47da2a5f8b599297b307d3bd0293adedc4e135be310620f061906070f/pybase64-1.4.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04fee0f5c174212868fde97b109db8fac8249b306a00ea323531ee61c7b0f398", size = 65212, upload-time = "2025-03-02T11:12:01.911Z" }, 1157 + { url = "https://files.pythonhosted.org/packages/90/95/ad9869c7cdcce3e8ada619dab5f9f2eff315ffb001704a3718c1597a2119/pybase64-1.4.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47737ff9eabc14b7553de6bc6395d67c5be80afcdbd25180285d13e089e40888", size = 60300, upload-time = "2025-03-02T11:12:03.071Z" }, 1158 + { url = "https://files.pythonhosted.org/packages/c2/91/4d8268b2488ae10c485cba04ecc23a5a7bdfb47ce9b876017b11ea0249a2/pybase64-1.4.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d8b5888cc239654fe68a0db196a18575ffc8b1c8c8f670c2971a44e3b7fe682", size = 63773, upload-time = "2025-03-02T11:12:04.231Z" }, 1159 + { url = "https://files.pythonhosted.org/packages/ae/1a/8afd27facc0723b1d69231da8c59a2343feb255f5db16f8b8765ddf1600b/pybase64-1.4.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a1af8d387dbce05944b65a618639918804b2d4438fed32bb7f06d9c90dbed01", size = 64684, upload-time = "2025-03-02T11:12:05.409Z" }, 1160 + { url = "https://files.pythonhosted.org/packages/cc/cd/422c74397210051125419fc8e425506ff27c04665459e18c8f7b037a754b/pybase64-1.4.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b0093c52bd099b80e422ad8cddf6f2c1ac1b09cb0922cca04891d736c2ad647", size = 72880, upload-time = "2025-03-02T11:12:06.652Z" }, 1161 + { url = "https://files.pythonhosted.org/packages/04/c1/c4f02f1d5f8e8a3d75715a3dd04196dde9e263e471470d099a26e91ebe2f/pybase64-1.4.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e54f9b2a1686f5bbdc4ac8440b6f6145d9699fd53aa30f347931f3063b0915", size = 75344, upload-time = "2025-03-02T11:12:07.816Z" }, 1162 + { url = "https://files.pythonhosted.org/packages/6e/0b/013006ca984f0472476cf7c0540db2e2b1f997d52977b15842a7681ab79c/pybase64-1.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3a0fdcf13f986c82f7ef04a1cd1163c70f39662d6f02aa4e7b448dacb966b39f", size = 63439, upload-time = "2025-03-02T11:12:09.669Z" }, 1163 + { url = "https://files.pythonhosted.org/packages/8a/d5/7848543b3c8dcc5396be574109acbe16706e6a9b4dbd9fc4e22f211668a9/pybase64-1.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac03f8eba72dd6da15dc25bb3e1b440ad21f5cb7ee2e6ffbbae4bd1b206bb503", size = 56004, upload-time = "2025-03-02T11:12:10.981Z" }, 1164 + { url = "https://files.pythonhosted.org/packages/63/58/70de1efb1b6f21d7aaea33578868214f82925d969e2091f7de3175a10092/pybase64-1.4.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea835272570aa811e08ae17612632b057623a9b27265d44288db666c02b438dc", size = 72460, upload-time = "2025-03-02T11:12:13.122Z" }, 1165 + { url = "https://files.pythonhosted.org/packages/90/0d/aa52dd1b1f25b98b1d94cc0522f864b03de55aa115de67cb6dbbddec4f46/pybase64-1.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8f52c4c29a35381f3ae06d520144a0707132f2cbfb53bc907b74811734bc4ef3", size = 62295, upload-time = "2025-03-02T11:12:15.004Z" }, 1166 + { url = "https://files.pythonhosted.org/packages/39/cf/4d378a330249c937676ee8eab7992ec700ade362f35db36c15922b33b1c8/pybase64-1.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fa5cdabcb4d21b7e56d0b2edd7ed6fa933ac3535be30c2a9cf0a2e270c5369c8", size = 60604, upload-time = "2025-03-02T11:12:16.23Z" }, 1167 + { url = "https://files.pythonhosted.org/packages/15/45/e3f23929018d0aada84246ddd398843050971af614da67450bb20f45f880/pybase64-1.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8db9acf239bb71a888748bc9ffc12c97c1079393a38bc180c0548330746ece94", size = 74500, upload-time = "2025-03-02T11:12:17.48Z" }, 1168 + { url = "https://files.pythonhosted.org/packages/8d/98/6d2adaec318cae6ee968a10df0a7e870f17ee385ef623bcb2ab63fa11b59/pybase64-1.4.1-cp313-cp313t-win32.whl", hash = "sha256:bc06186cfa9a43e871fdca47c1379bdf1cfe964bd94a47f0919a1ffab195b39e", size = 34543, upload-time = "2025-03-02T11:12:18.625Z" }, 1169 + { url = "https://files.pythonhosted.org/packages/8e/e7/1823de02d2c23324cf1142e9dce53b032085cee06c3f982806040f975ce7/pybase64-1.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:02c3647d270af1a3edd35e485bb7ccfe82180b8347c49e09973466165c03d7aa", size = 36909, upload-time = "2025-03-02T11:12:20.122Z" }, 1170 + { url = "https://files.pythonhosted.org/packages/43/6a/8ec0e4461bf89ef0499ef6c746b081f3520a1e710aeb58730bae693e0681/pybase64-1.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b3635e5873707906e72963c447a67969cfc6bac055432a57a91d7a4d5164fdf", size = 29961, upload-time = "2025-03-02T11:12:21.908Z" }, 1171 + ] 1172 + 1173 + [[package]] 1110 1174 name = "pycparser" 1111 1175 version = "2.22" 1112 1176 source = { registry = "https://pypi.org/simple" } ··· 1328 1392 sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } 1329 1393 wheels = [ 1330 1394 { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, 1395 + ] 1396 + 1397 + [[package]] 1398 + name = "pytest-sugar" 1399 + version = "1.0.0" 1400 + source = { registry = "https://pypi.org/simple" } 1401 + dependencies = [ 1402 + { name = "packaging" }, 1403 + { name = "pytest" }, 1404 + { name = "termcolor" }, 1405 + ] 1406 + sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } 1407 + wheels = [ 1408 + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, 1331 1409 ] 1332 1410 1333 1411 [[package]] ··· 1622 1700 ] 1623 1701 1624 1702 [[package]] 1703 + name = "termcolor" 1704 + version = "3.1.0" 1705 + source = { registry = "https://pypi.org/simple" } 1706 + sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } 1707 + wheels = [ 1708 + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, 1709 + ] 1710 + 1711 + [[package]] 1625 1712 name = "tokenizers" 1626 1713 version = "0.21.2" 1627 1714 source = { registry = "https://pypi.org/simple" } ··· 1656 1743 sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } 1657 1744 wheels = [ 1658 1745 { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, 1746 + ] 1747 + 1748 + [[package]] 1749 + name = "turbopuffer" 1750 + version = "0.5.13" 1751 + source = { registry = "https://pypi.org/simple" } 1752 + dependencies = [ 1753 + { name = "aiohttp" }, 1754 + { name = "anyio" }, 1755 + { name = "distro" }, 1756 + { name = "httpx" }, 1757 + { name = "pybase64" }, 1758 + { name = "pydantic" }, 1759 + { name = "sniffio" }, 1760 + { name = "typing-extensions" }, 1761 + ] 1762 + sdist = { url = "https://files.pythonhosted.org/packages/79/a2/59f6dbfcc43eb08c91bf77670ade5ca3ddc293c518db2b29703643799273/turbopuffer-0.5.13.tar.gz", hash = "sha256:e48ead6af4d493201ec6c9dfaaa6dca9bc96322f9a12f84d6866159a76eb6c27", size = 134367, upload-time = "2025-07-18T21:34:34.793Z" } 1763 + wheels = [ 1764 + { url = "https://files.pythonhosted.org/packages/35/fd/e27b0fc9b9bebf92dc24cb54ff3862aae2b6280d98704b8eff5e98e84ccd/turbopuffer-0.5.13-py3-none-any.whl", hash = "sha256:d48263aab236d697ab3321c00870ba1104cdddcd315d67f85d1bd150621e9ae8", size = 101727, upload-time = "2025-07-18T21:34:33.27Z" }, 1659 1765 ] 1660 1766 1661 1767 [[package]]