feat: wire hybrid provider into CLI
This commit is contained in:
+12
-4
@@ -90,6 +90,11 @@ Rootserver:
|
||||
`JAVIS_DATA_DIR\javis.toml` geladen; Umgebungsvariablen können sie sicher
|
||||
überschreiben. Die Aktivierungsvorlage enthält `free_only = true` und die
|
||||
lokale Billing-Bestätigung, aber niemals einen API-Key.
|
||||
- `javis configure gemini` verlangt zwei ausdrückliche Bestätigungen und lässt
|
||||
Gemini bei leerer oder negativer Antwort deaktiviert.
|
||||
- Der echte CLI-Chat verwendet nun `hybrid/auto`; der Key wird erst vor einem
|
||||
zulässigen Cloudaufruf gelesen. `/provider`, `/privacy` und `/status` zeigen
|
||||
ausschließlich bereinigte Modi, Entscheidungen und Zähler.
|
||||
|
||||
## Aktuelle Architektur
|
||||
|
||||
@@ -143,6 +148,9 @@ Rootserver:
|
||||
Cloudclient, `ask` verlangt Freigabe, 429/Netzwerk/Keyfehler fallen lokal zurück.
|
||||
- Lokalen TOML-Loader und sicheren Erzeuger ergänzt; vorhandene Konfigurationen
|
||||
werden nicht ungefragt überschrieben und fehlerhafte TOML wird abgelehnt.
|
||||
- Hybridprovider in die CLI verdrahtet, sicheren Aktivierungsdialog und
|
||||
bereinigte Provider-/Datenschutz-/Statusbefehle ergänzt.
|
||||
- Lesender Ollama-Status prüft nur Loopback-Erreichbarkeit und Modellliste.
|
||||
|
||||
## Aktuelle Tests
|
||||
|
||||
@@ -157,7 +165,8 @@ Letzter bestätigter Projektstand:
|
||||
- uv-Lock und `uv sync --dev`: bestanden
|
||||
- Python in `.venv`: 3.12.13
|
||||
- Ruff in `.venv`: 0.16.0
|
||||
- Unit-Tests: 55 bestanden; zusätzlich 4 lokale Konfigurationstests
|
||||
- Unit-Tests: 59 bestanden; CLI-Befehle, doppelte Bestätigung, verzögertes
|
||||
Key-Laden und Ollama-Status sind abgedeckt
|
||||
- Ruff Lint: bestanden
|
||||
- Ruff Formatprüfung: bestanden
|
||||
|
||||
@@ -243,9 +252,8 @@ Abnahmestatus:
|
||||
|
||||
Auf dem bestehenden Feature-Branch als nächstes einzelnes Arbeitspaket:
|
||||
|
||||
1. sichere Aktivierungsbestätigung als CLI-Einrichtungsschritt ergänzen
|
||||
2. Hybridrouter in die CLI verdrahten und `/provider`, `/privacy`, `/status`
|
||||
mit standardmäßig abgelehnter `ask`-Freigabe implementieren
|
||||
1. sicheres `scripts/start-javis.ps1` ohne Systemänderungen ergänzen
|
||||
2. Start- und Einrichtungsablauf kompakt dokumentieren und automatisiert prüfen
|
||||
3. noch keine echte Cloudanfrage ausführen und noch nicht nach `main` mergen
|
||||
|
||||
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
|
||||
|
||||
@@ -55,7 +55,10 @@ Stand: lokaler Textchat auf `main`, 30.07.2026.
|
||||
Datenschutz und lokale Grenzen; alte Ollama-Sitzungen bleiben ladbar
|
||||
- lokale nicht geheime TOML-Konfiguration außerhalb von Git; konservativer
|
||||
Erzeuger überschreibt keine vorhandene Datei
|
||||
- 55 Unit-Tests aktuell erfolgreich; Ruff-Format und Lint sauber
|
||||
- `javis configure gemini` mit doppelter ausdrücklicher Bestätigung
|
||||
- echter Chat auf Hybridprovider verdrahtet; Key wird verzögert geladen
|
||||
- `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus
|
||||
- 59 Unit-Tests aktuell erfolgreich; Ruff-Format und Lint sauber
|
||||
|
||||
## Nicht implementiert
|
||||
|
||||
@@ -80,6 +83,6 @@ Living-Mind-Frontend und automatische Ollama-Prozessverwaltung.
|
||||
Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden
|
||||
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
|
||||
`qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet.
|
||||
Der Hybridprovider ist implementiert, aber noch nicht in der echten CLI verdrahtet.
|
||||
CLI-Aktivierungsdialog, Statusbefehle und Startskript fehlen. Es wurde kein echter
|
||||
API-Key angefordert, angezeigt oder konfiguriert und keine Cloudanfrage ausgeführt.
|
||||
Das sichere Startskript und die abschließende Bedienungsdokumentation fehlen noch.
|
||||
Es wurde kein echter API-Key angefordert, angezeigt oder konfiguriert und keine
|
||||
Cloudanfrage ausgeführt.
|
||||
|
||||
+180
-8
@@ -6,27 +6,38 @@ import argparse
|
||||
import getpass
|
||||
import sys
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from javis.config.local_config import LocalConfigError, write_gemini_activation
|
||||
from javis.config.settings import ConfigurationError, Settings
|
||||
from javis.core.chat_service import ChatService, SessionProviderMismatchError
|
||||
from javis.core.provider_router import HybridProvider
|
||||
from javis.memory.sqlite_store import (
|
||||
ChatSession,
|
||||
SessionNotFoundError,
|
||||
SessionStoreError,
|
||||
SQLiteSessionStore,
|
||||
)
|
||||
from javis.providers.base import ProviderError
|
||||
from javis.memory.usage_store import SQLiteUsageStore, UsageStoreError
|
||||
from javis.providers.base import MissingApiKeyError, ProviderError
|
||||
from javis.providers.gemini import GeminiProvider
|
||||
from javis.providers.ollama import OllamaProvider
|
||||
from javis.security.privacy import PrivacyDecision, PrivacyRouter
|
||||
from javis.security.secrets import SecretProvider, SecretStoreError
|
||||
|
||||
InputFunction = Callable[[str], str]
|
||||
OutputFunction = Callable[[str], None]
|
||||
StatusFunction = Callable[[], list[str]]
|
||||
|
||||
HELP_TEXT = """Befehle:
|
||||
/new neue Sitzung beginnen
|
||||
/sessions gespeicherte Sitzungen auflisten
|
||||
/load <ID> frühere Sitzung laden und fortsetzen
|
||||
/clear Nachrichten der aktiven Sitzung leeren
|
||||
/provider aktiven Providermodus anzeigen
|
||||
/provider <MODUS> Modus auto, local oder gemini setzen
|
||||
/privacy letzte lokale Datenschutzentscheidung anzeigen
|
||||
/status bereinigten Providerstatus anzeigen
|
||||
/help diese Hilfe anzeigen
|
||||
/exit Chat ordentlich beenden"""
|
||||
|
||||
@@ -36,6 +47,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
chat_parser = subparsers.add_parser("chat", help="lokalen Textchat starten")
|
||||
chat_parser.add_argument("--session", help="vorhandene Sitzungs-ID laden")
|
||||
configure_parser = subparsers.add_parser(
|
||||
"configure",
|
||||
help="nicht geheime lokale Einstellungen einrichten",
|
||||
)
|
||||
configure_parser.add_argument("provider_name", choices=["gemini"])
|
||||
secrets_parser = subparsers.add_parser("secrets", help="lokale Geheimwerte sicher verwalten")
|
||||
secret_subparsers = secrets_parser.add_subparsers(dest="secret_action", required=True)
|
||||
set_parser = secret_subparsers.add_parser("set", help="Geheimwert verdeckt setzen")
|
||||
@@ -46,6 +62,34 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _confirmed(answer: str) -> bool:
|
||||
return answer.strip().lower() in {"j", "ja", "y", "yes"}
|
||||
|
||||
|
||||
def run_gemini_configuration(
|
||||
config_path: Path,
|
||||
*,
|
||||
input_fn: InputFunction = input,
|
||||
output: OutputFunction = print,
|
||||
) -> int:
|
||||
if not _confirmed(input_fn("Gemini Free Tier lokal aktivieren? [j/N] ")):
|
||||
output("Gemini bleibt deaktiviert.")
|
||||
return 1
|
||||
if not _confirmed(
|
||||
input_fn("Bestätigst du, dass für dieses Google-Projekt kein Billing aktiviert ist? [j/N] ")
|
||||
):
|
||||
output("Gemini bleibt deaktiviert; keine Konfiguration wurde geschrieben.")
|
||||
return 1
|
||||
try:
|
||||
write_gemini_activation(config_path)
|
||||
except LocalConfigError as exc:
|
||||
output(f"Konfigurationsfehler: {exc}")
|
||||
return 2
|
||||
output(f"Gemini-Free-Konfiguration gespeichert: {config_path}")
|
||||
output("Nächster Schritt: javis secrets set gemini")
|
||||
return 0
|
||||
|
||||
|
||||
def run_secrets(
|
||||
action: str,
|
||||
*,
|
||||
@@ -106,6 +150,7 @@ def run_chat(
|
||||
session_id: str | None = None,
|
||||
input_fn: InputFunction = input,
|
||||
output: OutputFunction = print,
|
||||
status_fn: StatusFunction | None = None,
|
||||
) -> int:
|
||||
try:
|
||||
if session_id:
|
||||
@@ -160,6 +205,40 @@ def run_chat(
|
||||
except (SessionNotFoundError, SessionStoreError) as exc:
|
||||
output(f"Fehler: {exc}")
|
||||
continue
|
||||
if entered.startswith("/provider"):
|
||||
parts = entered.split()
|
||||
provider = service.provider
|
||||
if len(parts) == 1:
|
||||
output(f"Aktiver Providermodus: {getattr(provider, 'mode', 'local')}")
|
||||
continue
|
||||
if len(parts) != 2 or parts[1] not in {"auto", "local", "gemini"}:
|
||||
output("Verwendung: /provider auto|local|gemini")
|
||||
continue
|
||||
set_mode = getattr(provider, "set_mode", None)
|
||||
if not callable(set_mode):
|
||||
output("Dieser Chat unterstützt keinen wechselbaren Providermodus.")
|
||||
continue
|
||||
set_mode(parts[1])
|
||||
output(f"Providermodus: {parts[1]}")
|
||||
continue
|
||||
if entered == "/privacy":
|
||||
route = getattr(service.provider, "last_route", None)
|
||||
if route is None:
|
||||
output("Noch keine Datenschutzentscheidung in dieser Sitzung.")
|
||||
else:
|
||||
output(
|
||||
"Letzte Datenschutzentscheidung: "
|
||||
f"{route.privacy_policy.value}; Provider: {route.provider}; "
|
||||
f"Fallback: {'ja' if route.fallback else 'nein'}"
|
||||
)
|
||||
continue
|
||||
if entered == "/status":
|
||||
if status_fn is None:
|
||||
output("Keine erweiterte Statusprüfung verfügbar.")
|
||||
else:
|
||||
for line in status_fn():
|
||||
output(line)
|
||||
continue
|
||||
if entered.startswith("/load"):
|
||||
parts = entered.split(maxsplit=1)
|
||||
if len(parts) != 2 or not parts[1].strip():
|
||||
@@ -190,6 +269,88 @@ def run_chat(
|
||||
output(f"Fehler: {exc}")
|
||||
|
||||
|
||||
def build_runtime(
|
||||
settings: Settings,
|
||||
*,
|
||||
input_fn: InputFunction,
|
||||
output: OutputFunction,
|
||||
secret_provider: SecretProvider | None = None,
|
||||
) -> tuple[ChatService, StatusFunction]:
|
||||
store = SQLiteSessionStore(settings.database_path)
|
||||
usage_store = SQLiteUsageStore(settings.usage_database_path)
|
||||
secrets = secret_provider or SecretProvider()
|
||||
local = OllamaProvider(
|
||||
settings.model,
|
||||
settings.ollama_base_url,
|
||||
settings.timeout_seconds,
|
||||
)
|
||||
|
||||
def approve(decision: PrivacyDecision) -> bool:
|
||||
answer = input_fn(
|
||||
f"Cloudfreigabe erforderlich ({decision.reason}). Einmalig erlauben? [j/N] "
|
||||
)
|
||||
return _confirmed(answer)
|
||||
|
||||
def create_cloud_provider() -> GeminiProvider:
|
||||
try:
|
||||
key = secrets.get_gemini_key()
|
||||
except SecretStoreError as exc:
|
||||
raise MissingApiKeyError(
|
||||
"Der Gemini-Schlüssel konnte nicht sicher geladen werden."
|
||||
) from exc
|
||||
return GeminiProvider(
|
||||
model=settings.gemini_model,
|
||||
api_key=key,
|
||||
timeout_seconds=settings.gemini_timeout_seconds,
|
||||
max_output_tokens=settings.max_cloud_output_tokens,
|
||||
max_retries=settings.gemini_max_retries,
|
||||
)
|
||||
|
||||
hybrid = HybridProvider(
|
||||
local_provider=local,
|
||||
cloud_provider_factory=create_cloud_provider,
|
||||
cloud_model=settings.gemini_model,
|
||||
privacy_router=PrivacyRouter(),
|
||||
usage_store=usage_store,
|
||||
approval_callback=approve,
|
||||
notice_callback=output,
|
||||
mode=settings.provider_mode,
|
||||
cloud_enabled=settings.gemini_enabled,
|
||||
billing_confirmed_disabled=settings.billing_confirmed_disabled,
|
||||
free_only=settings.free_only,
|
||||
max_cloud_requests_per_day=settings.max_cloud_requests_per_day,
|
||||
max_cloud_input_chars=settings.max_cloud_input_chars,
|
||||
max_cloud_context_messages=settings.max_cloud_context_messages,
|
||||
)
|
||||
|
||||
def status() -> list[str]:
|
||||
ollama = local.probe()
|
||||
try:
|
||||
key_available = secrets.gemini_key_available()
|
||||
except SecretStoreError:
|
||||
key_available = False
|
||||
gemini_configured = (
|
||||
settings.gemini_enabled and settings.free_only and settings.billing_confirmed_disabled
|
||||
)
|
||||
try:
|
||||
cloud_requests = usage_store.cloud_requests_on()
|
||||
local_fallbacks = usage_store.local_fallbacks_on()
|
||||
except UsageStoreError:
|
||||
cloud_requests = local_fallbacks = -1
|
||||
return [
|
||||
f"Ollama erreichbar: {'ja' if ollama.reachable else 'nein'}",
|
||||
f"Lokales Modell vorhanden: {'ja' if ollama.model_available else 'nein'}",
|
||||
f"Gemini-Schlüssel vorhanden: {'ja' if key_available else 'nein'}",
|
||||
f"Gemini konfiguriert: {'ja' if gemini_configured else 'nein'}",
|
||||
f"Nullkostenmodus: {'aktiv' if settings.free_only else 'inaktiv'}",
|
||||
f"Aktiver Providermodus: {hybrid.mode}",
|
||||
f"Heutige Cloudanfragen: {cloud_requests if cloud_requests >= 0 else 'unbekannt'}",
|
||||
f"Heutige lokale Fallbacks: {local_fallbacks if local_fallbacks >= 0 else 'unbekannt'}",
|
||||
]
|
||||
|
||||
return ChatService(store, hybrid), status
|
||||
|
||||
|
||||
def main(
|
||||
argv: Sequence[str] | None = None,
|
||||
*,
|
||||
@@ -203,26 +364,37 @@ def main(
|
||||
password_fn=input_fn if input_fn is not input else getpass.getpass,
|
||||
output=output,
|
||||
)
|
||||
if args.command == "configure":
|
||||
try:
|
||||
settings = Settings.from_env()
|
||||
except ConfigurationError as exc:
|
||||
output(f"Konfigurationsfehler: {exc}")
|
||||
return 2
|
||||
return run_gemini_configuration(
|
||||
settings.config_path,
|
||||
input_fn=input_fn,
|
||||
output=output,
|
||||
)
|
||||
if args.command != "chat":
|
||||
return 2
|
||||
|
||||
try:
|
||||
settings = Settings.from_env()
|
||||
store = SQLiteSessionStore(settings.database_path)
|
||||
provider = OllamaProvider(
|
||||
settings.model,
|
||||
settings.ollama_base_url,
|
||||
settings.timeout_seconds,
|
||||
service, status_fn = build_runtime(
|
||||
settings,
|
||||
input_fn=input_fn,
|
||||
output=output,
|
||||
)
|
||||
except (ConfigurationError, SessionStoreError) as exc:
|
||||
except (ConfigurationError, SessionStoreError, UsageStoreError) as exc:
|
||||
output(f"Konfigurationsfehler: {exc}")
|
||||
return 2
|
||||
|
||||
return run_chat(
|
||||
ChatService(store, provider),
|
||||
service,
|
||||
session_id=args.session,
|
||||
input_fn=input_fn,
|
||||
output=output,
|
||||
status_fn=status_fn,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
@@ -17,14 +18,40 @@ from javis.providers.base import (
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OllamaStatus:
|
||||
reachable: bool
|
||||
model_available: bool
|
||||
|
||||
|
||||
class OllamaProvider:
|
||||
name = "ollama"
|
||||
|
||||
def __init__(self, model: str, base_url: str, timeout_seconds: float) -> None:
|
||||
self.model = model
|
||||
self._endpoint = f"{base_url.rstrip('/')}/api/chat"
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._endpoint = f"{self._base_url}/api/chat"
|
||||
self._timeout_seconds = timeout_seconds
|
||||
|
||||
def probe(self) -> OllamaStatus:
|
||||
request = Request(f"{self._base_url}/api/tags", method="GET")
|
||||
try:
|
||||
with urlopen(request, timeout=min(self._timeout_seconds, 3)) as response:
|
||||
result = json.loads(response.read())
|
||||
except (HTTPError, URLError, TimeoutError, OSError, json.JSONDecodeError):
|
||||
return OllamaStatus(False, False)
|
||||
models = result.get("models")
|
||||
if not isinstance(models, list):
|
||||
return OllamaStatus(True, False)
|
||||
names = {
|
||||
value
|
||||
for item in models
|
||||
if isinstance(item, dict)
|
||||
for value in (item.get("name"), item.get("model"))
|
||||
if isinstance(value, str)
|
||||
}
|
||||
return OllamaStatus(True, self.model in names)
|
||||
|
||||
def chat(self, messages: list[ChatMessage]) -> str:
|
||||
payload = {
|
||||
"model": self.model,
|
||||
|
||||
+116
-1
@@ -1,11 +1,15 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from javis.config.settings import Settings
|
||||
from javis.core.chat_service import ChatService
|
||||
from javis.interface.cli import run_chat
|
||||
from javis.core.provider_router import HybridProvider
|
||||
from javis.interface.cli import build_runtime, run_chat, run_gemini_configuration
|
||||
from javis.memory.sqlite_store import SQLiteSessionStore
|
||||
from javis.providers.base import ChatMessage
|
||||
from javis.security.privacy import CloudPolicy
|
||||
|
||||
|
||||
class _FakeProvider:
|
||||
@@ -16,6 +20,30 @@ class _FakeProvider:
|
||||
return f"Echo: {messages[-1].content}"
|
||||
|
||||
|
||||
class _FakeHybridProvider(_FakeProvider):
|
||||
name = "hybrid"
|
||||
model = "auto"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.mode = "auto"
|
||||
self.last_route = None
|
||||
|
||||
def set_mode(self, mode: str) -> None:
|
||||
self.mode = mode
|
||||
|
||||
|
||||
class _LazySecrets:
|
||||
def __init__(self) -> None:
|
||||
self.reads = 0
|
||||
|
||||
def get_gemini_key(self) -> str | None:
|
||||
self.reads += 1
|
||||
return None
|
||||
|
||||
def gemini_key_available(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class CliTests(unittest.TestCase):
|
||||
def test_basic_chat_commands(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -35,6 +63,93 @@ class CliTests(unittest.TestCase):
|
||||
self.assertTrue(any("Nachrichten" in line for line in output))
|
||||
self.assertEqual(output[-1], "Chat beendet.")
|
||||
|
||||
def test_provider_privacy_and_status_commands_are_sanitized(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = SQLiteSessionStore(Path(directory) / "sessions.sqlite3")
|
||||
provider = _FakeHybridProvider()
|
||||
provider.last_route = SimpleNamespace(
|
||||
privacy_policy=CloudPolicy.ASK,
|
||||
provider="ollama",
|
||||
fallback=True,
|
||||
)
|
||||
service = ChatService(store, provider)
|
||||
inputs = iter(
|
||||
[
|
||||
"/provider",
|
||||
"/provider local",
|
||||
"/privacy",
|
||||
"/status",
|
||||
"/exit",
|
||||
]
|
||||
)
|
||||
output: list[str] = []
|
||||
|
||||
result = run_chat(
|
||||
service,
|
||||
input_fn=lambda _prompt: next(inputs),
|
||||
output=output.append,
|
||||
status_fn=lambda: [
|
||||
"Gemini-Schlüssel vorhanden: ja",
|
||||
"Heutige Cloudanfragen: 0",
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertIn("Aktiver Providermodus: auto", output)
|
||||
self.assertIn("Providermodus: local", output)
|
||||
self.assertTrue(any("ask; Provider: ollama; Fallback: ja" in line for line in output))
|
||||
self.assertIn("Gemini-Schlüssel vorhanden: ja", output)
|
||||
|
||||
def test_gemini_configuration_defaults_to_no(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "javis.toml"
|
||||
output: list[str] = []
|
||||
|
||||
result = run_gemini_configuration(
|
||||
path,
|
||||
input_fn=lambda _prompt: "",
|
||||
output=output.append,
|
||||
)
|
||||
|
||||
self.assertEqual(result, 1)
|
||||
self.assertFalse(path.exists())
|
||||
self.assertIn("deaktiviert", output[-1])
|
||||
|
||||
def test_gemini_configuration_requires_both_confirmations(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "javis.toml"
|
||||
answers = iter(["ja", "ja"])
|
||||
|
||||
result = run_gemini_configuration(
|
||||
path,
|
||||
input_fn=lambda _prompt: next(answers),
|
||||
output=lambda _line: None,
|
||||
)
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertTrue(path.exists())
|
||||
content = path.read_text(encoding="utf-8")
|
||||
self.assertIn("billing_confirmed_disabled = true", content)
|
||||
self.assertNotIn("api_key", content.lower())
|
||||
|
||||
def test_runtime_wires_hybrid_provider_without_loading_key_at_start(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = Settings(data_dir=Path(directory))
|
||||
secrets = _LazySecrets()
|
||||
|
||||
service, _status = build_runtime(
|
||||
settings,
|
||||
input_fn=lambda _prompt: "",
|
||||
output=lambda _line: None,
|
||||
secret_provider=secrets,
|
||||
)
|
||||
session = service.new_session()
|
||||
|
||||
self.assertIsInstance(service.provider, HybridProvider)
|
||||
self.assertEqual(session.provider, "hybrid")
|
||||
self.assertEqual(session.model, "auto")
|
||||
self.assertEqual(secrets.reads, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -26,6 +26,14 @@ class _OllamaHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
body = json.dumps({"models": [{"name": "test-model"}]}).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None:
|
||||
return
|
||||
|
||||
@@ -42,6 +50,7 @@ class OllamaProviderTests(unittest.TestCase):
|
||||
2,
|
||||
)
|
||||
response = provider.chat([ChatMessage("user", "Hallo")])
|
||||
status = provider.probe()
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
@@ -50,6 +59,8 @@ class OllamaProviderTests(unittest.TestCase):
|
||||
self.assertEqual(response, "Lokale Antwort")
|
||||
self.assertEqual(_OllamaHandler.request_payload["model"], "test-model")
|
||||
self.assertFalse(_OllamaHandler.request_payload["stream"])
|
||||
self.assertTrue(status.reachable)
|
||||
self.assertTrue(status.model_available)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user