Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/pieces/config/managers/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

21 changes: 21 additions & 0 deletions src/pieces/config/schemas/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions src/pieces/tui/controllers/chat_controller.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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."""

Expand Down Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions src/pieces/tui/controllers/copilot_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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}"
)
12 changes: 12 additions & 0 deletions src/pieces/tui/views/copilot_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down