mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-27 16:12:29 +00:00
feat: present Ask User as a native builtin in the tools dialog
It ships with the app and pauses the run like a first-class feature, so it belongs with the builtins (Run Code, Web Search, Memory, ...) rather than in the third-party plugin list — while its mechanics stay exactly a plugin's: - BuiltinId += 'ask_user_question' (documented exception: a native TOOL, not a capability; selection reads agent.tools, the toggle emits tool-add/remove patches instead of a capability field) - buildCatalog surfaces it as a builtin gated on the same signals as before (tools capability on + the server lists the plugin, i.e. not admin-filtered) and skips it in the plugin loop so it never double-lists - On-theme icon: lucide MessageCircleQuestion in a teal chip via the builtin icon map, matching the other native entries; the bespoke purple SVG and the manifest icon field are gone - i18n'd name/description keys like the other builtins
This commit is contained in:
parent
fc08aeac93
commit
a8a26a106f
11 changed files with 119 additions and 9 deletions
|
|
@ -94,7 +94,6 @@
|
|||
"name": "Ask User",
|
||||
"pluginKey": "ask_user_question",
|
||||
"description": "Let the agent pause mid-run to ask you a clarifying question and wait for your answer.",
|
||||
"icon": "assets/ask-user-question.svg",
|
||||
"agentsOnly": true,
|
||||
"authConfig": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<rect width="24" height="24" rx="6" fill="#7C3AED"/>
|
||||
<path d="M12 4.75c-3.31 0-6 2.32-6 5.18 0 1.63.87 3.08 2.24 4.03-.08.65-.35 1.5-1.04 2.29a.4.4 0 0 0 .32.66c1.5-.06 2.63-.63 3.36-1.19.36.06.74.09 1.12.09 3.31 0 6-2.32 6-5.18s-2.69-5.18-6-5.18Z" fill="#fff"/>
|
||||
<path d="M11.98 12.05c-.4 0-.68-.28-.68-.67v-.09c0-.72.4-1.12.93-1.48.51-.35.73-.55.73-.94 0-.44-.35-.74-.89-.74-.42 0-.73.19-.94.55-.16.24-.36.35-.63.35-.4 0-.63-.26-.63-.58 0-.13.03-.25.08-.37.3-.71 1.13-1.19 2.19-1.19 1.32 0 2.26.73 2.26 1.86 0 .74-.37 1.16-.98 1.56-.52.34-.72.56-.74.99-.03.44-.28.75-.7.75Zm-.01 2.1a.83.83 0 0 1-.84-.83c0-.46.37-.83.84-.83.46 0 .83.37.83.83a.83.83 0 0 1-.83.83Z" fill="#7C3AED"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 768 B |
|
|
@ -57,6 +57,30 @@ describe('buildCatalog', () => {
|
|||
expect(systemDefined?.kind === 'builtin' && systemDefined.userProvidedAuth).toBe(false);
|
||||
});
|
||||
|
||||
test('surfaces ask_user_question as a BUILTIN (not a plugin) when the server lists it', () => {
|
||||
const askPlugin = makePlugin({ pluginKey: 'ask_user_question', name: 'Ask User' });
|
||||
const items = buildCatalog({ ...toolInputs, regularTools: [askPlugin] });
|
||||
const builtin = items.find((i) => i.kind === 'builtin' && i.id === 'ask_user_question');
|
||||
expect(builtin).toBeDefined();
|
||||
expect(builtin?.iconKey).toBe('ask_user_question');
|
||||
// never double-listed in the plugin section
|
||||
expect(items.find((i) => i.kind === 'tool' && i.id === 'ask_user_question')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('omits the ask_user_question builtin when the server filtered it or tools are off', () => {
|
||||
const askPlugin = makePlugin({ pluginKey: 'ask_user_question', name: 'Ask User' });
|
||||
// admin filtered (not in regularTools)
|
||||
expect(
|
||||
buildCatalog(toolInputs).find((i) => i.kind === 'builtin' && i.id === 'ask_user_question'),
|
||||
).toBeUndefined();
|
||||
// tools capability off
|
||||
expect(
|
||||
buildCatalog({ ...emptyInputs, regularTools: [askPlugin] }).find(
|
||||
(i) => i.kind === 'builtin' && i.id === 'ask_user_question',
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('hides MCP items when the user lacks MCP permission', () => {
|
||||
const map = new Map();
|
||||
map.set('srv', { serverName: 'srv', isConfigured: true, tools: [] });
|
||||
|
|
|
|||
|
|
@ -152,3 +152,24 @@ describe('skillsEnabledTransition', () => {
|
|||
expect(skillsEnabledTransition([], undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeToggleAction — ask_user_question builtin', () => {
|
||||
const item = {
|
||||
kind: 'builtin',
|
||||
id: 'ask_user_question',
|
||||
iconKey: 'ask_user_question',
|
||||
name: 'com_ui_ask_user',
|
||||
description: 'com_agents_ask_user_info',
|
||||
} as const;
|
||||
|
||||
test('toggles agent.tools, never a capability field', () => {
|
||||
expect(computeToggleAction(item as never, { selected: false })).toEqual({
|
||||
type: 'tool-add',
|
||||
id: 'ask_user_question',
|
||||
});
|
||||
expect(computeToggleAction(item as never, { selected: true })).toEqual({
|
||||
type: 'tool-remove',
|
||||
id: 'ask_user_question',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -224,3 +224,28 @@ describe('matchesMcpServer', () => {
|
|||
expect(matchesMcpServer('search_mcp_github_extra', 'github_extra')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selection — ask_user_question builtin rides agent.tools', () => {
|
||||
const catalogWithAsk: AgentItem[] = [
|
||||
...sampleCatalog,
|
||||
{
|
||||
kind: 'builtin',
|
||||
id: 'ask_user_question',
|
||||
name: 'com_ui_ask_user',
|
||||
description: 'com_agents_ask_user_info',
|
||||
iconKey: 'ask_user_question',
|
||||
},
|
||||
];
|
||||
|
||||
test('selected iff agent.tools contains the tool (no capability field involved)', () => {
|
||||
const selected = deriveSelectedItems(
|
||||
{ ...emptyFormState, tools: ['ask_user_question'] },
|
||||
catalogWithAsk,
|
||||
[],
|
||||
);
|
||||
expect(selected.map((i) => i.id)).toContain('ask_user_question');
|
||||
|
||||
const none = deriveSelectedItems(emptyFormState, catalogWithAsk, []);
|
||||
expect(none.map((i) => i.id)).not.toContain('ask_user_question');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,6 +118,27 @@ export function buildCatalog(inputs: BuildCatalogInputs): AgentItem[] {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Native tool presented with the builtins (it ships with the app and pauses
|
||||
* the run like a first-class feature), while remaining an `agent.tools`
|
||||
* entry mechanically. Availability = the plugin listing itself: it appears
|
||||
* only when the `tools` capability is on AND the server lists the plugin
|
||||
* (i.e. the admin didn't filter it) — the same gates as the plugin section
|
||||
* it graduated from.
|
||||
*/
|
||||
if (
|
||||
enabled.has(AgentCapabilities.tools) &&
|
||||
inputs.regularTools.some((plugin) => plugin.pluginKey === 'ask_user_question')
|
||||
) {
|
||||
items.push({
|
||||
kind: 'builtin',
|
||||
id: 'ask_user_question',
|
||||
iconKey: 'ask_user_question',
|
||||
name: 'com_ui_ask_user',
|
||||
description: 'com_agents_ask_user_info',
|
||||
});
|
||||
}
|
||||
|
||||
if (inputs.showMemory) {
|
||||
items.push({
|
||||
kind: 'builtin',
|
||||
|
|
@ -150,6 +171,9 @@ export function buildCatalog(inputs: BuildCatalogInputs): AgentItem[] {
|
|||
|
||||
if (enabled.has(AgentCapabilities.tools)) {
|
||||
for (const plugin of inputs.regularTools) {
|
||||
if (plugin.pluginKey === 'ask_user_question') {
|
||||
continue; // surfaced as a builtin above — don't double-list as a plugin
|
||||
}
|
||||
items.push({
|
||||
kind: 'tool',
|
||||
id: plugin.pluginKey,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
Code,
|
||||
MessageCircleQuestion,
|
||||
Globe,
|
||||
Brain,
|
||||
Sparkles,
|
||||
|
|
@ -45,6 +46,10 @@ const BUILTIN_ICONS: Record<string, ItemIcon> = {
|
|||
Icon: Brain,
|
||||
colorClass: 'bg-indigo-500/15 text-indigo-600 dark:text-indigo-300',
|
||||
},
|
||||
ask_user_question: {
|
||||
Icon: MessageCircleQuestion,
|
||||
colorClass: 'bg-teal-500/15 text-teal-600 dark:text-teal-300',
|
||||
},
|
||||
};
|
||||
|
||||
const KIND_FALLBACK_ICONS: Record<AgentItem['kind'], ItemIcon> = {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,13 @@ export type TogglePatch =
|
|||
| { type: 'action-remove'; actionId: string };
|
||||
|
||||
function builtinTogglePatch(id: string, selected: boolean): TogglePatch {
|
||||
// Every BuiltinId string equals its AgentCapabilities enum value, so the id
|
||||
// is already the form field name.
|
||||
if (id === 'ask_user_question') {
|
||||
// Native tool presented as a builtin — it has no capability field; the
|
||||
// toggle edits agent.tools exactly like a plugin.
|
||||
return selected ? { type: 'tool-remove', id } : { type: 'tool-add', id };
|
||||
}
|
||||
// Every other BuiltinId string equals its AgentCapabilities enum value, so
|
||||
// the id is already the form field name.
|
||||
const field = id as AgentCapabilities;
|
||||
if (id === 'artifacts') {
|
||||
return { type: 'builtin', field, value: selected ? '' : ArtifactModes.DEFAULT };
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ function isBuiltinSelected(item: AgentItem, form: FormSelection): boolean {
|
|||
return Boolean(form.artifacts);
|
||||
case 'context':
|
||||
return form.context_files.length > 0;
|
||||
case 'ask_user_question':
|
||||
// Native tool presented as a builtin — selection lives in agent.tools.
|
||||
return form.tools.includes('ask_user_question');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,14 @@ export type BuiltinId =
|
|||
| `${AgentCapabilities.file_search}`
|
||||
| `${AgentCapabilities.artifacts}`
|
||||
| `${AgentCapabilities.memory}`
|
||||
| `${AgentCapabilities.context}`;
|
||||
| `${AgentCapabilities.context}`
|
||||
/**
|
||||
* Native tool, not a capability: PRESENTED with the builtins (it ships with
|
||||
* the app and pauses the run, like a first-class feature), but selection and
|
||||
* toggling ride `agent.tools` exactly like a plugin — see the special cases
|
||||
* in `selectors.ts` / `mutations.ts`.
|
||||
*/
|
||||
| 'ask_user_question';
|
||||
|
||||
export type AgentItemStatus = 'needs_setup';
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"com_a11y_summarize_completed": "Context summarized.",
|
||||
"com_a11y_summarize_failed": "Summarization failed, continuing with available context.",
|
||||
"com_a11y_summarize_started": "Summarizing context.",
|
||||
"com_agents_ask_user_info": "Lets the agent pause mid-run to ask you a clarifying question and wait for your answer.",
|
||||
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
|
||||
"com_agents_all": "All Agents",
|
||||
"com_agents_all_category": "All",
|
||||
|
|
@ -856,6 +857,7 @@
|
|||
"com_ui_artifacts_options": "Artifacts Options",
|
||||
"com_ui_artifacts_subtext": "Lets the agent render React, HTML, SVG, Markdown, and Mermaid as interactive artifacts in a side panel instead of plain code blocks.",
|
||||
"com_ui_ascending": "Asc",
|
||||
"com_ui_ask_user": "Ask User",
|
||||
"com_ui_asked_a_question": "Asked a question",
|
||||
"com_ui_assistant": "Assistant",
|
||||
"com_ui_assistant_delete_error": "There was an error deleting the assistant",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue