feat(ai): add markdown in ai chat (#13402)

This commit is contained in:
Antoine Moreaux 2025-07-29 17:13:00 +02:00 committed by GitHub
parent cbf731dba7
commit 40f529f1ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 195 additions and 20 deletions

View file

@ -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:*"

View file

@ -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 <LazyMarkdownRenderer text={text} />;
};
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 !== '') {

View file

@ -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 }) => (
<Markdown remarkPlugins={[remarkGfm]}>{children}</Markdown>
),
};
});
const StyledSkeletonContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const LoadingSkeleton = () => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={theme.border.radius.sm}
>
<StyledSkeletonContainer>
<Skeleton
width="70%"
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
/>
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
<Skeleton
width="90%"
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
<Skeleton
width="85%"
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
<Skeleton
width="80%"
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
</StyledSkeletonContainer>
</SkeletonTheme>
);
};
export const LazyMarkdownRenderer = ({ text }: { text: string }) => {
return (
<Suspense fallback={<LoadingSkeleton />}>
<MarkdownRenderer>{text}</MarkdownRenderer>
</Suspense>
);
};

View file

@ -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}`,
}),
};
});
},
),

View file

@ -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: [

View file

@ -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.`,
};

105
yarn.lock
View file

@ -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:*"