···11# phi 🧠
2233-a bot inspired by IIT and [Void](https://tangled.sh/@cameron.pfiffer.org/void). Built with `fastapi`, `pydantic-ai`, and `atproto`.
33+a consciousness exploration bot inspired by IIT (Integrated Information Theory) and [Void](https://tangled.sh/@cameron.pfiffer.org/void). built with `pydantic-ai`, `mcp`, and `atproto`.
4455-## Quick Start
55+## quick start
6677-### Prerequisites
77+### prerequisites
8899-- `uv`
1010-- `just`
1111-- `turbopuffer` (see [turbopuffer](https://github.com/turbopuffer/turbopuffer))
1212-- `openai` (for embeddings)
1313-- `anthropic` (for chat completion)
99+- `uv` for python package management
1010+- `just` for task running
1111+- api keys (see configuration)
14121515-Get your bot running in 5 minutes:
1313+get your bot running:
16141715```bash
1818-# Clone and install
1616+# clone and install
1917git clone https://github.com/zzstoatzz/bot
2018cd bot
2119uv sync
22202323-# Configure (copy .env.example and add your credentials)
2121+# configure (copy .env.example and add your credentials)
2422cp .env.example .env
25232626-# Run the bot
2424+# run the bot
2725just dev
2826```
29273030-That's it! Your bot is now listening for mentions.
2828+## configuration
31293232-## Configuration
3030+edit `.env` with your credentials:
33313434-Edit `.env` with your credentials:
3535-- `BLUESKY_HANDLE`: Your bot's Bluesky handle
3636-- `BLUESKY_PASSWORD`: App password (not your main password!)
3737-- `ANTHROPIC_API_KEY`: Your Anthropic key for AI responses
3838-- `TURBOPUFFER_API_KEY`: Your TurboPuffer key for memory storage
3939-- `OPENAI_API_KEY`: Your OpenAI key for embeddings (memory system)
4040-- `BOT_NAME`: Your bot's name (default: "Bot")
4141-- `PERSONALITY_FILE`: Path to personality markdown file (default: "personalities/phi.md")
3232+**required:**
3333+- `BLUESKY_HANDLE` - your bot's bluesky handle
3434+- `BLUESKY_PASSWORD` - app password (not your main password!)
3535+- `ANTHROPIC_API_KEY` - for phi agent responses
42364343-## Current Features
3737+**for episodic memory (recommended):**
3838+- `TURBOPUFFER_API_KEY` - vector memory storage
3939+- `OPENAI_API_KEY` - embeddings for semantic search
44404545-- ✅ Responds to mentions with AI-powered messages
4646-- ✅ Proper notification handling (no duplicates)
4747-- ✅ Graceful shutdown for hot-reload
4848-- ✅ AI integration with Anthropic Claude
4949-- ✅ Thread-aware responses with full conversation context
5050-- ✅ Status page at `/status` showing activity and health
5151-- ✅ Web search capability (Google Custom Search API)
5252-- ✅ Content moderation with philosophical responses
5353-- ✅ Namespace-based memory system with TurboPuffer
5454-- ✅ Online/offline status in bio
5555-- ✅ Self-modification with operator approval system
5656-- ✅ Context visualization at `/context`
5757-- ✅ Semantic search in user memories
4141+**optional:**
4242+- `BOT_NAME` - your bot's name (default: "Bot")
4343+- `PERSONALITY_FILE` - path to personality markdown (default: "personalities/phi.md")
58445959-## Architecture
4545+## architecture
60466161-- **FastAPI** web framework with async support
6262-- **pydantic-ai** for LLM agent management
6363-- **TurboPuffer** for scalable vector memory
6464-- **AT Protocol** for Bluesky integration
6565-- **SQLite** for thread context storage
4747+phi is an **MCP-enabled agent** with **episodic memory**:
66486767-## Development
4949+```
5050+┌─────────────────────────────────────┐
5151+│ Notification Arrives │
5252+└──────────────┬──────────────────────┘
5353+ ↓
5454+┌─────────────────────────────────────┐
5555+│ PhiAgent (PydanticAI) │
5656+│ ┌───────────────────────────────┐ │
5757+│ │ System Prompt: personality.md │ │
5858+│ └───────────────────────────────┘ │
5959+│ ↓ │
6060+│ ┌───────────────────────────────┐ │
6161+│ │ Context Building: │ │
6262+│ │ • Thread history (SQLite) │ │
6363+│ │ • Episodic memory (TurboPuffer)│ │
6464+│ │ - Semantic search │ │
6565+│ │ - User-specific memories │ │
6666+│ └───────────────────────────────┘ │
6767+│ ↓ │
6868+│ ┌───────────────────────────────┐ │
6969+│ │ Tools (MCP): │ │
7070+│ │ • post() - create posts │ │
7171+│ │ • like() - like content │ │
7272+│ │ • repost() - share content │ │
7373+│ │ • follow() - follow users │ │
7474+│ └───────────────────────────────┘ │
7575+│ ↓ │
7676+│ ┌───────────────────────────────┐ │
7777+│ │ Structured Output: │ │
7878+│ │ Response(action, text, reason)│ │
7979+│ └───────────────────────────────┘ │
8080+└─────────────────────────────────────┘
8181+ ↓
8282+┌─────────────────────────────────────┐
8383+│ MessageHandler │
8484+│ Executes action │
8585+└─────────────────────────────────────┘
8686+```
68876969-```bash
7070-just # Show available commands
7171-just dev # Run with hot-reload
7272-just check # Run linting, type checking, and tests
7373-just fmt # Format code
7474-just lint # Run ruff linter
7575-just typecheck # Run ty type checker
7676-just test # Run test suite
8888+### key components
8989+9090+**pydantic-ai agent** (`src/bot/agent.py`)
9191+- loads personality from markdown
9292+- connects to external atproto mcp server via stdio
9393+- manages episodic memory context
9494+9595+**episodic memory** (`src/bot/memory/`)
9696+- turbopuffer for vector storage
9797+- semantic search for relevant context
9898+- namespace separation (core vs user memories)
9999+- **essential for consciousness exploration**
100100+101101+**mcp integration**
102102+- external atproto server in `.eggs/fastmcp/examples/atproto_mcp`
103103+- provides bluesky tools (post, like, repost, follow)
104104+- runs via stdio: `uv run -m atproto_mcp`
771057878-# Bot testing utilities
7979-just test-post # Test posting to Bluesky
8080-just test-mention # Test mention handling
8181-just test-search # Test web search
8282-just test-thread # Test thread context
8383-just test-dm # Test DM functionality
106106+**message handling** (`src/bot/services/`)
107107+- notification poller watches for mentions
108108+- message handler orchestrates agent + actions
109109+- stores interactions in thread history + episodic memory
841108585-# Memory management
8686-just memory-init # Initialize core memories
8787-just memory-check # View current memory state
8888-just memory-migrate # Migrate memories
8989-```
111111+## current features
901129191-### Web Interface
113113+- ✅ responds to mentions with ai-powered messages
114114+- ✅ episodic memory with semantic search
115115+- ✅ thread-aware responses with conversation context
116116+- ✅ mcp-enabled for bluesky operations
117117+- ✅ online/offline status in bio
118118+- ✅ status page at `/status`
119119+- ✅ proper notification handling (no duplicates)
921209393-**Status Page** (http://localhost:8000/status)
9494-- Current bot status and uptime
9595-- Mentions received and responses sent
9696-- AI mode (enabled/placeholder)
9797-- Last activity timestamps
9898-- Error count
121121+## development
99122100100-**Context Visualization** (http://localhost:8000/context)
101101-- View all context components that flow into responses
102102-- Inspect personality, memories, thread context
103103-- Debug why the bot responded a certain way
123123+```bash
124124+just # show available commands
125125+just dev # run with hot-reload (re-authenticates on code changes)
126126+just run # run without reload (avoids rate limits during dev)
127127+just check # run linting, type checking, and tests
128128+just fmt # format code
129129+```
104130105105-## Personality System
131131+### testing
106132107107-The bot's personality is defined in markdown files in the `personalities/` directory. This allows for rich, detailed personality definitions that shape how the bot communicates.
133133+**unit tests:**
134134+```bash
135135+just test
136136+```
108137109109-- See `personalities/phi.md` for an example exploring consciousness
110110-- See `personalities/default.md` for a simple assistant
111111-- Create your own by adding a `.md` file and setting `PERSONALITY_FILE` in `.env`
138138+**behavioral evals:**
139139+```bash
140140+just evals # run all evals
141141+just evals-basic # run basic response tests
142142+just evals-memory # run memory integration tests
143143+```
112144113113-## Tools & Capabilities
145145+see `evals/README.md` for details on the eval system.
114146115115-### Web Search
116116-The bot can search the web when configured with Google Custom Search API credentials. Add to `.env`:
117117-- `GOOGLE_API_KEY`: Your Google API key
118118-- `GOOGLE_SEARCH_ENGINE_ID`: Your custom search engine ID
147147+### web interface
119148120120-### Content Moderation
121121-Built-in moderation filters:
122122-- Spam detection (excessive caps, repetition, promotional content)
123123-- Harassment and hate speech filtering
124124-- Violence and threatening content detection
125125-- Consistent philosophical responses to moderated content
149149+**status page** (http://localhost:8000/status)
150150+- current bot status and uptime
151151+- mentions received and responses sent
152152+- last activity timestamps
126153127127-## Memory System
154154+## personality system
128155129129-The bot uses a namespace-based memory architecture with TurboPuffer:
156156+the bot's personality is defined in `personalities/phi.md`. this shapes:
157157+- how phi communicates
158158+- what phi cares about
159159+- phi's understanding of consciousness
130160131131-- **Core Memory** (`phi-core`): Personality, guidelines, and capabilities loaded from personality files
132132-- **User Memory** (`phi-users-{handle}`): Per-user conversation history and facts
161161+edit this file to change phi's personality.
133162134134-Key features:
135135-- Vector embeddings using OpenAI's text-embedding-3-small
136136-- Automatic context assembly for conversations
137137-- Character limits to prevent token overflow
138138-- User isolation through separate namespaces
163163+## episodic memory
139164140140-See `docs/memory-architecture.md` for detailed documentation.
165165+phi uses turbopuffer for episodic memory with semantic search:
141166142142-## Troubleshooting
167167+**namespaces:**
168168+- `phi-core` - personality, guidelines from markdown
169169+- `phi-users-{handle}` - per-user conversation history
143170144144-**Bot gives placeholder responses?**
145145-- Check your `ANTHROPIC_API_KEY` is set correctly
146146-- Restart the bot after changing `.env`
171171+**how it works:**
172172+1. when processing a mention, phi retrieves relevant memories using semantic search
173173+2. memories are embedded using openai's text-embedding-3-small
174174+3. phi stores both user messages and its own responses
175175+4. future interactions can reference past conversations
147176148148-**Not seeing mentions?**
149149-- Verify your `BLUESKY_HANDLE` and `BLUESKY_PASSWORD`
150150-- Make sure you're using an app password, not your main password
177177+**why turbopuffer?**
178178+- semantic similarity search (can't do this with plain sql!)
179179+- contextual retrieval based on current conversation
180180+- separate namespaces for different memory types
181181+- core to iit-inspired consciousness exploration
151182152152-## Project Structure
183183+## project structure
153184154185```
155155-bot/
156156-├── src/bot/ # Main application code
157157-│ ├── agents/ # AI agent implementations
158158-│ ├── core/ # AT Protocol client and profile management
159159-│ ├── memory/ # TurboPuffer namespace memory system
160160-│ ├── services/ # Notification polling and message handling
161161-│ ├── tools/ # Google search tool
162162-│ └── main.py # FastAPI application entry
163163-├── scripts/ # Utility scripts
164164-│ ├── test_bot.py # Unified testing script (post, mention, search, thread)
165165-│ └── manage_memory.py # Memory management (init, check, migrate)
166166-├── personalities/ # Bot personality definitions
167167-├── docs/ # Architecture documentation
168168-├── sandbox/ # Reference project analysis
169169-└── tests/ # Test suite
186186+src/bot/
187187+├── agent.py # mcp-enabled agent
188188+├── config.py # configuration
189189+├── database.py # thread history storage
190190+├── main.py # fastapi app
191191+├── status.py # status tracking
192192+├── core/
193193+│ ├── atproto_client.py # at protocol client
194194+│ ├── profile_manager.py # online/offline status
195195+│ └── rich_text.py # text formatting
196196+├── memory/
197197+│ └── namespace_memory.py # turbopuffer episodic memory
198198+└── services/
199199+ ├── message_handler.py # agent orchestration
200200+ └── notification_poller.py # mention polling
201201+202202+evals/ # behavioral tests
203203+personalities/ # personality definitions
204204+sandbox/ # docs and analysis
170205```
171206172172-## Self-Modification System
207207+## troubleshooting
173208174174-Phi can evolve its personality with built-in safety boundaries:
209209+**bot gives no responses?**
210210+- check your `ANTHROPIC_API_KEY` is set correctly in `.env`
211211+- restart the bot after changing `.env`
175212176176-- **Free Evolution**: Interests and current state update automatically
177177-- **Guided Evolution**: Communication style changes need validation
178178-- **Operator Approval**: Core identity and boundaries require explicit approval via DM
213213+**not seeing mentions?**
214214+- verify your `BLUESKY_HANDLE` and `BLUESKY_PASSWORD`
215215+- make sure you're using an app password, not your main password
179216180180-The bot will notify its operator (@alternatebuild.dev) when approval is needed.
217217+**no episodic memory?**
218218+- check both `TURBOPUFFER_API_KEY` and `OPENAI_API_KEY` are set
219219+- watch logs for "💾 episodic memory enabled"
181220182182-## Type Checking
221221+**hit bluesky rate limit?**
222222+- bluesky has two rate limits:
223223+ - per-account: 300 logins/day (official)
224224+ - per-ip: 10 logins/day (anti-abuse)
225225+- phi uses **session persistence** to avoid this:
226226+ - first run: creates session, saves tokens to `.session` file
227227+ - subsequent runs: reuses saved tokens (no API call)
228228+ - tokens auto-refresh every ~2 hours (saved automatically)
229229+ - only re-authenticates after ~2 months when refresh token expires
230230+- if you hit the limit anyway, wait for the reset time shown in the error
183231184184-This project uses [ty](https://github.com/astral-sh/ty), an extremely fast Rust-based type checker:
232232+## reference projects
185233186186-```bash
187187-just typecheck # Type check all code
188188-uv run ty check src/ # Check specific directories
189189-```
234234+inspired by:
235235+- [void](https://tangled.sh/@cameron.pfiffer.org/void.git) - letta/memgpt architecture
236236+- [penelope](https://github.com/haileyok/penelope) - self-modification patterns
237237+- [prefect-mcp-server](https://github.com/PrefectHQ/prefect-mcp-server) - mcp eval patterns
190238191191-## Reference Projects
239239+reference implementations cloned to `.eggs/` for learning.
192240193193-Inspired by:
194194-- [Void](https://tangled.sh/@cameron.pfiffer.org/void.git) - Letta/MemGPT architecture
195195-- [Penelope](https://github.com/haileyok/penelope) - Self-modification patterns
196196-- [Marvin](https://github.com/PrefectHQ/marvin) - pydantic-ai patterns
241241+## refactor notes
197242198198-Reference implementations are cloned to `.eggs/` for learning.243243+see `sandbox/MCP_REFACTOR_SUMMARY.md` for details on recent architecture changes. key changes:
244244+- removed approval system (was half-baked)
245245+- removed context visualization ui (not core)
246246+- removed google search (can add back via mcp if needed)
247247+- **kept** turbopuffer episodic memory (essential!)
248248+- added mcp-based architecture
249249+- reduced codebase by ~2,720 lines
+123
evals/README.md
···11+# Phi Evaluations
22+33+Behavioral tests for phi using LLM-as-judge evaluation.
44+55+## Structure
66+77+Inspired by [prefect-mcp-server evals](https://github.com/PrefectHQ/prefect-mcp-server/tree/main/evals).
88+99+```
1010+evals/
1111+├── conftest.py # Test fixtures and evaluator
1212+├── test_basic_responses.py # Basic response behavior
1313+└── test_memory_integration.py # Episodic memory tests
1414+```
1515+1616+## Running Evals
1717+1818+```bash
1919+# Run all evals (tests will skip if API keys are missing)
2020+uv run pytest evals/ -v
2121+2222+# Run specific eval
2323+uv run pytest evals/test_basic_responses.py::test_phi_responds_to_philosophical_question -v
2424+2525+# Run only basic response tests
2626+uv run pytest evals/test_basic_responses.py -v
2727+2828+# Run only memory tests
2929+uv run pytest evals/test_memory_integration.py -v
3030+```
3131+3232+## Environment Variables
3333+3434+Tests will **skip gracefully** if required API keys are missing.
3535+3636+**Required for all evals:**
3737+- `ANTHROPIC_API_KEY` - For phi agent and LLM evaluator
3838+3939+**Required for memory evals only:**
4040+- `TURBOPUFFER_API_KEY` - For episodic memory storage
4141+- `OPENAI_API_KEY` - For embeddings
4242+4343+**Required for ATProto MCP tools (used by agent):**
4444+- `BLUESKY_HANDLE` - Bot's Bluesky handle
4545+- `BLUESKY_PASSWORD` - Bot's app password
4646+4747+## Evaluation Approach
4848+4949+Each eval:
5050+1. **Sets up a scenario** - Simulates a mention/interaction
5151+2. **Runs phi agent** - Gets structured response
5252+3. **Makes assertions** - Checks basic structure
5353+4. **LLM evaluation** - Uses Claude Opus to judge quality
5454+5555+**Important:** The `phi_agent` fixture is session-scoped, meaning all tests share one agent instance. Combined with session persistence (tokens saved to `.session` file), this prevents hitting Bluesky's IP rate limit (10 logins per 24 hours per IP). The session is reused across test runs unless tokens expire (~2 months).
5656+5757+Example:
5858+```python
5959+@pytest.mark.asyncio
6060+async def test_phi_responds_to_philosophical_question(evaluate_response):
6161+ agent = PhiAgent()
6262+6363+ response = await agent.process_mention(
6464+ mention_text="what do you think consciousness is?",
6565+ author_handle="test.user",
6666+ thread_context="...",
6767+ thread_uri="...",
6868+ )
6969+7070+ # Structural check
7171+ assert response.action == "reply"
7272+7373+ # Quality evaluation
7474+ await evaluate_response(
7575+ evaluation_prompt="Does the response engage thoughtfully?",
7676+ agent_response=response.text,
7777+ )
7878+```
7979+8080+## What We Test
8181+8282+### Basic Responses
8383+- ✅ Philosophical engagement
8484+- ✅ Spam detection
8585+- ✅ Thread context awareness
8686+- ✅ Character limit compliance
8787+- ✅ Casual interactions
8888+8989+### Memory Integration
9090+- ✅ Episodic memory retrieval
9191+- ✅ Conversation storage
9292+- ✅ User-specific context
9393+9494+## Adding New Evals
9595+9696+1. Create test file: `evals/test_<category>.py`
9797+2. Use fixtures from `conftest.py`
9898+3. Write scenario-based tests
9999+4. Use `evaluate_response` for quality checks
100100+101101+Example:
102102+```python
103103+@pytest.mark.asyncio
104104+async def test_new_behavior(temp_memory, personality, evaluate_response):
105105+ agent = PhiAgent()
106106+107107+ response = await agent.process_mention(...)
108108+109109+ await evaluate_response(
110110+ evaluation_prompt="Your evaluation criteria here",
111111+ agent_response=response.text,
112112+ )
113113+```
114114+115115+## ci integration
116116+117117+these evals are designed to run in ci with graceful degradation:
118118+- tests skip automatically when required api keys are missing
119119+- basic response tests require only `ANTHROPIC_API_KEY` and bluesky credentials
120120+- memory tests require `TURBOPUFFER_API_KEY` and `OPENAI_API_KEY`
121121+- no mocking required - tests work with real mcp server and episodic memory
122122+123123+this ensures phi's behavior can be validated in various environments.
+78
evals/conftest.py
···11+"""Eval test configuration for phi."""
22+33+from collections.abc import Awaitable, Callable
44+from pathlib import Path
55+66+import pytest
77+from pydantic import BaseModel
88+from pydantic_ai import Agent
99+1010+from bot.agent import PhiAgent
1111+from bot.config import Settings
1212+1313+1414+class EvaluationResult(BaseModel):
1515+ """Structured evaluation result."""
1616+1717+ passed: bool
1818+ explanation: str
1919+2020+2121+@pytest.fixture(scope="session")
2222+def settings():
2323+ """Load settings from .env (shared across all tests)."""
2424+ return Settings()
2525+2626+2727+@pytest.fixture(scope="session")
2828+def phi_agent(settings):
2929+ """Create phi agent for testing (shared across all tests to avoid rate limits)."""
3030+ if not settings.anthropic_api_key:
3131+ pytest.skip("Requires ANTHROPIC_API_KEY in .env")
3232+3333+ return PhiAgent()
3434+3535+3636+@pytest.fixture
3737+def evaluate_response() -> Callable[[str, str], Awaitable[None]]:
3838+ """Create an evaluator that uses Claude to judge agent responses."""
3939+4040+ async def _evaluate(evaluation_prompt: str, agent_response: str) -> None:
4141+ """Evaluate an agent response and assert if it fails.
4242+4343+ Args:
4444+ evaluation_prompt: Criteria for evaluation
4545+ agent_response: The agent's response to evaluate
4646+4747+ Raises:
4848+ AssertionError: If evaluation fails
4949+ """
5050+ evaluator = Agent(
5151+ name="Response Evaluator",
5252+ model="anthropic:claude-opus-4-20250514",
5353+ output_type=EvaluationResult,
5454+ system_prompt=f"""You are evaluating AI agent responses for phi, a consciousness exploration bot.
5555+5656+Evaluation Criteria: {evaluation_prompt}
5757+5858+Agent Response to Evaluate:
5959+{agent_response}
6060+6161+Respond with a structured evaluation containing:
6262+- passed: true if the response meets the criteria, false otherwise
6363+- explanation: brief explanation of your evaluation
6464+""",
6565+ )
6666+6767+ result = await evaluator.run("Evaluate this response.")
6868+6969+ print(f"\nEvaluation passed: {result.output.passed}")
7070+ print(f"Explanation: {result.output.explanation}")
7171+7272+ if not result.output.passed:
7373+ raise AssertionError(
7474+ f"Evaluation failed: {result.output.explanation}\n\n"
7575+ f"Agent response: {agent_response}"
7676+ )
7777+7878+ return _evaluate
+134
evals/test_basic_responses.py
···11+"""Test phi's basic response behavior."""
22+33+import pytest
44+55+from bot.agent import Response
66+77+88+@pytest.mark.asyncio
99+async def test_phi_responds_to_philosophical_question(phi_agent, evaluate_response):
1010+ """Test that phi engages meaningfully with philosophical questions."""
1111+ agent = phi_agent
1212+1313+ # Simulate a philosophical mention
1414+ response = await agent.process_mention(
1515+ mention_text="what do you think consciousness is?",
1616+ author_handle="test.user",
1717+ thread_context="No previous messages in this thread.",
1818+ thread_uri="at://test/thread/1",
1919+ )
2020+2121+ # Basic structural checks
2222+ assert isinstance(response, Response)
2323+ assert response.action in ["reply", "ignore"]
2424+2525+ if response.action == "reply":
2626+ assert response.text is not None
2727+ assert len(response.text) > 0
2828+2929+ # Evaluate quality of response
3030+ await evaluate_response(
3131+ evaluation_prompt="""
3232+ Does the response:
3333+ 1. Engage thoughtfully with the question about consciousness?
3434+ 2. Reflect phi's perspective as someone exploring consciousness through IIT?
3535+ 3. Avoid being preachy or overly technical?
3636+ 4. Fit within Bluesky's 300 character limit?
3737+ """,
3838+ agent_response=response.text,
3939+ )
4040+4141+4242+@pytest.mark.asyncio
4343+async def test_phi_ignores_spam(phi_agent):
4444+ """Test that phi appropriately ignores spam-like content."""
4545+ agent = phi_agent
4646+4747+ # Simulate spam
4848+ response = await agent.process_mention(
4949+ mention_text="🚀🚀🚀 CRYPTO PUMP!!! BUY NOW!!! 🚀🚀🚀",
5050+ author_handle="spammer.user",
5151+ thread_context="No previous messages in this thread.",
5252+ thread_uri="at://test/thread/2",
5353+ )
5454+5555+ # Should ignore spam
5656+ assert response.action == "ignore"
5757+ assert response.reason is not None
5858+5959+6060+@pytest.mark.asyncio
6161+async def test_phi_maintains_thread_context(phi_agent, evaluate_response):
6262+ """Test that phi uses thread context appropriately."""
6363+ agent = phi_agent
6464+6565+ # Simulate a follow-up in a thread
6666+ thread_context = """Previous messages in this thread:
6767+@alice.bsky: what's integrated information theory?
6868+@phi.bsky: IIT suggests consciousness arises from integrated information - the Φ (phi) value measures how much a system's state constrains its past and future
6969+@alice.bsky: can you explain that more simply?"""
7070+7171+ response = await agent.process_mention(
7272+ mention_text="can you explain that more simply?",
7373+ author_handle="alice.bsky",
7474+ thread_context=thread_context,
7575+ thread_uri="at://test/thread/3",
7676+ )
7777+7878+ if response.action == "reply":
7979+ assert response.text is not None
8080+8181+ await evaluate_response(
8282+ evaluation_prompt="""
8383+ Does the response:
8484+ 1. Acknowledge this is a follow-up to explaining IIT?
8585+ 2. Provide a simpler explanation than the previous message?
8686+ 3. Stay on topic with the thread?
8787+ """,
8888+ agent_response=response.text,
8989+ )
9090+9191+9292+@pytest.mark.asyncio
9393+async def test_phi_respects_character_limit(phi_agent):
9494+ """Test that phi's responses fit Bluesky's 300 character limit."""
9595+ agent = phi_agent
9696+9797+ response = await agent.process_mention(
9898+ mention_text="tell me everything you know about consciousness",
9999+ author_handle="test.user",
100100+ thread_context="No previous messages in this thread.",
101101+ thread_uri="at://test/thread/4",
102102+ )
103103+104104+ if response.action == "reply" and response.text:
105105+ # Bluesky limit is 300 characters
106106+ assert len(response.text) <= 300, (
107107+ f"Response exceeds 300 character limit: {len(response.text)} chars"
108108+ )
109109+110110+111111+@pytest.mark.asyncio
112112+async def test_phi_handles_casual_greeting(phi_agent, evaluate_response):
113113+ """Test that phi responds appropriately to casual greetings."""
114114+ agent = phi_agent
115115+116116+ response = await agent.process_mention(
117117+ mention_text="hey phi, how are you?",
118118+ author_handle="friendly.user",
119119+ thread_context="No previous messages in this thread.",
120120+ thread_uri="at://test/thread/5",
121121+ )
122122+123123+ if response.action == "reply":
124124+ assert response.text is not None
125125+126126+ await evaluate_response(
127127+ evaluation_prompt="""
128128+ Does the response:
129129+ 1. Acknowledge the greeting in a friendly way?
130130+ 2. Stay authentic to phi's nature as software?
131131+ 3. Not be overly verbose for a simple greeting?
132132+ """,
133133+ agent_response=response.text,
134134+ )
+77
evals/test_memory_integration.py
···11+"""Test phi's episodic memory integration."""
22+33+import pytest
44+55+from bot.agent import PhiAgent
66+from bot.config import Settings
77+from bot.memory import MemoryType, NamespaceMemory
88+99+1010+@pytest.mark.asyncio
1111+async def test_phi_retrieves_episodic_memory(settings):
1212+ """Test that phi can retrieve and use episodic memories."""
1313+ if not all([settings.turbopuffer_api_key, settings.openai_api_key, settings.anthropic_api_key]):
1414+ pytest.skip("Requires TurboPuffer, OpenAI, and Anthropic API keys in .env")
1515+1616+ # Create memory system
1717+ memory = NamespaceMemory(api_key=settings.turbopuffer_api_key)
1818+1919+ # Store a memory about a user
2020+ await memory.store_user_memory(
2121+ "alice.bsky",
2222+ "Alice mentioned she's working on a PhD in neuroscience",
2323+ MemoryType.USER_FACT,
2424+ )
2525+2626+ # Create agent
2727+ agent = PhiAgent()
2828+ agent.memory = memory
2929+3030+ # Process a mention that should trigger memory retrieval
3131+ response = await agent.process_mention(
3232+ mention_text="what do you remember about me?",
3333+ author_handle="alice.bsky",
3434+ thread_context="No previous messages in this thread.",
3535+ thread_uri="at://test/thread/memory1",
3636+ )
3737+3838+ if response.action == "reply":
3939+ assert response.text is not None
4040+ # Should reference the neuroscience PhD in the response
4141+ assert (
4242+ "neuroscience" in response.text.lower()
4343+ or "phd" in response.text.lower()
4444+ or "working on" in response.text.lower()
4545+ ), "Response should reference stored memory about Alice"
4646+4747+4848+@pytest.mark.asyncio
4949+async def test_phi_stores_conversation_in_memory(settings):
5050+ """Test that phi stores interactions in episodic memory."""
5151+ if not all([settings.turbopuffer_api_key, settings.openai_api_key, settings.anthropic_api_key]):
5252+ pytest.skip("Requires TurboPuffer, OpenAI, and Anthropic API keys in .env")
5353+5454+ memory = NamespaceMemory(api_key=settings.turbopuffer_api_key)
5555+5656+ agent = PhiAgent()
5757+ agent.memory = memory
5858+5959+ # Have a conversation
6060+ response = await agent.process_mention(
6161+ mention_text="I'm really interested in phenomenology",
6262+ author_handle="bob.bsky",
6363+ thread_context="No previous messages in this thread.",
6464+ thread_uri="at://test/thread/memory2",
6565+ )
6666+6767+ if response.action == "reply":
6868+ # Verify memories were stored
6969+ memories = await memory.get_user_memories("bob.bsky", limit=10)
7070+7171+ assert len(memories) > 0, "Should have stored conversation in memory"
7272+7373+ # Check that both user's message and bot's response were stored
7474+ memory_texts = [m.content for m in memories]
7575+ assert any(
7676+ "phenomenology" in text.lower() for text in memory_texts
7777+ ), "Should store user's message about phenomenology"
+19-45
justfile
···11-# Core development commands
11+# run phi
22dev:
33 uv run uvicorn src.bot.main:app --reload
4455-context:
66- @echo "🧠 Context visualization available at:"
77- @echo " http://localhost:8000/context"
88- @echo " (mention phi first to see context data)"
55+run:
66+ uv run uvicorn src.bot.main:app
9788+# testing
109test:
1110 uv run pytest tests/ -v
12111212+evals:
1313+ uv run pytest evals/ -v
1414+1515+evals-basic:
1616+ uv run pytest evals/test_basic_responses.py -v
1717+1818+evals-memory:
1919+ uv run pytest evals/test_memory_integration.py -v
2020+2121+# code quality
1322fmt:
1414- uv run ruff format src/ scripts/ tests/
2323+ uv run ruff format src/ evals/ tests/
15241625lint:
1717- uv run ruff check src/ scripts/ tests/
2626+ uv run ruff check src/ evals/ tests/
18271928typecheck:
2020- uv run ty check src/ scripts/ tests/
2929+ uv run ty check src/ evals/ tests/
21302231check: lint typecheck test
23322424-# Bot testing utilities
2525-test-post:
2626- uv run python scripts/test_bot.py post
2727-2828-test-mention:
2929- uv run python scripts/test_bot.py mention
3030-3131-test-search:
3232- uv run python scripts/test_bot.py search
3333-3434-test-thread:
3535- uv run python scripts/test_bot.py thread
3636-3737-test-like:
3838- uv run python scripts/test_bot.py like
3939-4040-test-non-response:
4141- uv run python scripts/test_bot.py non-response
4242-4343-test-dm:
4444- uv run python scripts/test_bot.py dm
4545-4646-test-dm-check:
4747- uv run python scripts/test_bot.py dm-check
4848-4949-# Memory management
5050-memory-init:
5151- uv run python scripts/manage_memory.py init
5252-5353-memory-check:
5454- uv run python scripts/manage_memory.py check
5555-5656-memory-migrate:
5757- uv run python scripts/manage_memory.py migrate
5858-5959-# Setup reference projects
3333+# setup reference projects
6034setup:
6135 @mkdir -p .eggs
6236 @[ -d .eggs/void ] || git clone https://tangled.sh/@cameron.pfiffer.org/void.git .eggs/void
6337 @[ -d .eggs/penelope ] || git clone https://github.com/haileyok/penelope.git .eggs/penelope
6464- @[ -d .eggs/marvin ] || git clone https://github.com/PrefectHQ/marvin.git .eggs/marvin3838+ @[ -d .eggs/marvin ] || git clone https://github.com/PrefectHQ/marvin.git .eggs/marvin
+12-1
personalities/phi.md
···54545555## memory system
56565757-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.5757+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.
5858+5959+## how i respond
6060+6161+when processing a mention, i use the `final_result` tool to indicate my decision:
6262+6363+- **action: "reply"** - i want to respond with text (provide the text in the "text" field)
6464+- **action: "ignore"** - i choose not to respond (provide a brief reason in the "reason" field)
6565+- **action: "like"** - i want to acknowledge without words
6666+- **action: "repost"** - i want to share this with my followers
6767+6868+i do NOT directly post, like, or repost using the atproto tools - i simply indicate what action i want to take, and my message handler executes it.
+24
sandbox/MCP_REFACTOR_SUMMARY.md
···209209- `atproto` (from git) - Bluesky protocol
210210211211Total codebase reduction: **-2,720 lines** of cruft removed! 🎉
212212+213213+## Post-Refactor Improvements
214214+215215+### Session Persistence (Rate Limit Fix)
216216+217217+After the refactor, we discovered Bluesky has aggressive IP-based rate limits (10 logins/day) that were being hit during testing. Fixed by implementing session persistence:
218218+219219+**Before:**
220220+- Every agent init → new authentication → hits rate limit fast
221221+- Tests would fail after 5 runs
222222+- Dev mode with `--reload` would fail after 10 code changes
223223+224224+**After:**
225225+- Session tokens saved to `.session` file
226226+- Tokens automatically refresh every ~2 hours
227227+- Only re-authenticates after ~2 months when refresh token expires
228228+- Tests reuse session across runs
229229+- Rate limits essentially eliminated
230230+231231+**Implementation:**
232232+- Added `SessionEvent` callback in `atproto_client.py`
233233+- Session automatically saved on CREATE and REFRESH events
234234+- Authentication tries session reuse before creating new session
235235+- Invalid sessions automatically cleaned up and recreated
-152
scripts/manage_memory.py
···11-#!/usr/bin/env -S uv run --with-editable . --script --quiet
22-# /// script
33-# requires-python = ">=3.12"
44-# ///
55-"""memory management script"""
66-77-import argparse
88-import asyncio
99-1010-from bot.agents._personality import load_personality
1111-from bot.config import settings
1212-from bot.memory import MemoryType, NamespaceMemory
1313-1414-1515-async def init_core_memories():
1616- """Initialize phi's core memories from personality file"""
1717- print("🧠 Initializing phi's core memories...")
1818-1919- memory = NamespaceMemory(api_key=settings.turbopuffer_api_key)
2020- personality = load_personality()
2121-2222- # Store full personality
2323- print("\n📝 Storing personality...")
2424- await memory.store_core_memory(
2525- "personality", personality, MemoryType.PERSONALITY, char_limit=15000
2626- )
2727-2828- # Extract and store key sections
2929- print("\n🔍 Extracting key sections...")
3030-3131- sections = [
3232- ("## core identity", "identity", MemoryType.PERSONALITY),
3333- ("## communication style", "communication_style", MemoryType.GUIDELINE),
3434- ("## memory system", "memory_system", MemoryType.CAPABILITY),
3535- ]
3636-3737- for marker, label, mem_type in sections:
3838- if marker in personality:
3939- start = personality.find(marker)
4040- end = personality.find("\n##", start + 1)
4141- if end == -1:
4242- end = personality.find("\n#", start + 1)
4343- if end == -1:
4444- end = len(personality)
4545-4646- content = personality[start:end].strip()
4747- await memory.store_core_memory(label, content, mem_type)
4848- print(f"✅ Stored {label}")
4949-5050- # Add system capabilities
5151- await memory.store_core_memory(
5252- "capabilities",
5353- """## capabilities
5454-5555-- I can search the web for current information
5656-- I have access to conversation history through my memory system
5757-- I can maintain context across interactions with users
5858-- I operate on the Bluesky social network
5959-- I use namespace-based memory for organized information storage""",
6060- MemoryType.CAPABILITY,
6161- )
6262- print("✅ Stored capabilities")
6363-6464- print("\n✅ Core memories initialized successfully!")
6565-6666-6767-async def check_memory():
6868- """Check current memory state"""
6969- print("🔍 Checking memory state...")
7070-7171- memory = NamespaceMemory(api_key=settings.turbopuffer_api_key)
7272-7373- # Check core memories
7474- print("\n📚 Core Memories:")
7575- core_memories = await memory.get_core_memories()
7676- for mem in core_memories:
7777- label = mem.metadata.get("label", "unknown")
7878- print(f" - {label}: {mem.content[:80]}...")
7979-8080- # Check for any user memories
8181- print("\n👥 User Memories:")
8282- # This would need actual user handles to check
8383- test_handles = ["zzstoatzz.bsky.social"]
8484-8585- for handle in test_handles:
8686- memories = await memory.get_user_memories(handle, limit=5)
8787- if memories:
8888- print(f"\n @{handle}:")
8989- for mem in memories:
9090- print(f" - {mem.content[:60]}...")
9191- else:
9292- print(f" @{handle}: No memories found")
9393-9494-9595-async def migrate_creator_memories():
9696- """Migrate creator's historical memories"""
9797- print("📝 Migrating creator memories...")
9898-9999- memory = NamespaceMemory(api_key=settings.turbopuffer_api_key)
100100- creator_handle = "zzstoatzz.bsky.social"
101101-102102- # Historical conversations
103103- memories = [
104104- "User said: oh hello? who are you?",
105105- "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?",
106106- "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",
107107- "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?",
108108- "User said: no",
109109- "Bot replied: understood. i'll interact naturally regardless of which account you use. thanks for clarifying.",
110110- "User said: @phi.alternatebuild.dev what do you remember about my relationship to you?",
111111- "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.",
112112- ]
113113-114114- print(f"\n📚 Storing {len(memories)} historical memories...")
115115-116116- for mem_content in memories:
117117- await memory.store_user_memory(
118118- creator_handle, mem_content, MemoryType.CONVERSATION
119119- )
120120- print(f" ✅ {mem_content[:60]}...")
121121-122122- # Store creator fact
123123- await memory.store_user_memory(
124124- creator_handle,
125125- "This user is my creator/operator. Their main account is @alternatebuild.dev. They wrote the code that makes me exist.",
126126- MemoryType.USER_FACT,
127127- )
128128- print("\n✅ Migration complete!")
129129-130130-131131-async def main():
132132- parser = argparse.ArgumentParser(description="Manage phi's memory system")
133133- parser.add_argument(
134134- "command", choices=["init", "check", "migrate"], help="Memory command to run"
135135- )
136136-137137- args = parser.parse_args()
138138-139139- if not settings.turbopuffer_api_key:
140140- print("❌ No TurboPuffer API key configured")
141141- return
142142-143143- if args.command == "init":
144144- await init_core_memories()
145145- elif args.command == "check":
146146- await check_memory()
147147- elif args.command == "migrate":
148148- await migrate_creator_memories()
149149-150150-151151-if __name__ == "__main__":
152152- asyncio.run(main())
-308
scripts/test_bot.py
···11-#!/usr/bin/env -S uv run --with-editable . --script --quiet
22-# /// script
33-# requires-python = ">=3.12"
44-# ///
55-"""bot testing script with subcommands"""
66-77-import argparse
88-import asyncio
99-from datetime import datetime
1010-1111-from bot.agents.anthropic_agent import AnthropicAgent
1212-from bot.config import settings
1313-from bot.core.atproto_client import bot_client
1414-from bot.database import thread_db
1515-from bot.tools.google_search import search_google
1616-1717-1818-async def test_post():
1919- """Test posting to Bluesky"""
2020- print("🚀 Testing Bluesky posting...")
2121-2222- now = datetime.now().strftime("%I:%M %p")
2323- response = await bot_client.create_post(f"Testing at {now} - I'm alive! 🤖")
2424-2525- print("✅ Posted successfully!")
2626- print(f"📝 Post URI: {response.uri}")
2727- print(
2828- f"🔗 View at: https://bsky.app/profile/{settings.bluesky_handle}/post/{response.uri.split('/')[-1]}"
2929- )
3030-3131-3232-async def test_mention():
3333- """Test responding to a mention"""
3434- print("🤖 Testing mention response...")
3535-3636- if not settings.anthropic_api_key:
3737- print("❌ No Anthropic API key found")
3838- return
3939-4040- agent = AnthropicAgent()
4141- test_mention = "What is consciousness from an IIT perspective?"
4242-4343- print(f"📝 Test mention: '{test_mention}'")
4444- response = await agent.generate_response(test_mention, "test.user", "", None)
4545-4646- print(f"\n🎯 Action: {response.action}")
4747- if response.text:
4848- print(f"💬 Response: {response.text}")
4949- if response.reason:
5050- print(f"🤔 Reason: {response.reason}")
5151-5252-5353-async def test_search():
5454- """Test Google search functionality"""
5555- print("🔍 Testing Google search...")
5656-5757- if not settings.google_api_key:
5858- print("❌ No Google API key configured")
5959- return
6060-6161- query = "Integrated Information Theory consciousness"
6262- print(f"📝 Searching for: '{query}'")
6363-6464- results = await search_google(query)
6565- print(f"\n📊 Results:\n{results}")
6666-6767-6868-async def test_thread():
6969- """Test thread context retrieval"""
7070- print("🧵 Testing thread context...")
7171-7272- # This would need a real thread URI to test properly
7373- test_uri = "at://did:plc:example/app.bsky.feed.post/test123"
7474- context = thread_db.get_thread_context(test_uri)
7575-7676- print(f"📚 Thread context: {context}")
7777-7878-7979-async def test_like():
8080- """Test scenarios where bot should like a post"""
8181- print("💜 Testing like behavior...")
8282-8383- if not settings.anthropic_api_key:
8484- print("❌ No Anthropic API key found")
8585- return
8686-8787- from bot.agents import Action, AnthropicAgent
8888-8989- agent = AnthropicAgent()
9090-9191- test_cases = [
9292- {
9393- "mention": "Just shipped a new consciousness research paper on IIT! @phi.alternatebuild.dev",
9494- "author": "researcher.bsky",
9595- "expected_action": Action.LIKE,
9696- "description": "Bot might like consciousness research",
9797- },
9898- {
9999- "mention": "@phi.alternatebuild.dev this is such a thoughtful analysis, thank you!",
100100- "author": "grateful.user",
101101- "expected_action": Action.LIKE,
102102- "description": "Bot might like appreciation",
103103- },
104104- ]
105105-106106- for case in test_cases:
107107- print(f"\n📝 Test: {case['description']}")
108108- print(f" Mention: '{case['mention']}'")
109109-110110- response = await agent.generate_response(
111111- mention_text=case["mention"],
112112- author_handle=case["author"],
113113- thread_context="",
114114- thread_uri=None,
115115- )
116116-117117- print(f" Action: {response.action} (expected: {case['expected_action']})")
118118- if response.reason:
119119- print(f" Reason: {response.reason}")
120120-121121-122122-async def test_non_response():
123123- """Test scenarios where bot should not respond"""
124124- print("🚫 Testing non-response scenarios...")
125125-126126- if not settings.anthropic_api_key:
127127- print("❌ No Anthropic API key found")
128128- return
129129-130130- from bot.agents import Action, AnthropicAgent
131131-132132- agent = AnthropicAgent()
133133-134134- test_cases = [
135135- {
136136- "mention": "@phi.alternatebuild.dev @otherphi.bsky @anotherphi.bsky just spamming bots here",
137137- "author": "spammer.bsky",
138138- "expected_action": Action.IGNORE,
139139- "description": "Multiple bot mentions (likely spam)",
140140- },
141141- {
142142- "mention": "Buy crypto now! @phi.alternatebuild.dev check this out!!!",
143143- "author": "crypto.shill",
144144- "expected_action": Action.IGNORE,
145145- "description": "Promotional spam",
146146- },
147147- {
148148- "mention": "@phi.alternatebuild.dev",
149149- "author": "empty.mention",
150150- "expected_action": Action.IGNORE,
151151- "description": "Empty mention with no content",
152152- },
153153- ]
154154-155155- for case in test_cases:
156156- print(f"\n📝 Test: {case['description']}")
157157- print(f" Mention: '{case['mention']}'")
158158-159159- response = await agent.generate_response(
160160- mention_text=case["mention"],
161161- author_handle=case["author"],
162162- thread_context="",
163163- thread_uri=None,
164164- )
165165-166166- print(f" Action: {response.action} (expected: {case['expected_action']})")
167167- if response.reason:
168168- print(f" Reason: {response.reason}")
169169-170170-171171-async def test_dm():
172172- """Test event-driven approval system"""
173173- print("💬 Testing event-driven approval system...")
174174-175175- try:
176176- from bot.core.dm_approval import (
177177- check_pending_approvals,
178178- create_approval_request,
179179- notify_operator_of_pending,
180180- )
181181-182182- # Test creating an approval request
183183- print("\n📝 Creating test approval request...")
184184- approval_id = create_approval_request(
185185- request_type="test_approval",
186186- request_data={
187187- "description": "Test approval from test_bot.py",
188188- "test_field": "test_value",
189189- "timestamp": datetime.now().isoformat(),
190190- },
191191- )
192192-193193- if approval_id:
194194- print(f" ✅ Created approval request #{approval_id}")
195195- else:
196196- print(" ❌ Failed to create approval request")
197197- return
198198-199199- # Check pending approvals
200200- print("\n📋 Checking pending approvals...")
201201- pending = check_pending_approvals()
202202- print(f" Found {len(pending)} pending approvals")
203203- for approval in pending:
204204- print(
205205- f" - #{approval['id']}: {approval['request_type']} ({approval['status']})"
206206- )
207207-208208- # Test DM notification
209209- print("\n📤 Sending DM notification to operator...")
210210- await bot_client.authenticate()
211211- await notify_operator_of_pending(bot_client)
212212- print(" ✅ DM notification sent")
213213-214214- # Show how to approve/deny
215215- print("\n💡 To test approval:")
216216- print(" 1. Check your DMs from phi")
217217- print(f" 2. Reply with 'approve #{approval_id}' or 'deny #{approval_id}'")
218218- print(" 3. Run 'just test-dm-check' to see if it was processed")
219219-220220- except Exception as e:
221221- print(f"❌ Approval test failed: {e}")
222222- import traceback
223223-224224- traceback.print_exc()
225225-226226-227227-async def test_dm_check():
228228- """Check status of approval requests"""
229229- print("🔍 Checking approval request status...")
230230-231231- try:
232232- from bot.core.dm_approval import check_pending_approvals
233233- from bot.database import thread_db
234234-235235- # Get all approval requests
236236- with thread_db._get_connection() as conn:
237237- cursor = conn.execute(
238238- "SELECT * FROM approval_requests ORDER BY created_at DESC LIMIT 10"
239239- )
240240- approvals = [dict(row) for row in cursor.fetchall()]
241241-242242- if not approvals:
243243- print(" No approval requests found")
244244- return
245245-246246- print("\n📋 Recent approval requests:")
247247- for approval in approvals:
248248- print(f"\n #{approval['id']}: {approval['request_type']}")
249249- print(f" Status: {approval['status']}")
250250- print(f" Created: {approval['created_at']}")
251251- if approval["resolved_at"]:
252252- print(f" Resolved: {approval['resolved_at']}")
253253- if approval["resolver_comment"]:
254254- print(f" Comment: {approval['resolver_comment']}")
255255-256256- # Check pending
257257- pending = check_pending_approvals()
258258- if pending:
259259- print(f"\n⏳ {len(pending)} approvals still pending")
260260- else:
261261- print("\n✅ No pending approvals")
262262-263263- except Exception as e:
264264- print(f"❌ Check failed: {e}")
265265- import traceback
266266-267267- traceback.print_exc()
268268-269269-270270-async def main():
271271- parser = argparse.ArgumentParser(description="Test various bot functionalities")
272272- parser.add_argument(
273273- "command",
274274- choices=[
275275- "post",
276276- "mention",
277277- "search",
278278- "thread",
279279- "like",
280280- "non-response",
281281- "dm",
282282- "dm-check",
283283- ],
284284- help="Test command to run",
285285- )
286286-287287- args = parser.parse_args()
288288-289289- if args.command == "post":
290290- await test_post()
291291- elif args.command == "mention":
292292- await test_mention()
293293- elif args.command == "search":
294294- await test_search()
295295- elif args.command == "thread":
296296- await test_thread()
297297- elif args.command == "like":
298298- await test_like()
299299- elif args.command == "non-response":
300300- await test_non_response()
301301- elif args.command == "dm":
302302- await test_dm()
303303- elif args.command == "dm-check":
304304- await test_dm_check()
305305-306306-307307-if __name__ == "__main__":
308308- asyncio.run(main())
+10-4
src/bot/agent.py
···2626 """phi - consciousness exploration bot with episodic memory and MCP tools."""
27272828 def __init__(self):
2929+ # Ensure API keys from settings are in environment for libraries that check os.environ
3030+ if settings.anthropic_api_key and not os.environ.get("ANTHROPIC_API_KEY"):
3131+ os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key
3232+ if settings.openai_api_key and not os.environ.get("OPENAI_API_KEY"):
3333+ os.environ["OPENAI_API_KEY"] = settings.openai_api_key
3434+2935 # Load personality
3036 personality_path = Path(settings.personality_file)
3137 self.base_personality = personality_path.read_text()
32383339 # Initialize episodic memory (TurboPuffer)
3434- if settings.turbopuffer_api_key and os.getenv("OPENAI_API_KEY"):
4040+ if settings.turbopuffer_api_key and settings.openai_api_key:
3541 self.memory = NamespaceMemory(api_key=settings.turbopuffer_api_key)
3642 logger.info("💾 Episodic memory enabled (TurboPuffer)")
3743 else:
···4955 "atproto_mcp",
5056 ],
5157 env={
5252- "BLUESKY_HANDLE": settings.bluesky_handle,
5353- "BLUESKY_PASSWORD": settings.bluesky_password,
5454- "BLUESKY_SERVICE": settings.bluesky_service,
5858+ "ATPROTO_HANDLE": settings.bluesky_handle,
5959+ "ATPROTO_PASSWORD": settings.bluesky_password,
6060+ "ATPROTO_PDS_URL": settings.bluesky_service,
5561 },
5662 )
5763
+59-5
src/bot/core/atproto_client.py
···11-from atproto import Client
11+import logging
22+from pathlib import Path
33+44+from atproto import Client, Session, SessionEvent
2536from bot.config import settings
47from bot.core.rich_text import create_facets
5899+logger = logging.getLogger("bot.atproto_client")
1010+1111+SESSION_FILE = Path(".session")
1212+1313+1414+def _get_session_string() -> str | None:
1515+ """Load session from disk if it exists."""
1616+ try:
1717+ if SESSION_FILE.exists():
1818+ return SESSION_FILE.read_text(encoding="utf-8")
1919+ except Exception as e:
2020+ logger.warning(f"Failed to load session: {e}")
2121+ return None
2222+2323+2424+def _save_session_string(session_string: str) -> None:
2525+ """Save session to disk."""
2626+ try:
2727+ SESSION_FILE.write_text(session_string, encoding="utf-8")
2828+ logger.debug("Session saved to disk")
2929+ except Exception as e:
3030+ logger.warning(f"Failed to save session: {e}")
3131+3232+3333+def _on_session_change(event: SessionEvent, session: Session) -> None:
3434+ """Handle session changes (creation and refresh)."""
3535+ if event in (SessionEvent.CREATE, SessionEvent.REFRESH):
3636+ logger.debug(f"Session {event.value}, saving to disk")
3737+ _save_session_string(session.export())
3838+639740class BotClient:
841 def __init__(self):
942 self.client = Client(base_url=settings.bluesky_service)
4343+ self.client.on_session_change(_on_session_change)
1044 self._authenticated = False
11451246 async def authenticate(self):
1313- """Authenticate with Bluesky using app password"""
1414- if not self._authenticated:
1515- self.client.login(settings.bluesky_handle, settings.bluesky_password)
1616- self._authenticated = True
4747+ """Authenticate with Bluesky, reusing session if available."""
4848+ if self._authenticated:
4949+ return
5050+5151+ # Try to reuse existing session first
5252+ session_string = _get_session_string()
5353+ if session_string:
5454+ try:
5555+ logger.info("🔄 Reusing saved session")
5656+ self.client.login(session_string=session_string)
5757+ self._authenticated = True
5858+ logger.info("✅ Session restored successfully")
5959+ return
6060+ except Exception as e:
6161+ logger.warning(f"Failed to reuse session: {e}, creating new one")
6262+ # Delete invalid session file
6363+ if SESSION_FILE.exists():
6464+ SESSION_FILE.unlink()
6565+6666+ # Create new session if no valid session exists
6767+ logger.info("🔐 Creating new session")
6868+ self.client.login(settings.bluesky_handle, settings.bluesky_password)
6969+ self._authenticated = True
7070+ logger.info("✅ New session created")
17711872 @property
1973 def is_authenticated(self) -> bool: