feat: connect desktop sessions and streaming

This commit is contained in:
2026-07-30 20:18:11 +02:00
parent 1658e0485e
commit cf35148576
7 changed files with 150 additions and 21 deletions
+27 -5
View File
@@ -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,
+8 -1
View File
@@ -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,
+26 -2
View File
@@ -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)
+7 -6
View File
@@ -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):