d094a145b1
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m44s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Drugi z pięciu kroków budowy trzech produktów. Czysty refaktor — zachowanie
aplikacji się nie zmienia, liczba testów rośnie tylko o nowe.
DLACZEGO WARUNEK W SZABLONIE NIE WYSTARCZA. Dotąd generowanie było chowane przez
`{% if can(request, 'ai') %}`. To sprawia, że funkcji nie WIDAĆ, ale nie że jej
NIE MA: plik szablonu dalej leży w obrazie i dalej zawiera jej nazwy, więc `grep`
po kontenerze pokazuje wszystko, czego warunek nie pokazał na ekranie. Astroklient
ma nie mieć śladu, nie mieć wyłączoną funkcję — więc granica musi być KATALOGIEM.
CO SIĘ PRZENIOSŁO do app/dodatki/: dwa szablony, pięć plików statycznych, trasa
strumienia, cztery metody klienta warstwy logicznej, wpis w katalogu funkcji,
wpisy tras i zasobów, akcje formularza i reguły CSS nazywające funkcję
(`textarea.prompt`).
JAK APLIKACJA O TO PYTA. Neutralny most `app/rozszerzenia.py` odpowiada wyłącznie
na pytanie „czy coś jest podpięte i co wnosi". Sam nie wymienia ani jednej nazwy —
pilnuje tego osobny test, bo most jest w KAŻDYM obrazie. Dlatego też katalog
nazywa się `dodatki`, a nie `ai`: nazwa w instrukcji importu byłaby dokładnie tym
śladem, którego wydzielanie ma się pozbyć.
TRZY MIEJSCA, KTÓRE OKAZAŁY SIĘ TRUDNE:
1. Pola formularza. FastAPI czyta je z SYGNATURY, a wspólny handler nie może
wymieniać `prompt_budget` ani `llm_provider`. Rozwiązane zależnością: moduł
deklaruje własne pola u siebie, handler wie tylko, że dostaje słownik.
2. Klient warstwy logicznej jest wspólny, więc metody `prompt`/`horoscope`/
`llm_models` musiały z niego wyjść. Wspólny klient daje teraz samą drogę
w dół (`wywolaj`, `pobierz`, `strumien`) — z szyfrowaniem łącza i tokenem
międzywarstwowym; co nią pojedzie, jest sprawą modułu.
3. Podtytuł ekranu Skompiluj miał wariant „z AI" i wariant bez. Zamiast warunku
jedno zdanie prawdziwe niezależnie od tego, jakie moduły są w obrazie.
DOCKERFILE. `COPY . .` wnosiło do obrazu także testy — a plik testowy nazywa
funkcje wprost. Teraz wchodzi wyłącznie `app/`.
TEST GRANICY. `test_ai_tylko_w_module.py` przechodzi po wszystkim poza modułem
i szuka siedemnastu słów. Znalazł dwanaście resztek, których nie widziałem:
komentarze w main.py i security.py, przykład `models.js` w komentarzu features.py,
komentarze w compile.js i w trzech arkuszach, oraz regułę `textarea.prompt`,
która przy rozbijaniu CSS trafiła do arkuszy ekranów zamiast do modułu.
Ma też kontrolę pozytywną (te słowa MAJĄ padać w module — inaczej test
przechodziłby, gdyby funkcję wydrążono) i test odłączalności mostu.
SPRAWDZONE NA KOPII BEZ MODUŁU: aplikacja startuje, wszystkie ekrany oddają 200,
katalog ma 13 funkcji zamiast 14, trasy strumienia nie ma, spreparowane
`action=prompt` wraca do akcji domyślnej bez śladu, a `grep` po drzewie nie
znajduje ani jednego ze słów.
Testy: presentation 362, logic 342, data 37, render 41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.6 KiB
Python
92 lines
3.6 KiB
Python
"""Co moduł wnosi do aplikacji: funkcję, trasy, zasoby, akcje i pola formularza.
|
|
|
|
Wszystko, co gdziekolwiek indziej musiałoby paść z nazwy, jest zebrane TUTAJ.
|
|
Reszta aplikacji zna wyłącznie neutralny most (`app.rozszerzenia`) i pyta go
|
|
o dane, nigdy o „generowanie" czy „model".
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from fastapi import Form, Request
|
|
|
|
from app.dodatki import klient
|
|
|
|
# Pozycja w katalogu funkcji — surowa krotka, żeby nie importować `features`
|
|
# (to zamknęłoby cykl features → most → moduł → features).
|
|
FUNKCJA: tuple[tuple[str, str, str, str], ...] = (
|
|
("ai", "Generowanie tekstu przez model",
|
|
"Horoskopy pisane przez model językowy. UWAGA: każde użycie kosztuje.",
|
|
"Rozszerzenia"),
|
|
)
|
|
|
|
TRASY: dict[tuple[str, str], str] = {("POST", "/horoscope/stream"): "ai"}
|
|
|
|
ZASOBY: dict[str, str] = {
|
|
"x-ai.css": "ai",
|
|
"models.js": "ai",
|
|
"progress.js": "ai",
|
|
"natal.js": "ai",
|
|
"predictions.js": "ai",
|
|
}
|
|
|
|
# Akcje formularza wnoszone przez moduł: nazwa → wymagane uprawnienie.
|
|
AKCJE: dict[str, str] = {"prompt": "ai", "horoscope": "ai"}
|
|
|
|
# Wstrzykiwane przez `zamontuj` z main.py — moduł nie importuje aplikacji,
|
|
# bo to byłby cykl, a przy okazji zmusiłoby aplikację do wiedzy o module.
|
|
_logic: Any = None
|
|
_szablony: Any = None
|
|
|
|
|
|
def ustaw_zaleznosci(*, logic: Any, templates: Any) -> None:
|
|
global _logic, _szablony
|
|
_logic, _szablony = logic, templates
|
|
|
|
|
|
def pola(prompt_budget: str = Form("medium"),
|
|
llm_provider: str = Form(""),
|
|
llm_model: str = Form("")) -> dict:
|
|
"""Pola formularza, których wspólny handler nie może wymienić w sygnaturze.
|
|
|
|
FastAPI czyta pola z sygnatury, więc zadeklarowanie ich we wspólnym handlerze
|
|
wpisałoby nazwy `llm_provider` i `prompt_budget` w kod, który trafia także do
|
|
obrazu bez tego modułu. Zależność przenosi deklarację tutaj."""
|
|
return {"prompt_budget": prompt_budget, "llm_provider": llm_provider,
|
|
"llm_model": llm_model}
|
|
|
|
|
|
def _katalog() -> dict:
|
|
"""Podpowiedzi modeli dla pola wyboru. Awaria logiki nie może wywrócić strony —
|
|
pole modelu jest tekstowe, więc bez katalogu nadal da się wpisać model ręcznie."""
|
|
try:
|
|
return klient.modele(_logic)
|
|
except httpx.HTTPError:
|
|
return {"providers": {}, "defaults": {}}
|
|
|
|
|
|
def katalog_dla(request: Request) -> dict:
|
|
"""Katalog modeli do kontekstu szablonu — pusty, gdy konto nie ma uprawnienia.
|
|
|
|
Katalog trafia do strony jako blok JSON, więc bez tego ograniczenia nazwy
|
|
dostawców, modeli i rozmiary okien kontekstu byłyby w źródle każdej strony,
|
|
także dla kont, które o modelach nie mają prawa wiedzieć."""
|
|
perms = getattr(getattr(request.state, "principal", None), "permissions", frozenset())
|
|
return {"llm_catalog": _katalog() if "ai" in perms else {}}
|
|
|
|
|
|
def obsluz(akcja: str, *, profil: str, iso_utc: str, lat: float, lon: float,
|
|
dodatki: dict, od: str = "", do: str = "", **_reszta) -> dict | None:
|
|
"""Wykonanie akcji modułu. None znaczy „to nie moja akcja"."""
|
|
if akcja not in AKCJE:
|
|
return None
|
|
metoda = klient.prompt if akcja == "prompt" else klient.horoscope
|
|
argumenty = {"logic": _logic, "profile": profil, "when_utc_iso": iso_utc, "lat": lat, "lon": lon,
|
|
"budget": dodatki.get("prompt_budget", "medium"),
|
|
"provider": dodatki.get("llm_provider", ""),
|
|
"model": dodatki.get("llm_model", "")}
|
|
if profil == "period":
|
|
argumenty["from_date"], argumenty["to_date"] = od, do
|
|
return metoda(**argumenty)
|