feat: Migrate documentation to Mintlify and implement Helper Agent with search functionality (#15443)
This commit is contained in:
@@ -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 {}
|
||||
|
||||
+18
@@ -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>;
|
||||
+73
@@ -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',
|
||||
|
||||
+29
@@ -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 =
|
||||
|
||||
+59
@@ -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: {},
|
||||
};
|
||||
+2
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user