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:
rob
2026-01-14 05:22:31 -04:00
co-authored by Claude Opus 4.5
parent 9d56f703cd
commit 9588c3f8b0
6 changed files with 473 additions and 3 deletions
+24 -1
View File
@@ -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
)
+9 -1
View File
@@ -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)
+250
View File
@@ -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")