Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/server/controllers/agents/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ const db = require('~/models');

const loadAgent = (params) =>
loadAgentFn(params, {
getAgent: db.getAgent,
getAgent: db.getAgentWithVersionCount,
getMCPServerTools,
getAccessibleMCPServers,
});
Expand Down
6 changes: 3 additions & 3 deletions api/server/controllers/agents/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ const updateAgentHandler = async (req, res) => {
includesToolsConfiguration ||
includesToolOptionsConfiguration
) {
existingAgent = await db.getAgent({ id });
existingAgent = await db.getAgent({ id }, {});
if (!existingAgent) {
return res.status(404).json({ error: 'Agent not found' });
}
Expand Down Expand Up @@ -1237,7 +1237,7 @@ const updateAgentHandler = async (req, res) => {
// Convert OCR to context in incoming updateData
convertOcrToContextInPlace(updateData);

existingAgent ??= await db.getAgent({ id });
existingAgent ??= await db.getAgent({ id }, {});

if (!existingAgent) {
return res.status(404).json({ error: 'Agent not found' });
Expand Down Expand Up @@ -2066,7 +2066,7 @@ const revertAgentVersionHandler = async (req, res) => {
return res.status(400).json({ error: 'version_index is required' });
}

const existingAgent = await db.getAgent({ id });
const existingAgent = await db.getAgent({ id }, {});

if (!existingAgent) {
return res.status(404).json({ error: 'Agent not found' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const { checkPermission } = require('~/server/services/PermissionService');
const { canAccessResource } = require('./canAccessResource');
const db = require('~/models');

const { getRoleByName, getAgent } = db;
const { getRoleByName, getAgentWithVersionCount } = db;

const resolveEnforcedAgentId = (req, endpoint) => {
const modelSpecs = req.config?.modelSpecs;
Expand All @@ -39,7 +39,7 @@ const resolveAgentIdFromBody = async (agentCustomId) => {
if (isEphemeralAgentId(agentCustomId)) {
return null;
}
return getAgent({ id: agentCustomId });
return getAgentWithVersionCount({ id: agentCustomId });
};

/**
Expand Down
2 changes: 1 addition & 1 deletion api/server/services/Endpoints/agents/addedConvo.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const db = require('~/models');

const loadAddedAgent = (params) =>
loadAddedAgentFn(params, {
getAgent: db.getAgent,
getAgent: db.getAgentWithVersionCount,
getMCPServerTools,
getAccessibleMCPServers,
});
Expand Down
2 changes: 1 addition & 1 deletion api/server/services/Endpoints/agents/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const db = require('~/models');

const loadAgent = (params) =>
loadAgentFn(params, {
getAgent: db.getAgent,
getAgent: db.getAgentWithVersionCount,
getMCPServerTools,
getAccessibleMCPServers,
});
Expand Down
7 changes: 4 additions & 3 deletions packages/api/src/agents/__tests__/load.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import { loadAgent } from '../load';

let Agent: mongoose.Model<unknown>;
let createAgent: ReturnType<typeof createMethods>['createAgent'];
let getAgent: ReturnType<typeof createMethods>['getAgent'];
let getAgentWithVersionCount: ReturnType<typeof createMethods>['getAgentWithVersionCount'];

const mockGetMCPServerTools = jest.fn();
const mockGetAccessibleMCPServers = jest.fn();

const deps: LoadAgentDeps = {
getAgent: (searchParameter) => getAgent(searchParameter) as Promise<LibreChatAgent | null>,
getAgent: (searchParameter) =>
getAgentWithVersionCount(searchParameter) as Promise<LibreChatAgent | null>,
getMCPServerTools: mockGetMCPServerTools,
getAccessibleMCPServers: mockGetAccessibleMCPServers,
};
Expand All @@ -37,7 +38,7 @@ describe('loadAgent', () => {
await mongoose.connect(mongoUri);
const methods = createMethods(mongoose);
createAgent = methods.createAgent;
getAgent = methods.getAgent;
getAgentWithVersionCount = methods.getAgentWithVersionCount;
}, 20000);

afterAll(async () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/api/src/agents/added.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ function applyModelSpecSubagents(
}

export interface LoadAddedAgentDeps {
getAgent: (searchParameter: { id: string }) => Promise<Agent | null>;
/** Resolves the agent without its `versions` history; `version` carries the count. */
getAgent: (searchParameter: {
id: string;
}) => Promise<(Agent & { version?: number; versions?: unknown[] }) | null>;
getMCPServerTools: (
userId: string,
serverName: string,
Expand Down Expand Up @@ -99,9 +102,8 @@ export async function loadAddedAgent(
return null;
}

const agentRecord = agent as Record<string, unknown>;
const versions = agentRecord.versions as unknown[] | undefined;
agentRecord.version = versions ? versions.length : 0;
const agentRecord = agent as Agent & { version?: number; versions?: unknown[] };
agentRecord.version ??= agentRecord.versions?.length ?? 0;
agent.id = appendAgentIdSuffix(agent.id, 1);
return agent;
}
Expand Down
9 changes: 5 additions & 4 deletions packages/api/src/agents/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ const { mcp_all, mcp_delimiter } = Constants;
type ModelParametersWithPromptPrefix = AgentModelParameters & { promptPrefix?: string | null };

export interface LoadAgentDeps {
getAgent: (searchParameter: { id: string }) => Promise<Agent | null>;
/** Resolves the agent without its `versions` history; `version` carries the count. */
getAgent: (searchParameter: {
id: string;
}) => Promise<(Agent & { version?: number; versions?: unknown[] }) | null>;
getMCPServerTools: (
userId: string,
serverName: string,
Expand Down Expand Up @@ -242,8 +245,6 @@ export async function loadAgent(
return null;
}

// Set version count from versions array length
const agentWithVersion = agent as Agent & { versions?: unknown[]; version?: number };
agentWithVersion.version = agentWithVersion.versions ? agentWithVersion.versions.length : 0;
agent.version ??= agent.versions?.length ?? 0;
return agent;
}
44 changes: 22 additions & 22 deletions packages/data-schemas/src/methods/agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2390,7 +2390,7 @@ describe('Agent Methods', () => {
},
);

const firstUpdate = await getAgent({ id: agentId });
const firstUpdate = await getAgent({ id: agentId }, {});
expect(firstUpdate!.description).toBe('Updated description');
expect(firstUpdate!.tools).toContain('tool1');
expect(firstUpdate!.tools).toContain('tool2');
Expand All @@ -2403,7 +2403,7 @@ describe('Agent Methods', () => {
},
);

const secondUpdate = await getAgent({ id: agentId });
const secondUpdate = await getAgent({ id: agentId }, {});
expect(secondUpdate!.tools).toHaveLength(2);
expect(secondUpdate!.tools).toContain('tool2');
expect(secondUpdate!.tools).toContain('tool3');
Expand All @@ -2417,7 +2417,7 @@ describe('Agent Methods', () => {
},
);

const thirdUpdate = await getAgent({ id: agentId });
const thirdUpdate = await getAgent({ id: agentId }, {});
const toolCount = thirdUpdate!.tools!.filter((t) => t === 'tool3').length;
expect(toolCount).toBe(2);
expect(thirdUpdate!.versions).toHaveLength(4);
Expand Down Expand Up @@ -2487,14 +2487,14 @@ describe('Agent Methods', () => {
},
);

const complexAgent = await getAgent({ id: agentId });
const complexAgent = await getAgent({ id: agentId }, {});
expect(complexAgent!.versions).toHaveLength(3);
expect(complexAgent!.model_parameters?.temperature).toBe(0.8);
expect(complexAgent!.model_parameters?.max_tokens).toBe(1000);

await updateAgent({ id: agentId }, { model_parameters: {} });

const emptyParamsAgent = await getAgent({ id: agentId });
const emptyParamsAgent = await getAgent({ id: agentId }, {});
expect(emptyParamsAgent!.versions).toHaveLength(4);
expect(emptyParamsAgent!.model_parameters).toEqual({});
});
Expand Down Expand Up @@ -2522,7 +2522,7 @@ describe('Agent Methods', () => {

expect(duplicateUpdate!.versions).toHaveLength(2); // No new version created

const agent = await getAgent({ id: testAgentId });
const agent = await getAgent({ id: testAgentId }, {});
expect(agent!.versions).toHaveLength(2);
}
});
Expand All @@ -2544,7 +2544,7 @@ describe('Agent Methods', () => {
* same shape `addAgentResourceFile` produces when it attaches a file. */
await updateAgent({ id: agentId }, { $addToSet: { tools: 'file_search' } });

const afterAdd = await getAgent({ id: agentId });
const afterAdd = await getAgent({ id: agentId }, {});
expect(afterAdd!.tools).toEqual(['file_search']);
expect(afterAdd!.versions).toHaveLength(2);
expect((afterAdd!.versions![1] as VersionEntry).tools).toEqual([]);
Expand All @@ -2556,7 +2556,7 @@ describe('Agent Methods', () => {
expect(removed!.tools).toEqual([]);
expect(removed!.versions).toHaveLength(2);

const reloaded = await getAgent({ id: agentId });
const reloaded = await getAgent({ id: agentId }, {});
expect(reloaded!.tools).toEqual([]);
expect(reloaded!.versions).toHaveLength(2);
});
Expand Down Expand Up @@ -2592,7 +2592,7 @@ describe('Agent Methods', () => {

/** The re-attach snapshots the current state, so the document now equals the newest
* version and the next attach is judged a duplicate. */
const settled = await getAgent({ id: agentId });
const settled = await getAgent({ id: agentId }, {});
const newestVersion = settled!.versions![settled!.versions!.length - 1] as VersionEntry;
expect(fileIdsOf(settled)).toEqual(['f1']);
expect(newestVersion.tool_resources).toEqual(settled!.tool_resources);
Expand Down Expand Up @@ -2632,7 +2632,7 @@ describe('Agent Methods', () => {

/** The document now equals its newest version, so the snapshot is a duplicate and
* only the operator can justify recording an entry. */
const settled = await getAgent({ id: agentId });
const settled = await getAgent({ id: agentId }, {});
const versionCount = settled!.versions!.length;

/** Re-attaching an id the agent already holds makes `$addToSet` a Mongo no-op. An
Expand All @@ -2643,7 +2643,7 @@ describe('Agent Methods', () => {
file_id: 'f1',
});

const after = await getAgent({ id: agentId });
const after = await getAgent({ id: agentId }, {});
expect(after!.versions).toHaveLength(versionCount);
expect(
(after?.tool_resources as Record<string, { file_ids?: string[] }> | undefined)?.file_search
Expand Down Expand Up @@ -2675,7 +2675,7 @@ describe('Agent Methods', () => {
file_id: 'f1',
});

const settled = await getAgent({ id: agentId });
const settled = await getAgent({ id: agentId }, {});
const versionCount = settled!.versions!.length;

/** The no-op reading comes from a document fetched before the write, so a `$pull`
Expand All @@ -2697,7 +2697,7 @@ describe('Agent Methods', () => {
expect(suppressedUpdate.$addToSet).toBeUndefined();
expect(suppressedUpdate.$push).toBeUndefined();
expect(suppressedUpdate.$pull).toBeUndefined();
expect((await getAgent({ id: agentId }))!.versions).toHaveLength(versionCount);
expect((await getAgent({ id: agentId }, {}))!.versions).toHaveLength(versionCount);
});

test('should record a version when a duplicate direct update carries an atomic operator', async () => {
Expand All @@ -2724,7 +2724,7 @@ describe('Agent Methods', () => {
expect(updated!.tools).toEqual(['appended_tool']);
expect(updated!.versions).toHaveLength(3);

const reloaded = await getAgent({ id: agentId });
const reloaded = await getAgent({ id: agentId }, {});
expect(reloaded!.tools).toEqual(['appended_tool']);
expect(reloaded!.versions).toHaveLength(3);
});
Expand Down Expand Up @@ -2772,7 +2772,7 @@ describe('Agent Methods', () => {
{ avatar: { filepath: '/images/a.png', source: 'local' } },
{ skipVersioning: true },
);
const withAvatar = await getAgent({ id: agentId });
const withAvatar = await getAgent({ id: agentId }, {});
expect(withAvatar!.avatar).toBeTruthy();
expect((withAvatar!.versions![1] as VersionEntry).avatar).toBeUndefined();

Expand All @@ -2798,13 +2798,13 @@ describe('Agent Methods', () => {
});
await updateAgent({ id: agentId }, { name: 'Idempotent' });

const before = await getAgent({ id: agentId });
const before = await getAgent({ id: agentId }, {});
const duplicate = await updateAgent({ id: agentId }, { name: 'Idempotent' });

/** The suppressed path reports the unchanged version count as `version`. */
expect((duplicate as IAgent & { version?: number }).version).toBe(before!.versions!.length);

const after = await getAgent({ id: agentId });
const after = await getAgent({ id: agentId }, {});
expect(after!.name).toBe(before!.name);
expect(after!.tools).toEqual(before!.tools);
expect(after!.versions).toHaveLength(before!.versions!.length);
Expand Down Expand Up @@ -3454,7 +3454,7 @@ describe('Agent Methods', () => {

// Since we're updating back to the same model_parameters but with a different description,
// it should create a new version
const agent = await getAgent({ id: agentId });
const agent = await getAgent({ id: agentId }, {});
expect(agent!.versions).toHaveLength(3);
});

Expand Down Expand Up @@ -3622,7 +3622,7 @@ describe('Agent Methods', () => {
expect(repopulated?.versions).toHaveLength(3);

// Verify all versions have correct support_contact
const finalAgent = await getAgent({ id: agentId });
const finalAgent = await getAgent({ id: agentId }, {});
expect(finalAgent!.versions![0]?.support_contact).toEqual({
name: 'Support',
email: 'support@test.com',
Expand Down Expand Up @@ -3672,7 +3672,7 @@ describe('Agent Methods', () => {
},
);

const finalAgent = await getAgent({ id: agentId });
const finalAgent = await getAgent({ id: agentId }, {});

// Verify version history
expect(finalAgent!.versions).toHaveLength(3);
Expand Down Expand Up @@ -3831,7 +3831,7 @@ describe('Agent Methods', () => {
await updateAgent({ id: agentId }, { description: `Version ${i}` });
}

const agent = await getAgent({ id: agentId });
const agent = await getAgent({ id: agentId }, {});
expect(agent!.versions).toHaveLength(21);
expect(agent!.description).toBe('Version 19');
});
Expand Down Expand Up @@ -4717,7 +4717,7 @@ describe('Agent Methods', () => {

await updateAgent({ id: agentId }, { agent_ids: ['agent3'] });

const finalAgent = await getAgent({ id: agentId });
const finalAgent = await getAgent({ id: agentId }, {});

expect(finalAgent!.versions).toHaveLength(3);
expect(finalAgent!.versions![0]?.agent_ids).toEqual(['agent1']);
Expand Down
4 changes: 3 additions & 1 deletion packages/data-schemas/src/methods/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,10 +732,12 @@ export function createAgentMethods(

/**
* Get an agent document based on the provided search parameter.
* Without an explicit projection, the unbounded `versions` history is excluded;
* pass `{}` (or a projection including `versions`) to read the full history.
*/
async function getAgent(
searchParameter: FilterQuery<IAgent>,
projection?: ProjectionType<IAgent>,
projection: ProjectionType<IAgent> = { versions: 0 },
): Promise<IAgent | null> {
const Agent = mongoose.models.Agent as Model<IAgent>;
return await Agent.findOne(searchParameter, projection).lean<IAgent>();
Expand Down
Loading