62d6f9d4d5
build / build (push) Failing after 2s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
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>
82 lines
3.2 KiB
Python
82 lines
3.2 KiB
Python
"""Trasy wnoszone przez moduł. Bez tego katalogu w obrazie nie istnieją."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from fastapi import Form
|
|
from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
|
from app.dodatki import katalog, klient
|
|
|
|
_logic: Any = None
|
|
_szablony: Any = None
|
|
_buduj_utc: Any = None
|
|
_blad: Any = None
|
|
|
|
|
|
def zamontuj(app, *, logic: Any, templates: Any, buduj_utc: Any, blad: Any) -> None:
|
|
"""Podpięcie tras i przekazanie modułowi tego, czego potrzebuje.
|
|
|
|
Zależności przychodzą z zewnątrz, zamiast być importowane: moduł nie może
|
|
importować `app.main`, bo to cykl — a przede wszystkim dlatego, że zależność
|
|
ma iść w JEDNĄ stronę. Aplikacja wie o istnieniu modułów opcjonalnych
|
|
(przez neutralny most), moduł nie wie nic o aplikacji.
|
|
"""
|
|
global _logic, _szablony, _buduj_utc, _blad
|
|
_logic, _szablony, _buduj_utc, _blad = logic, templates, buduj_utc, blad
|
|
katalog.ustaw_zaleznosci(logic=logic, templates=templates)
|
|
|
|
@app.post("/horoscope/stream")
|
|
def horoscope_stream( # noqa: ANN202
|
|
profile: str = Form("natal"),
|
|
date: str = Form(...),
|
|
time: str = Form(...),
|
|
tz_offset: float = Form(0.0),
|
|
lat: float = Form(0.0),
|
|
lon: float = Form(0.0),
|
|
prompt_budget: str = Form("medium"),
|
|
llm_provider: str = Form("local"),
|
|
llm_model: str = Form(""),
|
|
from_date: str = Form(""),
|
|
to_date: str = Form(""),
|
|
):
|
|
"""Przekazuje strumień postępu z logiki i DOKLEJA gotowy HTML wyniku.
|
|
|
|
Dzięki temu okno postępu wstawia dokładnie ten sam widok, który
|
|
wyrenderowałoby przeładowanie strony — jedno źródło prawdy dla wyglądu.
|
|
"""
|
|
try:
|
|
iso_utc, _ = _buduj_utc(date, time, tz_offset)
|
|
except ValueError as e:
|
|
return JSONResponse({"detail": f"Niepoprawne dane wejściowe: {e}"},
|
|
status_code=422)
|
|
|
|
payload: dict = {
|
|
"profile": profile, "when_utc": iso_utc, "lat": lat, "lon": lon,
|
|
"budget": prompt_budget, "provider": llm_provider, "model": llm_model,
|
|
}
|
|
if profile == "period" and from_date and to_date:
|
|
payload["from_date"], payload["to_date"] = from_date, to_date
|
|
|
|
def relay():
|
|
try:
|
|
for raw in klient.strumien(_logic, payload):
|
|
try:
|
|
event = json.loads(raw)
|
|
except ValueError:
|
|
continue
|
|
if event.get("type") == "result":
|
|
event["html"] = _szablony.get_template("_prompt_result.html").render(
|
|
prompt_result=event.get("result") or {}
|
|
)
|
|
yield json.dumps(event, ensure_ascii=False) + "\n"
|
|
except httpx.HTTPError as e:
|
|
yield json.dumps({"type": "error", "message": _blad(e)},
|
|
ensure_ascii=False) + "\n"
|
|
|
|
return StreamingResponse(relay(), media_type="application/x-ndjson",
|
|
headers={"Cache-Control": "no-store",
|
|
"X-Accel-Buffering": "no"})
|