diff --git a/.env.example b/.env.example index b5613fdfca..9864a41482 100644 --- a/.env.example +++ b/.env.example @@ -331,10 +331,6 @@ FLUX_API_BASE_URL=https://api.us1.bfl.ai GOOGLE_SEARCH_API_KEY= GOOGLE_CSE_ID= -# YOUTUBE -#----------------- -YOUTUBE_API_KEY= - # Stable Diffusion #----------------- SD_WEBUI_URL=http://host.docker.internal:7860 diff --git a/api/app/clients/tools/index.js b/api/app/clients/tools/index.js index 1a7c4ff47f..bb58e81221 100644 --- a/api/app/clients/tools/index.js +++ b/api/app/clients/tools/index.js @@ -5,7 +5,6 @@ const DALLE3 = require('./structured/DALLE3'); const FluxAPI = require('./structured/FluxAPI'); const OpenWeather = require('./structured/OpenWeather'); const StructuredWolfram = require('./structured/Wolfram'); -const createYouTubeTools = require('./structured/YouTube'); const StructuredACS = require('./structured/AzureAISearch'); const StructuredSD = require('./structured/StableDiffusion'); const GoogleSearchAPI = require('./structured/GoogleSearch'); @@ -25,7 +24,6 @@ module.exports = { GoogleSearchAPI, TraversaalSearch, StructuredWolfram, - createYouTubeTools, TavilySearchResults, createOpenAIImageTools, createGeminiImageTool, diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json index fc037caa4b..9262113501 100644 --- a/api/app/clients/tools/manifest.json +++ b/api/app/clients/tools/manifest.json @@ -30,20 +30,6 @@ } ] }, - { - "name": "YouTube", - "pluginKey": "youtube", - "toolkit": true, - "description": "Get YouTube video information, retrieve comments, analyze transcripts and search for videos.", - "icon": "https://www.youtube.com/s/desktop/7449ebf7/img/favicon_144x144.png", - "authConfig": [ - { - "authField": "YOUTUBE_API_KEY", - "label": "YouTube API Key", - "description": "Your YouTube Data API v3 key." - } - ] - }, { "name": "OpenAI Image Tools", "pluginKey": "image_gen_oai", diff --git a/api/app/clients/tools/structured/YouTube.js b/api/app/clients/tools/structured/YouTube.js deleted file mode 100644 index 8d1c7b9ff9..0000000000 --- a/api/app/clients/tools/structured/YouTube.js +++ /dev/null @@ -1,137 +0,0 @@ -const { ytToolkit } = require('@librechat/api'); -const { tool } = require('@langchain/core/tools'); -const { youtube } = require('@googleapis/youtube'); -const { logger } = require('@librechat/data-schemas'); -const { YoutubeTranscript } = require('youtube-transcript'); -const { getApiKey } = require('./credentials'); - -function extractVideoId(url) { - const rawIdRegex = /^[a-zA-Z0-9_-]{11}$/; - if (rawIdRegex.test(url)) { - return url; - } - - const regex = new RegExp( - '(?:youtu\\.be/|youtube(?:\\.com)?/(?:' + - '(?:watch\\?v=)|(?:embed/)|(?:shorts/)|(?:live/)|(?:v/)|(?:/))?)' + - '([a-zA-Z0-9_-]{11})(?:\\S+)?$', - ); - const match = url.match(regex); - return match ? match[1] : null; -} - -function parseTranscript(transcriptResponse) { - if (!Array.isArray(transcriptResponse)) { - return ''; - } - - return transcriptResponse - .map((entry) => entry.text.trim()) - .filter((text) => text) - .join(' ') - .replaceAll(''', "'"); -} - -function createYouTubeTools(fields = {}) { - const envVar = 'YOUTUBE_API_KEY'; - const override = fields.override ?? false; - const apiKey = fields.apiKey ?? fields[envVar] ?? getApiKey(envVar, override); - - const youtubeClient = youtube({ - version: 'v3', - auth: apiKey, - }); - - const searchTool = tool(async ({ query, maxResults = 5 }) => { - const response = await youtubeClient.search.list({ - part: 'snippet', - q: query, - type: 'video', - maxResults: maxResults || 5, - }); - const result = response.data.items.map((item) => ({ - title: item.snippet.title, - description: item.snippet.description, - url: `https://www.youtube.com/watch?v=${item.id.videoId}`, - })); - return JSON.stringify(result, null, 2); - }, ytToolkit.youtube_search); - - const infoTool = tool(async ({ url }) => { - const videoId = extractVideoId(url); - if (!videoId) { - throw new Error('Invalid YouTube URL or video ID'); - } - - const response = await youtubeClient.videos.list({ - part: 'snippet,statistics', - id: videoId, - }); - - if (!response.data.items?.length) { - throw new Error('Video not found'); - } - const video = response.data.items[0]; - - const result = { - title: video.snippet.title, - description: video.snippet.description, - views: video.statistics.viewCount, - likes: video.statistics.likeCount, - comments: video.statistics.commentCount, - }; - return JSON.stringify(result, null, 2); - }, ytToolkit.youtube_info); - - const commentsTool = tool(async ({ url, maxResults = 10 }) => { - const videoId = extractVideoId(url); - if (!videoId) { - throw new Error('Invalid YouTube URL or video ID'); - } - - const response = await youtubeClient.commentThreads.list({ - part: 'snippet', - videoId, - maxResults: maxResults || 10, - }); - - const result = response.data.items.map((item) => ({ - author: item.snippet.topLevelComment.snippet.authorDisplayName, - text: item.snippet.topLevelComment.snippet.textDisplay, - likes: item.snippet.topLevelComment.snippet.likeCount, - })); - return JSON.stringify(result, null, 2); - }, ytToolkit.youtube_comments); - - const transcriptTool = tool(async ({ url }) => { - const videoId = extractVideoId(url); - if (!videoId) { - throw new Error('Invalid YouTube URL or video ID'); - } - - try { - try { - const transcript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'en' }); - return parseTranscript(transcript); - } catch (e) { - logger.error(e); - } - - try { - const transcript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'de' }); - return parseTranscript(transcript); - } catch (e) { - logger.error(e); - } - - const transcript = await YoutubeTranscript.fetchTranscript(videoId); - return parseTranscript(transcript); - } catch (error) { - throw new Error(`Failed to fetch transcript: ${error.message}`); - } - }, ytToolkit.youtube_transcript); - - return [searchTool, infoTool, commentsTool, transcriptTool]; -} - -module.exports = createYouTubeTools; diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index e39bebd36a..da4c687b4d 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -34,7 +34,6 @@ const { StructuredACS, TraversaalSearch, StructuredWolfram, - createYouTubeTools, TavilySearchResults, createGeminiImageTool, createOpenAIImageTools, @@ -185,11 +184,6 @@ const loadTools = async ({ }; const customConstructors = { - youtube: async (_toolContextMap) => { - const authFields = getAuthFields('youtube'); - const authValues = await loadAuthValues({ userId: user, authFields }); - return createYouTubeTools(authValues); - }, image_gen_oai: async (toolContextMap) => { const authFields = getAuthFields('image_gen_oai'); const authValues = await loadAuthValues({ userId: user, authFields }); diff --git a/api/package.json b/api/package.json index c2f0dd9801..0881070652 100644 --- a/api/package.json +++ b/api/package.json @@ -43,7 +43,6 @@ "@azure/search-documents": "^12.0.0", "@azure/storage-blob": "^12.27.0", "@google/genai": "^1.19.0", - "@googleapis/youtube": "^20.0.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", "@librechat/agents": "^3.0.77", @@ -112,7 +111,6 @@ "undici": "^7.10.0", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", - "youtube-transcript": "^1.2.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index 6456b21325..18e55217d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,6 @@ "@azure/search-documents": "^12.0.0", "@azure/storage-blob": "^12.27.0", "@google/genai": "^1.19.0", - "@googleapis/youtube": "^20.0.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", "@librechat/agents": "^3.0.77", @@ -126,7 +125,6 @@ "undici": "^7.10.0", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", - "youtube-transcript": "^1.2.1", "zod": "^3.22.4" }, "devDependencies": { @@ -10739,18 +10737,6 @@ "node": ">=18.0.0" } }, - "node_modules/@googleapis/youtube": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@googleapis/youtube/-/youtube-20.0.0.tgz", - "integrity": "sha512-wdt1J0JoKYhvpoS2XIRHX0g/9ul/B0fQeeJAhuuBIdYINuuLt6/oZYZZCBmkuhtkA3IllXgqgAXOjLtLRAnR2g==", - "license": "Apache-2.0", - "dependencies": { - "googleapis-common": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@grpc/grpc-js": { "version": "1.9.15", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", @@ -27690,22 +27676,6 @@ "node": ">=14" } }, - "node_modules/googleapis-common": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz", - "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", - "dependencies": { - "extend": "^3.0.2", - "gaxios": "^6.0.3", - "google-auth-library": "^9.0.0", - "qs": "^6.7.0", - "url-template": "^2.0.8", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -41538,11 +41508,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" - }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -43095,15 +43060,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/youtube-transcript": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/youtube-transcript/-/youtube-transcript-1.2.1.tgz", - "integrity": "sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", diff --git a/packages/api/src/tools/toolkits/index.ts b/packages/api/src/tools/toolkits/index.ts index def468d18b..ce9e0584c4 100644 --- a/packages/api/src/tools/toolkits/index.ts +++ b/packages/api/src/tools/toolkits/index.ts @@ -1,4 +1,3 @@ export * from './gemini'; export * from './imageContext'; export * from './oai'; -export * from './yt'; diff --git a/packages/api/src/tools/toolkits/yt.ts b/packages/api/src/tools/toolkits/yt.ts deleted file mode 100644 index 7185a260d7..0000000000 --- a/packages/api/src/tools/toolkits/yt.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { z } from 'zod'; -export const ytToolkit = { - youtube_search: { - name: 'youtube_search' as const, - description: `Search for YouTube videos by keyword or phrase. -- Required: query (search terms to find videos) -- Optional: maxResults (number of videos to return, 1-50, default: 5) -- Returns: List of videos with titles, descriptions, and URLs -- Use for: Finding specific videos, exploring content, research -Example: query="cooking pasta tutorials" maxResults=3` as const, - schema: z.object({ - query: z.string().describe('Search query terms'), - maxResults: z.number().int().min(1).max(50).optional().describe('Number of results (1-50)'), - }), - }, - youtube_info: { - name: 'youtube_info' as const, - description: `Get detailed metadata and statistics for a specific YouTube video. -- Required: url (full YouTube URL or video ID) -- Returns: Video title, description, view count, like count, comment count -- Use for: Getting video metrics and basic metadata -- DO NOT USE FOR VIDEO SUMMARIES, USE TRANSCRIPTS FOR COMPREHENSIVE ANALYSIS -- Accepts both full URLs and video IDs -Example: url="https://youtube.com/watch?v=abc123" or url="abc123"` as const, - schema: z.object({ - url: z.string().describe('YouTube video URL or ID'), - }), - } as const, - youtube_comments: { - name: 'youtube_comments', - description: `Retrieve top-level comments from a YouTube video. -- Required: url (full YouTube URL or video ID) -- Optional: maxResults (number of comments, 1-50, default: 10) -- Returns: Comment text, author names, like counts -- Use for: Sentiment analysis, audience feedback, engagement review -Example: url="abc123" maxResults=20`, - schema: z.object({ - url: z.string().describe('YouTube video URL or ID'), - maxResults: z - .number() - .int() - .min(1) - .max(50) - .optional() - .describe('Number of comments to retrieve'), - }), - } as const, - youtube_transcript: { - name: 'youtube_transcript', - description: `Fetch and parse the transcript/captions of a YouTube video. -- Required: url (full YouTube URL or video ID) -- Returns: Full video transcript as plain text -- Use for: Content analysis, summarization, translation reference -- This is the "Go-to" tool for analyzing actual video content -- Attempts to fetch English first, then German, then any available language -Example: url="https://youtube.com/watch?v=abc123"`, - schema: z.object({ - url: z.string().describe('YouTube video URL or ID'), - }), - } as const, -} as const;