Skip to content

Commit adf2f74

Browse files
Update Slack message formatting
1 parent f59245d commit adf2f74

File tree

2 files changed

+217
-3
lines changed

2 files changed

+217
-3
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { llms } from '#agent/agentContextLocalStorage';
2+
import { convertMarkdownToMrkdwn } from './slackMessageFormatter';
3+
4+
/*
5+
https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data
6+
https://ai-sdk.dev/docs/reference/ai-sdk-core/json-schema
7+
https://docs.slack.dev/reference/block-kit/
8+
*/
9+
10+
// https://docs.slack.dev/reference/block-kit/blocks/markdown-block/
11+
// The markdown types that are not supported are code block with syntax highlighting, horizontal lines, tables, and task list.
12+
interface MarkdownBlock {
13+
type: 'markdown';
14+
text: string;
15+
}
16+
17+
// https://docs.slack.dev/reference/block-kit/blocks/divider-block/
18+
interface DividerBlock {
19+
type: 'divider';
20+
}
21+
22+
// https://docs.slack.dev/reference/block-kit/blocks/table-block
23+
interface TableBlock {
24+
type: 'table';
25+
/** An array consisting of table rows. Maximum 100 rows. Each row object is an array with a max of 20 table cells. Table cells can have a type of { type="raw_text", text=""} */
26+
rows: string[][];
27+
column_settings?: Array<{ align?: string; is_wrapped?: boolean }>;
28+
}
29+
30+
const SLACK_BLOCKS_SCHEMA = {
31+
type: 'object',
32+
properties: {
33+
blocks: {
34+
type: 'array',
35+
description: 'Array of Slack blocks',
36+
items: {
37+
type: 'object',
38+
properties: {
39+
type: {
40+
type: 'string',
41+
description: 'Block type: markdown, divider, or table',
42+
},
43+
text: {
44+
type: 'string',
45+
description: 'The markdown-formatted text content (for markdown blocks)',
46+
},
47+
rows: {
48+
type: 'array',
49+
description: 'Array of table rows (for table blocks)',
50+
items: {
51+
type: 'array',
52+
items: {
53+
type: 'string',
54+
},
55+
},
56+
},
57+
column_settings: {
58+
type: 'array',
59+
description: 'Optional column settings (for table blocks)',
60+
items: {
61+
type: 'object',
62+
properties: {
63+
align: {
64+
type: 'string',
65+
},
66+
is_wrapped: {
67+
type: 'boolean',
68+
},
69+
},
70+
},
71+
},
72+
},
73+
required: ['type'],
74+
},
75+
},
76+
},
77+
required: ['blocks'],
78+
};
79+
80+
interface SlackBlocks {
81+
blocks: Array<MarkdownBlock | DividerBlock | TableBlock>;
82+
}
83+
84+
const SLACK_MARKDOWN_FORMATTING_RULES = [
85+
'## Markdown Formatting Rules Overview',
86+
'1. **Bold** ',
87+
' - Use double asterisks (``**text**``) or double underscores (``__text__``). ',
88+
' - Example: ``**important**`` or ``__urgent__`` appears as **important**/**urgent** (visually bolded). ',
89+
'',
90+
'2. **Italic** ',
91+
' - Use single asterisks (``*text*``) or single underscores (``_text_``). ',
92+
' - Example: ``*note*`` or ``_caution_`` appears as *note*/*caution* (visually italicized). ',
93+
'',
94+
'3. **Bold + Italic** ',
95+
' - Nest italic inside bold: ``**bold with _emphasis_**`` ',
96+
' - *Alternatively*, use triple asterisks for combined effect: ``***critical***`` → ***critical*** (bold + italic). ',
97+
'',
98+
'4. **Links** ',
99+
' - Syntax: ``[display text](URL)`` ',
100+
' - Example: ``[Google](https://www.google.com)`` becomes a clickable link labeled "Google". ',
101+
'',
102+
'5. **Lists** ',
103+
' - **Unordered**: Start lines with ``- `` + space. ',
104+
' ```',
105+
' - Item one',
106+
' - Item two',
107+
' - Item three',
108+
' ``` ',
109+
' - **Ordered**: Start lines with ``1. ``, ``2. ``, etc. + space. ',
110+
' ```',
111+
' 1. First step',
112+
' 2. Second step',
113+
' 3. Third step',
114+
' ``` ',
115+
'',
116+
'6. **Strikethrough** ',
117+
' - Use double tildes: ``~~deleted text~~`` → appears with a strikethrough line. ',
118+
'',
119+
'7. **Headers** ',
120+
' - ``# Header`` → Level 1 (largest/bold) ',
121+
' - ``## Header`` → Level 2 (bold) ',
122+
' - ``### Header`` → Level 3 (bold, smaller), etc. ',
123+
'',
124+
'8. **Inline Code** ',
125+
' - Wrap code in single backticks: `` `print("hello")` `` → displays as monospace font. ',
126+
'',
127+
'9. **Block Quotes** ',
128+
' - Start with ``> `` + space: ``> This is a quote`` → appears indented as a quote block. ',
129+
'',
130+
'10. **Code Blocks** ',
131+
' - Wrap multi-line code in triple backticks: ',
132+
' ```',
133+
' ```',
134+
' line one',
135+
' line two',
136+
' ```',
137+
' ``` ',
138+
' - Appears as a formatted code block (monospace, preserved whitespace). ',
139+
'',
140+
'11. **Images** ',
141+
' - Syntax: ``![alt text](image_URL)`` ',
142+
' - Example: ``![Logo](https://example.com/logo.png)`` → displays image with "Logo" as alt text. ',
143+
'',
144+
'---',
145+
'',
146+
'### **Escaping Special Characters** ',
147+
'To display literal punctuation (instead of triggering formatting), prefix with ``\\\\``: ',
148+
'- ``\\\\*`` → shows ``*`` (not italic) ',
149+
'- ``\\\\_`` → shows ``_`` (not underscore) ',
150+
'- ``\\\\\\\\`` → shows ``\\\\`` ',
151+
'- Other escapable characters: `` ` ``, ``{``, ``}``, ``[``, ``]``, ``(``, ``)``, ``#``, ``+``, ``-``, ``.``, ``!``, ``&``. ',
152+
' Example: ``\\\\# not a header`` → displays ``# not a header``. ',
153+
'',
154+
'---',
155+
'',
156+
'### **Key Notes for Usage** ',
157+
'- Always separate list items/punctuation with spaces (e.g., ``- `` not ``-``). ',
158+
'- Headers must start at the beginning of a line (no leading spaces). ',
159+
'- Code blocks require **three backticks** on separate lines above/below the code. ',
160+
'- Avoid nested markdown complexity (e.g., mixing ```***bold-italic***``` with links may render inconsistently). ',
161+
'- Escaping is required only for characters that *start* a formatting rule (e.g., ``\\\\*`` needed but ``text*`` is safe). ',
162+
'',
163+
'--- ',
164+
].join('\n');
165+
166+
/**
167+
* Formats markdown to Slack blocks, using markdown blocks, table blocks and divider blocks, as the Slack markdown doesn't support code block with syntax highlighting, horizontal lines, tables, and task list.
168+
* @param message
169+
*/
170+
export async function formatAsSlackBlocks(markdown: string): Promise<SlackBlocks> {
171+
const prompt = `<message>${markdown}</message>\n\nYou are a Slack block formatter. Convert the message text/markdown to Slack blocks.
172+
173+
<formatting-rules>
174+
${SLACK_MARKDOWN_FORMATTING_RULES}
175+
176+
Slack markdown doesn't support code block with syntax highlighting, horizontal lines, tables, and task list.
177+
Horizontal lines must be converted to divider blocks.
178+
Tables must be converted to table blocks.
179+
Code blocks must have the language type stripped in the Markdown.
180+
</formatting-rules>
181+
182+
<response-format>
183+
interface MarkdownBlock {
184+
type: 'markdown';
185+
text: string;
186+
}
187+
188+
interface DividerBlock {
189+
type: 'divider';
190+
}
191+
192+
interface TableBlock {
193+
type: 'table';
194+
/** An array consisting of table rows. Maximum 100 rows. Each row object is an array with a max of 20 table cells. Table cells can have a type of { type="raw_text", text=""} */
195+
rows: string[][];
196+
/**
197+
* Optional column settings
198+
* align: The alignment for items in this column. Can be left, center, or right. Defaults to left if not defined.
199+
* is_wrapped: Whether the column should be wrapped. Defaults to false if not defined.
200+
*/
201+
column_settings?: Array<{ align?: string, is_wrapped?: boolean }>
202+
}
203+
204+
Return only a JSON object matching the type
205+
{
206+
blocks: Array<MarkdownBlock | DividerBlock | TableBlock>
207+
}
208+
</response-format>`;
209+
const blocks: SlackBlocks = await llms().easy.generateJson(prompt, { jsonSchema: SLACK_BLOCKS_SCHEMA, id: ' Markdown block formatter', temperature: 0 });
210+
for (const block of blocks.blocks) if (block.type === 'markdown') block.text = convertMarkdownToMrkdwn(block.text);
211+
return blocks;
212+
}

