This commit is contained in:
ehconitin 2026-04-20 23:54:53 +05:30
parent 3ca83f3a46
commit f9f21602aa
9 changed files with 70 additions and 49 deletions

View file

@ -0,0 +1,5 @@
// Public search tool ids shared across orchestration layers. The runtime can
// back these ids with provider-native or external implementations.
// one export per file -- to be cleaned after the strategy is clear
export const WEB_SEARCH_TOOL_ID = 'web_search';
export const X_SEARCH_TOOL_ID = 'x_search';

View file

@ -1,4 +0,0 @@
export const SEARCH_TOOL_NAMES = {
webSearch: 'web_search',
xSearch: 'x_search',
} as const;

View file

@ -9,7 +9,7 @@ jest.mock('ai', () => {
import { generateText, type ToolSet } from 'ai';
import { SEARCH_TOOL_NAMES } from 'src/engine/core-modules/tool-provider/constants/search-tool-names.const';
import { WEB_SEARCH_TOOL_ID } from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { type WebSearchService } from 'src/engine/core-modules/web-search/web-search.service';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@ -296,7 +296,7 @@ describe('AgentAsyncExecutorService', () => {
},
} as unknown as ToolSet;
const nativeModelTools = {
[SEARCH_TOOL_NAMES.webSearch]: {
[WEB_SEARCH_TOOL_ID]: {
description: 'Native search the web',
inputSchema: {},
execute: jest.fn(),

View file

@ -14,7 +14,7 @@ import { type Repository } from 'typeorm';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { SEARCH_TOOL_NAMES } from 'src/engine/core-modules/tool-provider/constants/search-tool-names.const';
import { WEB_SEARCH_TOOL_ID } from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
import { type ToolProviderAgent } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-agent.type';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { WebSearchService } from 'src/engine/core-modules/web-search/web-search.service';
@ -51,7 +51,7 @@ const toToolProviderAgent = (agent: AgentEntity): ToolProviderAgent => ({
const WORKFLOW_NO_ROLE_FALLBACK_TOOL_NAMES = [
'code_interpreter',
SEARCH_TOOL_NAMES.webSearch,
WEB_SEARCH_TOOL_ID,
] as const;
const WORKFLOW_NO_ROLE_FALLBACK_TOOL_NAMES_SET: ReadonlySet<string> = new Set(
WORKFLOW_NO_ROLE_FALLBACK_TOOL_NAMES,

View file

@ -1,6 +1,6 @@
import { type StepResult, type ToolSet } from 'ai';
import { SEARCH_TOOL_NAMES } from 'src/engine/core-modules/tool-provider/constants/search-tool-names.const';
import { WEB_SEARCH_TOOL_ID } from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
export const countNativeWebSearchCallsFromSteps = (
steps: StepResult<ToolSet>[],
@ -9,7 +9,7 @@ export const countNativeWebSearchCallsFromSteps = (
(count, step) =>
count +
step.toolCalls.filter(
(toolCall) => toolCall.toolName === SEARCH_TOOL_NAMES.webSearch,
(toolCall) => toolCall.toolName === WEB_SEARCH_TOOL_ID,
).length,
0,
);

View file

@ -1,6 +1,6 @@
import { type StepResult, type ToolSet } from 'ai';
import { SEARCH_TOOL_NAMES } from 'src/engine/core-modules/tool-provider/constants/search-tool-names.const';
import { X_SEARCH_TOOL_ID } from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
export const countNativeXSearchCallsFromSteps = (
steps: StepResult<ToolSet>[],
@ -9,7 +9,7 @@ export const countNativeXSearchCallsFromSteps = (
(count, step) =>
count +
step.toolCalls.filter(
(toolCall) => toolCall.toolName === SEARCH_TOOL_NAMES.xSearch,
(toolCall) => toolCall.toolName === X_SEARCH_TOOL_ID,
).length,
0,
);

View file

@ -23,7 +23,7 @@ import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
import { SEARCH_TOOL_NAMES } from 'src/engine/core-modules/tool-provider/constants/search-tool-names.const';
import { WEB_SEARCH_TOOL_ID } from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
import { wrapToolsWithOutputSerialization } from 'src/engine/core-modules/tool-provider/output-serialization/wrap-tools-with-output-serialization.util';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
@ -159,23 +159,18 @@ export class ChatExecutionService {
registeredModel.modelId,
);
const nativeSearchTools =
this.aiModelConfigService.getChatNativeSearchTools(registeredModel, {
useProviderNativeWebSearch: useNativeSearch,
});
const hasNativeWebSearch = Object.prototype.hasOwnProperty.call(
nativeSearchTools,
SEARCH_TOOL_NAMES.webSearch,
);
const hasNativeXSearch = Object.prototype.hasOwnProperty.call(
nativeSearchTools,
SEARCH_TOOL_NAMES.xSearch,
);
const {
tools: nativeSearchTools,
hasWebSearch: hasNativeWebSearch,
hasXSearch: hasNativeXSearch,
} = this.aiModelConfigService.getChatNativeSearchPlan(registeredModel, {
useProviderNativeWebSearch: useNativeSearch,
});
const toolNamesToPreload = [
...COMMON_PRELOAD_TOOLS,
...(!hasNativeWebSearch && externalWebSearchEnabled
? [SEARCH_TOOL_NAMES.webSearch]
? [WEB_SEARCH_TOOL_ID]
: []),
];
@ -196,10 +191,10 @@ export class ChatExecutionService {
...Object.keys(nativeSearchTools),
];
const excludedChatToolNames = hasNativeWebSearch
? new Set([SEARCH_TOOL_NAMES.webSearch])
? new Set([WEB_SEARCH_TOOL_ID])
: undefined;
const toolCatalogForPrompt = hasNativeWebSearch
? toolCatalog.filter((tool) => tool.name !== SEARCH_TOOL_NAMES.webSearch)
? toolCatalog.filter((tool) => tool.name !== WEB_SEARCH_TOOL_ID)
: toolCatalog;
// ToolSet is constant for the entire conversation — no mutation.

View file

@ -1,5 +1,9 @@
import { AiModelConfigService } from './ai-model-config.service';
import {
WEB_SEARCH_TOOL_ID,
X_SEARCH_TOOL_ID,
} from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
import {
AI_SDK_OPENAI,
AI_SDK_XAI,
@ -26,7 +30,7 @@ describe('AiModelConfigService', () => {
}),
});
const result = service.getChatNativeSearchTools(
const result = service.getChatNativeSearchPlan(
{
sdkPackage: AI_SDK_XAI,
providerName: 'xai',
@ -35,7 +39,11 @@ describe('AiModelConfigService', () => {
);
expect(result).toEqual({
x_search: xSearchTool,
tools: {
[X_SEARCH_TOOL_ID]: xSearchTool,
},
hasWebSearch: false,
hasXSearch: true,
});
});
@ -49,7 +57,7 @@ describe('AiModelConfigService', () => {
}),
});
const result = service.getChatNativeSearchTools(
const result = service.getChatNativeSearchPlan(
{
sdkPackage: AI_SDK_XAI,
providerName: 'xai',
@ -58,8 +66,12 @@ describe('AiModelConfigService', () => {
);
expect(result).toEqual({
web_search: webSearchTool,
x_search: xSearchTool,
tools: {
[WEB_SEARCH_TOOL_ID]: webSearchTool,
[X_SEARCH_TOOL_ID]: xSearchTool,
},
hasWebSearch: true,
hasXSearch: true,
});
});
@ -68,7 +80,7 @@ describe('AiModelConfigService', () => {
getRawOpenAIProvider: jest.fn(),
});
const result = service.getChatNativeSearchTools(
const result = service.getChatNativeSearchPlan(
{
sdkPackage: AI_SDK_OPENAI,
providerName: 'openai',
@ -76,6 +88,10 @@ describe('AiModelConfigService', () => {
{ useProviderNativeWebSearch: false },
);
expect(result).toEqual({});
expect(result).toEqual({
tools: {},
hasWebSearch: false,
hasXSearch: false,
});
});
});

View file

@ -5,7 +5,10 @@ import { ToolSet } from 'ai';
import { isAgentCapabilityEnabled } from 'twenty-shared/ai';
import { type ToolProviderAgent } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-agent.type';
import { SEARCH_TOOL_NAMES } from 'src/engine/core-modules/tool-provider/constants/search-tool-names.const';
import {
WEB_SEARCH_TOOL_ID,
X_SEARCH_TOOL_ID,
} from 'src/engine/core-modules/tool-provider/constants/search-tool-ids.const';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import {
AI_SDK_ANTHROPIC,
@ -17,6 +20,11 @@ import { type RegisteredAiModel } from 'src/engine/metadata-modules/ai/ai-models
import { SdkProviderFactoryService } from 'src/engine/metadata-modules/ai/ai-models/services/sdk-provider-factory.service';
type NativeSearchToolEntry = [string, ToolSet[string]];
type ChatNativeSearchPlan = {
tools: ToolSet;
hasWebSearch: boolean;
hasXSearch: boolean;
};
@Injectable()
export class AiModelConfigService {
@ -60,16 +68,22 @@ export class AiModelConfigService {
return Object.fromEntries(toolEntries) as ToolSet;
}
getChatNativeSearchTools(
getChatNativeSearchPlan(
model: RegisteredAiModel,
options: { useProviderNativeWebSearch: boolean },
): ToolSet {
): ChatNativeSearchPlan {
const toolEntries = this.getNativeSearchToolEntries(model, {
exposeWebSearch: options.useProviderNativeWebSearch,
exposeTwitterSearch: model.sdkPackage === AI_SDK_XAI,
});
return Object.fromEntries(toolEntries) as ToolSet;
return {
tools: Object.fromEntries(toolEntries) as ToolSet,
hasWebSearch: toolEntries.some(
([toolId]) => toolId === WEB_SEARCH_TOOL_ID,
),
hasXSearch: toolEntries.some(([toolId]) => toolId === X_SEARCH_TOOL_ID),
};
}
private getAnthropicProviderOptions(
@ -129,10 +143,7 @@ export class AiModelConfigService {
}
return [
[
SEARCH_TOOL_NAMES.webSearch,
anthropicProvider.tools.webSearch_20250305(),
],
[WEB_SEARCH_TOOL_ID, anthropicProvider.tools.webSearch_20250305()],
];
}
case AI_SDK_BEDROCK: {
@ -150,7 +161,7 @@ export class AiModelConfigService {
return [
[
SEARCH_TOOL_NAMES.webSearch,
WEB_SEARCH_TOOL_ID,
bedrockProvider.tools.webSearch_20250305() as ToolSet[string],
],
];
@ -168,9 +179,7 @@ export class AiModelConfigService {
return [];
}
return [
[SEARCH_TOOL_NAMES.webSearch, openAiProvider.tools.webSearch()],
];
return [[WEB_SEARCH_TOOL_ID, openAiProvider.tools.webSearch()]];
}
case AI_SDK_XAI: {
const xaiProvider = this.sdkProviderFactory.getRawXaiProvider(
@ -185,14 +194,14 @@ export class AiModelConfigService {
if (options.exposeWebSearch) {
toolEntries.push([
SEARCH_TOOL_NAMES.webSearch,
WEB_SEARCH_TOOL_ID,
xaiProvider.tools.webSearch() as ToolSet[string],
]);
}
if (options.exposeTwitterSearch) {
toolEntries.push([
SEARCH_TOOL_NAMES.xSearch,
X_SEARCH_TOOL_ID,
xaiProvider.tools.xSearch() as ToolSet[string],
]);
}