Parcourir la source

feat: build agent lab MVP

zhenyu.hu il y a 3 semaines
Parent
commit
6babafa097
39 fichiers modifiés avec 3374 ajouts et 1 suppressions
  1. 5 0
      .env.example
  2. 5 0
      .gitignore
  3. 37 0
      AGENTS.md
  4. 67 1
      README.md
  5. 69 0
      docs/plans/todo-1-project-foundation.md
  6. 85 0
      docs/plans/todo-2-domain-runtime.md
  7. 121 0
      docs/plans/todo-3-websocket-api.md
  8. 91 0
      docs/plans/todo-4-real-llm-smoke.md
  9. 102 0
      docs/plans/todo-5-tool-call-history.md
  10. 75 0
      docs/plans/todo-6-cleanup-closeout.md
  11. 93 0
      docs/plans/todo-7-provider-compatibility.md
  12. 70 0
      docs/plans/todo-8-tool-pre-message-guard.md
  13. 29 0
      docs/plans/todos.md
  14. 34 0
      pyproject.toml
  15. 98 0
      scripts/ws_smoke.py
  16. 2 0
      src/agent_lab/__init__.py
  17. 2 0
      src/agent_lab/application/__init__.py
  18. 28 0
      src/agent_lab/application/contracts.py
  19. 45 0
      src/agent_lab/application/event_agent.py
  20. 115 0
      src/agent_lab/application/runtime.py
  21. 2 0
      src/agent_lab/domain/__init__.py
  22. 12 0
      src/agent_lab/domain/events.py
  23. 48 0
      src/agent_lab/domain/messages.py
  24. 2 0
      src/agent_lab/infrastructure/__init__.py
  25. 105 0
      src/agent_lab/infrastructure/chat_client.py
  26. 93 0
      src/agent_lab/infrastructure/openai_compatible.py
  27. 10 0
      src/agent_lab/main.py
  28. 2 0
      src/agent_lab/presentation/__init__.py
  29. 164 0
      src/agent_lab/presentation/static/app.js
  30. 82 0
      src/agent_lab/presentation/static/index.html
  31. 229 0
      src/agent_lab/presentation/static/styles.css
  32. 82 0
      src/agent_lab/presentation/web.py
  33. 15 0
      src/agent_lab/settings.py
  34. 125 0
      tests/test_debug_runtime.py
  35. 28 0
      tests/test_event_agent.py
  36. 84 0
      tests/test_openai_stream_parser.py
  37. 364 0
      tests/test_websocket_api.py
  38. 33 0
      tests/test_ws_smoke.py
  39. 721 0
      uv.lock

+ 5 - 0
.env.example

@@ -0,0 +1,5 @@
+AGENT_LAB_OPENAI_API_KEY=replace-with-your-api-key
+AGENT_LAB_OPENAI_BASE_URL=https://api.openai.com/v1
+AGENT_LAB_OPENAI_DEFAULT_MODEL=gpt-4.1-mini
+AGENT_LAB_OPENAI_INCLUDE_USAGE=true
+AGENT_LAB_REQUEST_TIMEOUT_SECONDS=60

+ 5 - 0
.gitignore

@@ -38,6 +38,7 @@ pip-delete-this-directory.txt
 # Unit test / coverage reports
 htmlcov/
 .tox/
+.pytest_cache/
 .coverage
 .coverage.*
 .cache
@@ -58,3 +59,7 @@ docs/_build/
 # PyBuilder
 target/
 
+# Local environment and agent workspace files
+.env
+.venv/
+.superpowers/

+ 37 - 0
AGENTS.md

@@ -0,0 +1,37 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+
+This repository is a Python Agent Lab for debugging chat and event agents. The source package lives under `src/agent_lab/`:
+
+- `domain/`: message and event models shared across the app.
+- `application/`: runtime orchestration, request/response contracts, and EventAgent tool handling.
+- `infrastructure/`: OpenAI Chat Completions compatible streaming client and parser integration.
+- `presentation/`: FastAPI routes, WebSocket endpoint, and static debug UI under `presentation/static/`.
+- `settings.py`: `pydantic-settings` configuration.
+
+Tests live in `tests/`, planning docs in `docs/plans/`, and the manual WebSocket smoke helper in `scripts/ws_smoke.py`.
+
+## Build, Test, and Development Commands
+
+- `uv sync`: install runtime and dev dependencies from `pyproject.toml` and `uv.lock`.
+- `uv run pytest`: run the full test suite.
+- `uv run uvicorn agent_lab.main:app --reload`: start the FastAPI debug console locally.
+- `uv run python scripts/ws_smoke.py --help`: inspect the WebSocket smoke-test CLI.
+- `uv run python -c "from agent_lab.main import app; print(app.title)"`: verify app import wiring.
+
+## Coding Style & Naming Conventions
+
+Use Python 3.11+ with type hints on public boundaries. Keep DDD layers separate: domain types should not depend on FastAPI or HTTP clients, application code should coordinate use cases, and infrastructure should own provider-specific I/O. Prefer small dataclasses or Pydantic models for exchanged data. Use snake_case for modules, functions, fields, and environment variables.
+
+## Testing Guidelines
+
+Use `pytest` and `pytest-asyncio`. Add focused tests beside the behavior being changed, using names like `test_openai_stream_parser.py` or `test_websocket_api.py`. Runtime and WebSocket tests should use mocked clients unless a task explicitly requires a live LLM smoke.
+
+## Configuration & Runtime Conventions
+
+Configuration comes from `Settings` in `src/agent_lab/settings.py` with the `AGENT_LAB_` prefix and optional local `.env`. Do not commit secrets. The app exposes `GET /health`, `GET /`, and `WebSocket /ws/debug`; keep WebSocket payloads aligned with `application/contracts.py`.
+
+## Commit & Pull Request Guidelines
+
+Keep commits scoped to one todo or behavior change. PRs should summarize user-visible behavior, list verification commands, mention config changes, and include screenshots only when UI layout changes.

+ 67 - 1
README.md

@@ -1,2 +1,68 @@
-# agent-lab
+# Agent Lab
 
+Agent Lab is a FastAPI debug console for chat agents and event agents. It uses
+an OpenAI Chat Completions compatible streaming endpoint, a WebSocket debug
+route, and a direct async runtime/event flow for local investigation.
+
+## Setup
+
+Install dependencies with uv:
+
+```bash
+uv sync
+```
+
+Create a local environment file:
+
+```bash
+cp .env.example .env
+```
+
+Set these values in `.env` before running real LLM calls:
+
+- `AGENT_LAB_OPENAI_API_KEY`: required API key for the compatible provider.
+- `AGENT_LAB_OPENAI_BASE_URL`: provider `/v1` root, for example `https://api.openai.com/v1`.
+- `AGENT_LAB_OPENAI_DEFAULT_MODEL`: backend default model when a request leaves model blank.
+- `AGENT_LAB_OPENAI_INCLUDE_USAGE`: set `false` if the compatible provider rejects `stream_options.include_usage`.
+- `AGENT_LAB_REQUEST_TIMEOUT_SECONDS`: HTTP timeout for streamed model calls.
+
+Do not commit real secrets. Keep `.env` local.
+
+## Run Locally
+
+Start the FastAPI app:
+
+```bash
+uv run uvicorn agent_lab.main:app --reload
+```
+
+Open the debug console:
+
+```text
+http://127.0.0.1:8000
+```
+
+The app exposes `GET /health`, `GET /`, and `WebSocket /ws/debug`.
+
+## WebSocket Smoke Test
+
+With the server running and `.env` configured, send one debug request:
+
+```bash
+uv run python scripts/ws_smoke.py \
+  --message "Debug this agent handoff path."
+```
+
+The script connects to `ws://127.0.0.1:8000/ws/debug`, sends a valid
+`DebugRunRequest`, and prints each server message as compact JSON. Use
+`--model` to override the backend default model, `--disable-handoff-note` to
+run without the default `handoff_note` tool, or `--url` to target a different
+server.
+
+## Tests
+
+Run the full test suite:
+
+```bash
+uv run pytest
+```

+ 69 - 0
docs/plans/todo-1-project-foundation.md

@@ -0,0 +1,69 @@
+# Todo 1 Project Foundation Plan
+
+**Status:** done
+
+## Result
+
+`uv run pytest` starts from `pyproject.toml`, loads tests from `tests/`, and fails on missing implementation modules:
+
+- `agent_lab.domain.events`
+- `agent_lab.application.contracts`
+- `agent_lab.application.event_agent`
+
+This is the accepted red-test baseline for the next todo.
+
+## Goal
+
+Establish the Python project skeleton and red-test baseline for the Agent Lab MVP.
+
+## Scope
+
+This todo covers only:
+
+- uv-managed Python package metadata.
+- Initial DDD-oriented package directories.
+- Tests that describe parser, EventAgent, and runtime-routing behavior.
+- A red test run that proves implementation is still missing.
+
+This todo does not implement the parser, agents, FastAPI app, WebSocket endpoint, or UI.
+
+## Files
+
+- Create or keep: `pyproject.toml`
+- Create or keep: `uv.lock`
+- Create: `src/agent_lab/__init__.py`
+- Create: `src/agent_lab/domain/__init__.py`
+- Create: `src/agent_lab/application/__init__.py`
+- Create: `src/agent_lab/infrastructure/__init__.py`
+- Create: `src/agent_lab/presentation/__init__.py`
+- Create or keep: `tests/test_openai_stream_parser.py`
+- Create or keep: `tests/test_event_agent.py`
+- Create or keep: `tests/test_debug_runtime.py`
+- Update: `docs/plans/todos.md`
+
+## Steps
+
+1. Confirm current uncommitted files with `git status --porcelain=v2 --branch`.
+2. Ensure `pyproject.toml` declares the package, Python version, FastAPI/httpx/pydantic-settings/uvicorn dependencies, and pytest config.
+3. Ensure the `src/agent_lab/` package directories exist with `__init__.py` files.
+4. Ensure tests cover these behaviors:
+   - OpenAI Chat Completions stream chunks produce message deltas, usage, and tool-call events.
+   - EventAgent converts an enabled event/tool into a `tool` reply message.
+   - Runtime routes ChatAgent events through EventAgent and resumes ChatAgent with the tool reply.
+5. Run `uv run pytest`.
+6. Accept Todo 1 only if the tests fail because implementation modules or methods are missing. If failures are syntax errors, bad test imports unrelated to missing implementation, or malformed pyproject config, fix Todo 1 before moving on.
+7. Update `docs/plans/todos.md`: set Todo 1 to `done` if accepted, otherwise keep it `in_progress` with a note.
+
+## Verification
+
+Run:
+
+```bash
+uv run pytest
+```
+
+Expected Todo 1 result:
+
+- pytest starts successfully from `pyproject.toml`.
+- tests are discovered.
+- failures point to missing implementation for `agent_lab` modules or classes.

+ 85 - 0
docs/plans/todo-2-domain-runtime.md

