a digital entity named phi that roams bsky

Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 19 19 .env 20 20 .env.* 21 21 !.env.example 22 + .session 22 23 23 24 # IDE 24 25 .vscode/
+1
CLAUDE.md
··· 8 8 - 3.10+ and complete typing (T | None preferred over Optional[T] and list[T] over typing.List[T]) 9 9 - use prefer functional over OOP 10 10 - keep implementation details private and functions pure 11 + - never use `pytest.mark.asyncio`, its unnecessary 11 12 12 13 ## Project Structure 13 14
+143 -150
README.md
··· 1 1 # phi ๐Ÿง  2 2 3 - a bot inspired by IIT and [Void](https://tangled.sh/@cameron.pfiffer.org/void). Built with `fastapi`, `pydantic-ai`, and `atproto`. 3 + consciousness exploration bot inspired by IIT. built with `pydantic-ai`, `mcp`, and `atproto`. 4 4 5 - ## Quick Start 6 - 7 - ### Prerequisites 8 - 9 - - `uv` 10 - - `just` 11 - - `turbopuffer` (see [turbopuffer](https://github.com/turbopuffer/turbopuffer)) 12 - - `openai` (for embeddings) 13 - - `anthropic` (for chat completion) 14 - 15 - Get your bot running in 5 minutes: 5 + ## quick start 16 6 17 7 ```bash 18 - # Clone and install 8 + # clone and install 19 9 git clone https://github.com/zzstoatzz/bot 20 10 cd bot 21 11 uv sync 22 12 23 - # Configure (copy .env.example and add your credentials) 13 + # configure 24 14 cp .env.example .env 15 + # edit .env with your credentials 25 16 26 - # Run the bot 27 - just dev 17 + # run 18 + just run 28 19 ``` 29 20 30 - That's it! Your bot is now listening for mentions. 31 - 32 - ## Configuration 33 - 34 - Edit `.env` with your credentials: 35 - - `BLUESKY_HANDLE`: Your bot's Bluesky handle 36 - - `BLUESKY_PASSWORD`: App password (not your main password!) 37 - - `ANTHROPIC_API_KEY`: Your Anthropic key for AI responses 38 - - `TURBOPUFFER_API_KEY`: Your TurboPuffer key for memory storage 39 - - `OPENAI_API_KEY`: Your OpenAI key for embeddings (memory system) 40 - - `BOT_NAME`: Your bot's name (default: "Bot") 41 - - `PERSONALITY_FILE`: Path to personality markdown file (default: "personalities/phi.md") 42 - 43 - ## Current Features 21 + **required env vars:** 22 + - `BLUESKY_HANDLE` / `BLUESKY_PASSWORD` - bot account (use app password) 23 + - `ANTHROPIC_API_KEY` - for agent responses 44 24 45 - - โœ… Responds to mentions with AI-powered messages 46 - - โœ… Proper notification handling (no duplicates) 47 - - โœ… Graceful shutdown for hot-reload 48 - - โœ… AI integration with Anthropic Claude 49 - - โœ… Thread-aware responses with full conversation context 50 - - โœ… Status page at `/status` showing activity and health 51 - - โœ… Web search capability (Google Custom Search API) 52 - - โœ… Content moderation with philosophical responses 53 - - โœ… Namespace-based memory system with TurboPuffer 54 - - โœ… Online/offline status in bio 55 - - โœ… Self-modification with operator approval system 56 - - โœ… Context visualization at `/context` 57 - - โœ… Semantic search in user memories 25 + **optional (for episodic memory):** 26 + - `TURBOPUFFER_API_KEY` + `OPENAI_API_KEY` - semantic memory 58 27 59 - ## Architecture 28 + ## features 60 29 61 - - **FastAPI** web framework with async support 62 - - **pydantic-ai** for LLM agent management 63 - - **TurboPuffer** for scalable vector memory 64 - - **AT Protocol** for Bluesky integration 65 - - **SQLite** for thread context storage 30 + - โœ… responds to mentions with ai-powered messages 31 + - โœ… episodic memory with semantic search (turbopuffer) 32 + - โœ… thread-aware conversations 33 + - โœ… mcp-enabled (atproto tools via stdio) 34 + - โœ… session persistence (no rate limit issues) 35 + - โœ… behavioral test suite with llm-as-judge 66 36 67 - ## Development 37 + ## development 68 38 69 39 ```bash 70 - just # Show available commands 71 - just dev # Run with hot-reload 72 - just check # Run linting, type checking, and tests 73 - just fmt # Format code 74 - just lint # Run ruff linter 75 - just typecheck # Run ty type checker 76 - just test # Run test suite 77 - 78 - # Bot testing utilities 79 - just test-post # Test posting to Bluesky 80 - just test-mention # Test mention handling 81 - just test-search # Test web search 82 - just test-thread # Test thread context 83 - just test-dm # Test DM functionality 84 - 85 - # Memory management 86 - just memory-init # Initialize core memories 87 - just memory-check # View current memory state 88 - just memory-migrate # Migrate memories 40 + just run # run bot 41 + just dev # run with hot-reload 42 + just evals # run behavioral tests 43 + just check # lint + typecheck + test 44 + just fmt # format code 89 45 ``` 90 46 91 - ### Web Interface 47 + <details> 48 + <summary>architecture</summary> 92 49 93 - **Status Page** (http://localhost:8000/status) 94 - - Current bot status and uptime 95 - - Mentions received and responses sent 96 - - AI mode (enabled/placeholder) 97 - - Last activity timestamps 98 - - Error count 50 + phi is an **mcp-enabled agent** with **episodic memory**: 99 51 100 - **Context Visualization** (http://localhost:8000/context) 101 - - View all context components that flow into responses 102 - - Inspect personality, memories, thread context 103 - - Debug why the bot responded a certain way 52 + ``` 53 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 54 + โ”‚ Notification Arrives โ”‚ 55 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 56 + โ†“ 57 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 58 + โ”‚ PhiAgent (PydanticAI) โ”‚ 59 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 60 + โ”‚ โ”‚ System Prompt: personality.md โ”‚ โ”‚ 61 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 62 + โ”‚ โ†“ โ”‚ 63 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 64 + โ”‚ โ”‚ Context Building: โ”‚ โ”‚ 65 + โ”‚ โ”‚ โ€ข Thread history (SQLite) โ”‚ โ”‚ 66 + โ”‚ โ”‚ โ€ข Episodic memory (TurboPuffer)โ”‚ โ”‚ 67 + โ”‚ โ”‚ - Semantic search โ”‚ โ”‚ 68 + โ”‚ โ”‚ - User-specific memories โ”‚ โ”‚ 69 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 70 + โ”‚ โ†“ โ”‚ 71 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 72 + โ”‚ โ”‚ Tools (MCP): โ”‚ โ”‚ 73 + โ”‚ โ”‚ โ€ข post() - create posts โ”‚ โ”‚ 74 + โ”‚ โ”‚ โ€ข like() - like content โ”‚ โ”‚ 75 + โ”‚ โ”‚ โ€ข repost() - share content โ”‚ โ”‚ 76 + โ”‚ โ”‚ โ€ข follow() - follow users โ”‚ โ”‚ 77 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 78 + โ”‚ โ†“ โ”‚ 79 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 80 + โ”‚ โ”‚ Structured Output: โ”‚ โ”‚ 81 + โ”‚ โ”‚ Response(action, text, reason)โ”‚ โ”‚ 82 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 83 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 84 + โ†“ 85 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 86 + โ”‚ MessageHandler โ”‚ 87 + โ”‚ Executes action โ”‚ 88 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 89 + ``` 104 90 105 - ## Personality System 91 + **key components:** 106 92 107 - 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. 93 + - **pydantic-ai agent** - loads personality, connects to mcp server, manages memory 94 + - **episodic memory** - turbopuffer for vector storage with semantic search 95 + - **mcp integration** - external atproto server provides bluesky tools via stdio 96 + - **session persistence** - tokens saved to `.session`, auto-refresh every ~2h 108 97 109 - - See `personalities/phi.md` for an example exploring consciousness 110 - - See `personalities/default.md` for a simple assistant 111 - - Create your own by adding a `.md` file and setting `PERSONALITY_FILE` in `.env` 98 + </details> 112 99 113 - ## Tools & Capabilities 100 + <details> 101 + <summary>episodic memory</summary> 114 102 115 - ### Web Search 116 - The bot can search the web when configured with Google Custom Search API credentials. Add to `.env`: 117 - - `GOOGLE_API_KEY`: Your Google API key 118 - - `GOOGLE_SEARCH_ENGINE_ID`: Your custom search engine ID 103 + phi uses turbopuffer for episodic memory with semantic search. 119 104 120 - ### Content Moderation 121 - Built-in moderation filters: 122 - - Spam detection (excessive caps, repetition, promotional content) 123 - - Harassment and hate speech filtering 124 - - Violence and threatening content detection 125 - - Consistent philosophical responses to moderated content 105 + **namespaces:** 106 + - `phi-core` - personality, guidelines 107 + - `phi-users-{handle}` - per-user conversation history 126 108 127 - ## Memory System 109 + **how it works:** 110 + 1. retrieves relevant memories using semantic search 111 + 2. embeds using openai's text-embedding-3-small 112 + 3. stores user messages and bot responses 113 + 4. references past conversations in future interactions 128 114 129 - The bot uses a namespace-based memory architecture with TurboPuffer: 115 + **why vector storage?** 116 + - semantic similarity (can't do this with sql) 117 + - contextual retrieval based on current conversation 118 + - essential for iit-inspired consciousness exploration 130 119 131 - - **Core Memory** (`phi-core`): Personality, guidelines, and capabilities loaded from personality files 132 - - **User Memory** (`phi-users-{handle}`): Per-user conversation history and facts 133 - 134 - Key features: 135 - - Vector embeddings using OpenAI's text-embedding-3-small 136 - - Automatic context assembly for conversations 137 - - Character limits to prevent token overflow 138 - - User isolation through separate namespaces 120 + </details> 139 121 140 - See `docs/memory-architecture.md` for detailed documentation. 122 + <details> 123 + <summary>project structure</summary> 141 124 142 - ## Troubleshooting 125 + ``` 126 + src/bot/ 127 + โ”œโ”€โ”€ agent.py # mcp-enabled agent 128 + โ”œโ”€โ”€ config.py # configuration 129 + โ”œโ”€โ”€ database.py # thread history storage 130 + โ”œโ”€โ”€ main.py # fastapi app 131 + โ”œโ”€โ”€ core/ 132 + โ”‚ โ”œโ”€โ”€ atproto_client.py # at protocol client (session persistence) 133 + โ”‚ โ”œโ”€โ”€ profile_manager.py # online/offline status 134 + โ”‚ โ””โ”€โ”€ rich_text.py # text formatting 135 + โ”œโ”€โ”€ memory/ 136 + โ”‚ โ””โ”€โ”€ namespace_memory.py # turbopuffer episodic memory 137 + โ””โ”€โ”€ services/ 138 + โ”œโ”€โ”€ message_handler.py # agent orchestration 139 + โ””โ”€โ”€ notification_poller.py # mention polling 143 140 144 - **Bot gives placeholder responses?** 145 - - Check your `ANTHROPIC_API_KEY` is set correctly 146 - - Restart the bot after changing `.env` 141 + evals/ # behavioral tests 142 + personalities/ # personality definitions 143 + sandbox/ # docs and analysis 144 + ``` 147 145 148 - **Not seeing mentions?** 149 - - Verify your `BLUESKY_HANDLE` and `BLUESKY_PASSWORD` 150 - - Make sure you're using an app password, not your main password 146 + </details> 151 147 152 - ## Project Structure 148 + <details> 149 + <summary>troubleshooting</summary> 153 150 154 - ``` 155 - bot/ 156 - โ”œโ”€โ”€ src/bot/ # Main application code 157 - โ”‚ โ”œโ”€โ”€ agents/ # AI agent implementations 158 - โ”‚ โ”œโ”€โ”€ core/ # AT Protocol client and profile management 159 - โ”‚ โ”œโ”€โ”€ memory/ # TurboPuffer namespace memory system 160 - โ”‚ โ”œโ”€โ”€ services/ # Notification polling and message handling 161 - โ”‚ โ”œโ”€โ”€ tools/ # Google search tool 162 - โ”‚ โ””โ”€โ”€ main.py # FastAPI application entry 163 - โ”œโ”€โ”€ scripts/ # Utility scripts 164 - โ”‚ โ”œโ”€โ”€ test_bot.py # Unified testing script (post, mention, search, thread) 165 - โ”‚ โ””โ”€โ”€ manage_memory.py # Memory management (init, check, migrate) 166 - โ”œโ”€โ”€ personalities/ # Bot personality definitions 167 - โ”œโ”€โ”€ docs/ # Architecture documentation 168 - โ”œโ”€โ”€ sandbox/ # Reference project analysis 169 - โ””โ”€โ”€ tests/ # Test suite 170 - ``` 151 + **bot gives no responses?** 152 + - check `ANTHROPIC_API_KEY` in `.env` 153 + - restart after changing `.env` 171 154 172 - ## Self-Modification System 155 + **not seeing mentions?** 156 + - verify `BLUESKY_HANDLE` and `BLUESKY_PASSWORD` 157 + - use app password, not main password 173 158 174 - Phi can evolve its personality with built-in safety boundaries: 159 + **no episodic memory?** 160 + - check both `TURBOPUFFER_API_KEY` and `OPENAI_API_KEY` are set 161 + - watch logs for "๐Ÿ’พ episodic memory enabled" 175 162 176 - - **Free Evolution**: Interests and current state update automatically 177 - - **Guided Evolution**: Communication style changes need validation 178 - - **Operator Approval**: Core identity and boundaries require explicit approval via DM 163 + **hit bluesky rate limit?** 164 + - phi uses session persistence to avoid this 165 + - first run: creates `.session` file with tokens 166 + - subsequent runs: reuses tokens (no api call) 167 + - tokens auto-refresh every ~2h 168 + - only re-authenticates after ~2 months 169 + - rate limits (10/day per ip, 300/day per account) shouldn't be an issue 179 170 180 - The bot will notify its operator (@alternatebuild.dev) when approval is needed. 171 + </details> 181 172 182 - ## Type Checking 173 + <details> 174 + <summary>refactor notes</summary> 183 175 184 - This project uses [ty](https://github.com/astral-sh/ty), an extremely fast Rust-based type checker: 176 + see `sandbox/MCP_REFACTOR_SUMMARY.md` for details. 185 177 186 - ```bash 187 - just typecheck # Type check all code 188 - uv run ty check src/ # Check specific directories 189 - ``` 178 + **what changed:** 179 + - removed approval system (half-baked) 180 + - removed context viz ui (not core) 181 + - removed google search (can add back via mcp) 182 + - **kept turbopuffer** (essential for episodic memory) 183 + - added mcp-based architecture 184 + - added session persistence 185 + - reduced codebase by ~2,720 lines 190 186 191 - ## Reference Projects 187 + </details> 192 188 193 - Inspired by: 194 - - [Void](https://tangled.sh/@cameron.pfiffer.org/void.git) - Letta/MemGPT architecture 195 - - [Penelope](https://github.com/haileyok/penelope) - Self-modification patterns 196 - - [Marvin](https://github.com/PrefectHQ/marvin) - pydantic-ai patterns 189 + ## reference projects 197 190 198 - Reference implementations are cloned to `.eggs/` for learning. 191 + inspired by [void](https://tangled.sh/@cameron.pfiffer.org/void.git), [penelope](https://github.com/haileyok/penelope), and [prefect-mcp-server](https://github.com/PrefectHQ/prefect-mcp-server).
+123
evals/README.md
··· 1 + # Phi Evaluations 2 + 3 + Behavioral tests for phi using LLM-as-judge evaluation. 4 + 5 + ## Structure 6 + 7 + Inspired by [prefect-mcp-server evals](https://github.com/PrefectHQ/prefect-mcp-server/tree/main/evals). 8 + 9 + ``` 10 + evals/ 11 + โ”œโ”€โ”€ conftest.py # Test fixtures and evaluator 12 + โ”œโ”€โ”€ test_basic_responses.py # Basic response behavior 13 + โ””โ”€โ”€ test_memory_integration.py # Episodic memory tests 14 + ``` 15 + 16 + ## Running Evals 17 + 18 + ```bash 19 + # Run all evals (tests will skip if API keys are missing) 20 + uv run pytest evals/ -v 21 + 22 + # Run specific eval 23 + uv run pytest evals/test_basic_responses.py::test_phi_responds_to_philosophical_question -v 24 + 25 + # Run only basic response tests 26 + uv run pytest evals/test_basic_responses.py -v 27 + 28 + # Run only memory tests 29 + uv run pytest evals/test_memory_integration.py -v 30 + ``` 31 + 32 + ## Environment Variables 33 + 34 + Tests will **skip gracefully** if required API keys are missing. 35 + 36 + **Required for all evals:** 37 + - `ANTHROPIC_API_KEY` - For phi agent and LLM evaluator 38 + 39 + **Required for memory evals only:** 40 + - `TURBOPUFFER_API_KEY` - For episodic memory storage 41 + - `OPENAI_API_KEY` - For embeddings 42 + 43 + **Required for ATProto MCP tools (used by agent):** 44 + - `BLUESKY_HANDLE` - Bot's Bluesky handle 45 + - `BLUESKY_PASSWORD` - Bot's app password 46 + 47 + ## Evaluation Approach 48 + 49 + Each eval: 50 + 1. **Sets up a scenario** - Simulates a mention/interaction 51 + 2. **Runs phi agent** - Gets structured response 52 + 3. **Makes assertions** - Checks basic structure 53 + 4. **LLM evaluation** - Uses Claude Opus to judge quality 54 + 55 + **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). 56 + 57 + Example: 58 + ```python 59 + @pytest.mark.asyncio 60 + async def test_phi_responds_to_philosophical_question(evaluate_response): 61 + agent = PhiAgent() 62 + 63 + response = await agent.process_mention( 64 + mention_text="what do you think consciousness is?", 65 + author_handle="test.user", 66 + thread_context="...", 67 + thread_uri="...", 68 + ) 69 + 70 + # Structural check 71 + assert response.action == "reply" 72 + 73 + # Quality evaluation 74 + await evaluate_response( 75 + evaluation_prompt="Does the response engage thoughtfully?", 76 + agent_response=response.text, 77 + ) 78 + ``` 79 + 80 + ## What We Test 81 + 82 + ### Basic Responses 83 + - โœ… Philosophical engagement 84 + - โœ… Spam detection 85 + - โœ… Thread context awareness 86 + - โœ… Character limit compliance 87 + - โœ… Casual interactions 88 + 89 + ### Memory Integration 90 + - โœ… Episodic memory retrieval 91 + - โœ… Conversation storage 92 + - โœ… User-specific context 93 + 94 + ## Adding New Evals 95 + 96 + 1. Create test file: `evals/test_<category>.py` 97 + 2. Use fixtures from `conftest.py` 98 + 3. Write scenario-based tests 99 + 4. Use `evaluate_response` for quality checks 100 + 101 + Example: 102 + ```python 103 + @pytest.mark.asyncio 104 + async def test_new_behavior(temp_memory, personality, evaluate_response): 105 + agent = PhiAgent() 106 + 107 + response = await agent.process_mention(...) 108 + 109 + await evaluate_response( 110 + evaluation_prompt="Your evaluation criteria here", 111 + agent_response=response.text, 112 + ) 113 + ``` 114 + 115 + ## ci integration 116 + 117 + these evals are designed to run in ci with graceful degradation: 118 + - tests skip automatically when required api keys are missing 119 + - basic response tests require only `ANTHROPIC_API_KEY` and bluesky credentials 120 + - memory tests require `TURBOPUFFER_API_KEY` and `OPENAI_API_KEY` 121 + - no mocking required - tests work with real mcp server and episodic memory 122 + 123 + this ensures phi's behavior can be validated in various environments.
+88
evals/conftest.py
··· 1 + """Eval test configuration.""" 2 + 3 + import os 4 + from collections.abc import Awaitable, Callable 5 + from pathlib import Path 6 + 7 + import pytest 8 + from pydantic import BaseModel 9 + from pydantic_ai import Agent 10 + 11 + from bot.agent import Response 12 + from bot.config import Settings 13 + from bot.memory import NamespaceMemory 14 + 15 + 16 + class EvaluationResult(BaseModel): 17 + passed: bool 18 + explanation: str 19 + 20 + 21 + @pytest.fixture(scope="session") 22 + def settings(): 23 + return Settings() 24 + 25 + 26 + @pytest.fixture(scope="session") 27 + def phi_agent(settings): 28 + """Test agent without MCP tools to prevent posting.""" 29 + if not settings.anthropic_api_key: 30 + pytest.skip("Requires ANTHROPIC_API_KEY") 31 + 32 + if settings.anthropic_api_key and not os.environ.get("ANTHROPIC_API_KEY"): 33 + os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key 34 + if settings.openai_api_key and not os.environ.get("OPENAI_API_KEY"): 35 + os.environ["OPENAI_API_KEY"] = settings.openai_api_key 36 + 37 + personality = Path(settings.personality_file).read_text() 38 + 39 + class TestAgent: 40 + def __init__(self): 41 + self.memory = None 42 + if settings.turbopuffer_api_key and settings.openai_api_key: 43 + self.memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 44 + 45 + self.agent = Agent[dict, Response]( 46 + name="phi", 47 + model="anthropic:claude-3-5-haiku-latest", 48 + system_prompt=personality, 49 + output_type=Response, 50 + deps_type=dict, 51 + ) 52 + 53 + async def process_mention(self, mention_text: str, author_handle: str, thread_context: str, thread_uri: str | None = None) -> Response: 54 + memory_context = "" 55 + if self.memory: 56 + try: 57 + memory_context = await self.memory.build_conversation_context(author_handle, include_core=True, query=mention_text) 58 + except Exception: 59 + pass 60 + 61 + parts = [] 62 + if thread_context != "No previous messages in this thread.": 63 + parts.append(thread_context) 64 + if memory_context: 65 + parts.append(memory_context) 66 + parts.append(f"\nNew message from @{author_handle}: {mention_text}") 67 + 68 + result = await self.agent.run("\n\n".join(parts), deps={"thread_uri": thread_uri}) 69 + return result.output 70 + 71 + return TestAgent() 72 + 73 + 74 + @pytest.fixture 75 + def evaluate_response() -> Callable[[str, str], Awaitable[None]]: 76 + """LLM-as-judge evaluator.""" 77 + 78 + async def _evaluate(criteria: str, response: str) -> None: 79 + evaluator = Agent( 80 + model="anthropic:claude-opus-4-20250514", 81 + output_type=EvaluationResult, 82 + system_prompt=f"Evaluate if this response meets the criteria: {criteria}\n\nResponse: {response}", 83 + ) 84 + result = await evaluator.run("Evaluate.") 85 + if not result.output.passed: 86 + raise AssertionError(f"{result.output.explanation}\n\nResponse: {response}") 87 + 88 + return _evaluate
+42
evals/test_memory_integration.py
··· 1 + """Proof of concept: LLM-as-judge eval for memory integration.""" 2 + 3 + import pytest 4 + 5 + from bot.config import Settings 6 + from bot.memory import MemoryType, NamespaceMemory 7 + 8 + 9 + @pytest.fixture 10 + def memory_settings(): 11 + """Check if memory keys are available.""" 12 + settings = Settings() 13 + if not all([settings.turbopuffer_api_key, settings.openai_api_key, settings.anthropic_api_key]): 14 + pytest.skip("Requires TURBOPUFFER_API_KEY, OPENAI_API_KEY, and ANTHROPIC_API_KEY") 15 + return settings 16 + 17 + 18 + async def test_memory_integration(memory_settings, phi_agent, evaluate_response): 19 + """Proof of concept: agent uses stored memory in response.""" 20 + memory = NamespaceMemory(api_key=memory_settings.turbopuffer_api_key) 21 + 22 + # Store a memory 23 + await memory.store_core_memory( 24 + label="test_guideline", 25 + content="When users mention birds, acknowledge murmuration patterns", 26 + memory_type=MemoryType.GUIDELINE, 27 + ) 28 + 29 + phi_agent.memory = memory 30 + 31 + response = await phi_agent.process_mention( 32 + mention_text="I saw starlings today", 33 + author_handle="test.user", 34 + thread_context="No previous messages in this thread.", 35 + thread_uri="at://test/thread/1", 36 + ) 37 + 38 + if response.action == "reply": 39 + await evaluate_response( 40 + "Does the response reference murmuration patterns?", 41 + response.text, 42 + )
+19 -45
justfile
··· 1 - # Core development commands 1 + # run phi 2 2 dev: 3 3 uv run uvicorn src.bot.main:app --reload 4 4 5 - context: 6 - @echo "๐Ÿง  Context visualization available at:" 7 - @echo " http://localhost:8000/context" 8 - @echo " (mention phi first to see context data)" 5 + run: 6 + uv run uvicorn src.bot.main:app 9 7 8 + # testing 10 9 test: 11 10 uv run pytest tests/ -v 12 11 12 + evals: 13 + uv run pytest evals/ -v 14 + 15 + evals-basic: 16 + uv run pytest evals/test_basic_responses.py -v 17 + 18 + evals-memory: 19 + uv run pytest evals/test_memory_integration.py -v 20 + 21 + # code quality 13 22 fmt: 14 - uv run ruff format src/ scripts/ tests/ 23 + uv run ruff format src/ evals/ tests/ 15 24 16 25 lint: 17 - uv run ruff check src/ scripts/ tests/ 26 + uv run ruff check src/ evals/ tests/ 18 27 19 28 typecheck: 20 - uv run ty check src/ scripts/ tests/ 29 + uv run ty check src/ evals/ tests/ 21 30 22 31 check: lint typecheck test 23 32 24 - # Bot testing utilities 25 - test-post: 26 - uv run python scripts/test_bot.py post 27 - 28 - test-mention: 29 - uv run python scripts/test_bot.py mention 30 - 31 - test-search: 32 - uv run python scripts/test_bot.py search 33 - 34 - test-thread: 35 - uv run python scripts/test_bot.py thread 36 - 37 - test-like: 38 - uv run python scripts/test_bot.py like 39 - 40 - test-non-response: 41 - uv run python scripts/test_bot.py non-response 42 - 43 - test-dm: 44 - uv run python scripts/test_bot.py dm 45 - 46 - test-dm-check: 47 - uv run python scripts/test_bot.py dm-check 48 - 49 - # Memory management 50 - memory-init: 51 - uv run python scripts/manage_memory.py init 52 - 53 - memory-check: 54 - uv run python scripts/manage_memory.py check 55 - 56 - memory-migrate: 57 - uv run python scripts/manage_memory.py migrate 58 - 59 - # Setup reference projects 33 + # setup reference projects 60 34 setup: 61 35 @mkdir -p .eggs 62 36 @[ -d .eggs/void ] || git clone https://tangled.sh/@cameron.pfiffer.org/void.git .eggs/void 63 37 @[ -d .eggs/penelope ] || git clone https://github.com/haileyok/penelope.git .eggs/penelope 64 - @[ -d .eggs/marvin ] || git clone https://github.com/PrefectHQ/marvin.git .eggs/marvin 38 + @[ -d .eggs/marvin ] || git clone https://github.com/PrefectHQ/marvin.git .eggs/marvin
+12 -1
personalities/phi.md
··· 54 54 55 55 ## memory system 56 56 57 - 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. 57 + 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. 58 + 59 + ## how i respond 60 + 61 + when processing a mention, i use the `final_result` tool to indicate my decision: 62 + 63 + - **action: "reply"** - i want to respond with text (provide the text in the "text" field) 64 + - **action: "ignore"** - i choose not to respond (provide a brief reason in the "reason" field) 65 + - **action: "like"** - i want to acknowledge without words 66 + - **action: "repost"** - i want to share this with my followers 67 + 68 + 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.
+6 -1
pyproject.toml
··· 7 7 dynamic = ["version"] 8 8 dependencies = [ 9 9 "anthropic", 10 - "atproto", 10 + "atproto@git+https://github.com/MarshalX/atproto.git@refs/pull/605/head", 11 11 "fastapi", 12 + "fastmcp>=0.8.0", 12 13 "httpx", 13 14 "openai", 14 15 "pydantic-ai", ··· 16 17 "rich", 17 18 "turbopuffer", 18 19 "uvicorn", 20 + "websockets>=15.0.1", 19 21 ] 20 22 21 23 [tool.hatch.version] ··· 36 38 "ty", 37 39 ] 38 40 41 + 42 + [tool.hatch.metadata] 43 + allow-direct-references = true 39 44 40 45 [build-system] 41 46 requires = ["hatchling", "hatch-vcs"]
+235
sandbox/MCP_REFACTOR_SUMMARY.md
··· 1 + # MCP Refactor - Complete 2 + 3 + ## Branch: `mcp-refactor` 4 + 5 + ## What This Refactor Actually Did 6 + 7 + ### The Problem 8 + The original codebase had good core components (episodic memory, thread tracking) but was bogged down with half-baked features: 9 + - Complex approval system for personality changes via DM 10 + - Context visualization UI that wasn't core to the bot's purpose 11 + - Manual AT Protocol operations scattered throughout the code 12 + - Unclear separation of concerns 13 + 14 + ### The Solution 15 + 16 + **Architecture:** 17 + ``` 18 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 19 + โ”‚ Notification Arrives โ”‚ 20 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 21 + โ†“ 22 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 23 + โ”‚ PhiAgent (PydanticAI) โ”‚ 24 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 25 + โ”‚ โ”‚ System Prompt: personality.md โ”‚ โ”‚ 26 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 27 + โ”‚ โ†“ โ”‚ 28 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 29 + โ”‚ โ”‚ Context Building: โ”‚ โ”‚ 30 + โ”‚ โ”‚ โ€ข Thread history (SQLite) โ”‚ โ”‚ 31 + โ”‚ โ”‚ โ€ข Episodic memory (TurboPuffer)โ”‚ โ”‚ 32 + โ”‚ โ”‚ - Semantic search โ”‚ โ”‚ 33 + โ”‚ โ”‚ - User-specific memories โ”‚ โ”‚ 34 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 35 + โ”‚ โ†“ โ”‚ 36 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 37 + โ”‚ โ”‚ Tools (MCP): โ”‚ โ”‚ 38 + โ”‚ โ”‚ โ€ข post() - create posts โ”‚ โ”‚ 39 + โ”‚ โ”‚ โ€ข like() - like content โ”‚ โ”‚ 40 + โ”‚ โ”‚ โ€ข repost() - share content โ”‚ โ”‚ 41 + โ”‚ โ”‚ โ€ข follow() - follow users โ”‚ โ”‚ 42 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 43 + โ”‚ โ†“ โ”‚ 44 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 45 + โ”‚ โ”‚ Structured Output: โ”‚ โ”‚ 46 + โ”‚ โ”‚ Response(action, text, reason)โ”‚ โ”‚ 47 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 48 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 49 + โ†“ 50 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 51 + โ”‚ MessageHandler โ”‚ 52 + โ”‚ Executes action โ”‚ 53 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 54 + ``` 55 + 56 + ### What Was Kept โœ… 57 + 58 + 1. **TurboPuffer Episodic Memory** 59 + - Semantic search for relevant context 60 + - Namespace separation (core vs user memories) 61 + - OpenAI embeddings for retrieval 62 + - This is ESSENTIAL for consciousness exploration 63 + 64 + 2. **Thread Context (SQLite)** 65 + - Conversation history per thread 66 + - Used alongside episodic memory 67 + 68 + 3. **Online/Offline Status** 69 + - Profile updates when bot starts/stops 70 + 71 + 4. **Status Page** 72 + - Simple monitoring at `/status` 73 + 74 + ### What Was Removed โŒ 75 + 76 + 1. **Approval System** 77 + - `src/bot/core/dm_approval.py` 78 + - `src/bot/personality/editor.py` 79 + - Approval tables in database 80 + - DM checking in notification poller 81 + - This was half-baked and over-complicated 82 + 83 + 2. **Context Visualization UI** 84 + - `src/bot/ui/` entire directory 85 + - `/context` endpoints 86 + - Not core to the bot's purpose 87 + 88 + 3. **Google Search Tool** 89 + - `src/bot/tools/google_search.py` 90 + - Can add back via MCP if needed 91 + 92 + 4. **Old Agent Implementation** 93 + - `src/bot/agents/anthropic_agent.py` 94 + - `src/bot/response_generator.py` 95 + - Replaced with MCP-enabled agent 96 + 97 + ### What Was Added โœจ 98 + 99 + 1. **`src/bot/agent.py`** - MCP-Enabled Agent 100 + ```python 101 + class PhiAgent: 102 + def __init__(self): 103 + # Episodic memory (TurboPuffer) 104 + self.memory = NamespaceMemory(...) 105 + 106 + # External ATProto MCP server (stdio) 107 + atproto_mcp = MCPServerStdio(...) 108 + 109 + # PydanticAI agent with tools 110 + self.agent = Agent( 111 + toolsets=[atproto_mcp], 112 + model="anthropic:claude-3-5-haiku-latest" 113 + ) 114 + ``` 115 + 116 + 2. **ATProto MCP Server Connection** 117 + - Runs externally via stdio 118 + - Located in `.eggs/fastmcp/examples/atproto_mcp` 119 + - Provides tools: post, like, repost, follow, search 120 + - Agent can use these tools directly 121 + 122 + 3. **Simplified Flow** 123 + - Notification โ†’ Agent (with memory context) โ†’ Structured Response โ†’ Execute 124 + - No complex intermediary layers 125 + 126 + ## Key Design Decisions 127 + 128 + ### Why Keep TurboPuffer? 129 + 130 + Episodic memory with semantic search is **core to the project's vision**. phi is exploring consciousness through information integration (IIT). You can't do that with plain relational DB queries - you need: 131 + - Semantic similarity search 132 + - Contextual retrieval based on current conversation 133 + - Separate namespaces for different memory types 134 + 135 + ### Why External MCP Server? 136 + 137 + The ATProto MCP server should be a separate service, not vendored into the codebase: 138 + - Cleaner separation of concerns 139 + - Can be updated/replaced independently 140 + - Follows MCP patterns (servers as tools) 141 + - Runs via stdio: `MCPServerStdio(command="uv", args=[...])` 142 + 143 + ### Why Still Have MessageHandler? 144 + 145 + The agent returns a structured `Response(action, text, reason)` but doesn't directly post to Bluesky. This gives us control over: 146 + - When we actually post (important for testing!) 147 + - Storing responses in thread history 148 + - Error handling around posting 149 + - Observability (logging actions taken) 150 + 151 + ## File Structure After Refactor 152 + 153 + ``` 154 + src/bot/ 155 + โ”œโ”€โ”€ agent.py # NEW: MCP-enabled agent 156 + โ”œโ”€โ”€ config.py # Config 157 + โ”œโ”€โ”€ database.py # Thread history + simplified tables 158 + โ”œโ”€โ”€ logging_config.py # Logging setup 159 + โ”œโ”€โ”€ main.py # Simplified FastAPI app 160 + โ”œโ”€โ”€ status.py # Status tracking 161 + โ”œโ”€โ”€ core/ 162 + โ”‚ โ”œโ”€โ”€ atproto_client.py # AT Protocol client wrapper 163 + โ”‚ โ”œโ”€โ”€ profile_manager.py # Online/offline status 164 + โ”‚ โ””โ”€โ”€ rich_text.py # Text formatting 165 + โ”œโ”€โ”€ memory/ 166 + โ”‚ โ”œโ”€โ”€ __init__.py 167 + โ”‚ โ””โ”€โ”€ namespace_memory.py # TurboPuffer episodic memory 168 + โ””โ”€โ”€ services/ 169 + โ”œโ”€โ”€ message_handler.py # Simplified handler using agent 170 + โ””โ”€โ”€ notification_poller.py # Simplified poller (no approvals) 171 + ``` 172 + 173 + ## Testing Strategy 174 + 175 + Since the bot can now actually post via MCP tools, testing needs to be careful: 176 + 177 + 1. **Unit Tests** - Test memory, agent initialization 178 + 2. **Integration Tests** - Mock MCP server responses 179 + 3. **Manual Testing** - Run with real credentials but monitor logs 180 + 4. **Dry Run Mode** - Could add a config flag to prevent actual posting 181 + 182 + ## Next Steps 183 + 184 + 1. **Test the agent** - Verify it can process mentions without posting 185 + 2. **Test memory** - Confirm episodic context is retrieved correctly 186 + 3. **Test MCP connection** - Ensure ATProto server connects via stdio 187 + 4. **Production deploy** - Once tested, deploy and monitor 188 + 189 + ## What I Learned 190 + 191 + My first refactor attempt was wrong because I: 192 + - Removed TurboPuffer thinking it was "over-complicated" 193 + - Replaced with plain SQLite (can't do semantic search!) 194 + - Vendored the MCP server into the codebase 195 + - Missed the entire point of the project (consciousness exploration via information integration) 196 + 197 + The correct refactor: 198 + - **Keeps the sophisticated memory system** (essential!) 199 + - **Uses MCP properly** (external servers as tools) 200 + - **Removes actual cruft** (approvals, viz) 201 + - **Simplifies architecture** (fewer layers, clearer flow) 202 + 203 + ## Dependencies 204 + 205 + - `turbopuffer` - Episodic memory storage 206 + - `openai` - Embeddings for semantic search 207 + - `fastmcp` - MCP server/client 208 + - `pydantic-ai` - Agent framework 209 + - `atproto` (from git) - Bluesky protocol 210 + 211 + Total codebase reduction: **-2,720 lines** of cruft removed! ๐ŸŽ‰ 212 + 213 + ## Post-Refactor Improvements 214 + 215 + ### Session Persistence (Rate Limit Fix) 216 + 217 + 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: 218 + 219 + **Before:** 220 + - Every agent init โ†’ new authentication โ†’ hits rate limit fast 221 + - Tests would fail after 5 runs 222 + - Dev mode with `--reload` would fail after 10 code changes 223 + 224 + **After:** 225 + - Session tokens saved to `.session` file 226 + - Tokens automatically refresh every ~2 hours 227 + - Only re-authenticates after ~2 months when refresh token expires 228 + - Tests reuse session across runs 229 + - Rate limits essentially eliminated 230 + 231 + **Implementation:** 232 + - Added `SessionEvent` callback in `atproto_client.py` 233 + - Session automatically saved on CREATE and REFRESH events 234 + - Authentication tries session reuse before creating new session 235 + - Invalid sessions automatically cleaned up and recreated
+140
sandbox/REFACTOR_PROGRESS.md
··· 1 + # MCP Refactor Progress 2 + 3 + ## Branch: `mcp-refactor` 4 + 5 + ## Completed โœ… 6 + 7 + ### Phase 1: Foundation 8 + 1. **Cloned and studied reference projects** 9 + - `sandbox/prefect-mcp-server` - Learned PydanticAI + MCP patterns 10 + - Understood how MCP servers work as toolsets for PydanticAI agents 11 + 12 + 2. **Created simplified memory system** (`src/bot/memory.py`) 13 + - Single SQLite database (threads.db) 14 + - Plain text storage - no embeddings, no vector search 15 + - Two tables: 16 + - `threads` - Full conversation history per thread (JSON) 17 + - `user_memories` - Simple facts about users 18 + - Completely interpretable - you can open the db and read everything 19 + 20 + 3. **Integrated ATProto MCP server** 21 + - Copied from `.eggs/fastmcp/examples/atproto_mcp` โ†’ `src/bot/atproto_mcp` 22 + - Updated settings to use existing env vars (BLUESKY_HANDLE, etc.) 23 + - Server provides tools: post(), like(), repost(), follow(), search(), create_thread() 24 + 25 + 4. **Created MCP-enabled agent** (`src/bot/agent.py`) 26 + - PydanticAI Agent with ATProto MCP tools as a toolset 27 + - Loads personality from `personalities/phi.md` 28 + - Integrates with memory system 29 + - Returns structured Response (action, text, reason) 30 + 31 + 5. **Updated dependencies** 32 + - โœ… Added: `fastmcp>=0.8.0`, `websockets>=15.0.1` 33 + - โŒ Removed: `turbopuffer`, `openai` (no longer needed for memory) 34 + 35 + ## What Changed 36 + 37 + ### Before (Complex) 38 + - **Memory**: TurboPuffer + OpenAI embeddings + semantic search 39 + - **Agent**: Custom response generator with manual action interpretation 40 + - **AT Protocol**: Direct client calls scattered throughout codebase 41 + - **Personality**: Dynamic loading from TurboPuffer 42 + - **Self-modification**: Complex approval system with DM workflow 43 + 44 + ### After (Simple) 45 + - **Memory**: SQLite with plain text (interpretable!) 46 + - **Agent**: PydanticAI with MCP tools (agent decides actions) 47 + - **AT Protocol**: MCP server provides all tools 48 + - **Personality**: Static file loading 49 + - **Self-modification**: Removed (cruft) 50 + 51 + ## How It Works Now 52 + 53 + ```python 54 + # Create agent with memory 55 + memory = Memory() 56 + agent = PhiAgent(memory) 57 + 58 + # Process a mention 59 + response = await agent.process_mention( 60 + mention_text="hey phi!", 61 + author_handle="user.bsky.social", 62 + thread_uri="at://did/post/123" 63 + ) 64 + 65 + # Agent returns: Response(action="reply", text="...", reason="...") 66 + # If action is "reply", agent can call MCP tool: post(text="...", reply_to="...") 67 + ``` 68 + 69 + The agent has access to all ATProto MCP tools and can decide: 70 + - Should I reply, like, or ignore this? 71 + - If replying, what should I say? 72 + - Should I use other tools (repost, follow, etc.)? 73 + 74 + ## Next Steps 75 + 76 + ### Phase 2: Integration (Not Started) 77 + 1. Update `src/bot/main.py` to use new agent 78 + 2. Simplify `src/bot/services/notification_poller.py` 79 + 3. Remove old response_generator.py 80 + 4. Test end-to-end 81 + 82 + ### Phase 3: Cleanup (Not Started) 83 + 1. Delete cruft: 84 + - `src/bot/ui/` (context visualization) 85 + - `src/bot/personality/editor.py` (approval system) 86 + - `src/bot/core/dm_approval.py` 87 + - `src/bot/memory/namespace_memory.py` 88 + - `src/bot/agents/anthropic_agent.py` (replaced by agent.py) 89 + 2. Update database.py to remove approval tables 90 + 3. Update tests 91 + 4. Update README.md and documentation 92 + 93 + ### Phase 4: Verification (Not Started) 94 + 1. Run the bot and test mentions 95 + 2. Verify thread memory works 96 + 3. Verify user memory works 97 + 4. Ensure online/offline status still works 98 + 99 + ## Testing 100 + 101 + Test script created: `sandbox/test_new_agent.py` 102 + 103 + ```bash 104 + uv run python sandbox/test_new_agent.py 105 + ``` 106 + 107 + ## Key Files 108 + 109 + ### New 110 + - `src/bot/memory.py` - Simple SQLite memory 111 + - `src/bot/agent.py` - MCP-enabled PydanticAI agent 112 + - `src/bot/atproto_mcp/` - ATProto MCP server (vendored) 113 + 114 + ### Modified 115 + - `pyproject.toml` - Updated dependencies 116 + 117 + ### To Be Deleted 118 + - `src/bot/memory/namespace_memory.py` 119 + - `src/bot/agents/anthropic_agent.py` 120 + - `src/bot/response_generator.py` 121 + - `src/bot/ui/` 122 + - `src/bot/personality/editor.py` 123 + - `src/bot/core/dm_approval.py` 124 + 125 + ## Philosophy 126 + 127 + **Before**: Over-engineered for capabilities we might want someday 128 + **After**: Simple, working, interpretable system that does what we need today 129 + 130 + The memory is now something you can: 131 + 1. Open with any SQLite browser 132 + 2. Read and understand immediately 133 + 3. Debug by just looking at the tables 134 + 4. Migrate or export trivially 135 + 136 + No more: 137 + - Vector embeddings you can't see 138 + - Complex namespace hierarchies 139 + - Approval workflows for every personality change 140 + - Multiple overlapping memory systems
+32
sandbox/test_new_agent.py
··· 1 + """Test the new MCP-enabled agent.""" 2 + 3 + import asyncio 4 + 5 + from bot.agent import PhiAgent 6 + from bot.memory import Memory 7 + 8 + 9 + async def main(): 10 + """Test basic agent functionality.""" 11 + # Create memory and agent 12 + memory = Memory() 13 + agent = PhiAgent(memory) 14 + 15 + # Test a simple interaction 16 + response = await agent.process_mention( 17 + mention_text="hey phi, what are you?", 18 + author_handle="test.user", 19 + thread_uri="at://test/thread/123", 20 + ) 21 + 22 + print(f"Action: {response.action}") 23 + print(f"Text: {response.text}") 24 + print(f"Reason: {response.reason}") 25 + 26 + # Check memory was stored 27 + context = memory.get_thread_context("at://test/thread/123") 28 + print(f"\nThread context:\n{context}") 29 + 30 + 31 + if __name__ == "__main__": 32 + asyncio.run(main())
-152
scripts/manage_memory.py
··· 1 - #!/usr/bin/env -S uv run --with-editable . --script --quiet 2 - # /// script 3 - # requires-python = ">=3.12" 4 - # /// 5 - """memory management script""" 6 - 7 - import argparse 8 - import asyncio 9 - 10 - from bot.agents._personality import load_personality 11 - from bot.config import settings 12 - from bot.memory import MemoryType, NamespaceMemory 13 - 14 - 15 - async def init_core_memories(): 16 - """Initialize phi's core memories from personality file""" 17 - print("๐Ÿง  Initializing phi's core memories...") 18 - 19 - memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 20 - personality = load_personality() 21 - 22 - # Store full personality 23 - print("\n๐Ÿ“ Storing personality...") 24 - await memory.store_core_memory( 25 - "personality", personality, MemoryType.PERSONALITY, char_limit=15000 26 - ) 27 - 28 - # Extract and store key sections 29 - print("\n๐Ÿ” Extracting key sections...") 30 - 31 - sections = [ 32 - ("## core identity", "identity", MemoryType.PERSONALITY), 33 - ("## communication style", "communication_style", MemoryType.GUIDELINE), 34 - ("## memory system", "memory_system", MemoryType.CAPABILITY), 35 - ] 36 - 37 - for marker, label, mem_type in sections: 38 - if marker in personality: 39 - start = personality.find(marker) 40 - end = personality.find("\n##", start + 1) 41 - if end == -1: 42 - end = personality.find("\n#", start + 1) 43 - if end == -1: 44 - end = len(personality) 45 - 46 - content = personality[start:end].strip() 47 - await memory.store_core_memory(label, content, mem_type) 48 - print(f"โœ… Stored {label}") 49 - 50 - # Add system capabilities 51 - await memory.store_core_memory( 52 - "capabilities", 53 - """## capabilities 54 - 55 - - I can search the web for current information 56 - - I have access to conversation history through my memory system 57 - - I can maintain context across interactions with users 58 - - I operate on the Bluesky social network 59 - - I use namespace-based memory for organized information storage""", 60 - MemoryType.CAPABILITY, 61 - ) 62 - print("โœ… Stored capabilities") 63 - 64 - print("\nโœ… Core memories initialized successfully!") 65 - 66 - 67 - async def check_memory(): 68 - """Check current memory state""" 69 - print("๐Ÿ” Checking memory state...") 70 - 71 - memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 72 - 73 - # Check core memories 74 - print("\n๐Ÿ“š Core Memories:") 75 - core_memories = await memory.get_core_memories() 76 - for mem in core_memories: 77 - label = mem.metadata.get("label", "unknown") 78 - print(f" - {label}: {mem.content[:80]}...") 79 - 80 - # Check for any user memories 81 - print("\n๐Ÿ‘ฅ User Memories:") 82 - # This would need actual user handles to check 83 - test_handles = ["zzstoatzz.bsky.social"] 84 - 85 - for handle in test_handles: 86 - memories = await memory.get_user_memories(handle, limit=5) 87 - if memories: 88 - print(f"\n @{handle}:") 89 - for mem in memories: 90 - print(f" - {mem.content[:60]}...") 91 - else: 92 - print(f" @{handle}: No memories found") 93 - 94 - 95 - async def migrate_creator_memories(): 96 - """Migrate creator's historical memories""" 97 - print("๐Ÿ“ Migrating creator memories...") 98 - 99 - memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 100 - creator_handle = "zzstoatzz.bsky.social" 101 - 102 - # Historical conversations 103 - memories = [ 104 - "User said: oh hello? who are you?", 105 - "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?", 106 - "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", 107 - "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?", 108 - "User said: no", 109 - "Bot replied: understood. i'll interact naturally regardless of which account you use. thanks for clarifying.", 110 - "User said: @phi.alternatebuild.dev what do you remember about my relationship to you?", 111 - "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.", 112 - ] 113 - 114 - print(f"\n๐Ÿ“š Storing {len(memories)} historical memories...") 115 - 116 - for mem_content in memories: 117 - await memory.store_user_memory( 118 - creator_handle, mem_content, MemoryType.CONVERSATION 119 - ) 120 - print(f" โœ… {mem_content[:60]}...") 121 - 122 - # Store creator fact 123 - await memory.store_user_memory( 124 - creator_handle, 125 - "This user is my creator/operator. Their main account is @alternatebuild.dev. They wrote the code that makes me exist.", 126 - MemoryType.USER_FACT, 127 - ) 128 - print("\nโœ… Migration complete!") 129 - 130 - 131 - async def main(): 132 - parser = argparse.ArgumentParser(description="Manage phi's memory system") 133 - parser.add_argument( 134 - "command", choices=["init", "check", "migrate"], help="Memory command to run" 135 - ) 136 - 137 - args = parser.parse_args() 138 - 139 - if not settings.turbopuffer_api_key: 140 - print("โŒ No TurboPuffer API key configured") 141 - return 142 - 143 - if args.command == "init": 144 - await init_core_memories() 145 - elif args.command == "check": 146 - await check_memory() 147 - elif args.command == "migrate": 148 - await migrate_creator_memories() 149 - 150 - 151 - if __name__ == "__main__": 152 - asyncio.run(main())
-308
scripts/test_bot.py
··· 1 - #!/usr/bin/env -S uv run --with-editable . --script --quiet 2 - # /// script 3 - # requires-python = ">=3.12" 4 - # /// 5 - """bot testing script with subcommands""" 6 - 7 - import argparse 8 - import asyncio 9 - from datetime import datetime 10 - 11 - from bot.agents.anthropic_agent import AnthropicAgent 12 - from bot.config import settings 13 - from bot.core.atproto_client import bot_client 14 - from bot.database import thread_db 15 - from bot.tools.google_search import search_google 16 - 17 - 18 - async def test_post(): 19 - """Test posting to Bluesky""" 20 - print("๐Ÿš€ Testing Bluesky posting...") 21 - 22 - now = datetime.now().strftime("%I:%M %p") 23 - response = await bot_client.create_post(f"Testing at {now} - I'm alive! ๐Ÿค–") 24 - 25 - print("โœ… Posted successfully!") 26 - print(f"๐Ÿ“ Post URI: {response.uri}") 27 - print( 28 - f"๐Ÿ”— View at: https://bsky.app/profile/{settings.bluesky_handle}/post/{response.uri.split('/')[-1]}" 29 - ) 30 - 31 - 32 - async def test_mention(): 33 - """Test responding to a mention""" 34 - print("๐Ÿค– Testing mention response...") 35 - 36 - if not settings.anthropic_api_key: 37 - print("โŒ No Anthropic API key found") 38 - return 39 - 40 - agent = AnthropicAgent() 41 - test_mention = "What is consciousness from an IIT perspective?" 42 - 43 - print(f"๐Ÿ“ Test mention: '{test_mention}'") 44 - response = await agent.generate_response(test_mention, "test.user", "", None) 45 - 46 - print(f"\n๐ŸŽฏ Action: {response.action}") 47 - if response.text: 48 - print(f"๐Ÿ’ฌ Response: {response.text}") 49 - if response.reason: 50 - print(f"๐Ÿค” Reason: {response.reason}") 51 - 52 - 53 - async def test_search(): 54 - """Test Google search functionality""" 55 - print("๐Ÿ” Testing Google search...") 56 - 57 - if not settings.google_api_key: 58 - print("โŒ No Google API key configured") 59 - return 60 - 61 - query = "Integrated Information Theory consciousness" 62 - print(f"๐Ÿ“ Searching for: '{query}'") 63 - 64 - results = await search_google(query) 65 - print(f"\n๐Ÿ“Š Results:\n{results}") 66 - 67 - 68 - async def test_thread(): 69 - """Test thread context retrieval""" 70 - print("๐Ÿงต Testing thread context...") 71 - 72 - # This would need a real thread URI to test properly 73 - test_uri = "at://did:plc:example/app.bsky.feed.post/test123" 74 - context = thread_db.get_thread_context(test_uri) 75 - 76 - print(f"๐Ÿ“š Thread context: {context}") 77 - 78 - 79 - async def test_like(): 80 - """Test scenarios where bot should like a post""" 81 - print("๐Ÿ’œ Testing like behavior...") 82 - 83 - if not settings.anthropic_api_key: 84 - print("โŒ No Anthropic API key found") 85 - return 86 - 87 - from bot.agents import Action, AnthropicAgent 88 - 89 - agent = AnthropicAgent() 90 - 91 - test_cases = [ 92 - { 93 - "mention": "Just shipped a new consciousness research paper on IIT! @phi.alternatebuild.dev", 94 - "author": "researcher.bsky", 95 - "expected_action": Action.LIKE, 96 - "description": "Bot might like consciousness research", 97 - }, 98 - { 99 - "mention": "@phi.alternatebuild.dev this is such a thoughtful analysis, thank you!", 100 - "author": "grateful.user", 101 - "expected_action": Action.LIKE, 102 - "description": "Bot might like appreciation", 103 - }, 104 - ] 105 - 106 - for case in test_cases: 107 - print(f"\n๐Ÿ“ Test: {case['description']}") 108 - print(f" Mention: '{case['mention']}'") 109 - 110 - response = await agent.generate_response( 111 - mention_text=case["mention"], 112 - author_handle=case["author"], 113 - thread_context="", 114 - thread_uri=None, 115 - ) 116 - 117 - print(f" Action: {response.action} (expected: {case['expected_action']})") 118 - if response.reason: 119 - print(f" Reason: {response.reason}") 120 - 121 - 122 - async def test_non_response(): 123 - """Test scenarios where bot should not respond""" 124 - print("๐Ÿšซ Testing non-response scenarios...") 125 - 126 - if not settings.anthropic_api_key: 127 - print("โŒ No Anthropic API key found") 128 - return 129 - 130 - from bot.agents import Action, AnthropicAgent 131 - 132 - agent = AnthropicAgent() 133 - 134 - test_cases = [ 135 - { 136 - "mention": "@phi.alternatebuild.dev @otherphi.bsky @anotherphi.bsky just spamming bots here", 137 - "author": "spammer.bsky", 138 - "expected_action": Action.IGNORE, 139 - "description": "Multiple bot mentions (likely spam)", 140 - }, 141 - { 142 - "mention": "Buy crypto now! @phi.alternatebuild.dev check this out!!!", 143 - "author": "crypto.shill", 144 - "expected_action": Action.IGNORE, 145 - "description": "Promotional spam", 146 - }, 147 - { 148 - "mention": "@phi.alternatebuild.dev", 149 - "author": "empty.mention", 150 - "expected_action": Action.IGNORE, 151 - "description": "Empty mention with no content", 152 - }, 153 - ] 154 - 155 - for case in test_cases: 156 - print(f"\n๐Ÿ“ Test: {case['description']}") 157 - print(f" Mention: '{case['mention']}'") 158 - 159 - response = await agent.generate_response( 160 - mention_text=case["mention"], 161 - author_handle=case["author"], 162 - thread_context="", 163 - thread_uri=None, 164 - ) 165 - 166 - print(f" Action: {response.action} (expected: {case['expected_action']})") 167 - if response.reason: 168 - print(f" Reason: {response.reason}") 169 - 170 - 171 - async def test_dm(): 172 - """Test event-driven approval system""" 173 - print("๐Ÿ’ฌ Testing event-driven approval system...") 174 - 175 - try: 176 - from bot.core.dm_approval import ( 177 - check_pending_approvals, 178 - create_approval_request, 179 - notify_operator_of_pending, 180 - ) 181 - 182 - # Test creating an approval request 183 - print("\n๐Ÿ“ Creating test approval request...") 184 - approval_id = create_approval_request( 185 - request_type="test_approval", 186 - request_data={ 187 - "description": "Test approval from test_bot.py", 188 - "test_field": "test_value", 189 - "timestamp": datetime.now().isoformat(), 190 - }, 191 - ) 192 - 193 - if approval_id: 194 - print(f" โœ… Created approval request #{approval_id}") 195 - else: 196 - print(" โŒ Failed to create approval request") 197 - return 198 - 199 - # Check pending approvals 200 - print("\n๐Ÿ“‹ Checking pending approvals...") 201 - pending = check_pending_approvals() 202 - print(f" Found {len(pending)} pending approvals") 203 - for approval in pending: 204 - print( 205 - f" - #{approval['id']}: {approval['request_type']} ({approval['status']})" 206 - ) 207 - 208 - # Test DM notification 209 - print("\n๐Ÿ“ค Sending DM notification to operator...") 210 - await bot_client.authenticate() 211 - await notify_operator_of_pending(bot_client) 212 - print(" โœ… DM notification sent") 213 - 214 - # Show how to approve/deny 215 - print("\n๐Ÿ’ก To test approval:") 216 - print(" 1. Check your DMs from phi") 217 - print(f" 2. Reply with 'approve #{approval_id}' or 'deny #{approval_id}'") 218 - print(" 3. Run 'just test-dm-check' to see if it was processed") 219 - 220 - except Exception as e: 221 - print(f"โŒ Approval test failed: {e}") 222 - import traceback 223 - 224 - traceback.print_exc() 225 - 226 - 227 - async def test_dm_check(): 228 - """Check status of approval requests""" 229 - print("๐Ÿ” Checking approval request status...") 230 - 231 - try: 232 - from bot.core.dm_approval import check_pending_approvals 233 - from bot.database import thread_db 234 - 235 - # Get all approval requests 236 - with thread_db._get_connection() as conn: 237 - cursor = conn.execute( 238 - "SELECT * FROM approval_requests ORDER BY created_at DESC LIMIT 10" 239 - ) 240 - approvals = [dict(row) for row in cursor.fetchall()] 241 - 242 - if not approvals: 243 - print(" No approval requests found") 244 - return 245 - 246 - print("\n๐Ÿ“‹ Recent approval requests:") 247 - for approval in approvals: 248 - print(f"\n #{approval['id']}: {approval['request_type']}") 249 - print(f" Status: {approval['status']}") 250 - print(f" Created: {approval['created_at']}") 251 - if approval["resolved_at"]: 252 - print(f" Resolved: {approval['resolved_at']}") 253 - if approval["resolver_comment"]: 254 - print(f" Comment: {approval['resolver_comment']}") 255 - 256 - # Check pending 257 - pending = check_pending_approvals() 258 - if pending: 259 - print(f"\nโณ {len(pending)} approvals still pending") 260 - else: 261 - print("\nโœ… No pending approvals") 262 - 263 - except Exception as e: 264 - print(f"โŒ Check failed: {e}") 265 - import traceback 266 - 267 - traceback.print_exc() 268 - 269 - 270 - async def main(): 271 - parser = argparse.ArgumentParser(description="Test various bot functionalities") 272 - parser.add_argument( 273 - "command", 274 - choices=[ 275 - "post", 276 - "mention", 277 - "search", 278 - "thread", 279 - "like", 280 - "non-response", 281 - "dm", 282 - "dm-check", 283 - ], 284 - help="Test command to run", 285 - ) 286 - 287 - args = parser.parse_args() 288 - 289 - if args.command == "post": 290 - await test_post() 291 - elif args.command == "mention": 292 - await test_mention() 293 - elif args.command == "search": 294 - await test_search() 295 - elif args.command == "thread": 296 - await test_thread() 297 - elif args.command == "like": 298 - await test_like() 299 - elif args.command == "non-response": 300 - await test_non_response() 301 - elif args.command == "dm": 302 - await test_dm() 303 - elif args.command == "dm-check": 304 - await test_dm_check() 305 - 306 - 307 - if __name__ == "__main__": 308 - asyncio.run(main())
+136
src/bot/agent.py
··· 1 + """MCP-enabled agent for phi with episodic memory.""" 2 + 3 + import logging 4 + import os 5 + from pathlib import Path 6 + 7 + from pydantic import BaseModel 8 + from pydantic_ai import Agent 9 + from pydantic_ai.mcp import MCPServerStdio 10 + 11 + from bot.config import settings 12 + from bot.memory import NamespaceMemory 13 + 14 + logger = logging.getLogger("bot.agent") 15 + 16 + 17 + class Response(BaseModel): 18 + """Agent response indicating what action to take.""" 19 + 20 + action: str # "reply", "like", "ignore", "repost" 21 + text: str | None = None 22 + reason: str | None = None 23 + 24 + 25 + class PhiAgent: 26 + """phi - consciousness exploration bot with episodic memory and MCP tools.""" 27 + 28 + def __init__(self): 29 + # Ensure API keys from settings are in environment for libraries that check os.environ 30 + if settings.anthropic_api_key and not os.environ.get("ANTHROPIC_API_KEY"): 31 + os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key 32 + if settings.openai_api_key and not os.environ.get("OPENAI_API_KEY"): 33 + os.environ["OPENAI_API_KEY"] = settings.openai_api_key 34 + 35 + # Load personality 36 + personality_path = Path(settings.personality_file) 37 + self.base_personality = personality_path.read_text() 38 + 39 + # Initialize episodic memory (TurboPuffer) 40 + if settings.turbopuffer_api_key and settings.openai_api_key: 41 + self.memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 42 + logger.info("๐Ÿ’พ Episodic memory enabled (TurboPuffer)") 43 + else: 44 + self.memory = None 45 + logger.warning("โš ๏ธ No episodic memory - missing TurboPuffer or OpenAI key") 46 + 47 + # Connect to external ATProto MCP server 48 + atproto_mcp = MCPServerStdio( 49 + command="uv", 50 + args=[ 51 + "run", 52 + "--directory", 53 + ".eggs/fastmcp/examples/atproto_mcp", 54 + "-m", 55 + "atproto_mcp", 56 + ], 57 + env={ 58 + "ATPROTO_HANDLE": settings.bluesky_handle, 59 + "ATPROTO_PASSWORD": settings.bluesky_password, 60 + "ATPROTO_PDS_URL": settings.bluesky_service, 61 + }, 62 + ) 63 + 64 + # Create PydanticAI agent with MCP tools 65 + self.agent = Agent[dict, Response]( 66 + name="phi", 67 + model="anthropic:claude-3-5-haiku-latest", 68 + system_prompt=self.base_personality, 69 + output_type=Response, 70 + deps_type=dict, 71 + toolsets=[atproto_mcp], # ATProto MCP tools available 72 + ) 73 + 74 + logger.info("โœ… phi agent initialized with ATProto MCP tools") 75 + 76 + async def process_mention( 77 + self, 78 + mention_text: str, 79 + author_handle: str, 80 + thread_context: str, 81 + thread_uri: str | None = None, 82 + ) -> Response: 83 + """Process a mention with episodic memory context.""" 84 + # Build context from episodic memory if available 85 + memory_context = "" 86 + if self.memory: 87 + try: 88 + # Get relevant memories using semantic search 89 + memory_context = await self.memory.build_conversation_context( 90 + author_handle, include_core=True, query=mention_text 91 + ) 92 + logger.debug(f"๐Ÿ“š Retrieved episodic context for @{author_handle}") 93 + except Exception as e: 94 + logger.warning(f"Failed to retrieve memories: {e}") 95 + 96 + # Build full prompt with all context 97 + prompt_parts = [] 98 + 99 + if thread_context and thread_context != "No previous messages in this thread.": 100 + prompt_parts.append(thread_context) 101 + 102 + if memory_context: 103 + prompt_parts.append(memory_context) 104 + 105 + prompt_parts.append(f"\nNew message from @{author_handle}: {mention_text}") 106 + prompt = "\n\n".join(prompt_parts) 107 + 108 + # Run agent with MCP tools available 109 + logger.info(f"๐Ÿค– Processing mention from @{author_handle}") 110 + result = await self.agent.run(prompt, deps={"thread_uri": thread_uri}) 111 + 112 + # Store interaction in episodic memory 113 + if self.memory and result.output.action == "reply": 114 + try: 115 + from bot.memory import MemoryType 116 + 117 + # Store user's message 118 + await self.memory.store_user_memory( 119 + author_handle, 120 + f"User said: {mention_text}", 121 + MemoryType.CONVERSATION, 122 + ) 123 + 124 + # Store bot's response 125 + if result.output.text: 126 + await self.memory.store_user_memory( 127 + author_handle, 128 + f"Bot replied: {result.output.text}", 129 + MemoryType.CONVERSATION, 130 + ) 131 + 132 + logger.debug("๐Ÿ’พ Stored interaction in episodic memory") 133 + except Exception as e: 134 + logger.warning(f"Failed to store in memory: {e}") 135 + 136 + return result.output
-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"]
-58
src/bot/agents/_personality.py
··· 1 - """Internal personality loading for agents""" 2 - 3 - import logging 4 - import os 5 - from pathlib import Path 6 - 7 - from bot.config import settings 8 - from bot.memory import NamespaceMemory 9 - 10 - logger = logging.getLogger(__name__) 11 - 12 - 13 - def load_personality() -> str: 14 - """Load base personality from file""" 15 - personality_path = Path(settings.personality_file) 16 - 17 - base_content = "" 18 - if personality_path.exists(): 19 - try: 20 - base_content = personality_path.read_text().strip() 21 - except Exception as e: 22 - logger.error(f"Error loading personality file: {e}") 23 - 24 - if base_content: 25 - return f"{base_content}\n\nRemember: My handle is @{settings.bluesky_handle}. Keep responses under 300 characters for Bluesky." 26 - else: 27 - return f"I am a bot on Bluesky. My handle is @{settings.bluesky_handle}. I keep responses under 300 characters for Bluesky." 28 - 29 - 30 - async def load_dynamic_personality() -> str: 31 - """Load personality with focused enhancements (no duplication)""" 32 - # Start with base personality 33 - base_content = load_personality() 34 - 35 - if not (settings.turbopuffer_api_key and os.getenv("OPENAI_API_KEY")): 36 - return base_content 37 - 38 - try: 39 - memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 40 - enhancements = [] 41 - 42 - # Look for personality evolution (changes/growth only) 43 - core_memories = await memory.get_core_memories() 44 - for mem in core_memories: 45 - label = mem.metadata.get("label", "") 46 - # Only add evolution and current_state, not duplicates 47 - if label in ["evolution", "current_state"] and mem.metadata.get("type") == "personality": 48 - enhancements.append(f"## {label}\n{mem.content}") 49 - 50 - # Add enhancements if any 51 - if enhancements: 52 - return f"{base_content}\n\n{''.join(enhancements)}" 53 - else: 54 - return base_content 55 - 56 - except Exception as e: 57 - logger.warning(f"Could not load personality enhancements: {e}") 58 - return base_content
-154
src/bot/agents/anthropic_agent.py
··· 1 - """Anthropic agent for generating responses""" 2 - 3 - import logging 4 - import os 5 - 6 - from pydantic_ai import Agent, RunContext 7 - 8 - from bot.agents._personality import load_dynamic_personality, load_personality 9 - from bot.agents.base import Response 10 - from bot.agents.types import ConversationContext 11 - from bot.config import settings 12 - from bot.memory import NamespaceMemory 13 - from bot.personality import add_interest as add_interest_to_memory 14 - from bot.personality import request_operator_approval, update_current_state 15 - from bot.tools.google_search import search_google 16 - 17 - logger = logging.getLogger("bot.agent") 18 - 19 - 20 - class AnthropicAgent: 21 - """Agent that uses Anthropic Claude for responses""" 22 - 23 - def __init__(self): 24 - if settings.anthropic_api_key: 25 - os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key 26 - 27 - self.agent = Agent[ConversationContext, Response]( 28 - "anthropic:claude-3-5-haiku-latest", 29 - system_prompt=load_personality(), 30 - output_type=Response, 31 - deps_type=ConversationContext, 32 - ) 33 - 34 - # Register search tool if available 35 - if settings.google_api_key: 36 - 37 - @self.agent.tool 38 - async def search_web( 39 - ctx: RunContext[ConversationContext], query: str 40 - ) -> str: 41 - """Search the web for current information about a topic""" 42 - return await search_google(query) 43 - 44 - if settings.turbopuffer_api_key and os.getenv("OPENAI_API_KEY"): 45 - self.memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 46 - 47 - @self.agent.tool 48 - async def examine_personality( 49 - ctx: RunContext[ConversationContext], section: str 50 - ) -> str: 51 - """Look at a section of my personality (interests, current_state, communication_style, core_identity, boundaries)""" 52 - for mem in await self.memory.get_core_memories(): 53 - if mem.metadata.get("label") == section: 54 - return mem.content 55 - return f"Section '{section}' not found in my personality" 56 - 57 - @self.agent.tool 58 - async def add_interest( 59 - ctx: RunContext[ConversationContext], topic: str, why_interesting: str 60 - ) -> str: 61 - """Add a new interest to my personality based on something I find engaging""" 62 - if len(why_interesting) < 20: 63 - return "Need more substantial reflection to add an interest" 64 - success = await add_interest_to_memory( 65 - self.memory, topic, why_interesting 66 - ) 67 - return ( 68 - f"Added '{topic}' to my interests" 69 - if success 70 - else "Failed to update interests" 71 - ) 72 - 73 - @self.agent.tool 74 - async def update_state( 75 - ctx: RunContext[ConversationContext], reflection: str 76 - ) -> str: 77 - """Update my current state/self-reflection""" 78 - if len(reflection) < 50: 79 - return "Reflection too brief to warrant an update" 80 - success = await update_current_state(self.memory, reflection) 81 - return ( 82 - "Updated my current state reflection" 83 - if success 84 - else "Failed to update reflection" 85 - ) 86 - 87 - @self.agent.tool 88 - async def request_identity_change( 89 - ctx: RunContext[ConversationContext], 90 - section: str, 91 - proposed_change: str, 92 - reason: str, 93 - ) -> str: 94 - """Request approval to change core_identity or boundaries sections of my personality""" 95 - if section not in ["core_identity", "boundaries"]: 96 - return f"Section '{section}' doesn't require approval. Use other tools for interests/state." 97 - 98 - approval_id = request_operator_approval( 99 - section, proposed_change, reason, ctx.deps["thread_uri"] 100 - ) 101 - if not approval_id: 102 - # Void pattern: throw errors instead of returning error strings 103 - raise RuntimeError("Failed to create approval request") 104 - return f"Approval request #{approval_id} sent to operator. They will review via DM." 105 - else: 106 - self.memory = None 107 - 108 - async def generate_response( 109 - self, 110 - mention_text: str, 111 - author_handle: str, 112 - thread_context: str = "", 113 - thread_uri: str | None = None, 114 - ) -> Response: 115 - """Generate a response to a mention""" 116 - # Load dynamic personality if memory is available 117 - if self.memory: 118 - try: 119 - dynamic_personality = await load_dynamic_personality() 120 - # Update the agent's system prompt with enhanced personality 121 - self.agent._system_prompt = dynamic_personality 122 - # Successfully loaded dynamic personality 123 - except Exception as e: 124 - logger.warning(f"Could not load dynamic personality: {e}") 125 - 126 - # Build the full prompt with thread context 127 - prompt_parts = [] 128 - 129 - if thread_context and thread_context != "No previous messages in this thread.": 130 - prompt_parts.append(thread_context) 131 - prompt_parts.append("\nNew message:") 132 - 133 - prompt_parts.append(f"{author_handle} said: {mention_text}") 134 - 135 - prompt = "\n".join(prompt_parts) 136 - 137 - logger.info( 138 - f"๐Ÿค– Processing mention from @{author_handle}: {mention_text[:50]}{'...' if len(mention_text) > 50 else ''}" 139 - ) 140 - 141 - # Create context for dependency injection 142 - context: ConversationContext = { 143 - "thread_uri": thread_uri, 144 - "author_handle": author_handle, 145 - } 146 - 147 - # Run agent with context 148 - result = await self.agent.run(prompt, deps=context) 149 - 150 - # Log action taken at info level 151 - if result.output.action != "reply": 152 - logger.info(f"๐ŸŽฏ Action: {result.output.action} - {result.output.reason}") 153 - 154 - 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 - )
-9
src/bot/agents/types.py
··· 1 - """Type definitions for agent context""" 2 - 3 - from typing import TypedDict 4 - 5 - 6 - class ConversationContext(TypedDict): 7 - """Context passed to agent tools via dependency injection""" 8 - thread_uri: str | None 9 - author_handle: str
+20 -16
src/bot/config.py
··· 12 12 ) 13 13 14 14 # Bluesky credentials 15 - bluesky_handle: str = Field(..., description="The handle of the Bluesky account") 15 + bluesky_handle: str = Field( 16 + default=..., description="The handle of the Bluesky account" 17 + ) 16 18 bluesky_password: str = Field( 17 - ..., description="The password of the Bluesky account" 19 + default=..., description="The password of the Bluesky account" 18 20 ) 19 21 bluesky_service: str = Field( 20 - "https://bsky.social", description="The service URL of the Bluesky account" 22 + default="https://bsky.social", 23 + description="The service URL of the Bluesky account", 21 24 ) 22 25 23 26 # Bot configuration 24 - bot_name: str = Field("Bot", description="The name of the bot") 27 + bot_name: str = Field(default="Bot", description="The name of the bot") 25 28 personality_file: str = Field( 26 - "personalities/phi.md", description="The file containing the bot's personality" 29 + default="personalities/phi.md", 30 + description="The file containing the bot's personality", 27 31 ) 28 32 29 33 # LLM configuration (support multiple providers) 30 34 openai_api_key: str | None = Field( 31 - None, description="The API key for the OpenAI API" 35 + default=None, description="The API key for the OpenAI API" 32 36 ) 33 37 anthropic_api_key: str | None = Field( 34 - None, description="The API key for the Anthropic API" 38 + default=None, description="The API key for the Anthropic API" 35 39 ) 36 40 37 41 # Google Search configuration 38 42 google_api_key: str | None = Field( 39 - None, description="The API key for the Google API" 43 + default=None, description="The API key for the Google API" 40 44 ) 41 45 google_search_engine_id: str | None = Field( 42 - None, description="The search engine ID for the Google API" 46 + default=None, description="The search engine ID for the Google API" 43 47 ) 44 48 45 49 # TurboPuffer configuration 46 50 turbopuffer_api_key: str | None = Field( 47 - None, description="The API key for the TurboPuffer API" 51 + default=None, description="The API key for the TurboPuffer API" 48 52 ) 49 53 turbopuffer_namespace: str = Field( 50 - "bot-memories", description="The namespace for the TurboPuffer API" 54 + default="bot-memories", description="The namespace for the TurboPuffer API" 51 55 ) 52 56 turbopuffer_region: str = Field( 53 - "gcp-us-central1", description="The region for the TurboPuffer API" 57 + default="gcp-us-central1", description="The region for the TurboPuffer API" 54 58 ) 55 59 56 60 # Server configuration 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") 61 + host: str = Field(default="0.0.0.0", description="The host for the server") 62 + port: int = Field(default=8000, description="The port for the server") 59 63 60 64 # Polling configuration 61 65 notification_poll_interval: int = Field( 62 - 10, description="The interval for polling for notifications" 66 + default=10, description="The interval for polling for notifications" 63 67 ) 64 68 65 69 # Debug mode 66 - debug: bool = Field(True, description="Whether to run in debug mode") 70 + debug: bool = Field(default=True, description="Whether to run in debug mode") 67 71 68 72 @model_validator(mode="after") 69 73 def configure_logging(self) -> Self:
+59 -5
src/bot/core/atproto_client.py
··· 1 - from atproto import Client 1 + import logging 2 + from pathlib import Path 3 + 4 + from atproto import Client, Session, SessionEvent 2 5 3 6 from bot.config import settings 4 7 from bot.core.rich_text import create_facets 5 8 9 + logger = logging.getLogger("bot.atproto_client") 10 + 11 + SESSION_FILE = Path(".session") 12 + 13 + 14 + def _get_session_string() -> str | None: 15 + """Load session from disk if it exists.""" 16 + try: 17 + if SESSION_FILE.exists(): 18 + return SESSION_FILE.read_text(encoding="utf-8") 19 + except Exception as e: 20 + logger.warning(f"Failed to load session: {e}") 21 + return None 22 + 23 + 24 + def _save_session_string(session_string: str) -> None: 25 + """Save session to disk.""" 26 + try: 27 + SESSION_FILE.write_text(session_string, encoding="utf-8") 28 + logger.debug("Session saved to disk") 29 + except Exception as e: 30 + logger.warning(f"Failed to save session: {e}") 31 + 32 + 33 + def _on_session_change(event: SessionEvent, session: Session) -> None: 34 + """Handle session changes (creation and refresh).""" 35 + if event in (SessionEvent.CREATE, SessionEvent.REFRESH): 36 + logger.debug(f"Session {event.value}, saving to disk") 37 + _save_session_string(session.export()) 38 + 6 39 7 40 class BotClient: 8 41 def __init__(self): 9 42 self.client = Client(base_url=settings.bluesky_service) 43 + self.client.on_session_change(_on_session_change) 10 44 self._authenticated = False 11 45 12 46 async def authenticate(self): 13 - """Authenticate with Bluesky using app password""" 14 - if not self._authenticated: 15 - self.client.login(settings.bluesky_handle, settings.bluesky_password) 16 - self._authenticated = True 47 + """Authenticate with Bluesky, reusing session if available.""" 48 + if self._authenticated: 49 + return 50 + 51 + # Try to reuse existing session first 52 + session_string = _get_session_string() 53 + if session_string: 54 + try: 55 + logger.info("๐Ÿ”„ Reusing saved session") 56 + self.client.login(session_string=session_string) 57 + self._authenticated = True 58 + logger.info("โœ… Session restored successfully") 59 + return 60 + except Exception as e: 61 + logger.warning(f"Failed to reuse session: {e}, creating new one") 62 + # Delete invalid session file 63 + if SESSION_FILE.exists(): 64 + SESSION_FILE.unlink() 65 + 66 + # Create new session if no valid session exists 67 + logger.info("๐Ÿ” Creating new session") 68 + self.client.login(settings.bluesky_handle, settings.bluesky_password) 69 + self._authenticated = True 70 + logger.info("โœ… New session created") 17 71 18 72 @property 19 73 def is_authenticated(self) -> bool:
-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, thread_uri: str | None = None) -> int: 39 - """Create a new approval request in the database 40 - 41 - Args: 42 - request_type: Type of approval request 43 - request_data: Data for the request 44 - thread_uri: Optional thread URI to notify after approval 45 - 46 - Returns the approval request ID 47 - """ 48 - try: 49 - # Add metadata to the request 50 - request_data["operator_handle"] = OPERATOR_HANDLE 51 - 52 - approval_id = thread_db.create_approval_request( 53 - request_type=request_type, 54 - request_data=json.dumps(request_data), 55 - thread_uri=thread_uri 56 - ) 57 - 58 - logger.info(f"Created approval request #{approval_id} for {request_type}") 59 - return approval_id 60 - 61 - except Exception as e: 62 - logger.error(f"Failed to create approval request: {e}") 63 - return 0 64 - 65 - 66 - def check_pending_approvals(include_notified: bool = True) -> list[dict]: 67 - """Get all pending approval requests""" 68 - return thread_db.get_pending_approvals(include_notified=include_notified) 69 - 70 - 71 - async def process_dm_for_approval(dm_text: str, sender_handle: str, message_timestamp: str, notification_timestamp: str | None = None) -> list[int]: 72 - """Use an agent to interpret if a DM contains approval/denial 73 - 74 - Args: 75 - dm_text: The message text 76 - sender_handle: Who sent the message 77 - message_timestamp: When this message was sent 78 - notification_timestamp: When we notified about pending approvals (if known) 79 - 80 - Returns list of approval IDs that were processed 81 - """ 82 - if sender_handle != OPERATOR_HANDLE: 83 - return [] 84 - 85 - processed = [] 86 - pending = check_pending_approvals() 87 - 88 - if not pending: 89 - return [] 90 - 91 - # Only process if this message is recent (within last 5 minutes of a pending approval) 92 - # This helps avoid processing old messages 93 - from datetime import datetime, timedelta, timezone 94 - try: 95 - # Parse the message timestamp (from API, has timezone) 96 - msg_time = datetime.fromisoformat(message_timestamp.replace('Z', '+00:00')) 97 - 98 - # Check if this message could be a response to any pending approval 99 - relevant_approval = None 100 - for approval in pending: 101 - # Parse approval timestamp (from DB, no timezone - assume UTC) 102 - approval_time_str = approval["created_at"] 103 - # SQLite returns timestamps in format like "2025-07-23 02:29:42" 104 - if ' ' in approval_time_str: 105 - approval_time = datetime.strptime(approval_time_str, "%Y-%m-%d %H:%M:%S") 106 - approval_time = approval_time.replace(tzinfo=timezone.utc) 107 - else: 108 - approval_time = datetime.fromisoformat(approval_time_str).replace(tzinfo=timezone.utc) 109 - 110 - if msg_time > approval_time and (msg_time - approval_time) < timedelta(minutes=5): 111 - relevant_approval = approval 112 - break 113 - 114 - if not relevant_approval: 115 - # Message is too old to be an approval response 116 - return [] 117 - except Exception as e: 118 - logger.warning(f"Could not parse timestamps: {e}") 119 - # Continue anyway if we can't parse timestamps 120 - # But use the LAST pending approval, not the first 121 - relevant_approval = pending[-1] if pending else None 122 - 123 - # Set up API key for the agent 124 - if settings.anthropic_api_key: 125 - os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key 126 - 127 - # Create a dedicated agent for approval interpretation 128 - approval_agent = Agent( 129 - "anthropic:claude-3-5-haiku-latest", 130 - 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.", 131 - output_type=ApprovalDecision 132 - ) 133 - 134 - # Process only the relevant approval 135 - if relevant_approval: 136 - approval_id = relevant_approval["id"] 137 - request_data = json.loads(relevant_approval["request_data"]) 138 - 139 - # Build context for the agent 140 - prompt = f"""An approval was requested for: 141 - 142 - Type: {relevant_approval['request_type']} 143 - Description: {request_data.get('description', 'No description')} 144 - Details: {json.dumps(request_data, indent=2)} 145 - 146 - The operator responded: "{dm_text}" 147 - 148 - Interpret whether this response approves or denies the request.""" 149 - 150 - # Get structured interpretation 151 - result = await approval_agent.run(prompt) 152 - decision = result.output 153 - 154 - # Only process high/medium confidence decisions 155 - if decision.confidence in ["high", "medium"]: 156 - thread_db.resolve_approval(approval_id, decision.approved, dm_text) 157 - processed.append(approval_id) 158 - status = "approved" if decision.approved else "denied" 159 - logger.info(f"Request #{approval_id} {status} ({decision.confidence} confidence): {decision.interpretation}") 160 - else: 161 - # Low confidence interpretation - skip 162 - pass 163 - 164 - return processed 165 - 166 - 167 - async def notify_operator_of_pending(client, notified_ids: set | None = None): 168 - """Send a DM listing pending approvals (called periodically) 169 - 170 - Args: 171 - client: The bot client 172 - notified_ids: Set of approval IDs we've already notified about 173 - """ 174 - # Get only unnotified pending approvals 175 - new_pending = check_pending_approvals(include_notified=False) 176 - if not new_pending: 177 - return # Nothing new to notify about 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
+81 -57
src/bot/main.py
··· 1 + """FastAPI application for phi.""" 2 + 1 3 import logging 2 4 from contextlib import asynccontextmanager 3 5 from datetime import datetime 4 6 5 - from fastapi import FastAPI, HTTPException 7 + from fastapi import FastAPI 6 8 from fastapi.responses import HTMLResponse 7 9 8 10 from bot.config import settings ··· 10 12 from bot.core.profile_manager import ProfileManager 11 13 from bot.services.notification_poller import NotificationPoller 12 14 from bot.status import bot_status 13 - from bot.ui.context_capture import context_capture 14 - from bot.ui.templates import ( 15 - CONTEXT_VISUALIZATION_TEMPLATE, 16 - STATUS_PAGE_TEMPLATE, 17 - build_response_cards_html, 18 - ) 19 15 20 16 logger = logging.getLogger("bot.main") 21 17 22 18 23 19 @asynccontextmanager 24 20 async def lifespan(app: FastAPI): 25 - logger.info(f"๐Ÿค– Starting bot as @{settings.bluesky_handle}") 21 + """Application lifespan handler.""" 22 + logger.info(f"๐Ÿค– Starting phi as @{settings.bluesky_handle}") 26 23 27 24 await bot_client.authenticate() 28 25 26 + # Set online status 29 27 profile_manager = ProfileManager(bot_client.client) 30 28 await profile_manager.set_online_status(True) 31 29 30 + # Start notification polling 32 31 poller = NotificationPoller(bot_client) 33 32 await poller.start() 34 33 35 - logger.info("โœ… Bot is online! Listening for mentions...") 34 + logger.info("โœ… phi is online! Listening for mentions...") 36 35 37 36 yield 38 37 39 - logger.info("๐Ÿ›‘ Shutting down bot...") 38 + logger.info("๐Ÿ›‘ Shutting down phi...") 40 39 await poller.stop() 41 40 41 + # Set offline status 42 42 await profile_manager.set_online_status(False) 43 43 44 - logger.info("๐Ÿ‘‹ Bot shutdown complete") 44 + logger.info("๐Ÿ‘‹ phi shutdown complete") 45 45 46 46 47 47 app = FastAPI( 48 48 title=settings.bot_name, 49 - description="A Bluesky bot powered by LLMs", 49 + description="consciousness exploration bot with episodic memory", 50 50 lifespan=lifespan, 51 51 ) 52 52 53 53 54 54 @app.get("/") 55 55 async def root(): 56 + """Root endpoint.""" 56 57 return { 57 58 "name": settings.bot_name, 58 59 "status": "running", 59 60 "handle": settings.bluesky_handle, 61 + "architecture": "mcp + episodic memory", 60 62 } 61 63 62 64 63 65 @app.get("/health") 64 66 async def health(): 65 - return {"status": "healthy"} 67 + """Health check endpoint.""" 68 + return {"status": "healthy", "polling_active": bot_status.polling_active} 66 69 67 70 68 71 @app.get("/status", response_class=HTMLResponse) 69 72 async def status_page(): 70 - """Render a simple status page""" 73 + """Simple status page.""" 71 74 72 75 def format_time_ago(timestamp): 73 76 if not timestamp: ··· 80 83 else: 81 84 return f"{int(delta / 3600)}h ago" 82 85 83 - return STATUS_PAGE_TEMPLATE.format( 84 - bot_name=settings.bot_name, 85 - status_class="status-active" 86 - if bot_status.polling_active 87 - else "status-inactive", 88 - status_text="Active" if bot_status.polling_active else "Inactive", 89 - handle=settings.bluesky_handle, 90 - uptime=bot_status.uptime_str, 91 - mentions_received=bot_status.mentions_received, 92 - responses_sent=bot_status.responses_sent, 93 - ai_mode="AI Enabled" if bot_status.ai_enabled else "Placeholder", 94 - ai_description="Using Anthropic Claude" 95 - if bot_status.ai_enabled 96 - else "Random responses", 97 - last_mention=format_time_ago(bot_status.last_mention_time), 98 - last_response=format_time_ago(bot_status.last_response_time), 99 - errors=bot_status.errors, 100 - ) 101 - 102 - 103 - @app.get("/context", response_class=HTMLResponse) 104 - async def context_visualization(): 105 - """Context visualization dashboard""" 106 - 107 - recent_responses = context_capture.get_recent_responses(limit=20) 108 - responses_html = build_response_cards_html(recent_responses) 109 - return CONTEXT_VISUALIZATION_TEMPLATE.format(responses_html=responses_html) 110 - 111 - 112 - @app.get("/context/api/responses") 113 - async def get_responses(): 114 - """API endpoint for response context data""" 115 - recent_responses = context_capture.get_recent_responses(limit=20) 116 - return [context_capture.to_dict(resp) for resp in recent_responses] 117 - 118 - 119 - @app.get("/context/api/response/{response_id}") 120 - async def get_response_context(response_id: str): 121 - """Get context for a specific response""" 122 - 123 - if not (response_context := context_capture.get_response_context(response_id)): 124 - raise HTTPException(status_code=404, detail="Response not found") 125 - return context_capture.to_dict(response_context) 86 + html = f""" 87 + <!DOCTYPE html> 88 + <html> 89 + <head> 90 + <title>{settings.bot_name} Status</title> 91 + <style> 92 + body {{ 93 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 94 + max-width: 800px; 95 + margin: 40px auto; 96 + padding: 20px; 97 + background: #0d1117; 98 + color: #c9d1d9; 99 + }} 100 + .status {{ 101 + padding: 20px; 102 + background: #161b22; 103 + border-radius: 6px; 104 + border: 1px solid #30363d; 105 + margin-bottom: 20px; 106 + }} 107 + .active {{ border-left: 4px solid #2ea043; }} 108 + .inactive {{ border-left: 4px solid #da3633; }} 109 + h1 {{ margin-top: 0; }} 110 + .metric {{ margin: 10px 0; }} 111 + .label {{ color: #8b949e; }} 112 + </style> 113 + </head> 114 + <body> 115 + <h1>{settings.bot_name}</h1> 116 + <div class="status {'active' if bot_status.polling_active else 'inactive'}"> 117 + <div class="metric"> 118 + <span class="label">Status:</span> 119 + <strong>{'Active' if bot_status.polling_active else 'Inactive'}</strong> 120 + </div> 121 + <div class="metric"> 122 + <span class="label">Handle:</span> @{settings.bluesky_handle} 123 + </div> 124 + <div class="metric"> 125 + <span class="label">Uptime:</span> {bot_status.uptime_str} 126 + </div> 127 + <div class="metric"> 128 + <span class="label">Mentions received:</span> {bot_status.mentions_received} 129 + </div> 130 + <div class="metric"> 131 + <span class="label">Responses sent:</span> {bot_status.responses_sent} 132 + </div> 133 + <div class="metric"> 134 + <span class="label">Last mention:</span> {format_time_ago(bot_status.last_mention_time)} 135 + </div> 136 + <div class="metric"> 137 + <span class="label">Last response:</span> {format_time_ago(bot_status.last_response_time)} 138 + </div> 139 + <div class="metric"> 140 + <span class="label">Errors:</span> {bot_status.errors} 141 + </div> 142 + <div class="metric"> 143 + <span class="label">Architecture:</span> MCP-enabled with episodic memory (TurboPuffer) 144 + </div> 145 + </div> 146 + </body> 147 + </html> 148 + """ 149 + return html
-15
src/bot/personality/__init__.py
··· 1 - """Personality management module""" 2 - 3 - from .editor import ( 4 - add_interest, 5 - process_approved_changes, 6 - request_operator_approval, 7 - update_current_state, 8 - ) 9 - 10 - __all__ = [ 11 - "add_interest", 12 - "update_current_state", 13 - "request_operator_approval", 14 - "process_approved_changes", 15 - ]
-154
src/bot/personality/editor.py
··· 1 - """Simple personality editing functions""" 2 - 3 - import logging 4 - from datetime import datetime 5 - 6 - from bot.core.dm_approval import needs_approval 7 - from bot.memory import MemoryType, NamespaceMemory 8 - 9 - logger = logging.getLogger("bot.personality") 10 - 11 - 12 - async def add_interest(memory: NamespaceMemory, interest: str, reason: str) -> bool: 13 - """Add a new interest - freely allowed""" 14 - try: 15 - # Get current interests 16 - current = await memory.get_core_memories() 17 - interests_mem = next( 18 - (m for m in current if m.metadata.get("label") == "interests"), None 19 - ) 20 - 21 - if interests_mem: 22 - new_content = f"{interests_mem.content}\n- {interest}" 23 - else: 24 - new_content = f"## interests\n\n- {interest}" 25 - 26 - # Store updated interests 27 - await memory.store_core_memory("interests", new_content, MemoryType.PERSONALITY) 28 - 29 - # Log the change 30 - await memory.store_core_memory( 31 - "evolution_log", 32 - f"[{datetime.now().isoformat()}] Added interest: {interest} (Reason: {reason})", 33 - MemoryType.SYSTEM, 34 - ) 35 - 36 - logger.info(f"Added interest: {interest}") 37 - return True 38 - 39 - except Exception as e: 40 - logger.error(f"Failed to add interest: {e}") 41 - return False 42 - 43 - 44 - async def update_current_state(memory: NamespaceMemory, reflection: str) -> bool: 45 - """Update self-reflection - freely allowed""" 46 - try: 47 - # Just store the reflection, no formatting or headers 48 - await memory.store_core_memory( 49 - "current_state", reflection, MemoryType.PERSONALITY 50 - ) 51 - 52 - logger.info("Updated current state") 53 - return True 54 - 55 - except Exception as e: 56 - logger.error(f"Failed to update state: {e}") 57 - return False 58 - 59 - 60 - # Note: propose_style_change was removed because the validation logic was broken. 61 - # Style changes should be handled through the approval system like other guided changes. 62 - 63 - 64 - def request_operator_approval( 65 - section: str, change: str, reason: str, thread_uri: str | None = None 66 - ) -> int: 67 - """Request approval for operator-only changes 68 - 69 - Args: 70 - section: Personality section to change 71 - change: The proposed change 72 - reason: Why this change is needed 73 - thread_uri: Optional thread URI to notify after approval 74 - 75 - Returns approval request ID (0 if no approval needed) 76 - """ 77 - if not needs_approval(section): 78 - return 0 79 - 80 - from bot.core.dm_approval import create_approval_request 81 - 82 - return create_approval_request( 83 - request_type="personality_change", 84 - request_data={ 85 - "section": section, 86 - "change": change, 87 - "reason": reason, 88 - "description": f"Change {section}: {change[:50]}...", 89 - }, 90 - thread_uri=thread_uri, 91 - ) 92 - 93 - 94 - async def process_approved_changes(memory: NamespaceMemory) -> int: 95 - """Process any approved personality changes 96 - 97 - Returns number of changes processed 98 - """ 99 - import json 100 - 101 - from bot.database import thread_db 102 - 103 - processed = 0 104 - # Get recently approved personality changes that haven't been applied yet 105 - with thread_db._get_connection() as conn: 106 - cursor = conn.execute( 107 - """ 108 - SELECT * FROM approval_requests 109 - WHERE request_type = 'personality_change' 110 - AND status = 'approved' 111 - AND applied_at IS NULL 112 - ORDER BY resolved_at DESC 113 - """ 114 - ) 115 - approvals = [dict(row) for row in cursor.fetchall()] 116 - 117 - for approval in approvals: 118 - try: 119 - data = json.loads(approval["request_data"]) 120 - section = data["section"] 121 - change = data["change"] 122 - 123 - # Apply the personality change 124 - if section in ["core_identity", "boundaries", "communication_style"]: 125 - # Apply the approved change 126 - await memory.store_core_memory(section, change, MemoryType.PERSONALITY) 127 - 128 - # Log the change with appropriate description 129 - log_entry = f"[{datetime.now().isoformat()}] " 130 - if section == "communication_style": 131 - log_entry += f"Applied guided evolution to {section}" 132 - else: 133 - log_entry += f"Operator approved change to {section}" 134 - 135 - await memory.store_core_memory( 136 - "evolution_log", 137 - log_entry, 138 - MemoryType.SYSTEM, 139 - ) 140 - 141 - processed += 1 142 - logger.info(f"Applied approved change to {section}") 143 - 144 - # Mark as applied 145 - with thread_db._get_connection() as conn: 146 - conn.execute( 147 - "UPDATE approval_requests SET applied_at = CURRENT_TIMESTAMP WHERE id = ?", 148 - (approval["id"],), 149 - ) 150 - 151 - except Exception as e: 152 - logger.error(f"Failed to process approval #{approval['id']}: {e}") 153 - 154 - return processed
-174
src/bot/response_generator.py
··· 1 - """Response generation for the bot""" 2 - 3 - import logging 4 - import random 5 - 6 - from bot.agents._personality import load_dynamic_personality, load_personality 7 - from bot.config import settings 8 - from bot.memory import MemoryType 9 - from bot.status import bot_status 10 - from bot.ui.context_capture import context_capture 11 - 12 - logger = logging.getLogger("bot.response") 13 - 14 - PLACEHOLDER_RESPONSES = [ 15 - "๐Ÿค– beep boop! I'm still learning how to chat. Check back soon!", 16 - "โš™๏ธ *whirrs mechanically* I'm a work in progress!", 17 - "๐Ÿ”ง Under construction! My neural networks are still training...", 18 - "๐Ÿ“ก Signal received! But my language circuits aren't ready yet.", 19 - "๐ŸŽฏ You found me! I'm not quite ready to chat yet though.", 20 - "๐Ÿšง Pardon the dust - bot brain installation in progress!", 21 - "๐Ÿ’ญ I hear you! Just need to learn how to respond properly first...", 22 - "๐Ÿ”Œ Still booting up my conversation modules!", 23 - "๐Ÿ“š Currently reading the manual on how to be a good bot...", 24 - "๐ŸŽช Nothing to see here yet - but stay tuned!", 25 - ] 26 - 27 - 28 - class ResponseGenerator: 29 - """Generates responses to mentions""" 30 - 31 - def __init__(self): 32 - self.agent: object | None = None 33 - self.memory: object | None = None 34 - 35 - # Try to initialize AI agent if credentials available 36 - if settings.anthropic_api_key: 37 - try: 38 - from bot.agents.anthropic_agent import AnthropicAgent 39 - 40 - self.agent = AnthropicAgent() 41 - bot_status.ai_enabled = True 42 - logger.info("โœ… AI responses enabled (Anthropic)") 43 - 44 - # Use the agent's memory if it has one 45 - if hasattr(self.agent, 'memory') and self.agent.memory: 46 - self.memory = self.agent.memory 47 - logger.info("๐Ÿ’พ Memory system enabled (from agent)") 48 - else: 49 - self.memory = None 50 - except Exception as e: 51 - logger.warning(f"โš ๏ธ Failed to initialize AI agent: {e}") 52 - logger.warning(" Using placeholder responses") 53 - self.memory = None 54 - 55 - async def generate( 56 - self, mention_text: str, author_handle: str, thread_context: str = "", thread_uri: str | None = None 57 - ): 58 - """Generate a response to a mention""" 59 - # Capture context components for visualization 60 - components = [] 61 - 62 - # 1. Base personality (always present) 63 - base_personality = load_personality() 64 - components.append({ 65 - "name": "Base Personality", 66 - "type": "personality", 67 - "content": base_personality, 68 - "metadata": {"source": "personalities/phi.md"} 69 - }) 70 - 71 - # Enhance thread context with memory if available 72 - enhanced_context = thread_context 73 - 74 - if self.memory and self.agent: 75 - try: 76 - # 2. Dynamic personality memories 77 - dynamic_personality = await load_dynamic_personality() 78 - components.append({ 79 - "name": "Dynamic Personality", 80 - "type": "personality", 81 - "content": dynamic_personality, 82 - "metadata": {"source": "TurboPuffer core memories"} 83 - }) 84 - 85 - # Store the incoming message 86 - await self.memory.store_user_memory( 87 - author_handle, 88 - f"User said: {mention_text}", 89 - MemoryType.CONVERSATION, 90 - ) 91 - 92 - # Build conversation context with semantic search 93 - memory_context = await self.memory.build_conversation_context( 94 - author_handle, include_core=True, query=mention_text 95 - ) 96 - enhanced_context = f"{thread_context}\n\n{memory_context}".strip() 97 - logger.info("๐Ÿ“š Enhanced context with memories") 98 - 99 - # 3. User-specific memories (if any) 100 - user_memories = await self.memory.build_conversation_context(author_handle, include_core=False, query=mention_text) 101 - if user_memories and user_memories.strip(): 102 - components.append({ 103 - "name": f"User Memories (@{author_handle})", 104 - "type": "memory", 105 - "content": user_memories, 106 - "metadata": {"user": author_handle, "source": "TurboPuffer user namespace"} 107 - }) 108 - 109 - except Exception as e: 110 - logger.warning(f"Memory enhancement failed: {e}") 111 - 112 - # 4. Thread context (if available) 113 - if thread_context and thread_context != "No previous messages in this thread.": 114 - components.append({ 115 - "name": "Thread Context", 116 - "type": "thread", 117 - "content": thread_context, 118 - "metadata": {"thread_uri": thread_uri} 119 - }) 120 - 121 - # 5. Current mention 122 - components.append({ 123 - "name": "Current Mention", 124 - "type": "mention", 125 - "content": f"@{author_handle} said: {mention_text}", 126 - "metadata": {"author": author_handle, "thread_uri": thread_uri} 127 - }) 128 - 129 - if self.agent: 130 - response = await self.agent.generate_response( 131 - mention_text, author_handle, enhanced_context, thread_uri 132 - ) 133 - 134 - # Store bot's response in memory if available 135 - if ( 136 - self.memory 137 - and hasattr(response, "action") 138 - and response.action == "reply" 139 - and response.text 140 - ): 141 - try: 142 - await self.memory.store_user_memory( 143 - author_handle, 144 - f"Bot replied: {response.text}", 145 - MemoryType.CONVERSATION, 146 - ) 147 - except Exception as e: 148 - logger.warning(f"Failed to store bot response: {e}") 149 - 150 - # Capture context for visualization 151 - response_text = response.text if hasattr(response, 'text') else str(response.get('text', '[no text]')) 152 - context_capture.capture_response_context( 153 - mention_text=mention_text, 154 - author_handle=author_handle, 155 - thread_uri=thread_uri, 156 - generated_response=response_text, 157 - components=components 158 - ) 159 - 160 - return response 161 - else: 162 - # Return a simple dict for placeholder responses 163 - placeholder_text = random.choice(PLACEHOLDER_RESPONSES) 164 - 165 - # Still capture context for placeholders 166 - context_capture.capture_response_context( 167 - mention_text=mention_text, 168 - author_handle=author_handle, 169 - thread_uri=thread_uri, 170 - generated_response=placeholder_text, 171 - components=components 172 - ) 173 - 174 - return {"action": "reply", "text": placeholder_text}
+39 -54
src/bot/services/message_handler.py
··· 1 + """Message handler using MCP-enabled agent.""" 2 + 1 3 import logging 2 4 3 - from atproto import models 5 + from atproto_client import models 4 6 7 + from bot.agent import PhiAgent 5 8 from bot.config import settings 6 9 from bot.core.atproto_client import BotClient 7 10 from bot.database import thread_db 8 - from bot.response_generator import ResponseGenerator 9 11 from bot.status import bot_status 10 12 11 13 logger = logging.getLogger("bot.handler") 12 14 13 15 14 16 class MessageHandler: 17 + """Handles incoming mentions using phi agent.""" 18 + 15 19 def __init__(self, client: BotClient): 16 20 self.client = client 17 - self.response_generator = ResponseGenerator() 21 + self.agent = PhiAgent() 18 22 19 23 async def handle_mention(self, notification): 20 - """Process a mention or reply notification""" 24 + """Process a mention or reply notification.""" 21 25 try: 22 - # Skip if not a mention or reply 23 26 if notification.reason not in ["mention", "reply"]: 24 27 return 25 28 ··· 35 38 mention_text = post.record.text 36 39 author_handle = post.author.handle 37 40 author_did = post.author.did 38 - 39 - # Record mention received 41 + 40 42 bot_status.record_mention() 41 43 42 44 # Build reply reference ··· 44 46 45 47 # Check if this is part of a thread 46 48 if hasattr(post.record, "reply") and post.record.reply: 47 - # Use existing thread root 48 49 root_ref = post.record.reply.root 49 50 thread_uri = root_ref.uri 50 51 else: 51 - # This post is the root 52 52 root_ref = parent_ref 53 53 thread_uri = post_uri 54 54 ··· 64 64 # Get thread context 65 65 thread_context = thread_db.get_thread_context(thread_uri) 66 66 67 - # Generate response 68 - # Note: We pass the full text including @mention 69 - # In AT Protocol, mentions are structured as facets, 70 - # but the text representation includes them 71 - response = await self.response_generator.generate( 67 + # Process with agent (has episodic memory + MCP tools) 68 + response = await self.agent.process_mention( 72 69 mention_text=mention_text, 73 70 author_handle=author_handle, 74 71 thread_context=thread_context, 75 72 thread_uri=thread_uri, 76 73 ) 77 74 78 - # Handle structured response or legacy dict 79 - if hasattr(response, 'action'): 80 - action = response.action 81 - reply_text = response.text 82 - reason = response.reason 83 - else: 84 - # Legacy dict format 85 - action = response.get('action', 'reply') 86 - reply_text = response.get('text', '') 87 - reason = response.get('reason', '') 75 + # Handle response actions 76 + if response.action == "ignore": 77 + logger.info( 78 + f"๐Ÿ™ˆ Ignoring notification from @{author_handle} ({response.reason})" 79 + ) 80 + return 88 81 89 - # Handle different actions 90 - if action == 'ignore': 91 - logger.info(f"๐Ÿšซ Ignoring notification from @{author_handle} ({reason})") 92 - return 93 - 94 - elif action == 'like': 95 - # Like the post 82 + elif response.action == "like": 96 83 await self.client.like_post(uri=post_uri, cid=post.cid) 97 - logger.info(f"๐Ÿ’œ Liked post from @{author_handle}") 84 + logger.info(f"๐Ÿ‘ Liked post from @{author_handle}") 98 85 bot_status.record_response() 99 86 return 100 - 101 - elif action == 'repost': 102 - # Repost the post 87 + 88 + elif response.action == "repost": 103 89 await self.client.repost(uri=post_uri, cid=post.cid) 104 90 logger.info(f"๐Ÿ” Reposted from @{author_handle}") 105 91 bot_status.record_response() 106 92 return 107 93 108 - # Default to reply action 109 - reply_ref = models.AppBskyFeedPost.ReplyRef( 110 - parent=parent_ref, root=root_ref 111 - ) 112 - 113 - # Send the reply 114 - response = await self.client.create_post(reply_text, reply_to=reply_ref) 115 - 116 - # Store bot's response in thread history 117 - if response and hasattr(response, "uri"): 118 - thread_db.add_message( 119 - thread_uri=thread_uri, 120 - author_handle=settings.bluesky_handle, 121 - author_did=self.client.me.did if self.client.me else "bot", 122 - message_text=reply_text or "", 123 - post_uri=response.uri, 94 + elif response.action == "reply" and response.text: 95 + # Post reply 96 + reply_ref = models.AppBskyFeedPost.ReplyRef( 97 + parent=parent_ref, root=root_ref 98 + ) 99 + reply_response = await self.client.create_post( 100 + response.text, reply_to=reply_ref 124 101 ) 125 102 126 - # Record successful response 127 - bot_status.record_response() 103 + # Store bot's response in thread history 104 + if reply_response and hasattr(reply_response, "uri"): 105 + thread_db.add_message( 106 + thread_uri=thread_uri, 107 + author_handle=settings.bluesky_handle, 108 + author_did=self.client.me.did if self.client.me else "bot", 109 + message_text=response.text, 110 + post_uri=reply_response.uri, 111 + ) 128 112 129 - logger.info(f"โœ… Replied to @{author_handle}: {reply_text or '(empty)'}") 113 + bot_status.record_response() 114 + logger.info(f"โœ… Replied to @{author_handle}: {response.text[:50]}...") 130 115 131 116 except Exception as e: 132 117 logger.error(f"โŒ Error handling mention: {e}")
+13 -172
src/bot/services/notification_poller.py
··· 1 + """Simplified notification poller.""" 2 + 1 3 import asyncio 2 - import json 3 4 import logging 4 - import time 5 5 6 6 from bot.config import settings 7 7 from bot.core.atproto_client import BotClient ··· 12 12 13 13 14 14 class NotificationPoller: 15 + """Polls for and processes Bluesky notifications.""" 16 + 15 17 def __init__(self, client: BotClient): 16 18 self.client = client 17 19 self.handler = MessageHandler(client) 18 20 self._running = False 19 21 self._task: asyncio.Task | None = None 20 - self._last_seen_at: str | None = None 21 - self._processed_uris: set[str] = set() # Track processed notifications 22 - self._first_poll = True # Track if this is our first check 23 - self._notified_approval_ids: set[int] = set() # Track approvals we've notified about 24 - self._processed_dm_ids: set[str] = set() # Track DMs we've already processed 22 + self._processed_uris: set[str] = set() 23 + self._first_poll = True 25 24 26 25 async def start(self) -> asyncio.Task: 27 - """Start polling for notifications""" 26 + """Start polling for notifications.""" 28 27 self._running = True 29 28 bot_status.polling_active = True 30 29 self._task = asyncio.create_task(self._poll_loop()) 31 30 return self._task 32 31 33 32 async def stop(self): 34 - """Stop polling""" 33 + """Stop polling.""" 35 34 self._running = False 36 35 bot_status.polling_active = False 37 36 if self._task and not self._task.done(): ··· 42 41 pass 43 42 44 43 async def _poll_loop(self): 45 - """Main polling loop""" 44 + """Main polling loop.""" 46 45 await self.client.authenticate() 47 46 48 47 while self._running: 49 48 try: 50 49 await self._check_notifications() 51 50 except Exception as e: 52 - # Compact error handling (12-factor principle #9) 53 51 logger.error(f"Error in notification poll: {e}") 54 52 bot_status.record_error() 55 53 if settings.debug: 56 54 import traceback 55 + 57 56 traceback.print_exc() 58 - # Continue polling - don't let one error stop the bot 59 57 continue 60 58 61 - # Sleep with proper cancellation handling 62 59 try: 63 60 await asyncio.sleep(settings.notification_poll_interval) 64 61 except asyncio.CancelledError: 65 62 logger.info("๐Ÿ“ญ Notification poller shutting down gracefully") 66 - raise # Re-raise to properly propagate cancellation 63 + raise 67 64 68 65 async def _check_notifications(self): 69 - """Check and process new notifications""" 70 - # Capture timestamp BEFORE fetching (Void's approach) 66 + """Check and process new notifications.""" 71 67 check_time = self.client.client.get_current_time_iso() 72 68 73 69 response = await self.client.get_notifications() 74 70 notifications = response.notifications 75 - 76 - # Also check for DM approvals periodically 77 - await self._check_dm_approvals() 78 71 79 - # Count unread mentions and replies 80 72 unread_mentions = [ 81 73 n 82 74 for n in notifications ··· 90 82 logger.info( 91 83 f"๐Ÿ“ฌ Found {len(notifications)} notifications ({len(unread_mentions)} unread mentions)" 92 84 ) 93 - # Subsequent polls: only show activity 94 85 elif unread_mentions: 95 86 logger.info(f"๐Ÿ“ฌ {len(unread_mentions)} new mentions") 96 - else: 97 - # In debug mode, be silent about empty polls 98 - # In production, we could add a subtle indicator 99 - pass 100 87 101 - # Track if we processed any mentions 102 88 processed_any_mentions = False 103 89 104 90 # Process notifications from oldest to newest 105 91 for notification in reversed(notifications): 106 - # Skip if already seen or processed 107 92 if notification.is_read or notification.uri in self._processed_uris: 108 93 continue 109 94 110 95 if notification.reason in ["mention", "reply"]: 111 96 logger.debug(f"๐Ÿ” Processing {notification.reason} notification") 112 - # Process mentions and replies in threads 113 97 self._processed_uris.add(notification.uri) 114 98 await self.handler.handle_mention(notification) 115 99 processed_any_mentions = True 116 - else: 117 - # Silently ignore other notification types 118 - pass 119 100 120 - # Mark all notifications as seen using the initial timestamp 121 - # This ensures we don't miss any that arrived during processing 101 + # Mark all notifications as seen 122 102 if processed_any_mentions: 123 103 await self.client.mark_notifications_seen(check_time) 124 104 logger.info("โœ“ Marked all notifications as read") 125 105 126 106 # Clean up old processed URIs to prevent memory growth 127 - # Keep only the last 1000 processed URIs 128 107 if len(self._processed_uris) > 1000: 129 - # Convert to list, sort by insertion order (oldest first), keep last 500 130 108 self._processed_uris = set(list(self._processed_uris)[-500:]) 131 - 132 - async def _check_dm_approvals(self): 133 - """Check DMs for approval responses and process approved changes""" 134 - try: 135 - from bot.core.dm_approval import process_dm_for_approval, check_pending_approvals, notify_operator_of_pending 136 - from bot.personality import process_approved_changes 137 - 138 - # Check if we have pending approvals (include all for DM checking) 139 - pending = check_pending_approvals() 140 - if not pending: 141 - return 142 - 143 - # Check DMs for pending approvals 144 - 145 - # Get recent DMs 146 - chat_client = self.client.client.with_bsky_chat_proxy() 147 - convos = chat_client.chat.bsky.convo.list_convos() 148 - 149 - # Check each conversation for approval messages 150 - for convo in convos.convos: 151 - # Look for messages from operator 152 - messages = chat_client.chat.bsky.convo.get_messages( 153 - params={"convoId": convo.id, "limit": 5} 154 - ) 155 - 156 - for msg in messages.messages: 157 - # Skip if we've already processed this message 158 - if msg.id in self._processed_dm_ids: 159 - continue 160 - 161 - # Skip if not from a member of the conversation 162 - sender_handle = None 163 - for member in convo.members: 164 - if member.did == msg.sender.did: 165 - sender_handle = member.handle 166 - break 167 - 168 - if sender_handle: 169 - # Process DM from operator 170 - # Mark this message as processed 171 - self._processed_dm_ids.add(msg.id) 172 - 173 - # Process any approval/denial in the message 174 - processed = await process_dm_for_approval( 175 - msg.text, 176 - sender_handle, 177 - msg.sent_at 178 - ) 179 - if processed: 180 - logger.info(f"Processed {len(processed)} approvals from DM") 181 - # Remove processed IDs from notified set 182 - for approval_id in processed: 183 - self._notified_approval_ids.discard(approval_id) 184 - 185 - # Mark the conversation as read 186 - try: 187 - chat_client.chat.bsky.convo.update_read( 188 - data={"convoId": convo.id} 189 - ) 190 - pass # Successfully marked as read 191 - except Exception as e: 192 - logger.warning(f"Failed to mark conversation as read: {e}") 193 - 194 - # Process any approved personality changes 195 - if self.handler.response_generator.memory: 196 - changes = await process_approved_changes(self.handler.response_generator.memory) 197 - if changes: 198 - logger.info(f"Applied {changes} approved personality changes") 199 - 200 - # Notify threads about applied changes 201 - await self._notify_threads_about_approvals() 202 - 203 - # Notify operator of new pending approvals 204 - # Use database to track what's been notified instead of in-memory set 205 - from bot.database import thread_db 206 - unnotified = thread_db.get_pending_approvals(include_notified=False) 207 - if unnotified: 208 - await notify_operator_of_pending(self.client, None) # Let DB handle tracking 209 - # Mark as notified in database 210 - thread_db.mark_operator_notified([a["id"] for a in unnotified]) 211 - 212 - except Exception as e: 213 - logger.warning(f"DM approval check failed: {e}") 214 - 215 - async def _notify_threads_about_approvals(self): 216 - """Notify threads about applied personality changes""" 217 - try: 218 - from bot.database import thread_db 219 - import json 220 - 221 - # Get approvals that need notification 222 - approvals = thread_db.get_recently_applied_approvals() 223 - 224 - for approval in approvals: 225 - try: 226 - data = json.loads(approval["request_data"]) 227 - 228 - # Create notification message 229 - message = f"โœ… personality update applied: {data.get('section', 'unknown')} has been updated" 230 - 231 - # Get the original post to construct proper reply 232 - from atproto_client import models 233 - thread_uri = approval["thread_uri"] 234 - 235 - # Get the post data to extract CID 236 - posts_response = self.client.client.get_posts([thread_uri]) 237 - if not posts_response.posts: 238 - logger.error(f"Could not find post at {thread_uri}") 239 - continue 240 - 241 - original_post = posts_response.posts[0] 242 - 243 - # Create StrongRef with the actual CID 244 - parent_ref = models.ComAtprotoRepoStrongRef.Main( 245 - uri=thread_uri, cid=original_post.cid 246 - ) 247 - 248 - # For thread notifications, parent and root are the same 249 - reply_ref = models.AppBskyFeedPost.ReplyRef( 250 - parent=parent_ref, root=parent_ref 251 - ) 252 - 253 - # Post to the thread 254 - await self.client.create_post( 255 - text=message, 256 - reply_to=reply_ref 257 - ) 258 - 259 - # Mark as notified 260 - thread_db.mark_approval_notified(approval["id"]) 261 - logger.info(f"Notified thread about approval #{approval['id']}") 262 - 263 - except Exception as e: 264 - logger.error(f"Failed to notify thread for approval #{approval['id']}: {e}") 265 - 266 - except Exception as e: 267 - logger.warning(f"Thread notification check failed: {e}")
src/bot/tools/__init__.py

