diff --git a/docs/deweloper/ai-search.md b/docs/deweloper/ai-search.md new file mode 100644 index 000000000..931de7b14 --- /dev/null +++ b/docs/deweloper/ai-search.md @@ -0,0 +1,180 @@ +# Wyszukiwanie przez AI (`ai_search`) + +Formularz „zapytaj po polsku" (`ai_search.views.ZapytanieAIView`) tłumaczy +pytanie w języku naturalnym (PL) na zapytanie [DjangoQL](https://github.com/ivelum/djangoql) +przy pomocy modelu Anthropic Claude, a następnie przekierowuje do +istniejącego edytora zapytań (`bpp:zapytanie`) z gotowym DSL-em. Aplikacja +loguje każde zapytanie wraz z kosztem w `ai_search.models.AISearchQuery` +(admin: read-only, `/admin/ai_search/aisearchquery/`). + +## Zmienne środowiskowe + +| Zmienna | Domyślna | Znaczenie | +|---|---|---| +| `BPP_AI_SEARCH_ENABLED` | `False` | Włącza feature (link w menu + działający formularz). Gdy wyłączone **lub** brak poświadczeń aktywnego backendu, personel z dostępem do edytora zapytań widzi ekran instrukcji konfiguracji (zamiast 404), a pozycja w menu ma dopisek „(konfiguracja)". Niezalogowani/bez uprawnień: bez zmian (niewidoczne). | +| `ANTHROPIC_API_KEY` | — | Klucz API Anthropic. Alternatywa dla `BPP_AI_API_KEY` (to drugie ma pierwszeństwo); SDK `anthropic` czyta `ANTHROPIC_API_KEY` ze środowiska jako fallback. | +| `BPP_AI_BACKEND` | `anthropic` | `anthropic` (natywny SDK, płatny, budżet PLN) albo `openai` (lokalny/self-hosted serwer OpenAI-compatible — darmowy, budżet nieaktywny). Patrz [„Modele lokalne"](#modele-lokalne) niżej. | +| `BPP_AI_BASE_URL` | `""` | Tylko dla `BPP_AI_BACKEND=openai` — adres API zgodny z OpenAI (np. `http://localhost:11434/v1` dla Ollama). | +| `BPP_AI_API_KEY` | `""` | Klucz API. Dla `openai` — klucz lokalnego serwera (pusty dla serwerów bez auth, np. Ollama). Dla `anthropic` — jednolita alternatywa dla `ANTHROPIC_API_KEY` (jeśli ustawione, ma pierwszeństwo). | +| `BPP_AI_MODEL` | `claude-sonnet-5` | Model używany do tłumaczenia NL->DSL. Dla `anthropic` musi mieć wpis w `BPP_AI_PRICING` (cennik) w `settings/base.py`; dla `openai` to nazwa modelu na lokalnym serwerze (np. `qwen3:8b`). | +| `BPP_AI_DAILY_BUDGET_PLN` / `BPP_AI_MONTHLY_BUDGET_PLN` | `20` / `300` | Twarde limity kosztu (PLN); po przekroczeniu `ai_search.budget.check_budget()` blokuje kolejne zapytania (widok zwraca 200 z komunikatem, nic nie loguje). | +| `BPP_AI_MAX_RETRIES` | `1` | Ile razy `translator.translate` ponawia zapytanie do modelu po błędzie składni DjangoQL (z konkretnym komunikatem błędu, linia/kolumna). | +| `BPP_AI_LLM_TIMEOUT` | `30` | Timeout (s) klienta `anthropic.Anthropic`. | +| `BPP_AI_SCHEMA_CACHE_TTL` | `86400` | TTL (s) cache'a compact-schematu (`ai_search.schema_export`) wysyłanego jako część system prompta. | +| `BPP_AI_FX_CACHE_TTL` | `86400` | TTL (s) cache'a kursu USD->PLN (NBP). | +| `BPP_AI_FX_FALLBACK` | `4.5` | Kurs awaryjny, gdy NBP i cache/Redis zawiodą (patrz też `ai_search.models.FxRate` — trwały fallback ostatniego znanego kursu). | +| `BPP_AI_MAX_FK_OPTIONS` | `100` | Powyżej tego progu `describe_schema_for_llm` nie wypisuje pojedynczych wartości `suggest_options` dla pola FK/wyboru (schemat rośnie liniowo z liczbą opcji). | + +## Dane wysyłane do Anthropic + +Do modelu trafia (jako część system prompta, patrz `ai_search.prompts` + +`ai_search.schema_export`): + +- **treść pytania użytkownika** (pole `pytanie`), +- **compact schema** modelu „rekord" lub „autor" — nazwy pól, typy, oraz + **`suggested_values`/`suggest_options` — realne wartości z bazy** + (np. lista źródeł, dyscyplin, jednostek) dla pól słownikowych, ograniczona + przez `BPP_AI_MAX_FK_OPTIONS`. To są **dane z produkcyjnej bazy BPP**, nie + dane osobowe pracowników/autorów per se, ale przy konfiguracji progu warto + pamiętać, że treść trafia do zewnętrznego API (Anthropic). + +Schemat jest cache'owany (`django.core.cache`, klucz +`ai_search:schema:`) i odświeżany: + +- automatycznie po wygaśnięciu TTL (`BPP_AI_SCHEMA_CACHE_TTL`), +- raz na dobę przez Celery beat (`ai_search.tasks.regenerate_schemas`, + wpis `ai-search-regenerate-schemas` w `CELERYBEAT_SCHEDULE`, + `src/django_bpp/settings/base.py`, 3:45 w nocy) — żeby nowe wartości + słownikowe (nowe źródła, dyscypliny itd.) pojawiły się w schemacie bez + czekania na wygaśnięcie cache. + +## Jak zmierzyć rozmiar / podejrzeć treść schematu + +```bash +# Wypisz aktualny (cache'owany, budowany w razie braku) schemat na stdout: +uv run python src/manage.py ai_search_schema_dump rekord +uv run python src/manage.py ai_search_schema_dump autor + +# Wymuś regenerację (pomija cache) i zmierz rozmiar w znakach: +uv run python src/manage.py ai_search_schema_dump rekord --regenerate | wc -c +``` + +Polecenie drukuje też liczbę znaków na stderr (`# rekord: 1234 znaków`) — +przydatne przy dostrajaniu `BPP_AI_MAX_FK_OPTIONS`, żeby schemat (a więc i +koszt tokenów input) nie rósł niekontrolowanie wraz z bazą. + +## Koszty + +`ai_search.pricing.cost_usd_from_usage` liczy koszt (USD) na podstawie +`usage` zwróconego przez SDK (`input_tokens`, `output_tokens`, +`cache_read_tokens`, `cache_write_tokens`) i cennika `BPP_AI_PRICING` w +`settings/base.py` (per model, z opcjonalnym intro-pricingiem do daty). +Nieznany model w cenniku podnosi `KeyError` — widok łapie ten wyjątek, +zgłasza do Rollbar i loguje wpis z `cost_usd=0`/`cost_pln=0` zamiast cichego +zera bez śladu w monitoringu. + +To dotyczy wyłącznie backendu `anthropic` — dla `BPP_AI_BACKEND=openai` +(model lokalny) widok w ogóle pomija pre-check budżetu i cennik/FX; koszt +jest zawsze logowany jako `0` (patrz sekcja niżej). + +## Modele lokalne + +### Backendy + +`ai_search.backends.get_backend()` wybiera implementację wg +`settings.BPP_AI_BACKEND`: + +- **`anthropic`** (domyślny) — natywny SDK `anthropic`, `messages.parse` + z ustrukturyzowanym `output_format`, prompt caching (blok `system` z + `cache_control: ephemeral`) i pełny cennik/budżet PLN + (`ai_search.budget`, `ai_search.pricing`, `ai_search.fx`). +- **`openai`** — dowolny lokalny/self-hosted serwer zgodny z OpenAI Chat + Completions API: [Ollama](https://ollama.com/), + [llama.cpp/llama-server](https://github.com/ggml-org/llama.cpp), + [vLLM](https://github.com/vllm-project/vllm), + [LM Studio](https://lmstudio.ai/), [LocalAI](https://localai.io/) — ten + sam backend obsługuje wszystkie, wystarczy inny `BPP_AI_BASE_URL`. + Prosi model o JSON zgodny ze schematem `DSLQuery` + (`response_format={"type": "json_schema", ...}`, `strict: True`), + `temperature=0`. Darmowy: widok (`ai_search.views.ZapytanieAIView`) + pomija pre-check budżetu, nie przekazuje `budget_check` do + `translator.translate`, i loguje `cost_usd=0`/`fx_rate=0`/`cost_pln=0` + bez wołania `pricing`/`fx` (model lokalny i tak nie ma wpisu w + `BPP_AI_PRICING`). + +Konfiguracja: `BPP_AI_BACKEND` / `BPP_AI_BASE_URL` / `BPP_AI_API_KEY` / +`BPP_AI_MODEL` (patrz tabela zmiennych środowiskowych wyżej). + +### Przykład: Ollama + +```bash +ollama pull qwen3:8b +ollama serve +``` + +```bash +# .env / środowisko: +BPP_AI_SEARCH_ENABLED=1 +BPP_AI_BACKEND=openai +BPP_AI_BASE_URL=http://localhost:11434/v1 +BPP_AI_MODEL=qwen3:8b +# BPP_AI_API_KEY pozostaw puste — Ollama nie wymaga auth. +``` + +Dla llama.cpp/llama-server, vLLM, LM Studio czy LocalAI zmienia się +wyłącznie `BPP_AI_BASE_URL` (i ewentualnie `BPP_AI_API_KEY`) — sam backend +(`openai`) zostaje ten sam. + +### Szacunek kontekstu (WAŻNE przy doborze modelu) + +Rozmiar system prompta wysyłanego do modelu to głównie compact-schema +(`ai_search.schema_export`, patrz `ai_search_schema_dump` wyżej) + reguły +i few-shot z `ai_search.prompts` + samo pytanie i miejsce na wyjście: + +- **Model „rekord", ze słownikowymi wartościami** + (`BPP_AI_MAX_FK_OPTIONS=100`, domyślne): **~31k tokenów**. +- **Model „rekord", bez wartości słownikowych** (`BPP_AI_MAX_FK_OPTIONS=0`): + **~22k tokenów** — chudszy schemat, ale gorsza trafność dla pytań + odwołujących się do konkretnych wartości (np. nazw źródeł/dyscyplin), + bo model ich po prostu nie widzi. +- Do tego reguły + few-shot (~1-2k tokenów) oraz pytanie + miejsce na + wyjście (~0,5k). +- Model „autor" ma zauważalnie mniejszy schemat niż „rekord". + +**Zalecane okno kontekstu modelu lokalnego: ≥ 32k tokenów** — komfortowo +dla wariantu z wartościami słownikowymi (domyślny). Dla modeli z +kontekstem 8k/16k: ustaw `BPP_AI_MAX_FK_OPTIONS=0` (chudszy ~22k schemat) +i licz się z ciaśniejszym zapasem na resztę promptu; ewentualnie ogranicz +się do modeli o szerszym oknie. + +### Zalecane modele OSS + +Klasa: mały-średni model instruct, dobry w ustrukturyzowanym JSON, +kontekst ≥32k, rozumiejący polski (samo pytanie jest po polsku — DSL +wyjściowy jest już w tokenach angielskich/DjangoQL): + +- **Qwen3** — rekomendacja główna (najlepszy stosunek JSON+PL wśród + modeli tej klasy). **4B/8B** dla lekkich instalacji, **14B/32B** dla + lepszej trafności na pytaniach dwuznacznych po polsku. + **Uwaga:** Qwen3 domyślnie ma włączony tryb „thinking" — potrafi + wygenerować blok rozumowania PRZED właściwym JSON-em, co przy stałym + `max_tokens=500` (patrz `backends.py`) grozi ucięciem odpowiedzi zanim + dojdzie do samego JSON-a. Wyłącz myślenie (np. prompt kończący się + `/no_think` albo odpowiednia flaga serwera inferencji). Walidator + DjangoQL + bounded-retry i tak wyłapią taki przypadek (zwrócony + niepoprawny JSON), ale lepiej uniknąć strat tokenów/czasu i wyłączyć + myślenie u źródła. +- **Llama 3.1 8B** (128k ctx), **Gemma 2 9B**, **Mistral** — alternatywy, + jeśli Qwen3 nie jest dostępny lub preferowany. +- Dla twardej gwarancji poprawnej składni JSON: **llama.cpp z GBNF** + (grammar-constrained decoding) — wymusza strukturę na poziomie + dekodowania, niezależnie od jakości modelu. + +**Uwaga kluczowa:** niezależnie od wybranego modelu lokalnego, gwarancją +poprawności końcowego zapytania jest walidator DjangoQL +(`translator.validate_query`, realny parser + `BppQLSchema`) w połączeniu +z bounded-retry (do `BPP_AI_MAX_RETRIES`, max. 2) — to jest mechanizm +**niezależny od backendu**. Słabszy model lokalny, który zwróci błędną +składnię, dostaje z powrotem dokładny komunikat błędu (linia/kolumna) i +ma szansę się poprawić, zupełnie tak samo jak przy backendzie +`anthropic`. diff --git a/docs/deweloper/index.md b/docs/deweloper/index.md index e47e70a18..6d9081d40 100644 --- a/docs/deweloper/index.md +++ b/docs/deweloper/index.md @@ -20,3 +20,5 @@ repozytorium. Poniższe strony rozwijają wybrane tematy: - [WeasyPrint na macOS](weasyprint-macos.md) — konfiguracja PDF lokalnie. - [Testy: Channels broadcast (flake)](testy-channels-broadcast.md) — diagnostyka niestabilnego testu. +- [Wyszukiwanie przez AI](ai-search.md) — feature „zapytaj po polsku" + (NL->DjangoQL), env-vary, koszty, cache schematu. diff --git a/docs/superpowers/plans/2026-07-04-ai-search.md b/docs/superpowers/plans/2026-07-04-ai-search.md new file mode 100644 index 000000000..11193bdb9 --- /dev/null +++ b/docs/superpowers/plans/2026-07-04-ai-search.md @@ -0,0 +1,1933 @@ +# Wyszukiwanie „przez sztuczną inteligencję" — plan implementacji + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Nowy tryb wyszukiwania — użytkownik pisze pytanie po polsku, Claude tłumaczy je na DjangoQL, po walidacji następuje redirect na istniejące „szukaj zapytaniem"; koszt logowany i ograniczony budżetami PLN. + +**Architecture:** Nowa apka `src/ai_search/` (izolowana, własny namespace URL `ai_search`). NL→DSL przez oficjalny SDK `anthropic` (`messages.parse`, structured outputs, `thinking` wyłączone). Walidacja przez `apply_search` + bounded retry z konkretnym błędem DjangoQL. Render przez redirect na `bpp:zapytanie?model=&query=`. Koszt liczony z `usage` × cennik (Decimal), przeliczany USD→PLN kursem NBP (cache + trwały fallback), budżety dzienny/miesięczny jako twardy blok. + +**Tech Stack:** Django, pytest + model_bakery, `anthropic` SDK, `djangoql-iplweb>=0.28.0`, `requests` (NBP), Django cache (Redis), django-environ (settings). + +## Global Constraints + +- **Max linia 88 znaków** (ruff). +- **NIE modyfikować istniejących migracji** w `src/*/migrations/`. +- **Wszystkie polecenia Pythona przez `uv run`.** +- **Zakaz `except: pass` / `except Exception: pass`** — logować (Rollbar dla tła), re-raise albo zwracać sensowny błąd. +- **Testy: pytest, funkcje bez klas, `@pytest.mark.django_db`, `model_bakery.baker`.** +- **Model:** `claude-sonnet-5` (SDK `anthropic`; NIE ustawiać `temperature`/`top_p`/`top_k` — Sonnet 5 je odrzuca; `thinking={"type":"disabled"}`). +- **Dostęp:** superuser LUB staff w grupie „wprowadzanie danych" (`user_can_use_query_editor`). +- **Kontrola kosztów:** wyłącznie budżety PLN (dzienny + miesięczny), twardy blok. Brak liczników liczby zapytań. +- **Baseline:** `make baseline-update` dopiero przy scalaniu (nie w gałęzi). +- Ikony w adminie/templatkach publicznych: publiczny frontend Foundation Icons (``). + +**Reużywane, istniejące punkty (nie zmieniać semantyki):** +- `bpp.views.zapytanie.user_can_use_query_editor` (`zapytanie.py:274`), `WprowadzanieDanychOrSuperuserMixin` (`:287`). +- `bpp.djangoql_schema.BppQLSchema` (`:232`), alias `BppZapytanieSchema` (`zapytanie.py:26`). +- `MODELS = {"rekord": Rekord, "autor": Autor}` (`zapytanie.py:36`), `MODEL_CHOICES` (`:31`). +- `_error_location(exc, query)` (`zapytanie.py:356`), `_format_error_text` (`:333`) — przenoszone w Tasku 5. +- Reverse istniejącego widoku: `bpp:zapytanie`. +- Filtr szablonowy `can_use_query_editor` (`bpp/templatetags/query_editor.py:8`). + +--- + +## Struktura plików + +``` +src/ai_search/ + __init__.py + apps.py # AiSearchConfig (name="ai_search") + models.py # AISearchQuery, FxRate + migrations/0001_initial.py + pricing.py # cost_usd_from_usage(usage, model, at_date) -> Decimal + fx.py # usd_to_pln_rate() -> Decimal (NBP + cache + FxRate + fallback) + schema_export.py # schema_for_llm(model_key) -> dict (Redis cache) + prompts.py # HARD_RULES, FEW_SHOT, build_system(schema_dict, model_key) + translator.py # translate(pytanie, model_key) -> TranslationResult + budget.py # check_budget() -> BudgetStatus ; spent_today/month + forms.py # AISearchForm + views.py # ZapytanieAIView + urls.py # app_name="ai_search"; path("", index, name="index") + templates/ai_search/zapytanie_ai.html + management/commands/ai_search_schema_dump.py # pomiar rozmiaru + ręczny podgląd + tests/ + __init__.py conftest.py + test_pricing.py test_fx.py test_schema_export.py + test_translator.py test_budget.py test_views.py + test_accuracy.py # skipif brak ANTHROPIC_API_KEY, wykluczony z CI + +src/bpp/djangoql_helpers.py # NOWY: _error_location, _format_error_text (Task 5) +``` + +Modyfikacje istniejących plików: +- `pyproject.toml` — dodać `anthropic`, bump `djangoql-iplweb>=0.28.0`. +- `src/django_bpp/settings/base.py` — INSTALLED_APPS += `ai_search`; sekcja settings `BPP_AI_*`. +- `src/django_bpp/urls.py` — include `ai_search.urls`. +- `src/bpp/views/zapytanie.py` — importować `_error_location`/`_format_error_text` z `bpp.djangoql_helpers` (re-export dla zgodności). +- `src/django_bpp/templates/top_bar.html` — pozycja „przez sztuczną inteligencję". + +--- + +## Task 1: Scaffold apki + zależności + settings + +**Files:** +- Create: `src/ai_search/__init__.py`, `src/ai_search/apps.py`, `src/ai_search/tests/__init__.py` +- Modify: `pyproject.toml`, `src/django_bpp/settings/base.py:365` (INSTALLED_APPS), koniec `base.py` (settings BPP_AI_*) +- Test: `src/ai_search/tests/test_app.py` + +**Interfaces:** +- Produces: apka `ai_search` w INSTALLED_APPS; settingsy `BPP_AI_SEARCH_ENABLED`, `BPP_AI_MODEL`, `BPP_AI_DAILY_BUDGET_PLN`, `BPP_AI_MONTHLY_BUDGET_PLN`, `BPP_AI_MAX_RETRIES`, `BPP_AI_LLM_TIMEOUT`, `BPP_AI_SCHEMA_CACHE_TTL`, `BPP_AI_FX_CACHE_TTL`, `BPP_AI_FX_FALLBACK`, `BPP_AI_PRICING`. + +- [ ] **Step 1: Dodaj zależności do pyproject.toml** + +W `[project].dependencies` zmień pin djangoql i dodaj anthropic: +```toml + "djangoql-iplweb>=0.28.0", + "anthropic>=0.40", +``` +(zastępując istniejące `"djangoql-iplweb>=0.27.2",`). + +- [ ] **Step 2: uv lock + sync** + +Run: `uv lock && uv sync` +Expected: lock zaktualizowany; `uv run python -c "import djangoql,anthropic;print(djangoql.__version__)"` → `0.28.0` (lub wyżej) bez błędu importu `anthropic`. + +- [ ] **Step 3: Utwórz szkielet apki** + +`src/ai_search/__init__.py` (pusty). +`src/ai_search/apps.py`: +```python +from django.apps import AppConfig + + +class AiSearchConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "ai_search" + verbose_name = "Wyszukiwanie przez AI" +``` +`src/ai_search/tests/__init__.py` (pusty). + +- [ ] **Step 4: Zarejestruj apkę w INSTALLED_APPS** + +W `src/django_bpp/settings/base.py` w liście `INSTALLED_APPS` dodaj `"ai_search",` bezpośrednio po `"bpp",` (linia ~414). + +- [ ] **Step 5: Dodaj sekcję settings BPP_AI_* na końcu base.py** + +Dopisz (używając `env` z django-environ, wzór jak reszta pliku): +```python +# --- Wyszukiwanie przez AI (ai_search) --- +BPP_AI_SEARCH_ENABLED = env("BPP_AI_SEARCH_ENABLED", default=False, cast=bool) +BPP_AI_MODEL = env("BPP_AI_MODEL", default="claude-sonnet-5") +BPP_AI_DAILY_BUDGET_PLN = env( + "BPP_AI_DAILY_BUDGET_PLN", default="20", cast=str +) +BPP_AI_MONTHLY_BUDGET_PLN = env( + "BPP_AI_MONTHLY_BUDGET_PLN", default="300", cast=str +) +BPP_AI_MAX_RETRIES = env("BPP_AI_MAX_RETRIES", default=1, cast=int) +BPP_AI_LLM_TIMEOUT = env("BPP_AI_LLM_TIMEOUT", default=30, cast=int) +BPP_AI_SCHEMA_CACHE_TTL = env("BPP_AI_SCHEMA_CACHE_TTL", default=86400, cast=int) +BPP_AI_FX_CACHE_TTL = env("BPP_AI_FX_CACHE_TTL", default=86400, cast=int) +BPP_AI_FX_FALLBACK = env("BPP_AI_FX_FALLBACK", default="4.5", cast=str) +# Cennik per model, USD za 1M tokenów. `intro_until` (ISO date) -> do tej daty +# obowiązują ceny intro. cache_read/cache_write to mnożniki ceny input. +BPP_AI_PRICING = { + "claude-sonnet-5": { + "input": "3.0", + "output": "15.0", + "intro_input": "2.0", + "intro_output": "10.0", + "intro_until": "2026-08-31", + "cache_read_mult": "0.1", + "cache_write_mult": "1.25", + }, +} +``` +Uwaga: kwoty jako `str`, w kodzie konwertowane na `Decimal` (unikamy floatów w pieniądzach). + +- [ ] **Step 6: Test — apka i settingsy ładują się** + +`src/ai_search/tests/test_app.py`: +```python +from django.apps import apps +from django.conf import settings + + +def test_app_registered(): + assert apps.is_installed("ai_search") + + +def test_ai_settings_present(): + assert hasattr(settings, "BPP_AI_MODEL") + assert settings.BPP_AI_MODEL + assert "claude-sonnet-5" in settings.BPP_AI_PRICING +``` + +- [ ] **Step 7: Uruchom test** + +Run: `uv run pytest src/ai_search/tests/test_app.py -v` +Expected: PASS (2). + +- [ ] **Step 8: Commit** + +```bash +git add pyproject.toml uv.lock src/ai_search/ src/django_bpp/settings/base.py +git commit -m "feat(ai-search): scaffold apki + zaleznosci (anthropic, djangoql 0.28) + settings" +``` + +--- + +## Task 2: Modele — AISearchQuery + FxRate + migracja + +**Files:** +- Create: `src/ai_search/models.py`, `src/ai_search/migrations/__init__.py` +- Test: `src/ai_search/tests/test_models.py` + +**Interfaces:** +- Produces: + - `AISearchQuery` z polami: `user (FK, null)`, `created (auto, db_index)`, `model (str)`, `pytanie (Text)`, `wygenerowany_query (Text, blank)`, `wybrany_model_danych (str)`, `input_tokens/output_tokens/cache_read_tokens/cache_write_tokens (int)`, `cost_usd/fx_rate/cost_pln (Decimal)`, `success (bool)`, `error (Text, null)`, `retried (bool)`. + - `FxRate` z polami: `rate (Decimal)`, `fetched_at (auto)`; klasmetoda `latest()` i `store(rate)`. + +- [ ] **Step 1: Napisz modele** + +`src/ai_search/models.py`: +```python +from decimal import Decimal + +from django.conf import settings +from django.db import models + + +class AISearchQuery(models.Model): + """Log pojedynczego zapytania NL->DSL wraz z kosztem.""" + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL + ) + created = models.DateTimeField(auto_now_add=True, db_index=True) + model = models.CharField(max_length=100) + pytanie = models.TextField() + wygenerowany_query = models.TextField(blank=True, default="") + wybrany_model_danych = models.CharField(max_length=32) + input_tokens = models.IntegerField(default=0) + output_tokens = models.IntegerField(default=0) + cache_read_tokens = models.IntegerField(default=0) + cache_write_tokens = models.IntegerField(default=0) + cost_usd = models.DecimalField(max_digits=12, decimal_places=6, default=Decimal("0")) + fx_rate = models.DecimalField(max_digits=10, decimal_places=4, default=Decimal("0")) + cost_pln = models.DecimalField(max_digits=12, decimal_places=4, default=Decimal("0")) + success = models.BooleanField(default=False) + error = models.TextField(null=True, blank=True) + retried = models.BooleanField(default=False) + + class Meta: + verbose_name = "zapytanie AI" + verbose_name_plural = "zapytania AI" + ordering = ("-created",) + + def __str__(self): + return f"{self.created:%Y-%m-%d %H:%M} {self.pytanie[:60]}" + + +class FxRate(models.Model): + """Trwały fallback ostatniego znanego kursu USD->PLN (gdy Redis+NBP padną).""" + + rate = models.DecimalField(max_digits=10, decimal_places=4) + fetched_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ("-fetched_at",) + + @classmethod + def latest(cls): + return cls.objects.first() + + @classmethod + def store(cls, rate): + return cls.objects.create(rate=Decimal(str(rate))) +``` + +- [ ] **Step 2: Utwórz katalog migracji i wygeneruj migrację** + +```bash +mkdir -p src/ai_search/migrations && touch src/ai_search/migrations/__init__.py +uv run python src/manage.py makemigrations ai_search +``` +Expected: `src/ai_search/migrations/0001_initial.py` utworzone (AISearchQuery, FxRate). + +- [ ] **Step 3: Napisz testy modeli** + +`src/ai_search/tests/test_models.py`: +```python +from decimal import Decimal + +import pytest +from model_bakery import baker + +from ai_search.models import AISearchQuery, FxRate + + +@pytest.mark.django_db +def test_aisearchquery_str(): + q = baker.make(AISearchQuery, pytanie="ksiazki po 2020") + assert "ksiazki po 2020" in str(q) + + +@pytest.mark.django_db +def test_fxrate_store_and_latest(): + assert FxRate.latest() is None + FxRate.store("4.12") + FxRate.store("4.20") + assert FxRate.latest().rate == Decimal("4.2000") +``` + +- [ ] **Step 4: Uruchom testy** + +Run: `uv run pytest src/ai_search/tests/test_models.py -v` +Expected: PASS (2). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/models.py src/ai_search/migrations/ src/ai_search/tests/test_models.py +git commit -m "feat(ai-search): modele AISearchQuery + FxRate + migracja" +``` + +--- + +## Task 3: Wycena — pricing.py (koszt z usage) + +**Files:** +- Create: `src/ai_search/pricing.py` +- Test: `src/ai_search/tests/test_pricing.py` + +**Interfaces:** +- Consumes: `settings.BPP_AI_PRICING`. +- Produces: `cost_usd_from_usage(usage: dict, model: str, at_date: date) -> Decimal`, gdzie `usage` = `{"input_tokens","output_tokens","cache_read_tokens","cache_write_tokens"}`. + +- [ ] **Step 1: Napisz test (TDD)** + +`src/ai_search/tests/test_pricing.py`: +```python +from datetime import date +from decimal import Decimal + +from ai_search.pricing import cost_usd_from_usage + +USAGE = { + "input_tokens": 1_000_000, + "output_tokens": 100_000, + "cache_read_tokens": 0, + "cache_write_tokens": 0, +} + + +def test_intro_pricing_before_cutoff(): + # intro: input 2.0, output 10.0 -> 2 + 1 = 3.0 USD + cost = cost_usd_from_usage(USAGE, "claude-sonnet-5", date(2026, 7, 1)) + assert cost == Decimal("3.000000") + + +def test_standard_pricing_after_cutoff(): + # standard: input 3.0, output 15.0 -> 3 + 1.5 = 4.5 USD + cost = cost_usd_from_usage(USAGE, "claude-sonnet-5", date(2026, 9, 1)) + assert cost == Decimal("4.500000") + + +def test_cache_read_and_write_multipliers(): + usage = { + "input_tokens": 0, + "output_tokens": 0, + "cache_read_tokens": 1_000_000, + "cache_write_tokens": 1_000_000, + } + # standard input 3.0; read 0.1x=0.3, write 1.25x=3.75 -> 4.05 + cost = cost_usd_from_usage(usage, "claude-sonnet-5", date(2026, 9, 1)) + assert cost == Decimal("4.050000") + + +def test_unknown_model_raises(): + import pytest + + with pytest.raises(KeyError): + cost_usd_from_usage(USAGE, "no-such-model", date(2026, 9, 1)) +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_pricing.py -v` +Expected: FAIL (ModuleNotFoundError: ai_search.pricing). + +- [ ] **Step 3: Implementacja** + +`src/ai_search/pricing.py`: +```python +from datetime import date +from decimal import Decimal + +from django.conf import settings + +_MILLION = Decimal("1000000") + + +def _price(cfg: dict, key: str, at_date: date) -> Decimal: + """Cena input/output per 1M tokenów, z uwzględnieniem intro-pricingu.""" + intro_until = cfg.get("intro_until") + if intro_until and at_date <= date.fromisoformat(intro_until): + return Decimal(cfg[f"intro_{key}"]) + return Decimal(cfg[key]) + + +def cost_usd_from_usage(usage: dict, model: str, at_date: date) -> Decimal: + """Koszt (USD, Decimal) danego wywołania na podstawie usage i cennika. + + ``usage`` klucze: input_tokens, output_tokens, cache_read_tokens, + cache_write_tokens. Podnosi KeyError dla nieznanego modelu — brak ceny to + błąd, nie cichy koszt zero (patrz spec, KRYT. #1). + """ + cfg = settings.BPP_AI_PRICING[model] + in_price = _price(cfg, "input", at_date) + out_price = _price(cfg, "output", at_date) + read_mult = Decimal(cfg["cache_read_mult"]) + write_mult = Decimal(cfg["cache_write_mult"]) + + total = ( + Decimal(usage.get("input_tokens", 0)) * in_price + + Decimal(usage.get("output_tokens", 0)) * out_price + + Decimal(usage.get("cache_read_tokens", 0)) * in_price * read_mult + + Decimal(usage.get("cache_write_tokens", 0)) * in_price * write_mult + ) / _MILLION + return total.quantize(Decimal("0.000001")) +``` + +- [ ] **Step 4: Uruchom — ma przejść** + +Run: `uv run pytest src/ai_search/tests/test_pricing.py -v` +Expected: PASS (4). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/pricing.py src/ai_search/tests/test_pricing.py +git commit -m "feat(ai-search): wycena kosztu z usage (Decimal, intro-pricing, cache mult)" +``` + +--- + +## Task 4: Kurs walutowy — fx.py (NBP + cache + trwały fallback) + +**Files:** +- Create: `src/ai_search/fx.py` +- Test: `src/ai_search/tests/test_fx.py` + +**Interfaces:** +- Consumes: `FxRate` (Task 2), `settings.BPP_AI_FX_CACHE_TTL`, `settings.BPP_AI_FX_FALLBACK`, Django cache, `requests`. +- Produces: `usd_to_pln_rate() -> Decimal`. Klucz cache: `"ai_search:fx:usdpln"`. + +- [ ] **Step 1: Napisz testy (TDD)** + +`src/ai_search/tests/test_fx.py`: +```python +from decimal import Decimal +from unittest import mock + +import pytest +from django.core.cache import cache + +from ai_search import fx +from ai_search.models import FxRate + + +@pytest.fixture(autouse=True) +def _clear_cache(): + cache.delete("ai_search:fx:usdpln") + yield + cache.delete("ai_search:fx:usdpln") + + +def _nbp_response(mid): + m = mock.Mock() + m.raise_for_status = mock.Mock() + m.json.return_value = {"rates": [{"mid": mid}]} + return m + + +@pytest.mark.django_db +def test_fetches_from_nbp_and_persists(): + with mock.patch("ai_search.fx.requests.get", return_value=_nbp_response(4.11)): + rate = fx.usd_to_pln_rate() + assert rate == Decimal("4.1100") + assert FxRate.latest().rate == Decimal("4.1100") + + +@pytest.mark.django_db +def test_uses_cache_without_second_http_call(): + with mock.patch("ai_search.fx.requests.get", return_value=_nbp_response(4.11)) as g: + fx.usd_to_pln_rate() + fx.usd_to_pln_rate() + assert g.call_count == 1 + + +@pytest.mark.django_db +def test_falls_back_to_db_when_nbp_down(): + FxRate.store("4.05") + with mock.patch("ai_search.fx.requests.get", side_effect=OSError("boom")): + rate = fx.usd_to_pln_rate() + assert rate == Decimal("4.0500") + + +@pytest.mark.django_db +def test_terminal_fallback_when_nothing_available(settings): + settings.BPP_AI_FX_FALLBACK = "4.5" + with mock.patch("ai_search.fx.requests.get", side_effect=OSError("boom")): + rate = fx.usd_to_pln_rate() + assert rate == Decimal("4.5") +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_fx.py -v` +Expected: FAIL (brak `ai_search.fx`). + +- [ ] **Step 3: Implementacja** + +`src/ai_search/fx.py`: +```python +import logging +from decimal import Decimal + +import requests +from django.conf import settings +from django.core.cache import cache + +logger = logging.getLogger(__name__) + +_CACHE_KEY = "ai_search:fx:usdpln" +_NBP_URL = "https://api.nbp.pl/api/exchangerates/rates/A/USD/?format=json" + + +def _fetch_nbp() -> Decimal: + resp = requests.get(_NBP_URL, timeout=10) + resp.raise_for_status() + mid = resp.json()["rates"][0]["mid"] + return Decimal(str(mid)).quantize(Decimal("0.0001")) + + +def usd_to_pln_rate() -> Decimal: + """Kurs USD->PLN. Kolejność: cache Redis -> NBP -> DB FxRate -> stała. + + Nigdy nie podnosi wyjątku — FX nie może zablokować feature'a; do wyceny + wystarczy ostatni znany / konserwatywny kurs. + """ + from ai_search.models import FxRate + + cached = cache.get(_CACHE_KEY) + if cached is not None: + return Decimal(cached) + + try: + rate = _fetch_nbp() + cache.set(_CACHE_KEY, str(rate), settings.BPP_AI_FX_CACHE_TTL) + FxRate.store(rate) + return rate + except (requests.RequestException, OSError, KeyError, ValueError) as exc: + logger.warning("NBP FX niedostępny (%s), używam fallbacku", exc) + + last = FxRate.latest() + if last is not None: + return last.rate + return Decimal(settings.BPP_AI_FX_FALLBACK) +``` + +- [ ] **Step 4: Uruchom — ma przejść** + +Run: `uv run pytest src/ai_search/tests/test_fx.py -v` +Expected: PASS (4). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/fx.py src/ai_search/tests/test_fx.py +git commit -m "feat(ai-search): kurs USD->PLN z NBP (https) + cache + trwaly fallback" +``` + +--- + +## Task 5: Przeniesienie helpera błędów DjangoQL do współdzielonego modułu + +**Files:** +- Create: `src/bpp/djangoql_helpers.py` +- Modify: `src/bpp/views/zapytanie.py` (import + re-export dla zgodności) +- Test: `src/ai_search/tests/test_error_helper.py` + +**Interfaces:** +- Produces: `bpp.djangoql_helpers._error_location(exc, query) -> (line, column, mark)` i `_format_error_text(exc) -> str` (przeniesione 1:1 z `zapytanie.py`). `_locate_token` również. +- `zapytanie.py` re-eksportuje je, więc jego dotychczasowe użycia i testy działają bez zmian. + +- [ ] **Step 1: Utwórz współdzielony moduł (przenieś kod 1:1)** + +`src/bpp/djangoql_helpers.py` — przenieś **dokładnie** ciała `_format_error_text`, `_locate_token`, `_error_location`, `_error_payload` z `zapytanie.py:333-382` (razem z importami `re`, `ValidationError`): +```python +import re + +from django.core.exceptions import ValidationError + + +def _format_error_text(exc): + if isinstance(exc, ValidationError): + return "; ".join(exc.messages) + return str(exc) + + +def _locate_token(query, needle): + match = re.search(r"(? dict` (cache Redis, klucz `f"ai_search:schema:{model_key}"`), `regenerate(model_key) -> dict`. + +- [ ] **Step 1: Napisz testy (TDD)** + +`src/ai_search/tests/test_schema_export.py`: +```python +import pytest +from django.core.cache import cache + +from ai_search import schema_export + + +@pytest.fixture(autouse=True) +def _clear(): + for k in ("rekord", "autor"): + cache.delete(f"ai_search:schema:{k}") + yield + + +@pytest.mark.django_db +def test_schema_shape_rekord(): + data = schema_export.schema_for_llm("rekord") + assert "grammar" in data and "models" in data + assert "negation" in data["grammar"] + + +@pytest.mark.django_db +def test_schema_shape_autor(): + data = schema_export.schema_for_llm("autor") + assert data["models"] # niepusty słownik modeli + + +@pytest.mark.django_db +def test_cache_reused(monkeypatch): + calls = {"n": 0} + real = schema_export._build + + def counting(model_key): + calls["n"] += 1 + return real(model_key) + + monkeypatch.setattr(schema_export, "_build", counting) + schema_export.schema_for_llm("rekord") + schema_export.schema_for_llm("rekord") + assert calls["n"] == 1 + + +@pytest.mark.django_db +def test_unknown_model_key_raises(): + with pytest.raises(KeyError): + schema_export.schema_for_llm("nieistnieje") +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_schema_export.py -v` +Expected: FAIL (brak modułu). + +- [ ] **Step 3: Implementacja** + +`src/ai_search/schema_export.py`: +```python +import logging + +from django.conf import settings +from django.core.cache import cache +from djangoql.llm import describe_schema_for_llm + +from bpp.djangoql_schema import BppQLSchema +from bpp.views.zapytanie import MODELS + +logger = logging.getLogger(__name__) + + +def _cache_key(model_key: str) -> str: + return f"ai_search:schema:{model_key}" + + +def _build(model_key: str) -> dict: + """Zbuduj opis schematu dla danego klucza modelu (rekord/autor). + + Podnosi KeyError dla nieznanego klucza. describe_schema_for_llm sięga bazy + dla pól z suggest_options — dlatego wynik jest cache'owany.""" + model = MODELS[model_key] + return describe_schema_for_llm(BppQLSchema(model)) + + +def regenerate(model_key: str) -> dict: + data = _build(model_key) + cache.set(_cache_key(model_key), data, settings.BPP_AI_SCHEMA_CACHE_TTL) + return data + + +def schema_for_llm(model_key: str) -> dict: + cached = cache.get(_cache_key(model_key)) + if cached is not None: + return cached + return regenerate(model_key) +``` + +- [ ] **Step 4: Uruchom — ma przejść** + +Run: `uv run pytest src/ai_search/tests/test_schema_export.py -v` +Expected: PASS (4). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/schema_export.py src/ai_search/tests/test_schema_export.py +git commit -m "feat(ai-search): eksport schematu DjangoQL dla LLM z cache Redis" +``` + +--- + +## Task 7: Komenda pomiaru rozmiaru schematu (KRYT. #3 ze specu) + +**Files:** +- Create: `src/ai_search/management/__init__.py`, `src/ai_search/management/commands/__init__.py`, `src/ai_search/management/commands/ai_search_schema_dump.py` +- Test: `src/ai_search/tests/test_schema_dump_command.py` + +**Interfaces:** +- Produces: `manage.py ai_search_schema_dump ` — drukuje JSON + na stderr przybliżoną liczbę znaków/tokenów (do decyzji o przycinaniu). + +- [ ] **Step 1: Test komendy** + +`src/ai_search/tests/test_schema_dump_command.py`: +```python +import pytest +from django.core.management import call_command + + +@pytest.mark.django_db +def test_dump_rekord_runs(capsys): + call_command("ai_search_schema_dump", "rekord") + out = capsys.readouterr().out + assert '"grammar"' in out +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_schema_dump_command.py -v` +Expected: FAIL (Unknown command). + +- [ ] **Step 3: Implementacja** + +Utwórz `__init__.py` w `management/` i `management/commands/`. +`src/ai_search/management/commands/ai_search_schema_dump.py`: +```python +import json + +from django.core.management.base import BaseCommand, CommandError + +from ai_search import schema_export + + +class Command(BaseCommand): + help = "Zrzuca opis schematu dla LLM i szacuje jego rozmiar (tokeny)." + + def add_arguments(self, parser): + parser.add_argument("model_key", choices=["rekord", "autor"]) + + def handle(self, *args, **options): + try: + data = schema_export.regenerate(options["model_key"]) + except KeyError as exc: + raise CommandError(f"Nieznany model: {exc}") + text = json.dumps(data, ensure_ascii=False, indent=2) + self.stdout.write(text) + # Zgrubne oszacowanie: ~4 znaki/token (angielski JSON). + chars = len(text) + self.stderr.write( + f"\n[rozmiar] {chars} znaków, ~{chars // 4} tokenów. " + f"Jeśli > ~30k tokenów — rozważ przycinanie suggested_values." + ) +``` + +- [ ] **Step 4: Uruchom test + realny pomiar** + +Run: `uv run pytest src/ai_search/tests/test_schema_dump_command.py -v` +Expected: PASS. +Następnie (pomiar, wynik do specu/planu): +`uv run python src/manage.py ai_search_schema_dump rekord >/dev/null` +Odczytaj z stderr szacunek tokenów. **Jeśli > ~30k** — dopisz w tym tasku follow-up: przycięcie w `schema_export._build` (np. usuwanie `suggested_values` z pól-pickerów przez post-processing dict-a). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/management/ src/ai_search/tests/test_schema_dump_command.py +git commit -m "feat(ai-search): komenda pomiaru rozmiaru schematu dla LLM" +``` + +--- + +## Task 8: Prompty — reguły + few-shot (prompts.py) + +**Files:** +- Create: `src/ai_search/prompts.py` +- Test: `src/ai_search/tests/test_prompts.py` + +**Interfaces:** +- Consumes: nic (dane statyczne + dict schematu). +- Produces: `build_system(schema_dict: dict, model_key: str) -> list` — lista bloków system dla SDK anthropic; ostatni blok (schema+reguły+few-shot) ma `cache_control: {"type":"ephemeral"}`. Pytanie użytkownika NIE jest tu zawarte. Stałe: `HARD_RULES: str`, `FEW_SHOT: dict[str, list[tuple[str,str]]]` (klucze "rekord"/"autor"). + +- [ ] **Step 1: Test (TDD)** + +`src/ai_search/tests/test_prompts.py`: +```python +from ai_search.prompts import FEW_SHOT, build_system + + +def test_build_system_has_cache_control_on_last_block(): + blocks = build_system({"grammar": {}, "models": {}}, "rekord") + assert blocks[-1]["cache_control"] == {"type": "ephemeral"} + assert "type" in blocks[-1] and blocks[-1]["type"] == "text" + + +def test_build_system_embeds_schema_and_rules(): + blocks = build_system({"grammar": {"x": 1}, "models": {}}, "rekord") + text = blocks[-1]["text"] + assert "grammar" in text # schemat wklejony + assert "not startswith" in text # reguła negacji + + +def test_few_shot_covers_both_models(): + assert FEW_SHOT["rekord"] and FEW_SHOT["autor"] +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_prompts.py -v` +Expected: FAIL. + +- [ ] **Step 3: Implementacja** + +`src/ai_search/prompts.py`: +```python +import json + +HARD_RULES = """\ +Tłumaczysz pytania w języku polskim na zapytania DSL DjangoQL dla systemu +bibliografii publikacji (BPP). Zwracasz WYŁĄCZNIE poprawne zapytanie w polu +`query`, albo `query=null` i wyjaśnienie po polsku w `error`, gdy pytania nie +da się wyrazić w tym DSL. + +ZASADY TWARDE: +- Stringi zawsze w podwójnych cudzysłowach. +- Relacje trawersujesz kropką, także wielopoziomowo + (np. autorzy.autor.nazwisko). +- Listy: pole in ("a", "b"). +- NIE MA samodzielnego operatora `not`. Negujesz operatorem: !=, !~, + not in, not startswith, not endswith. (poprawnie: rok != 2020; + błędnie: not rok = 2020) +- Łączenie warunków: `and` / `or`, grupowanie nawiasami. +- Jeśli pytanie jest oceniające/nieostre ("najlepsze", "ciekawe") i nie da się + go zmapować na pola — ustaw query=null i wyjaśnij w error. +""" + +FEW_SHOT = { + "rekord": [ + ("publikacje z 2024 roku", "rok = 2024"), + ("prace po 2020 zawierające w tytule nowotwór", + 'rok > 2020 and tytul_oryginalny ~ "nowotwor"'), + ("artykuły o charakterze AC", + 'charakter_formalny.skrot = "AC"'), + ("prace autora o nazwisku Kowalski", + 'autorzy.autor.nazwisko ~ "Kowalski"'), + ("publikacje z lat 2022-2024", "rok >= 2022 and rok <= 2024"), + ("prace bez przypisanego źródła", "zrodlo = None"), + ("tytuły niezawierające słowa raport", 'tytul_oryginalny !~ "raport"'), + ], + "autor": [ + ("autorzy o nazwisku zaczynającym się na Kow", + 'nazwisko startswith "Kow"'), + ("autorzy z imieniem Jan lub Anna", + 'imiona ~ "Jan" or imiona ~ "Anna"'), + ("autorzy z podanym ORCID", "orcid != None"), + ("autorki i autorzy bez pseudonimu", "pseudonim = None"), + ], +} + + +def _few_shot_text(model_key: str) -> str: + lines = ["PRZYKŁADY:"] + for pl, dsl in FEW_SHOT[model_key]: + lines.append(f'"{pl}" -> {dsl}') + return "\n".join(lines) + + +def build_system(schema_dict: dict, model_key: str) -> list: + """Bloki `system` dla SDK anthropic. Ostatni (stabilny) blok — schemat + + reguły + few-shot — oznaczony cache_control, żeby retry i szybkie kolejne + zapytania czytały go z cache.""" + schema_json = json.dumps(schema_dict, ensure_ascii=False) + text = ( + f"{HARD_RULES}\n\nSCHEMAT (model {model_key}):\n{schema_json}\n\n" + f"{_few_shot_text(model_key)}" + ) + return [{"type": "text", "text": text, "cache_control": {"type": "ephemeral"}}] +``` + +- [ ] **Step 4: Uruchom — ma przejść** + +Run: `uv run pytest src/ai_search/tests/test_prompts.py -v` +Expected: PASS (3). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/prompts.py src/ai_search/tests/test_prompts.py +git commit -m "feat(ai-search): system prompt (reguly + few-shot) z cache_control" +``` + +--- + +## Task 9: Translator — SDK anthropic + walidacja + bounded retry + +**Files:** +- Create: `src/ai_search/translator.py` +- Test: `src/ai_search/tests/test_translator.py` + +**Interfaces:** +- Consumes: `anthropic` SDK, `ai_search.prompts.build_system`, `ai_search.schema_export.schema_for_llm`, `bpp.djangoql_helpers._error_location/_format_error_text`, `djangoql.queryset.apply_search`, `bpp.djangoql_schema.BppQLSchema`, `bpp.views.zapytanie.MODELS`, `settings.BPP_AI_MODEL/BPP_AI_MAX_RETRIES/BPP_AI_LLM_TIMEOUT`. +- Produces: + - dataclass `TranslationResult(query: str|None, error: str|None, usage: dict, attempts: int, retried: bool)` gdzie `usage` = zsumowane `{input_tokens,output_tokens,cache_read_tokens,cache_write_tokens}`. + - `translate(pytanie: str, model_key: str) -> TranslationResult`. + - `DSLQuery` (pydantic) `{query: Optional[str], error: Optional[str]}`. + - `validate_query(query: str, model_key: str) -> tuple[str|None, dict|None]` — zwraca `(None, None)` gdy OK, albo `(komunikat, {line,column,mark})`. + +- [ ] **Step 1: Testy (TDD, mockujemy SDK)** + +`src/ai_search/tests/test_translator.py`: +```python +from unittest import mock + +import pytest + +from ai_search import translator + + +def _fake_response(query, error=None, usage=None): + parsed = translator.DSLQuery(query=query, error=error) + resp = mock.Mock() + resp.parsed_output = parsed + resp.stop_reason = "end_turn" + u = usage or {} + resp.usage = mock.Mock( + input_tokens=u.get("input_tokens", 10), + output_tokens=u.get("output_tokens", 5), + cache_read_input_tokens=u.get("cache_read_tokens", 0), + cache_creation_input_tokens=u.get("cache_write_tokens", 0), + ) + return resp + + +@pytest.fixture(autouse=True) +def _schema(monkeypatch): + monkeypatch.setattr( + translator.schema_export, "schema_for_llm", + lambda k: {"grammar": {}, "models": {}}, + ) + + +@pytest.mark.django_db +def test_valid_query_first_try(): + with mock.patch.object( + translator, "_call_model", return_value=_fake_response("rok = 2024") + ) as call: + res = translator.translate("publikacje z 2024", "rekord") + assert res.query == "rok = 2024" + assert res.error is None + assert res.attempts == 1 + assert call.call_count == 1 + + +@pytest.mark.django_db +def test_null_query_passthrough(): + with mock.patch.object( + translator, "_call_model", + return_value=_fake_response(None, error="pytanie nieostre"), + ): + res = translator.translate("najlepsze prace", "rekord") + assert res.query is None + assert "nieostre" in res.error + + +@pytest.mark.django_db +def test_invalid_query_retries_then_succeeds(): + responses = [_fake_response('rok = "x'), _fake_response("rok = 2024")] + with mock.patch.object(translator, "_call_model", side_effect=responses): + res = translator.translate("prace z 2024", "rekord") + assert res.query == "rok = 2024" + assert res.retried is True + assert res.attempts == 2 + + +@pytest.mark.django_db +def test_invalid_after_retries_returns_error(settings): + settings.BPP_AI_MAX_RETRIES = 1 + bad = _fake_response('rok = "x') + with mock.patch.object(translator, "_call_model", return_value=bad) as call: + res = translator.translate("prace z 2024", "rekord") + assert res.query is None + assert res.error # komunikat błędu DjangoQL + assert call.call_count == 2 # 1 + 1 retry +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_translator.py -v` +Expected: FAIL (brak modułu). + +- [ ] **Step 3: Implementacja** + +`src/ai_search/translator.py`: +```python +import logging +from dataclasses import dataclass, field +from typing import Optional + +import anthropic +from django.conf import settings +from djangoql.exceptions import DjangoQLError +from djangoql.queryset import apply_search +from pydantic import BaseModel, ConfigDict + +from bpp.djangoql_helpers import _error_location, _format_error_text +from bpp.djangoql_schema import BppQLSchema +from bpp.views.zapytanie import MODELS +from django.core.exceptions import FieldError, ValidationError + +from ai_search import prompts, schema_export + +logger = logging.getLogger(__name__) + + +class DSLQuery(BaseModel): + model_config = ConfigDict(extra="forbid") + query: Optional[str] + error: Optional[str] + + +@dataclass +class TranslationResult: + query: Optional[str] = None + error: Optional[str] = None + usage: dict = field(default_factory=dict) + attempts: int = 0 + retried: bool = False + + +def validate_query(query: str, model_key: str): + """Zwraca (None, None) gdy zapytanie parsuje się poprawnie, inaczej + (komunikat, {line,column,mark}).""" + model = MODELS[model_key] + try: + apply_search(model.objects.all(), query, schema=BppQLSchema) + return None, None + except (DjangoQLError, FieldError, ValidationError, ValueError) as exc: + line, column, mark = _error_location(exc, query) + loc = {"line": line, "column": column, "mark": mark} if line else None + return _format_error_text(exc), loc + + +def _client() -> anthropic.Anthropic: + return anthropic.Anthropic(timeout=settings.BPP_AI_LLM_TIMEOUT) + + +def _call_model(system, messages): + """Pojedyncze wywołanie modelu (wydzielone dla testowalności).""" + return _client().messages.parse( + model=settings.BPP_AI_MODEL, + max_tokens=500, + thinking={"type": "disabled"}, + system=system, + messages=messages, + output_format=DSLQuery, + ) + + +def _extract_usage(resp) -> dict: + u = resp.usage + return { + "input_tokens": getattr(u, "input_tokens", 0) or 0, + "output_tokens": getattr(u, "output_tokens", 0) or 0, + "cache_read_tokens": getattr(u, "cache_read_input_tokens", 0) or 0, + "cache_write_tokens": getattr(u, "cache_creation_input_tokens", 0) or 0, + } + + +def _accumulate(total: dict, part: dict): + for k, v in part.items(): + total[k] = total.get(k, 0) + v + + +def translate(pytanie: str, model_key: str) -> TranslationResult: + """NL (polski) -> DjangoQL. Waliduje i, przy błędzie składni, zwraca do + modelu konkretny komunikat DjangoQL (linia/kolumna) i ponawia — bounded.""" + system = prompts.build_system(schema_export.schema_for_llm(model_key), model_key) + max_retries = settings.BPP_AI_MAX_RETRIES + total_usage: dict = {} + result = TranslationResult() + content = pytanie + + for attempt in range(max_retries + 1): + result.attempts = attempt + 1 + resp = _call_model(system, [{"role": "user", "content": content}]) + _accumulate(total_usage, _extract_usage(resp)) + result.usage = total_usage + + if getattr(resp, "stop_reason", None) == "refusal": + result.query = None + result.error = "Model odmówił odpowiedzi na to pytanie." + return result + + parsed = resp.parsed_output + if parsed.query is None: + result.query = None + result.error = parsed.error or "Nie można wyrazić pytania w DSL." + return result + + err, loc = validate_query(parsed.query, model_key) + if err is None: + result.query = parsed.query + result.error = None + return result + + # Błąd składni — przygotuj feedback do modelu i ponów (jeśli został budżet prób). + result.query = None + result.error = err + if attempt < max_retries: + result.retried = True + where = "" + if loc and loc.get("line"): + where = f" (linia {loc['line']}, kolumna {loc['column']})" + content = ( + f"{pytanie}\n\nPoprzednie zapytanie `{parsed.query}` zwróciło błąd " + f"DjangoQL: {err}{where}. Skoryguj i zwróć poprawne zapytanie." + ) + return result +``` + +- [ ] **Step 4: Uruchom — ma przejść** + +Run: `uv run pytest src/ai_search/tests/test_translator.py -v` +Expected: PASS (4). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/translator.py src/ai_search/tests/test_translator.py +git commit -m "feat(ai-search): translator NL->DjangoQL (SDK anthropic) + walidacja + bounded retry" +``` + +--- + +## Task 10: Budżet — budget.py (agregacja + guard) + +**Files:** +- Create: `src/ai_search/budget.py` +- Test: `src/ai_search/tests/test_budget.py` + +**Interfaces:** +- Consumes: `AISearchQuery`, `settings.BPP_AI_DAILY_BUDGET_PLN/BPP_AI_MONTHLY_BUDGET_PLN`, `django.utils.timezone`. +- Produces: + - `spent_today() -> Decimal`, `spent_this_month() -> Decimal` (agregacja `cost_pln`, granice wg `TIME_ZONE`). + - dataclass `BudgetStatus(ok: bool, reason: str|None)`. + - `check_budget() -> BudgetStatus`. + +- [ ] **Step 1: Testy (TDD)** + +`src/ai_search/tests/test_budget.py`: +```python +from decimal import Decimal + +import pytest +from model_bakery import baker + +from ai_search import budget +from ai_search.models import AISearchQuery + + +@pytest.mark.django_db +def test_ok_when_under_budget(settings): + settings.BPP_AI_DAILY_BUDGET_PLN = "10" + settings.BPP_AI_MONTHLY_BUDGET_PLN = "100" + baker.make(AISearchQuery, cost_pln=Decimal("2")) + status = budget.check_budget() + assert status.ok is True + + +@pytest.mark.django_db +def test_blocks_when_daily_exceeded(settings): + settings.BPP_AI_DAILY_BUDGET_PLN = "5" + settings.BPP_AI_MONTHLY_BUDGET_PLN = "100" + baker.make(AISearchQuery, cost_pln=Decimal("6")) + status = budget.check_budget() + assert status.ok is False + assert "dzien" in status.reason.lower() or "dzień" in status.reason.lower() + + +@pytest.mark.django_db +def test_blocks_when_monthly_exceeded(settings): + settings.BPP_AI_DAILY_BUDGET_PLN = "1000" + settings.BPP_AI_MONTHLY_BUDGET_PLN = "5" + baker.make(AISearchQuery, cost_pln=Decimal("6")) + status = budget.check_budget() + assert status.ok is False + assert "mies" in status.reason.lower() +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_budget.py -v` +Expected: FAIL. + +- [ ] **Step 3: Implementacja** + +`src/ai_search/budget.py`: +```python +from dataclasses import dataclass +from decimal import Decimal +from typing import Optional + +from django.conf import settings +from django.db.models import Sum +from django.utils import timezone + +from ai_search.models import AISearchQuery + + +@dataclass +class BudgetStatus: + ok: bool + reason: Optional[str] = None + + +def _sum_since(dt) -> Decimal: + agg = AISearchQuery.objects.filter(created__gte=dt).aggregate(s=Sum("cost_pln")) + return agg["s"] or Decimal("0") + + +def spent_today() -> Decimal: + now = timezone.localtime() + start = now.replace(hour=0, minute=0, second=0, microsecond=0) + return _sum_since(start) + + +def spent_this_month() -> Decimal: + now = timezone.localtime() + start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + return _sum_since(start) + + +def check_budget() -> BudgetStatus: + """Twardy blok, gdy dzienny lub miesięczny budżet PLN jest wyczerpany.""" + daily = Decimal(str(settings.BPP_AI_DAILY_BUDGET_PLN)) + monthly = Decimal(str(settings.BPP_AI_MONTHLY_BUDGET_PLN)) + if spent_today() >= daily: + return BudgetStatus( + ok=False, + reason="Dzienny limit kosztów AI został osiągnięty. " + "Spróbuj jutro lub użyj „szukaj zapytaniem”.", + ) + if spent_this_month() >= monthly: + return BudgetStatus( + ok=False, + reason="Miesięczny limit kosztów AI został osiągnięty. " + "Użyj „szukaj zapytaniem”.", + ) + return BudgetStatus(ok=True) +``` + +- [ ] **Step 4: Uruchom — ma przejść** + +Run: `uv run pytest src/ai_search/tests/test_budget.py -v` +Expected: PASS (3). + +- [ ] **Step 5: Commit** + +```bash +git add src/ai_search/budget.py src/ai_search/tests/test_budget.py +git commit -m "feat(ai-search): guard budzetowy (dzienny/miesieczny PLN, twardy blok)" +``` + +--- + +## Task 11: Widok + formularz + URL + szablon (redirect flow) + +**Files:** +- Create: `src/ai_search/forms.py`, `src/ai_search/views.py`, `src/ai_search/urls.py`, `src/ai_search/templates/ai_search/zapytanie_ai.html` +- Modify: `src/django_bpp/urls.py` (include) +- Test: `src/ai_search/tests/test_views.py` + +**Interfaces:** +- Consumes: `translator.translate`, `budget.check_budget`, `pricing.cost_usd_from_usage`, `fx.usd_to_pln_rate`, `AISearchQuery`, `WprowadzanieDanychOrSuperuserMixin`, `MODEL_CHOICES`, reverse `bpp:zapytanie`. +- Produces: URL `ai_search:index` (GET: formularz; POST: budżet→translate→log→redirect/render). Redirect: `bpp:zapytanie?model=&query=`; pytanie PL w `request.session["ai_search_last_question"]`. + +- [ ] **Step 1: Testy widoku (TDD, mock translatora)** + +`src/ai_search/tests/test_views.py`: +```python +from decimal import Decimal +from unittest import mock + +import pytest +from django.urls import reverse +from model_bakery import baker + +from ai_search import translator +from ai_search.models import AISearchQuery + + +@pytest.fixture +def staff_client(client, django_user_model): + u = django_user_model.objects.create_user( + username="ed", password="x", is_staff=True, is_superuser=True + ) + client.force_login(u) + return client + + +@pytest.mark.django_db +def test_anonymous_denied(client, settings): + settings.BPP_AI_SEARCH_ENABLED = True + r = client.get(reverse("ai_search:index")) + assert r.status_code in (302, 403) + + +@pytest.mark.django_db +def test_get_form_visible_for_staff(staff_client, settings): + settings.BPP_AI_SEARCH_ENABLED = True + r = staff_client.get(reverse("ai_search:index")) + assert r.status_code == 200 + + +@pytest.mark.django_db +def test_post_success_redirects_to_zapytanie(staff_client, settings): + settings.BPP_AI_SEARCH_ENABLED = True + res = translator.TranslationResult( + query="rok = 2024", usage={"input_tokens": 10, "output_tokens": 5}, attempts=1 + ) + with mock.patch("ai_search.views.translator.translate", return_value=res), \ + mock.patch("ai_search.views.fx.usd_to_pln_rate", return_value=Decimal("4.1")): + r = staff_client.post( + reverse("ai_search:index"), + {"model": "rekord", "pytanie": "publikacje z 2024"}, + ) + assert r.status_code == 302 + assert reverse("bpp:zapytanie") in r.url + assert "query=rok" in r.url.replace("%20", " ") or "rok" in r.url + log = AISearchQuery.objects.get() + assert log.success is True + assert log.cost_pln > 0 + + +@pytest.mark.django_db +def test_post_blocked_by_budget(staff_client, settings): + settings.BPP_AI_SEARCH_ENABLED = True + settings.BPP_AI_DAILY_BUDGET_PLN = "0" + r = staff_client.post( + reverse("ai_search:index"), + {"model": "rekord", "pytanie": "cokolwiek"}, + ) + assert r.status_code == 200 + assert b"limit" in r.content.lower() + assert AISearchQuery.objects.count() == 0 + + +@pytest.mark.django_db +def test_post_null_query_shows_error(staff_client, settings): + settings.BPP_AI_SEARCH_ENABLED = True + res = translator.TranslationResult( + query=None, error="pytanie nieostre", usage={"input_tokens": 5}, attempts=1 + ) + with mock.patch("ai_search.views.translator.translate", return_value=res), \ + mock.patch("ai_search.views.fx.usd_to_pln_rate", return_value=Decimal("4.1")): + r = staff_client.post( + reverse("ai_search:index"), + {"model": "rekord", "pytanie": "najlepsze prace"}, + ) + assert r.status_code == 200 + assert "nieostre" in r.content.decode() + assert AISearchQuery.objects.get().success is False +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_views.py -v` +Expected: FAIL (NoReverseMatch / brak modułów). + +- [ ] **Step 3: Formularz** + +`src/ai_search/forms.py`: +```python +from django import forms + +from bpp.views.zapytanie import MODEL_CHOICES, MODEL_REKORD + + +class AISearchForm(forms.Form): + model = forms.ChoiceField( + choices=MODEL_CHOICES, + widget=forms.RadioSelect, + initial=MODEL_REKORD, + label="Model do przeszukania", + ) + pytanie = forms.CharField( + label="Zadaj pytanie po polsku", + widget=forms.Textarea( + attrs={ + "rows": 3, + "placeholder": "np. publikacje z 2024 roku autora Kowalskiego", + "autocomplete": "off", + } + ), + ) +``` + +- [ ] **Step 4: Widok** + +`src/ai_search/views.py`: +```python +import logging +from datetime import date +from decimal import Decimal +from urllib.parse import urlencode + +import rollbar +from django.conf import settings +from django.http import Http404, HttpResponseRedirect +from django.urls import reverse +from django.views.generic import FormView + +from bpp.views.zapytanie import WprowadzanieDanychOrSuperuserMixin + +from ai_search import budget, fx, pricing, translator +from ai_search.forms import AISearchForm +from ai_search.models import AISearchQuery + +logger = logging.getLogger(__name__) + + +class ZapytanieAIView(WprowadzanieDanychOrSuperuserMixin, FormView): + template_name = "ai_search/zapytanie_ai.html" + form_class = AISearchForm + + def dispatch(self, request, *args, **kwargs): + if not settings.BPP_AI_SEARCH_ENABLED: + raise Http404("Wyszukiwanie AI jest wyłączone.") + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + model_key = form.cleaned_data["model"] + pytanie = form.cleaned_data["pytanie"].strip() + + status = budget.check_budget() + if not status.ok: + return self.render_to_response( + self.get_context_data(form=form, blad=status.reason) + ) + + try: + result = translator.translate(pytanie, model_key) + except Exception: # błędy SDK/sieci — log + generyczny komunikat + rollbar.report_exc_info() + logger.exception("Błąd tłumaczenia AI") + return self.render_to_response( + self.get_context_data( + form=form, + blad="Usługa AI jest chwilowo niedostępna. Spróbuj później " + "lub użyj „szukaj zapytaniem”.", + ) + ) + + self._log(result, model_key, pytanie) + + if result.query: + self.request.session["ai_search_last_question"] = pytanie + params = urlencode({"model": model_key, "query": result.query}) + return HttpResponseRedirect(f"{reverse('bpp:zapytanie')}?{params}") + + return self.render_to_response( + self.get_context_data( + form=form, + blad=result.error or "Nie udało się przetłumaczyć pytania.", + wygenerowany_query=result.query, + ) + ) + + def _log(self, result, model_key, pytanie): + rate = fx.usd_to_pln_rate() + try: + cost_usd = pricing.cost_usd_from_usage( + result.usage, settings.BPP_AI_MODEL, date.today() + ) + except KeyError: + rollbar.report_exc_info() + logger.error("Brak ceny dla modelu %s — koszt nieznany", settings.BPP_AI_MODEL) + cost_usd = Decimal("0") + AISearchQuery.objects.create( + user=self.request.user if self.request.user.is_authenticated else None, + model=settings.BPP_AI_MODEL, + pytanie=pytanie, + wygenerowany_query=result.query or "", + wybrany_model_danych=model_key, + input_tokens=result.usage.get("input_tokens", 0), + output_tokens=result.usage.get("output_tokens", 0), + cache_read_tokens=result.usage.get("cache_read_tokens", 0), + cache_write_tokens=result.usage.get("cache_write_tokens", 0), + cost_usd=cost_usd, + fx_rate=rate, + cost_pln=(cost_usd * rate).quantize(Decimal("0.0001")), + success=bool(result.query), + error=result.error, + retried=result.retried, + ) +``` + +- [ ] **Step 5: URL apki + include w root** + +`src/ai_search/urls.py`: +```python +from django.urls import path + +from ai_search.views import ZapytanieAIView + +app_name = "ai_search" + +urlpatterns = [ + path("", ZapytanieAIView.as_view(), name="index"), +] +``` +W `src/django_bpp/urls.py` dodaj do listy tras (obok innych `include`): +```python + path( + "ai-search/", + include(("ai_search.urls", "ai_search"), namespace="ai_search"), + ), +``` + +- [ ] **Step 6: Szablon** + +`src/ai_search/templates/ai_search/zapytanie_ai.html`: +```django +{% extends "base.html" %} +{% block content %} +
+
+