src/modules/slack/slackChatBotService.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { getLastFunctionCallArg } from '#agent/autonomous/agentCompletion';
55
import { resumeCompletedWithUpdatedUserRequest, startAgent } from '#agent/autonomous/autonomousAgentRunner';
66
import { appContext } from '#app/applicationContext';
77
import { GoogleCloud } from '#functions/cloud/google/google-cloud';
8+
import { Confluence } from '#functions/confluence';
89
import { Jira } from '#functions/jira';
910
import { LlmTools } from '#functions/llmTools';
1011
import { GitLab } from '#functions/scm/gitlab';
1112
import { Perplexity } from '#functions/web/perplexity';
13+
import { PublicWeb } from '#functions/web/web';
1214
import { defaultLLMs } from '#llm/services/defaultLlms';
1315
import { logger } from '#o11y/logger';
1416
import { getAgentUser } from '#routes/webhooks/webhookAgentUser';
@@ -18,12 +20,12 @@ import { runAsUser } from '#user/userContext';
1820
import type { ChatBotService } from '../../chatBot/chatBotService';
1921
import { SupportKnowledgebase } from '../../functions/supportKnowledgebase';
2022
import { SlackAPI } from './slackApi';
23+
import { formatAsSlackBlocks } from './slackBlockFormatter';
2124
import { slackConfig } from './slackConfig';
22-
import { textToBlocks } from './slackMessageFormatter';
2325

2426
let slackApp: App<StringIndexed> | undefined;
2527

26-
const CHATBOT_FUNCTIONS: Array<new () => any> = [GitLab, GoogleCloud, Perplexity, LlmTools, Jira];
28+
const CHATBOT_FUNCTIONS: Array<new () => any> = [GitLab, GoogleCloud, PublicWeb, Perplexity, LlmTools, Jira, Confluence];
2729

2830
/*
2931
There's a few steps involved with spotting a thread and then understanding the context of a message within it. Let's unspool them:
@@ -101,7 +103,7 @@ export class SlackChatBotService implements ChatBotService, AgentCompleted {
101103
const params: any = {
102104
channel: agent.metadata.slack.channel,
103105
thread_ts: agent.metadata.slack.thread_ts,
104-
blocks: textToBlocks(message),
106+
blocks: await formatAsSlackBlocks(message),
105107
text: message,
106108
};
107109

0 commit comments

Comments
 (0)