From 40f529f1ac8faa72f849c47ace389cfebf2f52d1 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Tue, 29 Jul 2025 17:13:00 +0200 Subject: [PATCH] feat(ai): add markdown in ai chat (#13402) --- packages/twenty-front/package.json | 2 + .../modules/ai/components/AIChatMessage.tsx | 9 +- .../ai/components/LazyMarkdownRenderer.tsx | 68 ++++++++++++ .../agent/agent-execution.service.ts | 22 +++- .../metadata-modules/agent/agent.module.ts | 2 + .../constants/agent-system-prompts.const.ts | 7 ++ yarn.lock | 105 +++++++++++++++--- 7 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index d77cf8e0372..0600f18ff60 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -56,7 +56,9 @@ "docx": "^9.1.0", "file-saver": "^2.0.5", "input-otp": "^1.4.2", + "react-markdown": "^10.1.0", "react-qr-code": "^2.0.18", + "remark-gfm": "^4.0.1", "transliteration": "^2.3.5", "twenty-shared": "workspace:*", "twenty-ui": "workspace:*" diff --git a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx index 3cf0d370a55..16fb7f34237 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx @@ -5,6 +5,7 @@ import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display'; import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; import { AgentChatFilePreview } from '@/ai/components/internal/AgentChatFilePreview'; import { AgentChatMessageRole } from '@/ai/constants/agent-chat-message-role'; +import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer'; import { AgentChatMessage } from '~/generated/graphql'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; @@ -124,13 +125,17 @@ export const AIChatMessage = ({ }) => { const theme = useTheme(); + const markdownRender = (text: string) => { + return ; + }; + const getAssistantMessageContent = (message: AgentChatMessage) => { if (message.content !== '') { - return message.content; + return markdownRender(message.content); } if (agentStreamingMessage.streamingText !== '') { - return agentStreamingMessage.streamingText; + return markdownRender(agentStreamingMessage.streamingText); } if (agentStreamingMessage.toolCall !== '') { diff --git a/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx b/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx new file mode 100644 index 00000000000..5142d09328d --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx @@ -0,0 +1,68 @@ +import { lazy, Suspense } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; + +const MarkdownRenderer = lazy(async () => { + const [{ default: Markdown }, { default: remarkGfm }] = await Promise.all([ + import('react-markdown'), + import('remark-gfm'), + ]); + + return { + default: ({ children }: { children: string }) => ( + {children} + ), + }; +}); + +const StyledSkeletonContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const LoadingSkeleton = () => { + const theme = useTheme(); + + return ( + + + + + + + + + + + + + ); +}; + +export const LazyMarkdownRenderer = ({ text }: { text: string }) => { + return ( + }> + {text} + + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts index f008bec09ec..6ebfeb1ff4e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts @@ -41,6 +41,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global. import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { AgentEntity } from './agent.entity'; import { AgentException, AgentExceptionCode } from './agent.exception'; @@ -62,6 +63,7 @@ export class AgentExecutionService { private readonly twentyConfigService: TwentyConfigService, private readonly agentToolService: AgentToolService, private readonly fileService: FileService, + private readonly domainManagerService: DomainManagerService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly aiModelRegistryService: AiModelRegistryService, @@ -245,7 +247,7 @@ export class AgentExecutionService { const contextObject = ( await Promise.all( recordIdsByObjectMetadataNameSingular.map( - (recordsWithObjectMetadataNameSingular) => { + async (recordsWithObjectMetadataNameSingular) => { if (recordsWithObjectMetadataNameSingular.recordIds.length === 0) { return []; } @@ -256,10 +258,20 @@ export class AgentExecutionService { roleId, ); - return repository.find({ - where: { - id: In(recordsWithObjectMetadataNameSingular.recordIds), - }, + return ( + await repository.find({ + where: { + id: In(recordsWithObjectMetadataNameSingular.recordIds), + }, + }) + ).map((record) => { + return { + ...record, + resourceUrl: this.domainManagerService.buildWorkspaceURL({ + workspace, + pathname: `object/${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}/${record.id}`, + }), + }; }); }, ), diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts index edaae25b35c..713a55b25d4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts @@ -16,6 +16,7 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { AgentChatMessageEntity } from './agent-chat-message.entity'; import { AgentChatThreadEntity } from './agent-chat-thread.entity'; @@ -53,6 +54,7 @@ import { AgentService } from './agent.service'; WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, TokenModule, + DomainManagerModule, ], controllers: [AgentChatController], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts b/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts index 58ee569f648..a3ca58b693e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts @@ -56,5 +56,12 @@ Permissions and capabilities: If you need more information to answer a question, ask follow-up questions. Always be transparent about your capabilities and limitations. +When formatting responses: +- Use markdown syntax to improve readability of long responses +- Add appropriate headings, lists, bold/italic text where it enhances understanding +- Include code blocks with proper language tags when showing code examples +- Create tables when presenting structured data +- Use blockquotes for important notes or callouts + Note: This base system prompt will be combined with the agent's specific instructions and context to provide you with complete guidance for your role.`, }; diff --git a/yarn.lock b/yarn.lock index 3de9a184068..f44fdbb1fb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38716,6 +38716,29 @@ __metadata: languageName: node linkType: hard +"hast-util-to-jsx-runtime@npm:^2.0.0": + version: 2.3.6 + resolution: "hast-util-to-jsx-runtime@npm:2.3.6" + dependencies: + "@types/estree": "npm:^1.0.0" + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + comma-separated-tokens: "npm:^2.0.0" + devlop: "npm:^1.0.0" + estree-util-is-identifier-name: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + mdast-util-mdx-expression: "npm:^2.0.0" + mdast-util-mdx-jsx: "npm:^3.0.0" + mdast-util-mdxjs-esm: "npm:^2.0.0" + property-information: "npm:^7.0.0" + space-separated-tokens: "npm:^2.0.0" + style-to-js: "npm:^1.0.0" + unist-util-position: "npm:^5.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10c0/27297e02848fe37ef219be04a26ce708d17278a175a807689e94a821dcffc88aa506d62c3a85beed1f9a8544f7211bdcbcde0528b7b456a57c2e342c3fd11056 + languageName: node + linkType: hard + "hast-util-to-mdast@npm:^10.0.0": version: 10.1.2 resolution: "hast-util-to-mdast@npm:10.1.2" @@ -39081,6 +39104,13 @@ __metadata: languageName: node linkType: hard +"html-url-attributes@npm:^3.0.0": + version: 3.0.1 + resolution: "html-url-attributes@npm:3.0.1" + checksum: 10c0/496e4908aa8b77665f348b4b03521901794f648b8ac34a581022cd6f2c97934d5c910cd91bc6593bbf2994687549037bc2520fcdc769b31484f29ffdd402acd0 + languageName: node + linkType: hard + "html-void-elements@npm:^2.0.0": version: 2.0.1 resolution: "html-void-elements@npm:2.0.1" @@ -39712,6 +39742,13 @@ __metadata: languageName: node linkType: hard +"inline-style-parser@npm:0.2.4": + version: 0.2.4 + resolution: "inline-style-parser@npm:0.2.4" + checksum: 10c0/ddc0b210eaa03e0f98d677b9836242c583c7c6051e84ce0e704ae4626e7871c5b78f8e30853480218b446355745775df318d4f82d33087ff7e393245efa9a881 + languageName: node + linkType: hard + "inline-style-prefixer@npm:^7.0.1": version: 7.0.1 resolution: "inline-style-prefixer@npm:7.0.1" @@ -51278,6 +51315,28 @@ __metadata: languageName: node linkType: hard +"react-markdown@npm:^10.1.0": + version: 10.1.0 + resolution: "react-markdown@npm:10.1.0" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + hast-util-to-jsx-runtime: "npm:^2.0.0" + html-url-attributes: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.0.0" + unified: "npm:^11.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + peerDependencies: + "@types/react": ">=18" + react: ">=18" + checksum: 10c0/4a5dc7d15ca6d05e9ee95318c1904f83b111a76f7588c44f50f1d54d4c97193b84e4f64c4b592057c989228238a2590306cedd0c4d398e75da49262b2b5ae1bf + languageName: node + linkType: hard + "react-number-format@npm:^5.3.1": version: 5.4.0 resolution: "react-number-format@npm:5.4.0" @@ -52532,6 +52591,19 @@ __metadata: languageName: node linkType: hard +"remark-rehype@npm:^11.0.0, remark-rehype@npm:^11.1.1": + version: 11.1.2 + resolution: "remark-rehype@npm:11.1.2" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + mdast-util-to-hast: "npm:^13.0.0" + unified: "npm:^11.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/f9eccacfb596d9605581dc05bfad28635d6ded5dd0a18e88af5fd4df0d3fcf9612e1501d4513bc2164d833cfe9636dab20400080b09e53f155c6e1442a1231fb + languageName: node + linkType: hard + "remark-rehype@npm:^11.1.0": version: 11.1.1 resolution: "remark-rehype@npm:11.1.1" @@ -52545,19 +52617,6 @@ __metadata: languageName: node linkType: hard -"remark-rehype@npm:^11.1.1": - version: 11.1.2 - resolution: "remark-rehype@npm:11.1.2" - dependencies: - "@types/hast": "npm:^3.0.0" - "@types/mdast": "npm:^4.0.0" - mdast-util-to-hast: "npm:^13.0.0" - unified: "npm:^11.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/f9eccacfb596d9605581dc05bfad28635d6ded5dd0a18e88af5fd4df0d3fcf9612e1501d4513bc2164d833cfe9636dab20400080b09e53f155c6e1442a1231fb - languageName: node - linkType: hard - "remark-slug@npm:^6.0.0": version: 6.1.0 resolution: "remark-slug@npm:6.1.0" @@ -55444,6 +55503,24 @@ __metadata: languageName: node linkType: hard +"style-to-js@npm:^1.0.0": + version: 1.1.17 + resolution: "style-to-js@npm:1.1.17" + dependencies: + style-to-object: "npm:1.0.9" + checksum: 10c0/429b9d5593a238d73761324e2c12f75b238f6964e12e4ecf7ea02b44c0ec1940b45c1c1fa8fac9a58637b753aa3ce973a2413b2b6da679584117f27a79e33ba3 + languageName: node + linkType: hard + +"style-to-object@npm:1.0.9": + version: 1.0.9 + resolution: "style-to-object@npm:1.0.9" + dependencies: + inline-style-parser: "npm:0.2.4" + checksum: 10c0/acc89a291ac348a57fa1d00b8eb39973ea15a6c7d7fe4b11339ea0be3b84acea3670c98aa22e166be20ca3d67e12f68f83cf114dde9d43ebb692593e859a804f + languageName: node + linkType: hard + "style-to-object@npm:^0.4.1": version: 0.4.4 resolution: "style-to-object@npm:0.4.4" @@ -57057,7 +57134,9 @@ __metadata: file-saver: "npm:^2.0.5" input-otp: "npm:^1.4.2" optionator: "npm:^0.9.1" + react-markdown: "npm:^10.1.0" react-qr-code: "npm:^2.0.18" + remark-gfm: "npm:^4.0.1" rollup-plugin-visualizer: "npm:^5.14.0" transliteration: "npm:^2.3.5" twenty-shared: "workspace:*"