From 163ace4283cfe59f8b533687eba3ef6b6d2a69da Mon Sep 17 00:00:00 2001 From: migatu Date: Wed, 22 Jul 2026 19:06:31 +0200 Subject: [PATCH] feat(ui): interaktywny wybor modelu u dostawcy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uzytkownik wybiera nie tylko dostawce, ale konkretny model — Fable czy Opus u Anthropica, gpt-4o-mini czy gpt-5 u OpenAI, cokolwiek ma pobrane lokalnie. - app/llm/catalog.py: podpowiedzi modeli per dostawca wraz z oknem kontekstu, nadpisywalne przez _MODELS; GET /llm/models wystawia je dla UI. - Pole modelu w UI jest TEKSTOWE z datalista, nie zamknietym : +// katalog to tylko wygoda, a konto może mieć dostęp do modeli, o których kod nie +// wie. Puste pole = model domyślny dostawcy. +document.addEventListener('DOMContentLoaded', function () { + const providerEl = document.getElementById('llmProvider'); + const modelEl = document.getElementById('llmModel'); + const listEl = document.getElementById('llmModelList'); + const hintEl = document.getElementById('llmModelHint'); + const dataEl = document.getElementById('llmCatalog'); + if (!providerEl || !modelEl || !listEl || !dataEl) return; + + let catalog = {}; + let defaults = {}; + try { + const parsed = JSON.parse(dataEl.textContent || '{}'); + catalog = parsed.providers || {}; + defaults = parsed.defaults || {}; + } catch (e) { + return; // brak katalogu — pole nadal działa jako wolny tekst + } + + const fmt = n => + n >= 1000000 ? (n / 1000000) + 'M' : (n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)); + + function refresh(resetValue) { + const provider = providerEl.value; + const models = catalog[provider] || []; + + listEl.innerHTML = ''; + models.forEach(m => { + const opt = document.createElement('option'); + opt.value = m.id; + opt.label = m.label + ' · kontekst ' + fmt(m.context_window); + listEl.appendChild(opt); + }); + + const fallback = defaults[provider] || ''; + modelEl.placeholder = fallback ? 'domyślny: ' + fallback : 'domyślny dostawcy'; + + // po zmianie dostawcy stary model nie ma sensu (np. gpt-4o u Anthropica) + if (resetValue) modelEl.value = ''; + + if (hintEl) { + const chosen = models.find(m => m.id === modelEl.value.trim()); + hintEl.textContent = chosen + ? chosen.label + ' — okno kontekstu ' + fmt(chosen.context_window) + + ' tokenów, maksymalna odpowiedź ' + fmt(chosen.max_output) + '.' + : 'Zostaw puste, by użyć modelu domyślnego. Możesz też wpisać dowolny ' + + 'identyfikator modelu, do którego Twoje konto ma dostęp.'; + } + } + + providerEl.addEventListener('change', () => refresh(true)); + modelEl.addEventListener('input', () => refresh(false)); + refresh(false); +}); diff --git a/services/presentation/app/templates/_prompt_block.html b/services/presentation/app/templates/_prompt_block.html index 5d4c57d..363fc6d 100644 --- a/services/presentation/app/templates/_prompt_block.html +++ b/services/presentation/app/templates/_prompt_block.html @@ -1,3 +1,6 @@ +{# Katalog modeli wstrzykiwany przez handler — pole jest tekstowe, więc + można wpisać dowolny model, do którego konto ma dostęp. #} + {# Wspólny blok: sterowanie generowaniem + prompt (LOG-29/30) + horoskop (LOG-31). Używany na ekranach Interpretacje i Kalendarz. #}
@@ -11,15 +14,22 @@ - +
+

Prompt zawiera oryginalne opisy z baz oraz dane urodzeniowe. Model lokalny przetwarza je u nas; wybór dostawcy w chmurze oznacza, że ta treść opuszcza naszą diff --git a/services/presentation/app/templates/interpret.html b/services/presentation/app/templates/interpret.html index f514fa2..584c4d5 100644 --- a/services/presentation/app/templates/interpret.html +++ b/services/presentation/app/templates/interpret.html @@ -90,4 +90,5 @@ + {% endblock %} diff --git a/services/presentation/app/templates/timeline.html b/services/presentation/app/templates/timeline.html index 4ac208a..fbe01e5 100644 --- a/services/presentation/app/templates/timeline.html +++ b/services/presentation/app/templates/timeline.html @@ -80,4 +80,5 @@ + {% endblock %} diff --git a/services/presentation/tests/test_client_auth.py b/services/presentation/tests/test_client_auth.py index 2ea038b..e29d6cc 100644 --- a/services/presentation/tests/test_client_auth.py +++ b/services/presentation/tests/test_client_auth.py @@ -60,3 +60,52 @@ def test_auth_headers_helper_is_lazy(): os.environ["INTERNAL_TOKEN"] = old else: os.environ.pop("INTERNAL_TOKEN", None) + + +# --------------------------------------------- wybor modelu musi dojsc do logiki +# Ta sama klasa bledu co przy tokenie: dokladajac nowa sciezke latwo zapomniec +# przekazac parametr, a objaw (cichy powrot do modelu domyslnego) jest niewidoczny. + +def _calls_to(path_fragment: str) -> list[int]: + """Linie wywolan logic.(...) w handlerach prezentacji.""" + main = CLIENT.parent.parent / "main.py" + tree = ast.parse(main.read_text(encoding="utf-8")) + out = [] + for node in ast.walk(tree): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and node.func.attr == path_fragment + and isinstance(node.func.value, ast.Name) and node.func.value.id == "logic"): + out.append(node.lineno) + return out + + +def _has_kwarg(main_src: str, lineno: int, name: str) -> bool: + tree = ast.parse(main_src) + for node in ast.walk(tree): + if isinstance(node, ast.Call) and node.lineno == lineno: + return any(kw.arg == name for kw in node.keywords) + return False + + +def test_every_llm_call_passes_selected_model(): + main = CLIENT.parent.parent / "main.py" + src = main.read_text(encoding="utf-8") + missing = [] + for method in ("prompt", "horoscope"): + for line in _calls_to(method): + if not _has_kwarg(src, line, "model"): + missing.append(f"main.py:{line} logic.{method}()") + assert not missing, ( + "Wywołania bez wybranego modelu — po cichu użyją domyślnego: " + ", ".join(missing) + ) + + +def test_every_llm_call_passes_provider(): + main = CLIENT.parent.parent / "main.py" + src = main.read_text(encoding="utf-8") + missing = [] + for method in ("prompt", "horoscope"): + for line in _calls_to(method): + if not _has_kwarg(src, line, "provider"): + missing.append(f"main.py:{line} logic.{method}()") + assert not missing, "Wywołania bez dostawcy: " + ", ".join(missing)