Skip to content

feat: user-defined custom prompts for Moonbeam interactions#203

Open
Copilot wants to merge 5 commits intomasterfrom
copilot/add-user-custom-prompts
Open

feat: user-defined custom prompts for Moonbeam interactions#203
Copilot wants to merge 5 commits intomasterfrom
copilot/add-user-custom-prompts

Conversation

Copy link

Copilot AI commented Mar 24, 2026

Users can now define a custom system prompt that overrides Moonbeam's default instructions when interacting via /prompt and direct @mentions.

Schema

  • Added nullable customPrompt text column to SlackUser (TypeORM sync applies automatically)

Custom Prompt Persistence

CRUD methods (getCustomPrompt, setCustomPrompt, clearCustomPrompt) are added to SlackPersistenceService — no separate service needed. setCustomPrompt trims and normalizes the prompt before persisting (whitespace-only values are stored as null).

New: /ai/set-prompt endpoint

Subject to the standard suppressedMiddleware / textMiddleware / aiMiddleware stack. Prompts are trimmed on receipt; whitespace-only values and prompts exceeding 800 characters are rejected with user-friendly error messages.

Input Behavior
clear (case-insensitive, whitespace-tolerant) Resets to default
any text (≤ 800 chars) Saves as custom prompt

AIService changes

Custom prompt CRUD is exposed via public setCustomPrompt / clearCustomPrompt methods on AIService, which delegates to SlackPersistenceService. The controller calls these service methods — no persistence service is instantiated directly in the controller.

Custom prompts apply to the participate and promptWithHistory code paths only. generateText always uses GENERAL_TEXT_INSTRUCTIONS unchanged. Both paths normalize the stored value (customPrompt?.trim() || null) before use to prevent blank overrides of the default system instructions.

Method Behavior with custom prompt
generateText() Always uses GENERAL_TEXT_INSTRUCTIONS (unaffected)
promptWithHistory() Custom prompt prepended to history instructions when set
participate(teamId, channelId, msg, userId?) Replaces MOONBEAM_SYSTEM_INSTRUCTIONS when set

handle() forwards request.event.user to participate() so tagging lookups resolve to the correct user's prompt.

View original Slack conversation


📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

Copilot AI changed the title [WIP] Add ability for users to create custom prompts for moonbeam feat: user-defined custom prompts for Moonbeam interactions Mar 24, 2026
Copilot AI requested a review from sfreeman422 March 24, 2026 02:56
Copy link
Collaborator

@copilot actually can you make this only apply to the participate code path and the prompt with history? The text path should stay with its current instructions

…cipate and promptWithHistory

Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/e5dacb98-90b9-4ae4-9d41-7b25200e02e8
Copy link
Author

Copilot AI commented Mar 24, 2026

@copilot actually can you make this only apply to the participate code path and the prompt with history? The text path should stay with its current instructions

Done in b6c8f8a. Custom prompts now only apply to participate (tagging) and promptWithHistory (/prompt). generateText (/text) always uses GENERAL_TEXT_INSTRUCTIONS unchanged.

@sfreeman422 sfreeman422 marked this pull request as ready for review March 24, 2026 03:02
Copilot AI review requested due to automatic review settings March 24, 2026 03:02
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for per-user custom system prompts (“custom prompts”) that override Moonbeam’s default instructions for /prompt and @mention participation, with persistence in the SlackUser table and a new endpoint to manage the prompt.

Changes:

  • Add nullable SlackUser.customPrompt column and CRUD persistence service for custom prompts.
  • Apply custom prompts in AIService.promptWithHistory() (prepend) and AIService.participate() (replace), and forward the triggering Slack user ID into participate().
  • Add /ai/set-prompt endpoint (registered before suppression/text/ai middleware) with controller + tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/backend/src/shared/db/models/SlackUser.ts Adds customPrompt column for per-user AI instructions.
packages/backend/src/ai/user-prompt.persistence.service.ts Implements DB read/update/clear for customPrompt.
packages/backend/src/ai/user-prompt.persistence.service.spec.ts Adds unit tests for custom prompt persistence behavior.
packages/backend/src/ai/ai.service.ts Fetches and applies customPrompt in promptWithHistory and participate; passes event user ID into participate.
packages/backend/src/ai/ai.service.spec.ts Adds tests asserting custom prompt is used/falls back correctly.
packages/backend/src/ai/ai.controller.ts Adds /set-prompt management endpoint before existing middlewares.
packages/backend/src/ai/ai.controller.spec.ts Adds endpoint tests for viewing/setting/clearing custom prompts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to +38
const { user_id, team_id, text } = req.body;