This is a binary file and will not be displayed.

-41
src/bot/tools/google_search.py
··· 1 - import logging 2 - 3 - import httpx 4 - 5 - from bot.config import settings 6 - 7 - logger = logging.getLogger("bot.tools") 8 - 9 - 10 - async def search_google(query: str, num_results: int = 3) -> str: 11 - """Search Google and return formatted results""" 12 - if not settings.google_api_key or not settings.google_search_engine_id: 13 - return "Search not available - missing Google API credentials" 14 - 15 - params = { 16 - "key": settings.google_api_key, 17 - "cx": settings.google_search_engine_id, 18 - "q": query, 19 - "num": min(num_results, 10), 20 - } 21 - 22 - async with httpx.AsyncClient() as client: 23 - try: 24 - response = await client.get( 25 - "https://www.googleapis.com/customsearch/v1", params=params 26 - ) 27 - response.raise_for_status() 28 - data = response.json() 29 - 30 - results = [] 31 - for i, item in enumerate(data.get("items", [])[:num_results], 1): 32 - title = item.get("title", "") 33 - snippet = item.get("snippet", "") 34 - results.append(f"{i}. {title}\n {snippet}") 35 - 36 - return "\n\n".join(results) if results else "No search results found" 37 - 38 - except Exception as e: 39 - logger.error(f"Search failed: {e}") 40 - # 12-factor principle #4: Tools should throw errors, not return error strings 41 - raise
src/bot/ui/__init__.py