Szukaj przez sztuczną inteligencję

+

Zadaj pytanie po polsku — zamienimy je na zapytanie i pokażemy wyniki.

+ + {% if blad %} + + {% endif %} + {% if wygenerowany_query %} +
+ Wygenerowane zapytanie: {{ wygenerowany_query }} +
+ {% endif %} + +
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+ +{% endblock %} +``` +(Jeśli `base.html` ma inną nazwę bloku niż `content` — dopasuj do wzoru z `bpp/zapytanie.html`.) + +- [ ] **Step 7: Uruchom testy widoku** + +Run: `uv run pytest src/ai_search/tests/test_views.py -v` +Expected: PASS (5). + +- [ ] **Step 8: Commit** + +```bash +git add src/ai_search/forms.py src/ai_search/views.py src/ai_search/urls.py \ + src/ai_search/templates/ src/django_bpp/urls.py src/ai_search/tests/test_views.py +git commit -m "feat(ai-search): widok+formularz+URL+szablon (budzet, log, redirect na zapytanie)" +``` + +--- + +## Task 12: Integracja z menu top-bar + +**Files:** +- Modify: `src/django_bpp/templates/top_bar.html:34-41` +- Test: `src/ai_search/tests/test_menu.py` + +**Interfaces:** +- Consumes: filtr `can_use_query_editor`, `settings.BPP_AI_SEARCH_ENABLED`, `ai_search:index`. + +- [ ] **Step 1: Test obecności/gate pozycji menu** + +`src/ai_search/tests/test_menu.py`: +```python +import pytest +from django.urls import reverse + + +@pytest.fixture +def staff_client(client, django_user_model): + u = django_user_model.objects.create_user( + username="ed", password="x", is_staff=True, is_superuser=True + ) + client.force_login(u) + return client + + +@pytest.mark.django_db +def test_menu_item_shown_when_enabled(staff_client, settings): + settings.BPP_AI_SEARCH_ENABLED = True + r = staff_client.get("/") + assert reverse("ai_search:index") in r.content.decode() + + +@pytest.mark.django_db +def test_menu_item_hidden_when_disabled(staff_client, settings): + settings.BPP_AI_SEARCH_ENABLED = False + r = staff_client.get("/") + assert reverse("ai_search:index") not in r.content.decode() +``` + +- [ ] **Step 2: Uruchom — ma paść** + +Run: `uv run pytest src/ai_search/tests/test_menu.py -v` +Expected: FAIL (linku brak). + +- [ ] **Step 3: Udostępnij flagę w kontekście szablonu** + +Sprawdź, czy `BPP_AI_SEARCH_ENABLED` jest widoczne w szablonach. Jeśli jest context processor eksponujący `settings` (poszukaj `context_processors` w `settings/base.py`) — użyj go. W przeciwnym razie dodaj lekki context processor: +`src/ai_search/context_processors.py`: +```python +from django.conf import settings + + +def ai_search_flags(request): + return {"BPP_AI_SEARCH_ENABLED": settings.BPP_AI_SEARCH_ENABLED} +``` +i dopisz `"ai_search.context_processors.ai_search_flags"` do `TEMPLATES[0]["OPTIONS"]["context_processors"]` w `base.py`. + +- [ ] **Step 4: Dodaj pozycję menu** + +W `src/django_bpp/templates/top_bar.html`, wewnątrz `
  • diff --git a/src/django_bpp/urls.py b/src/django_bpp/urls.py index 2b3bb42d7..5ae05a943 100644 --- a/src/django_bpp/urls.py +++ b/src/django_bpp/urls.py @@ -212,6 +212,10 @@ def protected_media_serve(request, path, document_root=None): include(("nowe_raporty.urls", "nowe_raporty"), namespace="nowe_raporty"), ), path("raport_slotow/", include("raport_slotow.urls")), + path( + "ai-search/", + include(("ai_search.urls", "ai_search"), namespace="ai_search"), + ), url(r"^bpp/", include(("bpp.urls", "bpp"), namespace="bpp")), url(r"^oswiadczenia/", include("oswiadczenia.urls", namespace="oswiadczenia")), path( diff --git a/uv.lock b/uv.lock index 01da1ed72..fde23331b 100644 --- a/uv.lock +++ b/uv.lock @@ -56,6 +56,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.116.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "platform_python_implementation != 'PyPy'" }, + { name = "distro", marker = "platform_python_implementation != 'PyPy'" }, + { name = "docstring-parser", marker = "platform_python_implementation != 'PyPy'" }, + { name = "httpx", marker = "platform_python_implementation != 'PyPy'" }, + { name = "jiter", marker = "platform_python_implementation != 'PyPy'" }, + { name = "pydantic", marker = "platform_python_implementation != 'PyPy'" }, + { name = "sniffio", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a2/d31f14e28d49bae983a3634e38dfb4b31c50110b5e403596c5c6a20b23f8/anthropic-0.116.0.tar.gz", hash = "sha256:5fc248fbb9fe03ef686f8a774f81586bca31a043260aab88b387ea3660f4a396", size = 949149, upload-time = "2026-07-02T19:08:10.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/dd/2a1e81cf1b163acc340afc4ec74ed1d86f5eed1a809fabdeed3e0997b346/anthropic-0.116.0-py3-none-any.whl", hash = "sha256:6c0a7698e8d652455da3499978279bb2588c7264d0a35be3666009a4258c8256", size = 956896, upload-time = "2026-07-02T19:08:08.756Z" }, +] + [[package]] name = "anyio" version = "4.11.0" @@ -365,6 +393,7 @@ name = "bpp-iplweb" version = "202607.1397" source = { editable = "." } dependencies = [ + { name = "anthropic", marker = "platform_python_implementation != 'PyPy'" }, { name = "arrow", marker = "platform_python_implementation != 'PyPy'" }, { name = "babel", marker = "platform_python_implementation != 'PyPy'" }, { name = "beautifulsoup4", marker = "platform_python_implementation != 'PyPy'" }, @@ -458,6 +487,7 @@ dependencies = [ { name = "mozilla-django-oidc", marker = "platform_python_implementation != 'PyPy'" }, { name = "nh3", marker = "platform_python_implementation != 'PyPy'" }, { name = "numpy", marker = "platform_python_implementation != 'PyPy'" }, + { name = "openai", marker = "platform_python_implementation != 'PyPy'" }, { name = "openpyxl", marker = "platform_python_implementation != 'PyPy'" }, { name = "ortools", marker = "platform_python_implementation != 'PyPy'" }, { name = "pandas", marker = "platform_python_implementation != 'PyPy'" }, @@ -548,6 +578,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "anthropic", specifier = ">=0.40" }, { name = "arrow", specifier = ">=1.3,<2" }, { name = "babel", specifier = ">=2.17" }, { name = "beautifulsoup4", specifier = ">=4.15.0" }, @@ -643,6 +674,7 @@ requires-dist = [ { name = "mozilla-django-oidc", specifier = ">=5.0.2,<6" }, { name = "nh3", specifier = ">=0.3.6" }, { name = "numpy", specifier = ">=2.4.6" }, + { name = "openai", specifier = ">=1.50" }, { name = "openpyxl", specifier = ">=3.1.5" }, { name = "ortools", specifier = ">=9.15.6755" }, { name = "pandas", specifier = ">=3.0.3" }, @@ -1435,6 +1467,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "dj-pagination" version = "2.5.0" @@ -2479,6 +2520,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -2846,6 +2896,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "h11", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + [[package]] name = "httptools" version = "0.8.0" @@ -2889,6 +2952,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, ] +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "platform_python_implementation != 'PyPy'" }, + { name = "certifi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "httpcore", marker = "platform_python_implementation != 'PyPy'" }, + { name = "idna", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "hyperframe" version = "6.1.0" @@ -3112,6 +3190,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -3790,6 +3954,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] +[[package]] +name = "openai" +version = "2.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "platform_python_implementation != 'PyPy'" }, + { name = "distro", marker = "platform_python_implementation != 'PyPy'" }, + { name = "httpx", marker = "platform_python_implementation != 'PyPy'" }, + { name = "jiter", marker = "platform_python_implementation != 'PyPy'" }, + { name = "pydantic", marker = "platform_python_implementation != 'PyPy'" }, + { name = "sniffio", marker = "platform_python_implementation != 'PyPy'" }, + { name = "tqdm", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/60/d4219875289b11d2c2f7da93c36283da224a2e55865ed865ab64e0ce9217/openai-2.45.0.tar.gz", hash = "sha256:10d34ca9c5643bce775852fddbfc172505cb1d4de1ccd101696c3ecff358765d", size = 1109653, upload-time = "2026-07-09T18:02:44.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" }, +] + [[package]] name = "openpyxl" version = "3.1.5" @@ -4357,6 +4540,123 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "platform_python_implementation != 'PyPy'" }, + { name = "pydantic-core", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-inspection", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + [[package]] name = "pydyf" version = "0.11.0" @@ -5924,6 +6224,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { 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" }, +] + [[package]] name = "tzdata" version = "2025.2"