diff --git a/src/pieces/config/managers/cli.py b/src/pieces/config/managers/cli.py index 7d361324..a0820009 100644 --- a/src/pieces/config/managers/cli.py +++ b/src/pieces/config/managers/cli.py @@ -43,3 +43,28 @@ def theme(self, value: str) -> None: self.config.theme = value self.save() + @property + def auto_enable_chat_ltm(self) -> bool: + """Whether new chats should auto-attach Long-Term Memory.""" + return self.config.auto_enable_chat_ltm + + @auto_enable_chat_ltm.setter + def auto_enable_chat_ltm(self, value: bool) -> None: + """Toggle auto-LTM-on-new-chat and persist.""" + self.config.auto_enable_chat_ltm = bool(value) + self.save() + + @property + def auto_enable_chat_ltm_lookback_days(self) -> int: + """How many days back the auto-attached LTM range covers.""" + return self.config.auto_enable_chat_ltm_lookback_days + + @auto_enable_chat_ltm_lookback_days.setter + def auto_enable_chat_ltm_lookback_days(self, value: int) -> None: + """Set lookback days (>=0; 0 = SDK 15-min default) and persist.""" + v = int(value) + if v < 0: + raise ValueError("auto_enable_chat_ltm_lookback_days must be >= 0") + self.config.auto_enable_chat_ltm_lookback_days = v + self.save() + diff --git a/src/pieces/config/schemas/cli.py b/src/pieces/config/schemas/cli.py index a89027ab..e2d289bc 100644 --- a/src/pieces/config/schemas/cli.py +++ b/src/pieces/config/schemas/cli.py @@ -15,6 +15,27 @@ class CLIConfigSchema(BaseModel): ) editor: Optional[str] = Field(default=None, description="Default editor command") theme: str = Field(default="pieces-dark", description="TUI theme preference") + auto_enable_chat_ltm: bool = Field( + default=False, + description=( + "Auto-attach Long-Term Memory to every new chat created in the " + "TUI. Only takes effect when system LTM (workstream pattern " + "engine) is already enabled. Defaults False so privacy-sensitive " + "users opt in explicitly." + ), + ) + auto_enable_chat_ltm_lookback_days: int = Field( + default=7, + ge=0, + description=( + "When auto-attaching LTM to a chat, the range covers the last N " + "days. The SDK default of 15 minutes is far too narrow for " + "long-term memory; 7 days covers a typical work week. Set to 0 " + "to keep the SDK 15-minute default (e.g. when a user only wants " + "very recent context attached). Has no effect unless " + "auto_enable_chat_ltm is also True." + ), + ) @field_validator("editor") @classmethod diff --git a/src/pieces/tui/controllers/chat_controller.py b/src/pieces/tui/controllers/chat_controller.py index 021d5aa4..567bd004 100644 --- a/src/pieces/tui/controllers/chat_controller.py +++ b/src/pieces/tui/controllers/chat_controller.py @@ -1,5 +1,6 @@ """Controller for handling chat-related events.""" +from datetime import datetime, timedelta from typing import Optional, TYPE_CHECKING from pieces.settings import Settings from pieces.copilot.ltm import update_ltm_cache @@ -9,11 +10,47 @@ ConversationWS, ) from pieces._vendor.pieces_os_client.wrapper.basic_identifier.chat import BasicChat +from pieces._vendor.pieces_os_client.wrapper.basic_identifier.range import BasicRange +from pieces._vendor.pieces_os_client.wrapper.streamed_identifiers import ( + ConversationsSnapshot, +) if TYPE_CHECKING: from pieces._vendor.pieces_os_client.models.conversation import Conversation +def _activate_chat_ltm_with_lookback() -> None: + """Activate Chat-LTM with a configurable lookback window. + + Mirrors ``ltm.chat_enable_ltm()`` but lets the caller widen the + backing range. The SDK default is 15 minutes, far too narrow for + long-term-memory; we honor + ``cli_config.auto_enable_chat_ltm_lookback_days`` (default 7) to + cover a typical work week. Setting the value to 0 falls back to + the SDK default for users who want only the most recent context. + + The trailing local-cache refresh mirrors what ``chat_enable_ltm`` + does so callers reading ``conversation.ranges`` immediately after + see the new range without an extra round-trip. + """ + days = Settings.cli_config.auto_enable_chat_ltm_lookback_days + chat = Settings.pieces_client.copilot.chat + if not chat: + chat = Settings.pieces_client.copilot.create_chat("New Conversation") + + if days > 0: + chat.associate_range( + BasicRange.create(from_=datetime.now() - timedelta(days=days)) + ) + else: + chat.associate_range(BasicRange.create()) # SDK 15-min default + + conv = Settings.pieces_client.conversation_api.conversation_get_specific_conversation( + chat.id + ) + ConversationsSnapshot.identifiers_snapshot[conv.id] = conv + + class ChatController(BaseController): """Handles chat-related events from the backend.""" @@ -72,11 +109,42 @@ def switch_chat(self, chat: Optional["BasicChat"]): Args: chat: The BasicChat object to switch to (None for new chat) """ + fresh_chat_created = chat is None if not chat: chat = Settings.pieces_client.copilot.create_chat() Settings.pieces_client.copilot.chat = chat + + if fresh_chat_created: + self._maybe_auto_enable_chat_ltm() + self.emit(EventType.CHAT_SWITCHED, chat) + def _maybe_auto_enable_chat_ltm(self) -> None: + """Auto-attach LTM to a freshly-created chat when the user opts in. + + Two preconditions must hold: the user has set + ``cli_config.auto_enable_chat_ltm`` (opt-in, default False) AND + the system-level workstream pattern engine is already enabled. + Without the second check, this would silently do nothing on a + machine where LTM permissions were never granted. + + Failures are logged and swallowed: a transient API hiccup must + not break chat creation. The user can always toggle manually + with Ctrl+L if auto-enable did not land. + """ + try: + if not Settings.cli_config.auto_enable_chat_ltm: + return + if self.is_chat_ltm_enabled(): + return + if not self.is_ltm_running(): + return + _activate_chat_ltm_with_lookback() + except Exception as e: + Settings.logger.error( + f"Auto-enable Chat-LTM failed: {e}" + ) + def create_new_chat(self): """Create a new chat and notify listeners.""" self.switch_chat(None) diff --git a/src/pieces/tui/controllers/copilot_controller.py b/src/pieces/tui/controllers/copilot_controller.py index 818844dc..60c0cf43 100644 --- a/src/pieces/tui/controllers/copilot_controller.py +++ b/src/pieces/tui/controllers/copilot_controller.py @@ -70,6 +70,13 @@ def ask_question(self, query: str): "No active chat, copilot will create new one automatically" ) + # Auto-enable Chat-LTM (opt-in via cli_config.auto_enable_chat_ltm) + # right before the stream goes out, so the *first* message of the + # chat receives LTM context. Hooking only on chat creation would + # miss the very common case of clicking an existing chat that + # never had LTM attached. + self._maybe_auto_enable_chat_ltm() + # Emit thinking started event self.emit(EventType.COPILOT_THINKING_STARTED, None) @@ -130,6 +137,13 @@ def _on_stream_message(self, response: "QGPTStreamOutput"): Settings.logger.info( f"Copilot created/switched to chat: {new_chat.id}" ) + # Auto-enable LTM on chats the SDK created + # implicitly (user typed before pressing Ctrl+N). + # The first response went out without LTM context + # — we cannot retroactively help it — but every + # follow-up message in this chat will see LTM. + if old_chat is None: + self._maybe_auto_enable_chat_ltm() # Emit event that event hub will bridge to CHAT_SWITCHED self.emit(EventType.CHAT_SWITCHED, new_chat) @@ -211,3 +225,40 @@ def stop_streaming(self): def is_streaming(self) -> bool: """Check if currently streaming a response.""" return self._current_status == "IN-PROGRESS" + + def _maybe_auto_enable_chat_ltm(self) -> None: + """Just-in-time activation of Chat-LTM before sending a message. + + Three preconditions: the user has opted in via + ``cli_config.auto_enable_chat_ltm`` (default False); system LTM + (workstream pattern engine) is running; the current chat does + not already have an LTM range attached. Skipping when already + enabled keeps repeated sends idempotent — no piling-on of + redundant ranges. + + Called from ``ask_question`` before ``stream_question`` so the + first message lands with LTM context. Covers all three chat + origin paths: Ctrl+N + send, click-existing-chat + send, and + type-and-send-without-active-chat (where ``chat_enable_ltm`` + creates the chat itself). Failures are logged and swallowed — + a transient API hiccup must not break the user's send. + """ + try: + if not Settings.cli_config.auto_enable_chat_ltm: + return + ltm = Settings.pieces_client.copilot.context.ltm + if ltm.is_chat_ltm_enabled: + return + ltm.ltm_status = ( + Settings.pieces_client + .work_stream_pattern_engine_api + .workstream_pattern_engine_processors_vision_status() + ) + if not ltm.is_enabled: + return + from .chat_controller import _activate_chat_ltm_with_lookback + _activate_chat_ltm_with_lookback() + except Exception as e: + Settings.logger.error( + f"Auto-enable Chat-LTM failed: {e}" + ) diff --git a/src/pieces/tui/views/copilot_view.py b/src/pieces/tui/views/copilot_view.py index 27a2ecc8..0d82eefd 100644 --- a/src/pieces/tui/views/copilot_view.py +++ b/src/pieces/tui/views/copilot_view.py @@ -145,6 +145,18 @@ async def _show_welcome_message(self): # Message handlers that need view-level handling async def on_chat_messages_switched(self, message: ChatMessages.Switched) -> None: """Handle chat switch event - load conversation and focus input.""" + # Refresh LTM status indicator: switching chats (or auto-enabling + # LTM on a chat just before sending) changes the per-chat flag, + # but the status bar would otherwise hold the previous value + # until the next manual toggle. + if self.status_bar and self.event_hub: + try: + self.status_bar.update_ltm_status( + self.event_hub.chat.is_chat_ltm_enabled() + ) + except Exception as e: + Settings.logger.error(f"Failed to refresh LTM status bar: {e}") + if self.chat_view_panel: Settings.logger.info( f"Updating conversation: {message.chat.id} - {message.chat.name}"