mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-09 17:31:19 +00:00
🧩 feat: Enable Model Spec Subagents (#13598)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
This commit is contained in:
parent
cb6dbc8f60
commit
fb87abe773
13 changed files with 303 additions and 19 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import mongoose from 'mongoose';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||
import { Constants, FileSources } from 'librechat-data-provider';
|
||||
import { agentSchema, createMethods } from '@librechat/data-schemas';
|
||||
import { Constants, FileSources, MAX_SUBAGENTS } from 'librechat-data-provider';
|
||||
import type {
|
||||
Agent as LibreChatAgent,
|
||||
AgentModelParameters,
|
||||
|
|
@ -265,7 +265,9 @@ describe('loadAgent', () => {
|
|||
{
|
||||
req: {
|
||||
user: { id: 'user123' },
|
||||
body: {},
|
||||
body: {
|
||||
ephemeralAgent: { subagents: { enabled: false, agent_ids: ['agent_tampered'] } },
|
||||
},
|
||||
config: {
|
||||
config: {},
|
||||
fileStrategy: FileSources.local,
|
||||
|
|
@ -330,6 +332,124 @@ describe('loadAgent', () => {
|
|||
expect(result?.skills).toEqual([]);
|
||||
});
|
||||
|
||||
test('should apply subagent config for ephemeral model specs', async () => {
|
||||
const { EPHEMERAL_AGENT_ID } = Constants;
|
||||
const subagents = { enabled: true, allowSelf: true, agent_ids: [] };
|
||||
|
||||
const result = await loadAgent(
|
||||
{
|
||||
req: {
|
||||
user: { id: 'user123' },
|
||||
body: {},
|
||||
config: {
|
||||
config: {},
|
||||
fileStrategy: FileSources.local,
|
||||
imageOutputType: 'png',
|
||||
modelSpecs: {
|
||||
list: [
|
||||
{
|
||||
name: 'self-spawn',
|
||||
label: 'Self Spawn',
|
||||
preset: { endpoint: 'openai', model: 'gpt-4' },
|
||||
subagents,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
spec: 'self-spawn',
|
||||
agent_id: EPHEMERAL_AGENT_ID as string,
|
||||
endpoint: 'openai',
|
||||
model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters,
|
||||
},
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(result?.subagents).toEqual(subagents);
|
||||
});
|
||||
|
||||
test('should discard oversized request subagent ids for ephemeral agents', async () => {
|
||||
const { EPHEMERAL_AGENT_ID } = Constants;
|
||||
const oversized = Array.from({ length: MAX_SUBAGENTS + 1 }, (_, index) => `agent_${index}`);
|
||||
|
||||
const result = await loadAgent(
|
||||
{
|
||||
req: {
|
||||
user: { id: 'user123' },
|
||||
body: {
|
||||
ephemeralAgent: {
|
||||
subagents: { enabled: true, allowSelf: false, agent_ids: oversized },
|
||||
},
|
||||
},
|
||||
},
|
||||
agent_id: EPHEMERAL_AGENT_ID as string,
|
||||
endpoint: 'openai',
|
||||
model_parameters: { model: 'gpt-4' } as unknown as AgentModelParameters,
|
||||
},
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(result?.subagents).toEqual({ enabled: true, allowSelf: false });
|
||||
});
|
||||
|
||||
test('should preserve request subagents when added agent mirrors ephemeral primary tools', async () => {
|
||||
const { EPHEMERAL_AGENT_ID } = Constants;
|
||||
const subagents = { enabled: true, allowSelf: true, agent_ids: [] };
|
||||
|
||||
const result = await loadAddedAgent(
|
||||
{
|
||||
req: {
|
||||
user: { id: 'user123' },
|
||||
config: {
|
||||
config: {},
|
||||
fileStrategy: FileSources.local,
|
||||
imageOutputType: 'png',
|
||||
},
|
||||
},
|
||||
conversation: {
|
||||
endpoint: 'openai',
|
||||
model: 'gpt-4',
|
||||
ephemeralAgent: { subagents },
|
||||
} as unknown as TConversation,
|
||||
primaryAgent: { id: EPHEMERAL_AGENT_ID as string, tools: ['web_search'] } as LibreChatAgent,
|
||||
},
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(result?.tools).toEqual(['web_search']);
|
||||
expect(result?.subagents).toEqual(subagents);
|
||||
});
|
||||
|
||||
test('should discard oversized request subagent ids for mirrored added agents', async () => {
|
||||
const { EPHEMERAL_AGENT_ID } = Constants;
|
||||
const oversized = Array.from({ length: MAX_SUBAGENTS + 1 }, (_, index) => `agent_${index}`);
|
||||
|
||||
const result = await loadAddedAgent(
|
||||
{
|
||||
req: {
|
||||
user: { id: 'user123' },
|
||||
config: {
|
||||
config: {},
|
||||
fileStrategy: FileSources.local,
|
||||
imageOutputType: 'png',
|
||||
},
|
||||
},
|
||||
conversation: {
|
||||
endpoint: 'openai',
|
||||
model: 'gpt-4',
|
||||
ephemeralAgent: {
|
||||
subagents: { enabled: true, allowSelf: false, agent_ids: oversized },
|
||||
},
|
||||
} as unknown as TConversation,
|
||||
primaryAgent: { id: EPHEMERAL_AGENT_ID as string, tools: ['web_search'] } as LibreChatAgent,
|
||||
},
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(result?.tools).toEqual(['web_search']);
|
||||
expect(result?.subagents).toEqual({ enabled: true, allowSelf: false });
|
||||
});
|
||||
|
||||
test('should enable full skill scope for added ephemeral model spec with skills true', async () => {
|
||||
const result = await loadAddedAgent(
|
||||
{
|
||||
|
|
@ -398,8 +518,44 @@ describe('loadAgent', () => {
|
|||
expect(result?.skills).toEqual([]);
|
||||
});
|
||||
|
||||
test('should apply subagent config for added ephemeral model specs', async () => {
|
||||
const subagents = { enabled: true, allowSelf: true, agent_ids: [] };
|
||||
|
||||
const result = await loadAddedAgent(
|
||||
{
|
||||
req: {
|
||||
user: { id: 'user123' },
|
||||
config: {
|
||||
config: {},
|
||||
fileStrategy: FileSources.local,
|
||||
imageOutputType: 'png',
|
||||
modelSpecs: {
|
||||
list: [
|
||||
{
|
||||
name: 'added-self-spawn',
|
||||
label: 'Added Self Spawn',
|
||||
preset: { endpoint: 'openai', model: 'gpt-4' },
|
||||
subagents,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
conversation: {
|
||||
endpoint: 'openai',
|
||||
model: 'gpt-4',
|
||||
spec: 'added-self-spawn',
|
||||
} as unknown as TConversation,
|
||||
},
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(result?.subagents).toEqual(subagents);
|
||||
});
|
||||
|
||||
test('should apply model spec skills when added agent mirrors ephemeral primary tools', async () => {
|
||||
const { EPHEMERAL_AGENT_ID } = Constants;
|
||||
const subagents = { enabled: true, allowSelf: true, agent_ids: [] };
|
||||
|
||||
const result = await loadAddedAgent(
|
||||
{
|
||||
|
|
@ -416,6 +572,7 @@ describe('loadAgent', () => {
|
|||
label: 'Mirrored Scoped Skills',
|
||||
preset: { endpoint: 'openai', model: 'gpt-4' },
|
||||
skills: ['brand-writer'],
|
||||
subagents,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -434,6 +591,7 @@ describe('loadAgent', () => {
|
|||
expect(result?.tools).toEqual(['web_search']);
|
||||
expect(result?.skills_enabled).toBe(true);
|
||||
expect(result?.skills).toEqual([]);
|
||||
expect(result?.subagents).toEqual(subagents);
|
||||
});
|
||||
|
||||
test('should handle ephemeral agent with undefined ephemeralAgent in body', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import {
|
||||
Tools,
|
||||
Constants,
|
||||
|
|
@ -8,8 +7,15 @@ import {
|
|||
appendAgentIdSuffix,
|
||||
encodeEphemeralAgentId,
|
||||
} from 'librechat-data-provider';
|
||||
import type { Agent, TConversation, TModelSpec } from 'librechat-data-provider';
|
||||
import type {
|
||||
Agent,
|
||||
TConversation,
|
||||
TModelSpec,
|
||||
AgentSubagentsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import { getCustomEndpointConfig } from '~/app/config';
|
||||
import { sanitizeRequestSubagents } from './subagents';
|
||||
|
||||
const { mcp_all, mcp_delimiter } = Constants;
|
||||
|
||||
|
|
@ -34,6 +40,17 @@ function applyModelSpecSkills(
|
|||
}
|
||||
}
|
||||
|
||||
function applyModelSpecSubagents(
|
||||
result: Record<string, unknown>,
|
||||
modelSpec: Pick<TModelSpec, 'subagents'> | null | undefined,
|
||||
ephemeralAgent?: { subagents?: AgentSubagentsConfig },
|
||||
): void {
|
||||
const subagents = modelSpec?.subagents ?? sanitizeRequestSubagents(ephemeralAgent?.subagents);
|
||||
if (subagents) {
|
||||
result.subagents = subagents;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadAddedAgentDeps {
|
||||
getAgent: (searchParameter: { id: string }) => Promise<Agent | null>;
|
||||
getMCPServerTools: (
|
||||
|
|
@ -88,6 +105,7 @@ export async function loadAddedAgent(
|
|||
file_search?: boolean;
|
||||
web_search?: boolean;
|
||||
artifacts?: unknown;
|
||||
subagents?: AgentSubagentsConfig;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
|
@ -98,6 +116,16 @@ export async function loadAddedAgent(
|
|||
}
|
||||
|
||||
const appConfig = req.config as AppConfig | undefined;
|
||||
const ephemeralAgent = rest.ephemeralAgent as
|
||||
| {
|
||||
mcp?: string[];
|
||||
execute_code?: boolean;
|
||||
file_search?: boolean;
|
||||
web_search?: boolean;
|
||||
artifacts?: unknown;
|
||||
subagents?: AgentSubagentsConfig;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const primaryIsEphemeral = primaryAgent && isEphemeralAgentId(primaryAgent.id);
|
||||
if (primaryIsEphemeral && Array.isArray(primaryAgent.tools)) {
|
||||
|
|
@ -132,18 +160,10 @@ export async function loadAddedAgent(
|
|||
tools: [...primaryAgent.tools],
|
||||
};
|
||||
applyModelSpecSkills(result, modelSpec);
|
||||
applyModelSpecSubagents(result, modelSpec, ephemeralAgent);
|
||||
return result as unknown as Agent;
|
||||
}
|
||||
|
||||
const ephemeralAgent = rest.ephemeralAgent as
|
||||
| {
|
||||
mcp?: string[];
|
||||
execute_code?: boolean;
|
||||
file_search?: boolean;
|
||||
web_search?: boolean;
|
||||
artifacts?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
const mcpServers = new Set<string>(ephemeralAgent?.mcp);
|
||||
const userId = req.user?.id ?? '';
|
||||
|
||||
|
|
@ -234,6 +254,7 @@ export async function loadAddedAgent(
|
|||
if (ephemeralAgent?.artifacts != null && ephemeralAgent.artifacts) {
|
||||
result.artifacts = ephemeralAgent.artifacts;
|
||||
}
|
||||
applyModelSpecSubagents(result, modelSpec, ephemeralAgent);
|
||||
applyModelSpecSkills(result, modelSpec);
|
||||
|
||||
return result as unknown as Agent;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import {
|
||||
Tools,
|
||||
Constants,
|
||||
|
|
@ -13,7 +12,9 @@ import type {
|
|||
TModelSpec,
|
||||
Agent,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import { getCustomEndpointConfig } from '~/app/config';
|
||||
import { sanitizeRequestSubagents } from './subagents';
|
||||
|
||||
const { mcp_all, mcp_delimiter } = Constants;
|
||||
type ModelParametersWithPromptPrefix = AgentModelParameters & { promptPrefix?: string | null };
|
||||
|
|
@ -135,6 +136,14 @@ export async function loadEphemeralAgent(
|
|||
if (ephemeralAgent?.artifacts) {
|
||||
result.artifacts = ephemeralAgent.artifacts;
|
||||
}
|
||||
if (modelSpec?.subagents) {
|
||||
result.subagents = modelSpec.subagents;
|
||||
} else {
|
||||
const requestSubagents = sanitizeRequestSubagents(ephemeralAgent?.subagents);
|
||||
if (requestSubagents) {
|
||||
result.subagents = requestSubagents;
|
||||
}
|
||||
}
|
||||
if (modelSpec && Object.prototype.hasOwnProperty.call(modelSpec, 'skills')) {
|
||||
if (modelSpec.skills === true) {
|
||||
result.skills_enabled = true;
|
||||
|
|
|
|||
27
packages/api/src/agents/subagents.ts
Normal file
27
packages/api/src/agents/subagents.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { MAX_SUBAGENTS } from 'librechat-data-provider';
|
||||
import type { AgentSubagentsConfig } from 'librechat-data-provider';
|
||||
|
||||
export function sanitizeRequestSubagents(
|
||||
subagents?: AgentSubagentsConfig | null,
|
||||
): AgentSubagentsConfig | undefined {
|
||||
if (!subagents || typeof subagents !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sanitized: AgentSubagentsConfig = {};
|
||||
if (typeof subagents.enabled === 'boolean') {
|
||||
sanitized.enabled = subagents.enabled;
|
||||
}
|
||||
if (typeof subagents.allowSelf === 'boolean') {
|
||||
sanitized.allowSelf = subagents.allowSelf;
|
||||
}
|
||||
if (
|
||||
Array.isArray(subagents.agent_ids) &&
|
||||
subagents.agent_ids.length <= MAX_SUBAGENTS &&
|
||||
subagents.agent_ids.every((agentId) => typeof agentId === 'string')
|
||||
) {
|
||||
sanitized.agent_ids = subagents.agent_ids;
|
||||
}
|
||||
|
||||
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
|
||||
}
|
||||
|
|
@ -203,6 +203,21 @@ export function sanitizeModelSpecs<T extends Partial<TSpecsConfig> | null | unde
|
|||
const preset = modelSpec?.preset;
|
||||
const sanitizedModelSpec = { ...modelSpec };
|
||||
delete (sanitizedModelSpec as { skills?: unknown }).skills;
|
||||
const subagents = sanitizedModelSpec.subagents;
|
||||
if (subagents && typeof subagents === 'object') {
|
||||
const sanitizedSubagents: TModelSpec['subagents'] = {};
|
||||
if (subagents.enabled !== undefined) {
|
||||
sanitizedSubagents.enabled = subagents.enabled;
|
||||
}
|
||||
if (subagents.allowSelf !== undefined) {
|
||||
sanitizedSubagents.allowSelf = subagents.allowSelf;
|
||||
}
|
||||
if (Object.keys(sanitizedSubagents).length > 0) {
|
||||
sanitizedModelSpec.subagents = sanitizedSubagents;
|
||||
} else {
|
||||
delete sanitizedModelSpec.subagents;
|
||||
}
|
||||
}
|
||||
if (!preset || typeof preset !== 'object') {
|
||||
return sanitizedModelSpec;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ describe('modelSpecs helpers', () => {
|
|||
name: 'guarded-spec',
|
||||
label: 'Guarded Spec',
|
||||
skills: ['private-skill'],
|
||||
subagents: { enabled: true, allowSelf: true, agent_ids: ['agent_private'] },
|
||||
preset: {
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4o',
|
||||
|
|
@ -34,6 +35,10 @@ describe('modelSpecs helpers', () => {
|
|||
};
|
||||
|
||||
const sanitizedModelSpecs = sanitizeModelSpecs(modelSpecs);
|
||||
expect(sanitizedModelSpecs.list[0].subagents).toEqual({
|
||||
enabled: true,
|
||||
allowSelf: true,
|
||||
});
|
||||
expect(sanitizedModelSpecs.list[0].preset).toEqual({
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4o',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue