Skip to content

Commit a0291ed

Browse files
🚧 chore: merge latest dev build to main repo (danny-avila#3844)
* agents - phase 1 (danny-avila#30) * chore: copy assistant files * feat: frontend and data-provider * feat: backend get endpoint test * fix(MessageEndpointIcon): switched to AgentName and AgentAvatar * fix: small fixes * fix: agent endpoint config * fix: show Agent Builder * chore: install agentus * chore: initial scaffolding for agents * fix: updated Assistant logic to Agent Logic for some Agent components * WIP first pass, demo of agent package * WIP: initial backend infra for agents * fix: agent list error * wip: agents routing * chore: Refactor useSSE hook to handle different data events * wip: correctly emit events * chore: Update @librechat/agentus npm dependency to version 1.0.9 * remove comment * first pass: streaming agent text * chore: Remove @librechat/agentus root-level workspace npm dependency * feat: Agent Schema and Model * fix: content handling fixes * fix: content message save * WIP: new content data * fix: run step issue with tool calls * chore: Update @librechat/agentus npm dependency to version 1.1.5 * feat: update controller and agent routes * wip: initial backend tool and tool error handling support * wip: tool chunks * chore: Update @librechat/agentus npm dependency to version 1.1.7 * chore: update tool_call typing, add test conditions and logs * fix: create agent * fix: create agent * first pass: render completed content parts * fix: remove logging, fix step handler typing * chore: Update @librechat/agentus npm dependency to version 1.1.9 * refactor: cleanup maps on unmount * chore: Update BaseClient.js to safely count tokens for string, number, and boolean values * fix: support subsequent messages with tool_calls * chore: export order * fix: select agent * fix: tool call types and handling * chore: switch to anthropic for testing * fix: AgentSelect * refactor: experimental: OpenAIClient to use array for intermediateReply * fix(useSSE): revert old condition for streaming legacy client tokens * fix: lint * revert `agent_id` to `id` * chore: update localization keys for agent-related components * feat: zod schema handling for actions * refactor(actions): if no params, no zodSchema * chore: Update @librechat/agentus npm dependency to version 1.2.1 * feat: first pass, actions * refactor: empty schema for actions without params * feat: Update createRun function to accept additional options * fix: message payload formatting; feat: add more client options * fix: ToolCall component rendering when action has no args but has output * refactor(ToolCall): allow non-stringy args * WIP: first pass, correctly formatted tool_calls between providers * refactor: Remove duplicate import of 'roles' module * refactor: Exclude 'vite.config.ts' from TypeScript compilation * refactor: fix agent related types > - no need to use endpoint/model fields for identifying agent metadata > - add `provider` distinction for agent-configured 'endpoint' - no need for agent-endpoint map - reduce complexity of tools as functions into tools as string[] - fix types related to above changes - reduce unnecessary variables for queries/mutations and corresponding react-query keys * refactor: Add tools and tool_kwargs fields to agent schema * refactor: Remove unused code and update dependencies * refactor: Update updateAgentHandler to use req.body directly * refactor: Update AgentSelect component to use localized hooks * refactor: Update agent schema to include tools and provider fields * refactor(AgentPanel): add scrollbar gutter, add provider field to form, fix agent schema required values * refactor: Update AgentSwitcher component to use selectedAgentId instead of selectedAgent * refactor: Update AgentPanel component to include alternateName import and defaultAgentFormValues * refactor(SelectDropDown): allow setting value as option while still supporting legacy usage (string values only) * refactor: SelectDropdown changes - Only necessary when the available values are objects with label/value fields and the selected value is expected to be a string. * refactor: TypeError issues and handle provider as option * feat: Add placeholder for provider selection in AgentPanel component * refactor: Update agent schema to include author and provider fields * fix: show expected 'create agent' placeholder when creating agent * chore: fix localization strings, hide capabilities form for now * chore: typing * refactor: import order and use compact agents schema for now * chore: typing * refactor: Update AgentForm type to use AgentCapabilities * fix agent form agent selection issues * feat: responsive agent selection * fix: Handle cancelled fetch in useSelectAgent hook * fix: reset agent form on accordion close/open * feat: Add agent_id to default conversation for agents endpoint * feat: agents endpoint request handling * refactor: reset conversation model on agent select * refactor: add `additional_instructions` to conversation schema, organize other fields * chore: casing * chore: types * refactor(loadAgentTools): explicitly pass agent_id, do not pass `model` to loadAgentTools for now, load action sets by agent_id * WIP: initial draft of real agent client initialization * WIP: first pass, anthropic agent requests * feat: remember last selected agent * feat: openai and azure connected * fix: prioritize agent model for runs unless an explicit override model is passed from client * feat: Agent Actions * fix: save agent id to convo * feat: model panel (danny-avila#29) * feat: model panel * bring back comments * fix: method still null * fix: AgentPanel FormContext * feat: add more parameters * fix: style issues; refactor: Agent Controller * fix: cherry-pick * fix: Update AgentAvatar component to use AssistantIcon instead of BrainCircuit * feat: OGDialog for delete agent; feat(assistant): update Agent types, introduced `model_parameters` * feat: icon and general `model_parameters` update * feat: use react-hook-form better * fix: agent builder form reset issue when switching panels * refactor: modularize agent builder form --------- Co-authored-by: Danny Avila <[email protected]> * fix: AgentPanel and ModelPanel type issues and use `useFormContext` and `watch` instead of `methods` directly and `useWatch`. * fix: tool call issues due to invalid input (anthropic) of empty string * fix: handle empty text in Part component --------- Co-authored-by: Marco Beretta <[email protected]> * refactor: remove form ModelPanel and fixed nested ternary expressions in AgentConfig * fix: Model Parameters not saved correctly * refactor: remove console log * feat: avatar upload and get for Agents (danny-avila#36) Co-authored-by: Marco Beretta <[email protected]> * chore: update to public package * fix: typing, optional chaining * fix: cursor not showing for content parts * chore: conditionally enable agents * ci: fix azure test * ci: fix frontend tests, fix eslint api * refactor: Remove unused errorContentPart variable * continue of the agent message PR (danny-avila#40) * last fixes * fix: agentMap * pr merge test (danny-avila#41) * fix: model icon not fetching correctly * remove console logs * feat: agent name * refactor: pass documentsMap as a prop to allow re-render of assistant form * refactor: pass documentsMap as a prop to allow re-render of assistant form * chore: Bump version to 0.7.419 * fix: TypeError: Cannot read properties of undefined (reading 'id') * refactor: update AgentSwitcher component to use ControlCombobox instead of Combobox --------- Co-authored-by: Marco Beretta <[email protected]>
1 parent 618be4b commit a0291ed

File tree

141 files changed

+15035
-6276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

141 files changed

+15035
-6276
lines changed

api/app/clients/BaseClient.js

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class BaseClient {
3434
this.userMessagePromise;
3535
/** @type {ClientDatabaseSavePromise} */
3636
this.responsePromise;
37+
/** @type {string} */
38+
this.user;
39+
/** @type {string} */
40+
this.conversationId;
41+
/** @type {string} */
42+
this.responseMessageId;
3743
}
3844

3945
setOptions() {
@@ -161,6 +167,8 @@ class BaseClient {
161167
this.currentMessages[this.currentMessages.length - 1].messageId = head;
162168
}
163169

170+
this.responseMessageId = responseMessageId;
171+
164172
return {
165173
...opts,
166174
user,
@@ -347,7 +355,12 @@ class BaseClient {
347355
};
348356
}
349357

350-
async handleContextStrategy({ instructions, orderedMessages, formattedMessages }) {
358+
async handleContextStrategy({
359+
instructions,
360+
orderedMessages,
361+
formattedMessages,
362+
buildTokenMap = true,
363+
}) {
351364
let _instructions;
352365
let tokenCount;
353366

@@ -417,19 +430,23 @@ class BaseClient {
417430
maxContextTokens: this.maxContextTokens,
418431
});
419432

420-
let tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
421-
const { messageId } = message;
422-
if (!messageId) {
423-
return map;
424-
}
433+
/** @type {Record<string, number> | undefined} */
434+
let tokenCountMap;
435+
if (buildTokenMap) {
436+
tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
437+
const { messageId } = message;
438+
if (!messageId) {
439+
return map;
440+
}
425441

426-
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
427-
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
428-
}
442+
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
443+
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
444+
}
429445

430-
map[messageId] = orderedWithInstructions[index].tokenCount;
431-
return map;
432-
}, {});
446+
map[messageId] = orderedWithInstructions[index].tokenCount;
447+
return map;
448+
}, {});
449+
}
433450

