|
| 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: ```` ', |
| 142 | + ' - Example: ```` → 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 | +} |
0 commit comments