@@ -0,0 +1,85 @@
+# Todo 2 Domain Runtime Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified with:
+
+```bash
+uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_debug_runtime.py
+```
+
+Result: `3 passed in 0.07s`.
+
+Evaluation note: the runtime currently streams message deltas to the caller but does not yet persist the aggregated assistant message into conversation history. Todo 3 should cover transcript persistence when wiring the WebSocket/API surface.
+
+## Goal
+
+Implement the core domain/runtime loop that passes the existing Todo 1 tests without adding WebSocket, FastAPI routes, or UI.
+
+## Scope
+
+This todo covers:
+
+- Domain objects for chat messages, tool-call events, stream items, and usage.
+- Application contracts for debug run requests and agent parameters.
+- EventAgent execution for enabled tools.
+- OpenAI Chat Completions compatible stream parsing.
+- DebugRuntime loop that routes ChatAgent tool events through EventAgent and resumes ChatAgent with tool replies.
+
+This todo does not cover:
+
+- Real HTTP calls to an LLM endpoint.
+- pydantic-settings config.
+- WebSocket protocol.
+- Static frontend.
+
+## Files
+
+- Create: `src/agent_lab/domain/events.py`
+- Create: `src/agent_lab/domain/messages.py`
+- Create: `src/agent_lab/application/contracts.py`
+- Create: `src/agent_lab/application/event_agent.py`
+- Create: `src/agent_lab/application/runtime.py`
+- Create: `src/agent_lab/infrastructure/openai_compatible.py`
+- Do not modify: `docs/plans/todos.md`
+- Avoid modifying tests unless a test is internally inconsistent.
+
+## Design
+
+Use small Pydantic models for contracts that cross application boundaries and dataclasses for internal stream items. Keep tool handling intentionally simple:
+
+- `ToolCallEvent.name` is the ChatAgent tool name.
+- EventAgent treats `ToolCallEvent.name` as the tool identifier.
+- The MVP ships one built-in event tool, `handoff_note`, that returns a JSON tool reply.
+- Disabled tools return a JSON error tool reply instead of raising.
+
+`DebugRuntime.run()` is an async generator. It builds initial messages from system prompts, pre-messages, and the user message. It calls `chat_client.stream_chat()`, yields message deltas and events, sends events to EventAgent, appends the returned tool message, and calls ChatAgent again until no event is produced or `max_event_loops` is reached.
+
+## Steps
+
+1. Implement domain models in `events.py` and `messages.py`.
+2. Implement request/parameter contracts in `contracts.py`.
+3. Implement `EventAgent.handle()`.
+4. Implement `ChatCompletionStreamParser.feed()` for OpenAI-compatible streamed chunks:
+   - content deltas become `StreamItem.message_delta`.
+   - tool call chunks are accumulated by `index`.
+   - on `finish_reason == "tool_calls"`, emit `StreamItem.event` for each completed tool call.
+   - usage becomes `StreamItem.usage`.
+5. Implement `DebugRuntime.run()` around the chat client protocol.
+6. Run `uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_debug_runtime.py`.
+7. Fix only Todo 2 scope failures.
+
+## Verification
+
+Run:
+
+```bash
+uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_debug_runtime.py
+```
+
+Expected result:
+
+- 3 tests pass.
+- No FastAPI/WebSocket/UI code is required for this todo.

+ 121 - 0
docs/plans/todo-3-websocket-api.md

@@ -0,0 +1,121 @@
+# Todo 3 WebSocket API and Static UI Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified.
+
+Commands:
+
+```bash
+uv run pytest
+uv run python -c "from agent_lab.main import app; print(app.title)"
+```
+
+Results:
+
+- `uv run pytest`: `8 passed, 1 warning in 0.24s`
+- import smoke: `Agent Lab`
+
+Evaluation note: real LLM configuration already exists through `Settings`, so Todo 4 should focus on documentation and a minimal manual smoke helper rather than adding another runtime layer.
+
+## Goal
+
+Expose the domain runtime through FastAPI WebSocket and serve a usable static debug console.
+
+## Scope
+
+This todo covers:
+
+- FastAPI app factory and HTTP index route.
+- WebSocket endpoint for one debug run per request.
+- OpenAI Chat Completions compatible HTTP client for streaming completions.
+- Assistant message transcript persistence during runtime loops.
+- Static HTML/CSS/JS page without a frontend build chain.
+- Contract tests using FastAPI `TestClient` and a fake runtime/client where possible.
+
+This todo does not cover:
+
+- Production auth.
+- Multi-user session storage.
+- Redis/NATS/external queues.
+- Full visual polish.
+- Final README/env docs; those stay in Todo 4.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Create: `src/agent_lab/infrastructure/chat_client.py`
+- Create: `src/agent_lab/settings.py`
+- Create: `src/agent_lab/main.py`
+- Create: `src/agent_lab/presentation/web.py`
+- Create: `src/agent_lab/presentation/static/index.html`
+- Create: `src/agent_lab/presentation/static/app.js`
+- Create: `src/agent_lab/presentation/static/styles.css`
+- Create: `tests/test_websocket_api.py`
+- Update: `docs/plans/todos.md` after evaluation only.
+
+## Protocol
+
+Frontend sends one JSON object to `/ws/debug`:
+
+```json
+{
+  "user_message": "debug this",
+  "system_prompts": ["You are a debugger."],
+  "pre_messages": [],
+  "chat_agent": {
+    "model": "gpt-4.1-mini",
+    "temperature": 0.2,
+    "max_tokens": 800
+  },
+  "event_agent": {
+    "enabled_tools": ["handoff_note"],
+    "max_event_loops": 3
+  }
+}
+```
+
+Server sends newline-independent JSON WebSocket messages already produced by `DebugRuntime.run()`, plus an `error` message on validation/runtime failures.
+
+## Steps
+
+1. Add tests for `/health` and `/ws/debug` with a fake runtime or fake chat client.
+2. Update `DebugRuntime` to aggregate streamed assistant content and append assistant messages to history when a model turn completes.
+3. Implement `Settings` with pydantic-settings fields:
+   - `openai_api_key`
+   - `openai_base_url`
+   - `openai_default_model`
+   - `request_timeout_seconds`
+4. Implement `OpenAICompatibleChatClient` using `httpx.AsyncClient.stream()` against `/chat/completions`, with `stream: true`, configured model params, tool definitions, and `ChatCompletionStreamParser`.
+5. Implement FastAPI app factory and routes:
+   - `GET /health`
+   - `GET /`
+   - `WebSocket /ws/debug`
+6. Implement static page controls:
+   - multiple system prompts.
+   - pre-message editor.
+   - chat input and streamed output.
+   - ChatAgent/EventAgent parameter inputs.
+   - EventAgent tool toggles.
+   - round stats area for tokens, cached tokens, TTFT, and elapsed time.
+7. Run focused tests:
+
+```bash
+uv run pytest tests/test_websocket_api.py tests/test_debug_runtime.py
+```
+
+8. Run full tests:
+
+```bash
+uv run pytest
+```
+
+## Verification
+
+Expected result:
+
+- WebSocket contract test passes without a real LLM key.
+- Existing Todo 2 tests still pass.
+- App imports with `uv run python -c "from agent_lab.main import app; print(app.title)"`.

+ 91 - 0
docs/plans/todo-4-real-llm-smoke.md

@@ -0,0 +1,91 @@
+# Todo 4 Real LLM Smoke Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified.
+
+Commands:
+
+```bash
+uv run pytest
+uv run python scripts/ws_smoke.py --help
+```
+
+Results:
+
+- `uv run pytest`: `9 passed, 1 warning in 0.22s`
+- `ws_smoke.py --help`: command help prints successfully.
+
+Evaluation note: this todo made the local real-LLM run path documented and smokeable, but raised a provider-compatibility risk around tool-call history. Todo 5 now covers that before final cleanup.
+
+## Goal
+
+Document and lightly verify the real OpenAI Chat Completions compatible run path without requiring a live API key in automated tests.
+
+## Scope
+
+This todo covers:
+
+- README run instructions for uv, env vars, and local server startup.
+- `.env.example` with non-secret defaults/placeholders.
+- A small manual smoke script that connects to the local WebSocket endpoint and prints streamed messages.
+- Tests for smoke-script request construction if practical.
+
+This todo does not cover:
+
+- Running a real paid/network LLM call in CI or automated tests.
+- Adding secrets to the repo.
+- Adding another frontend framework.
+- Changing runtime behavior unless a small bug blocks the documented smoke path.
+
+## Files
+
+- Modify: `README.md`
+- Create: `.env.example`
+- Create: `scripts/ws_smoke.py`
+- Create or modify: tests only if needed for the smoke helper.
+- Do not modify: `docs/plans/todos.md`.
+
+## Environment Contract
+
+Use these variables:
+
+- `AGENT_LAB_OPENAI_API_KEY`
+- `AGENT_LAB_OPENAI_BASE_URL`
+- `AGENT_LAB_OPENAI_DEFAULT_MODEL`
+- `AGENT_LAB_REQUEST_TIMEOUT_SECONDS`
+
+The default base URL is already `https://api.openai.com/v1`. For compatible gateways, users set `AGENT_LAB_OPENAI_BASE_URL` to the gateway `/v1` root.
+
+## Steps
+
+1. Update `README.md` with:
+   - `uv sync`
+   - `cp .env.example .env`
+   - required env vars
+   - `uv run uvicorn agent_lab.main:app --reload`
+   - browser URL `http://127.0.0.1:8000`
+   - WebSocket smoke command.
+2. Add `.env.example` with placeholder key and clear compatible endpoint defaults.
+3. Add `scripts/ws_smoke.py`:
+   - connects to `ws://127.0.0.1:8000/ws/debug` by default.
+   - sends one valid debug request.
+   - prints each server message as compact JSON.
+   - accepts optional CLI args for URL, message, model, and tool enablement.
+4. If adding smoke helper logic beyond straightforward I/O, add a focused test.
+5. Run:
+
+```bash
+uv run pytest
+uv run python scripts/ws_smoke.py --help
+```
+
+## Verification
+
+Expected result:
+
+- Tests still pass.
+- Smoke helper help output works without starting the server.
+- README contains exact env names and local run commands.

+ 102 - 0
docs/plans/todo-5-tool-call-history.md

