Skip to content

feat(ai-search): wyszukiwanie przez AI (NL→DjangoQL) + konfigurowalny backend (Anthropic/lokalny)#451

Open
mpasternak wants to merge 24 commits into
devfrom
feat/ai-search
Open

feat(ai-search): wyszukiwanie przez AI (NL→DjangoQL) + konfigurowalny backend (Anthropic/lokalny)#451
mpasternak wants to merge 24 commits into
devfrom
feat/ai-search

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Co to robi

Nowy tryb wyszukiwania „przez sztuczną inteligencję" (menu top-bar „szukaj").
Użytkownik pisze pytanie po polsku → Claude tłumaczy je na zapytanie DjangoQL →
po walidacji następuje redirect na istniejące /bpp/zapytanie/, które je
wykonuje. Maksymalny reuse: nie budujemy nowego silnika zapytań.

Główne elementy

  • Nowa apka src/ai_search/: translator (NL→DjangoQL), walidacja składni
    DjangoQL + bounded retry z konkretnym błędem (linia/kolumna), log kosztu
    (AISearchQuery), admin read-only, Celery-beat regeneracja cache schematu.
  • Konfigurowalny backend LLM:
    • anthropic (domyślny, płatny) — oficjalny SDK, prompt caching, structured
      outputs, thinking wyłączone; koszt liczony z usage × cennik (Decimal,
      intro-pricing), przeliczany USD→PLN kursem NBP (cache + trwały fallback).
    • openai (OpenAI-compatible, modele lokalne: Ollama / llama.cpp / vLLM /
      LM Studio / LocalAI) — jeden backend na cały ekosystem; darmowy → budżet i
      koszt nieaktywne. Wybór backendu przez jedno źródło prawdy
      (active_backend_name, odporne na literówki → bezpieczny fallback na
      anthropic-z-budżetem).
  • Kontrola kosztów (tylko anthropic): budżety PLN dzienny/miesięczny, twardy
    blok, re-check przed każdym wywołaniem/retry, cap retry = 2, twardy max_tokens
    • timeout.
  • Uszczelnienie współdzielonej BppQLSchema (używanej też przez istniejący
    edytor „zapytaniem" i adminy):
    • zamknięty realny wyciek: autorzy.autor.user.password_change_required nie
      jest już wyrażalny;
    • BppUser → tylko username bez wartości (fk_options); uczelnia przycięta
      do id/nazwa/skrot/pbn_uid; wykluczone modele nie-użytkowe/cache/test;
    • 216 → 87 modeli w schemacie; słowniki (charakter_formalny, typ_kbn,
      język, typ_odpowiedzialności) emitują teraz wartości nazwa dla lepszej
      trafności; compact schema ~22–31k tokenów.
  • Dependencje: djangoql-iplweb>=0.30.1 (field-level include/exclude + LLM
    schema), anthropic, openai.
  • Docs: docs/deweloper/ai-search.md — konfiguracja, przykład Ollama,
    szacunek kontekstu (~22–31k tok → model ≥32k, albo BPP_AI_MAX_FK_OPTIONS=0)
    i zalecane modele OSS (Qwen3 głównie; Llama 3.1 8B / Gemma 2 9B; llama.cpp
    • GBNF na twardą składnię).

Konfiguracja (env/settings)

BPP_AI_SEARCH_ENABLED, BPP_AI_BACKEND (anthropic|openai), BPP_AI_MODEL,
ANTHROPIC_API_KEY / (BPP_AI_BASE_URL+BPP_AI_API_KEY dla openai),
BPP_AI_DAILY_BUDGET_PLN, BPP_AI_MONTHLY_BUDGET_PLN, BPP_AI_MAX_RETRIES,
BPP_AI_MAX_FK_OPTIONS. Domyślnie feature wyłączony.

Bezpieczeństwo / prywatność

  • Nazwiska autorów, tytuły źródeł, loginy BppUser nie trafiają do modelu
    (auto-mode omija duże tabele + jawne fk_options=False na relacji do usera).
    Enumy choices i wartości małych słowników — tak (użyteczne, bezpieczne).
  • Uszczelnienie poprawia też higienę istniejącego edytora/introspekcji.

Testy

Pełna suita lokalnie: 5764 passed, 6 skipped, 1 xfailed (0 failures).
Feature pokryty (translator/backendy/budget/fx/pricing/schema-hardening/widok/
menu + testy gałęzi błędów; accuracy-set pod skipif ANTHROPIC_API_KEY).

Uwagi do scalenia

  • Baseline: odświeżyć make baseline-update przy scalaniu (migracje
    ai_search 0001/0002) — zgodnie z polityką repo (nie w gałęzi).
  • Fast-follow: djangoql PR Bump jquery from 3.4.1 to 3.5.0 #16 (AUTH_USER_MODEL jako sensitive w auto-mode,
    systemowa obrona) → po release 0.30.2 bump pinu (obecnie fk_options zamyka
    jedyną żywą ścieżkę).

