feat(logic): dostawcy LLM i pisanie horoskopu (LOG-31)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m48s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 29s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m46s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 26s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m48s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 29s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m46s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 26s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 19s
Domyslnie model LOKALNY — prompt niesie oryginalne opisy z baz, wiec domyslnie NIC nie opuszcza sieci. Chmura wlaczana swiadomie (LOG-32). - app/llm/: LLMProvider (jak EphemerisEngine z LOG-24) + dwie implementacje. Lokalny serwer modelu (Ollama/vLLM/llama.cpp) i OpenAI mowia TYM SAMYM protokolem /chat/completions, wiec obsluguje je jedna klasa; Anthropic ma wlasny /v1/messages. Napisane na samym httpx — bez SDK openai/anthropic: mniej zaleznosci i pelna kontrola nad tym, co wychodzi z sieci. - Kazdy dostawca deklaruje `leaves_lan` — interfejs MUSI jawnie mowic, czy tresc baz opuszcza siec; UI na tej podstawie ostrzega. - Ponawianie z backoffem (429/5xx), timeouty, czytelne bledy zamiast stacktrace. - Klucz wylacznie z LLM_API_KEY (sekret), nigdy w repo ani w UI. - POST /chart/horoscope + GET /llm/health. WAZNE: prompt jest zwracany ZAWSZE — takze gdy model padnie lub brakuje klucza. Dzieki temu awaria dostawcy nie blokuje pracy: prompt mozna skopiowac i uzyc recznie. Prezentacja: wybor modelu (lokalny/Anthropic/OpenAI), przycisk „Napisz horoskop (AI)", wynik z informacja kto go napisal, czy dane opuscily siec, ile wskazan weszlo, oraz zastrzezenie ze to nie porada medyczna (PRE-15). Testy: 13 nowych (transport podstawiony — zaden prawdziwy model nie wolany), calosc 131 passed / 1 skipped. Zweryfikowane e2e na atrapie serwera modelu: horoskop napisany, leaves_lan=false, tokeny zliczone; a przy padnietym modelu / braku klucza / zlym dostawcy — czytelny blad i zachowany prompt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,28 @@ class LogicClient:
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def horoscope(
|
||||
self, profile: str, when_utc_iso: str, lat: float, lon: float,
|
||||
budget: str = "medium", provider: str | None = None,
|
||||
from_date: str | None = None, to_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Napisany horoskop (LOG-31) — woła logic /chart/horoscope.
|
||||
|
||||
Generowanie tekstu trwa (zwłaszcza na modelu lokalnym), stąd długi timeout.
|
||||
"""
|
||||
payload: dict[str, Any] = {
|
||||
"profile": profile, "when_utc": when_utc_iso,
|
||||
"lat": lat, "lon": lon, "budget": budget,
|
||||
}
|
||||
if provider:
|
||||
payload["provider"] = provider
|
||||
if from_date and to_date:
|
||||
payload["from_date"], payload["to_date"] = from_date, to_date
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 300.0)) as client:
|
||||
r = client.post(f"{self.base_url}/chart/horoscope", json=payload)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def timeline(
|
||||
self, when_utc_iso: str, lat: float, lon: float,
|
||||
from_date: str, to_date: str, interpret: bool = True,
|
||||
|
||||
@@ -129,9 +129,11 @@ def interpret_run(
|
||||
group: bool = Form(False),
|
||||
action: str = Form("report"),
|
||||
prompt_budget: str = Form("medium"),
|
||||
llm_provider: str = Form("local"),
|
||||
):
|
||||
form = {"date": date, "time": time, "tz_offset": tz_offset,
|
||||
"lat": lat, "lon": lon, "group": group, "prompt_budget": prompt_budget}
|
||||
"lat": lat, "lon": lon, "group": group, "prompt_budget": prompt_budget,
|
||||
"llm_provider": llm_provider}
|
||||
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
|
||||
try:
|
||||
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||
@@ -140,6 +142,11 @@ def interpret_run(
|
||||
ctx["prompt_result"] = logic.prompt(
|
||||
profile="natal", when_utc_iso=iso_utc, lat=lat, lon=lon, budget=prompt_budget,
|
||||
)
|
||||
elif action == "horoscope":
|
||||
ctx["prompt_result"] = logic.horoscope(
|
||||
profile="natal", when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||
budget=prompt_budget, provider=llm_provider,
|
||||
)
|
||||
else:
|
||||
ctx["result"] = logic.report(when_utc_iso=iso_utc, lat=lat, lon=lon, group=group)
|
||||
except httpx.HTTPError as e:
|
||||
@@ -170,9 +177,11 @@ def timeline_run(
|
||||
to_date: str = Form(...),
|
||||
action: str = Form("timeline"),
|
||||
prompt_budget: str = Form("medium"),
|
||||
llm_provider: str = Form("local"),
|
||||
):
|
||||
form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon,
|
||||
"from_date": from_date, "to_date": to_date, "prompt_budget": prompt_budget}
|
||||
"from_date": from_date, "to_date": to_date, "prompt_budget": prompt_budget,
|
||||
"llm_provider": llm_provider}
|
||||
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
|
||||
try:
|
||||
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||
@@ -182,6 +191,12 @@ def timeline_run(
|
||||
profile="period", when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||
budget=prompt_budget, from_date=from_date, to_date=to_date,
|
||||
)
|
||||
elif action == "horoscope":
|
||||
ctx["prompt_result"] = logic.horoscope(
|
||||
profile="period", when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||
budget=prompt_budget, provider=llm_provider,
|
||||
from_date=from_date, to_date=to_date,
|
||||
)
|
||||
else:
|
||||
ctx["result"] = logic.timeline(
|
||||
when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{# Wspólny blok: sterowanie budżetem + wynik generatora promptu (LOG-29/30).
|
||||
{# Wspólny blok: sterowanie generowaniem + prompt (LOG-29/30) + horoskop (LOG-31).
|
||||
Używany na ekranach Interpretacje i Kalendarz. #}
|
||||
<div class="opts">
|
||||
<label>Budżet promptu
|
||||
@@ -9,12 +9,58 @@
|
||||
<option value="extensive" {{ 'selected' if pb == 'extensive' else '' }}>obszerny (~30 tys.)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Model
|
||||
<select name="llm_provider">
|
||||
{% set lp = form.llm_provider or 'local' %}
|
||||
<option value="local" {{ 'selected' if lp == 'local' else '' }}>lokalny — nic nie opuszcza sieci</option>
|
||||
<option value="anthropic" {{ 'selected' if lp == 'anthropic' else '' }}>Anthropic — dane wychodzą</option>
|
||||
<option value="openai" {{ 'selected' if lp == 'openai' else '' }}>OpenAI — dane wychodzą</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p class="muted small">
|
||||
Prompt zawiera <strong>oryginalne opisy z baz</strong> oraz dane urodzeniowe. Model lokalny
|
||||
przetwarza je u nas; wybór dostawcy w chmurze oznacza, że ta treść <strong>opuszcza naszą
|
||||
sieć</strong>. Możesz najpierw obejrzeć prompt, a dopiero potem wysłać.
|
||||
</p>
|
||||
|
||||
{% if prompt_result %}
|
||||
{% set st = prompt_result.stats %}
|
||||
|
||||
{# --- wynik: napisany horoskop (LOG-31) + transparentność (PRE-15) --- #}
|
||||
{% if prompt_result.horoscope %}
|
||||
<div class="meta">
|
||||
Horoskop napisany przez: <strong>{{ prompt_result.provider }}</strong> ·
|
||||
model: {{ prompt_result.model }}
|
||||
{% if prompt_result.usage and prompt_result.usage.completion_tokens %}
|
||||
· tokeny odpowiedzi: {{ prompt_result.usage.completion_tokens }}
|
||||
{% endif %}
|
||||
{% if prompt_result.leaves_lan %}
|
||||
· <strong class="retro">dane opuściły sieć</strong>
|
||||
{% else %}
|
||||
· dane nie opuściły sieci
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="ghost" data-copy="#horoscopeText">Kopiuj horoskop</button>
|
||||
</div>
|
||||
<textarea id="horoscopeText" class="prompt" rows="20" readonly>{{ prompt_result.horoscope }}</textarea>
|
||||
<p class="muted small">
|
||||
Treść wygenerował model językowy na podstawie {{ st.included }} wskazań z baz.
|
||||
<strong>Nie stanowi porady medycznej, prawnej ani finansowej.</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if prompt_result.llm_error %}
|
||||
<div class="error">
|
||||
Nie udało się napisać horoskopu: {{ prompt_result.llm_error }}<br>
|
||||
Prompt poniżej jest gotowy — możesz go skopiować i użyć ręcznie.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- prompt: zawsze dostępny do podglądu i skopiowania --- #}
|
||||
<div class="meta">
|
||||
Prompt gotowy · {{ st.chars }} znaków (~{{ st.est_tokens }} tokenów) ·
|
||||
Prompt · {{ st.chars }} znaków (~{{ st.est_tokens }} tokenów) ·
|
||||
budżet: {{ st.budget }} ·
|
||||
wskazań: <strong>{{ st.included }}</strong>
|
||||
{% if st.omitted %}· pominięto: <strong>{{ st.omitted }}</strong>{% endif %}
|
||||
@@ -34,7 +80,7 @@
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="ghost" data-copy="#promptText">Kopiuj prompt</button>
|
||||
<span class="muted small">Wklej do ChatGPT lub Claude.</span>
|
||||
<span class="muted small">Możesz też wkleić go samodzielnie do ChatGPT lub Claude.</span>
|
||||
</div>
|
||||
<textarea id="promptText" class="prompt" rows="18" readonly>{{ prompt_result.prompt }}</textarea>
|
||||
<textarea id="promptText" class="prompt" rows="14" readonly>{{ prompt_result.prompt }}</textarea>
|
||||
{% endif %}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
<button type="submit" name="action" value="report">Szukaj interpretacji</button>
|
||||
<button type="submit" name="action" value="prompt" class="ghost">Generuj prompt (AI)</button>
|
||||
<button type="submit" name="action" value="horoscope">Napisz horoskop (AI)</button>
|
||||
<span id="geoNote" class="muted small"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
<button type="submit" name="action" value="timeline">Pokaż kalendarz</button>
|
||||
<button type="submit" name="action" value="prompt" class="ghost">Generuj prompt (AI)</button>
|
||||
<button type="submit" name="action" value="horoscope">Napisz horoskop (AI)</button>
|
||||
<span id="geoNote" class="muted small"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user