Skip to content

Commit 7ddc364

Browse files
mawburndanny-avila
authored andcommitted
⌛ feat: Agent Version History and Management (#7455)
* ✨ feat: Enhance agent update functionality to save current state in versions array - Updated the `updateAgent` function to push the current agent's state into a new `versions` array when an agent is updated. - Modified the agent schema to include a `versions` field for storing historical states of agents. * ✨ feat: Add comprehensive CRUD operations for agents in tests - Introduced a new test suite for CRUD operations on agents, including create, read, update, and delete functionalities. - Implemented tests for listing agents by author and updating agent projects. - Enhanced the agent model to support version history tracking during updates. - Ensured proper environment variable management during tests. * ✨ feat: Introduce version tracking for agents and enhance UI components - Added a `version` property to the agent model to track the number of versions. - Updated the `getAgentHandler` to include the agent's version in the response. - Introduced a new `VersionButton` component for navigating to the version panel. - Created a `VersionPanel` component for displaying version-related information. - Updated the UI to conditionally render the version button and panel based on the active state. - Added localization for the new version-related UI elements. * ✨ i18n: Add "version" translation key across multiple languages - Introduced the "com_ui_agent_version" translation key in various language files to support version tracking for agents. - Updated Arabic, Czech, German, English, Spanish, Estonian, Persian, Finnish, French, Hebrew, Hungarian, Indonesian, Italian, Japanese, Korean, Dutch, Polish, Portuguese (Brazil and Portugal), Russian, Swedish, Thai, Turkish, Vietnamese, and Chinese (Simplified and Traditional) translations. * ✨ feat: Update AgentFooter to conditionally render AdminSettings - Modified the logic for displaying buttons in the AgentFooter component to only show them when the active panel is the builder. - Ensured that AdminSettings is displayed only when the user has an admin role and the buttons are visible. * ✨ feat: Enhance AgentPanelSwitch and VersionPanel for improved agent capabilities - Updated AgentPanelSwitch to include a new VersionPanel for displaying version-related information. - Enhanced agentsConfig logic to properly handle agent capabilities. - Modified VersionPanel to improve structure and localization support. - Integrated createAgent mutation for future agent creation functionality. * ✨ feat: Enhance VersionPanel to display agent version history and loading states - Integrated version fetching logic in VersionPanel to retrieve and display agent version history. - Added loading and error handling states to improve user experience. - Updated agent schema to use mixed types for versions, allowing for more flexible version data structures. - Introduced localization support for version-related UI elements. * ✨ feat: Update VersionPanel and AgentPanelSwitch to enhance agent selection and version display - Modified AgentPanelSwitch to pass selectedAgentId to VersionPanel for improved agent context. - Enhanced VersionPanel to handle multiple timestamp formats and display appropriate messages when no agent is selected. - Improved structure and readability of the VersionPanel component by adding a helper function for timestamp retrieval. * ✨ feat: Refactor VersionPanel to utilize localization and improve timestamp handling - Replaced hardcoded text constants with localization support for various UI elements in VersionPanel. - Enhanced the timestamp retrieval function to handle errors gracefully and utilize localized messages for unknown dates. - Improved user feedback by displaying localized messages for agent selection, version errors, and empty states. * ✨ refactor: Clean up VersionPanel by removing unused code and improving timestamp handling * ✨ feat: Implement agent version reverting functionality - Added `revertAgentVersion` method in the Agent model to allow reverting to a previous version of an agent. - Introduced `revertAgentVersionHandler` in the agents controller to handle requests for reverting agent versions. - Updated API routes to include a new endpoint for reverting agent versions. - Enhanced the VersionPanel component to support version restoration with user confirmation and feedback. - Added localization support for success and error messages related to version restoration. * ✨ i18n: Add localization for agent version restoration messages * Simplify VersionPanel by removing unused parameters and enhancing agent ID handling * Refactor Agent model and VersionPanel component to streamline version data handling * Update version handling in Agent model and VersionPanel - Enhanced the Agent model to include an `updatedAt` timestamp when pushing new versions. - Improved the VersionPanel component to sort versions by the `updatedAt` timestamp for better display order. - Added a new localization entry for indicating the active version of an agent. * ✨ i18n: Add localization for active agent version across multiple languages * ✨ feat: Introduce version management components for agent history - Added `isActiveVersion` utility to determine the active version of an agent based on various criteria. - Implemented `VersionContent` and `VersionItem` components to display agent version history, including loading and error states. - Enhanced `VersionPanel` to integrate new components and manage version context effectively. - Added comprehensive tests for version management functionalities to ensure reliability and correctness. * Add unit tests for AgentFooter component * cleanup * Enhance agent version update handling and add unit tests for update operators - Updated the `updateAgent` function to properly handle various update operators ($push, $pull, $addToSet) while maintaining version history. - Modified unit tests to validate the correct behavior of agent updates, including versioning and tool management. * Enhance version comparison logic and update tests for artifacts handling - Modified the `isActiveVersion` utility to include artifacts in the version comparison criteria. - Updated the `VersionPanel` component to support artifacts in the agent state. - Added new unit tests to validate artifacts matching scenarios and edge cases in the `isActiveVersion` function. * Implement duplicate version detection in agent updates and enhance error handling - Added `isDuplicateVersion` function to check for identical versions during agent updates, excluding certain fields. - Updated `updateAgent` function to throw an error if a duplicate version is detected, with detailed error information. - Enhanced the `updateAgentHandler` to return appropriate responses for duplicate version errors. - Modified client-side error handling to display user-friendly messages for duplicate version scenarios. - Added comprehensive unit tests to validate duplicate version detection and error handling across various update scenarios. * Update version title localization to include version number across multiple languages - Modified the `com_ui_agent_version_title` translation key to include a placeholder for the version number in various language files. - Enhanced the `VersionItem` component to utilize the updated localization for displaying version titles dynamically. * Enhance agent version handling and add revert functionality - Updated the `isDuplicateVersion` function to improve version comparison logic, including special handling for `projectIds` and arrays of objects. - Modified the `updateAgent` function to streamline version updates and removed unnecessary checks for test environments. - Introduced a new `revertAgentVersion` function to allow reverting agents to specific versions, with detailed documentation. - Enhanced unit tests to validate duplicate version detection and revert functionality, ensuring robust error handling and version management. * fix CI issues * cleanup * Revert all non-English translations * clean up tests
1 parent a2f330e commit 7ddc364

File tree

26 files changed

+2362
-16
lines changed

26 files changed

+2362
-16
lines changed

api/models/Agent.js

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@ const Agent = mongoose.model('agent', agentSchema);
2121
* @throws {Error} If the agent creation fails.
2222
*/
2323
const createAgent = async (agentData) => {
24-
return (await Agent.create(agentData)).toObject();
24+
const { versions, ...versionData } = agentData;
25+
const timestamp = new Date();
26+
const initialAgentData = {
27+
...agentData,
28+
versions: [
29+
{
30+
...versionData,
31+
createdAt: timestamp,
32+
updatedAt: timestamp,
33+
},
34+
],
35+
};
36+
return (await Agent.create(initialAgentData)).toObject();
2537
};
2638

2739
/**
@@ -103,6 +115,8 @@ const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => {
103115
return null;
104116
}
105117

118+
agent.version = agent.versions ? agent.versions.length : 0;
119+
106120
if (agent.author.toString() === req.user.id) {
107121
return agent;
108122
}
@@ -127,18 +141,146 @@ const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => {
127141
}
128142
};
129143

144+
/**
145+
* Check if a version already exists in the versions array, excluding timestamp and author fields
146+
* @param {Object} updateData - The update data to compare
147+
* @param {Array} versions - The existing versions array
148+
* @returns {Object|null} - The matching version if found, null otherwise
149+
*/
150+
const isDuplicateVersion = (updateData, currentData, versions) => {
151+
if (!versions || versions.length === 0) {
152+
return null;
153+
}
154+
155+
const excludeFields = [
156+
'_id',
157+
'id',
158+
'createdAt',
159+
'updatedAt',
160+
'author',
161+
'created_at',
162+
'updated_at',
163+
'__v',
164+
'agent_ids',
165+
'versions',
166+
];
167+
168+
const { $push, $pull, $addToSet, ...directUpdates } = updateData;
169+
170+
if (Object.keys(directUpdates).length === 0) {
171+
return null;
172+
}
173+
174+
const wouldBeVersion = { ...currentData, ...directUpdates };
175+
const lastVersion = versions[versions.length - 1];
176+
177+
const allFields = new Set([...Object.keys(wouldBeVersion), ...Object.keys(lastVersion)]);
178+
179+
const importantFields = Array.from(allFields).filter((field) => !excludeFields.includes(field));
180+
181+
let isMatch = true;
182+
for (const field of importantFields) {
183+
if (!wouldBeVersion[field] && !lastVersion[field]) {
184+
continue;
185+
}
186+
187+
if (Array.isArray(wouldBeVersion[field]) && Array.isArray(lastVersion[field])) {
188+
if (wouldBeVersion[field].length !== lastVersion[field].length) {
189+
isMatch = false;
190+
break;
191+
}
192+
193+
// Special handling for projectIds (MongoDB ObjectIds)
194+
if (field === 'projectIds') {
195+
const wouldBeIds = wouldBeVersion[field].map((id) => id.toString()).sort();
196+
const versionIds = lastVersion[field].map((id) => id.toString()).sort();
197+
198+
if (!wouldBeIds.every((id, i) => id === versionIds[i])) {
199+
isMatch = false;
200+
break;
201+
}
202+
}
203+
// Handle arrays of objects like tool_kwargs
204+
else if (typeof wouldBeVersion[field][0] === 'object' && wouldBeVersion[field][0] !== null) {
205+
const sortedWouldBe = [...wouldBeVersion[field]].map((item) => JSON.stringify(item)).sort();
206+
const sortedVersion = [...lastVersion[field]].map((item) => JSON.stringify(item)).sort();
207+
208+
if (!sortedWouldBe.every((item, i) => item === sortedVersion[i])) {
209+
isMatch = false;
210+
break;
211+
}
212+
} else {
213+
const sortedWouldBe = [...wouldBeVersion[field]].sort();
214+
const sortedVersion = [...lastVersion[field]].sort();
215+
216+
if (!sortedWouldBe.every((item, i) => item === sortedVersion[i])) {
217+
isMatch = false;
218+
break;
219+
}
220+
}
221+
} else if (field === 'model_parameters') {
222+
const wouldBeParams = wouldBeVersion[field] || {};
223+
const lastVersionParams = lastVersion[field] || {};
224+
if (JSON.stringify(wouldBeParams) !== JSON.stringify(lastVersionParams)) {
225+
isMatch = false;
226+
break;
227+
}
228+
} else if (wouldBeVersion[field] !== lastVersion[field]) {
229+
isMatch = false;
230+
break;
231+
}
232+
}
233+
234+
return isMatch ? lastVersion : null;
235+
};
236+
130237
/**
131238
* Update an agent with new data without overwriting existing
132239
* properties, or create a new agent if it doesn't exist.
240+
* When an agent is updated, a copy of the current state will be saved to the versions array.
133241
*
134242
* @param {Object} searchParameter - The search parameters to find the agent to update.
135243
* @param {string} searchParameter.id - The ID of the agent to update.
136244
* @param {string} [searchParameter.author] - The user ID of the agent's author.
137245
* @param {Object} updateData - An object containing the properties to update.
138246
* @returns {Promise<Agent>} The updated or newly created agent document as a plain object.
247+
* @throws {Error} If the update would create a duplicate version
139248
*/
140249
const updateAgent = async (searchParameter, updateData) => {
141250
const options = { new: true, upsert: false };
251+
252+
const currentAgent = await Agent.findOne(searchParameter);
253+
if (currentAgent) {
254+
const { __v, _id, id, versions, ...versionData } = currentAgent.toObject();
255+
const { $push, $pull, $addToSet, ...directUpdates } = updateData;
256+
257+
if (Object.keys(directUpdates).length > 0 && versions && versions.length > 0) {
258+
const duplicateVersion = isDuplicateVersion(updateData, versionData, versions);
259+
if (duplicateVersion) {
260+
const error = new Error(
261+
'Duplicate version: This would create a version identical to an existing one',
262+
);
263+
error.statusCode = 409;
264+
error.details = {
265+
duplicateVersion,
266+
versionIndex: versions.findIndex(
267+
(v) => JSON.stringify(duplicateVersion) === JSON.stringify(v),
268+
),
269+
};
270+
throw error;
271+
}
272+
}
273+
274+
updateData.$push = {
275+
...($push || {}),
276+
versions: {
277+
...versionData,
278+
...directUpdates,
279+
updatedAt: new Date(),
280+
},
281+
};
282+
}
283+
142284
return Agent.findOneAndUpdate(searchParameter, updateData, options).lean();
143285
};
144286

@@ -358,6 +500,38 @@ const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds
358500
return await getAgent({ id: agentId });
359501
};
360502

503+
/**
504+
* Reverts an agent to a specific version in its version history.
505+
* @param {Object} searchParameter - The search parameters to find the agent to revert.
506+
* @param {string} searchParameter.id - The ID of the agent to revert.
507+
* @param {string} [searchParameter.author] - The user ID of the agent's author.
508+
* @param {number} versionIndex - The index of the version to revert to in the versions array.
509+
* @returns {Promise<MongoAgent>} The updated agent document after reverting.
510+
* @throws {Error} If the agent is not found or the specified version does not exist.
511+
*/
512+
const revertAgentVersion = async (searchParameter, versionIndex) => {
513+
const agent = await Agent.findOne(searchParameter);
514+
if (!agent) {
515+
throw new Error('Agent not found');
516+
}
517+
518+
if (!agent.versions || !agent.versions[versionIndex]) {
519+
throw new Error(`Version ${versionIndex} not found`);
520+
}
521+
522+
const revertToVersion = agent.versions[versionIndex];
523+
524+
const updateData = {
525+
...revertToVersion,
526+
};
527+
528+
delete updateData._id;
529+
delete updateData.id;
530+
delete updateData.versions;
531+
532+
return Agent.findOneAndUpdate(searchParameter, updateData, { new: true }).lean();
533+
};
534+
361535
module.exports = {
362536
Agent,
363537
getAgent,
@@ -369,4 +543,5 @@ module.exports = {
369543
updateAgentProjects,
370544
addAgentResourceFile,
371545
removeAgentResourceFiles,
546+
revertAgentVersion,
372547
};

0 commit comments

Comments
 (0)