434451
const promptTokens = this.maxContextTokens - remainingContextTokens;
435452

@@ -542,13 +559,19 @@ class BaseClient {
542559
isEdited,
543560
model: this.modelOptions.model,
544561
sender: this.sender,
545-
text: addSpaceIfNeeded(generation) + completion,
546562
promptTokens,
547563
iconURL: this.options.iconURL,
548564
endpoint: this.options.endpoint,
549565
...(this.metadata ?? {}),
550566
};
551567

568+
if (typeof completion === 'string') {
569+
responseMessage.text = addSpaceIfNeeded(generation) + completion;
570+
} else if (completion) {
571+
responseMessage.text = '';
572+
responseMessage.content = completion;
573+
}
574+
552575
if (
553576
tokenCountMap &&
554577
this.recordTokenUsage &&
@@ -868,8 +891,12 @@ class BaseClient {
868891

869892
processValue(nestedValue);
870893
}
871-
} else {
894+
} else if (typeof value === 'string') {
872895
numTokens += this.getTokenCount(value);
896+
} else if (typeof value === 'number') {
897+
numTokens += this.getTokenCount(value.toString());
898+
} else if (typeof value === 'boolean') {
899+
numTokens += this.getTokenCount(value.toString());
873900
}
874901
};
875902

api/app/clients/OpenAIClient.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,7 @@ ${convo}
10231023
async chatCompletion({ payload, onProgress, abortController = null }) {
10241024
let error = null;
10251025
const errorCallback = (err) => (error = err);
1026-
let intermediateReply = '';
1026+
const intermediateReply = [];
10271027
try {
10281028
if (!abortController) {
10291029
abortController = new AbortController();
@@ -1217,19 +1217,19 @@ ${convo}
12171217
}
12181218

12191219
if (typeof finalMessage.content !== 'string' || finalMessage.content.trim() === '') {
1220-
finalChatCompletion.choices[0].message.content = intermediateReply;
1220+
finalChatCompletion.choices[0].message.content = intermediateReply.join('');
12211221
}
12221222
})
12231223
.on('finalMessage', (message) => {
12241224
if (message?.role !== 'assistant') {
1225-
stream.messages.push({ role: 'assistant', content: intermediateReply });
1225+
stream.messages.push({ role: 'assistant', content: intermediateReply.join('') });
12261226
UnexpectedRoleError = true;
12271227
}
12281228
});
12291229

