4ef90b30bc
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m38s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m43s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 24s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 24s
build / build (push) Successful in 59s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m47s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m53s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 29s
Testy / Kontrola składni wszystkich warstw (push) Successful in 18s
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>
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
"""Wybór dostawcy LLM (LOG-31) — jedyne miejsce znające konkretne implementacje.
|
|
|
|
Domyślny jest **model lokalny**: prompt niesie oryginalne opisy z baz, więc
|
|
domyślnie nic nie opuszcza naszej sieci (LOG-32). Chmurę włącza się świadomie —
|
|
przez konfigurację albo pojedyncze żądanie.
|
|
|
|
Zmienne środowiskowe:
|
|
LLM_PROVIDER local (domyślnie) | openai | anthropic
|
|
LLM_MODEL nazwa modelu (domyślna zależy od dostawcy)
|
|
LLM_BASE_URL adres API (domyślnie: lokalny serwer zgodny z OpenAI)
|
|
LLM_API_KEY klucz — WYŁĄCZNIE z sekretu; niepotrzebny dla modelu lokalnego
|
|
LLM_TIMEOUT sekundy (domyślnie 120)
|
|
LLM_MAX_TOKENS limit długości odpowiedzi (domyślnie 2000)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from app.llm.base import LLMError, LLMProvider
|
|
from app.llm.providers import AnthropicProvider, ChatCompletionsProvider
|
|
|
|
LOCAL = "local"
|
|
OPENAI = "openai"
|
|
ANTHROPIC = "anthropic"
|
|
PROVIDERS = (LOCAL, OPENAI, ANTHROPIC)
|
|
|
|
_DEFAULT_MODEL = {
|
|
LOCAL: "llama3.1:8b",
|
|
OPENAI: "gpt-4o-mini",
|
|
ANTHROPIC: "claude-sonnet-5",
|
|
}
|
|
_DEFAULT_URL = {
|
|
# Ollama i vLLM wystawiają zgodne API pod /v1
|
|
LOCAL: "http://localhost:11434/v1",
|
|
OPENAI: "https://api.openai.com/v1",
|
|
ANTHROPIC: "https://api.anthropic.com",
|
|
}
|
|
|
|
|
|
def default_provider_name() -> str:
|
|
return os.getenv("LLM_PROVIDER", LOCAL).lower()
|
|
|
|
|
|
def max_tokens() -> int:
|
|
return int(os.getenv("LLM_MAX_TOKENS", "2000"))
|
|
|
|
|
|
def timeout() -> float:
|
|
return float(os.getenv("LLM_TIMEOUT", "120"))
|
|
|
|
|
|
def build_provider(name: str | None = None) -> LLMProvider:
|
|
name = (name or default_provider_name()).lower()
|
|
if name not in PROVIDERS:
|
|
raise LLMError(f"Nieznany dostawca LLM: {name!r} (dostępne: {', '.join(PROVIDERS)})")
|
|
|
|
model = os.getenv("LLM_MODEL") or _DEFAULT_MODEL[name]
|
|
base_url = os.getenv("LLM_BASE_URL") or _DEFAULT_URL[name]
|
|
api_key = os.getenv("LLM_API_KEY", "")
|
|
|
|
if name == ANTHROPIC:
|
|
return AnthropicProvider(base_url, model, api_key, timeout())
|
|
if name == OPENAI:
|
|
if not api_key:
|
|
raise LLMError("Brak LLM_API_KEY — dostawca openai wymaga klucza.")
|
|
return ChatCompletionsProvider(OPENAI, base_url, model, api_key, timeout(),
|
|
leaves_lan=True)
|
|
# lokalny — klucz zwykle zbędny; treść NIE opuszcza sieci
|
|
return ChatCompletionsProvider(LOCAL, base_url, model, api_key, timeout(),
|
|
leaves_lan=False)
|