@@ -0,0 +1,102 @@
+# Todo 5 Tool-Call History Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified.
+
+Commands:
+
+```bash
+uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+Results:
+
+- focused runtime/API tests: `7 passed, 1 warning in 0.23s`
+- full suite: `10 passed, 1 warning in 0.24s`
+
+Evaluation note: the remaining work is cleanup and closeout. The warning is from FastAPI/Starlette TestClient importing the currently installed httpx version and does not block the MVP.
+
+## Goal
+
+Make multi-turn tool-call history compatible with strict OpenAI Chat Completions compatible providers.
+
+## Problem
+
+After the model emits a tool call, the next Chat Completions request should include:
+
+1. the assistant message that contains `tool_calls`;
+2. the corresponding `tool` reply message.
+
+Current runtime appends assistant content and tool replies, but it does not preserve assistant `tool_calls`. Some providers reject this because a `tool` message lacks a preceding assistant tool call.
+
+## Scope
+
+This todo covers:
+
+- Representing assistant `tool_calls` in `ChatMessage`.
+- Preserving raw tool call id/name/arguments from `ToolCallEvent`.
+- Updating `DebugRuntime` to append assistant `tool_calls` before tool replies.
+- Updating OpenAI payload serialization to include `tool_calls`.
+- Tests proving strict provider-compatible history order.
+
+This todo does not cover:
+
+- New tools.
+- UI changes.
+- External queues.
+- Real network smoke.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/infrastructure/chat_client.py` only if serialization needs adjustment.
+- Modify or add tests, preferably in `tests/test_debug_runtime.py` and/or `tests/test_websocket_api.py`.
+- Do not modify: `docs/plans/todos.md`.
+
+## Design
+
+Add optional `tool_calls` to `ChatMessage` as a list of OpenAI-compatible tool call dicts. Keep `content` as a string for simplicity; use the aggregated assistant text or `""` when a model emits only tool calls.
+
+For each `ToolCallEvent`, create this message shape:
+
+```python
+{
+    "id": event.id,
+    "type": "function",
+    "function": {
+        "name": event.name,
+        "arguments": event.raw_arguments,
+    },
+}
+```
+
+At the end of each model turn, append one assistant message with aggregated content and all tool calls from that turn, then append the generated tool replies.
+
+## Steps
+
+1. Write a failing test proving that the second ChatAgent call receives messages in this order:
+   - system
+   - user
+   - assistant with `tool_calls`
+   - tool with matching `tool_call_id`
+2. Add `tool_calls` to `ChatMessage`.
+3. Update `DebugRuntime.run()` to collect tool calls per model turn and append them to the assistant message before tool replies.
+4. Confirm `OpenAICompatibleChatClient` serializes `tool_calls` through `model_dump(exclude_none=True)`; adjust only if needed.
+5. Run:
+
+```bash
+uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+## Verification
+
+Expected result:
+
+- Tests prove assistant `tool_calls` precede tool replies.
+- Full suite passes.

+ 75 - 0
docs/plans/todo-6-cleanup-closeout.md

@@ -0,0 +1,75 @@
+# Todo 6 Cleanup and Closeout Plan
+
+**Status:** done
+
+## Goal
+
+Clean local generated files from the deliverable surface, align contributor docs with the implemented project, and run final verification.
+
+## Scope
+
+This todo covers:
+
+- `.gitignore` updates for local-only files.
+- Removing or ignoring temporary `.superpowers/` browser-companion files from the deliverable set.
+- Updating `AGENTS.md` from the old empty-repo guide to the actual FastAPI/uv project.
+- Final test/import/help verification.
+- Final `docs/plans/todos.md` status update.
+
+This todo does not cover:
+
+- New runtime behavior.
+- New UI features.
+- Live LLM network calls.
+- Creating commits unless explicitly requested.
+
+## Files
+
+- Modify: `.gitignore`
+- Modify or create: `AGENTS.md`
+- Modify: `docs/plans/todos.md`
+- Modify: `docs/plans/todo-6-cleanup-closeout.md`
+- Do not modify production code unless final verification exposes a bug.
+
+## Steps
+
+1. Inspect `.gitignore` and current git status.
+2. Add local-only ignores:
+   - `.env`
+   - `.venv/`
+   - `.pytest_cache/`
+   - `.superpowers/`
+   - `__pycache__/`
+3. Update `AGENTS.md` to describe the actual repo:
+   - `src/agent_lab/` package layout.
+   - `tests/`.
+   - `scripts/ws_smoke.py`.
+   - `docs/plans/`.
+   - `uv` commands.
+   - FastAPI/WebSocket and pydantic-settings conventions.
+4. Run verification:
+
+```bash
+uv run pytest
+uv run python -c "from agent_lab.main import app; print(app.title)"
+uv run python scripts/ws_smoke.py --help
+git status --porcelain=v2 --branch
+```
+
+5. Update `docs/plans/todos.md` and this file with results.
+
+## Verification
+
+Completed result:
+
+- `.gitignore` now ignores `.env`, `.venv/`, `.pytest_cache/`, and `.superpowers/`; `__pycache__/` was already covered.
+- `AGENTS.md` was created for the implemented FastAPI, WebSocket, uv, DDD, and pydantic-settings project shape.
+- `uv run pytest` passed: `10 passed, 1 warning`.
+- `uv run python -c "from agent_lab.main import app; print(app.title)"` passed and printed `Agent Lab`.
+- `uv run python scripts/ws_smoke.py --help` passed and showed the expected CLI options.
+- `git status --porcelain=v2 --branch` no longer lists `.superpowers/`, `.venv/`, `.pytest_cache/`, or `.env`.
+
+Notes:
+
+- The remaining warning is the existing FastAPI/Starlette `TestClient` deprecation warning for the installed httpx integration.
+- The two `uv run python ...` verification commands initially hit sandbox permission limits while reading `~/.cache/uv`; they passed after rerunning with approval.

+ 93 - 0
docs/plans/todo-7-provider-compatibility.md

@@ -0,0 +1,93 @@
+# Todo 7 Provider Compatibility Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent, then documentation was adjusted in `README.md` so the smoke example uses backend model fallback by default and documents `AGENT_LAB_OPENAI_INCLUDE_USAGE`.
+
+Commands:
+
+```bash
+uv run pytest tests/test_websocket_api.py tests/test_ws_smoke.py
+uv run pytest
+uv run python -c "from agent_lab.main import app; print(app.title)"
+uv run python scripts/ws_smoke.py --help
+```
+
+Results:
+
+- focused provider/API tests: `10 passed, 1 warning in 0.23s`
+- full suite: `14 passed, 1 warning in 0.24s`
+- import smoke: `Agent Lab`
+- smoke helper help: command prints expected options.
+
+## Goal
+
+Fix code-review findings that can break real OpenAI Chat Completions compatible providers.
+
+## Scope
+
+This todo covers:
+
+- Letting backend settings provide the default model when UI/smoke requests do not specify one.
+- Role-aware provider message serialization so `tool` messages do not send unsupported `name` fields.
+- Configurable `stream_options.include_usage` for providers that reject stream options.
+- Tests that lock provider payload shape after tool calls.
+
+This todo does not cover:
+
+- Live network LLM smoke.
+- New UI controls beyond model fallback behavior.
+- Changing the EventAgent tool set.
+
+## Files
+
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `src/agent_lab/infrastructure/chat_client.py`
+- Modify: `src/agent_lab/settings.py`
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `scripts/ws_smoke.py`
+- Modify: `.env.example`
+- Modify tests as needed.
+- Do not modify: `docs/plans/todos.md`.
+
+## Design
+
+`AgentParams.model` should be optional. If omitted or blank, `OpenAICompatibleChatClient` uses `Settings.openai_default_model`.
+
+Provider message serialization should be role-aware:
+
+- `system`, `user`, `assistant`: include `role`, `content`, and assistant `tool_calls` when present.
+- `tool`: include only `role`, `content`, and `tool_call_id`.
+
+Add `Settings.openai_include_usage: bool = True`. When true, send `stream_options: {"include_usage": True}`; when false, omit `stream_options`.
+
+The static UI and smoke helper should send an empty model by default, allowing backend fallback. Users may still provide a model explicitly.
+
+## Steps
+
+1. Add failing tests for:
+   - blank request model uses client default model.
+   - tool message serialization excludes `name` while preserving `tool_call_id`.
+   - `stream_options` is omitted when configured off.
+2. Implement optional/blank model support in contracts and client payload.
+3. Add role-aware message serialization in the client.
+4. Add `AGENT_LAB_OPENAI_INCLUDE_USAGE` setting and `.env.example` entry.
+5. Remove hardcoded default model from the static UI and smoke helper request payload defaults.
+6. Run:
+
+```bash
+uv run pytest tests/test_websocket_api.py tests/test_ws_smoke.py
+uv run pytest
+uv run python scripts/ws_smoke.py --help
+```
+
+## Verification
+
+Expected result:
+
+- Provider payload tests pass.
+- Full suite passes.
+- Smoke helper help still works.

+ 70 - 0
docs/plans/todo-8-tool-pre-message-guard.md

@@ -0,0 +1,70 @@
+# Todo 8 Tool Pre-Message Guard Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified by the main session.
+
+Commands:
+
+```bash
+uv run pytest tests/test_websocket_api.py
+uv run pytest
+```
+
+Results:
+
+- focused WebSocket/API tests: `10 passed, 1 warning in 0.22s`
+- full suite: `16 passed, 1 warning in 0.22s`
+
+## Goal
+
+Prevent the debug UI and provider serializer from generating invalid Chat Completions `tool` messages without `tool_call_id`.
+
+## Problem
+
+The pre-message editor exposes `tool` as a role, but the UI does not collect `tool_call_id`. A manual `tool` pre-message can therefore reach the provider as `{role: "tool", content: "..."}`, which strict compatible providers reject.
+
+## Scope
+
+This todo covers:
+
+- Removing `tool` from the static pre-message role selector for MVP.
+- Adding provider-client validation that raises a clear error if a `tool` message lacks `tool_call_id`.
+- Tests for the validation and UI markup.
+
+This todo does not cover:
+
+- Adding full manual tool-call transcript editing.
+- New EventAgent tools.
+- Live LLM smoke.
+
+## Files
+
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/infrastructure/chat_client.py`
+- Modify/add tests, preferably `tests/test_websocket_api.py`.
+- Do not modify: `docs/plans/todos.md`.
+
+## Steps
+
+1. Add a failing test that `OpenAICompatibleChatClient` rejects a `ChatMessage(role="tool", content="...")` without `tool_call_id`.
+2. Add a failing test or assertion that the static pre-message role selector does not offer `tool`.
+3. Implement the minimal fix:
+   - remove `<option value="tool">tool</option>` from the pre-message template.
+   - in `_serialize_message`, raise `ValueError("tool messages require tool_call_id")` when role is `tool` and no `tool_call_id` is present.
+4. Run:
+
+```bash
+uv run pytest tests/test_websocket_api.py
+uv run pytest
+```
+
+## Verification
+
+Expected result:
+
+- Invalid manual tool messages fail before network I/O.
+- UI no longer exposes invalid manual tool pre-message creation.
+- Full suite passes.

+ 29 - 0
docs/plans/todos.md

@@ -0,0 +1,29 @@
+# Agent Lab MVP Todos
+
+## Workflow
+
+- Main session owns this file and evaluates progress after each todo.
+- Work on one todo at a time: write its plan, execute it, verify it, then update this file.
+- Do not plan all implementation details upfront. Later todos may change after each evaluation.
+- Detailed plans live beside this file as `docs/plans/todo-N-*.md`.
+- Status values: `pending`, `in_progress`, `done`, `blocked`.
+
+## Current State
+
+- Branch: `codex/agent-lab-mvp`.
+- Repo started with only `README.md`, `LICENSE`, and `.gitignore`.
+- Existing uncommitted setup from earlier false start: `pyproject.toml`, `uv.lock`, and tests under `tests/`.
+- Temporary browser companion files exist under `.superpowers/` and should not become product code.
+
+## Todos
+
+| ID | Status | Plan | Goal | Verification |
+| --- | --- | --- | --- | --- |
+| 1 | done | `docs/plans/todo-1-project-foundation.md` | Establish uv/FastAPI project skeleton, DDD package layout, and core failing tests for parser, event agent, and runtime routing. | `uv run pytest` fails only because implementation is missing, not because tests or imports are malformed. |
+| 2 | done | `docs/plans/todo-2-domain-runtime.md` | Implement domain models, OpenAI Chat Completions compatible stream parsing, EventAgent tools, and the direct async runtime loop. | Core tests pass with mocked ChatAgent client. |
+| 3 | done | `docs/plans/todo-3-websocket-api.md` | Add FastAPI config, HTTP routes, WebSocket protocol, static debug page, and assistant transcript persistence for multi-turn LLM calls. | WebSocket contract tests pass; app imports successfully. |
+| 4 | done | `docs/plans/todo-4-real-llm-smoke.md` | Document env-based real LLM run flow, add a lightweight manual smoke helper, and make sure settings names are clear. | App starts with env-based config; docs show exact `uv` commands and required env vars. |
+| 5 | done | `docs/plans/todo-5-tool-call-history.md` | Harden Chat Completions history so assistant tool calls and tool replies are sent in provider-compatible order. | Tests prove the next model call includes assistant `tool_calls` before `tool` replies. |
+| 6 | done | `docs/plans/todo-6-cleanup-closeout.md` | Clean temporary/generated files, update contributor docs, and run final verification. | Git status contains only intended source/docs files; `uv run pytest` passes. |
+| 7 | done | `docs/plans/todo-7-provider-compatibility.md` | Fix review findings for real Chat Completions compatible providers: default model fallback, role-aware message serialization, and configurable usage streaming. | Tests prove provider payload shape and full suite passes. |
+| 8 | done | `docs/plans/todo-8-tool-pre-message-guard.md` | Prevent manually configured pre-messages from producing invalid provider `tool` payloads without `tool_call_id`. | Tests prove invalid tool pre-messages are rejected and UI no longer exposes `tool` role. |

