feat: connect desktop sessions and streaming
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from javis.memory.usage_store import ProviderEvent, SQLiteUsageStore, UsageStoreError
|
||||
from javis.providers.base import (
|
||||
@@ -18,7 +19,14 @@ from javis.providers.base import (
|
||||
)
|
||||
from javis.security.privacy import CloudPolicy, PrivacyDecision, PrivacyRouter
|
||||
|
||||
ApprovalCallback = Callable[[PrivacyDecision], bool]
|
||||
|
||||
class ApprovalChoice(StrEnum):
|
||||
ALLOW = "allow"
|
||||
LOCAL = "local"
|
||||
CANCEL = "cancel"
|
||||
|
||||
|
||||
ApprovalCallback = Callable[[PrivacyDecision], ApprovalChoice | bool]
|
||||
NoticeCallback = Callable[[str], None]
|
||||
CloudProviderFactory = Callable[[], LocalModelProvider]
|
||||
|
||||
@@ -89,6 +97,14 @@ class HybridProvider:
|
||||
raise ValueError("Provider-Modus muss auto, local oder gemini sein.")
|
||||
self.mode = mode
|
||||
|
||||
def _approval_choice(self, decision: PrivacyDecision) -> ApprovalChoice:
|
||||
result = self._approval_callback(decision)
|
||||
if isinstance(result, bool):
|
||||
return ApprovalChoice.ALLOW if result else ApprovalChoice.LOCAL
|
||||
if isinstance(result, ApprovalChoice):
|
||||
return result
|
||||
raise ValueError("Ungültiges Ergebnis der Cloudfreigabe.")
|
||||
|
||||
def chat(self, messages: list[ChatMessage]) -> str:
|
||||
current_text = messages[-1].content if messages else ""
|
||||
cloud_requested = self.mode == "gemini"
|
||||
@@ -110,8 +126,11 @@ class HybridProvider:
|
||||
|
||||
approved = decision.policy is CloudPolicy.ALLOWED
|
||||
if decision.policy is CloudPolicy.ASK:
|
||||
approved = self._approval_callback(decision)
|
||||
if not approved:
|
||||
choice = self._approval_choice(decision)
|
||||
if choice is ApprovalChoice.CANCEL:
|
||||
raise ResponseAbortedError("Anfrage vor der Cloudfreigabe abgebrochen.")
|
||||
approved = choice is ApprovalChoice.ALLOW
|
||||
if choice is ApprovalChoice.LOCAL:
|
||||
return self._local(messages, decision, "Cloudfreigabe abgelehnt", fallback=True)
|
||||
|
||||
unavailable_reason = self._cloud_unavailable_reason()
|
||||
@@ -194,8 +213,11 @@ class HybridProvider:
|
||||
|
||||
approved = decision.policy is CloudPolicy.ALLOWED
|
||||
if decision.policy is CloudPolicy.ASK:
|
||||
approved = self._approval_callback(decision)
|
||||
if not approved:
|
||||
choice = self._approval_choice(decision)
|
||||
if choice is ApprovalChoice.CANCEL:
|
||||
raise ResponseAbortedError("Anfrage vor der Cloudfreigabe abgebrochen.")
|
||||
approved = choice is ApprovalChoice.ALLOW
|
||||
if choice is ApprovalChoice.LOCAL:
|
||||
yield from self._stream_local(
|
||||
messages,
|
||||
decision,
|
||||
|
||||
@@ -88,6 +88,10 @@ class DesktopController:
|
||||
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
||||
yield StreamEvent(StreamEventKind.CHUNK, text=chunk)
|
||||
except ProviderError as exc:
|
||||
if cancel_event.is_set():
|
||||
self.state = GenerationState.ABORTED
|
||||
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
||||
return
|
||||
self.state = GenerationState.ERROR
|
||||
yield StreamEvent(StreamEventKind.ERROR, text=sanitized_error(exc), state=self.state)
|
||||
return
|
||||
@@ -101,8 +105,11 @@ class DesktopController:
|
||||
def status(self) -> DesktopStatus:
|
||||
runtime_status = self.runtime.status()
|
||||
route = self.runtime.hybrid_provider.last_route
|
||||
state = self.state
|
||||
if state is GenerationState.READY and not runtime_status.ollama_reachable:
|
||||
state = GenerationState.OFFLINE
|
||||
return DesktopStatus(
|
||||
state=self.state,
|
||||
state=state,
|
||||
mode="Lokal" if runtime_status.provider_mode == "local" else "Hybrid/Auto",
|
||||
provider=route.provider if route else "—",
|
||||
ollama_reachable=runtime_status.ollama_reachable,
|
||||
|
||||
@@ -25,6 +25,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from javis.core.provider_router import ApprovalChoice
|
||||
from javis.providers.base import ChatMessage
|
||||
from javis.security.privacy import PrivacyDecision
|
||||
from javis.ui.chat_controller import DesktopController
|
||||
@@ -63,7 +64,7 @@ class MainWindow(QMainWindow):
|
||||
self._build_ui()
|
||||
self._connect_signals()
|
||||
self._apply_style()
|
||||
self._new_session()
|
||||
self._open_initial_session()
|
||||
self._animation = QTimer(self)
|
||||
self._animation.timeout.connect(self._animate_state)
|
||||
self._animation.start(450)
|
||||
@@ -191,6 +192,20 @@ class MainWindow(QMainWindow):
|
||||
self.message_edit.setFocus()
|
||||
self._refresh_status()
|
||||
|
||||
def _open_initial_session(self) -> None:
|
||||
try:
|
||||
sessions = self.controller.list_sessions()
|
||||
if sessions:
|
||||
loaded = self.controller.load_session(sessions[0].id)
|
||||
self._messages = list(loaded.messages)
|
||||
self._render_chat()
|
||||
self._refresh_sessions()
|
||||
self._refresh_status()
|
||||
return
|
||||
except Exception as exc:
|
||||
self._show_error(str(exc))
|
||||
self._new_session()
|
||||
|
||||
def _refresh_sessions(self) -> None:
|
||||
try:
|
||||
sessions = self.controller.list_sessions(self.search_edit.text())
|
||||
@@ -332,8 +347,17 @@ class MainWindow(QMainWindow):
|
||||
box.setInformativeText(f"Lokale Einstufung: {decision.reason}")
|
||||
allow = box.addButton("Einmal erlauben", QMessageBox.AcceptRole)
|
||||
local = box.addButton("Lokal beantworten", QMessageBox.RejectRole)
|
||||
cancel = box.addButton("Abbrechen", QMessageBox.DestructiveRole)
|
||||
box.exec()
|
||||
self.approval_bridge.resolve(box.clickedButton() is allow)
|
||||
clicked = box.clickedButton()
|
||||
choice = ApprovalChoice.CANCEL
|
||||
if clicked is allow:
|
||||
choice = ApprovalChoice.ALLOW
|
||||
elif clicked is local:
|
||||
choice = ApprovalChoice.LOCAL
|
||||
elif clicked is cancel:
|
||||
choice = ApprovalChoice.CANCEL
|
||||
self.approval_bridge.resolve(choice)
|
||||
if box.clickedButton() is local:
|
||||
self.statusBar().showMessage("Die Anfrage wird lokal beantwortet.", 3000)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import threading
|
||||
|
||||
from PySide6.QtCore import QObject, Signal, Slot
|
||||
|
||||
from javis.core.provider_router import ApprovalChoice
|
||||
from javis.security.privacy import PrivacyDecision
|
||||
from javis.ui.chat_controller import DesktopController
|
||||
|
||||
@@ -18,13 +19,13 @@ class ApprovalBridge(QObject):
|
||||
super().__init__()
|
||||
self._lock = threading.Lock()
|
||||
self._event: threading.Event | None = None
|
||||
self._result = False
|
||||
self._result = ApprovalChoice.CANCEL
|
||||
|
||||
def request(self, decision: PrivacyDecision) -> bool:
|
||||
def request(self, decision: PrivacyDecision) -> ApprovalChoice:
|
||||
event = threading.Event()
|
||||
with self._lock:
|
||||
self._event = event
|
||||
self._result = False
|
||||
self._result = ApprovalChoice.CANCEL
|
||||
self.approval_requested.emit(decision)
|
||||
event.wait()
|
||||
with self._lock:
|
||||
@@ -32,15 +33,15 @@ class ApprovalBridge(QObject):
|
||||
self._event = None
|
||||
return result
|
||||
|
||||
def resolve(self, allowed: bool) -> None:
|
||||
def resolve(self, choice: ApprovalChoice) -> None:
|
||||
with self._lock:
|
||||
self._result = allowed
|
||||
self._result = choice
|
||||
event = self._event
|
||||
if event is not None:
|
||||
event.set()
|
||||
|
||||
def cancel_pending(self) -> None:
|
||||
self.resolve(False)
|
||||
self.resolve(ApprovalChoice.CANCEL)
|
||||
|
||||
|
||||
class StreamWorker(QObject):
|
||||
|
||||
Reference in New Issue
Block a user