feat: wire hybrid provider into CLI
This commit is contained in:
+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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user