🎥 feat: YouTube Tool (#5582)

* adding youtube tool

* refactor: use short `url` param instead of `videoUrl`

* refactor: move API key retrieval to a separate credentials module

* refactor: remove unnecessary `isEdited` message property

* refactor: remove unnecessary `isEdited` message property pt. 2

* refactor: YouTube Tool with new `tool()` generator, handle tools already created by new `tool` generator

* fix: only reset request data for multi-convo messages

* refactor: enhance YouTube tool by adding transcript parsing and returning structured JSON responses

* refactor: update transcript parsing to handle raw response and clean up text output

* feat: support toolkits and refactor YouTube tool as a toolkit for better LLM usage

* refactor: remove unused OpenAPI specs and streamline tools transformation in loadAsyncEndpoints

* refactor: implement manifestToolMap for better tool management and streamline authentication handling

* feat: support toolkits for assistants

* refactor: rename loadedTools to toolDefinitions for clarity in PluginController and assistant controllers

* feat: complete support of toolkits for assistants

---------

Co-authored-by: Danilo Pejakovic <danilo.pejakovic@leoninestudios.com>
This commit is contained in:
Danny Avila 2025-01-31 19:11:04 -05:00 committed by GitHub
parent 33f6093775
commit 352565c9a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 456 additions and 102 deletions

View file

@ -1,25 +1,27 @@
import { useState, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useForm, FormProvider, Controller, useWatch } from 'react-hook-form';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import {
Tools,
QueryKeys,
Capabilities,
actionDelimiter,
ImageVisionTool,
defaultAssistantFormValues,
} from 'librechat-data-provider';
import type { FunctionTool, TConfig, TPlugin } from 'librechat-data-provider';
import type { FunctionTool, TConfig } from 'librechat-data-provider';
import type { AssistantForm, AssistantPanelProps } from '~/common';
import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider';
import {
useCreateAssistantMutation,
useUpdateAssistantMutation,
useAvailableAgentToolsQuery,
} from '~/data-provider';
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
import AssistantConversationStarters from './AssistantConversationStarters';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { useSelectAssistant, useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
import CapabilitiesForm from './CapabilitiesForm';
import AppendDateCheckbox from './AppendDateCheckbox';
import CapabilitiesForm from './CapabilitiesForm';
import { SelectDropDown } from '~/components/ui';
import AssistantAvatar from './AssistantAvatar';
import AssistantSelect from './AssistantSelect';
@ -49,11 +51,10 @@ export default function AssistantPanel({
assistantsConfig,
version,
}: AssistantPanelProps & { assistantsConfig?: TConfig | null }) {
const queryClient = useQueryClient();
const modelsQuery = useGetModelsQuery();
const assistantMap = useAssistantsMapContext();
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
const { data: allTools = [] } = useAvailableAgentToolsQuery();
const { onSelect: onSelectAssistant } = useSelectAssistant(endpoint);
const { showToast } = useToastContext();
const localize = useLocalize();
@ -227,6 +228,7 @@ export default function AssistantPanel({
value={field.value}
endpoint={endpoint}
documentsMap={documentsMap}
allTools={allTools}
setCurrentAssistantId={setCurrentAssistantId}
selectedAssistant={current_assistant_id ?? null}
createMutation={create}

View file

@ -1,5 +1,5 @@
import { Plus } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { useMemo, useCallback, useEffect, useRef } from 'react';
import {
Tools,
FileSources,
@ -12,6 +12,7 @@ import {
import type { UseFormReset } from 'react-hook-form';
import type { UseMutationResult } from '@tanstack/react-query';
import type {
TPlugin,
Assistant,
AssistantDocument,
AssistantsEndpoint,
@ -48,6 +49,7 @@ export default function AssistantSelect({
selectedAssistant,
setCurrentAssistantId,
createMutation,
allTools,
}: {
reset: UseFormReset<AssistantForm>;
value: TAssistantOption;
@ -56,6 +58,7 @@ export default function AssistantSelect({
documentsMap: Map<string, AssistantDocument> | null;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
allTools?: TPlugin[];
}) {
const localize = useLocalize();
const fileMap = useFileMapContext();
@ -65,6 +68,11 @@ export default function AssistantSelect({
{} as LastSelectedModels,
);
const toolkits = useMemo(
() => new Set(allTools?.filter((tool) => tool.toolkit === true).map((tool) => tool.pluginKey)),
[allTools],
);
const query = useListAssistantsQuery(endpoint, undefined, {
select: (res) =>
res.data.map((_assistant) => {
@ -153,7 +161,7 @@ export default function AssistantSelect({
const update = {
...assistant,
label: assistant.name ?? '',
value: assistant.id ?? '',
value: assistant.id || '',
};
const actions: Actions = {
@ -164,7 +172,7 @@ export default function AssistantSelect({
(assistant.tools ?? [])
.filter((tool) => tool.type !== 'function' || isImageVisionTool(tool))
.map((tool) => tool.function?.name || tool.type)
.map((tool) => (tool.function?.name ?? '') || tool.type)
.forEach((tool) => {
if (tool === Tools.file_search) {
actions[Capabilities.retrieval] = true;
@ -172,9 +180,22 @@ export default function AssistantSelect({
actions[tool] = true;
});
const seenToolkits = new Set<string>();
const functions = (assistant.tools ?? [])
.filter((tool) => tool.type === 'function' && !isImageVisionTool(tool))
.map((tool) => tool.function?.name ?? '');
.map((tool) => tool.function?.name ?? '')
.filter((fnName) => {
const fnPrefix = fnName.split('_')[0];
const seenToolkit = toolkits.has(fnPrefix);
if (seenToolkit) {
seenToolkits.add(fnPrefix);
}
return !seenToolkit;
});
if (seenToolkits.size > 0) {
functions.push(...Array.from(seenToolkits));
}
const formValues: Partial<AssistantForm & Actions> = {
functions,
@ -210,7 +231,15 @@ export default function AssistantSelect({
reset(formValues);
setCurrentAssistantId(assistant.id);
},
[query.data, reset, setCurrentAssistantId, createMutation, endpoint, lastSelectedModels],
[
query.data,
reset,
setCurrentAssistantId,
createMutation,
endpoint,
lastSelectedModels,
toolkits,
],
);
useEffect(() => {