Skip to content

Commit b2f44fc

Browse files
authored
🧩 feat: Web Search Config Validations & Clipboard Citation Processing (#7530)
* 🔧 chore: Add missing optional `scraperTimeout` to webSearchSchema * chore: Add missing optional `scraperTimeout` to web search authentication result * chore: linting * feat: Integrate attachment handling and citation processing in message components - Added `useAttachments` hook to manage message attachments and search results. - Updated `MessageParts`, `ContentParts`, and `ContentRender` components to utilize the new hook for improved attachment handling. - Enhanced `useCopyToClipboard` to format citations correctly, including support for composite citations and deduplication. - Introduced utility functions for citation processing and cleanup. - Added tests for the new `useCopyToClipboard` functionality to ensure proper citation formatting and handling. * feat: Add configuration for LibreChat Code Interpreter API and Web Search variables * fix: Update searchResults type to use SearchResultData for better type safety * feat: Add web search configuration validation and logging - Introduced `checkWebSearchConfig` function to validate web search configuration values, ensuring they are environment variable references. - Added logging for proper configuration and warnings for incorrect values. - Created unit tests for `checkWebSearchConfig` to cover various scenarios, including valid and invalid configurations. * docs: Update README to include Web Search feature details - Added a section for the Web Search feature, highlighting its capabilities to search the internet and enhance AI context. - Included links for further information on the Web Search functionality. * ci: Add mock for checkWebSearchConfig in AppService tests * chore: linting * feat: Enhance Shared Messages with Web Search UI by adding searchResults prop to SearchContent and MinimalHoverButtons components * chore: linting * refactor: remove Meilisearch index sync from importConversations function * feat: update safeSearch implementation to use SafeSearchTypes enum * refactor: remove commented-out code in loadTools function * fix: ensure responseMessageId handles latestMessage ID correctly * feat: enhance Vite configuration for improved chunking and caching - Added additional globIgnores for map files in Workbox configuration. - Implemented high-impact chunking for various large libraries to optimize performance. - Increased chunkSizeWarningLimit from 1200 to 1500 for better handling of larger chunks. * refactor: move health check hook to Root, fix bad setState for Temporary state - Enhanced the `useHealthCheck` hook to initiate health checks only when the user is authenticated. - Added logic for managing health check intervals and handling window focus events. - Introduced a new test suite for `useHealthCheck` to cover various scenarios including authentication state changes and error handling. - Removed the health check invocation from `ChatRoute` and added it to `Root` for global health monitoring. * fix: update font alias in Vite configuration for correct path resolution
1 parent cede5d1 commit b2f44fc

34 files changed

+1707
-138
lines changed

.env.example

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,3 +590,33 @@ HELP_AND_FAQ_URL=https://librechat.ai
590590
# OpenWeather #
591591
#=====================================================#
592592
OPENWEATHER_API_KEY=
593+
594+
#====================================#
595+
# LibreChat Code Interpreter API #
596+
#====================================#
597+
598+
# https://code.librechat.ai
599+
# LIBRECHAT_CODE_API_KEY=your-key
600+
601+
#======================#
602+
# Web Search #
603+
#======================#
604+
605+
# Note: All of the following variable names can be customized.
606+
# Omit values to allow user to provide them.
607+
608+
# For more information on configuration values, see:
609+
# https://librechat.ai/docs/features/web_search
610+
611+
# Search Provider (Required)
612+
# SERPER_API_KEY=your_serper_api_key
613+
614+
# Scraper (Required)
615+
# FIRECRAWL_API_KEY=your_firecrawl_api_key
616+
# Optional: Custom Firecrawl API URL
617+
# FIRECRAWL_API_URL=your_firecrawl_api_url
618+
619+
# Reranker (Required)
620+
# JINA_API_KEY=your_jina_api_key
621+
# or
622+
# COHERE_API_KEY=your_cohere_api_key

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
- [Model Context Protocol (MCP) Support](https://modelcontextprotocol.io/clients#librechat) for Tools
7272
- Use LibreChat Agents and OpenAI Assistants with Files, Code Interpreter, Tools, and API Actions
7373

74+
- 🔍 **Web Search**:
75+
- Search the internet and retrieve relevant information to enhance your AI context
76+
- Combines search providers, content scrapers, and result rerankers for optimal results
77+
- **[Learn More →](https://www.librechat.ai/docs/features/web_search)**
78+
7479
- 🪄 **Generative UI with Code Artifacts**:
7580
- [Code Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3) allow creation of React, HTML, and Mermaid diagrams directly in chat
7681

api/app/clients/tools/util/handleTools.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,6 @@ const loadTools = async ({
277277
});
278278
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
279279
requestedTools[tool] = async () => {
280-
// const { files, toolContext } = await primeSearchFiles(options);
281-
// if (toolContext) {
282-
// toolContextMap[tool] = toolContext;
283-
// }
284280
toolContextMap[tool] = `# \`${tool}\`:
285281
Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
286282
1. **Execute immediately without preface** when using \`${tool}\`.

api/server/routes/convos.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ router.post('/gen_title', async (req, res) => {
7474
res.status(200).json({ title });
7575
} else {
7676
res.status(404).json({
77-
message: 'Title not found or method not implemented for the conversation\'s endpoint',
77+
message: "Title not found or method not implemented for the conversation's endpoint",
7878
});
7979
}
8080
});

api/server/services/AppService.interface.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jest.mock('./start/checks', () => ({
2525
checkHealth: jest.fn(),
2626
checkConfig: jest.fn(),
2727
checkAzureVariables: jest.fn(),
28+
checkWebSearchConfig: jest.fn(),
2829
}));
2930

3031
const AppService = require('./AppService');

api/server/services/AppService.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ const {
66
getConfigDefaults,
77
loadWebSearchConfig,
88
} = require('librechat-data-provider');
9-
const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks');
9+
const {
10+
checkHealth,
11+
checkConfig,
12+
checkVariables,
13+
checkAzureVariables,
14+
checkWebSearchConfig,
15+
} = require('./start/checks');
1016
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
1117
const { initializeAzureBlobService } = require('./Files/Azure/initialize');
1218
const { initializeFirebase } = require('./Files/Firebase/initialize');
@@ -37,6 +43,7 @@ const AppService = async (app) => {
3743

3844
const ocr = loadOCRConfig(config.ocr);
3945
const webSearch = loadWebSearchConfig(config.webSearch);
46+
checkWebSearchConfig(webSearch);
4047
const filteredTools = config.filteredTools;
4148
const includedTools = config.includedTools;
4249
const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy;

api/server/services/AppService.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ describe('AppService', () => {
146146
firecrawlApiKey: '${FIRECRAWL_API_KEY}',
147147
firecrawlApiUrl: '${FIRECRAWL_API_URL}',
148148
jinaApiKey: '${JINA_API_KEY}',
149-
safeSearch: true,
149+
safeSearch: 1,
150150
serperApiKey: '${SERPER_API_KEY}',
151151
},
152152
});

api/server/services/start/checks.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const {
22
Constants,
3+
webSearchKeys,
34
deprecatedAzureVariables,
45
conflictingAzureVariables,
6+
extractVariableName,
57
} = require('librechat-data-provider');
68
const { isEnabled, checkEmailConfig } = require('~/server/utils');
79
const { logger } = require('~/config');
@@ -141,4 +143,56 @@ function checkPasswordReset() {
141143
}
142144
}
143145

144-
module.exports = { checkVariables, checkHealth, checkConfig, checkAzureVariables };
146+
/**
147+
* Checks web search configuration values to ensure they are environment variable references.
148+
* Warns if actual API keys or URLs are used instead of environment variable references.
149+
* Logs debug information for properly configured environment variable references.
150+
* @param {Object} webSearchConfig - The loaded web search configuration object.
151+
*/
152+
function checkWebSearchConfig(webSearchConfig) {
153+
if (!webSearchConfig) {
154+
return;
155+
}
156+
157+
webSearchKeys.forEach((key) => {
158+
const value = webSearchConfig[key];
159+
160+
if (typeof value === 'string') {
161+
const varName = extractVariableName(value);
162+
163+
if (varName) {
164+
// This is a proper environment variable reference
165+
const actualValue = process.env[varName];
166+
if (actualValue) {
167+
logger.debug(`Web search ${key}: Using environment variable ${varName} with value set`);
168+
} else {
169+
logger.debug(
170+
`Web search ${key}: Using environment variable ${varName} (not set in environment, user provided value)`,
171+
);
172+
}
173+
} else {
174+
// This is not an environment variable reference - warn user
175+
logger.warn(
176+
`❗ Web search configuration error: ${key} contains an actual value instead of an environment variable reference.
177+
178+
Current value: "${value.substring(0, 10)}..."
179+
180+
This is incorrect! You should use environment variable references in your librechat.yaml file, such as:
181+
${key}: "\${YOUR_ENV_VAR_NAME}"
182+
183+
Then set the actual API key in your .env file or environment variables.
184+
185+
More info: https://www.librechat.ai/docs/configuration/librechat_yaml/web_search`,
186+
);
187+
}
188+
}
189+
});
190+
}
191+
192+
module.exports = {
193+
checkHealth,
194+
checkConfig,
195+
checkVariables,
196+
checkAzureVariables,
197+
checkWebSearchConfig,
198+
};
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Mock librechat-data-provider
2+
jest.mock('librechat-data-provider', () => ({
3+
...jest.requireActual('librechat-data-provider'),
4+
extractVariableName: jest.fn(),
5+
}));
6+
7+
// Mock the config logger
8+
jest.mock('~/config', () => ({
9+
logger: {
10+
debug: jest.fn(),
11+
warn: jest.fn(),
12+
},
13+
}));
14+
15+
const { checkWebSearchConfig } = require('./checks');
16+
const { logger } = require('~/config');
17+
const { extractVariableName } = require('librechat-data-provider');
18+
19+
describe('checkWebSearchConfig', () => {
20+
let originalEnv;
21+
22+
beforeEach(() => {
23+
// Clear all mocks
24+
jest.clearAllMocks();
25+
26+
// Store original environment
27+
originalEnv = process.env;
28+
29+
// Reset process.env
30+
process.env = { ...originalEnv };
31+
});
32+
33+
afterEach(() => {
34+
// Restore original environment
35+
process.env = originalEnv;
36+
});
37+
38+
describe('when webSearchConfig is undefined or null', () => {
39+
it('should return early without logging when config is undefined', () => {
40+
checkWebSearchConfig(undefined);
41+
42+
expect(logger.debug).not.toHaveBeenCalled();
43+
expect(logger.warn).not.toHaveBeenCalled();
44+
});
45+
46+
it('should return early without logging when config is null', () => {
47+
checkWebSearchConfig(null);
48+
49+
expect(logger.debug).not.toHaveBeenCalled();
50+
expect(logger.warn).not.toHaveBeenCalled();
51+
});
52+
});
53+
54+
describe('when config values are proper environment variable references', () => {
55+
it('should log debug message for each valid environment variable with value set', () => {
56+
const config = {
57+
serperApiKey: '${SERPER_API_KEY}',
58+
jinaApiKey: '${JINA_API_KEY}',
59+
};
60+
61+
extractVariableName.mockReturnValueOnce('SERPER_API_KEY').mockReturnValueOnce('JINA_API_KEY');
62+
63+
process.env.SERPER_API_KEY = 'test-serper-key';
64+
process.env.JINA_API_KEY = 'test-jina-key';
65+
66+
checkWebSearchConfig(config);
67+
68+
expect(extractVariableName).toHaveBeenCalledWith('${SERPER_API_KEY}');
69+
expect(extractVariableName).toHaveBeenCalledWith('${JINA_API_KEY}');
70+
expect(logger.debug).toHaveBeenCalledWith(
71+
'Web search serperApiKey: Using environment variable SERPER_API_KEY with value set',
72+
);
73+
expect(logger.debug).toHaveBeenCalledWith(
74+
'Web search jinaApiKey: Using environment variable JINA_API_KEY with value set',
75+
);
76+
expect(logger.warn).not.toHaveBeenCalled();
77+
});
78+
79+
it('should log debug message for environment variables not set in environment', () => {
80+
const config = {
81+
cohereApiKey: '${COHERE_API_KEY}',
82+
};
83+
84+
extractVariableName.mockReturnValue('COHERE_API_KEY');
85+
86+
delete process.env.COHERE_API_KEY;
87+
88+
checkWebSearchConfig(config);
89+
90+
expect(logger.debug).toHaveBeenCalledWith(
91+
'Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)',
92+
);
93+
expect(logger.warn).not.toHaveBeenCalled();
94+
});
95+
});
96+
97+
describe('when config values are actual values instead of environment variable references', () => {
98+
it('should warn when serperApiKey contains actual API key', () => {
99+
const config = {
100+
serperApiKey: 'sk-1234567890abcdef',
101+
};
102+
103+
extractVariableName.mockReturnValue(null);
104+
105+
checkWebSearchConfig(config);
106+
107+
expect(logger.warn).toHaveBeenCalledWith(
108+
expect.stringContaining(
109+
'❗ Web search configuration error: serperApiKey contains an actual value',
110+
),
111+
);
112+
expect(logger.warn).toHaveBeenCalledWith(
113+
expect.stringContaining('Current value: "sk-1234567..."'),
114+
);
115+
expect(logger.debug).not.toHaveBeenCalled();
116+
});
117+
118+
it('should warn when firecrawlApiUrl contains actual URL', () => {
119+
const config = {
120+
firecrawlApiUrl: 'https://api.firecrawl.dev',
121+
};
122+
123+
extractVariableName.mockReturnValue(null);
124+
125+
checkWebSearchConfig(config);
126+
127+
expect(logger.warn).toHaveBeenCalledWith(
128+
expect.stringContaining(
129+
'❗ Web search configuration error: firecrawlApiUrl contains an actual value',
130+
),
131+
);
132+
expect(logger.warn).toHaveBeenCalledWith(
133+
expect.stringContaining('Current value: "https://ap..."'),
134+
);
135+
});
136+
137+
it('should include documentation link in warning message', () => {
138+
const config = {
139+
firecrawlApiKey: 'fc-actual-key',
140+
};
141+
142+
extractVariableName.mockReturnValue(null);
143+
144+
checkWebSearchConfig(config);
145+
146+
expect(logger.warn).toHaveBeenCalledWith(
147+
expect.stringContaining(
148+
'More info: https://www.librechat.ai/docs/configuration/librechat_yaml/web_search',
149+
),
150+
);
151+
});
152+
});
153+
154+
describe('when config contains mixed value types', () => {
155+
it('should only process string values and ignore non-string values', () => {
156+
const config = {
157+
serperApiKey: '${SERPER_API_KEY}',
158+
safeSearch: 1,
159+
scraperTimeout: 7500,
160+
jinaApiKey: 'actual-key',
161+
};
162+
163+
extractVariableName.mockReturnValueOnce('SERPER_API_KEY').mockReturnValueOnce(null);
164+
165+
process.env.SERPER_API_KEY = 'test-key';
166+
167+
checkWebSearchConfig(config);
168+
169+
expect(extractVariableName).toHaveBeenCalledTimes(2);
170+
expect(logger.debug).toHaveBeenCalledTimes(1);
171+
expect(logger.warn).toHaveBeenCalledTimes(1);
172+
});
173+
});
174+
175+
describe('edge cases', () => {
176+
it('should handle config with no web search keys', () => {
177+
const config = {
178+
someOtherKey: 'value',
179+
anotherKey: '${SOME_VAR}',
180+
};
181+
182+
checkWebSearchConfig(config);
183+
184+
expect(extractVariableName).not.toHaveBeenCalled();
185+
expect(logger.debug).not.toHaveBeenCalled();
186+
expect(logger.warn).not.toHaveBeenCalled();
187+
});
188+
189+
it('should truncate long values in warning messages', () => {
190+
const config = {
191+
serperApiKey: 'this-is-a-very-long-api-key-that-should-be-truncated-in-the-warning-message',
192+
};
193+
194+
extractVariableName.mockReturnValue(null);
195+
196+
checkWebSearchConfig(config);
197+
198+
expect(logger.warn).toHaveBeenCalledWith(
199+
expect.stringContaining('Current value: "this-is-a-..."'),
200+
);
201+
});
202+
});
203+
});

api/server/utils/import/importConversations.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const fs = require('fs').promises;
22
const { getImporter } = require('./importers');
3-
const { indexSync } = require('~/lib/db');
43
const { logger } = require('~/config');
54

65
/**
@@ -15,8 +14,6 @@ const importConversations = async (job) => {
1514
const jsonData = JSON.parse(fileData);
1615
const importer = getImporter(jsonData);
1716
await importer(jsonData, requestUserId);
18-
// Sync Meilisearch index
19-
await indexSync();
2017
logger.debug(`user: ${requestUserId} | Finished importing conversations`);
2118
} catch (error) {
2219
logger.error(`user: ${requestUserId} | Failed to import conversation: `, error);

0 commit comments

Comments
 (0)