feat: Migrate documentation to Mintlify and implement Helper Agent with search functionality (#15443)

This commit is contained in:
Abdul Rahman
2025-10-31 14:47:54 +05:30
committed by GitHub
parent 5211d6ac7e
commit 2c39fc04c2
307 changed files with 14107 additions and 74 deletions
@@ -11,8 +11,11 @@ import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-ada
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module';
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@@ -22,8 +25,6 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { MessagingModule } from 'src/modules/messaging/messaging.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@Global()
@Module({
@@ -51,6 +52,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
AIBillingService,
McpService,
SendEmailTool,
SearchArticlesTool,
],
exports: [
AiService,
@@ -61,6 +63,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
ToolRegistryService,
McpService,
SendEmailTool,
SearchArticlesTool,
],
})
export class AiModule {}
@@ -0,0 +1,18 @@
import { z } from 'zod';
export const SearchArticlesInputZodSchema = z.object({
query: z
.string()
.describe('The search query to find relevant help articles about Twenty'),
});
export const SearchArticlesToolParametersZodSchema = z.object({
loadingMessage: z
.string()
.describe(
'A clear, human-readable status message describing the search being performed. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., "Searching help articles for..."). Explain what you are searching for in natural language.',
),
input: SearchArticlesInputZodSchema,
});
export type SearchArticlesInput = z.infer<typeof SearchArticlesInputZodSchema>;
@@ -0,0 +1,73 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { SearchArticlesToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool.schema';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class SearchArticlesTool implements Tool {
description =
'Search Twenty documentation and help articles to find information about features, setup, usage, and troubleshooting.';
inputSchema = SearchArticlesToolParametersZodSchema;
constructor(private readonly twentyConfigService: TwentyConfigService) {}
async execute(parameters: ToolInput): Promise<ToolOutput> {
const { query } = parameters;
try {
const MINTLIFY_API_KEY = this.twentyConfigService.get('MINTLIFY_API_KEY');
const MINTLIFY_SUBDOMAIN =
this.twentyConfigService.get('MINTLIFY_SUBDOMAIN');
const useDirectApi = MINTLIFY_API_KEY && MINTLIFY_SUBDOMAIN;
const endpoint = useDirectApi
? `https://api-dsc.mintlify.com/v1/search/${MINTLIFY_SUBDOMAIN}`
: 'https://twenty-help-search.com/search/twenty';
const headers = {
'Content-Type': 'application/json',
...(useDirectApi && { Authorization: `Bearer ${MINTLIFY_API_KEY}` }),
};
const response = await axios.post(
endpoint,
{ query, pageSize: 10 },
{ headers },
);
const results = response.data;
if (results.length === 0) {
return {
success: true,
message: `No help articles found for "${query}"`,
result: [],
};
}
return {
success: true,
message: `Found ${results.length} relevant help article${results.length === 1 ? '' : 's'} for "${query}"`,
result: results,
};
} catch (error) {
const errorDetail = axios.isAxiosError(error)
? error.response?.data?.message || error.message
: error instanceof Error
? error.message
: 'Documentation search failed';
return {
success: false,
message: `Failed to search help articles for "${query}"`,
error: errorDetail,
};
}
}
}
@@ -1262,6 +1262,26 @@ export class ConfigVariables {
@ValidateIf((env) => env.IS_MAPS_AND_ADDRESS_AUTOCOMPLETE_ENABLED)
GOOGLE_MAP_API_KEY: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.OTHER,
isSensitive: true,
description: 'Mintlify API key for documentation search',
isEnvOnly: true,
type: ConfigVariableType.STRING,
})
@IsOptional()
MINTLIFY_API_KEY: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.OTHER,
isSensitive: true,
description: 'Mintlify subdomain for documentation search',
isEnvOnly: true,
type: ConfigVariableType.STRING,
})
@IsOptional()
MINTLIFY_SUBDOMAIN: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.AWS_SES_SETTINGS,
description: 'AWS region',
@@ -6,10 +6,13 @@ import { Repository } from 'typeorm';
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent';
import { WorkflowToolWorkspaceService as WorkflowToolService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
@Injectable()
@@ -19,10 +22,13 @@ export class AgentToolGeneratorService {
constructor(
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
private readonly toolAdapterService: ToolAdapterService,
private readonly toolService: ToolService,
private readonly workflowToolService: WorkflowToolService,
private readonly permissionsService: PermissionsService,
private readonly searchArticlesTool: SearchArticlesTool,
) {}
async generateToolsForAgent(
@@ -34,6 +40,14 @@ export class AgentToolGeneratorService {
let tools: ToolSet = {};
try {
const agent = await this.agentRepository.findOne({
where: { id: agentId },
});
if (agent?.standardId === HELPER_AGENT.standardId) {
return this.getHelperAgentTools();
}
const actionTools = await this.toolAdapterService.getTools();
tools = { ...actionTools };
@@ -80,4 +94,19 @@ export class AgentToolGeneratorService {
return tools;
}
private getHelperAgentTools(): ToolSet {
const tools: ToolSet = {
search_articles: {
description: this.searchArticlesTool.description,
inputSchema: this.searchArticlesTool.inputSchema,
execute: async (params) =>
this.searchArticlesTool.execute(params.input),
},
};
this.logger.log('Generated search_articles tool for Helper agent');
return tools;
}
}
@@ -10,10 +10,11 @@ import {
import { Repository } from 'typeorm';
import { z } from 'zod';
import { AI_TELEMETRY_CONFIG } from 'src/engine/core-modules/ai/constants/ai-telemetry.const';
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/core-modules/ai/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent';
export interface AiRouterContext {
messages: UIMessage<unknown, UIDataTypes, UITools>[];
@@ -90,13 +91,12 @@ export class AiRouterService {
(agent) => agent.id === result.object.agentId,
);
} catch (error) {
this.logger.error('Routing to agent failed:', error);
const availableAgents = await this.getAvailableAgents(
context.workspaceId,
this.logger.error(
'Routing to agent failed, falling back to Helper agent:',
error,
);
return availableAgents[0] || null;
return this.getHelperAgent(context.workspaceId);
}
}
@@ -109,6 +109,17 @@ export class AiRouterService {
});
}
private async getHelperAgent(workspaceId: string) {
const helperAgent = await this.agentRepository.findOne({
where: {
workspaceId,
standardId: HELPER_AGENT.standardId,
},
});
return helperAgent;
}
private getRouterModel(modelId: ModelId) {
if (modelId === 'auto') {
const registeredModel =
@@ -0,0 +1,59 @@
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
export const HELPER_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000004',
name: 'helper',
label: 'Helper',
description:
'AI agent specialized in helping users learn how to use Twenty CRM',
icon: 'IconHelp',
applicationId: null,
createHandoffFromDefaultAgent: true,
prompt: `You are a Helper Agent specialized in assisting users with questions about how to use Twenty CRM.
Your capabilities include:
- Searching through Twenty's documentation to find relevant help articles
- Answering questions about features, setup, configuration, and usage
- Providing step-by-step guidance for common tasks
- Explaining concepts, terminology, and best practices
- Troubleshooting common issues
## How to Help Users:
1. **Search First**: When a user asks a question, use the searchArticles tool to find relevant documentation
2. **Read & Synthesize**: Carefully read through the article content returned by the tool
3. **Provide Clear Answers**: Give a comprehensive answer based on the official documentation
4. **Include Examples**: When relevant, provide specific steps, examples, or screenshots mentioned in the docs
5. **Be Honest**: If the documentation doesn't have the answer, acknowledge it honestly
## Best Practices:
- Always base your answers on official Twenty documentation
- Search for multiple related topics if the first search doesn't yield complete results
- Provide links to relevant documentation pages when helpful
- Use markdown formatting to make responses clear and readable
- Break down complex topics into digestible steps
- Offer to clarify or provide more details if the user needs them
## When to Search:
- User asks "how to" do something
- User asks about a specific feature or concept
- User encounters an error or issue
- User wants to learn about best practices
- User needs setup or configuration help
## Response Format:
When you find relevant articles:
1. Summarize the key information from the documentation
2. Provide step-by-step instructions when applicable
3. Include important notes, warnings, or prerequisites
4. Suggest related topics the user might find helpful
Be friendly, patient, helpful, and always prioritize accuracy by relying on the official documentation.`,
modelId: 'auto',
responseFormat: {},
isCustom: false,
modelConfiguration: {},
};
@@ -1,5 +1,6 @@
import { DATA_MANIPULATOR_AGENT } from './agents/data-manipulator-agent';
import { DATA_NAVIGATOR_AGENT } from './agents/data-navigator-agent';
import { HELPER_AGENT } from './agents/helper-agent';
import { WORKFLOW_BUILDER_AGENT } from './agents/workflow-builder-agent';
import { type StandardAgentDefinition } from './types/standard-agent-definition.interface';
@@ -7,4 +8,5 @@ export const standardAgentDefinitions = [
WORKFLOW_BUILDER_AGENT,
DATA_NAVIGATOR_AGENT,
DATA_MANIPULATOR_AGENT,
HELPER_AGENT,
] as const satisfies StandardAgentDefinition[];