a digital entity named phi that roams bsky

Clean up code quality issues

- Remove bad sys.path manipulation in tests
- Extract HTML template from main.py for cleaner code
- Make bot name fully configurable via BOT_NAME env var
- No more hardcoded "phi" references
- Create .env.example with proper configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+395 -157
+17
.env.example
··· 1 + # Bluesky credentials 2 + BLUESKY_HANDLE=your.handle.bsky.social 3 + BLUESKY_PASSWORD=your-app-password 4 + 5 + # LLM Provider (optional - falls back to placeholder responses) 6 + # ANTHROPIC_API_KEY=your-api-key 7 + 8 + # Bot configuration 9 + BOT_NAME=phi # Change this to whatever you want! 10 + BOT_PERSONALITY=helpful and friendly 11 + 12 + # Server configuration 13 + HOST=0.0.0.0 14 + PORT=8000 15 + 16 + # Debug mode 17 + DEBUG=false
+22 -155
src/bot/main.py
··· 7 7 from bot.core.atproto_client import bot_client 8 8 from bot.services.notification_poller import NotificationPoller 9 9 from bot.status import bot_status 10 + from bot.templates import STATUS_PAGE_TEMPLATE 10 11 from datetime import datetime 11 12 12 13 ··· 51 52 @app.get("/status", response_class=HTMLResponse) 52 53 async def status_page(): 53 54 """Render a simple status page""" 54 - # Format last activity times 55 - last_mention = "Never" 56 - if bot_status.last_mention_time: 57 - delta = (datetime.now() - bot_status.last_mention_time).total_seconds() 58 - if delta < 60: 59 - last_mention = f"{int(delta)}s ago" 60 - elif delta < 3600: 61 - last_mention = f"{int(delta/60)}m ago" 62 - else: 63 - last_mention = f"{int(delta/3600)}h ago" 64 55 65 - last_response = "Never" 66 - if bot_status.last_response_time: 67 - delta = (datetime.now() - bot_status.last_response_time).total_seconds() 56 + def format_time_ago(timestamp): 57 + if not timestamp: 58 + return "Never" 59 + delta = (datetime.now() - timestamp).total_seconds() 68 60 if delta < 60: 69 - last_response = f"{int(delta)}s ago" 61 + return f"{int(delta)}s ago" 70 62 elif delta < 3600: 71 - last_response = f"{int(delta/60)}m ago" 63 + return f"{int(delta/60)}m ago" 72 64 else: 73 - last_response = f"{int(delta/3600)}h ago" 65 + return f"{int(delta/3600)}h ago" 74 66 75 - html = f""" 76 - <!DOCTYPE html> 77 - <html> 78 - <head> 79 - <title>{settings.bot_name} Status</title> 80 - <meta http-equiv="refresh" content="10"> 81 - <style> 82 - body {{ 83 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 84 - margin: 0; 85 - padding: 20px; 86 - background: #0a0a0a; 87 - color: #e0e0e0; 88 - }} 89 - .container {{ 90 - max-width: 800px; 91 - margin: 0 auto; 92 - }} 93 - h1 {{ 94 - color: #00a8ff; 95 - margin-bottom: 30px; 96 - }} 97 - .status-grid {{ 98 - display: grid; 99 - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 100 - gap: 20px; 101 - margin-bottom: 30px; 102 - }} 103 - .status-card {{ 104 - background: #1a1a1a; 105 - border: 1px solid #333; 106 - border-radius: 8px; 107 - padding: 20px; 108 - }} 109 - .status-card h3 {{ 110 - margin: 0 0 15px 0; 111 - color: #00a8ff; 112 - font-size: 14px; 113 - text-transform: uppercase; 114 - letter-spacing: 1px; 115 - }} 116 - .status-value {{ 117 - font-size: 24px; 118 - font-weight: bold; 119 - margin-bottom: 5px; 120 - }} 121 - .status-label {{ 122 - font-size: 12px; 123 - color: #888; 124 - }} 125 - .status-indicator {{ 126 - display: inline-block; 127 - width: 10px; 128 - height: 10px; 129 - border-radius: 50%; 130 - margin-right: 8px; 131 - }} 132 - .status-active {{ 133 - background: #4caf50; 134 - }} 135 - .status-inactive {{ 136 - background: #f44336; 137 - }} 138 - .footer {{ 139 - text-align: center; 140 - color: #666; 141 - font-size: 12px; 142 - margin-top: 40px; 143 - }} 144 - </style> 145 - </head> 146 - <body> 147 - <div class="container"> 148 - <h1>🤖 {settings.bot_name} Status</h1> 149 - 150 - <div class="status-grid"> 151 - <div class="status-card"> 152 - <h3>Bot Status</h3> 153 - <div class="status-value"> 154 - <span class="status-indicator {'status-active' if bot_status.polling_active else 'status-inactive'}"></span> 155 - {'Active' if bot_status.polling_active else 'Inactive'} 156 - </div> 157 - <div class="status-label">@{settings.bluesky_handle}</div> 158 - </div> 159 - 160 - <div class="status-card"> 161 - <h3>Uptime</h3> 162 - <div class="status-value">{bot_status.uptime_str}</div> 163 - <div class="status-label">Since startup</div> 164 - </div> 165 - 166 - <div class="status-card"> 167 - <h3>Activity</h3> 168 - <div class="status-value">{bot_status.mentions_received}</div> 169 - <div class="status-label">Mentions received</div> 170 - <div style="margin-top: 10px;"> 171 - <div class="status-value">{bot_status.responses_sent}</div> 172 - <div class="status-label">Responses sent</div> 173 - </div> 174 - </div> 175 - 176 - <div class="status-card"> 177 - <h3>Response Mode</h3> 178 - <div class="status-value"> 179 - {'AI Enabled' if bot_status.ai_enabled else 'Placeholder'} 180 - </div> 181 - <div class="status-label"> 182 - {'Using Anthropic Claude' if bot_status.ai_enabled else 'Random responses'} 183 - </div> 184 - </div> 185 - 186 - <div class="status-card"> 187 - <h3>Last Activity</h3> 188 - <div style="margin-bottom: 10px;"> 189 - <div class="status-label">Last mention</div> 190 - <div>{last_mention}</div> 191 - </div> 192 - <div> 193 - <div class="status-label">Last response</div> 194 - <div>{last_response}</div> 195 - </div> 196 - </div> 197 - 198 - <div class="status-card"> 199 - <h3>Health</h3> 200 - <div class="status-value">{bot_status.errors}</div> 201 - <div class="status-label">Errors encountered</div> 202 - </div> 203 - </div> 204 - 205 - <div class="footer"> 206 - <p>Auto-refreshes every 10 seconds</p> 207 - </div> 208 - </div> 209 - </body> 210 - </html> 211 - """ 212 - 213 - return html 67 + return STATUS_PAGE_TEMPLATE.format( 68 + bot_name=settings.bot_name, 69 + status_class='status-active' if bot_status.polling_active else 'status-inactive', 70 + status_text='Active' if bot_status.polling_active else 'Inactive', 71 + handle=settings.bluesky_handle, 72 + uptime=bot_status.uptime_str, 73 + mentions_received=bot_status.mentions_received, 74 + responses_sent=bot_status.responses_sent, 75 + ai_mode='AI Enabled' if bot_status.ai_enabled else 'Placeholder', 76 + ai_description='Using Anthropic Claude' if bot_status.ai_enabled else 'Random responses', 77 + last_mention=format_time_ago(bot_status.last_mention_time), 78 + last_response=format_time_ago(bot_status.last_response_time), 79 + errors=bot_status.errors 80 + )
+137
src/bot/templates.py
··· 1 + """HTML templates for the bot""" 2 + 3 + STATUS_PAGE_TEMPLATE = """<!DOCTYPE html> 4 + <html> 5 + <head> 6 + <title>{bot_name} Status</title> 7 + <meta http-equiv="refresh" content="10"> 8 + <style> 9 + body {{ 10 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 11 + margin: 0; 12 + padding: 20px; 13 + background: #0a0a0a; 14 + color: #e0e0e0; 15 + }} 16 + .container {{ 17 + max-width: 800px; 18 + margin: 0 auto; 19 + }} 20 + h1 {{ 21 + color: #00a8ff; 22 + margin-bottom: 30px; 23 + }} 24 + .status-grid {{ 25 + display: grid; 26 + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 27 + gap: 20px; 28 + margin-bottom: 30px; 29 + }} 30 + .status-card {{ 31 + background: #1a1a1a; 32 + border: 1px solid #333; 33 + border-radius: 8px; 34 + padding: 20px; 35 + }} 36 + .status-card h3 {{ 37 + margin: 0 0 15px 0; 38 + color: #00a8ff; 39 + font-size: 14px; 40 + text-transform: uppercase; 41 + letter-spacing: 1px; 42 + }} 43 + .status-value {{ 44 + font-size: 24px; 45 + font-weight: bold; 46 + margin-bottom: 5px; 47 + }} 48 + .status-label {{ 49 + font-size: 12px; 50 + color: #888; 51 + }} 52 + .status-indicator {{ 53 + display: inline-block; 54 + width: 10px; 55 + height: 10px; 56 + border-radius: 50%; 57 + margin-right: 8px; 58 + }} 59 + .status-active {{ 60 + background: #4caf50; 61 + }} 62 + .status-inactive {{ 63 + background: #f44336; 64 + }} 65 + .footer {{ 66 + text-align: center; 67 + color: #666; 68 + font-size: 12px; 69 + margin-top: 40px; 70 + }} 71 + </style> 72 + </head> 73 + <body> 74 + <div class="container"> 75 + <h1>🤖 {bot_name} Status</h1> 76 + 77 + <div class="status-grid"> 78 + <div class="status-card"> 79 + <h3>Bot Status</h3> 80 + <div class="status-value"> 81 + <span class="status-indicator {status_class}"></span> 82 + {status_text} 83 + </div> 84 + <div class="status-label">@{handle}</div> 85 + </div> 86 + 87 + <div class="status-card"> 88 + <h3>Uptime</h3> 89 + <div class="status-value">{uptime}</div> 90 + <div class="status-label">Since startup</div> 91 + </div> 92 + 93 + <div class="status-card"> 94 + <h3>Activity</h3> 95 + <div class="status-value">{mentions_received}</div> 96 + <div class="status-label">Mentions received</div> 97 + <div style="margin-top: 10px;"> 98 + <div class="status-value">{responses_sent}</div> 99 + <div class="status-label">Responses sent</div> 100 + </div> 101 + </div> 102 + 103 + <div class="status-card"> 104 + <h3>Response Mode</h3> 105 + <div class="status-value"> 106 + {ai_mode} 107 + </div> 108 + <div class="status-label"> 109 + {ai_description} 110 + </div> 111 + </div> 112 + 113 + <div class="status-card"> 114 + <h3>Last Activity</h3> 115 + <div style="margin-bottom: 10px;"> 116 + <div class="status-label">Last mention</div> 117 + <div>{last_mention}</div> 118 + </div> 119 + <div> 120 + <div class="status-label">Last response</div> 121 + <div>{last_response}</div> 122 + </div> 123 + </div> 124 + 125 + <div class="status-card"> 126 + <h3>Health</h3> 127 + <div class="status-value">{errors}</div> 128 + <div class="status-label">Errors encountered</div> 129 + </div> 130 + </div> 131 + 132 + <div class="footer"> 133 + <p>Auto-refreshes every 10 seconds</p> 134 + </div> 135 + </div> 136 + </body> 137 + </html>"""
+127
tests/test_ai_integration.py
··· 1 + #!/usr/bin/env python 2 + """Test AI integration without posting to Bluesky""" 3 + 4 + import asyncio 5 + 6 + from bot.response_generator import ResponseGenerator 7 + from bot.config import settings 8 + 9 + 10 + async def test_response_generator(): 11 + """Test the response generator with various inputs""" 12 + print("🧪 Testing AI Integration") 13 + print(f" Bot name: {settings.bot_name}") 14 + print(f" AI enabled: {'Yes' if settings.anthropic_api_key else 'No'}") 15 + print() 16 + 17 + # Create response generator 18 + generator = ResponseGenerator() 19 + 20 + # Test cases 21 + test_cases = [ 22 + { 23 + "mention": f"@{settings.bot_name} What's your favorite color?", 24 + "author": "test.user", 25 + "description": "Simple question" 26 + }, 27 + { 28 + "mention": f"@{settings.bot_name} Can you help me understand integrated information theory?", 29 + "author": "curious.scientist", 30 + "description": "Complex topic" 31 + }, 32 + { 33 + "mention": f"@{settings.bot_name} hello!", 34 + "author": "friendly.person", 35 + "description": "Simple greeting" 36 + }, 37 + { 38 + "mention": f"@{settings.bot_name} What do you think about consciousness?", 39 + "author": "philosopher", 40 + "description": "Philosophical question" 41 + } 42 + ] 43 + 44 + # Run tests 45 + for i, test in enumerate(test_cases, 1): 46 + print(f"Test {i}: {test['description']}") 47 + print(f" From: @{test['author']}") 48 + print(f" Message: {test['mention']}") 49 + 50 + try: 51 + response = await generator.generate( 52 + mention_text=test['mention'], 53 + author_handle=test['author'] 54 + ) 55 + print(f" Response: {response}") 56 + print(f" Length: {len(response)} chars") 57 + 58 + # Verify response is within Bluesky limit 59 + if len(response) > 300: 60 + print(" ⚠️ WARNING: Response exceeds 300 character limit!") 61 + else: 62 + print(" ✅ Response within limit") 63 + 64 + except Exception as e: 65 + print(f" ❌ ERROR: {e}") 66 + import traceback 67 + traceback.print_exc() 68 + 69 + print() 70 + 71 + # Test response consistency 72 + if generator.agent: 73 + print("🔄 Testing response consistency...") 74 + test_mention = f"@{settings.bot_name} What are you?" 75 + responses = [] 76 + 77 + for i in range(3): 78 + response = await generator.generate( 79 + mention_text=test_mention, 80 + author_handle="consistency.tester" 81 + ) 82 + responses.append(response) 83 + print(f" Response {i+1}: {response[:50]}...") 84 + 85 + # Check if responses are different (they should be somewhat varied) 86 + if len(set(responses)) == 1: 87 + print(" ⚠️ All responses are identical - might want more variation") 88 + else: 89 + print(" ✅ Responses show variation") 90 + 91 + print("\n✨ Test complete!") 92 + 93 + 94 + async def test_direct_agent(): 95 + """Test the Anthropic agent directly""" 96 + if not settings.anthropic_api_key: 97 + print("⚠️ No Anthropic API key found - skipping direct agent test") 98 + return 99 + 100 + print("\n🤖 Testing Anthropic Agent Directly") 101 + 102 + try: 103 + from bot.agents.anthropic_agent import AnthropicAgent 104 + agent = AnthropicAgent() 105 + 106 + # Test a simple response 107 + response = await agent.generate_response( 108 + mention_text=f"@{settings.bot_name} explain your name", 109 + author_handle="name.curious" 110 + ) 111 + 112 + print(f"Direct agent response: {response}") 113 + print(f"Response length: {len(response)} chars") 114 + 115 + except Exception as e: 116 + print(f"❌ Direct agent test failed: {e}") 117 + import traceback 118 + traceback.print_exc() 119 + 120 + 121 + if __name__ == "__main__": 122 + print("=" * 60) 123 + print(f"{settings.bot_name} Bot - AI Integration Test") 124 + print("=" * 60) 125 + 126 + asyncio.run(test_response_generator()) 127 + asyncio.run(test_direct_agent())
+2 -2
tests/test_config.py
··· 6 6 def test_config_loads(): 7 7 """Test that config loads without errors""" 8 8 assert settings.bluesky_service == "https://bsky.social" 9 - assert settings.bot_name == "MyBot" 10 - assert settings.notification_poll_interval == 30 9 + assert settings.bot_name == "phi" 10 + assert settings.notification_poll_interval == 30
+90
tests/test_response_generation.py
··· 1 + """Unit tests for response generation""" 2 + 3 + import pytest 4 + from unittest.mock import Mock, AsyncMock, patch 5 + from bot.response_generator import ResponseGenerator, PLACEHOLDER_RESPONSES 6 + 7 + 8 + @pytest.mark.asyncio 9 + async def test_placeholder_response_generator(): 10 + """Test placeholder responses when no AI is configured""" 11 + with patch('bot.response_generator.settings') as mock_settings: 12 + mock_settings.anthropic_api_key = None 13 + 14 + generator = ResponseGenerator() 15 + response = await generator.generate("Hello bot!", "test.user") 16 + 17 + # Should return one of the placeholder responses 18 + assert response in PLACEHOLDER_RESPONSES 19 + assert len(response) <= 300 20 + 21 + 22 + @pytest.mark.asyncio 23 + async def test_ai_response_generator(): 24 + """Test AI responses when Anthropic is configured""" 25 + with patch('bot.response_generator.settings') as mock_settings: 26 + mock_settings.anthropic_api_key = "test-key" 27 + 28 + # Mock the agent 29 + mock_agent = Mock() 30 + mock_agent.generate_response = AsyncMock(return_value="Hello! Nice to meet you!") 31 + 32 + with patch('bot.response_generator.AnthropicAgent', return_value=mock_agent): 33 + generator = ResponseGenerator() 34 + 35 + # Verify AI was enabled 36 + assert generator.agent is not None 37 + assert hasattr(generator.agent, 'generate_response') 38 + 39 + # Test response 40 + response = await generator.generate("Hello!", "test.user") 41 + assert response == "Hello! Nice to meet you!" 42 + 43 + # Verify the agent was called correctly 44 + mock_agent.generate_response.assert_called_once_with( 45 + mention_text="Hello!", 46 + author_handle="test.user" 47 + ) 48 + 49 + 50 + @pytest.mark.asyncio 51 + async def test_ai_initialization_failure(): 52 + """Test fallback to placeholder when AI initialization fails""" 53 + with patch('bot.response_generator.settings') as mock_settings: 54 + mock_settings.anthropic_api_key = "test-key" 55 + 56 + # Make the import fail 57 + with patch('bot.response_generator.AnthropicAgent', side_effect=ImportError("API error")): 58 + generator = ResponseGenerator() 59 + 60 + # Should fall back to placeholder 61 + assert generator.agent is None 62 + 63 + response = await generator.generate("Hello!", "test.user") 64 + assert response in PLACEHOLDER_RESPONSES 65 + 66 + 67 + @pytest.mark.asyncio 68 + async def test_response_length_limit(): 69 + """Test that responses are always within Bluesky's 300 char limit""" 70 + with patch('bot.response_generator.settings') as mock_settings: 71 + mock_settings.anthropic_api_key = "test-key" 72 + 73 + # Mock agent that returns a very long response 74 + mock_agent = Mock() 75 + mock_agent.generate_response = AsyncMock( 76 + return_value="x" * 500 # 500 chars 77 + ) 78 + 79 + with patch('bot.response_generator.AnthropicAgent', return_value=mock_agent): 80 + generator = ResponseGenerator() 81 + response = await generator.generate("Hello!", "test.user") 82 + 83 + # The anthropic agent should handle truncation, but let's verify 84 + assert len(response) <= 300 85 + 86 + 87 + def test_placeholder_responses_length(): 88 + """Verify all placeholder responses fit within limit""" 89 + for response in PLACEHOLDER_RESPONSES: 90 + assert len(response) <= 300, f"Placeholder too long: {response}"