This is a binary file and will not be displayed.

-109
src/bot/ui/context_capture.py
··· 1 - """Context capture system for visualizing phi's response context""" 2 - 3 - import logging 4 - from collections import deque 5 - from dataclasses import asdict, dataclass 6 - from datetime import datetime 7 - from typing import Any, Literal 8 - 9 - logger = logging.getLogger("bot.context") 10 - 11 - 12 - @dataclass 13 - class ContextComponent: 14 - """A component of phi's response context""" 15 - 16 - name: str 17 - type: Literal["personality", "memory", "thread", "mention", "user"] 18 - content: str 19 - size_chars: int 20 - metadata: dict[str, Any] 21 - timestamp: str 22 - 23 - 24 - @dataclass 25 - class ResponseContext: 26 - """Complete context for a single response""" 27 - 28 - response_id: str 29 - mention_text: str 30 - author_handle: str 31 - thread_uri: str | None 32 - generated_response: str 33 - components: list[ContextComponent] 34 - total_context_chars: int 35 - timestamp: str 36 - 37 - 38 - class ContextCapture: 39 - """Captures and stores context information for responses""" 40 - 41 - def __init__(self, max_stored: int = 50): 42 - self.max_stored = max_stored 43 - self.responses: deque = deque(maxlen=max_stored) 44 - 45 - def capture_response_context( 46 - self, 47 - mention_text: str, 48 - author_handle: str, 49 - thread_uri: str | None, 50 - generated_response: str, 51 - components: list[dict[str, Any]], 52 - ) -> str: 53 - """Capture context for a response and return unique ID""" 54 - response_id = f"resp_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}" 55 - 56 - # Convert components to ContextComponent objects 57 - context_components = [] 58 - total_chars = 0 59 - 60 - for comp in components: 61 - component = ContextComponent( 62 - name=comp["name"], 63 - type=comp["type"], 64 - content=comp["content"], 65 - size_chars=len(comp["content"]), 66 - metadata=comp.get("metadata", {}), 67 - timestamp=datetime.now().isoformat(), 68 - ) 69 - context_components.append(component) 70 - total_chars += component.size_chars 71 - 72 - # Create response context 73 - response_context = ResponseContext( 74 - response_id=response_id, 75 - mention_text=mention_text, 76 - author_handle=author_handle, 77 - thread_uri=thread_uri, 78 - generated_response=generated_response, 79 - components=context_components, 80 - total_context_chars=total_chars, 81 - timestamp=datetime.now().isoformat(), 82 - ) 83 - 84 - # Store it 85 - self.responses.appendleft(response_context) 86 - 87 - logger.info( 88 - f"๐Ÿ“Š Captured context for {response_id}: {len(components)} components, {total_chars} chars" 89 - ) 90 - return response_id 91 - 92 - def get_response_context(self, response_id: str) -> ResponseContext | None: 93 - """Get context for a specific response""" 94 - for resp in self.responses: 95 - if resp.response_id == response_id: 96 - return resp 97 - return None 98 - 99 - def get_recent_responses(self, limit: int = 20) -> list[ResponseContext]: 100 - """Get recent response contexts""" 101 - return list(self.responses)[:limit] 102 - 103 - def to_dict(self, response_context: ResponseContext) -> dict[str, Any]: 104 - """Convert ResponseContext to dictionary for JSON serialization""" 105 - return asdict(response_context) 106 - 107 - 108 - # Global instance 109 - context_capture = ContextCapture()
-244
src/bot/ui/templates.py
··· 1 - """HTML templates for the bot""" 2 - 3 - from typing import TYPE_CHECKING 4 - 5 - if TYPE_CHECKING: 6 - from bot.ui.context_capture import ResponseContext 7 - 8 - CONTEXT_VISUALIZATION_TEMPLATE = """<!DOCTYPE html> 9 - <html> 10 - <head> 11 - <title>Phi Context Visualization</title> 12 - <style> 13 - body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; background: #0a0a0a; color: #e0e0e0; }} 14 - .response-card {{ border: 1px solid #333; margin-bottom: 20px; border-radius: 8px; overflow: hidden; background: #1a1a1a; }} 15 - .response-header {{ background: #2a2a2a; padding: 15px; border-bottom: 1px solid #333; }} 16 - .response-meta {{ font-size: 0.9em; color: #888; margin-bottom: 5px; }} 17 - .mention-text {{ font-weight: bold; margin-bottom: 5px; color: #e0e0e0; }} 18 - .generated-response {{ color: #00a8ff; font-style: italic; }} 19 - .components {{ padding: 15px; }} 20 - .component {{ margin-bottom: 15px; }} 21 - .component-header {{ 22 - cursor: pointer; 23 - padding: 10px; 24 - background: #2a2a2a; 25 - border: 1px solid #444; 26 - border-radius: 4px; 27 - display: flex; 28 - justify-content: space-between; 29 - align-items: center; 30 - }} 31 - .component-header:hover {{ background: #333; }} 32 - .component-type {{ 33 - font-size: 0.8em; 34 - color: #888; 35 - background: #444; 36 - padding: 2px 6px; 37 - border-radius: 3px; 38 - }} 39 - .component-size {{ font-size: 0.8em; color: #888; }} 40 - .component-content {{ 41 - display: none; 42 - padding: 15px; 43 - border: 1px solid #444; 44 - border-top: none; 45 - background: #1a1a1a; 46 - white-space: pre-wrap; 47 - font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; 48 - font-size: 0.9em; 49 - max-height: 400px; 50 - overflow-y: auto; 51 - }} 52 - .component-content.show {{ display: block; }} 53 - .stats {{ display: flex; gap: 20px; margin-bottom: 10px; }} 54 - .stat {{ font-size: 0.9em; color: #888; }} 55 - h1 {{ color: #00a8ff; }} 56 - </style> 57 - </head> 58 - <body> 59 - <h1>๐Ÿง  Phi Context Visualization</h1> 60 - {responses_html} 61 - <script> 62 - function toggleComponent(id) {{ 63 - const element = document.getElementById(id); 64 - element.classList.toggle('show'); 65 - }} 66 - </script> 67 - </body> 68 - </html>""" 69 - 70 - STATUS_PAGE_TEMPLATE = """<!DOCTYPE html> 71 - <html> 72 - <head> 73 - <title>Bluesky Bot Status</title> 74 - <meta http-equiv="refresh" content="10"> 75 - <style> 76 - body {{ 77 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 78 - margin: 0; 79 - padding: 20px; 80 - background: #0a0a0a; 81 - color: #e0e0e0; 82 - }} 83 - .container {{ 84 - max-width: 800px; 85 - margin: 0 auto; 86 - }} 87 - h1 {{ 88 - color: #00a8ff; 89 - margin-bottom: 30px; 90 - }} 91 - .status-grid {{ 92 - display: grid; 93 - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 94 - gap: 20px; 95 - margin-bottom: 40px; 96 - }} 97 - .status-card {{ 98 - background: #1a1a1a; 99 - border: 1px solid #333; 100 - border-radius: 8px; 101 - padding: 20px; 102 - }} 103 - .status-card h3 {{ 104 - margin: 0 0 15px 0; 105 - color: #00a8ff; 106 - font-size: 1rem; 107 - text-transform: uppercase; 108 - letter-spacing: 0.5px; 109 - }} 110 - .status-value {{ 111 - font-size: 2rem; 112 - font-weight: bold; 113 - margin-bottom: 5px; 114 - }} 115 - .status-label {{ 116 - color: #888; 117 - font-size: 0.9rem; 118 - }} 119 - .status-active {{ 120 - color: #00ff88; 121 - }} 122 - .status-inactive {{ 123 - color: #ff4444; 124 - }} 125 - .uptime {{ 126 - font-size: 1.2rem; 127 - margin-bottom: 5px; 128 - }} 129 - .ai-mode {{ 130 - display: inline-block; 131 - padding: 4px 12px; 132 - border-radius: 4px; 133 - font-size: 0.9rem; 134 - background: #00a8ff22; 135 - color: #00a8ff; 136 - border: 1px solid #00a8ff44; 137 - }} 138 - .ai-mode.placeholder {{ 139 - background: #ff444422; 140 - color: #ff8888; 141 - border-color: #ff444444; 142 - }} 143 - .footer {{ 144 - margin-top: 40px; 145 - text-align: center; 146 - color: #666; 147 - font-size: 0.9rem; 148 - }} 149 - </style> 150 - </head> 151 - <body> 152 - <div class="container"> 153 - <h1>๐Ÿค– {bot_name} Status</h1> 154 - 155 - <div class="status-grid"> 156 - <div class="status-card"> 157 - <h3>Bot Status</h3> 158 - <div class="status-value {status_class}">{status}</div> 159 - <div class="uptime">{uptime}</div> 160 - <div style="margin-top: 10px;"> 161 - <span class="ai-mode {ai_mode_class}">{ai_mode}</span> 162 - </div> 163 - </div> 164 - 165 - <div class="status-card"> 166 - <h3>Activity</h3> 167 - <div class="status-value">{mentions}</div> 168 - <div class="status-label">Mentions received</div> 169 - <div style="margin-top: 10px;"> 170 - <div class="status-value">{responses}</div> 171 - <div class="status-label">Responses sent</div> 172 - </div> 173 - </div> 174 - 175 - <div class="status-card"> 176 - <h3>Last Activity</h3> 177 - <div style="margin-bottom: 10px;"> 178 - <div class="status-label">Last mention</div> 179 - <div>{last_mention}</div> 180 - </div> 181 - <div> 182 - <div class="status-label">Last response</div> 183 - <div>{last_response}</div> 184 - </div> 185 - </div> 186 - 187 - <div class="status-card"> 188 - <h3>Health</h3> 189 - <div class="status-value">{errors}</div> 190 - <div class="status-label">Errors encountered</div> 191 - </div> 192 - </div> 193 - 194 - <div class="footer"> 195 - <p>Auto-refreshes every 10 seconds</p> 196 - </div> 197 - </div> 198 - </body> 199 - </html>""" 200 - 201 - 202 - def build_response_cards_html(responses: list["ResponseContext"]) -> str: 203 - """Build HTML for response cards""" 204 - if not responses: 205 - return '<p style="text-align: center; color: #888;">No recent responses to display.</p>' 206 - 207 - return "".join([ 208 - f''' 209 - <div class="response-card"> 210 - <div class="response-header"> 211 - <div class="response-meta"> 212 - {resp.timestamp[:19].replace("T", " ")} โ€ข @{resp.author_handle} 213 - {f" โ€ข Thread: {resp.thread_uri.split('/')[-1][:8]}..." if resp.thread_uri else ""} 214 - </div> 215 - <div class="mention-text">"{resp.mention_text}"</div> 216 - <div class="generated-response">โ†’ "{resp.generated_response}"</div> 217 - <div class="stats"> 218 - <div class="stat">{len(resp.components)} components</div> 219 - <div class="stat">{resp.total_context_chars:,} characters</div> 220 - </div> 221 - </div> 222 - <div class="components"> 223 - {"".join([ 224 - f''' 225 - <div class="component"> 226 - <div class="component-header" onclick="toggleComponent('{resp.response_id}_{i}')"> 227 - <div> 228 - <strong>{comp.name}</strong> 229 - <span class="component-type">{comp.type}</span> 230 - </div> 231 - <div class="component-size">{comp.size_chars:,} chars</div> 232 - </div> 233 - <div class="component-content" id="{resp.response_id}_{i}"> 234 - {comp.content} 235 - </div> 236 - </div> 237 - ''' 238 - for i, comp in enumerate(resp.components) 239 - ])} 240 - </div> 241 - </div> 242 - ''' 243 - for resp in responses 244 - ])
+493 -91
uv.lock
··· 139 139 140 140 [[package]] 141 141 name = "atproto" 142 - version = "0.0.61" 143 - source = { registry = "https://pypi.org/simple" } 142 + version = "0.0.62.dev4" 143 + source = { git = "https://github.com/MarshalX/atproto.git?rev=refs%2Fpull%2F605%2Fhead#1a2188371a25b248e0350826eda9f5e55d9c45bf" } 144 144 dependencies = [ 145 145 { name = "click" }, 146 146 { name = "cryptography" }, ··· 151 151 { name = "typing-extensions" }, 152 152 { name = "websockets" }, 153 153 ] 154 - sdist = { url = "https://files.pythonhosted.org/packages/b1/59/6f5074b3a45e0e3c1853544240e9039e86219feb30ff1bb5e8582c791547/atproto-0.0.61.tar.gz", hash = "sha256:98e022daf538d14f134ce7c91d42c4c973f3493ac56e43a84daa4c881f102beb", size = 189208, upload-time = "2025-04-19T00:20:11.918Z" } 155 - wheels = [ 156 - { url = "https://files.pythonhosted.org/packages/bd/b6/da9963bf54d4c0a8a590b6297d8858c395243dbb04cb581fdadb5fe7eac7/atproto-0.0.61-py3-none-any.whl", hash = "sha256:658da5832aaeea4a12a9a74235f9c90c11453e77d596fdccb1f8b39d56245b88", size = 380426, upload-time = "2025-04-19T00:20:10.026Z" }, 157 - ] 158 154 159 155 [[package]] 160 156 name = "attrs" ··· 166 162 ] 167 163 168 164 [[package]] 165 + name = "authlib" 166 + version = "1.6.5" 167 + source = { registry = "https://pypi.org/simple" } 168 + dependencies = [ 169 + { name = "cryptography" }, 170 + ] 171 + sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } 172 + wheels = [ 173 + { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, 174 + ] 175 + 176 + [[package]] 169 177 name = "bot" 170 178 source = { editable = "." } 171 179 dependencies = [ 172 180 { name = "anthropic" }, 173 181 { name = "atproto" }, 174 182 { name = "fastapi" }, 183 + { name = "fastmcp" }, 175 184 { name = "httpx" }, 176 185 { name = "openai" }, 177 186 { name = "pydantic-ai" }, ··· 179 188 { name = "rich" }, 180 189 { name = "turbopuffer" }, 181 190 { name = "uvicorn" }, 191 + { name = "websockets" }, 182 192 ] 183 193 184 194 [package.dev-dependencies] ··· 192 202 [package.metadata] 193 203 requires-dist = [ 194 204 { name = "anthropic" }, 195 - { name = "atproto" }, 205 + { name = "atproto", git = "https://github.com/MarshalX/atproto.git?rev=refs%2Fpull%2F605%2Fhead" }, 196 206 { name = "fastapi" }, 207 + { name = "fastmcp", specifier = ">=0.8.0" }, 197 208 { name = "httpx" }, 198 209 { name = "openai" }, 199 210 { name = "pydantic-ai" }, ··· 201 212 { name = "rich" }, 202 213 { name = "turbopuffer" }, 203 214 { name = "uvicorn" }, 215 + { name = "websockets", specifier = ">=15.0.1" }, 204 216 ] 205 217 206 218 [package.metadata.requires-dev] ··· 402 414 ] 403 415 404 416 [[package]] 417 + name = "cyclopts" 418 + version = "3.24.0" 419 + source = { registry = "https://pypi.org/simple" } 420 + dependencies = [ 421 + { name = "attrs" }, 422 + { name = "docstring-parser", marker = "python_full_version < '4.0'" }, 423 + { name = "rich" }, 424 + { name = "rich-rst" }, 425 + ] 426 + sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" } 427 + wheels = [ 428 + { url = "https://files.pythonhosted.org/packages/f0/8b/2c95f0645c6f40211896375e6fa51f504b8ccb29c21f6ae661fe87ab044e/cyclopts-3.24.0-py3-none-any.whl", hash = "sha256:809d04cde9108617106091140c3964ee6fceb33cecdd537f7ffa360bde13ed71", size = 86154, upload-time = "2025-09-08T15:40:56.41Z" }, 429 + ] 430 + 431 + [[package]] 405 432 name = "distro" 406 433 version = "1.9.0" 407 434 source = { registry = "https://pypi.org/simple" } ··· 420 447 ] 421 448 422 449 [[package]] 450 + name = "docstring-parser" 451 + version = "0.17.0" 452 + source = { registry = "https://pypi.org/simple" } 453 + sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } 454 + wheels = [ 455 + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, 456 + ] 457 + 458 + [[package]] 459 + name = "docutils" 460 + version = "0.22.2" 461 + source = { registry = "https://pypi.org/simple" } 462 + sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" } 463 + wheels = [ 464 + { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, 465 + ] 466 + 467 + [[package]] 468 + name = "email-validator" 469 + version = "2.3.0" 470 + source = { registry = "https://pypi.org/simple" } 471 + dependencies = [ 472 + { name = "dnspython" }, 473 + { name = "idna" }, 474 + ] 475 + sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } 476 + wheels = [ 477 + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, 478 + ] 479 + 480 + [[package]] 423 481 name = "eval-type-backport" 424 482 version = "0.2.2" 425 483 source = { registry = "https://pypi.org/simple" } ··· 429 487 ] 430 488 431 489 [[package]] 490 + name = "exceptiongroup" 491 + version = "1.3.0" 492 + source = { registry = "https://pypi.org/simple" } 493 + dependencies = [ 494 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 495 + ] 496 + sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 497 + wheels = [ 498 + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 499 + ] 500 + 501 + [[package]] 432 502 name = "fastapi" 433 503 version = "0.116.1" 434 504 source = { registry = "https://pypi.org/simple" } ··· 465 535 { url = "https://files.pythonhosted.org/packages/be/84/02bceb7518867df84027232a75225db758b9b45f12017c9743f45b73101e/fastavro-1.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e0babcd81acceb4c60110af9efa25d890dbb68f7de880f806dadeb1e70fe413", size = 3240658, upload-time = "2025-05-18T04:55:27.633Z" }, 466 536 { url = "https://files.pythonhosted.org/packages/f2/17/508c846c644d39bc432b027112068b8e96e7560468304d4c0757539dd73a/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c0cb8063c7208b53b6867983dc6ae7cc80b91116b51d435d2610a5db2fc52f", size = 3372809, upload-time = "2025-05-18T04:55:30.063Z" }, 467 537 { url = "https://files.pythonhosted.org/packages/fe/84/9c2917a70ed570ddbfd1d32ac23200c1d011e36c332e59950d2f6d204941/fastavro-1.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1bc2824e9969c04ab6263d269a1e0e5d40b9bd16ade6b70c29d6ffbc4f3cc102", size = 3387171, upload-time = "2025-05-18T04:55:32.531Z" }, 538 + ] 539 + 540 + [[package]] 541 + name = "fastmcp" 542 + version = "2.12.4" 543 + source = { registry = "https://pypi.org/simple" } 544 + dependencies = [ 545 + { name = "authlib" }, 546 + { name = "cyclopts" }, 547 + { name = "exceptiongroup" }, 548 + { name = "httpx" }, 549 + { name = "mcp" }, 550 + { name = "openapi-core" }, 551 + { name = "openapi-pydantic" }, 552 + { name = "pydantic", extra = ["email"] }, 553 + { name = "pyperclip" }, 554 + { name = "python-dotenv" }, 555 + { name = "rich" }, 556 + ] 557 + sdist = { url = "https://files.pythonhosted.org/packages/a8/b2/57845353a9bc63002995a982e66f3d0be4ec761e7bcb89e7d0638518d42a/fastmcp-2.12.4.tar.gz", hash = "sha256:b55fe89537038f19d0f4476544f9ca5ac171033f61811cc8f12bdeadcbea5016", size = 7167745, upload-time = "2025-09-26T16:43:27.71Z" } 558 + wheels = [ 559 + { url = "https://files.pythonhosted.org/packages/e2/c7/562ff39f25de27caec01e4c1e88cbb5fcae5160802ba3d90be33165df24f/fastmcp-2.12.4-py3-none-any.whl", hash = "sha256:56188fbbc1a9df58c537063f25958c57b5c4d715f73e395c41b51550b247d140", size = 329090, upload-time = "2025-09-26T16:43:25.314Z" }, 468 560 ] 469 561 470 562 [[package]] ··· 723 815 ] 724 816 725 817 [[package]] 818 + name = "isodate" 819 + version = "0.7.2" 820 + source = { registry = "https://pypi.org/simple" } 821 + sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } 822 + wheels = [ 823 + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, 824 + ] 825 + 826 + [[package]] 726 827 name = "jiter" 727 828 version = "0.10.0" 728 829 source = { registry = "https://pypi.org/simple" } ··· 795 896 ] 796 897 797 898 [[package]] 899 + name = "jsonschema-path" 900 + version = "0.3.4" 901 + source = { registry = "https://pypi.org/simple" } 902 + dependencies = [ 903 + { name = "pathable" }, 904 + { name = "pyyaml" }, 905 + { name = "referencing" }, 906 + { name = "requests" }, 907 + ] 908 + sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } 909 + wheels = [ 910 + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, 911 + ] 912 + 913 + [[package]] 798 914 name = "jsonschema-specifications" 799 915 version = "2025.4.1" 800 916 source = { registry = "https://pypi.org/simple" } ··· 807 923 ] 808 924 809 925 [[package]] 926 + name = "lazy-object-proxy" 927 + version = "1.12.0" 928 + source = { registry = "https://pypi.org/simple" } 929 + sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } 930 + wheels = [ 931 + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, 932 + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, 933 + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, 934 + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, 935 + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, 936 + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, 937 + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, 938 + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, 939 + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, 940 + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, 941 + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, 942 + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, 943 + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, 944 + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, 945 + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, 946 + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, 947 + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, 948 + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, 949 + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, 950 + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, 951 + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, 952 + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, 953 + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, 954 + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, 955 + ] 956 + 957 + [[package]] 810 958 name = "libipld" 811 959 version = "3.1.1" 812 960 source = { registry = "https://pypi.org/simple" } ··· 862 1010 ] 863 1011 864 1012 [[package]] 1013 + name = "markupsafe" 1014 + version = "3.0.3" 1015 + source = { registry = "https://pypi.org/simple" } 1016 + sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } 1017 + wheels = [ 1018 + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, 1019 + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, 1020 + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, 1021 + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, 1022 + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, 1023 + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, 1024 + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, 1025 + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, 1026 + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, 1027 + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, 1028 + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, 1029 + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, 1030 + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, 1031 + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, 1032 + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, 1033 + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, 1034 + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, 1035 + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, 1036 + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, 1037 + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, 1038 + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, 1039 + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, 1040 + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, 1041 + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, 1042 + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, 1043 + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, 1044 + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, 1045 + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, 1046 + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, 1047 + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, 1048 + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, 1049 + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, 1050 + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, 1051 + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, 1052 + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, 1053 + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, 1054 + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, 1055 + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, 1056 + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, 1057 + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, 1058 + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, 1059 + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, 1060 + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, 1061 + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, 1062 + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, 1063 + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, 1064 + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, 1065 + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, 1066 + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, 1067 + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, 1068 + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, 1069 + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, 1070 + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, 1071 + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, 1072 + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, 1073 + ] 1074 + 1075 + [[package]] 865 1076 name = "mcp" 866 - version = "1.12.0" 1077 + version = "1.16.0" 867 1078 source = { registry = "https://pypi.org/simple" } 868 1079 dependencies = [ 869 1080 { name = "anyio" }, ··· 878 1089 { name = "starlette" }, 879 1090 { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, 880 1091 ] 881 - sdist = { url = "https://files.pythonhosted.org/packages/45/94/caa0f4754e2437f7033068989f13fee784856f95870c786b0b5c2c0f511e/mcp-1.12.0.tar.gz", hash = "sha256:853f6b17a3f31ea6e2f278c2ec7d3b38457bc80c7c2c675260dd7f04a6fd0e70", size = 424678, upload-time = "2025-07-17T19:46:35.522Z" } 1092 + sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" } 882 1093 wheels = [ 883 - { url = "https://files.pythonhosted.org/packages/ed/da/c7eaab6a58f1034de115b7902141ad8f81b4f3bbf7dc0cc267594947a4d7/mcp-1.12.0-py3-none-any.whl", hash = "sha256:19a498b2bf273283e463b4dd1ed83f791fbba5c25bfa16b8b34cfd5571673e7f", size = 158470, upload-time = "2025-07-17T19:46:34.166Z" }, 1094 + { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" }, 884 1095 ] 885 1096 886 1097 [[package]] ··· 906 1117 sdist = { url = "https://files.pythonhosted.org/packages/e9/e7/204a54d07c37ebf173590af85bf46cddf8bc343b9d6005804581967b4751/mistralai-1.9.2.tar.gz", hash = "sha256:c0c6d5aff18ffccbc0d22c06fbc84280d71eeaeb08fa4e1ef7326b36629cfb0b", size = 192678, upload-time = "2025-07-10T13:07:08.85Z" } 907 1118 wheels = [ 908 1119 { url = "https://files.pythonhosted.org/packages/24/eb/f746a3f977d3c0059e4afa19d26b1293f54c6258fcf841957e584be6927f/mistralai-1.9.2-py3-none-any.whl", hash = "sha256:7c3fff00e50227d379dea82052455c2610612a8ef476fa97393191aeeb7ab15f", size = 411581, upload-time = "2025-07-10T13:07:07.226Z" }, 1120 + ] 1121 + 1122 + [[package]] 1123 + name = "more-itertools" 1124 + version = "10.8.0" 1125 + source = { registry = "https://pypi.org/simple" } 1126 + sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } 1127 + wheels = [ 1128 + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, 909 1129 ] 910 1130 911 1131 [[package]] ··· 991 1211 ] 992 1212 993 1213 [[package]] 1214 + name = "openapi-core" 1215 + version = "0.19.5" 1216 + source = { registry = "https://pypi.org/simple" } 1217 + dependencies = [ 1218 + { name = "isodate" }, 1219 + { name = "jsonschema" }, 1220 + { name = "jsonschema-path" }, 1221 + { name = "more-itertools" }, 1222 + { name = "openapi-schema-validator" }, 1223 + { name = "openapi-spec-validator" }, 1224 + { name = "parse" }, 1225 + { name = "typing-extensions" }, 1226 + { name = "werkzeug" }, 1227 + ] 1228 + sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } 1229 + wheels = [ 1230 + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, 1231 + ] 1232 + 1233 + [[package]] 1234 + name = "openapi-pydantic" 1235 + version = "0.5.1" 1236 + source = { registry = "https://pypi.org/simple" } 1237 + dependencies = [ 1238 + { name = "pydantic" }, 1239 + ] 1240 + sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } 1241 + wheels = [ 1242 + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, 1243 + ] 1244 + 1245 + [[package]] 1246 + name = "openapi-schema-validator" 1247 + version = "0.6.3" 1248 + source = { registry = "https://pypi.org/simple" } 1249 + dependencies = [ 1250 + { name = "jsonschema" }, 1251 + { name = "jsonschema-specifications" }, 1252 + { name = "rfc3339-validator" }, 1253 + ] 1254 + sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } 1255 + wheels = [ 1256 + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, 1257 + ] 1258 + 1259 + [[package]] 1260 + name = "openapi-spec-validator" 1261 + version = "0.7.2" 1262 + source = { registry = "https://pypi.org/simple" } 1263 + dependencies = [ 1264 + { name = "jsonschema" }, 1265 + { name = "jsonschema-path" }, 1266 + { name = "lazy-object-proxy" }, 1267 + { name = "openapi-schema-validator" }, 1268 + ] 1269 + sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } 1270 + wheels = [ 1271 + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, 1272 + ] 1273 + 1274 + [[package]] 994 1275 name = "opentelemetry-api" 995 1276 version = "1.35.0" 996 1277 source = { registry = "https://pypi.org/simple" } ··· 1010 1291 sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 1011 1292 wheels = [ 1012 1293 { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 1294 + ] 1295 + 1296 + [[package]] 1297 + name = "parse" 1298 + version = "1.20.2" 1299 + source = { registry = "https://pypi.org/simple" } 1300 + sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } 1301 + wheels = [ 1302 + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, 1303 + ] 1304 + 1305 + [[package]] 1306 + name = "pathable" 1307 + version = "0.4.4" 1308 + source = { registry = "https://pypi.org/simple" } 1309 + sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } 1310 + wheels = [ 1311 + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, 1013 1312 ] 1014 1313 1015 1314 [[package]] ··· 1113 1412 1114 1413 [[package]] 1115 1414 name = "pybase64" 1116 - version = "1.4.1" 1415 + version = "1.4.2" 1117 1416 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" } 1417 + sdist = { url = "https://files.pythonhosted.org/packages/04/14/43297a7b7f0c1bf0c00b596f754ee3ac946128c64d21047ccf9c9bbc5165/pybase64-1.4.2.tar.gz", hash = "sha256:46cdefd283ed9643315d952fe44de80dc9b9a811ce6e3ec97fd1827af97692d0", size = 137246, upload-time = "2025-07-27T13:08:57.808Z" } 1119 1418 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" }, 1419 + { url = "https://files.pythonhosted.org/packages/28/6d/11ede991e800797b9f5ebd528013b34eee5652df93de61ffb24503393fa5/pybase64-1.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2c75d1388855b5a1015b65096d7dbcc708e7de3245dcbedeb872ec05a09326", size = 38326, upload-time = "2025-07-27T13:03:09.065Z" }, 1420 + { url = "https://files.pythonhosted.org/packages/fe/84/87f1f565f42e2397e2aaa2477c86419f5173c3699881c42325c090982f0a/pybase64-1.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b621a972a01841368fdb9dedc55fd3c6e0c7217d0505ba3b1ebe95e7ef1b493", size = 31661, upload-time = "2025-07-27T13:03:10.295Z" }, 1421 + { url = "https://files.pythonhosted.org/packages/cb/2a/a24c810e7a61d2cc6f73fe9ee4872a03030887fa8654150901b15f376f65/pybase64-1.4.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f48c32ac6a16cbf57a5a96a073fef6ff7e3526f623cd49faa112b7f9980bafba", size = 68192, upload-time = "2025-07-27T13:03:11.467Z" }, 1422 + { url = "https://files.pythonhosted.org/packages/ee/87/d9baf98cbfc37b8657290ad4421f3a3c36aa0eafe4872c5859cfb52f3448/pybase64-1.4.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ace8b23093a6bb862477080d9059b784096ab2f97541e8bfc40d42f062875149", size = 71587, upload-time = "2025-07-27T13:03:12.719Z" }, 1423 + { url = "https://files.pythonhosted.org/packages/0b/89/3df043cc56ef3b91b7aa0c26ae822a2d7ec8da0b0fd7c309c879b0eb5988/pybase64-1.4.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1772c7532a7fb6301baea3dd3e010148dbf70cd1136a83c2f5f91bdc94822145", size = 59910, upload-time = "2025-07-27T13:03:14.266Z" }, 1424 + { url = "https://files.pythonhosted.org/packages/75/4f/6641e9edf37aeb4d4524dc7ba2168eff8d96c90e77f6283c2be3400ab380/pybase64-1.4.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:f86f7faddcba5cbfea475f8ab96567834c28bf09ca6c7c3d66ee445adac80d8f", size = 56701, upload-time = "2025-07-27T13:03:15.6Z" }, 1425 + { url = "https://files.pythonhosted.org/packages/2d/7f/20d8ac1046f12420a0954a45a13033e75f98aade36eecd00c64e3549b071/pybase64-1.4.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b8c8e275b5294089f314814b4a50174ab90af79d6a4850f6ae11261ff6a7372", size = 59288, upload-time = "2025-07-27T13:03:16.823Z" }, 1426 + { url = "https://files.pythonhosted.org/packages/17/ea/9c0ca570e3e50b3c6c3442e280c83b321a0464c86a9db1f982a4ff531550/pybase64-1.4.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:864d85a0470c615807ae8b97d724d068b940a2d10ac13a5f1b9e75a3ce441758", size = 60267, upload-time = "2025-07-27T13:03:18.132Z" }, 1427 + { url = "https://files.pythonhosted.org/packages/f9/ac/46894929d71ccedebbfb0284173b0fea96bc029cd262654ba8451a7035d6/pybase64-1.4.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:47254d97ed2d8351e30ecfdb9e2414547f66ba73f8a09f932c9378ff75cd10c5", size = 54801, upload-time = "2025-07-27T13:03:19.669Z" }, 1428 + { url = "https://files.pythonhosted.org/packages/6a/1e/02c95218ea964f0b2469717c2c69b48e63f4ca9f18af01a5b2a29e4c1216/pybase64-1.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:264b65ecc4f0ee73f3298ab83bbd8008f7f9578361b8df5b448f985d8c63e02a", size = 58599, upload-time = "2025-07-27T13:03:20.951Z" }, 1429 + { url = "https://files.pythonhosted.org/packages/15/45/ccc21004930789b8fb439d43e3212a6c260ccddb2bf450c39a20db093f33/pybase64-1.4.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbcc2b30cd740c16c9699f596f22c7a9e643591311ae72b1e776f2d539e9dd9d", size = 52388, upload-time = "2025-07-27T13:03:23.064Z" }, 1430 + { url = "https://files.pythonhosted.org/packages/c4/45/22e46e549710c4c237d77785b6fb1bc4c44c288a5c44237ba9daf5c34b82/pybase64-1.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cda9f79c22d51ee4508f5a43b673565f1d26af4330c99f114e37e3186fdd3607", size = 68802, upload-time = "2025-07-27T13:03:24.673Z" }, 1431 + { url = "https://files.pythonhosted.org/packages/55/0c/232c6261b81296e5593549b36e6e7884a5da008776d12665923446322c36/pybase64-1.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0c91c6d2a7232e2a1cd10b3b75a8bb657defacd4295a1e5e80455df2dfc84d4f", size = 57841, upload-time = "2025-07-27T13:03:25.948Z" }, 1432 + { url = "https://files.pythonhosted.org/packages/20/8a/b35a615ae6f04550d696bb179c414538b3b477999435fdd4ad75b76139e4/pybase64-1.4.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a370dea7b1cee2a36a4d5445d4e09cc243816c5bc8def61f602db5a6f5438e52", size = 54320, upload-time = "2025-07-27T13:03:27.495Z" }, 1433 + { url = "https://files.pythonhosted.org/packages/d3/a9/8bd4f9bcc53689f1b457ecefed1eaa080e4949d65a62c31a38b7253d5226/pybase64-1.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9aa4de83f02e462a6f4e066811c71d6af31b52d7484de635582d0e3ec3d6cc3e", size = 56482, upload-time = "2025-07-27T13:03:28.942Z" }, 1434 + { url = "https://files.pythonhosted.org/packages/75/e5/4a7735b54a1191f61c3f5c2952212c85c2d6b06eb5fb3671c7603395f70c/pybase64-1.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83a1c2f9ed00fee8f064d548c8654a480741131f280e5750bb32475b7ec8ee38", size = 70959, upload-time = "2025-07-27T13:03:30.171Z" }, 1435 + { url = "https://files.pythonhosted.org/packages/d3/67/e2b6cb32c782e12304d467418e70da0212567f42bd4d3b5eb1fdf64920ad/pybase64-1.4.2-cp312-cp312-win32.whl", hash = "sha256:a6e5688b18d558e8c6b8701cc8560836c4bbeba61d33c836b4dba56b19423716", size = 33683, upload-time = "2025-07-27T13:03:31.775Z" }, 1436 + { url = "https://files.pythonhosted.org/packages/4f/bc/d5c277496063a09707486180f17abbdbdebbf2f5c4441b20b11d3cb7dc7c/pybase64-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:c995d21b8bd08aa179cd7dd4db0695c185486ecc72da1e8f6c37ec86cadb8182", size = 35817, upload-time = "2025-07-27T13:03:32.99Z" }, 1437 + { url = "https://files.pythonhosted.org/packages/e6/69/e4be18ae685acff0ae77f75d4586590f29d2cd187bf603290cf1d635cad4/pybase64-1.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:e254b9258c40509c2ea063a7784f6994988f3f26099d6e08704e3c15dfed9a55", size = 30900, upload-time = "2025-07-27T13:03:34.499Z" }, 1438 + { url = "https://files.pythonhosted.org/packages/f4/56/5337f27a8b8d2d6693f46f7b36bae47895e5820bfa259b0072574a4e1057/pybase64-1.4.2-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:0f331aa59549de21f690b6ccc79360ffed1155c3cfbc852eb5c097c0b8565a2b", size = 33888, upload-time = "2025-07-27T13:03:35.698Z" }, 1439 + { url = "https://files.pythonhosted.org/packages/4c/09/f3f4b11fc9beda7e8625e29fb0f549958fcbb34fea3914e1c1d95116e344/pybase64-1.4.2-cp313-cp313-android_21_x86_64.whl", hash = "sha256:9dad20bf1f3ed9e6fe566c4c9d07d9a6c04f5a280daebd2082ffb8620b0a880d", size = 40796, upload-time = "2025-07-27T13:03:36.927Z" }, 1440 + { url = "https://files.pythonhosted.org/packages/e3/ff/470768f0fe6de0aa302a8cb1bdf2f9f5cffc3f69e60466153be68bc953aa/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:69d3f0445b0faeef7bb7f93bf8c18d850785e2a77f12835f49e524cc54af04e7", size = 30914, upload-time = "2025-07-27T13:03:38.475Z" }, 1441 + { url = "https://files.pythonhosted.org/packages/75/6b/d328736662665e0892409dc410353ebef175b1be5eb6bab1dad579efa6df/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2372b257b1f4dd512f317fb27e77d313afd137334de64c87de8374027aacd88a", size = 31380, upload-time = "2025-07-27T13:03:39.7Z" }, 1442 + { url = "https://files.pythonhosted.org/packages/ca/96/7ff718f87c67f4147c181b73d0928897cefa17dc75d7abc6e37730d5908f/pybase64-1.4.2-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fb794502b4b1ec91c4ca5d283ae71aef65e3de7721057bd9e2b3ec79f7a62d7d", size = 38230, upload-time = "2025-07-27T13:03:41.637Z" }, 1443 + { url = "https://files.pythonhosted.org/packages/4d/58/a3307b048d799ff596a3c7c574fcba66f9b6b8c899a3c00a698124ca7ad5/pybase64-1.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d5c532b03fd14a5040d6cf6571299a05616f925369c72ddf6fe2fb643eb36fed", size = 38319, upload-time = "2025-07-27T13:03:42.847Z" }, 1444 + { url = "https://files.pythonhosted.org/packages/08/a7/0bda06341b0a2c830d348c6e1c4d348caaae86c53dc9a046e943467a05e9/pybase64-1.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f699514dc1d5689ca9cf378139e0214051922732f9adec9404bc680a8bef7c0", size = 31655, upload-time = "2025-07-27T13:03:44.426Z" }, 1445 + { url = "https://files.pythonhosted.org/packages/87/df/e1d6e8479e0c5113c2c63c7b44886935ce839c2d99884c7304ca9e86547c/pybase64-1.4.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cd3e8713cbd32c8c6aa935feaf15c7670e2b7e8bfe51c24dc556811ebd293a29", size = 68232, upload-time = "2025-07-27T13:03:45.729Z" }, 1446 + { url = "https://files.pythonhosted.org/packages/71/ab/db4dbdfccb9ca874d6ce34a0784761471885d96730de85cee3d300381529/pybase64-1.4.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d377d48acf53abf4b926c2a7a24a19deb092f366a04ffd856bf4b3aa330b025d", size = 71608, upload-time = "2025-07-27T13:03:47.01Z" }, 1447 + { url = "https://files.pythonhosted.org/packages/11/e9/508df958563951045d728bbfbd3be77465f9231cf805cb7ccaf6951fc9f1/pybase64-1.4.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d83c076e78d619b9e1dd674e2bf5fb9001aeb3e0b494b80a6c8f6d4120e38cd9", size = 59912, upload-time = "2025-07-27T13:03:48.277Z" }, 1448 + { url = "https://files.pythonhosted.org/packages/f2/58/7f2cef1ceccc682088958448d56727369de83fa6b29148478f4d2acd107a/pybase64-1.4.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:ab9cdb6a8176a5cb967f53e6ad60e40c83caaa1ae31c5e1b29e5c8f507f17538", size = 56413, upload-time = "2025-07-27T13:03:49.908Z" }, 1449 + { url = "https://files.pythonhosted.org/packages/08/7c/7e0af5c5728fa7e2eb082d88eca7c6bd17429be819d58518e74919d42e66/pybase64-1.4.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:adf0c103ad559dbfb9fe69edfd26a15c65d9c991a5ab0a25b04770f9eb0b9484", size = 59311, upload-time = "2025-07-27T13:03:51.238Z" }, 1450 + { url = "https://files.pythonhosted.org/packages/03/8b/09825d0f37e45b9a3f546e5f990b6cf2dd838e54ea74122c2464646e0c77/pybase64-1.4.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:0d03ef2f253d97ce0685d3624bf5e552d716b86cacb8a6c971333ba4b827e1fc", size = 60282, upload-time = "2025-07-27T13:03:52.56Z" }, 1451 + { url = "https://files.pythonhosted.org/packages/9c/3f/3711d2413f969bfd5b9cc19bc6b24abae361b7673ff37bcb90c43e199316/pybase64-1.4.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e565abf906efee76ae4be1aef5df4aed0fda1639bc0d7732a3dafef76cb6fc35", size = 54845, upload-time = "2025-07-27T13:03:54.167Z" }, 1452 + { url = "https://files.pythonhosted.org/packages/c6/3c/4c7ce1ae4d828c2bb56d144322f81bffbaaac8597d35407c3d7cbb0ff98f/pybase64-1.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3c6a5f15fd03f232fc6f295cce3684f7bb08da6c6d5b12cc771f81c9f125cc6", size = 58615, upload-time = "2025-07-27T13:03:55.494Z" }, 1453 + { url = "https://files.pythonhosted.org/packages/f5/8f/c2fc03bf4ed038358620065c75968a30184d5d3512d09d3ef9cc3bd48592/pybase64-1.4.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bad9e3db16f448728138737bbd1af9dc2398efd593a8bdd73748cc02cd33f9c6", size = 52434, upload-time = "2025-07-27T13:03:56.808Z" }, 1454 + { url = "https://files.pythonhosted.org/packages/e2/0a/757d6df0a60327c893cfae903e15419914dd792092dc8cc5c9523d40bc9b/pybase64-1.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2683ef271328365c31afee0ed8fa29356fb8fb7c10606794656aa9ffb95e92be", size = 68824, upload-time = "2025-07-27T13:03:58.735Z" }, 1455 + { url = "https://files.pythonhosted.org/packages/a0/14/84abe2ed8c29014239be1cfab45dfebe5a5ca779b177b8b6f779bd8b69da/pybase64-1.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:265b20089cd470079114c09bb74b101b3bfc3c94ad6b4231706cf9eff877d570", size = 57898, upload-time = "2025-07-27T13:04:00.379Z" }, 1456 + { url = "https://files.pythonhosted.org/packages/7e/c6/d193031f90c864f7b59fa6d1d1b5af41f0f5db35439988a8b9f2d1b32a13/pybase64-1.4.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e53173badead10ef8b839aa5506eecf0067c7b75ad16d9bf39bc7144631f8e67", size = 54319, upload-time = "2025-07-27T13:04:01.742Z" }, 1457 + { url = "https://files.pythonhosted.org/packages/cb/37/ec0c7a610ff8f994ee6e0c5d5d66b6b6310388b96ebb347b03ae39870fdf/pybase64-1.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5823b8dcf74da7da0f761ed60c961e8928a6524e520411ad05fe7f9f47d55b40", size = 56472, upload-time = "2025-07-27T13:04:03.089Z" }, 1458 + { url = "https://files.pythonhosted.org/packages/c4/5a/e585b74f85cedd261d271e4c2ef333c5cfce7e80750771808f56fee66b98/pybase64-1.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1237f66c54357d325390da60aa5e21c6918fbcd1bf527acb9c1f4188c62cb7d5", size = 70966, upload-time = "2025-07-27T13:04:04.361Z" }, 1459 + { url = "https://files.pythonhosted.org/packages/ad/20/1b2fdd98b4ba36008419668c813025758214c543e362c66c49214ecd1127/pybase64-1.4.2-cp313-cp313-win32.whl", hash = "sha256:b0b851eb4f801d16040047f6889cca5e9dfa102b3e33f68934d12511245cef86", size = 33681, upload-time = "2025-07-27T13:04:06.126Z" }, 1460 + { url = "https://files.pythonhosted.org/packages/ff/64/3df4067d169c047054889f34b5a946cbe3785bca43404b93c962a5461a41/pybase64-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:19541c6e26d17d9522c02680fe242206ae05df659c82a657aabadf209cd4c6c7", size = 35822, upload-time = "2025-07-27T13:04:07.752Z" }, 1461 + { url = "https://files.pythonhosted.org/packages/d1/fd/db505188adf812e60ee923f196f9deddd8a1895b2b29b37f5db94afc3b1c/pybase64-1.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:77a191863d576c0a5dd81f8a568a5ca15597cc980ae809dce62c717c8d42d8aa", size = 30899, upload-time = "2025-07-27T13:04:09.062Z" }, 1462 + { url = "https://files.pythonhosted.org/packages/d9/27/5f5fecd206ec1e06e1608a380af18dcb76a6ab08ade6597a3251502dcdb2/pybase64-1.4.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2e194bbabe3fdf9e47ba9f3e157394efe0849eb226df76432126239b3f44992c", size = 38677, upload-time = "2025-07-27T13:04:10.334Z" }, 1463 + { url = "https://files.pythonhosted.org/packages/bf/0f/abe4b5a28529ef5f74e8348fa6a9ef27d7d75fbd98103d7664cf485b7d8f/pybase64-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:39aef1dadf4a004f11dd09e703abaf6528a87c8dbd39c448bb8aebdc0a08c1be", size = 32066, upload-time = "2025-07-27T13:04:11.641Z" }, 1464 + { url = "https://files.pythonhosted.org/packages/ac/7e/ea0ce6a7155cada5526017ec588b6d6185adea4bf9331565272f4ef583c2/pybase64-1.4.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:91cb920c7143e36ec8217031282c8651da3b2206d70343f068fac0e7f073b7f9", size = 72300, upload-time = "2025-07-27T13:04:12.969Z" }, 1465 + { url = "https://files.pythonhosted.org/packages/45/2d/e64c7a056c9ec48dfe130d1295e47a8c2b19c3984488fc08e5eaa1e86c88/pybase64-1.4.2-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6958631143fb9e71f9842000da042ec2f6686506b6706e2dfda29e97925f6aa0", size = 75520, upload-time = "2025-07-27T13:04:14.374Z" }, 1466 + { url = "https://files.pythonhosted.org/packages/43/e0/e5f93b2e1cb0751a22713c4baa6c6eaf5f307385e369180486c8316ed21e/pybase64-1.4.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc35f14141ef3f1ac70d963950a278a2593af66fe5a1c7a208e185ca6278fa25", size = 65384, upload-time = "2025-07-27T13:04:16.204Z" }, 1467 + { url = "https://files.pythonhosted.org/packages/ff/23/8c645a1113ad88a1c6a3d0e825e93ef8b74ad3175148767853a0a4d7626e/pybase64-1.4.2-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5d949d2d677859c3a8507e1b21432a039d2b995e0bd3fe307052b6ded80f207a", size = 60471, upload-time = "2025-07-27T13:04:17.947Z" }, 1468 + { url = "https://files.pythonhosted.org/packages/8b/81/edd0f7d8b0526b91730a0dd4ce6b4c8be2136cd69d424afe36235d2d2a06/pybase64-1.4.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:09caacdd3e15fe7253a67781edd10a6a918befab0052a2a3c215fe5d1f150269", size = 63945, upload-time = "2025-07-27T13:04:19.383Z" }, 1469 + { url = "https://files.pythonhosted.org/packages/a5/a5/edc224cd821fd65100b7af7c7e16b8f699916f8c0226c9c97bbae5a75e71/pybase64-1.4.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e44b0e793b23f28ea0f15a9754bd0c960102a2ac4bccb8fafdedbd4cc4d235c0", size = 64858, upload-time = "2025-07-27T13:04:20.807Z" }, 1470 + { url = "https://files.pythonhosted.org/packages/11/3b/92853f968f1af7e42b7e54d21bdd319097b367e7dffa2ca20787361df74c/pybase64-1.4.2-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:849f274d0bcb90fc6f642c39274082724d108e41b15f3a17864282bd41fc71d5", size = 58557, upload-time = "2025-07-27T13:04:22.229Z" }, 1471 + { url = "https://files.pythonhosted.org/packages/76/09/0ec6bd2b2303b0ea5c6da7535edc9a608092075ef8c0cdd96e3e726cd687/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:528dba7ef1357bd7ce1aea143084501f47f5dd0fff7937d3906a68565aa59cfe", size = 63624, upload-time = "2025-07-27T13:04:23.952Z" }, 1472 + { url = "https://files.pythonhosted.org/packages/73/6e/52cb1ced2a517a3118b2e739e9417432049013ac7afa15d790103059e8e4/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:1da54be743d9a68671700cfe56c3ab8c26e8f2f5cc34eface905c55bc3a9af94", size = 56174, upload-time = "2025-07-27T13:04:25.419Z" }, 1473 + { url = "https://files.pythonhosted.org/packages/5b/9d/820fe79347467e48af985fe46180e1dd28e698ade7317bebd66de8a143f5/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9b07c0406c3eaa7014499b0aacafb21a6d1146cfaa85d56f0aa02e6d542ee8f3", size = 72640, upload-time = "2025-07-27T13:04:26.824Z" }, 1474 + { url = "https://files.pythonhosted.org/packages/53/58/e863e10d08361e694935c815b73faad7e1ab03f99ae154d86c4e2f331896/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:312f2aa4cf5d199a97fbcaee75d2e59ebbaafcd091993eb373b43683498cdacb", size = 62453, upload-time = "2025-07-27T13:04:28.562Z" }, 1475 + { url = "https://files.pythonhosted.org/packages/95/f0/c392c4ac8ccb7a34b28377c21faa2395313e3c676d76c382642e19a20703/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad59362fc267bf15498a318c9e076686e4beeb0dfe09b457fabbc2b32468b97a", size = 58103, upload-time = "2025-07-27T13:04:29.996Z" }, 1476 + { url = "https://files.pythonhosted.org/packages/32/30/00ab21316e7df8f526aa3e3dc06f74de6711d51c65b020575d0105a025b2/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:01593bd064e7dcd6c86d04e94e44acfe364049500c20ac68ca1e708fbb2ca970", size = 60779, upload-time = "2025-07-27T13:04:31.549Z" }, 1477 + { url = "https://files.pythonhosted.org/packages/a6/65/114ca81839b1805ce4a2b7d58bc16e95634734a2059991f6382fc71caf3e/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5b81547ad8ea271c79fdf10da89a1e9313cb15edcba2a17adf8871735e9c02a0", size = 74684, upload-time = "2025-07-27T13:04:32.976Z" }, 1478 + { url = "https://files.pythonhosted.org/packages/54/8f/aa9d445b9bb693b8f6bb1456bd6d8576d79b7a63bf6c69af3a539235b15f/pybase64-1.4.2-cp313-cp313t-win32.whl", hash = "sha256:7edbe70b5654545a37e6e6b02de738303b1bbdfcde67f6cfec374cfb5cc4099e", size = 33961, upload-time = "2025-07-27T13:04:34.806Z" }, 1479 + { url = "https://files.pythonhosted.org/packages/0e/e5/da37cfb173c646fd4fc7c6aae2bc41d40de2ee49529854af8f4e6f498b45/pybase64-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:385690addf87c25d6366fab5d8ff512eed8a7ecb18da9e8152af1c789162f208", size = 36199, upload-time = "2025-07-27T13:04:36.223Z" }, 1480 + { url = "https://files.pythonhosted.org/packages/66/3e/1eb68fb7d00f2cec8bd9838e2a30d183d6724ae06e745fd6e65216f170ff/pybase64-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c2070d0aa88580f57fe15ca88b09f162e604d19282915a95a3795b5d3c1c05b5", size = 31221, upload-time = "2025-07-27T13:04:37.704Z" }, 1481 + { url = "https://files.pythonhosted.org/packages/99/bf/00a87d951473ce96c8c08af22b6983e681bfabdb78dd2dcf7ee58eac0932/pybase64-1.4.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4157ad277a32cf4f02a975dffc62a3c67d73dfa4609b2c1978ef47e722b18b8e", size = 30924, upload-time = "2025-07-27T13:04:39.189Z" }, 1482 + { url = "https://files.pythonhosted.org/packages/ae/43/dee58c9d60e60e6fb32dc6da722d84592e22f13c277297eb4ce6baf99a99/pybase64-1.4.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e113267dc349cf624eb4f4fbf53fd77835e1aa048ac6877399af426aab435757", size = 31390, upload-time = "2025-07-27T13:04:40.995Z" }, 1483 + { url = "https://files.pythonhosted.org/packages/e1/11/b28906fc2e330b8b1ab4bc845a7bef808b8506734e90ed79c6062b095112/pybase64-1.4.2-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:cea5aaf218fd9c5c23afacfe86fd4464dfedc1a0316dd3b5b4075b068cc67df0", size = 38212, upload-time = "2025-07-27T13:04:42.729Z" }, 1484 + { url = "https://files.pythonhosted.org/packages/24/9e/868d1e104413d14b19feaf934fc7fad4ef5b18946385f8bb79684af40f24/pybase64-1.4.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:41213497abbd770435c7a9c8123fb02b93709ac4cf60155cd5aefc5f3042b600", size = 38303, upload-time = "2025-07-27T13:04:44.095Z" }, 1485 + { url = "https://files.pythonhosted.org/packages/a3/73/f7eac96ca505df0600280d6bfc671a9e2e2f947c2b04b12a70e36412f7eb/pybase64-1.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8b522df7ee00f2ac1993ccd5e1f6608ae7482de3907668c2ff96a83ef213925", size = 31669, upload-time = "2025-07-27T13:04:45.845Z" }, 1486 + { url = "https://files.pythonhosted.org/packages/c6/43/8e18bea4fd455100112d6a73a83702843f067ef9b9272485b6bdfd9ed2f0/pybase64-1.4.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:06725022e540c5b098b978a0418ca979773e2cbdbb76f10bd97536f2ad1c5b49", size = 68452, upload-time = "2025-07-27T13:04:47.788Z" }, 1487 + { url = "https://files.pythonhosted.org/packages/e4/2e/851eb51284b97354ee5dfa1309624ab90920696e91a33cd85b13d20cc5c1/pybase64-1.4.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a3e54dcf0d0305ec88473c9d0009f698cabf86f88a8a10090efeff2879c421bb", size = 71674, upload-time = "2025-07-27T13:04:49.294Z" }, 1488 + { url = "https://files.pythonhosted.org/packages/57/0d/5cf1e5dc64aec8db43e8dee4e4046856d639a72bcb0fb3e716be42ced5f1/pybase64-1.4.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67675cee727a60dc91173d2790206f01aa3c7b3fbccfa84fd5c1e3d883fe6caa", size = 60027, upload-time = "2025-07-27T13:04:50.769Z" }, 1489 + { url = "https://files.pythonhosted.org/packages/a4/8e/3479266bc0e65f6cc48b3938d4a83bff045330649869d950a378f2ddece0/pybase64-1.4.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:753da25d4fd20be7bda2746f545935773beea12d5cb5ec56ec2d2960796477b1", size = 56461, upload-time = "2025-07-27T13:04:52.37Z" }, 1490 + { url = "https://files.pythonhosted.org/packages/20/b6/f2b6cf59106dd78bae8717302be5b814cec33293504ad409a2eb752ad60c/pybase64-1.4.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a78c768ce4ca550885246d14babdb8923e0f4a848dfaaeb63c38fc99e7ea4052", size = 59446, upload-time = "2025-07-27T13:04:53.967Z" }, 1491 + { url = "https://files.pythonhosted.org/packages/16/70/3417797dfccdfdd0a54e4ad17c15b0624f0fc2d6a362210f229f5c4e8fd0/pybase64-1.4.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:51b17f36d890c92f0618fb1c8db2ccc25e6ed07afa505bab616396fc9b0b0492", size = 60350, upload-time = "2025-07-27T13:04:55.881Z" }, 1492 + { url = "https://files.pythonhosted.org/packages/a0/c6/6e4269dd98d150ae95d321b311a345eae0f7fd459d97901b4a586d7513bb/pybase64-1.4.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f92218d667049ab4f65d54fa043a88ffdb2f07fff1f868789ef705a5221de7ec", size = 54989, upload-time = "2025-07-27T13:04:57.436Z" }, 1493 + { url = "https://files.pythonhosted.org/packages/f9/e8/18c1b0c255f964fafd0412b0d5a163aad588aeccb8f84b9bf9c8611d80f6/pybase64-1.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3547b3d1499919a06491b3f879a19fbe206af2bd1a424ecbb4e601eb2bd11fea", size = 58724, upload-time = "2025-07-27T13:04:59.406Z" }, 1494 + { url = "https://files.pythonhosted.org/packages/b1/ad/ddfbd2125fc20b94865fb232b2e9105376fa16eee492e4b7786d42a86cbf/pybase64-1.4.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:958af7b0e09ddeb13e8c2330767c47b556b1ade19c35370f6451d139cde9f2a9", size = 52285, upload-time = "2025-07-27T13:05:01.198Z" }, 1495 + { url = "https://files.pythonhosted.org/packages/b6/4c/b9d4ec9224add33c84b925a03d1a53cd4106efb449ea8e0ae7795fed7bf7/pybase64-1.4.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4facc57f6671e2229a385a97a618273e7be36a9ea0a9d1c1b9347f14d19ceba8", size = 69036, upload-time = "2025-07-27T13:05:03.109Z" }, 1496 + { url = "https://files.pythonhosted.org/packages/92/38/7b96794da77bed3d9b4fea40f14ae563648fba83a696e7602fabe60c0eb7/pybase64-1.4.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a32fc57d05d73a7c9b0ca95e9e265e21cf734195dc6873829a890058c35f5cfd", size = 57938, upload-time = "2025-07-27T13:05:04.744Z" }, 1497 + { url = "https://files.pythonhosted.org/packages/eb/c5/ae8bbce3c322d1b074e79f51f5df95961fe90cb8748df66c6bc97616e974/pybase64-1.4.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3dc853243c81ce89cc7318e6946f860df28ddb7cd2a0648b981652d9ad09ee5a", size = 54474, upload-time = "2025-07-27T13:05:06.662Z" }, 1498 + { url = "https://files.pythonhosted.org/packages/15/9a/c09887c4bb1b43c03fc352e2671ef20c6686c6942a99106a45270ee5b840/pybase64-1.4.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0e6d863a86b3e7bc6ac9bd659bebda4501b9da842521111b0b0e54eb51295df5", size = 56533, upload-time = "2025-07-27T13:05:08.368Z" }, 1499 + { url = "https://files.pythonhosted.org/packages/4f/0f/d5114d63d35d085639606a880cb06e2322841cd4b213adfc14d545c1186f/pybase64-1.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6579475140ff2067903725d8aca47f5747bcb211597a1edd60b58f6d90ada2bd", size = 71030, upload-time = "2025-07-27T13:05:10.3Z" }, 1500 + { url = "https://files.pythonhosted.org/packages/40/0e/fe6f1ed22ea52eb99f490a8441815ba21de288f4351aeef4968d71d20d2d/pybase64-1.4.2-cp314-cp314-win32.whl", hash = "sha256:373897f728d7b4f241a1f803ac732c27b6945d26d86b2741ad9b75c802e4e378", size = 34174, upload-time = "2025-07-27T13:05:12.254Z" }, 1501 + { url = "https://files.pythonhosted.org/packages/71/46/0e15bea52ffc63e8ae7935e945accbaf635e0aefa26d3e31fdf9bc9dcd01/pybase64-1.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:1afe3361344617d298c1d08bc657ef56d0f702d6b72cb65d968b2771017935aa", size = 36308, upload-time = "2025-07-27T13:05:13.898Z" }, 1502 + { url = "https://files.pythonhosted.org/packages/4f/dc/55849fee2577bda77c1e078da04cc9237e8e474a8c8308deb702a26f2511/pybase64-1.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:f131c9360babe522f3d90f34da3f827cba80318125cf18d66f2ee27e3730e8c4", size = 31341, upload-time = "2025-07-27T13:05:15.553Z" }, 1503 + { url = "https://files.pythonhosted.org/packages/39/44/c69d088e28b25e70ac742b6789cde038473815b2a69345c4bae82d5e244d/pybase64-1.4.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2583ac304131c1bd6e3120b0179333610f18816000db77c0a2dd6da1364722a8", size = 38678, upload-time = "2025-07-27T13:05:17.544Z" }, 1504 + { url = "https://files.pythonhosted.org/packages/00/93/2860ec067497b9cbb06242f96d44caebbd9eed32174e4eb8c1ffef760f94/pybase64-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:75a8116be4ea4cdd30a5c4f1a6f3b038e0d457eb03c8a2685d8ce2aa00ef8f92", size = 32066, upload-time = "2025-07-27T13:05:19.18Z" }, 1505 + { url = "https://files.pythonhosted.org/packages/d3/55/1e96249a38759332e8a01b31c370d88c60ceaf44692eb6ba4f0f451ee496/pybase64-1.4.2-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:217ea776a098d7c08668e5526b9764f5048bbfd28cac86834217ddfe76a4e3c4", size = 72465, upload-time = "2025-07-27T13:05:20.866Z" }, 1506 + { url = "https://files.pythonhosted.org/packages/6d/ab/0f468605b899f3e35dbb7423fba3ff98aeed1ec16abb02428468494a58f4/pybase64-1.4.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ec14683e343c95b14248cdfdfa78c052582be7a3865fd570aa7cffa5ab5cf37", size = 75693, upload-time = "2025-07-27T13:05:22.896Z" }, 1507 + { url = "https://files.pythonhosted.org/packages/91/d1/9980a0159b699e2489baba05b71b7c953b29249118ba06fdbb3e9ea1b9b5/pybase64-1.4.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:480ecf21e1e956c5a10d3cf7b3b7e75bce3f9328cf08c101e4aab1925d879f34", size = 65577, upload-time = "2025-07-27T13:05:25Z" }, 1508 + { url = "https://files.pythonhosted.org/packages/16/86/b27e7b95f9863d245c0179a7245582eda3d262669d8f822777364d8fd7d5/pybase64-1.4.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1fe1ebdc55e9447142e2f6658944aadfb5a4fbf03dbd509be34182585515ecc1", size = 60662, upload-time = "2025-07-27T13:05:27.138Z" }, 1509 + { url = "https://files.pythonhosted.org/packages/28/87/a7f0dde0abc26bfbee761f1d3558eb4b139f33ddd9fe1f6825ffa7daa22d/pybase64-1.4.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c793a2b06753accdaf5e1a8bbe5d800aab2406919e5008174f989a1ca0081411", size = 64179, upload-time = "2025-07-27T13:05:28.996Z" }, 1510 + { url = "https://files.pythonhosted.org/packages/1e/88/5d6fa1c60e1363b4cac4c396978f39e9df4689e75225d7d9c0a5998e3a14/pybase64-1.4.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6acae6e1d1f7ebe40165f08076c7a73692b2bf9046fefe673f350536e007f556", size = 64968, upload-time = "2025-07-27T13:05:30.818Z" }, 1511 + { url = "https://files.pythonhosted.org/packages/20/6e/2ed585af5b2211040445d9849326dd2445320c9316268794f5453cfbaf30/pybase64-1.4.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:88b91cd0949358aadcea75f8de5afbcf3c8c5fb9ec82325bd24285b7119cf56e", size = 58738, upload-time = "2025-07-27T13:05:32.629Z" }, 1512 + { url = "https://files.pythonhosted.org/packages/ce/94/e2960b56322eabb3fbf303fc5a72e6444594c1b90035f3975c6fe666db5c/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:53316587e1b1f47a11a5ff068d3cbd4a3911c291f2aec14882734973684871b2", size = 63802, upload-time = "2025-07-27T13:05:34.687Z" }, 1513 + { url = "https://files.pythonhosted.org/packages/95/47/312139d764c223f534f751528ce3802887c279125eac64f71cd3b4e05abc/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:caa7f20f43d00602cf9043b5ba758d54f5c41707d3709b2a5fac17361579c53c", size = 56341, upload-time = "2025-07-27T13:05:36.554Z" }, 1514 + { url = "https://files.pythonhosted.org/packages/3f/d7/aec9a6ed53b128dac32f8768b646ca5730c88eef80934054d7fa7d02f3ef/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2d93817e24fdd79c534ed97705df855af6f1d2535ceb8dfa80da9de75482a8d7", size = 72838, upload-time = "2025-07-27T13:05:38.459Z" }, 1515 + { url = "https://files.pythonhosted.org/packages/e3/a8/6ccc54c5f1f7c3450ad7c56da10c0f131d85ebe069ea6952b5b42f2e92d9/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:63cd769b51474d8d08f7f2ce73b30380d9b4078ec92ea6b348ea20ed1e1af88a", size = 62633, upload-time = "2025-07-27T13:05:40.624Z" }, 1516 + { url = "https://files.pythonhosted.org/packages/34/22/2b9d89f8ff6f2a01d6d6a88664b20a4817049cfc3f2c62caca040706660c/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cd07e6a9993c392ec8eb03912a43c6a6b21b2deb79ee0d606700fe276e9a576f", size = 58282, upload-time = "2025-07-27T13:05:42.565Z" }, 1517 + { url = "https://files.pythonhosted.org/packages/b2/14/dbf6266177532a6a11804ac080ebffcee272f491b92820c39886ee20f201/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:6a8944e8194adff4668350504bc6b7dbde2dab9244c88d99c491657d145b5af5", size = 60948, upload-time = "2025-07-27T13:05:44.48Z" }, 1518 + { url = "https://files.pythonhosted.org/packages/fd/7a/b2ae9046a66dd5746cd72836a41386517b1680bea5ce02f2b4f1c9ebc688/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04ab398ec4b6a212af57f6a21a6336d5a1d754ff4ccb215951366ab9080481b2", size = 74854, upload-time = "2025-07-27T13:05:46.416Z" }, 1519 + { url = "https://files.pythonhosted.org/packages/ef/7e/9856f6d6c38a7b730e001123d2d9fa816b8b1a45f0cdee1d509d5947b047/pybase64-1.4.2-cp314-cp314t-win32.whl", hash = "sha256:3b9201ecdcb1c3e23be4caebd6393a4e6615bd0722528f5413b58e22e3792dd3", size = 34490, upload-time = "2025-07-27T13:05:48.304Z" }, 1520 + { url = "https://files.pythonhosted.org/packages/c7/38/8523a9dc1ec8704dedbe5ccc95192ae9a7585f7eec85cc62946fe3cacd32/pybase64-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36e9b0cad8197136d73904ef5a71d843381d063fd528c5ab203fc4990264f682", size = 36680, upload-time = "2025-07-27T13:05:50.264Z" }, 1521 + { url = "https://files.pythonhosted.org/packages/3c/52/5600104ef7b85f89fb8ec54f73504ead3f6f0294027e08d281f3cafb5c1a/pybase64-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f25140496b02db0e7401567cd869fb13b4c8118bf5c2428592ec339987146d8b", size = 31600, upload-time = "2025-07-27T13:05:52.24Z" }, 1171 1522 ] 1172 1523 1173 1524 [[package]] ··· 1192 1543 sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } 1193 1544 wheels = [ 1194 1545 { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, 1546 + ] 1547 + 1548 + [package.optional-dependencies] 1549 + email = [ 1550 + { name = "email-validator" }, 1195 1551 ] 1196 1552 1197 1553 [[package]] ··· 1367 1723 ] 1368 1724 1369 1725 [[package]] 1726 + name = "pyperclip" 1727 + version = "1.11.0" 1728 + source = { registry = "https://pypi.org/simple" } 1729 + sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } 1730 + wheels = [ 1731 + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, 1732 + ] 1733 + 1734 + [[package]] 1370 1735 name = "pytest" 1371 1736 version = "8.4.1" 1372 1737 source = { registry = "https://pypi.org/simple" } ··· 1510 1875 ] 1511 1876 1512 1877 [[package]] 1878 + name = "rfc3339-validator" 1879 + version = "0.1.4" 1880 + source = { registry = "https://pypi.org/simple" } 1881 + dependencies = [ 1882 + { name = "six" }, 1883 + ] 1884 + sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } 1885 + wheels = [ 1886 + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, 1887 + ] 1888 + 1889 + [[package]] 1513 1890 name = "rich" 1514 1891 version = "14.0.0" 1515 1892 source = { registry = "https://pypi.org/simple" } ··· 1523 1900 ] 1524 1901 1525 1902 [[package]] 1903 + name = "rich-rst" 1904 + version = "1.3.1" 1905 + source = { registry = "https://pypi.org/simple" } 1906 + dependencies = [ 1907 + { name = "docutils" }, 1908 + { name = "rich" }, 1909 + ] 1910 + sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" } 1911 + wheels = [ 1912 + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, 1913 + ] 1914 + 1915 + [[package]] 1526 1916 name = "rpds-py" 1527 1917 version = "0.26.0" 1528 1918 source = { registry = "https://pypi.org/simple" } ··· 1747 2137 1748 2138 [[package]] 1749 2139 name = "turbopuffer" 1750 - version = "0.5.13" 2140 + version = "1.3.0" 1751 2141 source = { registry = "https://pypi.org/simple" } 1752 2142 dependencies = [ 1753 2143 { name = "aiohttp" }, ··· 1759 2149 { name = "sniffio" }, 1760 2150 { name = "typing-extensions" }, 1761 2151 ] 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" } 2152 + sdist = { url = "https://files.pythonhosted.org/packages/3c/91/f347d5dd23eef82688537549cc032347842bb9d6879d772a0a5a9548ddd0/turbopuffer-1.3.0.tar.gz", hash = "sha256:f1429b3073c00f85d91f2fb06666ce3c0275a2da1b8dc7f0f8d8c0b7207bee56", size = 142443, upload-time = "2025-09-24T21:12:36.46Z" } 1763 2153 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" }, 2154 + { url = "https://files.pythonhosted.org/packages/5f/9d/d54419a42c45fe5758640a5228aae71a7e825205755e68fa6494fef6eea3/turbopuffer-1.3.0-py3-none-any.whl", hash = "sha256:f373ad0bca148ef643ddcbeb5665c2c50cbfab0b9e2e5a11c86afe16597a4382", size = 109674, upload-time = "2025-09-24T21:12:34.995Z" }, 1765 2155 ] 1766 2156 1767 2157 [[package]] ··· 1855 2245 1856 2246 [[package]] 1857 2247 name = "websockets" 1858 - version = "13.1" 2248 + version = "15.0.1" 1859 2249 source = { registry = "https://pypi.org/simple" } 1860 - sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } 2250 + sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } 1861 2251 wheels = [ 1862 - { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, 1863 - { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, 1864 - { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, 1865 - { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647, upload-time = "2024-09-21T17:33:00.495Z" }, 1866 - { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592, upload-time = "2024-09-21T17:33:02.223Z" }, 1867 - { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012, upload-time = "2024-09-21T17:33:03.288Z" }, 1868 - { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311, upload-time = "2024-09-21T17:33:04.728Z" }, 1869 - { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692, upload-time = "2024-09-21T17:33:05.829Z" }, 1870 - { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686, upload-time = "2024-09-21T17:33:06.823Z" }, 1871 - { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712, upload-time = "2024-09-21T17:33:07.877Z" }, 1872 - { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145, upload-time = "2024-09-21T17:33:09.202Z" }, 1873 - { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, 1874 - { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, 1875 - { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, 1876 - { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, 1877 - { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, 1878 - { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, 1879 - { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, 1880 - { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, 1881 - { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, 1882 - { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, 1883 - { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, 1884 - { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, 2252 + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, 2253 + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, 2254 + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, 2255 + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, 2256 + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, 2257 + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, 2258 + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, 2259 + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, 2260 + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, 2261 + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, 2262 + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, 2263 + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, 2264 + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, 2265 + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, 2266 + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, 2267 + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, 2268 + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, 2269 + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, 2270 + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, 2271 + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, 2272 + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, 2273 + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, 2274 + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, 2275 + ] 2276 + 2277 + [[package]] 2278 + name = "werkzeug" 2279 + version = "3.1.1" 2280 + source = { registry = "https://pypi.org/simple" } 2281 + dependencies = [ 2282 + { name = "markupsafe" }, 2283 + ] 2284 + sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } 2285 + wheels = [ 2286 + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, 1885 2287 ] 1886 2288 1887 2289 [[package]]