feat(ui): okno postepu z logiem podczas pisania horoskopu
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m57s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 38s
Testy / Kontrola składni wszystkich warstw (push) Successful in 24s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m50s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 34s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 21s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m57s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 38s
Testy / Kontrola składni wszystkich warstw (push) Successful in 24s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m50s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 34s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 21s
Generowanie trwa minutami, a zwykly POST nie dawal zadnego sygnalu — aplikacja wygladala na zawieszona. Teraz w trakcie pracy pojawia sie okno z logiem, zegarem i spinnerem. Log pokazuje RZECZYWISTE zdarzenia z serwera, nie udawany pasek postepu: - app/progress.py — strumien NDJSON; praca leci w watku roboczym, generator odpompowuje kolejke, wiec zdarzenia docz w TRAKCIE pracy, nie na koncu; heartbeat co 10s, zeby proxy nie uznalo polaczenia za martwe, - providers.generate(..., on_event) — raportuje kazda ture (start, czas trwania, liczba znakow, czy urwana), bo to tura trwa, - POST /chart/horoscope/stream w logice + proxy /horoscope/stream w prezentacji. Wynik: ostatnie zdarzenie niesie GOTOWY HTML wyrenderowany z tego samego szablonu, ktory renderuje przeladowanie strony (_prompt_result.html wydzielony z _prompt_block.html). Jedno zrodlo prawdy dla wygladu wyniku — okno wstawia go bez przeladowania. Degradacja: bez strumieniowania w przegladarce formularz idzie klasycznie i wszystko dziala jak wczesniej, tylko bez okna. Blad polaczenia konczy sie komunikatem w logu, nie cisza. BLAD ZNALEZIONY PRZY TESCIE NA ZYWO: petla kontynuacji odejmowala od budzetu ZAMOWIONY limit tury zamiast tokenow faktycznie wyprodukowanych — pierwsza tura zjadala caly budzet, wiec urwana odpowiedz nigdy nie doczekala sie dokonczenia i wracala do uzytkownika jako calosc. Naprawione i pokryte testem regresyjnym. Testy: 176 passed / 1 skipped (logika) + 17 (prezentacja). Zweryfikowane na zywo z wolna atrapa modelu: zdarzenia z poprawnymi czasami, okno z 11 liniami logu, wynik wstawiony bez przeladowania strony. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -265,6 +265,72 @@ def chart_horoscope(req: HoroscopeRequest) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
@app.post("/chart/horoscope/stream")
|
||||
def chart_horoscope_stream(req: HoroscopeRequest):
|
||||
"""To samo co /chart/horoscope, ale strumieniuje POSTĘP w trakcie pracy.
|
||||
|
||||
Pisanie horoskopu trwa minutami — bez sygnału aplikacja wygląda na zawieszoną.
|
||||
Strumień (NDJSON, jedna linia = jedno zdarzenie) niesie RZECZYWISTE etapy:
|
||||
budowę promptu, limity modelu i każdą turę generowania. Ostatnie zdarzenie
|
||||
(`result`) ma identyczny kształt co odpowiedź zwykłego endpointu.
|
||||
"""
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.progress import stream
|
||||
|
||||
def work(emit) -> dict:
|
||||
from app.llm.base import LLMError
|
||||
from app.llm.factory import build_provider
|
||||
from app.llm.limits import plan
|
||||
|
||||
emit({"type": "stage", "message": "Liczę horoskop i szukam wskazań w bazach…"})
|
||||
out = chart_prompt(req)
|
||||
st = out.get("stats", {})
|
||||
emit({"type": "stage", "message":
|
||||
f"Prompt gotowy: {st.get('chars', 0)} znaków, "
|
||||
f"wskazań {st.get('included', 0)}"
|
||||
+ (f", pominięto {st['omitted']}" if st.get("omitted") else "")})
|
||||
if out.get("data_error"):
|
||||
emit({"type": "warn", "message": out["data_error"]})
|
||||
|
||||
try:
|
||||
provider = build_provider(req.provider, req.model)
|
||||
emit({"type": "stage", "message":
|
||||
f"Dostawca: {provider.name}, model: {provider.model}"
|
||||
+ ("" if not provider.leaves_lan else " — dane opuszczają sieć")})
|
||||
|
||||
emit({"type": "stage", "message": "Liczę tokeny promptu…"})
|
||||
prompt_tokens = provider.count_tokens(out["prompt"])
|
||||
budget = plan(provider.name, provider.model, prompt_tokens, req.max_tokens)
|
||||
out["token_plan"] = budget
|
||||
emit({"type": "stage", "message":
|
||||
f"Prompt {prompt_tokens} tok. · okno modelu {budget['context_window']} · "
|
||||
f"na odpowiedź {budget['max_output']}"})
|
||||
for warning in budget["warnings"]:
|
||||
emit({"type": "warn", "message": warning})
|
||||
if budget["warnings"]:
|
||||
out["warnings"] = budget["warnings"]
|
||||
if not budget["fits"]:
|
||||
out["llm_error"] = " ".join(budget["warnings"])
|
||||
return out
|
||||
|
||||
result = provider.generate(out["prompt"], budget["max_output"], on_event=emit)
|
||||
except LLMError as e:
|
||||
emit({"type": "warn", "message": f"Model zawiódł: {e}"})
|
||||
out["llm_error"] = str(e)
|
||||
return out
|
||||
|
||||
out.update(horoscope=result.text, provider=result.provider, model=result.model,
|
||||
leaves_lan=result.leaves_lan, usage=result.usage)
|
||||
return out
|
||||
|
||||
return StreamingResponse(
|
||||
stream(work),
|
||||
media_type="application/x-ndjson",
|
||||
headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/llm/models")
|
||||
def llm_models() -> dict:
|
||||
"""Podpowiedzi modeli per dostawca — UI buduje z tego listę wyboru.
|
||||
|
||||
Reference in New Issue
Block a user