if (!text) {
void userPromptPersistenceService.getCustomPrompt(user_id, team_id).then((prompt) => {
if (prompt) {
res.send(`Your current custom prompt: "${prompt}"`);
} else {
res.send(
'You have no custom prompt set. Use `/set-prompt [prompt]` to set one, or `/set-prompt clear` to remove it.',
);
}
});
return;
}

if (text.toLowerCase() === 'clear') {
void userPromptPersistenceService.clearCustomPrompt(user_id, team_id).then((success) => {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text is used without normalization. A whitespace-only value (e.g. " ") will be treated as a real prompt and saved, and clear with surrounding whitespace (" clear ") won’t match the clear branch. Consider trimming once (e.g. const normalized = (text ?? '').trim()), treating empty-after-trim as “show current prompt”, and using the normalized value for the clear check + save.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d07dc43. text is now trimmed once up front ((text ?? '').trim()), whitespace-only values are rejected, and " CLEAR " correctly routes to the clear branch.

Comment on lines +48 to +53
void userPromptPersistenceService.setCustomPrompt(user_id, team_id, text).then((success) => {
if (success) {
res.send(`Your custom prompt has been set.`);
} else {
res.send('Failed to set your custom prompt. Please try again.');
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because /set-prompt bypasses textMiddleware’s 800-char guard, users can persist very large prompts. That can cause DB bloat and downstream OpenAI request failures (token/size limits) when the prompt is later used as instructions. Consider enforcing an explicit maximum length for customPrompt here (with a user-friendly error message) before calling setCustomPrompt.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d07dc43. Added an explicit 800-character limit check in the handler with a user-friendly error message before any persistence call.

Comment on lines +451 to +452
const baseInstructions = customPrompt ?? MOONBEAM_SYSTEM_INSTRUCTIONS;
const systemInstructions = this.appendMemoryContext(baseInstructions, memoryContext);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customPrompt ?? MOONBEAM_SYSTEM_INSTRUCTIONS treats an empty string as a valid override, which can effectively wipe Moonbeam’s system instructions if a blank/whitespace prompt is stored. Consider normalizing via const normalized = customPrompt?.trim(); and only overriding when normalized is non-empty (otherwise fall back to MOONBEAM_SYSTEM_INSTRUCTIONS).

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d07dc43. Both promptWithHistory and participate now do customPrompt?.trim() || null so blank/whitespace-stored values fall back to the default system instructions.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
import { UserPromptPersistenceService } from './user-prompt.persistence.service';

export const aiController: Router = express.Router();
aiController.use(suppressedMiddleware);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should definitely still apply. Please add back.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d07dc43. suppressedMiddleware, textMiddleware, and aiMiddleware are now registered before all routes, including /set-prompt.


const webService = new WebService();
const aiService = new AIService();
const userPromptPersistenceService = new UserPromptPersistenceService();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a whole new service for this. Can these be moved into the user persistence service?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are also breaking a pattern here by having the ai.controller hit a persistence service directly. The AIService should be leveraging any persistence services.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d07dc43. getCustomPrompt, setCustomPrompt, and clearCustomPrompt are now methods on SlackPersistenceService. UserPromptPersistenceService has been deleted. The controller now calls aiService.setCustomPrompt/clearCustomPrompt — no persistence service is instantiated directly in the controller.

const userPromptPersistenceService = new UserPromptPersistenceService();
const aiLogger = logger.child({ module: 'AIController' });

// /set-prompt does not require rate-limiting or text-length validation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does.

Comment on lines +451 to +452
const baseInstructions = customPrompt ?? MOONBEAM_SYSTEM_INSTRUCTIONS;
const systemInstructions = this.appendMemoryContext(baseInstructions, memoryContext);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix it

Comment on lines +48 to +53
void userPromptPersistenceService.setCustomPrompt(user_id, team_id, text).then((success) => {
if (success) {
res.send(`Your custom prompt has been set.`);
} else {
res.send('Failed to set your custom prompt. Please try again.');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix it

Comment on lines +22 to +38
const { user_id, team_id, text } = req.body;

if (!text) {
void userPromptPersistenceService.getCustomPrompt(user_id, team_id).then((prompt) => {
if (prompt) {
res.send(`Your current custom prompt: "${prompt}"`);
} else {
res.send(
'You have no custom prompt set. Use `/set-prompt [prompt]` to set one, or `/set-prompt clear` to remove it.',
);
}
});
return;
}

if (text.toLowerCase() === 'clear') {
void userPromptPersistenceService.clearCustomPrompt(user_id, team_id).then((success) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix it

…hrough AIService, restore middleware order

Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/ef34bcb8-cd7e-474b-913e-339106bccdb1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants