Update project structure
Browse files- Renamed the example environment file to reflect the FastAPI backend usage.
- Removed the Gradio UI and associated files, consolidating the project to a single Next.js frontend served by FastAPI.
- Added a pre-commit configuration for local linting and formatting.
- Updated AGENTS.md and README.md to clarify the architecture and deployment process, emphasizing the new structure and removing references to the Gradio UI.
- Introduced CI workflows for automated linting, testing, and deployment to Hugging Face Spaces.
- .env.example +1 -1
- .github/workflows/ci.yml +65 -0
- .github/workflows/deploy-prod-to-hf.yml +22 -0
- .github/workflows/sync-to-hf.yml +7 -22
- .pre-commit-config.yaml +10 -0
- AGENTS.md +13 -13
- Dockerfile.gradio +0 -27
- README.md +11 -87
- data/scraping_scripts/README.md +4 -4
- pyproject.toml +5 -1
- scripts/chat_service.py +91 -157
- scripts/gradio_presenter.py +0 -375
- scripts/main.py +0 -151
- tests/manual_e2e_langsmith.md +0 -60
- tests/test_gradio_kb_e2e.py +0 -209
- tests/test_gradio_presenter.py +0 -91
- uv.lock +80 -271
.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# To run AI Tutor
|
| 2 |
OPENAI_API_KEY=...
|
| 3 |
ANTHROPIC_API_KEY=...
|
| 4 |
GEMINI_API_KEY=...
|
|
|
|
| 1 |
+
# To run the AI Tutor app (FastAPI backend)
|
| 2 |
OPENAI_API_KEY=...
|
| 3 |
ANTHROPIC_API_KEY=...
|
| 4 |
GEMINI_API_KEY=...
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches: [main]
|
| 6 |
+
push:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
concurrency:
|
| 10 |
+
group: ci-${{ github.ref }}
|
| 11 |
+
cancel-in-progress: true
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
qa:
|
| 15 |
+
name: Lint & format
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
steps:
|
| 18 |
+
- uses: actions/checkout@v4
|
| 19 |
+
|
| 20 |
+
- name: Install uv
|
| 21 |
+
uses: astral-sh/setup-uv@v4
|
| 22 |
+
with:
|
| 23 |
+
enable-cache: true
|
| 24 |
+
|
| 25 |
+
- name: Set up Python
|
| 26 |
+
uses: actions/setup-python@v5
|
| 27 |
+
with:
|
| 28 |
+
python-version-file: ".python-version"
|
| 29 |
+
|
| 30 |
+
- name: Install dev tooling
|
| 31 |
+
run: uv sync --only-dev --frozen
|
| 32 |
+
|
| 33 |
+
- name: Format check
|
| 34 |
+
run: uv run --no-sync ruff format --check .
|
| 35 |
+
|
| 36 |
+
- name: Lint check
|
| 37 |
+
run: uv run --no-sync ruff check .
|
| 38 |
+
|
| 39 |
+
tests:
|
| 40 |
+
name: Tests
|
| 41 |
+
runs-on: ubuntu-latest
|
| 42 |
+
steps:
|
| 43 |
+
- uses: actions/checkout@v4
|
| 44 |
+
|
| 45 |
+
# run_kb_command sandbox tests shell out to ripgrep; without it they skip.
|
| 46 |
+
- name: Install ripgrep
|
| 47 |
+
run: sudo apt-get update && sudo apt-get install -y ripgrep
|
| 48 |
+
|
| 49 |
+
- name: Install uv
|
| 50 |
+
uses: astral-sh/setup-uv@v4
|
| 51 |
+
with:
|
| 52 |
+
enable-cache: true
|
| 53 |
+
|
| 54 |
+
- name: Set up Python
|
| 55 |
+
uses: actions/setup-python@v5
|
| 56 |
+
with:
|
| 57 |
+
python-version-file: ".python-version"
|
| 58 |
+
|
| 59 |
+
- name: Install project
|
| 60 |
+
run: uv sync --frozen
|
| 61 |
+
|
| 62 |
+
# Live model-backed tests stay skipped here (no RUN_LIVE_* / no API keys);
|
| 63 |
+
# they are run manually/locally. CI guards the offline suite only.
|
| 64 |
+
- name: Run tests
|
| 65 |
+
run: uv run --no-sync pytest
|
.github/workflows/deploy-prod-to-hf.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy prod to Hugging Face (ai-tutor-chatbot)
|
| 2 |
+
|
| 3 |
+
# Manual-only promotion to the public prod Space. Run it from the Actions tab
|
| 4 |
+
# after verifying the dev Space (ai-tutor) looks good. Deploys the ref you
|
| 5 |
+
# pick in the "Run workflow" dropdown (defaults to main).
|
| 6 |
+
on:
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
deploy-prod:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v3
|
| 14 |
+
with:
|
| 15 |
+
fetch-depth: 0
|
| 16 |
+
lfs: true
|
| 17 |
+
|
| 18 |
+
- name: Push to HuggingFace Space (ai-tutor-chatbot)
|
| 19 |
+
env:
|
| 20 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 21 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 22 |
+
run: git push --force "https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/towardsai-tutors/ai-tutor-chatbot" HEAD:main
|
.github/workflows/sync-to-hf.yml
CHANGED
|
@@ -1,4 +1,7 @@
|
|
| 1 |
-
name:
|
|
|
|
|
|
|
|
|
|
| 2 |
on:
|
| 3 |
push:
|
| 4 |
branches: [main]
|
|
@@ -7,39 +10,21 @@ on:
|
|
| 7 |
- 'README.md'
|
| 8 |
- 'docs/**'
|
| 9 |
- '**.md'
|
| 10 |
-
# Add any other paths you want to exclude
|
| 11 |
|
| 12 |
# to run this workflow manually from the Actions tab
|
| 13 |
workflow_dispatch:
|
| 14 |
|
| 15 |
jobs:
|
| 16 |
-
|
| 17 |
runs-on: ubuntu-latest
|
| 18 |
-
strategy:
|
| 19 |
-
fail-fast: false
|
| 20 |
-
matrix:
|
| 21 |
-
include:
|
| 22 |
-
- space: ai-tutor
|
| 23 |
-
dockerfile: Dockerfile
|
| 24 |
-
- space: ai-tutor-chatbot
|
| 25 |
-
dockerfile: Dockerfile.gradio
|
| 26 |
steps:
|
| 27 |
- uses: actions/checkout@v3
|
| 28 |
with:
|
| 29 |
fetch-depth: 0
|
| 30 |
lfs: true
|
| 31 |
|
| 32 |
-
- name:
|
| 33 |
-
if: matrix.dockerfile != 'Dockerfile'
|
| 34 |
-
run: |
|
| 35 |
-
git config user.email "actions@github.com"
|
| 36 |
-
git config user.name "GitHub Actions"
|
| 37 |
-
cp "${{ matrix.dockerfile }}" Dockerfile
|
| 38 |
-
git add Dockerfile
|
| 39 |
-
git commit -m "Use ${{ matrix.dockerfile }} for ${{ matrix.space }} space"
|
| 40 |
-
|
| 41 |
-
- name: Push to HuggingFace Space (${{ matrix.space }})
|
| 42 |
env:
|
| 43 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 44 |
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 45 |
-
run: git push --force "https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/towardsai-tutors/
|
|
|
|
| 1 |
+
name: Deploy dev to Hugging Face (ai-tutor)
|
| 2 |
+
|
| 3 |
+
# Continuous deployment to the private dev Space. Test there, then promote
|
| 4 |
+
# the same commit to prod with the "Deploy prod" workflow (manual trigger).
|
| 5 |
on:
|
| 6 |
push:
|
| 7 |
branches: [main]
|
|
|
|
| 10 |
- 'README.md'
|
| 11 |
- 'docs/**'
|
| 12 |
- '**.md'
|
|
|
|
| 13 |
|
| 14 |
# to run this workflow manually from the Actions tab
|
| 15 |
workflow_dispatch:
|
| 16 |
|
| 17 |
jobs:
|
| 18 |
+
deploy-dev:
|
| 19 |
runs-on: ubuntu-latest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
steps:
|
| 21 |
- uses: actions/checkout@v3
|
| 22 |
with:
|
| 23 |
fetch-depth: 0
|
| 24 |
lfs: true
|
| 25 |
|
| 26 |
+
- name: Push to HuggingFace Space (ai-tutor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
env:
|
| 28 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 29 |
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 30 |
+
run: git push --force "https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/towardsai-tutors/ai-tutor" HEAD:main
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Local fast-feedback hooks. One-time setup per clone: `uv run pre-commit install`.
|
| 2 |
+
# CI (.github/workflows/ci.yml) is the real gate; these just fix lint/format
|
| 3 |
+
# before a commit so you don't get a red CI run for formatting.
|
| 4 |
+
repos:
|
| 5 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 6 |
+
rev: v0.15.15 # keep in sync with the ruff version in pyproject dev deps
|
| 7 |
+
hooks:
|
| 8 |
+
- id: ruff-check # lint
|
| 9 |
+
args: [--fix]
|
| 10 |
+
- id: ruff-format # format
|
AGENTS.md
CHANGED
|
@@ -4,17 +4,14 @@ This is the **canonical, tool-agnostic** instruction file for the repo. `CLAUDE.
|
|
| 4 |
|
| 5 |
## Project Overview
|
| 6 |
|
| 7 |
-
AI tutor for applied AI, LLMs, RAG, and Python. **Agentic RAG**: a LangChain/LangGraph agent grounds answers in a curated corpus of course + library docs, can browse a local file-based knowledge base, and (optionally) search the live web.
|
| 8 |
-
|
| 9 |
-
- **Gradio** UI — `scripts/main.py` (the original chatbot).
|
| 10 |
-
- **Next.js** UI — `frontend/`, served by a **FastAPI** backend (`scripts/api.py`) that streams in the Vercel AI SDK UI-message protocol.
|
| 11 |
|
| 12 |
ChromaDB for vectors; Cohere for embeddings/rerank; chat model is provider-configurable (Gemini default, Anthropic, OpenAI). Python ≥3.13, managed with `uv`.
|
| 13 |
|
| 14 |
## Key URLs
|
| 15 |
|
| 16 |
- [GitHub repo](https://github.com/towardsai/ai-tutor-app)
|
| 17 |
-
- [Live demo
|
| 18 |
- [Vector DB + KB bundle](https://huggingface.co/datasets/towardsai-tutors/ai-tutor-vector-db) · [Private raw JSONL data](https://huggingface.co/datasets/towardsai-tutors/ai-tutor-data)
|
| 19 |
|
| 20 |
## Where things live
|
|
@@ -26,7 +23,6 @@ ChromaDB for vectors; Cohere for embeddings/rerank; chat model is provider-confi
|
|
| 26 |
| Hybrid retrieval | `scripts/chroma_rag.py` |
|
| 27 |
| KB browsing sandbox + citation resolution | `scripts/kb_shell.py`, `scripts/kb_manifest.py` |
|
| 28 |
| FastAPI server (`/api/chat`, `/api/tools`, `/healthz`) | `scripts/api.py` |
|
| 29 |
-
| Gradio app + renderer | `scripts/main.py`, `scripts/gradio_presenter.py` |
|
| 30 |
| Paths, models, startup downloads | `scripts/setup.py` |
|
| 31 |
| **Sources — single source of truth** | `data/scraping_scripts/source_registry.py` |
|
| 32 |
| Agent tracing (LangSmith) + server logging (stdlib `logging` → stdout) | `scripts/agent_tracing.py`, `scripts/setup.py` |
|
|
@@ -35,7 +31,7 @@ ChromaDB for vectors; Cohere for embeddings/rerank; chat model is provider-confi
|
|
| 35 |
|
| 36 |
## Architecture in brief
|
| 37 |
|
| 38 |
-
The agent is built with `langchain.agents.create_agent()` (LangGraph), an `InMemorySaver` checkpointer keyed by `thread_id`, and middlewares for context-editing, summarization, and source preference. `stream_chat()` is the single entry point
|
| 39 |
|
| 40 |
- **`retrieve_tutor_context(query)`** — hybrid RAG over the corpus, scoped to the user's selected sources.
|
| 41 |
- **`run_kb_command(...)`** — read-only KB file browsing (see below).
|
|
@@ -63,13 +59,12 @@ Runtime guidance the agent follows is in `data/kb/AGENTS.md` (injected into the
|
|
| 63 |
|
| 64 |
## Sources & config
|
| 65 |
|
| 66 |
-
`data/scraping_scripts/source_registry.py` is the **single source of truth** for sources (`SOURCE_CONFIGS`, key groupings, UI labels, defaults); `scripts/setup.py` re-exports them and
|
| 67 |
|
| 68 |
## Running locally
|
| 69 |
|
| 70 |
```bash
|
| 71 |
uv sync && cp .env.example .env # then fill in keys
|
| 72 |
-
uv run -m scripts.main # Gradio UI (:7860)
|
| 73 |
uv run -m scripts.api # FastAPI backend (:8000; override AI_TUTOR_API_PORT/PORT)
|
| 74 |
# Next.js frontend (needs the API running):
|
| 75 |
cd frontend && npm install && cp .env.example .env.local && npm run dev # :3000
|
|
@@ -77,7 +72,7 @@ cd frontend && npm install && cp .env.example .env.local && npm run dev # :300
|
|
| 77 |
|
| 78 |
First start downloads the vector-db/KB bundle from HF if missing (`HF_TOKEN`). The frontend is a static export (`output: 'export'`); `npm run build` emits `frontend/out`, which `scripts/api.py` mounts at `/`.
|
| 79 |
|
| 80 |
-
Test &
|
| 81 |
|
| 82 |
## Data update workflows
|
| 83 |
|
|
@@ -97,12 +92,17 @@ Chat runtime: `COHERE_API_KEY` (retrieval), one chat-model provider key (`GEMINI
|
|
| 97 |
|
| 98 |
## Deployment
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
## Conventions
|
| 103 |
|
| 104 |
-
- **No em-dashes in frontend user-facing text.** Use a comma, parentheses, a colon, or two sentences instead. This covers every string the UI renders (Next.js components
|
| 105 |
-
- **Decide frontend vs. backend ownership before changing behavior, and fix it on the side that owns the data.** The backend is the single source of truth for data shape and meaning; the frontend renders what it receives instead of reshaping it. If a field is empty, that should be because the server wrote it empty, not because the UI stripped it. For example, a source with no library version must get `version: null` from `capture_source_versions.py`; it should never be filtered out client-side. Reshaping data in the frontend causes
|
| 106 |
|
| 107 |
## Gotchas
|
| 108 |
|
|
|
|
| 4 |
|
| 5 |
## Project Overview
|
| 6 |
|
| 7 |
+
AI tutor for applied AI, LLMs, RAG, and Python. **Agentic RAG**: a LangChain/LangGraph agent grounds answers in a curated corpus of course + library docs, can browse a local file-based knowledge base, and (optionally) search the live web. One frontend: a **Next.js** UI (`frontend/`), served by a **FastAPI** backend (`scripts/api.py`) that streams in the Vercel AI SDK UI-message protocol. (A Gradio UI existed historically; it was removed to keep one rendering path.)
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
ChromaDB for vectors; Cohere for embeddings/rerank; chat model is provider-configurable (Gemini default, Anthropic, OpenAI). Python ≥3.13, managed with `uv`.
|
| 10 |
|
| 11 |
## Key URLs
|
| 12 |
|
| 13 |
- [GitHub repo](https://github.com/towardsai/ai-tutor-app)
|
| 14 |
+
- [Live demo — prod Space](https://huggingface.co/spaces/towardsai-tutors/ai-tutor-chatbot) · [Dev Space (private)](https://huggingface.co/spaces/towardsai-tutors/ai-tutor)
|
| 15 |
- [Vector DB + KB bundle](https://huggingface.co/datasets/towardsai-tutors/ai-tutor-vector-db) · [Private raw JSONL data](https://huggingface.co/datasets/towardsai-tutors/ai-tutor-data)
|
| 16 |
|
| 17 |
## Where things live
|
|
|
|
| 23 |
| Hybrid retrieval | `scripts/chroma_rag.py` |
|
| 24 |
| KB browsing sandbox + citation resolution | `scripts/kb_shell.py`, `scripts/kb_manifest.py` |
|
| 25 |
| FastAPI server (`/api/chat`, `/api/tools`, `/healthz`) | `scripts/api.py` |
|
|
|
|
| 26 |
| Paths, models, startup downloads | `scripts/setup.py` |
|
| 27 |
| **Sources — single source of truth** | `data/scraping_scripts/source_registry.py` |
|
| 28 |
| Agent tracing (LangSmith) + server logging (stdlib `logging` → stdout) | `scripts/agent_tracing.py`, `scripts/setup.py` |
|
|
|
|
| 31 |
|
| 32 |
## Architecture in brief
|
| 33 |
|
| 34 |
+
The agent is built with `langchain.agents.create_agent()` (LangGraph), an `InMemorySaver` checkpointer keyed by `thread_id`, and middlewares for context-editing, summarization, and source preference. `stream_chat()` is the single entry point the API calls; it yields typed `ChatEvent`s that `scripts/api.py` encodes into the AI SDK UI-message stream. It always exposes two custom tools, plus provider-native web tools when enabled:
|
| 35 |
|
| 36 |
- **`retrieve_tutor_context(query)`** — hybrid RAG over the corpus, scoped to the user's selected sources.
|
| 37 |
- **`run_kb_command(...)`** — read-only KB file browsing (see below).
|
|
|
|
| 59 |
|
| 60 |
## Sources & config
|
| 61 |
|
| 62 |
+
`data/scraping_scripts/source_registry.py` is the **single source of truth** for sources (`SOURCE_CONFIGS`, key groupings, UI labels, defaults); `scripts/setup.py` re-exports them and the frontend derives the picker from it (via `/api/tools`). Docs sources ingest via the GitHub API or `llms.txt`; course sources are Notion exports. To add a source: add it to the registry (+ the relevant grouping tuples), then run the matching workflow — no separate UI edit needed. Models live in `setup.AVAILABLE_MODELS` (default `google-genai:gemini-3.5-flash`; also Claude Haiku 4.5; OpenAI supported in code).
|
| 63 |
|
| 64 |
## Running locally
|
| 65 |
|
| 66 |
```bash
|
| 67 |
uv sync && cp .env.example .env # then fill in keys
|
|
|
|
| 68 |
uv run -m scripts.api # FastAPI backend (:8000; override AI_TUTOR_API_PORT/PORT)
|
| 69 |
# Next.js frontend (needs the API running):
|
| 70 |
cd frontend && npm install && cp .env.example .env.local && npm run dev # :3000
|
|
|
|
| 72 |
|
| 73 |
First start downloads the vector-db/KB bundle from HF if missing (`HF_TOKEN`). The frontend is a static export (`output: 'export'`); `npm run build` emits `frontend/out`, which `scripts/api.py` mounts at `/`.
|
| 74 |
|
| 75 |
+
Test, lint & format: `uv run pytest` · `uv run ruff check .` · `uv run ruff format .`. CI (`.github/workflows/ci.yml`) enforces all three on PRs/pushes to `main`; for local auto-fix on commit, run `uv run pre-commit install` once.
|
| 76 |
|
| 77 |
## Data update workflows
|
| 78 |
|
|
|
|
| 92 |
|
| 93 |
## Deployment
|
| 94 |
|
| 95 |
+
Both HF Spaces run the same image (`Dockerfile`: FastAPI + Next.js static export, `ripgrep` installed for `run_kb_command`, port :7860), in a dev → prod flow:
|
| 96 |
+
|
| 97 |
+
- **Dev — `ai-tutor`** (private): `.github/workflows/sync-to-hf.yml` force-pushes on every push to `main`. Verify changes here first.
|
| 98 |
+
- **Prod — `ai-tutor-chatbot`** (public): `.github/workflows/deploy-prod-to-hf.yml`, **manual trigger only** (Actions tab → "Deploy prod to Hugging Face" → Run workflow).
|
| 99 |
+
|
| 100 |
+
Both Spaces need the same runtime secrets (`COHERE_API_KEY`, model provider key, `HF_TOKEN`, optional `LANGSMITH_*`/`MONGODB_URI`) configured in their HF settings.
|
| 101 |
|
| 102 |
## Conventions
|
| 103 |
|
| 104 |
+
- **No em-dashes in frontend user-facing text.** Use a comma, parentheses, a colon, or two sentences instead. This covers every string the UI renders (Next.js components): tool descriptions, popovers, labels, `title` tooltips, placeholders, empty states. This file and other docs are exempt.
|
| 105 |
+
- **Decide frontend vs. backend ownership before changing behavior, and fix it on the side that owns the data.** The backend is the single source of truth for data shape and meaning; the frontend renders what it receives instead of reshaping it. If a field is empty, that should be because the server wrote it empty, not because the UI stripped it. For example, a source with no library version must get `version: null` from `capture_source_versions.py`; it should never be filtered out client-side. Reshaping data in the frontend causes ambiguity about why a value is missing (did the server omit it, or did the client hide it?).
|
| 106 |
|
| 107 |
## Gotchas
|
| 108 |
|
Dockerfile.gradio
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
FROM python:3.13
|
| 2 |
-
|
| 3 |
-
# ripgrep backs run_kb_command's `rg`; without it the model falls back to find+cat.
|
| 4 |
-
RUN apt-get update \
|
| 5 |
-
&& apt-get install -y --no-install-recommends ripgrep \
|
| 6 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 7 |
-
|
| 8 |
-
# Install uv into /usr/local/bin (on PATH for every user, including HF's uid 1000).
|
| 9 |
-
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
| 10 |
-
&& mv /root/.local/bin/uv /root/.local/bin/uvx /usr/local/bin/ \
|
| 11 |
-
&& rm -rf /root/.local /root/.cache
|
| 12 |
-
|
| 13 |
-
RUN useradd -m -u 1000 user
|
| 14 |
-
WORKDIR /app
|
| 15 |
-
|
| 16 |
-
COPY pyproject.toml uv.lock ./
|
| 17 |
-
RUN uv sync --locked
|
| 18 |
-
|
| 19 |
-
COPY . .
|
| 20 |
-
|
| 21 |
-
ENV HOME=/home/user \
|
| 22 |
-
PORT=7860
|
| 23 |
-
RUN chown -R user:user /app
|
| 24 |
-
USER user
|
| 25 |
-
|
| 26 |
-
EXPOSE 7860
|
| 27 |
-
CMD ["uv", "run", "-m", "scripts.main"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -8,15 +8,15 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
###
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
The
|
| 16 |
|
| 17 |
-
**
|
| 18 |
|
| 19 |
-
###
|
| 20 |
|
| 21 |
1. Install dependencies (requires [uv](https://docs.astral.sh/uv/getting-started/installation/#installation-methods)):
|
| 22 |
|
|
@@ -30,17 +30,9 @@ The Gradio demo is deployed on Hugging Face Spaces at: [AI Tutor Chatbot on Hugg
|
|
| 30 |
cp .env.example .env # then edit values
|
| 31 |
```
|
| 32 |
|
| 33 |
-
The chat model is provider-agnostic
|
| 34 |
To trace requests in LangSmith, set `LANGSMITH_API_KEY`. The app enables tracing automatically when that key is present unless `LANGSMITH_TRACING=false` is set.
|
| 35 |
|
| 36 |
-
3. Run:
|
| 37 |
-
|
| 38 |
-
```bash
|
| 39 |
-
uv run -m scripts.main
|
| 40 |
-
```
|
| 41 |
-
|
| 42 |
-
Starts the Gradio AI Tutor interface.
|
| 43 |
-
|
| 44 |
### LangSmith Agent Tracing
|
| 45 |
|
| 46 |
The chatbot is built with `langchain.agents.create_agent()`, so LangSmith can trace the LangGraph/LangChain run tree without extra dependencies. Add these values to `.env`:
|
|
@@ -57,7 +49,7 @@ Tracing sends prompts, retrieved snippets, tool inputs, tool outputs, and model
|
|
| 57 |
|
| 58 |
### Next.js Frontend — Quick Start
|
| 59 |
|
| 60 |
-
The
|
| 61 |
|
| 62 |
1. Start the Python API:
|
| 63 |
|
|
@@ -102,79 +94,11 @@ This frontend consumes:
|
|
| 102 |
|
| 103 |
and renders sources, tool activity, and reasoning as separate UI elements rather than a single markdown block.
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
The chat endpoint is exposed as `chat`, so the API flow is:
|
| 108 |
-
|
| 109 |
-
```bash
|
| 110 |
-
POST /gradio_api/call/chat
|
| 111 |
-
GET /gradio_api/call/chat/{event_id}
|
| 112 |
-
```
|
| 113 |
-
|
| 114 |
-
The `POST` body must send `data` in this exact order:
|
| 115 |
-
|
| 116 |
-
1. User message: string
|
| 117 |
-
2. History: array
|
| 118 |
-
3. Sources: array of source labels exactly as shown in the UI
|
| 119 |
-
4. Model: `provider:model` string
|
| 120 |
-
5. Show Gemini thoughts: boolean
|
| 121 |
-
6. Thread ID: string, empty for a new conversation
|
| 122 |
-
|
| 123 |
-
Example first turn:
|
| 124 |
-
|
| 125 |
-
```bash
|
| 126 |
-
curl -s http://127.0.0.1:7860/gradio_api/call/chat \
|
| 127 |
-
-H 'Content-Type: application/json' \
|
| 128 |
-
-d '{
|
| 129 |
-
"data": [
|
| 130 |
-
"What is LoRA?",
|
| 131 |
-
[],
|
| 132 |
-
["PEFT Docs", "Transformers Docs"],
|
| 133 |
-
"openai:gpt-4o-mini",
|
| 134 |
-
false,
|
| 135 |
-
""
|
| 136 |
-
]
|
| 137 |
-
}'
|
| 138 |
-
```
|
| 139 |
-
|
| 140 |
-
That returns an `event_id`. Open the server-sent event stream with:
|
| 141 |
-
|
| 142 |
-
```bash
|
| 143 |
-
curl -N http://127.0.0.1:7860/gradio_api/call/chat/<event_id>
|
| 144 |
-
```
|
| 145 |
-
|
| 146 |
-
The streamed payloads look like this:
|
| 147 |
-
|
| 148 |
-
```json
|
| 149 |
-
["Partial assistant text", null, "thread-id"]
|
| 150 |
-
```
|
| 151 |
-
|
| 152 |
-
- `data[0]`: current streamed assistant text
|
| 153 |
-
- `data[1]`: Gradio's hidden state placeholder, ignore this
|
| 154 |
-
- `data[2]`: `thread_id` to reuse on the next turn
|
| 155 |
-
|
| 156 |
-
Example follow-up turn on the same conversation:
|
| 157 |
-
|
| 158 |
-
```bash
|
| 159 |
-
curl -s http://127.0.0.1:7860/gradio_api/call/chat \
|
| 160 |
-
-H 'Content-Type: application/json' \
|
| 161 |
-
-d '{
|
| 162 |
-
"data": [
|
| 163 |
-
"How is it different from adapters?",
|
| 164 |
-
[],
|
| 165 |
-
["PEFT Docs", "Transformers Docs"],
|
| 166 |
-
"openai:gpt-4o-mini",
|
| 167 |
-
false,
|
| 168 |
-
"thread-id-from-previous-response"
|
| 169 |
-
]
|
| 170 |
-
}'
|
| 171 |
-
```
|
| 172 |
-
|
| 173 |
-
Notes:
|
| 174 |
|
| 175 |
-
- API clients should usually send
|
| 176 |
-
- The source filter is request-scoped, so you can keep the same `
|
| 177 |
-
- Sending an empty `
|
| 178 |
|
| 179 |
### Knowledge Base (file-based)
|
| 180 |
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
### AI Tutor Chatbot
|
| 12 |
|
| 13 |
+
An agentic RAG tutor for applied AI, LLMs, RAG, and Python: a Next.js frontend served by a FastAPI backend, with a LangChain/LangGraph agent core. See [AGENTS.md](./AGENTS.md) for the architecture map.
|
| 14 |
|
| 15 |
+
The live app is deployed on Hugging Face Spaces at: [AI Tutor Chatbot on Hugging Face](https://huggingface.co/spaces/towardsai-tutors/ai-tutor-chatbot) (prod).
|
| 16 |
|
| 17 |
+
**Deployment flow:** every push to `main` auto-deploys to the private dev Space ([ai-tutor](https://huggingface.co/spaces/towardsai-tutors/ai-tutor)) for verification; the prod Space is promoted manually via the "Deploy prod to Hugging Face" workflow in the Actions tab.
|
| 18 |
|
| 19 |
+
### Backend — Quick Start
|
| 20 |
|
| 21 |
1. Install dependencies (requires [uv](https://docs.astral.sh/uv/getting-started/installation/#installation-methods)):
|
| 22 |
|
|
|
|
| 30 |
cp .env.example .env # then edit values
|
| 31 |
```
|
| 32 |
|
| 33 |
+
The chat model is provider-agnostic, configured in `provider:model` format, for example `google-genai:gemini-3.5-flash`. Optional provider keys include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and `GOOGLE_API_KEY`. Anthropic support is wired through the `anthropic` SDK, and Gemini support is wired through Google’s `google-genai` SDK.
|
| 34 |
To trace requests in LangSmith, set `LANGSMITH_API_KEY`. The app enables tracing automatically when that key is present unless `LANGSMITH_TRACING=false` is set.
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
### LangSmith Agent Tracing
|
| 37 |
|
| 38 |
The chatbot is built with `langchain.agents.create_agent()`, so LangSmith can trace the LangGraph/LangChain run tree without extra dependencies. Add these values to `.env`:
|
|
|
|
| 49 |
|
| 50 |
### Next.js Frontend — Quick Start
|
| 51 |
|
| 52 |
+
The Next.js frontend in [frontend](./frontend) talks to the FastAPI backend.
|
| 53 |
|
| 54 |
1. Start the Python API:
|
| 55 |
|
|
|
|
| 94 |
|
| 95 |
and renders sources, tool activity, and reasoning as separate UI elements rather than a single markdown block.
|
| 96 |
|
| 97 |
+
API notes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
- API clients should usually send only the new user message and continue the conversation with `threadId` (the `data-thread` part of the stream carries it; send it back on the next request).
|
| 100 |
+
- The source filter is request-scoped, so you can keep the same `threadId` while changing sources between turns.
|
| 101 |
+
- Sending an empty `threadId` starts a new backend conversation.
|
| 102 |
|
| 103 |
### Knowledge Base (file-based)
|
| 104 |
|
data/scraping_scripts/README.md
CHANGED
|
@@ -125,7 +125,7 @@ uv run -m data.scraping_scripts.add_course_workflow --courses master_ai_for_work
|
|
| 125 |
Run the chatbot locally to test if the course has been added correctly.
|
| 126 |
|
| 127 |
```bash
|
| 128 |
-
uv run -m scripts.
|
| 129 |
```
|
| 130 |
|
| 131 |
----
|
|
@@ -284,11 +284,11 @@ leaving the source configured as active for a future rebuild.
|
|
| 284 |
|
| 285 |
3. By default, only new content will have context added to save time and resources. Use `--process-all-context` only if you need to regenerate context for all documents. Use `--skip-data-upload` if you don't want to upload data files to the private HuggingFace repo (they're uploaded by default).
|
| 286 |
|
| 287 |
-
4. When adding a new course, verify that it appears in the
|
| 288 |
- Add the source label and default-selection metadata in `source_registry.py`
|
| 289 |
-
- Check that the new source appears in the
|
| 290 |
- Make sure it's properly included in the default selected sources if desired
|
| 291 |
-
- Restart the
|
| 292 |
|
| 293 |
5. First time setup or missing files:
|
| 294 |
- Both workflows automatically check for and download required data files:
|
|
|
|
| 125 |
Run the chatbot locally to test if the course has been added correctly.
|
| 126 |
|
| 127 |
```bash
|
| 128 |
+
uv run -m scripts.api
|
| 129 |
```
|
| 130 |
|
| 131 |
----
|
|
|
|
| 284 |
|
| 285 |
3. By default, only new content will have context added to save time and resources. Use `--process-all-context` only if you need to regenerate context for all documents. Use `--skip-data-upload` if you don't want to upload data files to the private HuggingFace repo (they're uploaded by default).
|
| 286 |
|
| 287 |
+
4. When adding a new course, verify that it appears in the UI:
|
| 288 |
- Add the source label and default-selection metadata in `source_registry.py`
|
| 289 |
+
- Check that the new source appears in the source picker in the UI
|
| 290 |
- Make sure it's properly included in the default selected sources if desired
|
| 291 |
+
- Restart the API server to see the changes
|
| 292 |
|
| 293 |
5. First time setup or missing files:
|
| 294 |
- Both workflows automatically check for and download required data files:
|
pyproject.toml
CHANGED
|
@@ -10,7 +10,6 @@ dependencies = [
|
|
| 10 |
"cohere",
|
| 11 |
"fastapi",
|
| 12 |
"google-genai",
|
| 13 |
-
"gradio",
|
| 14 |
"hf-xet",
|
| 15 |
"huggingface-hub",
|
| 16 |
"instructor",
|
|
@@ -34,9 +33,14 @@ dependencies = [
|
|
| 34 |
[dependency-groups]
|
| 35 |
dev = [
|
| 36 |
"httpx",
|
|
|
|
| 37 |
"pytest>=9.0.3",
|
| 38 |
"ruff>=0.15.10",
|
| 39 |
]
|
| 40 |
|
| 41 |
[tool.pytest.ini_options]
|
| 42 |
pythonpath = ["."]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"cohere",
|
| 11 |
"fastapi",
|
| 12 |
"google-genai",
|
|
|
|
| 13 |
"hf-xet",
|
| 14 |
"huggingface-hub",
|
| 15 |
"instructor",
|
|
|
|
| 33 |
[dependency-groups]
|
| 34 |
dev = [
|
| 35 |
"httpx",
|
| 36 |
+
"pre-commit",
|
| 37 |
"pytest>=9.0.3",
|
| 38 |
"ruff>=0.15.10",
|
| 39 |
]
|
| 40 |
|
| 41 |
[tool.pytest.ini_options]
|
| 42 |
pythonpath = ["."]
|
| 43 |
+
|
| 44 |
+
[tool.ruff]
|
| 45 |
+
line-length = 88
|
| 46 |
+
target-version = "py313"
|
scripts/chat_service.py
CHANGED
|
@@ -57,19 +57,8 @@ from .setup import (
|
|
| 57 |
|
| 58 |
logger = logging.getLogger(__name__)
|
| 59 |
|
| 60 |
-
SOURCES_HEADER = "📝 Here are the sources I used to answer your question:"
|
| 61 |
-
ACTIVITY_BLOCK_START = "<!-- MODEL_ACTIVITY_START -->"
|
| 62 |
-
ACTIVITY_BLOCK_END = "<!-- MODEL_ACTIVITY_END -->"
|
| 63 |
-
THOUGHTS_BLOCK_START = "<!-- GEMINI_THOUGHTS_START -->"
|
| 64 |
-
THOUGHTS_BLOCK_END = "<!-- GEMINI_THOUGHTS_END -->"
|
| 65 |
-
ANSWER_HEADER = "**Answer**"
|
| 66 |
-
LEGACY_THOUGHTS_DETAILS_OPEN = "<details><summary>Gemini thoughts</summary>"
|
| 67 |
-
LEGACY_THOUGHTS_DETAILS_OPEN_EXPANDED = (
|
| 68 |
-
"<details open><summary>Gemini thoughts</summary>"
|
| 69 |
-
)
|
| 70 |
CHECKPOINTER = InMemorySaver()
|
| 71 |
_RETRIEVER_INIT_LOCK = Lock()
|
| 72 |
-
KB_TOOL_NAMES = ("run_kb_command",)
|
| 73 |
DEFAULT_KB_COMMAND_LIMIT = 20
|
| 74 |
_KB_COMMAND_COUNTS: dict[str, int] = {}
|
| 75 |
_KB_COMMAND_COUNT_LOCK = Lock()
|
|
@@ -246,79 +235,15 @@ def message_content_to_text(content: Any) -> str:
|
|
| 246 |
return str(content)
|
| 247 |
|
| 248 |
|
| 249 |
-
def
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
if marker:
|
| 253 |
-
return body.strip()
|
| 254 |
-
if text.startswith(SOURCES_HEADER):
|
| 255 |
-
return ""
|
| 256 |
-
return text.strip()
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
def strip_hidden_block(text: str, start_marker: str, end_marker: str) -> str:
|
| 260 |
-
stripped = text
|
| 261 |
-
while True:
|
| 262 |
-
start = stripped.find(start_marker)
|
| 263 |
-
if start == -1:
|
| 264 |
-
return stripped
|
| 265 |
-
end = stripped.find(end_marker, start)
|
| 266 |
-
if end == -1:
|
| 267 |
-
return stripped[:start]
|
| 268 |
-
stripped = stripped[:start] + stripped[end + len(end_marker) :]
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
def strip_activity_block(text: str) -> str:
|
| 272 |
-
stripped = strip_hidden_block(text, ACTIVITY_BLOCK_START, ACTIVITY_BLOCK_END)
|
| 273 |
-
stripped = strip_hidden_block(stripped, THOUGHTS_BLOCK_START, THOUGHTS_BLOCK_END)
|
| 274 |
-
|
| 275 |
-
for marker in (
|
| 276 |
-
LEGACY_THOUGHTS_DETAILS_OPEN_EXPANDED,
|
| 277 |
-
LEGACY_THOUGHTS_DETAILS_OPEN,
|
| 278 |
-
):
|
| 279 |
-
separator = f"\n\n{marker}"
|
| 280 |
-
body, found, _ = stripped.partition(separator)
|
| 281 |
-
if found:
|
| 282 |
-
return body.strip()
|
| 283 |
-
if stripped.startswith(marker):
|
| 284 |
-
return ""
|
| 285 |
-
return stripped.strip()
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
def strip_answer_header(text: str) -> str:
|
| 289 |
-
stripped = text.strip()
|
| 290 |
-
answer_prefix = f"{ANSWER_HEADER}\n\n"
|
| 291 |
-
if stripped.startswith(answer_prefix):
|
| 292 |
-
return stripped[len(answer_prefix) :].strip()
|
| 293 |
-
if stripped == ANSWER_HEADER:
|
| 294 |
-
return ""
|
| 295 |
-
return stripped
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
def strip_display_blocks(text: str) -> str:
|
| 299 |
-
return strip_answer_header(strip_activity_block(strip_sources_block(text)))
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
def normalize_history(
|
| 303 |
-
history: list[dict[str, Any]] | tuple[ChatTurn, ...] | list[ChatTurn],
|
| 304 |
-
) -> tuple[ChatTurn, ...]:
|
| 305 |
-
if not history:
|
| 306 |
-
return ()
|
| 307 |
-
|
| 308 |
normalized: list[ChatTurn] = []
|
| 309 |
-
for
|
| 310 |
-
if
|
| 311 |
-
role = message.role
|
| 312 |
-
content = message.content
|
| 313 |
-
else:
|
| 314 |
-
role = str(message.get("role", ""))
|
| 315 |
-
content = message_content_to_text(message.get("content"))
|
| 316 |
-
|
| 317 |
-
if role not in {"user", "assistant"}:
|
| 318 |
continue
|
| 319 |
-
if role == "assistant"
|
| 320 |
-
|
| 321 |
-
normalized.append(ChatTurn(role=role, content=content))
|
| 322 |
return tuple(normalized)
|
| 323 |
|
| 324 |
|
|
@@ -333,7 +258,7 @@ def checkpoint_messages_to_history(messages: list[BaseMessage]) -> tuple[ChatTur
|
|
| 333 |
history.append(
|
| 334 |
ChatTurn(
|
| 335 |
"assistant",
|
| 336 |
-
|
| 337 |
)
|
| 338 |
)
|
| 339 |
return tuple(history)
|
|
@@ -814,7 +739,7 @@ def effective_tool_names(
|
|
| 814 |
model_name: str,
|
| 815 |
enabled_tools: tuple[str, ...],
|
| 816 |
) -> tuple[str, ...]:
|
| 817 |
-
names = ["retrieve_tutor_context",
|
| 818 |
enabled = set(enabled_tools)
|
| 819 |
if is_google_genai_model(model_name):
|
| 820 |
if "web_search" in enabled:
|
|
@@ -928,6 +853,76 @@ def extract_grounding_source_matches(
|
|
| 928 |
|
| 929 |
|
| 930 |
GOOGLE_SEARCH_TOOL_NAME = "google_search"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
ANTHROPIC_SERVER_TOOL_NAMES = frozenset({"web_search", "web_fetch"})
|
| 932 |
ANTHROPIC_RESULT_BLOCK_TYPES = {
|
| 933 |
"web_search_tool_result": ("web_search", "Web"),
|
|
@@ -1057,9 +1052,7 @@ async def stream_chat(request: ChatRequest) -> AsyncIterator[ChatEvent]:
|
|
| 1057 |
include_reasoning = bool(request.include_reasoning) and is_google_genai_model(
|
| 1058 |
request.model_name
|
| 1059 |
)
|
| 1060 |
-
|
| 1061 |
-
google_search_queries: list[str] = []
|
| 1062 |
-
google_search_match_count = 0
|
| 1063 |
|
| 1064 |
logger.info("Running query: %s", request.query)
|
| 1065 |
agent = build_agent(
|
|
@@ -1138,33 +1131,11 @@ async def stream_chat(request: ChatRequest) -> AsyncIterator[ChatEvent]:
|
|
| 1138 |
},
|
| 1139 |
)
|
| 1140 |
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
q
|
| 1144 |
-
for q in extract_web_search_queries(token_metadata)
|
| 1145 |
-
if q not in google_search_queries
|
| 1146 |
-
]
|
| 1147 |
-
new_grounding = extract_grounding_source_matches(
|
| 1148 |
-
token_metadata,
|
| 1149 |
-
web_evidence,
|
| 1150 |
)
|
| 1151 |
-
if
|
| 1152 |
-
|
| 1153 |
-
yield ChatEvent(
|
| 1154 |
-
"tool_call_started",
|
| 1155 |
-
{
|
| 1156 |
-
"message_id": message_id,
|
| 1157 |
-
"call_id": google_search_call_id,
|
| 1158 |
-
"tool_name": GOOGLE_SEARCH_TOOL_NAME,
|
| 1159 |
-
"args": {
|
| 1160 |
-
"query": "; ".join(new_queries) if new_queries else ""
|
| 1161 |
-
},
|
| 1162 |
-
"args_text": "; ".join(new_queries),
|
| 1163 |
-
},
|
| 1164 |
-
)
|
| 1165 |
-
if new_queries:
|
| 1166 |
-
google_search_queries.extend(new_queries)
|
| 1167 |
-
google_search_match_count += len(new_grounding)
|
| 1168 |
continue
|
| 1169 |
|
| 1170 |
if chunk["type"] != "updates":
|
|
@@ -1224,33 +1195,11 @@ async def stream_chat(request: ChatRequest) -> AsyncIterator[ChatEvent]:
|
|
| 1224 |
if step != "model" or getattr(message, "type", None) != "ai":
|
| 1225 |
continue
|
| 1226 |
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
q
|
| 1230 |
-
for q in extract_web_search_queries(message_metadata)
|
| 1231 |
-
if q not in google_search_queries
|
| 1232 |
-
]
|
| 1233 |
-
new_grounding = extract_grounding_source_matches(
|
| 1234 |
-
message_metadata,
|
| 1235 |
-
web_evidence,
|
| 1236 |
)
|
| 1237 |
-
if
|
| 1238 |
-
|
| 1239 |
-
yield ChatEvent(
|
| 1240 |
-
"tool_call_started",
|
| 1241 |
-
{
|
| 1242 |
-
"message_id": message_id,
|
| 1243 |
-
"call_id": google_search_call_id,
|
| 1244 |
-
"tool_name": GOOGLE_SEARCH_TOOL_NAME,
|
| 1245 |
-
"args": {
|
| 1246 |
-
"query": "; ".join(new_queries) if new_queries else ""
|
| 1247 |
-
},
|
| 1248 |
-
"args_text": "; ".join(new_queries),
|
| 1249 |
-
},
|
| 1250 |
-
)
|
| 1251 |
-
if new_queries:
|
| 1252 |
-
google_search_queries.extend(new_queries)
|
| 1253 |
-
google_search_match_count += len(new_grounding)
|
| 1254 |
|
| 1255 |
if is_anthropic_model(request.model_name):
|
| 1256 |
anthropic_updates, anthropic_tool_uses = (
|
|
@@ -1301,24 +1250,9 @@ async def stream_chat(request: ChatRequest) -> AsyncIterator[ChatEvent]:
|
|
| 1301 |
finally:
|
| 1302 |
_clear_kb_command_budget(message_id)
|
| 1303 |
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
| 1307 |
-
output_text = "Google search ran but returned no grounding results."
|
| 1308 |
-
else:
|
| 1309 |
-
plural = "" if google_search_match_count == 1 else "s"
|
| 1310 |
-
output_text = f"Google search returned {google_search_match_count} web result{plural}."
|
| 1311 |
-
yield ChatEvent(
|
| 1312 |
-
"tool_call_completed",
|
| 1313 |
-
{
|
| 1314 |
-
"message_id": message_id,
|
| 1315 |
-
"call_id": google_search_call_id,
|
| 1316 |
-
"tool_name": GOOGLE_SEARCH_TOOL_NAME,
|
| 1317 |
-
"args": {"query": joined_queries},
|
| 1318 |
-
"args_text": joined_queries,
|
| 1319 |
-
"output_text": output_text,
|
| 1320 |
-
},
|
| 1321 |
-
)
|
| 1322 |
|
| 1323 |
answer = "".join(answer_chunks).strip() or completed_answer.strip()
|
| 1324 |
matched_sources = list(
|
|
|
|
| 57 |
|
| 58 |
logger = logging.getLogger(__name__)
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
CHECKPOINTER = InMemorySaver()
|
| 61 |
_RETRIEVER_INIT_LOCK = Lock()
|
|
|
|
| 62 |
DEFAULT_KB_COMMAND_LIMIT = 20
|
| 63 |
_KB_COMMAND_COUNTS: dict[str, int] = {}
|
| 64 |
_KB_COMMAND_COUNT_LOCK = Lock()
|
|
|
|
| 235 |
return str(content)
|
| 236 |
|
| 237 |
|
| 238 |
+
def normalize_history(history: tuple[ChatTurn, ...]) -> tuple[ChatTurn, ...]:
|
| 239 |
+
"""Keep user/assistant turns, trimming assistant whitespace so incoming
|
| 240 |
+
history compares equal to checkpoint-derived history."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
normalized: list[ChatTurn] = []
|
| 242 |
+
for turn in history:
|
| 243 |
+
if turn.role not in {"user", "assistant"}:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
continue
|
| 245 |
+
content = turn.content.strip() if turn.role == "assistant" else turn.content
|
| 246 |
+
normalized.append(ChatTurn(role=turn.role, content=content))
|
|
|
|
| 247 |
return tuple(normalized)
|
| 248 |
|
| 249 |
|
|
|
|
| 258 |
history.append(
|
| 259 |
ChatTurn(
|
| 260 |
"assistant",
|
| 261 |
+
message_content_to_text(message.content).strip(),
|
| 262 |
)
|
| 263 |
)
|
| 264 |
return tuple(history)
|
|
|
|
| 739 |
model_name: str,
|
| 740 |
enabled_tools: tuple[str, ...],
|
| 741 |
) -> tuple[str, ...]:
|
| 742 |
+
names = ["retrieve_tutor_context", "run_kb_command"]
|
| 743 |
enabled = set(enabled_tools)
|
| 744 |
if is_google_genai_model(model_name):
|
| 745 |
if "web_search" in enabled:
|
|
|
|
| 853 |
|
| 854 |
|
| 855 |
GOOGLE_SEARCH_TOOL_NAME = "google_search"
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
class GoogleSearchActivity:
|
| 859 |
+
"""Surface Gemini's server-side google_search activity as tool events.
|
| 860 |
+
|
| 861 |
+
Gemini reports search grounding via response metadata instead of tool
|
| 862 |
+
messages, so queries and grounding results are accumulated from every
|
| 863 |
+
metadata payload and exposed as a single synthetic tool call per turn.
|
| 864 |
+
"""
|
| 865 |
+
|
| 866 |
+
def __init__(self, message_id: str, web_evidence: dict[str, SourceMatch]) -> None:
|
| 867 |
+
self._message_id = message_id
|
| 868 |
+
self._web_evidence = web_evidence
|
| 869 |
+
self._call_id = ""
|
| 870 |
+
self._queries: list[str] = []
|
| 871 |
+
self._match_count = 0
|
| 872 |
+
|
| 873 |
+
def observe(self, response_metadata: Any) -> ChatEvent | None:
|
| 874 |
+
"""Record metadata; return a tool_call_started event on first activity."""
|
| 875 |
+
new_queries = [
|
| 876 |
+
q
|
| 877 |
+
for q in extract_web_search_queries(response_metadata)
|
| 878 |
+
if q not in self._queries
|
| 879 |
+
]
|
| 880 |
+
new_grounding = extract_grounding_source_matches(
|
| 881 |
+
response_metadata,
|
| 882 |
+
self._web_evidence,
|
| 883 |
+
)
|
| 884 |
+
started: ChatEvent | None = None
|
| 885 |
+
if (new_queries or new_grounding) and not self._call_id:
|
| 886 |
+
self._call_id = uuid4().hex
|
| 887 |
+
joined = "; ".join(new_queries)
|
| 888 |
+
started = ChatEvent(
|
| 889 |
+
"tool_call_started",
|
| 890 |
+
{
|
| 891 |
+
"message_id": self._message_id,
|
| 892 |
+
"call_id": self._call_id,
|
| 893 |
+
"tool_name": GOOGLE_SEARCH_TOOL_NAME,
|
| 894 |
+
"args": {"query": joined},
|
| 895 |
+
"args_text": joined,
|
| 896 |
+
},
|
| 897 |
+
)
|
| 898 |
+
self._queries.extend(new_queries)
|
| 899 |
+
self._match_count += len(new_grounding)
|
| 900 |
+
return started
|
| 901 |
+
|
| 902 |
+
def completed_event(self) -> ChatEvent | None:
|
| 903 |
+
if not self._call_id:
|
| 904 |
+
return None
|
| 905 |
+
joined = "; ".join(self._queries)
|
| 906 |
+
if self._match_count == 0:
|
| 907 |
+
output_text = "Google search ran but returned no grounding results."
|
| 908 |
+
else:
|
| 909 |
+
plural = "" if self._match_count == 1 else "s"
|
| 910 |
+
output_text = (
|
| 911 |
+
f"Google search returned {self._match_count} web result{plural}."
|
| 912 |
+
)
|
| 913 |
+
return ChatEvent(
|
| 914 |
+
"tool_call_completed",
|
| 915 |
+
{
|
| 916 |
+
"message_id": self._message_id,
|
| 917 |
+
"call_id": self._call_id,
|
| 918 |
+
"tool_name": GOOGLE_SEARCH_TOOL_NAME,
|
| 919 |
+
"args": {"query": joined},
|
| 920 |
+
"args_text": joined,
|
| 921 |
+
"output_text": output_text,
|
| 922 |
+
},
|
| 923 |
+
)
|
| 924 |
+
|
| 925 |
+
|
| 926 |
ANTHROPIC_SERVER_TOOL_NAMES = frozenset({"web_search", "web_fetch"})
|
| 927 |
ANTHROPIC_RESULT_BLOCK_TYPES = {
|
| 928 |
"web_search_tool_result": ("web_search", "Web"),
|
|
|
|
| 1052 |
include_reasoning = bool(request.include_reasoning) and is_google_genai_model(
|
| 1053 |
request.model_name
|
| 1054 |
)
|
| 1055 |
+
google_search = GoogleSearchActivity(message_id, web_evidence)
|
|
|
|
|
|
|
| 1056 |
|
| 1057 |
logger.info("Running query: %s", request.query)
|
| 1058 |
agent = build_agent(
|
|
|
|
| 1131 |
},
|
| 1132 |
)
|
| 1133 |
|
| 1134 |
+
search_started = google_search.observe(
|
| 1135 |
+
getattr(token, "response_metadata", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1136 |
)
|
| 1137 |
+
if search_started:
|
| 1138 |
+
yield search_started
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1139 |
continue
|
| 1140 |
|
| 1141 |
if chunk["type"] != "updates":
|
|
|
|
| 1195 |
if step != "model" or getattr(message, "type", None) != "ai":
|
| 1196 |
continue
|
| 1197 |
|
| 1198 |
+
search_started = google_search.observe(
|
| 1199 |
+
getattr(message, "response_metadata", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
)
|
| 1201 |
+
if search_started:
|
| 1202 |
+
yield search_started
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
|
| 1204 |
if is_anthropic_model(request.model_name):
|
| 1205 |
anthropic_updates, anthropic_tool_uses = (
|
|
|
|
| 1250 |
finally:
|
| 1251 |
_clear_kb_command_budget(message_id)
|
| 1252 |
|
| 1253 |
+
search_completed = google_search.completed_event()
|
| 1254 |
+
if search_completed:
|
| 1255 |
+
yield search_completed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1256 |
|
| 1257 |
answer = "".join(answer_chunks).strip() or completed_answer.strip()
|
| 1258 |
matched_sources = list(
|
scripts/gradio_presenter.py
DELETED
|
@@ -1,375 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
from dataclasses import dataclass, field
|
| 5 |
-
from typing import Any
|
| 6 |
-
|
| 7 |
-
from .chat_types import ChatEvent
|
| 8 |
-
|
| 9 |
-
SOURCES_HEADER = "📝 Here are the sources I used to answer your question:"
|
| 10 |
-
ACTIVITY_BLOCK_START = "<!-- MODEL_ACTIVITY_START -->"
|
| 11 |
-
ACTIVITY_BLOCK_END = "<!-- MODEL_ACTIVITY_END -->"
|
| 12 |
-
THOUGHTS_HEADER = "**Thinking**"
|
| 13 |
-
THOUGHTS_HINT = "_Reasoning summary from Gemini. This is not the final answer._"
|
| 14 |
-
TOOL_HEADER = "**Tool**"
|
| 15 |
-
TOOL_PENDING_HINT = "_Searching the selected sources..._"
|
| 16 |
-
ANSWER_HEADER = "**Answer**"
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@dataclass
|
| 20 |
-
class ActivityEvent:
|
| 21 |
-
key: str
|
| 22 |
-
kind: str
|
| 23 |
-
body: str
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def as_blockquote(text: str) -> str:
|
| 27 |
-
lines = text.splitlines()
|
| 28 |
-
if not lines:
|
| 29 |
-
return ""
|
| 30 |
-
return "\n".join("> " + line if line else ">" for line in lines)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def merge_stream_text(existing: str, incoming: str) -> str:
|
| 34 |
-
current = existing.strip()
|
| 35 |
-
update = incoming.strip()
|
| 36 |
-
if not update:
|
| 37 |
-
return current
|
| 38 |
-
if not current:
|
| 39 |
-
return update
|
| 40 |
-
if update == current or update in current:
|
| 41 |
-
return current
|
| 42 |
-
if current in update:
|
| 43 |
-
return update
|
| 44 |
-
return f"{current}\n\n{update}"
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def upsert_activity_event(
|
| 48 |
-
events: list[ActivityEvent],
|
| 49 |
-
*,
|
| 50 |
-
key: str,
|
| 51 |
-
kind: str,
|
| 52 |
-
body: str,
|
| 53 |
-
replace: bool = False,
|
| 54 |
-
) -> None:
|
| 55 |
-
if not body.strip():
|
| 56 |
-
return
|
| 57 |
-
|
| 58 |
-
for index, event in enumerate(events):
|
| 59 |
-
if event.key != key:
|
| 60 |
-
continue
|
| 61 |
-
events[index] = ActivityEvent(
|
| 62 |
-
key=key,
|
| 63 |
-
kind=kind,
|
| 64 |
-
body=body.strip() if replace else merge_stream_text(event.body, body),
|
| 65 |
-
)
|
| 66 |
-
return
|
| 67 |
-
|
| 68 |
-
events.append(ActivityEvent(key=key, kind=kind, body=body.strip()))
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def format_tool_args(args: Any, args_text: str = "") -> str:
|
| 72 |
-
query = ""
|
| 73 |
-
url = ""
|
| 74 |
-
command = ""
|
| 75 |
-
if isinstance(args, dict):
|
| 76 |
-
query = str(args.get("query", "")).strip()
|
| 77 |
-
url = str(args.get("url", "")).strip()
|
| 78 |
-
command = str(args.get("command", "")).strip()
|
| 79 |
-
|
| 80 |
-
if command:
|
| 81 |
-
trimmed = command[:157] + "..." if len(command) > 160 else command
|
| 82 |
-
return f"Command: `{trimmed}`"
|
| 83 |
-
|
| 84 |
-
if not query and args_text.strip():
|
| 85 |
-
query = args_text.strip()
|
| 86 |
-
|
| 87 |
-
if query:
|
| 88 |
-
trimmed = query[:157] + "..." if len(query) > 160 else query
|
| 89 |
-
return f'Query: "{trimmed}"'
|
| 90 |
-
|
| 91 |
-
if url:
|
| 92 |
-
trimmed = url[:157] + "..." if len(url) > 160 else url
|
| 93 |
-
return f"URL: `{trimmed}`"
|
| 94 |
-
|
| 95 |
-
if isinstance(args, dict) and args:
|
| 96 |
-
serialized = json.dumps(args, ensure_ascii=False, sort_keys=True)
|
| 97 |
-
trimmed = serialized[:197] + "..." if len(serialized) > 200 else serialized
|
| 98 |
-
return f"Args: `{trimmed}`"
|
| 99 |
-
|
| 100 |
-
if args is None:
|
| 101 |
-
return ""
|
| 102 |
-
serialized = str(args).strip()
|
| 103 |
-
if not serialized:
|
| 104 |
-
return ""
|
| 105 |
-
trimmed = serialized[:197] + "..." if len(serialized) > 200 else serialized
|
| 106 |
-
return f"Args: `{trimmed}`"
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def summarize_activity_sources(sources: list[str], *, max_items: int = 3) -> str:
|
| 110 |
-
if not sources:
|
| 111 |
-
return ""
|
| 112 |
-
if len(sources) <= max_items:
|
| 113 |
-
return ", ".join(sources)
|
| 114 |
-
remaining = len(sources) - max_items
|
| 115 |
-
return f"{', '.join(sources[:max_items])}, and {remaining} more"
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def summarize_tool_result(event: ChatEvent) -> str:
|
| 119 |
-
tool_name = str(event.data.get("tool_name", ""))
|
| 120 |
-
matches = event.data.get("matches", [])
|
| 121 |
-
match_count = len(matches)
|
| 122 |
-
|
| 123 |
-
if tool_name == "retrieve_tutor_context":
|
| 124 |
-
if not matches:
|
| 125 |
-
try:
|
| 126 |
-
payload = json.loads(str(event.data.get("output_text", "")))
|
| 127 |
-
matches = (
|
| 128 |
-
payload.get("matches", []) if isinstance(payload, dict) else []
|
| 129 |
-
)
|
| 130 |
-
match_count = len(matches)
|
| 131 |
-
except json.JSONDecodeError:
|
| 132 |
-
matches = []
|
| 133 |
-
match_count = 0
|
| 134 |
-
if not matches:
|
| 135 |
-
return "_No matching sources found in the selected sources._"
|
| 136 |
-
ordered_sources: list[str] = []
|
| 137 |
-
seen_sources: set[str] = set()
|
| 138 |
-
for match in matches:
|
| 139 |
-
source_label = str(
|
| 140 |
-
match.get("source_label")
|
| 141 |
-
or match.get("source_key")
|
| 142 |
-
or match.get("source")
|
| 143 |
-
or "unknown"
|
| 144 |
-
)
|
| 145 |
-
if source_label in seen_sources:
|
| 146 |
-
continue
|
| 147 |
-
seen_sources.add(source_label)
|
| 148 |
-
ordered_sources.append(source_label)
|
| 149 |
-
source_summary = summarize_activity_sources(ordered_sources)
|
| 150 |
-
match_label = "match" if match_count == 1 else "matches"
|
| 151 |
-
return f"_Found {match_count} {match_label} from {source_summary}._"
|
| 152 |
-
|
| 153 |
-
if tool_name == "run_kb_command":
|
| 154 |
-
output_text = str(event.data.get("output_text", "")).strip()
|
| 155 |
-
command = ""
|
| 156 |
-
exit_code = ""
|
| 157 |
-
for line in output_text.splitlines():
|
| 158 |
-
if line.startswith("$ "):
|
| 159 |
-
command = line[2:].strip()
|
| 160 |
-
elif line.startswith("exit_code:"):
|
| 161 |
-
exit_code = line.partition(":")[2].strip()
|
| 162 |
-
if "error:" in output_text:
|
| 163 |
-
return f"_KB command failed: {output_text.split('error:', 1)[1].strip()[:160]}_"
|
| 164 |
-
if command and exit_code:
|
| 165 |
-
return f"_Ran `{command}` with exit code {exit_code}._"
|
| 166 |
-
if command:
|
| 167 |
-
return f"_Ran `{command}`._"
|
| 168 |
-
return "_Ran KB command._"
|
| 169 |
-
|
| 170 |
-
output_text = str(event.data.get("output_text", "")).strip()
|
| 171 |
-
if output_text:
|
| 172 |
-
return f"_{output_text}_"
|
| 173 |
-
|
| 174 |
-
if tool_name in ("google_search", "web_search"):
|
| 175 |
-
if match_count == 0:
|
| 176 |
-
return "_Search ran but returned no results._"
|
| 177 |
-
label = "result" if match_count == 1 else "results"
|
| 178 |
-
return f"_Searched the web — {match_count} {label} used as sources._"
|
| 179 |
-
|
| 180 |
-
if tool_name in ("url_context", "web_fetch"):
|
| 181 |
-
if match_count == 0:
|
| 182 |
-
return "_URL fetched._"
|
| 183 |
-
label = "page" if match_count == 1 else "pages"
|
| 184 |
-
return f"_Fetched {match_count} {label}._"
|
| 185 |
-
|
| 186 |
-
return "_Tool completed._"
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
def format_tool_event(
|
| 190 |
-
tool_name: str,
|
| 191 |
-
args: Any,
|
| 192 |
-
status_line: str,
|
| 193 |
-
*,
|
| 194 |
-
args_text: str = "",
|
| 195 |
-
) -> str:
|
| 196 |
-
lines = [f"Using `{tool_name}`"]
|
| 197 |
-
args_line = format_tool_args(args, args_text=args_text)
|
| 198 |
-
if args_line:
|
| 199 |
-
lines.append(args_line)
|
| 200 |
-
lines.append(status_line)
|
| 201 |
-
return "\n".join(lines)
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
def render_activity_block(events: list[ActivityEvent]) -> str:
|
| 205 |
-
sections: list[str] = []
|
| 206 |
-
for event in events:
|
| 207 |
-
if event.kind == "thinking":
|
| 208 |
-
sections.append(
|
| 209 |
-
"\n".join(
|
| 210 |
-
[
|
| 211 |
-
THOUGHTS_HEADER,
|
| 212 |
-
THOUGHTS_HINT,
|
| 213 |
-
"",
|
| 214 |
-
as_blockquote(event.body),
|
| 215 |
-
]
|
| 216 |
-
).strip()
|
| 217 |
-
)
|
| 218 |
-
continue
|
| 219 |
-
if event.kind == "tool":
|
| 220 |
-
sections.append(f"{TOOL_HEADER}\n{event.body}".strip())
|
| 221 |
-
|
| 222 |
-
if not sections:
|
| 223 |
-
return ""
|
| 224 |
-
|
| 225 |
-
rendered_sections = "\n\n".join(section for section in sections if section)
|
| 226 |
-
return f"{ACTIVITY_BLOCK_START}\n\n{rendered_sections}\n\n{ACTIVITY_BLOCK_END}"
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def format_sources(matches_by_doc_id: dict[str, dict[str, Any]]) -> str:
|
| 230 |
-
if not matches_by_doc_id:
|
| 231 |
-
return ""
|
| 232 |
-
|
| 233 |
-
lines = [SOURCES_HEADER]
|
| 234 |
-
sorted_matches = sorted(
|
| 235 |
-
matches_by_doc_id.values(),
|
| 236 |
-
key=lambda item: item["score"],
|
| 237 |
-
reverse=True,
|
| 238 |
-
)
|
| 239 |
-
for match in sorted_matches:
|
| 240 |
-
lines.append(
|
| 241 |
-
f"- [🔗 {match['source_label']}: {match['title']}]({match['url']}), relevance: {match['score']:.2f}"
|
| 242 |
-
)
|
| 243 |
-
return "\n".join(lines)
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def render_output(
|
| 247 |
-
answer: str,
|
| 248 |
-
activity_block: str = "",
|
| 249 |
-
sources_block: str = "",
|
| 250 |
-
) -> str:
|
| 251 |
-
visible_answer = answer.strip()
|
| 252 |
-
visible_activity = activity_block.strip()
|
| 253 |
-
visible_sources = sources_block.strip()
|
| 254 |
-
|
| 255 |
-
output_parts = [visible_activity]
|
| 256 |
-
if visible_answer:
|
| 257 |
-
if visible_activity:
|
| 258 |
-
output_parts.append(f"{ANSWER_HEADER}\n\n{visible_answer}")
|
| 259 |
-
else:
|
| 260 |
-
output_parts.append(visible_answer)
|
| 261 |
-
output_parts.append(visible_sources)
|
| 262 |
-
return "\n\n".join(part for part in output_parts if part)
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
@dataclass
|
| 266 |
-
class GradioPresenterState:
|
| 267 |
-
show_activity: bool = False
|
| 268 |
-
thread_id: str = ""
|
| 269 |
-
message_completed: bool = False
|
| 270 |
-
matches_by_doc_id: dict[str, dict[str, Any]] = field(default_factory=dict)
|
| 271 |
-
activity_events: list[ActivityEvent] = field(default_factory=list)
|
| 272 |
-
answer_chunks: list[str] = field(default_factory=list)
|
| 273 |
-
completed_answer: str = ""
|
| 274 |
-
tool_matches_by_call_id: dict[str, list[dict[str, Any]]] = field(
|
| 275 |
-
default_factory=dict
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
def apply(self, event: ChatEvent) -> None:
|
| 279 |
-
if event.type == "thread_started":
|
| 280 |
-
self.thread_id = str(event.data.get("thread_id", self.thread_id))
|
| 281 |
-
return
|
| 282 |
-
|
| 283 |
-
if event.type == "text_delta":
|
| 284 |
-
text = str(event.data.get("text", ""))
|
| 285 |
-
if text:
|
| 286 |
-
self.answer_chunks.append(text)
|
| 287 |
-
return
|
| 288 |
-
|
| 289 |
-
if event.type == "reasoning_delta":
|
| 290 |
-
if not self.show_activity:
|
| 291 |
-
return
|
| 292 |
-
step = str(event.data.get("step", ""))
|
| 293 |
-
body = str(event.data.get("text", ""))
|
| 294 |
-
upsert_activity_event(
|
| 295 |
-
self.activity_events,
|
| 296 |
-
key=f"thinking:{step}",
|
| 297 |
-
kind="thinking",
|
| 298 |
-
body=body,
|
| 299 |
-
)
|
| 300 |
-
return
|
| 301 |
-
|
| 302 |
-
if event.type == "tool_call_started":
|
| 303 |
-
if not self.show_activity:
|
| 304 |
-
return
|
| 305 |
-
call_id = str(event.data.get("call_id", ""))
|
| 306 |
-
upsert_activity_event(
|
| 307 |
-
self.activity_events,
|
| 308 |
-
key=f"tool:{call_id}",
|
| 309 |
-
kind="tool",
|
| 310 |
-
body=format_tool_event(
|
| 311 |
-
str(event.data.get("tool_name", "tool")),
|
| 312 |
-
event.data.get("args"),
|
| 313 |
-
TOOL_PENDING_HINT,
|
| 314 |
-
args_text=str(event.data.get("args_text", "")),
|
| 315 |
-
),
|
| 316 |
-
replace=True,
|
| 317 |
-
)
|
| 318 |
-
return
|
| 319 |
-
|
| 320 |
-
if event.type == "source_match":
|
| 321 |
-
doc_id = str(event.data.get("doc_id", ""))
|
| 322 |
-
existing = self.matches_by_doc_id.get(doc_id)
|
| 323 |
-
incoming_score = float(event.data.get("score", 0.0))
|
| 324 |
-
if existing and float(existing["score"]) >= incoming_score:
|
| 325 |
-
return
|
| 326 |
-
source_data = {
|
| 327 |
-
"title": str(event.data.get("title", "")),
|
| 328 |
-
"url": str(event.data.get("url", "")),
|
| 329 |
-
"source_label": str(event.data.get("source_label", "")),
|
| 330 |
-
"score": incoming_score,
|
| 331 |
-
}
|
| 332 |
-
self.matches_by_doc_id[doc_id] = source_data
|
| 333 |
-
|
| 334 |
-
call_id = str(event.data.get("call_id", ""))
|
| 335 |
-
if call_id:
|
| 336 |
-
self.tool_matches_by_call_id.setdefault(call_id, []).append(source_data)
|
| 337 |
-
return
|
| 338 |
-
|
| 339 |
-
if event.type == "tool_call_completed":
|
| 340 |
-
if not self.show_activity:
|
| 341 |
-
return
|
| 342 |
-
call_id = str(event.data.get("call_id", ""))
|
| 343 |
-
matches = self.tool_matches_by_call_id.get(call_id, [])
|
| 344 |
-
event_with_matches = ChatEvent(
|
| 345 |
-
type=event.type,
|
| 346 |
-
data={**event.data, "matches": matches},
|
| 347 |
-
)
|
| 348 |
-
upsert_activity_event(
|
| 349 |
-
self.activity_events,
|
| 350 |
-
key=f"tool:{call_id}",
|
| 351 |
-
kind="tool",
|
| 352 |
-
body=format_tool_event(
|
| 353 |
-
str(event.data.get("tool_name", "tool")),
|
| 354 |
-
event.data.get("args"),
|
| 355 |
-
summarize_tool_result(event_with_matches),
|
| 356 |
-
args_text=str(event.data.get("args_text", "")),
|
| 357 |
-
),
|
| 358 |
-
replace=True,
|
| 359 |
-
)
|
| 360 |
-
return
|
| 361 |
-
|
| 362 |
-
if event.type == "message_completed":
|
| 363 |
-
self.message_completed = True
|
| 364 |
-
self.thread_id = str(event.data.get("thread_id", self.thread_id))
|
| 365 |
-
self.completed_answer = str(event.data.get("answer", "")).strip()
|
| 366 |
-
|
| 367 |
-
def render(self) -> str:
|
| 368 |
-
answer = "".join(self.answer_chunks).strip() or self.completed_answer
|
| 369 |
-
activity_block = (
|
| 370 |
-
render_activity_block(self.activity_events) if self.show_activity else ""
|
| 371 |
-
)
|
| 372 |
-
sources_block = (
|
| 373 |
-
format_sources(self.matches_by_doc_id) if self.message_completed else ""
|
| 374 |
-
)
|
| 375 |
-
return render_output(answer, activity_block, sources_block)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/main.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import gradio as gr
|
| 4 |
-
|
| 5 |
-
from .chat_service import (
|
| 6 |
-
ChatRequest,
|
| 7 |
-
normalize_history,
|
| 8 |
-
stream_chat,
|
| 9 |
-
warm_up_retriever,
|
| 10 |
-
)
|
| 11 |
-
from .gradio_presenter import GradioPresenterState
|
| 12 |
-
from .setup import (
|
| 13 |
-
AVAILABLE_SOURCES_UI,
|
| 14 |
-
DEFAULT_MODEL_NAME,
|
| 15 |
-
DEFAULT_SELECTED_SOURCES_UI,
|
| 16 |
-
SOURCE_UI_TO_KEY,
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
async def generate_completion(
|
| 21 |
-
query: str,
|
| 22 |
-
history,
|
| 23 |
-
sources,
|
| 24 |
-
model,
|
| 25 |
-
thread_id,
|
| 26 |
-
enable_web_search,
|
| 27 |
-
enable_url_read,
|
| 28 |
-
):
|
| 29 |
-
enabled_tools: list[str] = []
|
| 30 |
-
if enable_web_search:
|
| 31 |
-
enabled_tools.append("web_search")
|
| 32 |
-
if enable_url_read:
|
| 33 |
-
enabled_tools.extend(["url_context", "web_fetch"])
|
| 34 |
-
request = ChatRequest(
|
| 35 |
-
query=query,
|
| 36 |
-
history=normalize_history(history),
|
| 37 |
-
source_keys=tuple(
|
| 38 |
-
SOURCE_UI_TO_KEY[source] for source in sources if source in SOURCE_UI_TO_KEY
|
| 39 |
-
),
|
| 40 |
-
model_name=model,
|
| 41 |
-
include_reasoning=True,
|
| 42 |
-
thread_id=thread_id,
|
| 43 |
-
enabled_tools=tuple(enabled_tools),
|
| 44 |
-
)
|
| 45 |
-
presenter = GradioPresenterState(show_activity=True)
|
| 46 |
-
last_emitted = ""
|
| 47 |
-
|
| 48 |
-
async for event in stream_chat(request):
|
| 49 |
-
presenter.apply(event)
|
| 50 |
-
current_output = presenter.render()
|
| 51 |
-
if current_output and current_output != last_emitted:
|
| 52 |
-
last_emitted = current_output
|
| 53 |
-
yield current_output, presenter.thread_id
|
| 54 |
-
|
| 55 |
-
final_output = presenter.render()
|
| 56 |
-
if final_output and final_output != last_emitted:
|
| 57 |
-
yield final_output, presenter.thread_id
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
accordion = gr.Accordion(label="Customize Sources (Click to expand)", open=False)
|
| 61 |
-
sources = gr.CheckboxGroup(
|
| 62 |
-
AVAILABLE_SOURCES_UI,
|
| 63 |
-
label="Sources",
|
| 64 |
-
value=DEFAULT_SELECTED_SOURCES_UI,
|
| 65 |
-
interactive=True,
|
| 66 |
-
)
|
| 67 |
-
model = gr.Textbox(
|
| 68 |
-
label="Model (provider:model)",
|
| 69 |
-
value=DEFAULT_MODEL_NAME,
|
| 70 |
-
interactive=False,
|
| 71 |
-
placeholder="openai:gpt-5.4-mini | anthropic:claude-opus-4-6 | google-genai:gemini-3.5-flash",
|
| 72 |
-
)
|
| 73 |
-
enable_web_search = gr.Checkbox(
|
| 74 |
-
label="Web search",
|
| 75 |
-
value=True,
|
| 76 |
-
info="Let the model use its built-in web search (Gemini google_search / Claude web_search).",
|
| 77 |
-
)
|
| 78 |
-
enable_url_read = gr.Checkbox(
|
| 79 |
-
label="URL read",
|
| 80 |
-
value=True,
|
| 81 |
-
info="Let the model fetch URLs (Gemini url_context / Claude web_fetch).",
|
| 82 |
-
)
|
| 83 |
-
thread_id = gr.Textbox(
|
| 84 |
-
label="Thread ID",
|
| 85 |
-
value="",
|
| 86 |
-
visible=False,
|
| 87 |
-
container=False,
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
-
with gr.Blocks(
|
| 91 |
-
title="Towards AI 🤖",
|
| 92 |
-
analytics_enabled=True,
|
| 93 |
-
fill_height=True,
|
| 94 |
-
) as demo:
|
| 95 |
-
|
| 96 |
-
def reset_thread_id():
|
| 97 |
-
return ""
|
| 98 |
-
|
| 99 |
-
chatbot = gr.Chatbot(
|
| 100 |
-
scale=20,
|
| 101 |
-
placeholder="<strong>Towards AI 🤖: A Question-Answering Bot for anything AI-related</strong><br>",
|
| 102 |
-
show_label=False,
|
| 103 |
-
buttons=["copy"],
|
| 104 |
-
)
|
| 105 |
-
chatbot.clear(
|
| 106 |
-
reset_thread_id,
|
| 107 |
-
None,
|
| 108 |
-
[thread_id],
|
| 109 |
-
api_visibility="undocumented",
|
| 110 |
-
queue=False,
|
| 111 |
-
)
|
| 112 |
-
chatbot.undo(
|
| 113 |
-
reset_thread_id,
|
| 114 |
-
None,
|
| 115 |
-
[thread_id],
|
| 116 |
-
api_visibility="undocumented",
|
| 117 |
-
queue=False,
|
| 118 |
-
)
|
| 119 |
-
chatbot.retry(
|
| 120 |
-
reset_thread_id,
|
| 121 |
-
None,
|
| 122 |
-
[thread_id],
|
| 123 |
-
api_visibility="undocumented",
|
| 124 |
-
queue=False,
|
| 125 |
-
)
|
| 126 |
-
chatbot.edit(
|
| 127 |
-
reset_thread_id,
|
| 128 |
-
None,
|
| 129 |
-
[thread_id],
|
| 130 |
-
api_visibility="undocumented",
|
| 131 |
-
queue=False,
|
| 132 |
-
)
|
| 133 |
-
gr.ChatInterface(
|
| 134 |
-
fn=generate_completion,
|
| 135 |
-
chatbot=chatbot,
|
| 136 |
-
additional_inputs=[
|
| 137 |
-
sources,
|
| 138 |
-
model,
|
| 139 |
-
thread_id,
|
| 140 |
-
enable_web_search,
|
| 141 |
-
enable_url_read,
|
| 142 |
-
],
|
| 143 |
-
additional_outputs=[thread_id],
|
| 144 |
-
additional_inputs_accordion=accordion,
|
| 145 |
-
api_name="chat",
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
if __name__ == "__main__":
|
| 150 |
-
warm_up_retriever()
|
| 151 |
-
demo.launch(server_name="0.0.0.0", server_port=7860, debug=False, share=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/manual_e2e_langsmith.md
CHANGED
|
@@ -51,13 +51,6 @@ uv run dotenv -f .env run -- env RUN_LIVE_API_E2E=1 \
|
|
| 51 |
pytest tests/test_api.py::test_live_api_stream_exposes_frontend_parts -q
|
| 52 |
```
|
| 53 |
|
| 54 |
-
Gradio curl path:
|
| 55 |
-
|
| 56 |
-
```bash
|
| 57 |
-
uv run dotenv -f .env run -- env RUN_LIVE_GRADIO_CURL_E2E=1 \
|
| 58 |
-
pytest tests/test_gradio_kb_e2e.py::test_live_gradio_curl_can_use_retrieval_and_shell -q
|
| 59 |
-
```
|
| 60 |
-
|
| 61 |
## Manual API Test
|
| 62 |
|
| 63 |
Start the FastAPI app in one terminal:
|
|
@@ -168,59 +161,6 @@ Expected result:
|
|
| 168 |
- The answer has inline citations, not only a final sources list.
|
| 169 |
- `data-source` parts include the source cards the frontend will render.
|
| 170 |
|
| 171 |
-
## Manual Gradio Test
|
| 172 |
-
|
| 173 |
-
Start the Gradio app in one terminal:
|
| 174 |
-
|
| 175 |
-
```bash
|
| 176 |
-
uv run dotenv -f .env run -- python -m scripts.main
|
| 177 |
-
```
|
| 178 |
-
|
| 179 |
-
Submit a Gradio job:
|
| 180 |
-
|
| 181 |
-
```bash
|
| 182 |
-
cat >/tmp/ai_tutor_gradio_payload.json <<'JSON'
|
| 183 |
-
{
|
| 184 |
-
"data": [
|
| 185 |
-
"Use run_kb_command to answer: can you tell me about codex?",
|
| 186 |
-
[],
|
| 187 |
-
[
|
| 188 |
-
"Agentic AI Engineering",
|
| 189 |
-
"LangChain Docs",
|
| 190 |
-
"LangGraph Docs",
|
| 191 |
-
"PEFT Docs",
|
| 192 |
-
"Transformers Docs"
|
| 193 |
-
],
|
| 194 |
-
"google-genai:gemini-3.5-flash",
|
| 195 |
-
"",
|
| 196 |
-
false,
|
| 197 |
-
false
|
| 198 |
-
]
|
| 199 |
-
}
|
| 200 |
-
JSON
|
| 201 |
-
|
| 202 |
-
curl -s http://127.0.0.1:7860/gradio_api/call/chat \
|
| 203 |
-
-H "Content-Type: application/json" \
|
| 204 |
-
-d @/tmp/ai_tutor_gradio_payload.json \
|
| 205 |
-
-o /tmp/ai_tutor_gradio_post.json
|
| 206 |
-
```
|
| 207 |
-
|
| 208 |
-
Stream the result:
|
| 209 |
-
|
| 210 |
-
```bash
|
| 211 |
-
EVENT_ID="$(jq -r '.event_id' /tmp/ai_tutor_gradio_post.json)"
|
| 212 |
-
|
| 213 |
-
curl -N --max-time 240 \
|
| 214 |
-
"http://127.0.0.1:7860/gradio_api/call/chat/${EVENT_ID}" \
|
| 215 |
-
-o /tmp/ai_tutor_gradio_stream.sse
|
| 216 |
-
```
|
| 217 |
-
|
| 218 |
-
Expected result:
|
| 219 |
-
|
| 220 |
-
- The rendered text contains `Using \`retrieve_tutor_context\``.
|
| 221 |
-
- The rendered text contains `Using \`run_kb_command\``.
|
| 222 |
-
- The rendered text contains inline citations and source cards.
|
| 223 |
-
|
| 224 |
## Find The LangSmith Trace
|
| 225 |
|
| 226 |
List recent traces in the project:
|
|
|
|
| 51 |
pytest tests/test_api.py::test_live_api_stream_exposes_frontend_parts -q
|
| 52 |
```
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
## Manual API Test
|
| 55 |
|
| 56 |
Start the FastAPI app in one terminal:
|
|
|
|
| 161 |
- The answer has inline citations, not only a final sources list.
|
| 162 |
- `data-source` parts include the source cards the frontend will render.
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
## Find The LangSmith Trace
|
| 165 |
|
| 166 |
List recent traces in the project:
|
tests/test_gradio_kb_e2e.py
DELETED
|
@@ -1,209 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import socket
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
import pytest
|
| 7 |
-
from gradio_client import Client
|
| 8 |
-
|
| 9 |
-
from scripts.chat_types import ChatEvent
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def free_port() -> int:
|
| 13 |
-
with socket.socket() as sock:
|
| 14 |
-
sock.bind(("127.0.0.1", 0))
|
| 15 |
-
return int(sock.getsockname()[1])
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def test_gradio_chat_api_surfaces_shell_and_retrieval_activity(monkeypatch) -> None:
|
| 19 |
-
import scripts.main as main
|
| 20 |
-
|
| 21 |
-
async def fake_stream_chat(_request):
|
| 22 |
-
yield ChatEvent("thread_started", {"thread_id": "thread_kb"})
|
| 23 |
-
yield ChatEvent("message_started", {"message_id": "message_kb"})
|
| 24 |
-
yield ChatEvent(
|
| 25 |
-
"tool_call_started",
|
| 26 |
-
{
|
| 27 |
-
"message_id": "message_kb",
|
| 28 |
-
"call_id": "call_search",
|
| 29 |
-
"tool_name": "retrieve_tutor_context",
|
| 30 |
-
"args": {"query": "generate_next_queries_tool"},
|
| 31 |
-
"args_text": "generate_next_queries_tool",
|
| 32 |
-
},
|
| 33 |
-
)
|
| 34 |
-
yield ChatEvent(
|
| 35 |
-
"tool_call_completed",
|
| 36 |
-
{
|
| 37 |
-
"message_id": "message_kb",
|
| 38 |
-
"call_id": "call_search",
|
| 39 |
-
"tool_name": "retrieve_tutor_context",
|
| 40 |
-
"args": {"query": "generate_next_queries_tool"},
|
| 41 |
-
"args_text": "generate_next_queries_tool",
|
| 42 |
-
"output_text": (
|
| 43 |
-
'{"matches": [{"doc_id": "agentic_ai_engineering:lesson-18", '
|
| 44 |
-
'"title": "Lesson 18: Research Loop", '
|
| 45 |
-
'"url": "https://academy.towardsai.net/lesson-18", '
|
| 46 |
-
'"source": "agentic_ai_engineering", "score": 10.0}]}'
|
| 47 |
-
),
|
| 48 |
-
},
|
| 49 |
-
)
|
| 50 |
-
yield ChatEvent(
|
| 51 |
-
"tool_call_started",
|
| 52 |
-
{
|
| 53 |
-
"message_id": "message_kb",
|
| 54 |
-
"call_id": "call_rg",
|
| 55 |
-
"tool_name": "run_kb_command",
|
| 56 |
-
"args": {"command": "rg generate_next_queries_tool raw"},
|
| 57 |
-
"args_text": "rg generate_next_queries_tool raw",
|
| 58 |
-
},
|
| 59 |
-
)
|
| 60 |
-
yield ChatEvent(
|
| 61 |
-
"tool_call_completed",
|
| 62 |
-
{
|
| 63 |
-
"message_id": "message_kb",
|
| 64 |
-
"call_id": "call_rg",
|
| 65 |
-
"tool_name": "run_kb_command",
|
| 66 |
-
"args": {"command": "rg generate_next_queries_tool raw"},
|
| 67 |
-
"args_text": "rg generate_next_queries_tool raw",
|
| 68 |
-
"output_text": "$ rg generate_next_queries_tool raw\ncwd: data/kb\nexit_code: 0\nstdout:\nraw/courses/agentic_ai_engineering/lesson-18.md:4:Call `generate_next_queries_tool`.",
|
| 69 |
-
},
|
| 70 |
-
)
|
| 71 |
-
yield ChatEvent(
|
| 72 |
-
"text_delta",
|
| 73 |
-
{
|
| 74 |
-
"message_id": "message_kb",
|
| 75 |
-
"text": "The symbol appears in Lesson 18: Research Loop.",
|
| 76 |
-
},
|
| 77 |
-
)
|
| 78 |
-
yield ChatEvent(
|
| 79 |
-
"source_match",
|
| 80 |
-
{
|
| 81 |
-
"message_id": "message_kb",
|
| 82 |
-
"doc_id": "agentic_ai_engineering:lesson-18",
|
| 83 |
-
"title": "Lesson 18: Research Loop",
|
| 84 |
-
"url": "https://academy.towardsai.net/lesson-18",
|
| 85 |
-
"source_key": "agentic_ai_engineering",
|
| 86 |
-
"source_label": "Agentic AI Engineering",
|
| 87 |
-
"score": 10.0,
|
| 88 |
-
"group": "courses",
|
| 89 |
-
},
|
| 90 |
-
)
|
| 91 |
-
yield ChatEvent(
|
| 92 |
-
"message_completed",
|
| 93 |
-
{
|
| 94 |
-
"message_id": "message_kb",
|
| 95 |
-
"thread_id": "thread_kb",
|
| 96 |
-
"answer": "The symbol appears in Lesson 18: Research Loop.",
|
| 97 |
-
},
|
| 98 |
-
)
|
| 99 |
-
|
| 100 |
-
monkeypatch.setattr(main, "stream_chat", fake_stream_chat)
|
| 101 |
-
port = free_port()
|
| 102 |
-
main.demo.launch(
|
| 103 |
-
server_name="127.0.0.1",
|
| 104 |
-
server_port=port,
|
| 105 |
-
prevent_thread_lock=True,
|
| 106 |
-
quiet=True,
|
| 107 |
-
)
|
| 108 |
-
try:
|
| 109 |
-
client = Client(f"http://127.0.0.1:{port}")
|
| 110 |
-
result = client.predict(
|
| 111 |
-
"Where is generate_next_queries_tool discussed?",
|
| 112 |
-
[],
|
| 113 |
-
["Agentic AI Engineering"],
|
| 114 |
-
"google-genai:gemini-3.5-flash",
|
| 115 |
-
"",
|
| 116 |
-
False,
|
| 117 |
-
False,
|
| 118 |
-
api_name="/chat",
|
| 119 |
-
)
|
| 120 |
-
finally:
|
| 121 |
-
main.demo.close()
|
| 122 |
-
|
| 123 |
-
rendered = result[0] if isinstance(result, tuple) else str(result)
|
| 124 |
-
assert "Using `retrieve_tutor_context`" in rendered
|
| 125 |
-
assert "Using `run_kb_command`" in rendered
|
| 126 |
-
assert "rg generate_next_queries_tool raw" in rendered
|
| 127 |
-
assert "Lesson 18: Research Loop" in rendered
|
| 128 |
-
assert "Agentic AI Engineering" in rendered
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
@pytest.mark.skipif(
|
| 132 |
-
os.getenv("RUN_LIVE_GRADIO_CURL_E2E") != "1",
|
| 133 |
-
reason="Set RUN_LIVE_GRADIO_CURL_E2E=1 to run the live model-backed Gradio curl smoke test.",
|
| 134 |
-
)
|
| 135 |
-
def test_live_gradio_curl_can_use_retrieval_and_shell() -> None:
|
| 136 |
-
if os.getenv("RUN_LIVE_GRADIO_CURL_E2E") != "1":
|
| 137 |
-
pytest.skip("Set RUN_LIVE_GRADIO_CURL_E2E=1 to run this test.")
|
| 138 |
-
import scripts.main as main
|
| 139 |
-
|
| 140 |
-
import json
|
| 141 |
-
import shutil
|
| 142 |
-
import subprocess
|
| 143 |
-
|
| 144 |
-
if shutil.which("curl") is None:
|
| 145 |
-
pytest.skip("curl is required for the live Gradio curl smoke test.")
|
| 146 |
-
if not os.getenv("COHERE_API_KEY"):
|
| 147 |
-
pytest.skip("COHERE_API_KEY is required for live retrieval fallback.")
|
| 148 |
-
if not (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")):
|
| 149 |
-
pytest.skip("GEMINI_API_KEY or GOOGLE_API_KEY is required for the live model.")
|
| 150 |
-
if not os.path.exists("data/kb/wiki/index.md"):
|
| 151 |
-
pytest.skip("data/kb artifacts must exist before running the live smoke test.")
|
| 152 |
-
|
| 153 |
-
port = free_port()
|
| 154 |
-
main.demo.launch(
|
| 155 |
-
server_name="127.0.0.1",
|
| 156 |
-
server_port=port,
|
| 157 |
-
prevent_thread_lock=True,
|
| 158 |
-
quiet=True,
|
| 159 |
-
)
|
| 160 |
-
try:
|
| 161 |
-
post = subprocess.run(
|
| 162 |
-
[
|
| 163 |
-
"curl",
|
| 164 |
-
"-s",
|
| 165 |
-
f"http://127.0.0.1:{port}/gradio_api/call/chat",
|
| 166 |
-
"-H",
|
| 167 |
-
"Content-Type: application/json",
|
| 168 |
-
"-d",
|
| 169 |
-
json.dumps(
|
| 170 |
-
{
|
| 171 |
-
"data": [
|
| 172 |
-
"Use both retrieve_tutor_context and run_kb_command to answer: how does PEFT configure LoRA with LoraConfig?",
|
| 173 |
-
[],
|
| 174 |
-
["PEFT Docs", "Transformers Docs"],
|
| 175 |
-
os.getenv(
|
| 176 |
-
"LIVE_GRADIO_E2E_MODEL", "google-genai:gemini-3.5-flash"
|
| 177 |
-
),
|
| 178 |
-
"",
|
| 179 |
-
False,
|
| 180 |
-
False,
|
| 181 |
-
]
|
| 182 |
-
}
|
| 183 |
-
),
|
| 184 |
-
],
|
| 185 |
-
check=True,
|
| 186 |
-
capture_output=True,
|
| 187 |
-
text=True,
|
| 188 |
-
timeout=30,
|
| 189 |
-
)
|
| 190 |
-
event_id = json.loads(post.stdout)["event_id"]
|
| 191 |
-
stream = subprocess.run(
|
| 192 |
-
[
|
| 193 |
-
"curl",
|
| 194 |
-
"-N",
|
| 195 |
-
f"http://127.0.0.1:{port}/gradio_api/call/chat/{event_id}",
|
| 196 |
-
],
|
| 197 |
-
check=True,
|
| 198 |
-
capture_output=True,
|
| 199 |
-
text=True,
|
| 200 |
-
timeout=180,
|
| 201 |
-
)
|
| 202 |
-
finally:
|
| 203 |
-
main.demo.close()
|
| 204 |
-
|
| 205 |
-
rendered = stream.stdout
|
| 206 |
-
assert "Using `retrieve_tutor_context`" in rendered
|
| 207 |
-
assert "Using `run_kb_command`" in rendered
|
| 208 |
-
assert "LoRA" in rendered
|
| 209 |
-
assert "PEFT" in rendered or "Transformers" in rendered
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_gradio_presenter.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import unittest
|
| 4 |
-
|
| 5 |
-
from scripts.chat_types import ChatEvent
|
| 6 |
-
from scripts.gradio_presenter import (
|
| 7 |
-
GradioPresenterState,
|
| 8 |
-
SOURCES_HEADER,
|
| 9 |
-
summarize_tool_result,
|
| 10 |
-
)
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class GradioPresenterTestCase(unittest.TestCase):
|
| 14 |
-
def test_sources_render_only_after_message_completion(self) -> None:
|
| 15 |
-
presenter = GradioPresenterState(show_activity=True)
|
| 16 |
-
|
| 17 |
-
presenter.apply(ChatEvent("thread_started", {"thread_id": "thread_1"}))
|
| 18 |
-
presenter.apply(ChatEvent("text_delta", {"message_id": "m1", "text": "Answer"}))
|
| 19 |
-
presenter.apply(
|
| 20 |
-
ChatEvent(
|
| 21 |
-
"source_match",
|
| 22 |
-
{
|
| 23 |
-
"message_id": "m1",
|
| 24 |
-
"call_id": "c1",
|
| 25 |
-
"doc_id": "doc_1",
|
| 26 |
-
"title": "RAG overview",
|
| 27 |
-
"url": "https://example.com/rag",
|
| 28 |
-
"source_label": "LangChain Docs",
|
| 29 |
-
"score": 0.92,
|
| 30 |
-
},
|
| 31 |
-
)
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
in_progress = presenter.render()
|
| 35 |
-
self.assertIn("Answer", in_progress)
|
| 36 |
-
self.assertNotIn(SOURCES_HEADER, in_progress)
|
| 37 |
-
|
| 38 |
-
presenter.apply(
|
| 39 |
-
ChatEvent(
|
| 40 |
-
"message_completed",
|
| 41 |
-
{
|
| 42 |
-
"message_id": "m1",
|
| 43 |
-
"thread_id": "thread_1",
|
| 44 |
-
"answer": "Answer",
|
| 45 |
-
},
|
| 46 |
-
)
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
-
completed = presenter.render()
|
| 50 |
-
self.assertIn(SOURCES_HEADER, completed)
|
| 51 |
-
self.assertIn("RAG overview", completed)
|
| 52 |
-
|
| 53 |
-
def test_retrieval_summary_uses_tool_payload_matches(self) -> None:
|
| 54 |
-
event = ChatEvent(
|
| 55 |
-
"tool_call_completed",
|
| 56 |
-
{
|
| 57 |
-
"tool_name": "retrieve_tutor_context",
|
| 58 |
-
"output_text": (
|
| 59 |
-
'{"matches": [{"title": "LoRA", "source_label": "PEFT Docs"}]}'
|
| 60 |
-
),
|
| 61 |
-
},
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
self.assertEqual(
|
| 65 |
-
summarize_tool_result(event),
|
| 66 |
-
"_Found 1 match from PEFT Docs._",
|
| 67 |
-
)
|
| 68 |
-
|
| 69 |
-
def test_run_kb_command_summary_confirms_command_execution(self) -> None:
|
| 70 |
-
event = ChatEvent(
|
| 71 |
-
"tool_call_completed",
|
| 72 |
-
{
|
| 73 |
-
"tool_name": "run_kb_command",
|
| 74 |
-
"output_text": (
|
| 75 |
-
"$ rg LoraConfig raw\n"
|
| 76 |
-
"cwd: data/kb\n"
|
| 77 |
-
"exit_code: 0\n"
|
| 78 |
-
"stdout:\n"
|
| 79 |
-
"raw/docs/peft/lora.md:3:LoraConfig"
|
| 80 |
-
),
|
| 81 |
-
},
|
| 82 |
-
)
|
| 83 |
-
|
| 84 |
-
self.assertEqual(
|
| 85 |
-
summarize_tool_result(event),
|
| 86 |
-
"_Ran `rg LoraConfig raw` with exit code 0._",
|
| 87 |
-
)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
if __name__ == "__main__":
|
| 91 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uv.lock
CHANGED
|
@@ -20,7 +20,6 @@ dependencies = [
|
|
| 20 |
{ name = "cohere" },
|
| 21 |
{ name = "fastapi" },
|
| 22 |
{ name = "google-genai" },
|
| 23 |
-
{ name = "gradio" },
|
| 24 |
{ name = "hf-xet" },
|
| 25 |
{ name = "huggingface-hub" },
|
| 26 |
{ name = "instructor" },
|
|
@@ -44,6 +43,7 @@ dependencies = [
|
|
| 44 |
[package.dev-dependencies]
|
| 45 |
dev = [
|
| 46 |
{ name = "httpx" },
|
|
|
|
| 47 |
{ name = "pytest" },
|
| 48 |
{ name = "ruff" },
|
| 49 |
]
|
|
@@ -55,7 +55,6 @@ requires-dist = [
|
|
| 55 |
{ name = "cohere" },
|
| 56 |
{ name = "fastapi" },
|
| 57 |
{ name = "google-genai" },
|
| 58 |
-
{ name = "gradio" },
|
| 59 |
{ name = "hf-xet" },
|
| 60 |
{ name = "huggingface-hub" },
|
| 61 |
{ name = "instructor" },
|
|
@@ -79,6 +78,7 @@ requires-dist = [
|
|
| 79 |
[package.metadata.requires-dev]
|
| 80 |
dev = [
|
| 81 |
{ name = "httpx" },
|
|
|
|
| 82 |
{ name = "pytest", specifier = ">=9.0.3" },
|
| 83 |
{ name = "ruff", specifier = ">=0.15.10" },
|
| 84 |
]
|
|
@@ -270,62 +270,6 @@ wheels = [
|
|
| 270 |
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
| 271 |
]
|
| 272 |
|
| 273 |
-
[[package]]
|
| 274 |
-
name = "audioop-lts"
|
| 275 |
-
version = "0.2.2"
|
| 276 |
-
source = { registry = "https://pypi.org/simple" }
|
| 277 |
-
sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" }
|
| 278 |
-
wheels = [
|
| 279 |
-
{ url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" },
|
| 280 |
-
{ url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" },
|
| 281 |
-
{ url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" },
|
| 282 |
-
{ url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" },
|
| 283 |
-
{ url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" },
|
| 284 |
-
{ url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" },
|
| 285 |
-
{ url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" },
|
| 286 |
-
{ url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" },
|
| 287 |
-
{ url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" },
|
| 288 |
-
{ url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" },
|
| 289 |
-
{ url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" },
|
| 290 |
-
{ url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" },
|
| 291 |
-
{ url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" },
|
| 292 |
-
{ url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" },
|
| 293 |
-
{ url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" },
|
| 294 |
-
{ url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" },
|
| 295 |
-
{ url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" },
|
| 296 |
-
{ url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" },
|
| 297 |
-
{ url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" },
|
| 298 |
-
{ url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" },
|
| 299 |
-
{ url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" },
|
| 300 |
-
{ url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" },
|
| 301 |
-
{ url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" },
|
| 302 |
-
{ url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" },
|
| 303 |
-
{ url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" },
|
| 304 |
-
{ url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" },
|
| 305 |
-
{ url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" },
|
| 306 |
-
{ url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" },
|
| 307 |
-
{ url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" },
|
| 308 |
-
{ url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" },
|
| 309 |
-
{ url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" },
|
| 310 |
-
{ url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" },
|
| 311 |
-
{ url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" },
|
| 312 |
-
{ url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" },
|
| 313 |
-
{ url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" },
|
| 314 |
-
{ url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" },
|
| 315 |
-
{ url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" },
|
| 316 |
-
{ url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" },
|
| 317 |
-
{ url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" },
|
| 318 |
-
{ url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" },
|
| 319 |
-
{ url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" },
|
| 320 |
-
{ url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" },
|
| 321 |
-
{ url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" },
|
| 322 |
-
{ url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" },
|
| 323 |
-
{ url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" },
|
| 324 |
-
{ url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" },
|
| 325 |
-
{ url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" },
|
| 326 |
-
{ url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
|
| 327 |
-
]
|
| 328 |
-
|
| 329 |
[[package]]
|
| 330 |
name = "banks"
|
| 331 |
version = "2.4.2"
|
|
@@ -439,34 +383,6 @@ css = [
|
|
| 439 |
{ name = "tinycss2" },
|
| 440 |
]
|
| 441 |
|
| 442 |
-
[[package]]
|
| 443 |
-
name = "brotli"
|
| 444 |
-
version = "1.2.0"
|
| 445 |
-
source = { registry = "https://pypi.org/simple" }
|
| 446 |
-
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
|
| 447 |
-
wheels = [
|
| 448 |
-
{ url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
|
| 449 |
-
{ url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
|
| 450 |
-
{ url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
|
| 451 |
-
{ url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
|
| 452 |
-
{ url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
|
| 453 |
-
{ url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
|
| 454 |
-
{ url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
|
| 455 |
-
{ url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
|
| 456 |
-
{ url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
|
| 457 |
-
{ url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
|
| 458 |
-
{ url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
|
| 459 |
-
{ url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
|
| 460 |
-
{ url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
|
| 461 |
-
{ url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
|
| 462 |
-
{ url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
|
| 463 |
-
{ url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
|
| 464 |
-
{ url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
|
| 465 |
-
{ url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
|
| 466 |
-
{ url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
|
| 467 |
-
{ url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
|
| 468 |
-
]
|
| 469 |
-
|
| 470 |
[[package]]
|
| 471 |
name = "build"
|
| 472 |
version = "1.5.0"
|
|
@@ -535,6 +451,15 @@ wheels = [
|
|
| 535 |
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
| 536 |
]
|
| 537 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
[[package]]
|
| 539 |
name = "charset-normalizer"
|
| 540 |
version = "3.4.7"
|
|
@@ -805,6 +730,15 @@ wheels = [
|
|
| 805 |
{ url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" },
|
| 806 |
]
|
| 807 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
[[package]]
|
| 809 |
name = "distro"
|
| 810 |
version = "1.9.0"
|
|
@@ -1066,61 +1000,6 @@ wheels = [
|
|
| 1066 |
{ url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" },
|
| 1067 |
]
|
| 1068 |
|
| 1069 |
-
[[package]]
|
| 1070 |
-
name = "gradio"
|
| 1071 |
-
version = "6.16.0"
|
| 1072 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1073 |
-
dependencies = [
|
| 1074 |
-
{ name = "anyio" },
|
| 1075 |
-
{ name = "audioop-lts" },
|
| 1076 |
-
{ name = "brotli" },
|
| 1077 |
-
{ name = "fastapi" },
|
| 1078 |
-
{ name = "gradio-client" },
|
| 1079 |
-
{ name = "groovy" },
|
| 1080 |
-
{ name = "hf-gradio" },
|
| 1081 |
-
{ name = "httpx" },
|
| 1082 |
-
{ name = "huggingface-hub" },
|
| 1083 |
-
{ name = "jinja2" },
|
| 1084 |
-
{ name = "markupsafe" },
|
| 1085 |
-
{ name = "numpy" },
|
| 1086 |
-
{ name = "orjson" },
|
| 1087 |
-
{ name = "packaging" },
|
| 1088 |
-
{ name = "pandas" },
|
| 1089 |
-
{ name = "pillow" },
|
| 1090 |
-
{ name = "pydantic" },
|
| 1091 |
-
{ name = "pydub" },
|
| 1092 |
-
{ name = "python-multipart" },
|
| 1093 |
-
{ name = "pytz" },
|
| 1094 |
-
{ name = "pyyaml" },
|
| 1095 |
-
{ name = "safehttpx" },
|
| 1096 |
-
{ name = "semantic-version" },
|
| 1097 |
-
{ name = "starlette" },
|
| 1098 |
-
{ name = "tomlkit" },
|
| 1099 |
-
{ name = "typer" },
|
| 1100 |
-
{ name = "typing-extensions" },
|
| 1101 |
-
{ name = "uvicorn" },
|
| 1102 |
-
]
|
| 1103 |
-
sdist = { url = "https://files.pythonhosted.org/packages/a3/26/44044687cdd16d0d9d9137c8f9d445eb68391b14583c82a6c2d528da952c/gradio-6.16.0.tar.gz", hash = "sha256:a48d03ffb436644265e4386388ad8a50ec183c8e1fdd3bae0548f7f229c474da", size = 36441812, upload-time = "2026-06-03T19:07:12.973Z" }
|
| 1104 |
-
wheels = [
|
| 1105 |
-
{ url = "https://files.pythonhosted.org/packages/2a/12/ac98683fa45d84c2c96c14acdb040aa746209089ea045c766f380549ecec/gradio-6.16.0-py3-none-any.whl", hash = "sha256:a5de01bf17336bf421d52af4b5dbd9fdbd530762c485067748456ccb797700bb", size = 20097732, upload-time = "2026-06-03T19:07:09.904Z" },
|
| 1106 |
-
]
|
| 1107 |
-
|
| 1108 |
-
[[package]]
|
| 1109 |
-
name = "gradio-client"
|
| 1110 |
-
version = "2.5.0"
|
| 1111 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1112 |
-
dependencies = [
|
| 1113 |
-
{ name = "fsspec" },
|
| 1114 |
-
{ name = "httpx" },
|
| 1115 |
-
{ name = "huggingface-hub" },
|
| 1116 |
-
{ name = "packaging" },
|
| 1117 |
-
{ name = "typing-extensions" },
|
| 1118 |
-
]
|
| 1119 |
-
sdist = { url = "https://files.pythonhosted.org/packages/e8/e6/6b6029f5fe2ad7f1211105d530e34d991014c2cae463f9223033031cfc4f/gradio_client-2.5.0.tar.gz", hash = "sha256:4cde99bad62149595c30c90876ca2e405e3a13687ecf895474f3412cb476673d", size = 59013, upload-time = "2026-04-20T23:16:21.518Z" }
|
| 1120 |
-
wheels = [
|
| 1121 |
-
{ url = "https://files.pythonhosted.org/packages/78/81/0a861b8e1ff42960139c6cd4c7dd591292fa09ea1ae2d87677441cba4c00/gradio_client-2.5.0-py3-none-any.whl", hash = "sha256:d43e2179c29076292a76485ad7ed2e6eaa19d14ac58283bd7f5beabfe4ca958c", size = 59952, upload-time = "2026-04-20T23:16:20.186Z" },
|
| 1122 |
-
]
|
| 1123 |
-
|
| 1124 |
[[package]]
|
| 1125 |
name = "greenlet"
|
| 1126 |
version = "3.5.1"
|
|
@@ -1213,15 +1092,6 @@ wheels = [
|
|
| 1213 |
{ url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" },
|
| 1214 |
]
|
| 1215 |
|
| 1216 |
-
[[package]]
|
| 1217 |
-
name = "groovy"
|
| 1218 |
-
version = "0.1.2"
|
| 1219 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1220 |
-
sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" }
|
| 1221 |
-
wheels = [
|
| 1222 |
-
{ url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" },
|
| 1223 |
-
]
|
| 1224 |
-
|
| 1225 |
[[package]]
|
| 1226 |
name = "grpcio"
|
| 1227 |
version = "1.81.0"
|
|
@@ -1262,19 +1132,6 @@ wheels = [
|
|
| 1262 |
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
| 1263 |
]
|
| 1264 |
|
| 1265 |
-
[[package]]
|
| 1266 |
-
name = "hf-gradio"
|
| 1267 |
-
version = "0.4.1"
|
| 1268 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1269 |
-
dependencies = [
|
| 1270 |
-
{ name = "gradio-client" },
|
| 1271 |
-
{ name = "typer" },
|
| 1272 |
-
]
|
| 1273 |
-
sdist = { url = "https://files.pythonhosted.org/packages/ce/86/c9694b7cfada5780e75769e60dc161a161f4dd7fc91b61db5e3a3338bef9/hf_gradio-0.4.1.tar.gz", hash = "sha256:a017d942618f0d495a58ee4563047fa04bef614c00e0cb789a9a6d0633cffa7b", size = 6560, upload-time = "2026-04-22T14:01:32.334Z" }
|
| 1274 |
-
wheels = [
|
| 1275 |
-
{ url = "https://files.pythonhosted.org/packages/30/2d/afff2ee87e75d8eb85c92bb8cf0e15b05c23c2ebd8fd8dec781d8601ed7f/hf_gradio-0.4.1-py3-none-any.whl", hash = "sha256:76b8cb8be6abe62d74c1ad2d35b42f0629db89aa9e1a8d033cecfe7c856eeab3", size = 4482, upload-time = "2026-04-17T19:53:31.827Z" },
|
| 1276 |
-
]
|
| 1277 |
-
|
| 1278 |
[[package]]
|
| 1279 |
name = "hf-xet"
|
| 1280 |
version = "1.5.0"
|
|
@@ -1385,6 +1242,15 @@ wheels = [
|
|
| 1385 |
{ url = "https://files.pythonhosted.org/packages/02/28/d7cef5e477b855c25d415b8f57e5bc7347c7a90cad3acf1725d0c92ca294/huggingface_hub-1.17.0-py3-none-any.whl", hash = "sha256:3b8156d23118e87f6a587648bfbc04f04a12a757ccb4ed298b35c4ae638bf24c", size = 671546, upload-time = "2026-05-28T15:12:11.441Z" },
|
| 1386 |
]
|
| 1387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1388 |
[[package]]
|
| 1389 |
name = "idna"
|
| 1390 |
version = "3.18"
|
|
@@ -2309,6 +2175,15 @@ wheels = [
|
|
| 2309 |
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
|
| 2310 |
]
|
| 2311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2312 |
[[package]]
|
| 2313 |
name = "numpy"
|
| 2314 |
version = "2.4.6"
|
|
@@ -2581,50 +2456,6 @@ wheels = [
|
|
| 2581 |
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
| 2582 |
]
|
| 2583 |
|
| 2584 |
-
[[package]]
|
| 2585 |
-
name = "pandas"
|
| 2586 |
-
version = "3.0.3"
|
| 2587 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2588 |
-
dependencies = [
|
| 2589 |
-
{ name = "numpy" },
|
| 2590 |
-
{ name = "python-dateutil" },
|
| 2591 |
-
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
| 2592 |
-
]
|
| 2593 |
-
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
|
| 2594 |
-
wheels = [
|
| 2595 |
-
{ url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
|
| 2596 |
-
{ url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
|
| 2597 |
-
{ url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
|
| 2598 |
-
{ url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
|
| 2599 |
-
{ url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
|
| 2600 |
-
{ url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
|
| 2601 |
-
{ url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
|
| 2602 |
-
{ url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
|
| 2603 |
-
{ url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
|
| 2604 |
-
{ url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
|
| 2605 |
-
{ url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
|
| 2606 |
-
{ url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
|
| 2607 |
-
{ url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
|
| 2608 |
-
{ url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
|
| 2609 |
-
{ url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
|
| 2610 |
-
{ url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
|
| 2611 |
-
{ url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
|
| 2612 |
-
{ url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
|
| 2613 |
-
{ url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
|
| 2614 |
-
{ url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
|
| 2615 |
-
{ url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
|
| 2616 |
-
{ url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
|
| 2617 |
-
{ url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
|
| 2618 |
-
{ url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
|
| 2619 |
-
{ url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
|
| 2620 |
-
{ url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
|
| 2621 |
-
{ url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
|
| 2622 |
-
{ url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
|
| 2623 |
-
{ url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
|
| 2624 |
-
{ url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
|
| 2625 |
-
{ url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
|
| 2626 |
-
]
|
| 2627 |
-
|
| 2628 |
[[package]]
|
| 2629 |
name = "pandocfilters"
|
| 2630 |
version = "1.5.1"
|
|
@@ -2731,6 +2562,22 @@ wheels = [
|
|
| 2731 |
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
| 2732 |
]
|
| 2733 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2734 |
[[package]]
|
| 2735 |
name = "prompt-toolkit"
|
| 2736 |
version = "3.0.52"
|
|
@@ -3090,15 +2937,6 @@ wheels = [
|
|
| 3090 |
{ url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" },
|
| 3091 |
]
|
| 3092 |
|
| 3093 |
-
[[package]]
|
| 3094 |
-
name = "pydub"
|
| 3095 |
-
version = "0.25.1"
|
| 3096 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3097 |
-
sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
|
| 3098 |
-
wheels = [
|
| 3099 |
-
{ url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
|
| 3100 |
-
]
|
| 3101 |
-
|
| 3102 |
[[package]]
|
| 3103 |
name = "pygments"
|
| 3104 |
version = "2.20.0"
|
|
@@ -3196,30 +3034,25 @@ wheels = [
|
|
| 3196 |
]
|
| 3197 |
|
| 3198 |
[[package]]
|
| 3199 |
-
name = "python-
|
| 3200 |
-
version = "1.
|
| 3201 |
source = { registry = "https://pypi.org/simple" }
|
| 3202 |
-
|
| 3203 |
-
|
| 3204 |
-
{
|
| 3205 |
]
|
| 3206 |
-
|
| 3207 |
-
[[package]]
|
| 3208 |
-
name = "python-multipart"
|
| 3209 |
-
version = "0.0.31"
|
| 3210 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3211 |
-
sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" }
|
| 3212 |
wheels = [
|
| 3213 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 3214 |
]
|
| 3215 |
|
| 3216 |
[[package]]
|
| 3217 |
-
name = "
|
| 3218 |
-
version = "
|
| 3219 |
source = { registry = "https://pypi.org/simple" }
|
| 3220 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 3221 |
wheels = [
|
| 3222 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 3223 |
]
|
| 3224 |
|
| 3225 |
[[package]]
|
|
@@ -3559,27 +3392,6 @@ wheels = [
|
|
| 3559 |
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
| 3560 |
]
|
| 3561 |
|
| 3562 |
-
[[package]]
|
| 3563 |
-
name = "safehttpx"
|
| 3564 |
-
version = "0.1.7"
|
| 3565 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3566 |
-
dependencies = [
|
| 3567 |
-
{ name = "httpx" },
|
| 3568 |
-
]
|
| 3569 |
-
sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" }
|
| 3570 |
-
wheels = [
|
| 3571 |
-
{ url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" },
|
| 3572 |
-
]
|
| 3573 |
-
|
| 3574 |
-
[[package]]
|
| 3575 |
-
name = "semantic-version"
|
| 3576 |
-
version = "2.10.0"
|
| 3577 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3578 |
-
sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" }
|
| 3579 |
-
wheels = [
|
| 3580 |
-
{ url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
|
| 3581 |
-
]
|
| 3582 |
-
|
| 3583 |
[[package]]
|
| 3584 |
name = "setuptools"
|
| 3585 |
version = "82.0.1"
|
|
@@ -3787,15 +3599,6 @@ wheels = [
|
|
| 3787 |
{ url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" },
|
| 3788 |
]
|
| 3789 |
|
| 3790 |
-
[[package]]
|
| 3791 |
-
name = "tomlkit"
|
| 3792 |
-
version = "0.14.0"
|
| 3793 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3794 |
-
sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
|
| 3795 |
-
wheels = [
|
| 3796 |
-
{ url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
|
| 3797 |
-
]
|
| 3798 |
-
|
| 3799 |
[[package]]
|
| 3800 |
name = "tornado"
|
| 3801 |
version = "6.5.6"
|
|
@@ -3895,15 +3698,6 @@ wheels = [
|
|
| 3895 |
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
| 3896 |
]
|
| 3897 |
|
| 3898 |
-
[[package]]
|
| 3899 |
-
name = "tzdata"
|
| 3900 |
-
version = "2026.2"
|
| 3901 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3902 |
-
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
| 3903 |
-
wheels = [
|
| 3904 |
-
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
| 3905 |
-
]
|
| 3906 |
-
|
| 3907 |
[[package]]
|
| 3908 |
name = "urllib3"
|
| 3909 |
version = "2.7.0"
|
|
@@ -4027,6 +3821,21 @@ wheels = [
|
|
| 4027 |
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
| 4028 |
]
|
| 4029 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4030 |
[[package]]
|
| 4031 |
name = "watchfiles"
|
| 4032 |
version = "1.2.0"
|
|
|
|
| 20 |
{ name = "cohere" },
|
| 21 |
{ name = "fastapi" },
|
| 22 |
{ name = "google-genai" },
|
|
|
|
| 23 |
{ name = "hf-xet" },
|
| 24 |
{ name = "huggingface-hub" },
|
| 25 |
{ name = "instructor" },
|
|
|
|
| 43 |
[package.dev-dependencies]
|
| 44 |
dev = [
|
| 45 |
{ name = "httpx" },
|
| 46 |
+
{ name = "pre-commit" },
|
| 47 |
{ name = "pytest" },
|
| 48 |
{ name = "ruff" },
|
| 49 |
]
|
|
|
|
| 55 |
{ name = "cohere" },
|
| 56 |
{ name = "fastapi" },
|
| 57 |
{ name = "google-genai" },
|
|
|
|
| 58 |
{ name = "hf-xet" },
|
| 59 |
{ name = "huggingface-hub" },
|
| 60 |
{ name = "instructor" },
|
|
|
|
| 78 |
[package.metadata.requires-dev]
|
| 79 |
dev = [
|
| 80 |
{ name = "httpx" },
|
| 81 |
+
{ name = "pre-commit" },
|
| 82 |
{ name = "pytest", specifier = ">=9.0.3" },
|
| 83 |
{ name = "ruff", specifier = ">=0.15.10" },
|
| 84 |
]
|
|
|
|
| 270 |
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
| 271 |
]
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
[[package]]
|
| 274 |
name = "banks"
|
| 275 |
version = "2.4.2"
|
|
|
|
| 383 |
{ name = "tinycss2" },
|
| 384 |
]
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
[[package]]
|
| 387 |
name = "build"
|
| 388 |
version = "1.5.0"
|
|
|
|
| 451 |
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
| 452 |
]
|
| 453 |
|
| 454 |
+
[[package]]
|
| 455 |
+
name = "cfgv"
|
| 456 |
+
version = "3.5.0"
|
| 457 |
+
source = { registry = "https://pypi.org/simple" }
|
| 458 |
+
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
| 459 |
+
wheels = [
|
| 460 |
+
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
| 461 |
+
]
|
| 462 |
+
|
| 463 |
[[package]]
|
| 464 |
name = "charset-normalizer"
|
| 465 |
version = "3.4.7"
|
|
|
|
| 730 |
{ url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" },
|
| 731 |
]
|
| 732 |
|
| 733 |
+
[[package]]
|
| 734 |
+
name = "distlib"
|
| 735 |
+
version = "0.4.2"
|
| 736 |
+
source = { registry = "https://pypi.org/simple" }
|
| 737 |
+
sdist = { url = "https://files.pythonhosted.org/packages/46/8d/873e9252ea2c0e0c857884e0a2899ec43ade132345df1925ef24cbe64f18/distlib-0.4.2.tar.gz", hash = "sha256:baeb401c90f27acd15c4861ae0847d1e731c27ac3dbf4210643ba61fa1e813db", size = 614914, upload-time = "2026-06-08T16:24:15.439Z" }
|
| 738 |
+
wheels = [
|
| 739 |
+
{ url = "https://files.pythonhosted.org/packages/c1/60/aa891c893821d4d127292ed66c6940d1d715894bd5a0ce048056bc641773/distlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:ca4cb11e5d746b5ec13c199cbf19ae27a241f89702b54e153a74332955446067", size = 470510, upload-time = "2026-06-08T16:24:13.208Z" },
|
| 740 |
+
]
|
| 741 |
+
|
| 742 |
[[package]]
|
| 743 |
name = "distro"
|
| 744 |
version = "1.9.0"
|
|
|
|
| 1000 |
{ url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" },
|
| 1001 |
]
|
| 1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
[[package]]
|
| 1004 |
name = "greenlet"
|
| 1005 |
version = "3.5.1"
|
|
|
|
| 1092 |
{ url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" },
|
| 1093 |
]
|
| 1094 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
[[package]]
|
| 1096 |
name = "grpcio"
|
| 1097 |
version = "1.81.0"
|
|
|
|
| 1132 |
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
| 1133 |
]
|
| 1134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
[[package]]
|
| 1136 |
name = "hf-xet"
|
| 1137 |
version = "1.5.0"
|
|
|
|
| 1242 |
{ url = "https://files.pythonhosted.org/packages/02/28/d7cef5e477b855c25d415b8f57e5bc7347c7a90cad3acf1725d0c92ca294/huggingface_hub-1.17.0-py3-none-any.whl", hash = "sha256:3b8156d23118e87f6a587648bfbc04f04a12a757ccb4ed298b35c4ae638bf24c", size = 671546, upload-time = "2026-05-28T15:12:11.441Z" },
|
| 1243 |
]
|
| 1244 |
|
| 1245 |
+
[[package]]
|
| 1246 |
+
name = "identify"
|
| 1247 |
+
version = "2.6.19"
|
| 1248 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1249 |
+
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
|
| 1250 |
+
wheels = [
|
| 1251 |
+
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
|
| 1252 |
+
]
|
| 1253 |
+
|
| 1254 |
[[package]]
|
| 1255 |
name = "idna"
|
| 1256 |
version = "3.18"
|
|
|
|
| 2175 |
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
|
| 2176 |
]
|
| 2177 |
|
| 2178 |
+
[[package]]
|
| 2179 |
+
name = "nodeenv"
|
| 2180 |
+
version = "1.10.0"
|
| 2181 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2182 |
+
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
| 2183 |
+
wheels = [
|
| 2184 |
+
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
| 2185 |
+
]
|
| 2186 |
+
|
| 2187 |
[[package]]
|
| 2188 |
name = "numpy"
|
| 2189 |
version = "2.4.6"
|
|
|
|
| 2456 |
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
| 2457 |
]
|
| 2458 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2459 |
[[package]]
|
| 2460 |
name = "pandocfilters"
|
| 2461 |
version = "1.5.1"
|
|
|
|
| 2562 |
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
| 2563 |
]
|
| 2564 |
|
| 2565 |
+
[[package]]
|
| 2566 |
+
name = "pre-commit"
|
| 2567 |
+
version = "4.6.0"
|
| 2568 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2569 |
+
dependencies = [
|
| 2570 |
+
{ name = "cfgv" },
|
| 2571 |
+
{ name = "identify" },
|
| 2572 |
+
{ name = "nodeenv" },
|
| 2573 |
+
{ name = "pyyaml" },
|
| 2574 |
+
{ name = "virtualenv" },
|
| 2575 |
+
]
|
| 2576 |
+
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
|
| 2577 |
+
wheels = [
|
| 2578 |
+
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
|
| 2579 |
+
]
|
| 2580 |
+
|
| 2581 |
[[package]]
|
| 2582 |
name = "prompt-toolkit"
|
| 2583 |
version = "3.0.52"
|
|
|
|
| 2937 |
{ url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" },
|
| 2938 |
]
|
| 2939 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2940 |
[[package]]
|
| 2941 |
name = "pygments"
|
| 2942 |
version = "2.20.0"
|
|
|
|
| 3034 |
]
|
| 3035 |
|
| 3036 |
[[package]]
|
| 3037 |
+
name = "python-discovery"
|
| 3038 |
+
version = "1.4.0"
|
| 3039 |
source = { registry = "https://pypi.org/simple" }
|
| 3040 |
+
dependencies = [
|
| 3041 |
+
{ name = "filelock" },
|
| 3042 |
+
{ name = "platformdirs" },
|
| 3043 |
]
|
| 3044 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3045 |
wheels = [
|
| 3046 |
+
{ url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" },
|
| 3047 |
]
|
| 3048 |
|
| 3049 |
[[package]]
|
| 3050 |
+
name = "python-dotenv"
|
| 3051 |
+
version = "1.2.2"
|
| 3052 |
source = { registry = "https://pypi.org/simple" }
|
| 3053 |
+
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
| 3054 |
wheels = [
|
| 3055 |
+
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
| 3056 |
]
|
| 3057 |
|
| 3058 |
[[package]]
|
|
|
|
| 3392 |
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
| 3393 |
]
|
| 3394 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3395 |
[[package]]
|
| 3396 |
name = "setuptools"
|
| 3397 |
version = "82.0.1"
|
|
|
|
| 3599 |
{ url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" },
|
| 3600 |
]
|
| 3601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3602 |
[[package]]
|
| 3603 |
name = "tornado"
|
| 3604 |
version = "6.5.6"
|
|
|
|
| 3698 |
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
| 3699 |
]
|
| 3700 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3701 |
[[package]]
|
| 3702 |
name = "urllib3"
|
| 3703 |
version = "2.7.0"
|
|
|
|
| 3821 |
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
| 3822 |
]
|
| 3823 |
|
| 3824 |
+
[[package]]
|
| 3825 |
+
name = "virtualenv"
|
| 3826 |
+
version = "21.4.2"
|
| 3827 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3828 |
+
dependencies = [
|
| 3829 |
+
{ name = "distlib" },
|
| 3830 |
+
{ name = "filelock" },
|
| 3831 |
+
{ name = "platformdirs" },
|
| 3832 |
+
{ name = "python-discovery" },
|
| 3833 |
+
]
|
| 3834 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" }
|
| 3835 |
+
wheels = [
|
| 3836 |
+
{ url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" },
|
| 3837 |
+
]
|
| 3838 |
+
|
| 3839 |
[[package]]
|
| 3840 |
name = "watchfiles"
|
| 3841 |
version = "1.2.0"
|