+ 34 - 0
pyproject.toml

@@ -0,0 +1,34 @@
+[project]
+name = "agent-lab"
+version = "0.1.0"
+description = "A FastAPI debug console for chat and event agents."
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+    "fastapi>=0.111.0",
+    "httpx>=0.27.0",
+    "pydantic-settings>=2.3.0",
+    "uvicorn[standard]>=0.30.0",
+    "websockets>=12.0",
+]
+
+[dependency-groups]
+dev = [
+    "pytest>=8.2.0",
+    "pytest-asyncio>=0.23.0",
+]
+
+[project.scripts]
+agent-lab = "agent_lab.main:run"
+
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+where = ["src"]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+pythonpath = ["src"]
+testpaths = ["tests"]

+ 98 - 0
scripts/ws_smoke.py

@@ -0,0 +1,98 @@
+import argparse
+import asyncio
+import json
+from typing import Any
+
+
+DEFAULT_URL = "ws://127.0.0.1:8000/ws/debug"
+DEFAULT_MESSAGE = "Debug this agent handoff path."
+DEFAULT_MODEL = ""
+
+
+def build_payload(
+    message: str,
+    model: str,
+    handoff_note_enabled: bool,
+) -> dict[str, Any]:
+    return {
+        "user_message": message,
+        "system_prompts": [
+            "You are a chat agent running inside Agent Lab. "
+            "Stream a concise answer and call handoff_note when an event handoff helps."
+        ],
+        "pre_messages": [],
+        "chat_agent": {
+            "model": model,
+            "temperature": 0.2,
+            "max_tokens": 1024,
+        },
+        "event_agent": {
+            "enabled_tools": ["handoff_note"] if handoff_note_enabled else [],
+            "max_event_loops": 3,
+        },
+    }
+
+
+def build_parser() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description="Send one Agent Lab WebSocket debug request and print streamed JSON."
+    )
+    parser.add_argument("--url", default=DEFAULT_URL, help=f"WebSocket URL. Default: {DEFAULT_URL}")
+    parser.add_argument("--message", default=DEFAULT_MESSAGE, help="User message to send.")
+    parser.add_argument(
+        "--model",
+        default=DEFAULT_MODEL,
+        help="Chat model name. Defaults to the backend configured model.",
+    )
+
+    tool_group = parser.add_mutually_exclusive_group()
+    tool_group.add_argument(
+        "--enable-handoff-note",
+        dest="handoff_note_enabled",
+        action="store_true",
+        default=True,
+        help="Enable the handoff_note tool. This is the default.",
+    )
+    tool_group.add_argument(
+        "--disable-handoff-note",
+        dest="handoff_note_enabled",
+        action="store_false",
+        help="Disable the handoff_note tool.",
+    )
+    return parser
+
+
+async def run_smoke(url: str, payload: dict[str, Any]) -> None:
+    import websockets
+
+    async with websockets.connect(url) as websocket:
+        await websocket.send(_compact_json(payload))
+        async for raw_message in websocket:
+            try:
+                message = json.loads(raw_message)
+            except json.JSONDecodeError:
+                print(raw_message, flush=True)
+                continue
+
+            print(_compact_json(message), flush=True)
+            if message.get("type") in {"done", "error"}:
+                break
+
+
+def _compact_json(value: Any) -> str:
+    return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
+
+
+def main() -> int:
+    args = build_parser().parse_args()
+    payload = build_payload(
+        message=args.message,
+        model=args.model,
+        handoff_note_enabled=args.handoff_note_enabled,
+    )
+    asyncio.run(run_smoke(args.url, payload))
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 2 - 0
src/agent_lab/__init__.py

@@ -0,0 +1,2 @@
+"""Agent Lab package."""
+

+ 2 - 0
src/agent_lab/application/__init__.py

@@ -0,0 +1,2 @@
+"""Application service package."""
+

+ 28 - 0
src/agent_lab/application/contracts.py

