🔗 feat: Convo Settings via URL Query Params & Mention Models (#5184)

* feat: first pass, convo settings from query params

* feat: Enhance query parameter handling for assistants and agents endpoints

* feat: Update message formatting and localization for AI responses, bring awareness to mention command

* docs: Update translations README with detailed instructions for translation script usage and contribution guidelines

* chore: update localizations

* fix: missing agent_id assignment

* feat: add models as initial mention option

* feat: update query parameters schema to confine possible query params

* fix: normalize custom endpoints

* refactor: optimize custom endpoint type check
This commit is contained in:
Danny Avila 2025-01-04 20:36:12 -05:00 committed by GitHub
parent 766657da83
commit 7987e04a2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 370 additions and 48 deletions

View file

@ -7,6 +7,7 @@ import {
import {
alternateName,
EModelEndpoint,
isAgentsEndpoint,
getConfigDefaults,
isAssistantsEndpoint,
} from 'librechat-data-provider';
@ -121,6 +122,26 @@ export default function useMentions({
if (!includeAssistants) {
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
}
const modelOptions = validEndpoints.flatMap((endpoint) => {
if (isAssistantsEndpoint(endpoint) || isAgentsEndpoint(endpoint)) {
return [];
}
const models = (modelsConfig?.[endpoint] ?? []).map((model) => ({
value: endpoint,
label: model,
type: 'model' as const,
icon: EndpointIcon({
conversation: { endpoint, model },
endpointsConfig,
context: 'menu-item',
size: 20,
}),
}));
return models;
});
const mentions = [
...(modelSpecs.length > 0 ? modelSpecs : []).map((modelSpec) => ({
value: modelSpec.name,
@ -169,6 +190,7 @@ export default function useMentions({
}),
type: 'preset' as const,
})) ?? []),
...modelOptions,
];
return mentions;
@ -178,6 +200,7 @@ export default function useMentions({
modelSpecs,
agentsList,
assistantMap,
modelsConfig,
endpointsConfig,
assistantListMap,
includeAssistants,

View file

@ -1,64 +1,213 @@
import { useEffect, useRef } from 'react';
import { useEffect, useCallback, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useSearchParams } from 'react-router-dom';
import { useChatFormContext } from '~/Providers';
import { useQueryClient } from '@tanstack/react-query';
import {
QueryKeys,
EModelEndpoint,
isAgentsEndpoint,
tQueryParamsSchema,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
import type { ZodAny } from 'zod';
import { getConvoSwitchLogic, removeUnavailableTools } from '~/utils';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useChatContext, useChatFormContext } from '~/Providers';
import store from '~/store';
const parseQueryValue = (value: string) => {
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
if (!isNaN(Number(value))) {
return Number(value);
}
return value;
};
const processValidSettings = (queryParams: Record<string, string>) => {
const validSettings = {} as TPreset;
Object.entries(queryParams).forEach(([key, value]) => {
try {
const schema = tQueryParamsSchema.shape[key] as ZodAny | undefined;
if (schema) {
const parsedValue = parseQueryValue(value);
const validValue = schema.parse(parsedValue);
validSettings[key] = validValue;
}
} catch (error) {
console.warn(`Invalid value for setting ${key}:`, error);
}
});
if (
validSettings.assistant_id != null &&
validSettings.assistant_id &&
!isAssistantsEndpoint(validSettings.endpoint)
) {
validSettings.endpoint = EModelEndpoint.assistants;
}
if (
validSettings.agent_id != null &&
validSettings.agent_id &&
!isAgentsEndpoint(validSettings.endpoint)
) {
validSettings.endpoint = EModelEndpoint.agents;
}
return validSettings;
};
export default function useQueryParams({
textAreaRef,
}: {
textAreaRef: React.RefObject<HTMLTextAreaElement>;
}) {
const methods = useChatFormContext();
const [searchParams] = useSearchParams();
const maxAttempts = 50;
const attemptsRef = useRef(0);
const processedRef = useRef(false);
const maxAttempts = 50; // 5 seconds maximum (50 * 100ms)
const methods = useChatFormContext();
const [searchParams] = useSearchParams();
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
useEffect(() => {
const decodedPrompt = searchParams.get('prompt') ?? '';
if (!decodedPrompt) {
const queryClient = useQueryClient();
const { conversation, newConversation } = useChatContext();
const newQueryConvo = useCallback(
(_newPreset?: TPreset) => {
if (!_newPreset) {
return;
}
const newPreset = removeUnavailableTools(_newPreset, availableTools);
let newEndpoint = newPreset.endpoint ?? '';
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
if (newEndpoint && endpointsConfig && !endpointsConfig[newEndpoint]) {
const normalizedNewEndpoint = newEndpoint.toLowerCase();
for (const [key, value] of Object.entries(endpointsConfig)) {
if (
value &&
value.type === EModelEndpoint.custom &&
key.toLowerCase() === normalizedNewEndpoint
) {
newEndpoint = key;
newPreset.endpoint = key;
newPreset.endpointType = EModelEndpoint.custom;
break;
}
}
}
const {
template,
shouldSwitch,
isNewModular,
newEndpointType,
isCurrentModular,
isExistingConversation,
} = getConvoSwitchLogic({
newEndpoint,
modularChat,
conversation,
endpointsConfig,
});
const isModular = isCurrentModular && isNewModular && shouldSwitch;
if (isExistingConversation && isModular) {
template.endpointType = newEndpointType as EModelEndpoint | undefined;
const currentConvo = getDefaultConversation({
/* target endpointType is necessary to avoid endpoint mixing */
conversation: { ...(conversation ?? {}), endpointType: template.endpointType },
preset: template,
});
/* We don't reset the latest message, only when changing settings mid-converstion */
newConversation({
template: currentConvo,
preset: newPreset,
keepLatestMessage: true,
keepAddedConvos: true,
});
return;
}
newConversation({ preset: newPreset, keepAddedConvos: true });
},
[
queryClient,
modularChat,
conversation,
availableTools,
newConversation,
getDefaultConversation,
],
);
useEffect(() => {
const processQueryParams = () => {
const queryParams: Record<string, string> = {};
searchParams.forEach((value, key) => {
queryParams[key] = value;
});
const decodedPrompt = queryParams.prompt || '';
delete queryParams.prompt;
const validSettings = processValidSettings(queryParams);
return { decodedPrompt, validSettings };
};
const intervalId = setInterval(() => {
// If already processed or max attempts reached, clear interval and stop
if (processedRef.current || attemptsRef.current >= maxAttempts) {
clearInterval(intervalId);
if (attemptsRef.current >= maxAttempts) {
console.warn('Max attempts reached, failed to process prompt');
console.warn('Max attempts reached, failed to process parameters');
}
return;
}
attemptsRef.current += 1;
if (textAreaRef.current) {
if (!textAreaRef.current) {
return;
}
const { decodedPrompt, validSettings } = processQueryParams();
const currentText = methods.getValues('text');
// Only update if the textarea is empty
if (!currentText) {
/** Clean up URL parameters after successful processing */
const success = () => {
const newUrl = window.location.pathname;
window.history.replaceState({}, '', newUrl);
processedRef.current = true;
console.log('Parameters processed successfully');
clearInterval(intervalId);
};
if (!currentText && decodedPrompt) {
methods.setValue('text', decodedPrompt, { shouldValidate: true });
textAreaRef.current.focus();
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length);
// Remove the 'prompt' parameter from the URL
searchParams.delete('prompt');
const newUrl = `${window.location.pathname}${
searchParams.toString() ? `?${searchParams.toString()}` : ''
}`;
window.history.replaceState({}, '', newUrl);
processedRef.current = true;
console.log('Prompt processed successfully');
}
clearInterval(intervalId);
if (Object.keys(validSettings).length > 0) {
newQueryConvo(validSettings);
}
}, 100); // Check every 100ms
// Clean up the interval on unmount
success();
}, 100);
return () => {
clearInterval(intervalId);
console.log('Cleanup: interval cleared');
console.log('Cleanup: `useQueryParams` interval cleared');
};
}, [searchParams, methods, textAreaRef]);
}, [searchParams, methods, textAreaRef, newQueryConvo, newConversation]);
}

View file

@ -144,6 +144,10 @@ export default function useSelectMention({
if (assistant_id) {
template.assistant_id = assistant_id;
}
const agent_id = kwargs.agent_id ?? '';
if (agent_id) {
template.agent_id = agent_id;
}
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
template.endpointType = newEndpointType;

View file

@ -128,7 +128,10 @@ export default function useTextarea({
? getEntityName({ name: entityName, isAgent, localize })
: getSender(conversation as TEndpointOption);
return `${localize('com_endpoint_message')} ${sender ? sender : 'AI'}`;
return `${localize(
'com_endpoint_message_new',
sender ? sender : localize('com_endpoint_ai'),
)}`;
};
const placeholder = getPlaceholderText();

View file

@ -871,7 +871,8 @@ export default {
com_error_invalid_action_error: 'تم رفض الطلب: نطاق الإجراء المحدد غير مسموح به',
com_agents_code_interpreter_title: 'واجهة برمجة مُفسِّر الشفرة',
com_agents_by_librechat: 'بواسطة LibreChat',
com_agents_code_interpreter: 'عند التمكين، يسمح للوكيل الخاص بك باستخدام واجهة برمجة التطبيقات لمفسر الشفرة LibreChat لتشغيل الشفرة المُنشأة، بما في ذلك معالجة الملفات، بشكل آمن. يتطلب مفتاح API صالح.',
com_agents_code_interpreter:
'عند التمكين، يسمح للوكيل الخاص بك باستخدام واجهة برمجة التطبيقات لمفسر الشفرة LibreChat لتشغيل الشفرة المُنشأة، بما في ذلك معالجة الملفات، بشكل آمن. يتطلب مفتاح API صالح.',
com_ui_export_convo_modal: 'نافذة تصدير المحادثة',
com_ui_endpoints_available: 'نقاط النهاية المتاحة',
com_ui_endpoint_menu: 'قائمة نقطة نهاية LLM',
@ -885,7 +886,8 @@ export default {
com_ui_upload_code_files: 'تحميل لمفسر الكود',
com_ui_zoom: 'تكبير',
com_ui_role_select: 'الدور',
com_ui_admin_access_warning: 'قد يؤدي تعطيل وصول المسؤول إلى هذه الميزة إلى مشاكل غير متوقعة في واجهة المستخدم تتطلب تحديث الصفحة. في حالة الحفظ، الطريقة الوحيدة للتراجع هي عبر إعداد الواجهة في ملف librechat.yaml والذي يؤثر على جميع الأدوار.',
com_ui_admin_access_warning:
'قد يؤدي تعطيل وصول المسؤول إلى هذه الميزة إلى مشاكل غير متوقعة في واجهة المستخدم تتطلب تحديث الصفحة. في حالة الحفظ، الطريقة الوحيدة للتراجع هي عبر إعداد الواجهة في ملف librechat.yaml والذي يؤثر على جميع الأدوار.',
com_ui_run_code_error: 'حدث خطأ أثناء تشغيل الكود',
com_ui_duplication_success: 'تم نسخ المحادثة بنجاح',
com_ui_duplication_processing: 'جارِ نسخ المحادثة...',
@ -905,4 +907,10 @@ export default {
com_ui_enter_openapi_schema: 'أدخل مخطط OpenAPI هنا',
com_ui_delete_shared_link: 'حذف الرابط المشترك؟',
com_nav_welcome_agent: 'الرجاء اختيار مساعد',
com_ui_bookmarks_edit: 'تعديل الإشارة المرجعية',
com_ui_page: 'صفحة',
com_ui_bookmarks_add: 'إضافة إشارات مرجعية',
com_endpoint_ai: 'الذكاء الاصطناعي',
com_endpoint_message_new: 'الرسالة {0} أو اكتب "@" للتبديل إلى الذكاء الاصطناعي',
com_nav_maximize_chat_space: 'تكبير مساحة الدردشة',
};

View file

@ -939,4 +939,10 @@ export default {
com_nav_welcome_agent: 'Bitte wähle einen Agenten',
com_endpoint_agent_placeholder: 'Bitte wähle einen Agenten aus',
com_ui_delete_shared_link: 'Geteilten Link löschen?',
com_ui_bookmarks_edit: 'Lesezeichen bearbeiten',
com_endpoint_ai: 'KI',
com_ui_page: 'Seite',
com_ui_bookmarks_add: 'Lesezeichen hinzufügen',
com_endpoint_message_new: 'Nachricht {0} oder "@" eingeben, um KI zu wechseln',
com_nav_maximize_chat_space: 'Chat-Bereich maximieren',
};

View file

@ -525,6 +525,8 @@ export default {
'WARNING: Misuse of this feature can get you BANNED from using Bing! Click on \'System Message\' for full instructions and the default message if omitted, which is the \'Sydney\' preset that is considered safe.',
com_endpoint_system_message: 'System Message',
com_endpoint_message: 'Message',
com_endpoint_ai: 'AI',
com_endpoint_message_new: 'Message {0} or type "@" to switch AI',
com_endpoint_message_not_appendable: 'Edit your message or Regenerate.',
com_endpoint_default_blank: 'default: blank',
com_endpoint_default_false: 'default: false',

View file

@ -1195,4 +1195,10 @@ export default {
com_endpoint_agent_placeholder: 'Por favor seleccione un Agente',
com_nav_welcome_agent: 'Seleccione un agente',
com_ui_delete_shared_link: '¿Eliminar enlace compartido?',
com_ui_bookmarks_add: 'Agregar Marcadores',
com_ui_page: 'Página',
com_ui_bookmarks_edit: 'Editar Marcador',
com_endpoint_message_new: 'Mensaje {0} o escriba "@" para cambiar de IA',
com_nav_maximize_chat_space: 'Maximizar espacio del chat',
com_endpoint_ai: 'IA',
};

View file

@ -957,4 +957,10 @@ export default {
com_endpoint_agent_placeholder: 'Veuillez sélectionner un Agent',
com_nav_welcome_agent: 'Veuillez sélectionner un agent',
com_ui_delete_shared_link: 'Supprimer le lien partagé ?',
com_ui_bookmarks_add: 'Ajouter des signets',
com_ui_bookmarks_edit: 'Modifier le signet',
com_endpoint_ai: 'IA',
com_nav_maximize_chat_space: 'Maximiser l\'espace de discussion',
com_endpoint_message_new: 'Message {0} ou tapez "@" pour changer d\'IA',
com_ui_page: 'Page',
};

View file

@ -951,4 +951,10 @@ export default {
com_nav_welcome_agent: 'Seleziona un Assistente',
com_endpoint_agent_placeholder: 'Seleziona un Agente',
com_ui_delete_shared_link: 'Eliminare il link condiviso?',
com_ui_bookmarks_add: 'Aggiungi Segnalibri',
com_ui_bookmarks_edit: 'Modifica Segnalibro',
com_endpoint_message_new: 'Messaggio {0} oppure digita "@" per cambiare IA',
com_endpoint_ai: 'IA',
com_nav_maximize_chat_space: 'Massimizza spazio chat',
com_ui_page: 'Pagina',
};

View file

@ -905,4 +905,10 @@ export default {
com_ui_duplicate_agent_confirm: 'このエージェントを複製しますか?',
com_nav_welcome_agent: 'エージェントを選択してください',
com_ui_delete_shared_link: '共有リンクを削除しますか?',
com_ui_bookmarks_add: 'ブックマークを追加',
com_ui_page: 'ページ',
com_ui_bookmarks_edit: 'ブックマークを編集',
com_endpoint_ai: 'AI',
com_endpoint_message_new: 'メッセージ {0} または「@」を入力してAIを切り替え',
com_nav_maximize_chat_space: 'チャット画面を最大化',
};

View file

@ -1143,4 +1143,10 @@ export default {
com_ui_duplicate_agent_confirm: '이 에이전트를 복제하시겠습니까?',
com_nav_welcome_agent: '에이전트를 선택해 주세요',
com_ui_delete_shared_link: '공유 링크를 삭제하시겠습니까?',
com_ui_bookmarks_add: '북마크 추가',
com_ui_bookmarks_edit: '북마크 수정',
com_ui_page: '페이지',
com_endpoint_ai: '인공지능',
com_nav_maximize_chat_space: '채팅창 최대화',
com_endpoint_message_new: '메시지 {0} 또는 "@"를 입력하여 AI 전환',
};

View file

@ -1169,4 +1169,10 @@ export default {
com_nav_welcome_agent: 'Выберите агента',
com_ui_duplicate_agent_confirm: 'Вы действительно хотите создать копию этого агента?',
com_ui_delete_shared_link: 'Удалить общую ссылку?',
com_ui_bookmarks_edit: 'Редактировать закладку',
com_ui_page: 'Страница',
com_endpoint_ai: 'ИИ',
com_endpoint_message_new: 'Сообщение {0} или введите "@" для смены ИИ',
com_nav_maximize_chat_space: 'Развернуть чат',
com_ui_bookmarks_add: 'Добавить закладку',
};

View file

@ -897,4 +897,10 @@ export default {
com_endpoint_agent_placeholder: '请选择代理',
com_ui_delete_shared_link: '删除分享链接?',
com_nav_welcome_agent: '请选择 Agent',
com_ui_bookmarks_add: '添加书签',
com_ui_bookmarks_edit: '编辑书签',
com_endpoint_ai: '人工智能',
com_ui_page: '页面',
com_nav_maximize_chat_space: '最大化聊天窗口',
com_endpoint_message_new: '发送消息 {0} 或输入"@"切换AI',
};

View file

@ -874,4 +874,10 @@ export default {
com_ui_duplicate_agent_confirm: '您確定要複製這個助理嗎?',
com_ui_delete_shared_link: '刪除共享連結?',
com_nav_welcome_agent: '請選擇代理',
com_ui_bookmarks_edit: '編輯書籤',
com_ui_bookmarks_add: '新增書籤',
com_ui_page: '頁面',
com_nav_maximize_chat_space: '最大化聊天視窗',
com_endpoint_ai: 'AI',
com_endpoint_message_new: '輸入訊息 {0} 或輸入 "@" 以切換 AI',
};

View file

@ -11,7 +11,20 @@ This script can be expensive, several dollars worth, even with prompt caching. I
### Instructions:
1. Main script: Run `bun config/translations/scan.ts` from the root directory.
2. Observe translations being generated in all supported languages.
- Supported languages are localizations with general translation prompts:
- These prompts are directly found in `client/src/localization/prompts`.
*All commands are run from the root directory.*
**Supported languages are localizations with general translation prompts**
- These prompts are directly found in `client/src/localization/prompts`.
- If your language is missing, you can contribute by adding a new file in `client/src/localization/prompts` with the language code as the file name.
0. Make sure git history is clean with `git status`.
1. Install `hnswlib-node` package temporarily (we don't need to include it in the project):
```bash
npm install --save-dev hnswlib-node
```
2. Run `bun install`.
3. Main script: Run `bun config/translations/scan.ts`.
4. Observe translations being generated in all supported languages and saved in `client/src/localization/languages`.
- e.g.: `client/src/localization/languages/Es_missing_keys.json`
5. Discard all git changes with `git checkout .`.
6. Copy the generated translations to their respective files, e.g.: `client/src/localization/languages/Es.ts`.

2
package-lock.json generated
View file

@ -36292,7 +36292,7 @@
},
"packages/data-provider": {
"name": "librechat-data-provider",
"version": "0.7.68",
"version": "0.7.69",
"license": "ISC",
"dependencies": {
"axios": "^1.7.7",

View file

@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.68",
"version": "0.7.69",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View file

@ -16,16 +16,19 @@ export const authTypeSchema = z.nativeEnum(AuthType);
export enum EModelEndpoint {
azureOpenAI = 'azureOpenAI',
openAI = 'openAI',
bingAI = 'bingAI',
chatGPTBrowser = 'chatGPTBrowser',
google = 'google',
gptPlugins = 'gptPlugins',
anthropic = 'anthropic',
assistants = 'assistants',
azureAssistants = 'azureAssistants',
agents = 'agents',
custom = 'custom',
bedrock = 'bedrock',
/** @deprecated */
bingAI = 'bingAI',
/** @deprecated */
chatGPTBrowser = 'chatGPTBrowser',
/** @deprecated */
gptPlugins = 'gptPlugins',
}
export const paramEndpoints = new Set<EModelEndpoint | string>([
@ -630,6 +633,69 @@ export const tConvoUpdateSchema = tConversationSchema.merge(
}),
);
export const tQueryParamsSchema = tConversationSchema
.pick({
// librechat settings
/** The AI context window, overrides the system-defined window as determined by `model` value */
maxContextTokens: true,
/**
* Whether or not to re-submit files from previous messages on subsequent messages
* */
resendFiles: true,
/**
* AKA Custom Instructions, dynamically added to chat history as a system message;
* for `bedrock` endpoint, this is used as the `system` model param if the provider uses it;
* for `assistants` endpoint, this is used as the `additional_instructions` model param:
* https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-additional_instructions
* ; otherwise, a message with `system` role is added to the chat history
*/
promptPrefix: true,
// Model parameters
/** @endpoints openAI, custom, azureOpenAI, google, anthropic, assistants, azureAssistants, bedrock */
model: true,
/** @endpoints openAI, custom, azureOpenAI, google, anthropic, bedrock */
temperature: true,
/** @endpoints openAI, custom, azureOpenAI */
presence_penalty: true,
/** @endpoints openAI, custom, azureOpenAI */
frequency_penalty: true,
/** @endpoints openAI, custom, azureOpenAI */
stop: true,
/** @endpoints openAI, custom, azureOpenAI */
top_p: true,
/** @endpoints openAI, custom, azureOpenAI */
max_tokens: true,
/** @endpoints google, anthropic, bedrock */
topP: true,
/** @endpoints google, anthropic */
topK: true,
/** @endpoints google, anthropic */
maxOutputTokens: true,
/** @endpoints anthropic */
promptCache: true,
/** @endpoints bedrock */
region: true,
/** @endpoints bedrock */
maxTokens: true,
/** @endpoints agents */
agent_id: true,
/** @endpoints assistants, azureAssistants */
assistant_id: true,
/**
* @endpoints assistants, azureAssistants
*
* Overrides existing assistant instructions, only used for the current run:
* https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-instructions
* */
instructions: true,
})
.merge(
z.object({
/** @endpoints openAI, custom, azureOpenAI, google, anthropic, assistants, azureAssistants, bedrock, agents */
endpoint: extendedModelEndpointSchema.nullable(),
}),
);
export type TPreset = z.infer<typeof tPresetSchema>;
export type TSetOption = (