Proces: spec → plan → TDD task-by-task z per-task i finalnym whole-branch review
(docs/superpowers/).

🤖 Generated with Claude Code

mpasternak and others added 21 commits July 4, 2026 22:27
NL (polski) -> Claude (SDK anthropic, Sonnet 5) -> DjangoQL -> redirect na
istniejace 'szukaj zapytaniem'. Nowa apka src/ai_search/, budzety w PLN
(NBP FX), log kosztu, walidacja+bounded retry z konkretnym bledem DjangoQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… mult)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…obetnij BppUser

Zamyka realny wyciek danych: z Rekord dało się dojść przez
autorzy.autor.user.password_change_required do password_policies.
excluded() odcina aplikacje bezpieczeństwa/PII/systemu (HARD) i
robocze/narzędziowe (SOFT) oraz tabele cache_* z grafu DjangoQL;
get_fields() obcina BppUser do allowlisty username/nazwisko/imiona.
Graf modeli (BFS z Rekord) spada z 216 do 87. pbn_api celowo NIE
wykluczone (niesie pbn_uid). Wszystkie istniejące suity (test_zapytanie,
djangoql/multiseek, admin) zielone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migruje reczne przycinanie BppUser w BppQLSchema na deklaratywne
include_fields z djangoql-iplweb>=0.30 i dodaje analogiczne przyciecie dla
Uczelnia (tylko id/nazwa/skrot/pbn_uid - bez pol konfiguracyjnych typu
haseł do zewnetrznych API, integracji PBN/DSpace/ORCID/Clarivate).

Dwa realne ryzyka obejscia filtra zalatane w tym samym commicie:
- RelPickerSchemaMixin dokladal picker <fk>__rel dla KAZDEGO FK modelu,
  wiec mogl odslonic pole ktore include_fields wlasnie ukryl - poprawka
  dodaje picker tylko gdy bazowe pole przetrwalo super().get_fields().
- Wbudowane w ExtrasSchema mixiny (AggregateSchemaMixin: <rel>__count,
  DatePartsSchemaMixin: <pole>__year itp.) tez iteruja
  model._meta.get_fields() bezposrednio, wiec dla BppUser/Uczelnia i tak
  przeciekaly pochodne pola (np. password_history_entries__count,
  last_login__year) - dodany finalny re-trim w BppQLSchema.get_fields
  po wszystkich mixinach.

Nowe testy w test_djangoql_schema_hardening.py pokrywaja oba przypadki.
Rozmiar compact dumpu schematu Rekord: 103507 -> 88015 B (-15%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…angoql>=0.30.1

djangoql-iplweb 0.30.1 w auto-mode describe_schema_for_llm zaczal emitowac
related_values dla malych slownikow -- w tym loginy BppUser przez jedyna
eksponowana relacje bpp.autor.user. Jawny fk_options={Autor: {"user": False}}
uszczelnia to belt-and-suspenders obok istniejacych wykluczen (nie wplywa
na edytor/admin, ktore fk_options nie czytaja).
…ions z settings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…irect na zapytanie)

Wiaze budget guard, translator i logowanie kosztu AISearchQuery z
istniejacym edytorem zapytan: ZapytanieAIView przyjmuje pytanie PL,
sprawdza budzet, tlumaczy na DjangoQL, loguje koszt (USD->PLN) i albo
przekierowuje do bpp:zapytanie z gotowym DSL-em, albo pokazuje blad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t, testy galezi bledow, docs

Celery beat (ai_search.tasks.regenerate_schemas, codziennie 3:45) odswieza
cache compact-schematu dla "rekord"/"autor", zeby nowe wartosci slownikowe
z bazy pojawialy sie w promptcie LLM bez czekania na TTL. Admin AISearchQuery
jest read-only (log kosztow/audytu). Dodano smoke accuracy-set (skipif bez
ANTHROPIC_API_KEY, wylaczony z realnego CI), management command
ai_search_schema_dump do inspekcji/pomiaru rozmiaru schematu, dwa testy
brakujacych galezi bledow z Task 11 review (translate() rzuca wyjatek ->
przyjazny komunikat + Rollbar; cost_usd_from_usage() rzuca KeyError -> log
z kosztem zerowym + Rollbar) oraz krotka dokumentacja feature'u.
…xRate tiebreaker, martwy kwarg/logger, docstring)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tible dla modeli lokalnych) + docs

Nowy ai_search/backends.py wydziela wywolanie modelu z translator.py:
AnthropicBackend (natywny SDK, bez zmiany zachowania) i
OpenAICompatibleBackend (openai SDK, dla Ollama/llama.cpp/vLLM/LM
Studio/LocalAI). Wybor przez BPP_AI_BACKEND (domyslnie "anthropic").
Widok pomija budzet/cennik/FX dla backendu "openai" (lokalny = darmowy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ docstring/docs/config-guard

Literowka w BPP_AI_BACKEND (np. "Anthropic") kierowala get_backend() na
platne Anthropic API, podczas gdy views.py (por. do settings.BPP_AI_BACKEND
== "anthropic") traktowal ja jak backend darmowy - pomijal budzet i logowal
koszt 0. Dodaje backends.active_backend_name() jako jedyne zrodlo prawdy
(uzywane przez get_backend() i views.py) - nieznana wartosc trafia zawsze
na bezpieczna sciezke anthropic+budzet, nigdy na darmowa-sciezke z realnym
wydatkiem. Plus: aktualny docstring translatora, ostrzezenie w docs o
trybie "thinking" Qwen3 (ucina JSON przy max_tokens=500) i czytelny
ImproperlyConfigured gdy backend openai bez BPP_AI_BASE_URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented Jul 5, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
34572324 Triggered Username Password bbbcc2f src/bpp/tests/test_djangoql_schema_hardening.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

mpasternak and others added 2 commits July 5, 2026 20:41
…tive w auto-mode)

Konsumuje PR #16 djangoql-iplweb: systemowa obrona AUTH_USER_MODEL w auto-mode
(zadna relacja do modelu uzytkownika nie emituje wartosci). fk_options
{Autor:{user:False}} zostaje jako pas bezpieczenstwa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(false-positive GitGuardian)

Prefiks 'sk-' wygladal jak klucz OpenAI i wywalal secret-scan; to tylko
placeholder dla lokalnych serwerow OpenAI-compatible ktore nie wymagaja auth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mpasternak

Copy link
Copy Markdown
Member Author

Code review

Found 1 issue:

  1. usd_to_pln_rate() deklaruje w docstringu "Nigdy nie podnosi wyjątku", ale zdeformowana odpowiedź NBP z HTTP 200 wymyka się klauzuli except: {"rates": []} rzuca IndexError (linia 17), a "mid": null rzuca decimal.InvalidOperation (linia 18; to podklasa ArithmeticError, nie ValueError) — żadnego z nich nie łapie except (requests.RequestException, OSError, KeyError, ValueError). Nieosłonięte miejsce wywołania to ZapytanieAIView._log(), uruchamiane poza try/except widoku — request kończy się 500 już po udanym (i opłaconym) wywołaniu Anthropic, a koszt nie zostaje zalogowany w AISearchQuery.

bpp/src/ai_search/fx.py

Lines 14 to 26 in b5510b9

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.
"""

bpp/src/ai_search/fx.py

Lines 37 to 39 in b5510b9

return rate
except (requests.RequestException, OSError, KeyError, ValueError) as exc:
logger.warning("NBP FX niedostępny (%s), używam fallbacku", exc)

self._log(result, model_key, pytanie)

if backends.active_backend_name() == "anthropic":
rate = fx.usd_to_pln_rate()
try:

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- fx.usd_to_pln_rate: zdeformowana odpowiedź NBP z HTTP 200 (puste
  "rates" → IndexError, mid=null → decimal.InvalidOperation, rates
  nie-listą → TypeError) uciekała z except i łamała kontrakt „Nigdy
  nie podnosi wyjątku", wywalając request już po opłaconym wywołaniu
  LLM; poszerzono tuple o LookupError/TypeError/ArithmeticError
  + 3 testy regresyjne (TDD red→green)
- views._log: date.today() → timezone.localdate() przy wyborze tieru
  cenowego (spójnie z budget.py, poprawny dzień przy TZ != Warszawa)
- translator: docstring modułu opisuje twardy limit
  min(BPP_AI_MAX_RETRIES, 2) zamiast sugerować brak capu
- test_graph_significantly_smaller: docstring poprawiony na stan
  faktyczny — 86 modeli (bpp + pbn_api + taggit), pbn_export_queue
  nieosiągalne

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mpasternak

Copy link
Copy Markdown
Member Author

Wnioski z review naprawione w 559c8fe5f5324b1069cd93cbd07c761e9a86234c: poszerzony except w fx.usd_to_pln_rate() (+3 testy regresyjne na zdeformowane odpowiedzi NBP), timezone.localdate() zamiast date.today() w wycenie, oraz korekty docstringów (twardy cap retry = 2, graf = 86 modeli bez pbn_export_queue).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant