feat(llm): horoskop powstaje zawsze — kontynuacja, okna kontekstu, budzet max
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m59s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m51s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 41s
Testy / Kontrola składni wszystkich warstw (push) Successful in 27s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m55s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m48s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 37s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m59s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m51s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 41s
Testy / Kontrola składni wszystkich warstw (push) Successful in 27s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m55s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m48s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 37s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 26s
PRZYCZYNA PUSTYCH ODPOWIEDZI NA ANTHROPICU (potwierdzona w dokumentacji API): domyslnym modelem byl `claude-sonnet-5`, ktory przy POMINIETYM parametrze `thinking` wlacza myslenie adaptacyjne, a `thinking.display` domyslnie jest "omitted". Tokeny myslenia licza sie do max_tokens, wiec przy LLM_MAX_TOKENS=2000 cala tura wychodzila jako bloki `thinking` z pustym tekstem — parser filtrowal type=="text" i zwracal pusty string. Opus 4.8 bez `thinking` nie mysli, wiec tam objaw by nie wystapil. Gwarancja niepustej odpowiedzi (wszyscy trzej dostawcy): - generate() to teraz PETLA, nie pojedynczy strzal: tura -> jesli urwana na limicie, dopisz ture „kontynuuj" w tej samej rozmowie i sklej tekst, - tura zlozona z samego myslenia traktowana jak urwana (nie jak pustka), - pusta i NIE urwana -> jedna proba z podpowiedzia, dopiero potem blad, - kontynuacja konczy sie tura UZYTKOWNIKA — Claude odrzuca prefill asystenta (400), - `thinking` konfigurowany JAWNIE (adaptive + effort=high; ANTHROPIC_THINKING=off). Okna kontekstu i rezerwa na odpowiedz (app/llm/limits.py): - tabela okien/limitow wyjscia per model + nadpisanie z ENV, - plan() liczy okno odpowiedzi jako okno - prompt - margines i NIGDY nie oddaje calego kontekstu promptowi, - Anthropic liczy tokeny DOKLADNIE (/v1/messages/count_tokens), reszta szacuje, - >90 tys. tokenow promptu -> ostrzezenie, ale wyslanie NADAL mozliwe i z pelnym oknem odpowiedzi. UI: suwak budzetu rozszerzony o „bardzo obszerny" i „maksymalny kontekst modelu" (liczony z okna wybranego modelu po odjeciu rezerwy); przy wyniku widac plan tokenow, liczbe tur i ostrzezenia. Domyslny model Anthropic: claude-opus-4-8. Testy: 170 passed / 1 skipped (logika) + 15 (prezentacja). Nowe testy pokrywaja sklejanie kontynuacji, brak prefillu asystenta, ture z samego myslenia, rezerwe na odpowiedz i prog ostrzezenia. Zweryfikowane e2e na atrapie Anthropica odtwarzajacej zgloszony objaw: 3 tury, obie czesci tekstu obecne. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -129,8 +129,10 @@ class PromptRequest(BaseModel):
|
||||
when_utc: datetime
|
||||
lat: float = 0.0
|
||||
lon: float = 0.0
|
||||
budget: str = "medium" # concise | medium | extensive
|
||||
budget: str = "medium" # concise | medium | extensive | huge | max
|
||||
limit: int = 5000
|
||||
provider: str | None = None # do wyliczenia budżetu „max" wg okna modelu
|
||||
model: str | None = None
|
||||
# tylko dla profilu period:
|
||||
from_date: str | None = None
|
||||
to_date: str | None = None
|
||||
@@ -149,7 +151,7 @@ def chart_prompt(req: PromptRequest) -> dict:
|
||||
"""
|
||||
from app.engine.chart import build_chart
|
||||
from app.engine.models import ChartMoment
|
||||
from app.prompt import build_natal_prompt, build_period_prompt
|
||||
from app.prompt import CHARS_PER_TOKEN, MAX_BUDGET, build_natal_prompt, build_period_prompt
|
||||
|
||||
engine = get_engine()
|
||||
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||
@@ -157,6 +159,18 @@ def chart_prompt(req: PromptRequest) -> dict:
|
||||
label = req.when_utc.strftime("%Y-%m-%d %H:%M UTC")
|
||||
data_error = None
|
||||
|
||||
# Budżet „maksymalny kontekst modelu": limit znaków liczymy z okna kontekstu
|
||||
# WYBRANEGO modelu, zawsze po odjęciu miejsca zarezerwowanego na odpowiedź.
|
||||
budget_chars = None
|
||||
if req.budget == MAX_BUDGET:
|
||||
from app.llm.factory import build_provider
|
||||
from app.llm.limits import prompt_token_budget
|
||||
try:
|
||||
prov = build_provider(req.provider)
|
||||
budget_chars = int(prompt_token_budget(prov.name, prov.model) * CHARS_PER_TOKEN)
|
||||
except Exception: # brak klucza/konfiguracji — zapas z tabeli
|
||||
budget_chars = None
|
||||
|
||||
# Warstwa danych dokłada wyłącznie WSKAZANIA. Wyliczenia (horoskop, oś czasu) są
|
||||
# od niej niezależne — gdy padnie, prompt musi zachować wszystko, co policzyliśmy.
|
||||
try:
|
||||
@@ -171,7 +185,7 @@ def chart_prompt(req: PromptRequest) -> dict:
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
data_error = f"Warstwa danych niedostępna: {e}"
|
||||
out = build_natal_prompt(chart, report, req.budget, label)
|
||||
out = build_natal_prompt(chart, report, req.budget, label, budget_chars)
|
||||
|
||||
elif req.profile == "period":
|
||||
if not (req.from_date and req.to_date):
|
||||
@@ -191,7 +205,7 @@ def chart_prompt(req: PromptRequest) -> dict:
|
||||
except httpx.HTTPError as e:
|
||||
data_error = f"Warstwa danych niedostępna: {e}" # oś czasu zostaje
|
||||
out = build_period_prompt(chart, events, req.from_date, req.to_date,
|
||||
req.budget, label)
|
||||
req.budget, label, budget_chars)
|
||||
else:
|
||||
raise HTTPException(422, f"Nieznany profil: {req.profile!r} (natal | period)")
|
||||
except ValueError as e: # nieznany budżet
|
||||
@@ -218,12 +232,26 @@ def chart_horoscope(req: HoroscopeRequest) -> dict:
|
||||
(LOG-32); prezentacja ma na tej podstawie ostrzegać.
|
||||
"""
|
||||
from app.llm.base import LLMError
|
||||
from app.llm.factory import build_provider, max_tokens
|
||||
from app.llm.factory import build_provider
|
||||
from app.llm.limits import plan
|
||||
|
||||
out = chart_prompt(req) # ten sam prompt co w podglądzie
|
||||
try:
|
||||
provider = build_provider(req.provider)
|
||||
result = provider.generate(out["prompt"], req.max_tokens or max_tokens())
|
||||
|
||||
# Ile tokenów ma naprawdę ten prompt i ile zostaje na odpowiedź. Anthropic
|
||||
# liczy dokładnie (własny endpoint), reszta szacuje — od tego zależy, czy
|
||||
# w oknie kontekstu w ogóle zmieści się miejsce na horoskop.
|
||||
prompt_tokens = provider.count_tokens(out["prompt"])
|
||||
budget = plan(provider.name, provider.model, prompt_tokens, req.max_tokens)
|
||||
out["token_plan"] = budget
|
||||
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"])
|
||||
except LLMError as e:
|
||||
# prompt zostaje — użytkownik moze go skopiowac i uzyc recznie
|
||||
out["llm_error"] = str(e)
|
||||
|
||||
Reference in New Issue
Block a user