Slash-command replies and the echoed /setup command are persisted to session history so they render in the transcript, but they are UI chatter the user never meant as conversation. They were sent to the model on the next turn, which then commented on '/setup ...' and exposed transient values (e.g. the Copilot device user_code) to the LLM. - get_context_messages() (the LLM-API view) now skips messages tagged metadata.source == 'slash'. Display/history-load paths use raw history and are unaffected. - slashCommands.js tags the echoed user command with source:'slash' too (the assistant replies already carried it); the user line was the one untagged path that still reached context. Fixes #2634.
97 lines
2.8 KiB
Python
97 lines
2.8 KiB
Python
# core/models.py
|
|
"""
|
|
Pure data models — no database logic, no side effects.
|
|
|
|
These are simple datacontainers. All persistence is handled by SessionManager.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Any, Optional, TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from .session_manager import SessionManager
|
|
|
|
# Module-level session manager reference (set at app startup)
|
|
_session_manager: Optional["SessionManager"] = None
|
|
|
|
|
|
def set_session_manager(manager: "SessionManager"):
|
|
"""Set the global session manager reference."""
|
|
global _session_manager
|
|
_session_manager = manager
|
|
|
|
|
|
@dataclass
|
|
class ChatMessage:
|
|
"""A single chat message."""
|
|
role: str
|
|
content: str
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dict for API responses."""
|
|
result = {"role": self.role, "content": self.content}
|
|
if self.metadata:
|
|
result["metadata"] = self.metadata
|
|
return result
|
|
|
|
def get(self, key: str, default=None):
|
|
"""Dict-like access for compatibility."""
|
|
return getattr(self, key, default)
|
|
|
|
|
|
@dataclass
|
|
class Session:
|
|
"""A chat session — pure data container."""
|
|
id: str
|
|
name: str
|
|
endpoint_url: str
|
|
model: str
|
|
rag: bool = False
|
|
archived: bool = False
|
|
headers: Optional[Dict[str, str]] = None
|
|
history: List[ChatMessage] = None
|
|
owner: Optional[str] = None
|
|
is_important: bool = False
|
|
message_count: int = 0
|
|
|
|
def __post_init__(self):
|
|
if self.history is None:
|
|
self.history = []
|
|
if self.headers is None:
|
|
self.headers = {}
|
|
|
|
def add_message(self, message: ChatMessage):
|
|
"""
|
|
Add a message to this session.
|
|
|
|
Delegates to SessionManager for persistence if available,
|
|
otherwise just appends to history.
|
|
"""
|
|
self.history.append(message)
|
|
self.message_count = len(self.history)
|
|
|
|
# Delegate to session manager for persistence
|
|
if _session_manager:
|
|
_session_manager._persist_message(self.id, message)
|
|
|
|
def get_context_messages(self) -> List[Dict[str, Any]]:
|
|
"""Get messages in format for LLM API.
|
|
|
|
Slash-command / setup replies are persisted to history so they render
|
|
in the transcript, but they are UI chatter (e.g. ``/setup ...`` and its
|
|
status lines) the user never meant as conversation. They carry
|
|
``metadata.source == "slash"``; exclude them here so they never reach
|
|
the model. Display/history-load paths use the raw ``history`` and are
|
|
unaffected.
|
|
"""
|
|
return [
|
|
msg.to_dict()
|
|
for msg in self.history
|
|
if (msg.metadata or {}).get("source") != "slash"
|
|
]
|
|
|
|
def get(self, key: str, default=None):
|
|
"""Dict-like access for compatibility."""
|
|
return getattr(self, key, default)
|