12301230
for await (const chunk of stream) {
12311231
const token = chunk.choices[0]?.delta?.content || '';
1232-
intermediateReply += token;
1232+
intermediateReply.push(token);
12331233
onProgress(token);
12341234
if (abortController.signal.aborted) {
12351235
stream.controller.abort();
@@ -1285,11 +1285,12 @@ ${convo}
12851285
}
12861286

12871287
if (typeof message.content !== 'string' || message.content.trim() === '') {
1288+
const reply = intermediateReply.join('');
12881289
logger.debug(
12891290
'[OpenAIClient] chatCompletion: using intermediateReply due to empty message.content',
1290-
{ intermediateReply },
1291+
{ intermediateReply: reply },
12911292
);
1292-
return intermediateReply;
1293+
return reply;
12931294
}
12941295

12951296
return message.content;
@@ -1298,7 +1299,7 @@ ${convo}
12981299
err?.message?.includes('abort') ||
12991300
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
13001301
) {
1301-
return intermediateReply;
1302+
return intermediateReply.join('');
13021303
}
13031304
if (
13041305
err?.message?.includes(
@@ -1313,10 +1314,10 @@ ${convo}
13131314
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
13141315
) {
13151316
logger.error('[OpenAIClient] Known OpenAI error:', err);
1316-
return intermediateReply;
1317+
return intermediateReply.join('');
13171318
} else if (err instanceof OpenAI.APIError) {
13181319
if (intermediateReply) {
1319-
return intermediateReply;
1320+
return intermediateReply.join('');
13201321
} else {
13211322
throw err;
13221323
}

api/app/clients/llm/createLLM.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const { isEnabled } = require('~/server/utils');
88
* @param {Object} options - The options for creating the LLM.
99
* @param {ModelOptions} options.modelOptions - The options specific to the model, including modelName, temperature, presence_penalty, frequency_penalty, and other model-related settings.
1010
* @param {ConfigOptions} options.configOptions - Configuration options for the API requests, including proxy settings and custom headers.
11-
* @param {Callbacks} options.callbacks - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
11+
* @param {Callbacks} [options.callbacks] - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
1212
* @param {boolean} [options.streaming=false] - Determines if the LLM should operate in streaming mode.
1313
* @param {string} options.openAIApiKey - The API key for OpenAI, used for authentication.
1414
* @param {AzureOptions} [options.azure={}] - Optional Azure-specific configurations. If provided, Azure configurations take precedence over OpenAI configurations.

api/app/clients/prompts/formatMessages.js

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const { EModelEndpoint } = require('librechat-data-provider');
1+
const { ToolMessage } = require('@langchain/core/messages');
2+
const { EModelEndpoint, ContentTypes } = require('librechat-data-provider');
23
const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
34

45
/**
@@ -14,11 +15,11 @@ const { HumanMessage, AIMessage, SystemMessage } = require('langchain/schema');
1415
*/
1516
const formatVisionMessage = ({ message, image_urls, endpoint }) => {
1617
if (endpoint === EModelEndpoint.anthropic) {
17-
message.content = [...image_urls, { type: 'text', text: message.content }];
18+
message.content = [...image_urls, { type: ContentTypes.TEXT, text: message.content }];
1819
return message;
1920
}
2021

21-
message.content = [{ type: 'text', text: message.content }, ...image_urls];
22+
message.content = [{ type: ContentTypes.TEXT, text: message.content }, ...image_urls];
2223

2324
return message;
2425
};
@@ -51,7 +52,7 @@ const formatMessage = ({ message, userName, assistantName, endpoint, langChain =
5152
_role = roleMapping[lc_id[2]];
5253
}
5354
const role = _role ?? (sender && sender?.toLowerCase() === 'user' ? 'user' : 'assistant');
54-
const content = text ?? _content ?? '';
55+
const content = _content ?? text ?? '';
5556
const formattedMessage = {
5657
role,
5758
content,
@@ -131,4 +132,71 @@ const formatFromLangChain = (message) => {
131132
};
132133
};
133134

134-
module.exports = { formatMessage, formatLangChainMessages, formatFromLangChain };
135+
/**
136+
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
137+
*
138+
* @param {Array<Partial<TMessage>>} payload - The array of messages to format.
139+
* @returns {Array<(HumanMessage|AIMessage|SystemMessage|ToolMessage)>} - The array of formatted LangChain messages, including ToolMessages for tool calls.
140+
*/
141+
const formatAgentMessages = (payload) => {
142+
const messages = [];
143+
144+
for (const message of payload) {
145+
if (message.role !== 'assistant') {
146+
messages.push(formatMessage({ message, langChain: true }));
147+
continue;
148+
}
149+
150+
let currentContent = [];
151+
let lastAIMessage = null;
152+
153+
for (const part of message.content) {
154+
if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
155+
// If there's pending content, add it as an AIMessage
156+
if (currentContent.length > 0) {
157+
messages.push(new AIMessage({ content: currentContent }));
158+
currentContent = [];
159+
}
160+
161+
// Create a new AIMessage with this text and prepare for tool calls
162+
lastAIMessage = new AIMessage({
163+
content: part.text || '',
164+
});
165+
166+
messages.push(lastAIMessage);
167+
} else if (part.type === ContentTypes.TOOL_CALL) {
168+
if (!lastAIMessage) {
169+
throw new Error('Invalid tool call structure: No preceding AIMessage with tool_call_ids');
170+
}
171+
172+
// Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
173+
const { output, ...tool_call } = part.tool_call;
174+
lastAIMessage.tool_calls.push(tool_call);
175+
176+
// Add the corresponding ToolMessage
177+
messages.push(
178+
new ToolMessage({
179+
tool_call_id: tool_call.id,
180+
name: tool_call.name,
181+
content: output,
182+
}),
183+
);
184+
} else {
185+
currentContent.push(part);
186+
}
187+
}
188+
189+
if (currentContent.length > 0) {
190+
messages.push(new AIMessage({ content: currentContent }));
191+
}
192+
}
193+
194+
return messages;
195+
};
196+
197+
module.exports = {
198+
formatMessage,
199+
formatFromLangChain,
200+
formatAgentMessages,
201+
formatLangChainMessages,
202+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const { z } = require('zod');
2+
const { tool } = require('@langchain/core/tools');
3+
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
4+
5+
function createTavilySearchTool(fields = {}) {
6+
const envVar = 'TAVILY_API_KEY';
7+
const override = fields.override ?? false;
8+
const apiKey = fields.apiKey ?? getApiKey(envVar, override);
9+
const kwargs = fields?.kwargs ?? {};
10+
11+
function getApiKey(envVar, override) {
12+
const key = getEnvironmentVariable(envVar);
13+
if (!key && !override) {
14+
throw new Error(`Missing ${envVar} environment variable.`);
15+
}
16+
return key;
17+
}
18+
19+
return tool(
20+
async (input) => {
21+
const { query, ...rest } = input;
22+
23+
const requestBody = {
24+
api_key: apiKey,
25+
query,
26+
...rest,
27+
...kwargs,
28+
};
29+
30+
const response = await fetch('https://api.tavily.com/search', {
31+
method: 'POST',
32+
headers: {
33+
'Content-Type': 'application/json',
34+
},
35+
body: JSON.stringify(requestBody),
36+
});
37+
38+
const json = await response.json();
39+
if (!response.ok) {
40+
throw new Error(`Request failed with status ${response.status}: ${json.error}`);
41+
}
42+
43+
return JSON.stringify(json);
44+
},
45+
{
46+
name: 'tavily_search_results_json',
47+
description:
48+
'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.',
49+
schema: z.object({
50+
query: z.string().min(1).describe('The search query string.'),
51+
max_results: z
52+
.number()
53+
.min(1)
54+
.max(10)
55+
.optional()
56+
.describe('The maximum number of search results to return. Defaults to 5.'),
57+
search_depth: z
58+
.enum(['basic', 'advanced'])
59+
.optional()
60+
.describe(
61+
'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.',
62+
),
63+
include_images: z
64+
.boolean()
65+
.optional()
66+
.describe(
67+
'Whether to include a list of query-related images in the response. Default is False.',
68+
),
69+
include_answer: z
70+
.boolean()
71+
.optional()
72+
.describe('Whether to include answers in the search results. Default is False.'),
73+
}),
74+
},
75+
);
76+
}
77+
78+
module.exports = createTavilySearchTool;

api/models/Action.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const Action = mongoose.model('action', actionSchema);
1212
* @param {string} searchParams.user - The user ID of the action's author.
1313
* @param {Object} updateData - An object containing the properties to update.
1414
* @param {mongoose.ClientSession} [session] - The transaction session to use.
15-
* @returns {Promise<Object>} The updated or newly created action document as a plain object.
15+
* @returns {Promise<Action>} The updated or newly created action document as a plain object.
1616
*/
1717
const updateAction = async (searchParams, updateData, session = null) => {
1818
const options = { new: true, upsert: true, session };
@@ -24,7 +24,7 @@ const updateAction = async (searchParams, updateData, session = null) => {
2424
*
2525
* @param {Object} searchParams - The search parameters to find matching actions.
2626
* @param {boolean} includeSensitive - Flag to include sensitive data in the metadata.
27-
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
27+
* @returns {Promise<Array<Action>>} A promise that resolves to an array of action documents as plain objects.
2828
*/
2929
const getActions = async (searchParams, includeSensitive = false) => {
3030
const actions = await Action.find(searchParams).lean();
@@ -55,7 +55,7 @@ const getActions = async (searchParams, includeSensitive = false) => {
5555
* @param {string} searchParams.action_id - The ID of the action to delete.
5656
* @param {string} searchParams.user - The user ID of the action's author.
5757
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
58-
* @returns {Promise<Object>} A promise that resolves to the deleted action document as a plain object, or null if no document was found.
58+
* @returns {Promise<Action>} A promise that resolves to the deleted action document as a plain object, or null if no document was found.
5959
*/
6060
const deleteAction = async (searchParams, session = null) => {
6161
const options = session ? { session } : {};

0 commit comments

Comments
 (0)