Add AI persona profiles for prompt injection
Profiles allow users to inject system instructions into prompts, customizing the AI's behavior and persona for tool execution. Features: - Profile dataclass with name, description, system_prompt - 8 built-in profiles: None, Comedian, Technical Writer, Teacher, Concise, Creative, Code Reviewer, Analyst - Custom profile creation and storage in ~/.cmdforge/profiles/ - Profile selector in Prompt Step dialog - Profile injection during tool execution - Profiles page in GUI (Ctrl+4) for viewing and managing profiles Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -5,9 +5,11 @@ from PySide6.QtWidgets import (
|
||||
QComboBox, QPushButton, QHBoxLayout, QLabel,
|
||||
QPlainTextEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from ...tool import PromptStep, CodeStep
|
||||
from ...providers import load_providers
|
||||
from ...profiles import list_profiles
|
||||
|
||||
|
||||
class PromptStepDialog(QDialog):
|
||||
@@ -43,6 +45,16 @@ class PromptStepDialog(QDialog):
|
||||
self.provider_combo.addItem(default)
|
||||
form.addRow("Provider:", self.provider_combo)
|
||||
|
||||
# Profile selection
|
||||
self.profile_combo = QComboBox()
|
||||
profiles = list_profiles()
|
||||
for profile in profiles:
|
||||
self.profile_combo.addItem(profile.name)
|
||||
# Set tooltip with description
|
||||
idx = self.profile_combo.count() - 1
|
||||
self.profile_combo.setItemData(idx, profile.description, Qt.ToolTipRole)
|
||||
form.addRow("Profile:", self.profile_combo)
|
||||
|
||||
# Output variable
|
||||
self.output_input = QLineEdit()
|
||||
self.output_input.setPlaceholderText("response")
|
||||
@@ -87,6 +99,12 @@ class PromptStepDialog(QDialog):
|
||||
else:
|
||||
self.provider_combo.setCurrentText(step.provider)
|
||||
|
||||
# Load profile
|
||||
if step.profile:
|
||||
idx = self.profile_combo.findText(step.profile)
|
||||
if idx >= 0:
|
||||
self.profile_combo.setCurrentIndex(idx)
|
||||
|
||||
self.output_input.setText(step.output_var)
|
||||
self.prompt_input.setPlainText(step.prompt)
|
||||
|
||||
@@ -106,10 +124,15 @@ class PromptStepDialog(QDialog):
|
||||
|
||||
def get_step(self) -> PromptStep:
|
||||
"""Get the step from form data."""
|
||||
profile = self.profile_combo.currentText()
|
||||
# Don't store "None" profile
|
||||
if profile == "None":
|
||||
profile = None
|
||||
return PromptStep(
|
||||
prompt=self.prompt_input.toPlainText(),
|
||||
provider=self.provider_combo.currentText(),
|
||||
output_var=self.output_input.text().strip()
|
||||
output_var=self.output_input.text().strip(),
|
||||
profile=profile
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ class MainWindow(QMainWindow):
|
||||
("Tools", "Manage your tools"),
|
||||
("Registry", "Browse and install tools"),
|
||||
("Providers", "Configure AI providers"),
|
||||
("Profiles", "AI persona profiles"),
|
||||
]
|
||||
|
||||
font = QFont()
|
||||
@@ -92,14 +93,17 @@ class MainWindow(QMainWindow):
|
||||
from .pages.tools_page import ToolsPage
|
||||
from .pages.registry_page import RegistryPage
|
||||
from .pages.providers_page import ProvidersPage
|
||||
from .pages.profiles_page import ProfilesPage
|
||||
|
||||
self.tools_page = ToolsPage(self)
|
||||
self.registry_page = RegistryPage(self)
|
||||
self.providers_page = ProvidersPage(self)
|
||||
self.profiles_page = ProfilesPage(self)
|
||||
|
||||
self.pages.addWidget(self.tools_page)
|
||||
self.pages.addWidget(self.registry_page)
|
||||
self.pages.addWidget(self.providers_page)
|
||||
self.pages.addWidget(self.profiles_page)
|
||||
|
||||
def _on_page_changed(self, index: int):
|
||||
"""Handle page changes."""
|
||||
@@ -120,6 +124,7 @@ class MainWindow(QMainWindow):
|
||||
"tools": 0,
|
||||
"registry": 1,
|
||||
"providers": 2,
|
||||
"profiles": 3,
|
||||
}
|
||||
if page_name.lower() in page_map:
|
||||
self.sidebar.setCurrentRow(page_map[page_name.lower()])
|
||||
@@ -164,7 +169,7 @@ class MainWindow(QMainWindow):
|
||||
shortcut_escape = QShortcut(QKeySequence("Escape"), self)
|
||||
shortcut_escape.activated.connect(self._shortcut_escape)
|
||||
|
||||
# Ctrl+1/2/3: Navigate pages
|
||||
# Ctrl+1/2/3/4: Navigate pages
|
||||
shortcut_page1 = QShortcut(QKeySequence("Ctrl+1"), self)
|
||||
shortcut_page1.activated.connect(lambda: self.sidebar.setCurrentRow(0))
|
||||
|
||||
@@ -174,6 +179,9 @@ class MainWindow(QMainWindow):
|
||||
shortcut_page3 = QShortcut(QKeySequence("Ctrl+3"), self)
|
||||
shortcut_page3.activated.connect(lambda: self.sidebar.setCurrentRow(2))
|
||||
|
||||
shortcut_page4 = QShortcut(QKeySequence("Ctrl+4"), self)
|
||||
shortcut_page4.activated.connect(lambda: self.sidebar.setCurrentRow(3))
|
||||
|
||||
# Ctrl+R: Refresh current page
|
||||
shortcut_refresh = QShortcut(QKeySequence("Ctrl+R"), self)
|
||||
shortcut_refresh.activated.connect(self._shortcut_refresh)
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Profiles page - manage AI persona profiles."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
|
||||
QPushButton, QLabel, QGroupBox, QTextEdit, QLineEdit,
|
||||
QFormLayout, QMessageBox, QSplitter
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from ...profiles import list_profiles, save_profile, delete_profile, Profile
|
||||
|
||||
|
||||
class ProfilesPage(QWidget):
|
||||
"""Profiles management page."""
|
||||
|
||||
def __init__(self, main_window):
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self._setup_ui()
|
||||
self.refresh()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Header
|
||||
header = QHBoxLayout()
|
||||
title = QLabel("Profiles")
|
||||
title.setObjectName("heading")
|
||||
header.addWidget(title)
|
||||
|
||||
header.addStretch()
|
||||
|
||||
self.btn_new = QPushButton("New Profile")
|
||||
self.btn_new.clicked.connect(self._new_profile)
|
||||
header.addWidget(self.btn_new)
|
||||
|
||||
layout.addLayout(header)
|
||||
|
||||
# Description
|
||||
desc = QLabel(
|
||||
"AI persona profiles inject system instructions into prompts. "
|
||||
"Select a profile when creating a prompt step to customize the AI's behavior."
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #718096;")
|
||||
layout.addWidget(desc)
|
||||
|
||||
# Main content splitter
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Left: Profile list
|
||||
left = QWidget()
|
||||
left_layout = QVBoxLayout(left)
|
||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.profile_list = QListWidget()
|
||||
self.profile_list.currentItemChanged.connect(self._on_profile_selected)
|
||||
left_layout.addWidget(self.profile_list)
|
||||
|
||||
splitter.addWidget(left)
|
||||
|
||||
# Right: Profile details/editor
|
||||
right = QWidget()
|
||||
right_layout = QVBoxLayout(right)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Details group
|
||||
details_box = QGroupBox("Profile Details")
|
||||
details_layout = QFormLayout(details_box)
|
||||
details_layout.setSpacing(12)
|
||||
|
||||
self.name_input = QLineEdit()
|
||||
self.name_input.setPlaceholderText("Profile name")
|
||||
details_layout.addRow("Name:", self.name_input)
|
||||
|
||||
self.desc_input = QLineEdit()
|
||||
self.desc_input.setPlaceholderText("Brief description")
|
||||
details_layout.addRow("Description:", self.desc_input)
|
||||
|
||||
right_layout.addWidget(details_box)
|
||||
|
||||
# System prompt group
|
||||
prompt_box = QGroupBox("System Prompt")
|
||||
prompt_layout = QVBoxLayout(prompt_box)
|
||||
|
||||
self.prompt_input = QTextEdit()
|
||||
self.prompt_input.setPlaceholderText(
|
||||
"Enter the system instructions that will be injected into prompts.\n\n"
|
||||
"Example: You are a helpful assistant who explains things clearly..."
|
||||
)
|
||||
prompt_layout.addWidget(self.prompt_input)
|
||||
|
||||
right_layout.addWidget(prompt_box, 1)
|
||||
|
||||
# Action buttons
|
||||
actions = QHBoxLayout()
|
||||
|
||||
self.btn_save = QPushButton("Save")
|
||||
self.btn_save.clicked.connect(self._save_profile)
|
||||
self.btn_save.setEnabled(False)
|
||||
actions.addWidget(self.btn_save)
|
||||
|
||||
self.btn_delete = QPushButton("Delete")
|
||||
self.btn_delete.setObjectName("danger")
|
||||
self.btn_delete.clicked.connect(self._delete_profile)
|
||||
self.btn_delete.setEnabled(False)
|
||||
actions.addWidget(self.btn_delete)
|
||||
|
||||
actions.addStretch()
|
||||
|
||||
right_layout.addLayout(actions)
|
||||
|
||||
splitter.addWidget(right)
|
||||
splitter.setSizes([300, 500])
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
# Status label
|
||||
self.status_label = QLabel("")
|
||||
self.status_label.setStyleSheet("color: #718096; font-style: italic;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the profile list."""
|
||||
self.profile_list.clear()
|
||||
profiles = list_profiles()
|
||||
|
||||
for profile in profiles:
|
||||
item = QListWidgetItem()
|
||||
if profile.builtin:
|
||||
item.setText(f"{profile.name} (built-in)")
|
||||
else:
|
||||
item.setText(profile.name)
|
||||
item.setData(Qt.UserRole, profile)
|
||||
self.profile_list.addItem(item)
|
||||
|
||||
self._clear_form()
|
||||
self.status_label.setText(f"{len(profiles)} profiles available")
|
||||
|
||||
def _on_profile_selected(self, current, previous):
|
||||
"""Handle profile selection."""
|
||||
if not current:
|
||||
self._clear_form()
|
||||
return
|
||||
|
||||
profile = current.data(Qt.UserRole)
|
||||
self._load_profile(profile)
|
||||
|
||||
def _load_profile(self, profile: Profile):
|
||||
"""Load profile into form."""
|
||||
self.name_input.setText(profile.name)
|
||||
self.desc_input.setText(profile.description)
|
||||
self.prompt_input.setPlainText(profile.system_prompt)
|
||||
|
||||
# Built-in profiles can't be edited
|
||||
is_builtin = profile.builtin
|
||||
self.name_input.setEnabled(not is_builtin)
|
||||
self.desc_input.setEnabled(not is_builtin)
|
||||
self.prompt_input.setEnabled(not is_builtin)
|
||||
self.btn_save.setEnabled(not is_builtin)
|
||||
self.btn_delete.setEnabled(not is_builtin and profile.name != "None")
|
||||
|
||||
if is_builtin:
|
||||
self.status_label.setText("Built-in profile (read-only)")
|
||||
else:
|
||||
self.status_label.setText("Custom profile")
|
||||
|
||||
def _clear_form(self):
|
||||
"""Clear the form."""
|
||||
self.name_input.clear()
|
||||
self.name_input.setEnabled(True)
|
||||
self.desc_input.clear()
|
||||
self.desc_input.setEnabled(True)
|
||||
self.prompt_input.clear()
|
||||
self.prompt_input.setEnabled(True)
|
||||
self.btn_save.setEnabled(False)
|
||||
self.btn_delete.setEnabled(False)
|
||||
|
||||
def _new_profile(self):
|
||||
"""Start creating a new profile."""
|
||||
self.profile_list.clearSelection()
|
||||
self._clear_form()
|
||||
self.name_input.setFocus()
|
||||
self.btn_save.setEnabled(True)
|
||||
self.status_label.setText("Creating new profile...")
|
||||
|
||||
def _save_profile(self):
|
||||
"""Save the current profile."""
|
||||
name = self.name_input.text().strip()
|
||||
if not name:
|
||||
QMessageBox.warning(self, "Validation", "Profile name is required")
|
||||
return
|
||||
|
||||
if name == "None":
|
||||
QMessageBox.warning(self, "Validation", "'None' is a reserved profile name")
|
||||
return
|
||||
|
||||
description = self.desc_input.text().strip()
|
||||
system_prompt = self.prompt_input.toPlainText().strip()
|
||||
|
||||
if not system_prompt:
|
||||
QMessageBox.warning(self, "Validation", "System prompt is required")
|
||||
return
|
||||
|
||||
profile = Profile(
|
||||
name=name,
|
||||
description=description,
|
||||
system_prompt=system_prompt
|
||||
)
|
||||
|
||||
try:
|
||||
save_profile(profile)
|
||||
self.main_window.show_status(f"Saved profile '{name}'")
|
||||
self.refresh()
|
||||
# Select the saved profile
|
||||
for i in range(self.profile_list.count()):
|
||||
item = self.profile_list.item(i)
|
||||
if item.data(Qt.UserRole).name == name:
|
||||
self.profile_list.setCurrentItem(item)
|
||||
break
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to save profile:\n{e}")
|
||||
|
||||
def _delete_profile(self):
|
||||
"""Delete the selected profile."""
|
||||
current = self.profile_list.currentItem()
|
||||
if not current:
|
||||
return
|
||||
|
||||
profile = current.data(Qt.UserRole)
|
||||
if profile.builtin:
|
||||
QMessageBox.warning(self, "Error", "Cannot delete built-in profiles")
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self, "Confirm Delete",
|
||||
f"Delete profile '{profile.name}'?\n\nThis cannot be undone.",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
if delete_profile(profile.name):
|
||||
self.main_window.show_status(f"Deleted profile '{profile.name}'")
|
||||
self.refresh()
|
||||
else:
|
||||
QMessageBox.warning(self, "Error", "Failed to delete profile")
|
||||
Reference in New Issue
Block a user