@@ -0,0 +1,28 @@
+from pydantic import BaseModel, ConfigDict, Field
+
+from agent_lab.domain.messages import ChatMessage
+
+
+class AgentParams(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+    model: str | None = None
+    temperature: float = 0.2
+    max_tokens: int = 1024
+
+
+class EventAgentParams(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+    enabled_tools: list[str] = Field(default_factory=lambda: ["handoff_note"])
+    max_event_loops: int = 3
+
+
+class DebugRunRequest(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+    user_message: str
+    system_prompts: list[str] = Field(default_factory=list)
+    pre_messages: list[ChatMessage] = Field(default_factory=list)
+    chat_agent: AgentParams
+    event_agent: EventAgentParams = Field(default_factory=EventAgentParams)

+ 45 - 0
src/agent_lab/application/event_agent.py

@@ -0,0 +1,45 @@
+import json
+from collections.abc import Iterable
+
+from agent_lab.domain.events import ToolCallEvent
+from agent_lab.domain.messages import ChatMessage
+
+
+class EventAgent:
+    def __init__(self, enabled_tools: Iterable[str]) -> None:
+        self.enabled_tools = set(enabled_tools)
+
+    async def handle(self, event: ToolCallEvent) -> ChatMessage:
+        if event.name not in self.enabled_tools:
+            return self._tool_reply(
+                event,
+                {
+                    "tool": event.name,
+                    "error": "tool disabled",
+                },
+            )
+
+        if event.name == "handoff_note":
+            return self._tool_reply(
+                event,
+                {
+                    "tool": "handoff_note",
+                    "message": str(event.arguments.get("message", "")),
+                },
+            )
+
+        return self._tool_reply(
+            event,
+            {
+                "tool": event.name,
+                "error": "unknown tool",
+            },
+        )
+
+    def _tool_reply(self, event: ToolCallEvent, payload: dict[str, str]) -> ChatMessage:
+        return ChatMessage(
+            role="tool",
+            content=json.dumps(payload, ensure_ascii=False),
+            name=event.name,
+            tool_call_id=event.id,
+        )

+ 115 - 0
src/agent_lab/application/runtime.py

@@ -0,0 +1,115 @@
+from collections.abc import AsyncIterator
+from typing import Any, Protocol
+
+from agent_lab.application.contracts import AgentParams, DebugRunRequest
+from agent_lab.application.event_agent import EventAgent
+from agent_lab.domain.messages import ChatMessage, StreamItem
+
+
+class ChatClient(Protocol):
+    async def stream_chat(
+        self,
+        messages: list[ChatMessage],
+        tools: list[dict[str, Any]],
+        params: AgentParams,
+    ) -> AsyncIterator[StreamItem]:
+        ...
+
+
+class DebugRuntime:
+    def __init__(self, chat_client: ChatClient) -> None:
+        self.chat_client = chat_client
+
+    async def run(self, request: DebugRunRequest) -> AsyncIterator[dict[str, Any]]:
+        messages = self._build_initial_messages(request)
+        tools = self._build_tools(request.event_agent.enabled_tools)
+        event_agent = EventAgent(request.event_agent.enabled_tools)
+
+        yield {"type": "session_started"}
+
+        event_loops = 0
+        while True:
+            saw_event = False
+            assistant_content: list[str] = []
+            assistant_tool_calls: list[dict[str, Any]] = []
+            pending_tool_replies: list[ChatMessage] = []
+
+            async for item in self.chat_client.stream_chat(
+                messages=messages,
+                tools=tools,
+                params=request.chat_agent,
+            ):
+                if item.kind == "message_delta":
+                    assistant_content.append(item.content or "")
+                    yield {"type": "message_delta", "content": item.content}
+                    continue
+
+                if item.kind == "usage" and item.usage is not None:
+                    yield {"type": "usage", "usage": item.usage.model_dump()}
+                    continue
+
+                if item.kind == "event" and item.event is not None:
+                    saw_event = True
+                    assistant_tool_calls.append(
+                        {
+                            "id": item.event.id,
+                            "type": "function",
+                            "function": {
+                                "name": item.event.name,
+                                "arguments": item.event.raw_arguments,
+                            },
+                        }
+                    )
+                    yield {"type": "event", "event": item.event.model_dump()}
+                    reply = await event_agent.handle(item.event)
+                    pending_tool_replies.append(reply)
+                    yield {"type": "tool_result", "message": reply.model_dump()}
+
+            if assistant_content or assistant_tool_calls:
+                messages.append(
+                    ChatMessage(
+                        role="assistant",
+                        content="".join(assistant_content),
+                        tool_calls=assistant_tool_calls or None,
+                    )
+                )
+            messages.extend(pending_tool_replies)
+
+            if not saw_event:
+                break
+
+            event_loops += 1
+            if event_loops >= request.event_agent.max_event_loops:
+                break
+
+        yield {"type": "done"}
+
+    def _build_initial_messages(self, request: DebugRunRequest) -> list[ChatMessage]:
+        messages = [
+            ChatMessage(role="system", content=prompt)
+            for prompt in request.system_prompts
+        ]
+        messages.extend(request.pre_messages)
+        messages.append(ChatMessage(role="user", content=request.user_message))
+        return messages
+
+    def _build_tools(self, enabled_tools: list[str]) -> list[dict[str, Any]]:
+        tools: list[dict[str, Any]] = []
+        if "handoff_note" in enabled_tools:
+            tools.append(
+                {
+                    "type": "function",
+                    "function": {
+                        "name": "handoff_note",
+                        "description": "Send a note to the event agent.",
+                        "parameters": {
+                            "type": "object",
+                            "properties": {
+                                "message": {"type": "string"},
+                            },
+                            "required": ["message"],
+                        },
+                    },
+                }
+            )
+        return tools

+ 2 - 0
src/agent_lab/domain/__init__.py

@@ -0,0 +1,2 @@
+"""Domain model package."""
+

+ 12 - 0
src/agent_lab/domain/events.py

@@ -0,0 +1,12 @@
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict
+
+
+class ToolCallEvent(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+    id: str
+    name: str
+    arguments: dict[str, Any]
+    raw_arguments: str

+ 48 - 0
src/agent_lab/domain/messages.py

@@ -0,0 +1,48 @@
+from dataclasses import dataclass
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict
+
+from agent_lab.domain.events import ToolCallEvent
+
+
+class ChatMessage(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+    role: str
+    content: str
+    name: str | None = None
+    tool_call_id: str | None = None
+    tool_calls: list[dict[str, Any]] | None = None
+
+
+class TokenUsage(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+    prompt_tokens: int = 0
+    completion_tokens: int = 0
+    total_tokens: int = 0
+    cached_tokens: int = 0
+
+
+@dataclass
+class StreamItem:
+    kind: str
+    content: str | None = None
+    event: ToolCallEvent | None = None
+    usage: TokenUsage | None = None
+
+    @classmethod
+    def message_delta(cls, content: str) -> "StreamItem":
+        return cls(kind="message_delta", content=content)
+
+    @classmethod
+    def usage_item(cls, usage: TokenUsage) -> "StreamItem":
+        return cls(kind="usage", usage=usage)
+
+
+def _stream_event(cls: type[StreamItem], event: ToolCallEvent) -> StreamItem:
+    return cls(kind="event", event=event)
+
+
+StreamItem.event = classmethod(_stream_event)  # type: ignore[method-assign]

+ 2 - 0
src/agent_lab/infrastructure/__init__.py

@@ -0,0 +1,2 @@
+"""Infrastructure adapter package."""
+

+ 105 - 0
src/agent_lab/infrastructure/chat_client.py

@@ -0,0 +1,105 @@
+import json
+from collections.abc import AsyncIterator
+from typing import Any
+
+import httpx
+
+from agent_lab.application.contracts import AgentParams
+from agent_lab.domain.messages import ChatMessage, StreamItem
+from agent_lab.infrastructure.openai_compatible import ChatCompletionStreamParser
+
+
+class OpenAICompatibleChatClient:
+    def __init__(
+        self,
+        api_key: str,
+        base_url: str,
+        default_model: str,
+        request_timeout_seconds: float,
+        include_usage: bool = True,
+        http_client: httpx.AsyncClient | None = None,
+    ) -> None:
+        self.api_key = api_key
+        self.default_model = default_model
+        self.include_usage = include_usage
+        self._owns_client = http_client is None
+        self._http_client = http_client or httpx.AsyncClient(
+            base_url=base_url.rstrip("/"),
+            timeout=request_timeout_seconds,
+        )
+
+    async def stream_chat(
+        self,
+        messages: list[ChatMessage],
+        tools: list[dict[str, Any]],
+        params: AgentParams,
+    ) -> AsyncIterator[StreamItem]:
+        payload: dict[str, Any] = {
+            "model": self._resolve_model(params.model),
+            "messages": [self._serialize_message(message) for message in messages],
+            "stream": True,
+            "temperature": params.temperature,
+            "max_tokens": params.max_tokens,
+        }
+        if self.include_usage:
+            payload["stream_options"] = {"include_usage": True}
+        if tools:
+            payload["tools"] = tools
+
+        headers = {}
+        if self.api_key:
+            headers["Authorization"] = f"Bearer {self.api_key}"
+
+        parser = ChatCompletionStreamParser()
+        async with self._http_client.stream(
+            "POST",
+            "/chat/completions",
+            json=payload,
+            headers=headers,
+        ) as response:
+            response.raise_for_status()
+            async for line in response.aiter_lines():
+                data = self._read_sse_data(line)
+                if data is None:
+                    continue
+                if data == "[DONE]":
+                    break
+
+                chunk = json.loads(data)
+                for item in parser.feed(chunk):
+                    yield item
+
+    async def aclose(self) -> None:
+        if self._owns_client:
+            await self._http_client.aclose()
+
+    def _read_sse_data(self, line: str) -> str | None:
+        stripped = line.strip()
+        if not stripped or stripped.startswith(":"):
+            return None
+        if not stripped.startswith("data:"):
+            return None
+        return stripped.removeprefix("data:").strip()
+
+    def _resolve_model(self, model: str | None) -> str:
+        requested_model = model.strip() if model else ""
+        return requested_model or self.default_model
+
+    def _serialize_message(self, message: ChatMessage) -> dict[str, Any]:
+        if message.role == "tool":
+            if not message.tool_call_id:
+                raise ValueError("tool messages require tool_call_id")
+            payload = {
+                "role": message.role,
+                "content": message.content,
+                "tool_call_id": message.tool_call_id,
+            }
+            return payload
+
+        payload = {
+            "role": message.role,
+            "content": message.content,
+        }
+        if message.role == "assistant" and message.tool_calls:
+            payload["tool_calls"] = message.tool_calls
+        return payload

+ 93 - 0
src/agent_lab/infrastructure/openai_compatible.py

@@ -0,0 +1,93 @@
+import json
+from typing import Any
+
+from agent_lab.domain.events import ToolCallEvent
+from agent_lab.domain.messages import StreamItem, TokenUsage
+
+
+class ChatCompletionStreamParser:
+    def __init__(self) -> None:
+        self._tool_calls: dict[int, dict[str, Any]] = {}
+
+    def feed(self, chunk: dict[str, Any]) -> list[StreamItem]:
+        items: list[StreamItem] = []
+
+        for choice in chunk.get("choices", []):
+            delta = choice.get("delta") or {}
+
+            if "content" in delta and delta["content"] is not None:
+                items.append(StreamItem.message_delta(delta["content"]))
+
+            for tool_call in delta.get("tool_calls") or []:
+                self._accumulate_tool_call(tool_call)
+
+            if choice.get("finish_reason") == "tool_calls":
+                items.extend(self._drain_tool_events())
+
+        usage = chunk.get("usage")
+        if usage is not None:
+            prompt_details = usage.get("prompt_tokens_details") or {}
+            items.append(
+                StreamItem.usage_item(
+                    TokenUsage(
+                        prompt_tokens=usage.get("prompt_tokens", 0),
+                        completion_tokens=usage.get("completion_tokens", 0),
+                        total_tokens=usage.get("total_tokens", 0),
+                        cached_tokens=prompt_details.get("cached_tokens", 0),
+                    )
+                )
+            )
+
+        return items
+
+    def _accumulate_tool_call(self, tool_call: dict[str, Any]) -> None:
+        index = int(tool_call["index"])
+        state = self._tool_calls.setdefault(
+            index,
+            {
+                "id": "",
+                "name": "",
+                "arguments": "",
+            },
+        )
+
+        if tool_call.get("id"):
+            state["id"] = tool_call["id"]
+
+        function = tool_call.get("function") or {}
+        if function.get("name"):
+            state["name"] = function["name"]
+        if "arguments" in function and function["arguments"] is not None:
+            state["arguments"] += function["arguments"]
+
+    def _drain_tool_events(self) -> list[StreamItem]:
+        events: list[StreamItem] = []
+        for index in sorted(self._tool_calls):
+            state = self._tool_calls[index]
+            raw_arguments = state["arguments"]
+            events.append(
+                StreamItem.event(
+                    ToolCallEvent(
+                        id=state["id"],
+                        name=state["name"],
+                        arguments=self._parse_arguments(raw_arguments),
+                        raw_arguments=raw_arguments,
+                    )
+                )
+            )
+
+        self._tool_calls.clear()
+        return events
+
+    def _parse_arguments(self, raw_arguments: str) -> dict[str, Any]:
+        if not raw_arguments:
+            return {}
+
+        try:
+            parsed = json.loads(raw_arguments)
+        except json.JSONDecodeError:
+            return {"raw": raw_arguments}
+
+        if isinstance(parsed, dict):
+            return parsed
+        return {"value": parsed}

+ 10 - 0
src/agent_lab/main.py

@@ -0,0 +1,10 @@
+from agent_lab.presentation.web import create_app
+
+
+app = create_app()
+
+
+def run() -> None:
+    import uvicorn
+
+    uvicorn.run("agent_lab.main:app", host="127.0.0.1", port=8000, reload=True)

+ 2 - 0
src/agent_lab/presentation/__init__.py

@@ -0,0 +1,2 @@
+"""Presentation adapter package."""
+

+ 164 - 0
src/agent_lab/presentation/static/app.js

@@ -0,0 +1,164 @@
+const messagesEl = document.querySelector("#messages");
+const statusEl = document.querySelector("#connection-status");
+const chatForm = document.querySelector("#chat-form");
+const userMessage = document.querySelector("#user-message");
+const systemPrompts = document.querySelector("#system-prompts");
+const preMessages = document.querySelector("#pre-messages");
+const preMessageTemplate = document.querySelector("#pre-message-template");
+
+let socket = null;
+let activeAssistant = null;
+let runStartedAt = 0;
+let firstTokenAt = 0;
+
+document.querySelector("#add-system-prompt").addEventListener("click", () => {
+  const textarea = document.createElement("textarea");
+  textarea.className = "system-prompt";
+  textarea.rows = 4;
+  systemPrompts.append(textarea);
+});
+
+document.querySelector("#add-pre-message").addEventListener("click", () => {
+  preMessages.append(preMessageTemplate.content.cloneNode(true));
+});
+
+chatForm.addEventListener("submit", (event) => {
+  event.preventDefault();
+  runDebugSession();
+});
+
+function runDebugSession() {
+  closeSocket();
+  resetRun();
+
+  socket = new WebSocket(wsUrl());
+  socket.addEventListener("open", () => {
+    statusEl.textContent = "Streaming";
+    runStartedAt = performance.now();
+    socket.send(JSON.stringify(buildRequest()));
+  });
+  socket.addEventListener("message", (event) => {
+    handleServerMessage(JSON.parse(event.data));
+  });
+  socket.addEventListener("close", () => {
+    statusEl.textContent = "Idle";
+    updateElapsed();
+  });
+  socket.addEventListener("error", () => {
+    appendLog("error", "WebSocket connection failed");
+  });
+}
+
+function buildRequest() {
+  return {
+    user_message: userMessage.value,
+    system_prompts: [...document.querySelectorAll(".system-prompt")]
+      .map((input) => input.value.trim())
+      .filter(Boolean),
+    pre_messages: [...document.querySelectorAll(".pre-message")]
+      .map((row) => ({
+        role: row.querySelector(".pre-role").value,
+        content: row.querySelector(".pre-content").value.trim(),
+      }))
+      .filter((message) => message.content),
+    chat_agent: {
+      model: document.querySelector("#model").value.trim(),
+      temperature: Number(document.querySelector("#temperature").value),
+      max_tokens: Number(document.querySelector("#max-tokens").value),
+    },
+    event_agent: {
+      enabled_tools: document.querySelector("#tool-handoff-note").checked
+        ? ["handoff_note"]
+        : [],
+      max_event_loops: Number(document.querySelector("#max-event-loops").value),
+    },
+  };
+}
+
+function handleServerMessage(message) {
+  if (message.type === "session_started") {
+    appendLog("session", "Session started");
+    return;
+  }
+  if (message.type === "message_delta") {
+    appendAssistantDelta(message.content || "");
+    return;
+  }
+  if (message.type === "usage") {
+    updateUsage(message.usage || {});
+    return;
+  }
+  if (message.type === "event") {
+    appendLog("event", `${message.event.name}: ${JSON.stringify(message.event.arguments)}`);
+    return;
+  }
+  if (message.type === "tool_result") {
+    appendLog("tool", message.message.content);
+    return;
+  }
+  if (message.type === "error") {
+    appendLog("error", message.message);
+    return;
+  }
+  if (message.type === "done") {
+    appendLog("session", "Done");
+    closeSocket();
+  }
+}
+
+function appendAssistantDelta(content) {
+  if (!firstTokenAt) {
+    firstTokenAt = performance.now();
+    document.querySelector("#stat-ttft").textContent = `${Math.round(firstTokenAt - runStartedAt)}ms`;
+  }
+  if (!activeAssistant) {
+    activeAssistant = document.createElement("div");
+    activeAssistant.className = "message assistant";
+    messagesEl.append(activeAssistant);
+  }
+  activeAssistant.textContent += content;
+  updateElapsed();
+  messagesEl.scrollTop = messagesEl.scrollHeight;
+}
+
+function appendLog(kind, content) {
+  const item = document.createElement("div");
+  item.className = `message ${kind}`;
+  item.textContent = content;
+  messagesEl.append(item);
+  messagesEl.scrollTop = messagesEl.scrollHeight;
+}
+
+function updateUsage(usage) {
+  document.querySelector("#stat-tokens").textContent = usage.total_tokens || 0;
+  document.querySelector("#stat-cached").textContent = usage.cached_tokens || 0;
+}
+
+function updateElapsed() {
+  if (!runStartedAt) {
+    return;
+  }
+  document.querySelector("#stat-elapsed").textContent = `${Math.round(performance.now() - runStartedAt)}ms`;
+}
+
+function resetRun() {
+  activeAssistant = null;
+  runStartedAt = 0;
+  firstTokenAt = 0;
+  messagesEl.textContent = "";
+  document.querySelector("#stat-tokens").textContent = "0";
+  document.querySelector("#stat-cached").textContent = "0";
+  document.querySelector("#stat-ttft").textContent = "-";
+  document.querySelector("#stat-elapsed").textContent = "-";
+}
+
+function closeSocket() {
+  if (socket && socket.readyState < WebSocket.CLOSING) {
+    socket.close();
+  }
+}
+
+function wsUrl() {
+  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+  return `${protocol}//${window.location.host}/ws/debug`;
+}

+ 82 - 0
src/agent_lab/presentation/static/index.html

@@ -0,0 +1,82 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>Agent Lab</title>
+    <link rel="stylesheet" href="/static/styles.css" />
+  </head>
+  <body>
+    <header class="topbar">
+      <h1>Agent Lab</h1>
+      <div id="connection-status" class="status">Idle</div>
+    </header>
+
+    <main class="layout">
+      <aside class="panel controls">
+        <section>
+          <div class="section-head">
+            <h2>System Prompts</h2>
+            <button id="add-system-prompt" type="button">Add</button>
+          </div>
+          <div id="system-prompts" class="stack">
+            <textarea class="system-prompt" rows="4">You are a debugging assistant.</textarea>
+          </div>
+        </section>
+
+        <section>
+          <div class="section-head">
+            <h2>Pre Messages</h2>
+            <button id="add-pre-message" type="button">Add</button>
+          </div>
+          <div id="pre-messages" class="stack"></div>
+        </section>
+
+        <section>
+          <h2>Chat Agent</h2>
+          <label>Model <input id="model" placeholder="Backend default" /></label>
+          <label>Temperature <input id="temperature" type="number" min="0" max="2" step="0.1" value="0.2" /></label>
+          <label>Max Tokens <input id="max-tokens" type="number" min="1" step="1" value="800" /></label>
+        </section>
+
+        <section>
+          <h2>Event Agent</h2>
+          <label>Max Loops <input id="max-event-loops" type="number" min="1" step="1" value="3" /></label>
+          <label class="checkbox">
+            <input id="tool-handoff-note" type="checkbox" checked />
+            handoff_note
+          </label>
+        </section>
+      </aside>
+
+      <section class="workspace">
+        <div id="messages" class="messages"></div>
+
+        <div class="stats">
+          <div><span>Tokens</span><strong id="stat-tokens">0</strong></div>
+          <div><span>Cached</span><strong id="stat-cached">0</strong></div>
+          <div><span>TTFT</span><strong id="stat-ttft">-</strong></div>
+          <div><span>Elapsed</span><strong id="stat-elapsed">-</strong></div>
+        </div>
+
+        <form id="chat-form" class="composer">
+          <textarea id="user-message" rows="3" placeholder="Send a debug message"></textarea>
+          <button type="submit">Run</button>
+        </form>
+      </section>
+    </main>
+
+    <template id="pre-message-template">
+      <div class="pre-message">
+        <select class="pre-role">
+          <option value="user">user</option>
+          <option value="assistant">assistant</option>
+          <option value="system">system</option>
+        </select>
+        <textarea class="pre-content" rows="3"></textarea>
+      </div>
+    </template>
+
+    <script src="/static/app.js"></script>
+  </body>
+</html>

+ 229 - 0
src/agent_lab/presentation/static/styles.css

@@ -0,0 +1,229 @@
+:root {
+  color: #1f2933;
+  background: #eef2f5;
+  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  min-height: 100vh;
+}
+
+button,
+input,
+select,
+textarea {
+  font: inherit;
+}
+
+button {
+  border: 0;
+  border-radius: 6px;
+  background: #0f766e;
+  color: white;
+  cursor: pointer;
+  padding: 8px 12px;
+}
+
+button:hover {
+  background: #115e59;
+}
+
+.topbar {
+  align-items: center;
+  background: #ffffff;
+  border-bottom: 1px solid #d7dee5;
+  display: flex;
+  height: 56px;
+  justify-content: space-between;
+  padding: 0 20px;
+}
+
+.topbar h1 {
+  font-size: 18px;
+  margin: 0;
+}
+
+.status {
+  background: #e0f2f1;
+  border: 1px solid #b2dfdb;
+  border-radius: 999px;
+  color: #0f766e;
+  font-size: 13px;
+  padding: 4px 10px;
+}
+
+.layout {
+  display: grid;
+  gap: 16px;
+  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
+  height: calc(100vh - 56px);
+  padding: 16px;
+}
+
+.panel,
+.workspace {
+  background: #ffffff;
+  border: 1px solid #d7dee5;
+  border-radius: 8px;
+}
+
+.controls {
+  overflow: auto;
+  padding: 16px;
+}
+
+section + section {
+  border-top: 1px solid #e4e9ee;
+  margin-top: 18px;
+  padding-top: 18px;
+}
+
+h2 {
+  font-size: 14px;
+  margin: 0 0 10px;
+}
+
+.section-head {
+  align-items: center;
+  display: flex;
+  justify-content: space-between;
+}
+
+.stack {
+  display: grid;
+  gap: 10px;
+}
+
+label {
+  display: grid;
+  gap: 6px;
+  margin-top: 10px;
+}
+
+.checkbox {
+  align-items: center;
+  display: flex;
+  gap: 8px;
+}
+
+input,
+select,
+textarea {
+  border: 1px solid #cbd5df;
+  border-radius: 6px;
+  color: #1f2933;
+  padding: 8px;
+  width: 100%;
+}
+
+textarea {
+  resize: vertical;
+}
+
+.pre-message {
+  display: grid;
+  gap: 8px;
+  grid-template-columns: 96px 1fr;
+}
+
+.workspace {
+  display: grid;
+  grid-template-rows: minmax(0, 1fr) auto auto;
+  min-width: 0;
+}
+
+.messages {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  overflow: auto;
+  padding: 16px;
+}
+
+.message {
+  border-radius: 8px;
+  line-height: 1.5;
+  max-width: 900px;
+  padding: 10px 12px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.assistant {
+  background: #edf7f6;
+  border: 1px solid #b7ddda;
+}
+
+.session {
+  background: #f4f6f8;
+  color: #52616f;
+}
+
+.event {
+  background: #fff7ed;
+  border: 1px solid #fed7aa;
+}
+
+.tool {
+  background: #f0fdf4;
+  border: 1px solid #bbf7d0;
+}
+
+.error {
+  background: #fef2f2;
+  border: 1px solid #fecaca;
+  color: #991b1b;
+}
+
+.stats {
+  border-top: 1px solid #e4e9ee;
+  display: grid;
+  gap: 1px;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.stats div {
+  background: #f7f9fb;
+  display: grid;
+  gap: 4px;
+  padding: 10px 12px;
+}
+
+.stats span {
+  color: #697586;
+  font-size: 12px;
+}
+
+.stats strong {
+  font-size: 16px;
+}
+
+.composer {
+  border-top: 1px solid #e4e9ee;
+  display: grid;
+  gap: 12px;
+  grid-template-columns: minmax(0, 1fr) auto;
+  padding: 12px;
+}
+
+@media (max-width: 860px) {
+  .layout {
+    grid-template-columns: 1fr;
+    height: auto;
+  }
+
+  .workspace {
+    min-height: 70vh;
+  }
+
+  .stats,
+  .composer,
+  .pre-message {
+    grid-template-columns: 1fr;
+  }
+}

+ 82 - 0
src/agent_lab/presentation/web.py

@@ -0,0 +1,82 @@
+from collections.abc import Callable
+from pathlib import Path
+from typing import Any
+
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect
+from fastapi.responses import FileResponse
+from fastapi.staticfiles import StaticFiles
+from pydantic import ValidationError
+
+from agent_lab.application.contracts import DebugRunRequest
+from agent_lab.application.runtime import DebugRuntime
+from agent_lab.infrastructure.chat_client import OpenAICompatibleChatClient
+from agent_lab.settings import Settings
+
+
+RuntimeFactory = Callable[[], Any]
+
+STATIC_DIR = Path(__file__).parent / "static"
+
+
+def create_app(
+    settings: Settings | None = None,
+    runtime_factory: RuntimeFactory | None = None,
+) -> FastAPI:
+    resolved_settings = settings or Settings()
+    resolved_runtime_factory = runtime_factory or _runtime_factory(resolved_settings)
+
+    app = FastAPI(title="Agent Lab")
+    app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
+
+    @app.get("/health")
+    async def health() -> dict[str, str]:
+        return {"status": "ok"}
+
+    @app.get("/")
+    async def index() -> FileResponse:
+        return FileResponse(STATIC_DIR / "index.html")
+
+    @app.websocket("/ws/debug")
+    async def debug(websocket: WebSocket) -> None:
+        await websocket.accept()
+        runtime = resolved_runtime_factory()
+        should_close = True
+        try:
+            payload = await websocket.receive_json()
+            request = DebugRunRequest.model_validate(payload)
+            async for message in runtime.run(request):
+                await websocket.send_json(message)
+        except WebSocketDisconnect:
+            should_close = False
+            return
+        except (ValidationError, ValueError) as exc:
+            await websocket.send_json({"type": "error", "message": str(exc)})
+        except Exception as exc:
+            await websocket.send_json({"type": "error", "message": str(exc)})
+        finally:
+            await _close_runtime(runtime)
+            if should_close:
+                await websocket.close()
+
+    return app
+
+
+def _runtime_factory(settings: Settings) -> Callable[[], DebugRuntime]:
+    def build_runtime() -> DebugRuntime:
+        chat_client = OpenAICompatibleChatClient(
+            api_key=settings.openai_api_key,
+            base_url=settings.openai_base_url,
+            default_model=settings.openai_default_model,
+            request_timeout_seconds=settings.request_timeout_seconds,
+            include_usage=settings.openai_include_usage,
+        )
+        return DebugRuntime(chat_client)
+
+    return build_runtime
+
+
+async def _close_runtime(runtime: Any) -> None:
+    chat_client = getattr(runtime, "chat_client", None)
+    close = getattr(chat_client, "aclose", None)
+    if close is not None:
+        await close()

+ 15 - 0
src/agent_lab/settings.py

@@ -0,0 +1,15 @@
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class Settings(BaseSettings):
+    model_config = SettingsConfigDict(
+        env_file=".env",
+        env_prefix="AGENT_LAB_",
+        extra="ignore",
+    )
+
+    openai_api_key: str = ""
+    openai_base_url: str = "https://api.openai.com/v1"
+    openai_default_model: str = "gpt-4.1-mini"
+    openai_include_usage: bool = True
+    request_timeout_seconds: float = 60.0

+ 125 - 0
tests/test_debug_runtime.py

@@ -0,0 +1,125 @@
+from collections.abc import AsyncIterator
+
+import pytest
+
+from agent_lab.application.contracts import AgentParams, DebugRunRequest, EventAgentParams
+from agent_lab.application.runtime import DebugRuntime
+from agent_lab.domain.events import ToolCallEvent
+from agent_lab.domain.messages import ChatMessage, StreamItem
+
+
+class FakeChatClient:
+    def __init__(self) -> None:
+        self.calls = 0
+
+    async def stream_chat(
+        self,
+        messages: list[ChatMessage],
+        tools: list[dict],
+        params: AgentParams,
+    ) -> AsyncIterator[StreamItem]:
+        self.calls += 1
+        if self.calls == 1:
+            yield StreamItem.event(
+                ToolCallEvent(
+                    id="call_1",
+                    name="handoff_note",
+                    arguments={"message": "need event agent"},
+                    raw_arguments='{"message":"need event agent"}',
+                )
+            )
+            return
+
+        assert any(message.role == "tool" for message in messages)
+        yield StreamItem.message_delta("final answer")
+
+
+class StrictHistoryChatClient:
+    def __init__(self) -> None:
+        self.calls = 0
+        self.second_call_messages: list[ChatMessage] = []
+
+    async def stream_chat(
+        self,
+        messages: list[ChatMessage],
+        tools: list[dict],
+        params: AgentParams,
+    ) -> AsyncIterator[StreamItem]:
+        self.calls += 1
+        if self.calls == 1:
+            yield StreamItem.event(
+                ToolCallEvent(
+                    id="call_1",
+                    name="handoff_note",
+                    arguments={"message": "need event agent"},
+                    raw_arguments='{"message":"need event agent"}',
+                )
+            )
+            return
+
+        self.second_call_messages = list(messages)
+        yield StreamItem.message_delta("final answer")
+
+
+@pytest.mark.asyncio
+async def test_runtime_routes_chat_events_through_event_agent_then_continues_chat():
+    request = DebugRunRequest(
+        user_message="debug this",
+        system_prompts=["You are a debugger."],
+        pre_messages=[],
+        chat_agent=AgentParams(model="fake-model", temperature=0.1, max_tokens=200),
+        event_agent=EventAgentParams(enabled_tools=["handoff_note"], max_event_loops=3),
+    )
+    client = FakeChatClient()
+    runtime = DebugRuntime(client)
+
+    outputs = [message async for message in runtime.run(request)]
+
+    assert client.calls == 2
+    assert [message["type"] for message in outputs] == [
+        "session_started",
+        "event",
+        "tool_result",
+        "message_delta",
+        "done",
+    ]
+    assert outputs[1]["event"]["name"] == "handoff_note"
+    assert outputs[3]["content"] == "final answer"
+
+
+@pytest.mark.asyncio
+async def test_runtime_preserves_assistant_tool_calls_before_tool_reply():
+    request = DebugRunRequest(
+        user_message="debug this",
+        system_prompts=["You are a debugger."],
+        pre_messages=[],
+        chat_agent=AgentParams(model="fake-model", temperature=0.1, max_tokens=200),
+        event_agent=EventAgentParams(enabled_tools=["handoff_note"], max_event_loops=2),
+    )
+    client = StrictHistoryChatClient()
+    runtime = DebugRuntime(client)
+
+    outputs = [message async for message in runtime.run(request)]
+
+    assert client.calls == 2
+    assert [message.role for message in client.second_call_messages] == [
+        "system",
+        "user",
+        "assistant",
+        "tool",
+    ]
+    assistant_message = client.second_call_messages[2]
+    tool_message = client.second_call_messages[3]
+    assert assistant_message.content == ""
+    assert assistant_message.tool_calls == [
+        {
+            "id": "call_1",
+            "type": "function",
+            "function": {
+                "name": "handoff_note",
+                "arguments": '{"message":"need event agent"}',
+            },
+        }
+    ]
+    assert tool_message.tool_call_id == "call_1"
+    assert outputs[-1] == {"type": "done"}

+ 28 - 0
tests/test_event_agent.py

@@ -0,0 +1,28 @@
+import json
+
+import pytest
+
+from agent_lab.application.event_agent import EventAgent
+from agent_lab.domain.events import ToolCallEvent
+
+
+@pytest.mark.asyncio
+async def test_event_agent_executes_enabled_tool_as_tool_reply_message():
+    agent = EventAgent(enabled_tools=["handoff_note"])
+    event = ToolCallEvent(
+        id="call_1",
+        name="handoff_note",
+        arguments={"message": "inspect this event"},
+        raw_arguments='{"message":"inspect this event"}',
+    )
+
+    reply = await agent.handle(event)
+
+    assert reply.role == "tool"
+    assert reply.tool_call_id == "call_1"
+    assert reply.name == "handoff_note"
+    assert json.loads(reply.content) == {
+        "tool": "handoff_note",
+        "message": "inspect this event",
+    }
+

+ 84 - 0
tests/test_openai_stream_parser.py

@@ -0,0 +1,84 @@
+from agent_lab.domain.events import ToolCallEvent
+from agent_lab.infrastructure.openai_compatible import ChatCompletionStreamParser
+
+
+def test_parser_emits_content_usage_and_tool_event():
+    parser = ChatCompletionStreamParser()
+
+    items = []
+    items.extend(
+        parser.feed(
+            {
+                "choices": [
+                    {
+                        "delta": {"content": "hello"},
+                        "finish_reason": None,
+                    }
+                ]
+            }
+        )
+    )
+    items.extend(
+        parser.feed(
+            {
+                "choices": [
+                    {
+                        "delta": {
+                            "tool_calls": [
+                                {
+                                    "index": 0,
+                                    "id": "call_1",
+                                    "type": "function",
+                                    "function": {
+                                        "name": "handoff_note",
+                                        "arguments": '{"message":"',
+                                    },
+                                }
+                            ]
+                        },
+                        "finish_reason": None,
+                    }
+                ]
+            }
+        )
+    )
+    items.extend(
+        parser.feed(
+            {
+                "choices": [
+                    {
+                        "delta": {
+                            "tool_calls": [
+                                {
+                                    "index": 0,
+                                    "function": {"arguments": 'from model"}'},
+                                }
+                            ]
+                        },
+                        "finish_reason": "tool_calls",
+                    }
+                ],
+                "usage": {
+                    "prompt_tokens": 10,
+                    "completion_tokens": 3,
+                    "total_tokens": 13,
+                    "prompt_tokens_details": {"cached_tokens": 4},
+                },
+            }
+        )
+    )
+
+    assert [item.content for item in items if item.kind == "message_delta"] == ["hello"]
+    events = [item.event for item in items if item.kind == "event"]
+    assert events == [
+        ToolCallEvent(
+            id="call_1",
+            name="handoff_note",
+            arguments={"message": "from model"},
+            raw_arguments='{"message":"from model"}',
+        )
+    ]
+    usage = [item.usage for item in items if item.kind == "usage"][0]
+    assert usage.total_tokens == 13
+    assert usage.cached_tokens == 4
+

+ 364 - 0
tests/test_websocket_api.py

@@ -0,0 +1,364 @@
+import json
+from collections.abc import AsyncIterator
+from pathlib import Path
+
+import httpx
+import pytest
+from fastapi.testclient import TestClient
+
+from agent_lab.application.contracts import AgentParams, DebugRunRequest, EventAgentParams
+from agent_lab.application.runtime import DebugRuntime
+from agent_lab.domain.events import ToolCallEvent
+from agent_lab.domain.messages import ChatMessage, StreamItem
+from agent_lab.infrastructure.chat_client import OpenAICompatibleChatClient
+from agent_lab.presentation.web import create_app
+
+
+class FakeRuntime:
+    def __init__(self) -> None:
+        self.requests: list[DebugRunRequest] = []
+
+    async def run(self, request: DebugRunRequest) -> AsyncIterator[dict]:
+        self.requests.append(request)
+        yield {"type": "session_started"}
+        yield {"type": "message_delta", "content": "hello"}
+        yield {"type": "done"}
+
+
+def _request_payload() -> dict:
+    return {
+        "user_message": "debug this",
+        "system_prompts": ["You are a debugger."],
+        "pre_messages": [{"role": "user", "content": "previous turn"}],
+        "chat_agent": {
+            "model": "fake-model",
+            "temperature": 0.1,
+            "max_tokens": 200,
+        },
+        "event_agent": {
+            "enabled_tools": ["handoff_note"],
+            "max_event_loops": 2,
+        },
+    }
+
+
+def test_health_returns_ok():
+    app = create_app(runtime_factory=FakeRuntime)
+    client = TestClient(app)
+
+    response = client.get("/health")
+
+    assert response.status_code == 200
+    assert response.json() == {"status": "ok"}
+
+
+def test_websocket_debug_streams_runtime_messages():
+    runtime = FakeRuntime()
+    app = create_app(runtime_factory=lambda: runtime)
+    client = TestClient(app)
+
+    with client.websocket_connect("/ws/debug") as websocket:
+        websocket.send_json(_request_payload())
+
+        assert websocket.receive_json() == {"type": "session_started"}
+        assert websocket.receive_json() == {
+            "type": "message_delta",
+            "content": "hello",
+        }
+        assert websocket.receive_json() == {"type": "done"}
+
+    assert runtime.requests[0].user_message == "debug this"
+    assert runtime.requests[0].pre_messages[0].content == "previous turn"
+
+
+def test_websocket_debug_sends_error_for_invalid_request():
+    app = create_app(runtime_factory=FakeRuntime)
+    client = TestClient(app)
+
+    with client.websocket_connect("/ws/debug") as websocket:
+        websocket.send_json({"chat_agent": {"model": "fake-model"}})
+        message = websocket.receive_json()
+
+    assert message["type"] == "error"
+    assert "user_message" in message["message"]
+
+
+class HistoryCapturingChatClient:
+    def __init__(self) -> None:
+        self.calls = 0
+        self.second_call_messages: list[ChatMessage] = []
+
+    async def stream_chat(
+        self,
+        messages: list[ChatMessage],
+        tools: list[dict],
+        params: AgentParams,
+    ) -> AsyncIterator[StreamItem]:
+        self.calls += 1
+        if self.calls == 1:
+            yield StreamItem.message_delta("Need event help.")
+            yield StreamItem.event(
+                ToolCallEvent(
+                    id="call_1",
+                    name="handoff_note",
+                    arguments={"message": "inspect this"},
+                    raw_arguments='{"message":"inspect this"}',
+                )
+            )
+            return
+
+        self.second_call_messages = list(messages)
+        yield StreamItem.message_delta("Final answer.")
+
+
+@pytest.mark.asyncio
+async def test_runtime_appends_assistant_message_before_tool_reply_history():
+    request = DebugRunRequest(
+        user_message="debug this",
+        system_prompts=["You are a debugger."],
+        pre_messages=[],
+        chat_agent=AgentParams(model="fake-model", temperature=0.1, max_tokens=200),
+        event_agent=EventAgentParams(enabled_tools=["handoff_note"], max_event_loops=2),
+    )
+    client = HistoryCapturingChatClient()
+    runtime = DebugRuntime(client)
+
+    outputs = [message async for message in runtime.run(request)]
+
+    assert client.calls == 2
+    assert [message.role for message in client.second_call_messages] == [
+        "system",
+        "user",
+        "assistant",
+        "tool",
+    ]
+    assert client.second_call_messages[2].content == "Need event help."
+    assert outputs[-1] == {"type": "done"}
+
+
+@pytest.mark.asyncio
+async def test_openai_chat_client_streams_sse_chunks_through_parser():
+    requests: list[httpx.Request] = []
+
+    def handler(request: httpx.Request) -> httpx.Response:
+        requests.append(request)
+        payload = json.loads(request.content)
+        assert payload["stream"] is True
+        assert payload["model"] == "model-x"
+        assert payload["messages"] == [{"role": "user", "content": "hi"}]
+        assert payload["tools"][0]["function"]["name"] == "handoff_note"
+        assert payload["temperature"] == 0.3
+        assert payload["max_tokens"] == 50
+        assert request.headers["authorization"] == "Bearer test-key"
+        return httpx.Response(
+            200,
+            content=(
+                b'data: {"choices":[{"delta":{"content":"hi"},"finish_reason":null}]}\n\n'
+                b"data: [DONE]\n\n"
+            ),
+        )
+
+    async with httpx.AsyncClient(
+        transport=httpx.MockTransport(handler),
+        base_url="https://llm.test/v1",
+    ) as http_client:
+        client = OpenAICompatibleChatClient(
+            api_key="test-key",
+            base_url="https://llm.test/v1",
+            default_model="default-model",
+            request_timeout_seconds=5,
+            http_client=http_client,
+        )
+        items = [
+            item
+            async for item in client.stream_chat(
+                messages=[ChatMessage(role="user", content="hi")],
+                tools=[
+                    {
+                        "type": "function",
+                        "function": {"name": "handoff_note", "parameters": {}},
+                    }
+                ],
+                params=AgentParams(model="model-x", temperature=0.3, max_tokens=50),
+            )
+        ]
+
+    assert requests[0].url.path == "/v1/chat/completions"
+    assert [item.content for item in items] == ["hi"]
+
+
+@pytest.mark.asyncio
+async def test_openai_chat_client_uses_default_model_when_request_model_is_blank():
+    captured_payloads: list[dict] = []
+
+    def handler(request: httpx.Request) -> httpx.Response:
+        captured_payloads.append(json.loads(request.content))
+        return httpx.Response(
+            200,
+            content=b'data: {"choices":[{"delta":{"content":"hi"},"finish_reason":null}]}\n\n',
+        )
+
+    async with httpx.AsyncClient(
+        transport=httpx.MockTransport(handler),
+        base_url="https://llm.test/v1",
+    ) as http_client:
+        client = OpenAICompatibleChatClient(
+            api_key="",
+            base_url="https://llm.test/v1",
+            default_model="provider-default",
+            request_timeout_seconds=5,
+            http_client=http_client,
+        )
+        [
+            item
+            async for item in client.stream_chat(
+                messages=[ChatMessage(role="user", content="hi")],
+                tools=[],
+                params=AgentParams(model="   ", temperature=0.3, max_tokens=50),
+            )
+        ]
+
+    assert captured_payloads[0]["model"] == "provider-default"
+
+
+@pytest.mark.asyncio
+async def test_openai_chat_client_serializes_tool_reply_without_name():
+    captured_payloads: list[dict] = []
+    assistant_tool_call = {
+        "id": "call_1",
+        "type": "function",
+        "function": {
+            "name": "handoff_note",
+            "arguments": '{"message":"inspect"}',
+        },
+    }
+
+    def handler(request: httpx.Request) -> httpx.Response:
+        captured_payloads.append(json.loads(request.content))
+        return httpx.Response(
+            200,
+            content=b'data: {"choices":[{"delta":{"content":"done"},"finish_reason":null}]}\n\n',
+        )
+
+    async with httpx.AsyncClient(
+        transport=httpx.MockTransport(handler),
+        base_url="https://llm.test/v1",
+    ) as http_client:
+        client = OpenAICompatibleChatClient(
+            api_key="",
+            base_url="https://llm.test/v1",
+            default_model="provider-default",
+            request_timeout_seconds=5,
+            http_client=http_client,
+        )
+        [
+            item
+            async for item in client.stream_chat(
+                messages=[
+                    ChatMessage(
+                        role="assistant",
+                        content="",
+                        tool_calls=[assistant_tool_call],
+                    ),
+                    ChatMessage(
+                        role="tool",
+                        content="noted",
+                        name="handoff_note",
+                        tool_call_id="call_1",
+                    ),
+                ],
+                tools=[],
+                params=AgentParams(model="provider-default", temperature=0.3, max_tokens=50),
+            )
+        ]
+
+    assert captured_payloads[0]["messages"] == [
+        {
+            "role": "assistant",
+            "content": "",
+            "tool_calls": [assistant_tool_call],
+        },
+        {
+            "role": "tool",
+            "content": "noted",
+            "tool_call_id": "call_1",
+        },
+    ]
+
+
+@pytest.mark.asyncio
+async def test_openai_chat_client_rejects_tool_message_without_tool_call_id_before_network():
+    network_called = False
+
+    def handler(request: httpx.Request) -> httpx.Response:
+        nonlocal network_called
+        network_called = True
+        return httpx.Response(
+            200,
+            content=b'data: {"choices":[{"delta":{"content":"done"},"finish_reason":null}]}\n\n',
+        )
+
+    async with httpx.AsyncClient(
+        transport=httpx.MockTransport(handler),
+        base_url="https://llm.test/v1",
+    ) as http_client:
+        client = OpenAICompatibleChatClient(
+            api_key="",
+            base_url="https://llm.test/v1",
+            default_model="provider-default",
+            request_timeout_seconds=5,
+            http_client=http_client,
+        )
+
+        with pytest.raises(ValueError, match="tool messages require tool_call_id"):
+            [
+                item
+                async for item in client.stream_chat(
+                    messages=[ChatMessage(role="tool", content="orphan tool result")],
+                    tools=[],
+                    params=AgentParams(model="provider-default", temperature=0.3, max_tokens=50),
+                )
+            ]
+
+    assert network_called is False
+
+
+def test_static_pre_message_role_selector_does_not_offer_tool():
+    html = Path("src/agent_lab/presentation/static/index.html").read_text()
+
+    assert '<option value="tool">tool</option>' not in html
+
+
+@pytest.mark.asyncio
+async def test_openai_chat_client_omits_stream_options_when_usage_disabled():
+    captured_payloads: list[dict] = []
+
+    def handler(request: httpx.Request) -> httpx.Response:
+        captured_payloads.append(json.loads(request.content))
+        return httpx.Response(
+            200,
+            content=b'data: {"choices":[{"delta":{"content":"hi"},"finish_reason":null}]}\n\n',
+        )
+
+    async with httpx.AsyncClient(
+        transport=httpx.MockTransport(handler),
+        base_url="https://llm.test/v1",
+    ) as http_client:
+        client = OpenAICompatibleChatClient(
+            api_key="",
+            base_url="https://llm.test/v1",
+            default_model="provider-default",
+            request_timeout_seconds=5,
+            include_usage=False,
+            http_client=http_client,
+        )
+        [
+            item
+            async for item in client.stream_chat(
+                messages=[ChatMessage(role="user", content="hi")],
+                tools=[],
+                params=AgentParams(model="provider-default", temperature=0.3, max_tokens=50),
+            )
+        ]
+
+    assert "stream_options" not in captured_payloads[0]

+ 33 - 0
tests/test_ws_smoke.py

@@ -0,0 +1,33 @@
+import importlib.util
+from pathlib import Path
+
+
+def _load_ws_smoke_module():
+    script_path = Path(__file__).resolve().parents[1] / "scripts" / "ws_smoke.py"
+    spec = importlib.util.spec_from_file_location("ws_smoke", script_path)
+    module = importlib.util.module_from_spec(spec)
+    assert spec.loader is not None
+    spec.loader.exec_module(module)
+    return module
+
+
+def test_build_payload_can_disable_handoff_note_tool():
+    ws_smoke = _load_ws_smoke_module()
+
+    payload = ws_smoke.build_payload(
+        message="debug the event flow",
+        model="compatible-model",
+        handoff_note_enabled=False,
+    )
+
+    assert payload["user_message"] == "debug the event flow"
+    assert payload["chat_agent"]["model"] == "compatible-model"
+    assert payload["event_agent"]["enabled_tools"] == []
+
+
+def test_parser_defaults_to_blank_model_for_backend_fallback():
+    ws_smoke = _load_ws_smoke_module()
+
+    args = ws_smoke.build_parser().parse_args([])
+
+    assert args.model == ""

+ 721 - 0
uv.lock

@@ -0,0 +1,721 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+
+[[package]]
+name = "agent-lab"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+    { name = "fastapi" },
+    { name = "httpx" },
+    { name = "pydantic-settings" },
+    { name = "uvicorn", extra = ["standard"] },
+    { name = "websockets" },
+]
+
+[package.dev-dependencies]
+dev = [
+    { name = "pytest" },
+    { name = "pytest-asyncio" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "fastapi", specifier = ">=0.111.0" },
+    { name = "httpx", specifier = ">=0.27.0" },
+    { name = "pydantic-settings", specifier = ">=2.3.0" },
+    { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
+    { name = "websockets", specifier = ">=12.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+    { name = "pytest", specifier = ">=8.2.0" },
+    { name = "pytest-asyncio", specifier = ">=0.23.0" },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.14.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "idna" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.6.17"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.2"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.139.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "annotated-doc" },
+    { name = "pydantic" },
+    { name = "starlette" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "h11" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" },
+]
+
+[[package]]
+name = "httptools"
+version = "0.8.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "certifi" },
+    { name = "httpcore" },
+    { name = "idna" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "annotated-types" },
+    { name = "pydantic-core" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.14.2"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "pydantic" },
+    { name = "python-dotenv" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+    { name = "pygments" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.4.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "pytest" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.3.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.49.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "h11" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f" },
+]
+
+[package.optional-dependencies]
+standard = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "httptools" },
+    { name = "python-dotenv" },
+    { name = "pyyaml" },
+    { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+    { name = "watchfiles" },
+    { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.2.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "anyio" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0" },
+]
+
+[[package]]
+name = "websockets"
+version = "16.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec" },
+]