fix(telegram): wrap markdown pipe tables in fenced code blocks
Telegram does not support markdown pipe tables. Detect pipe tables (header + separator + body rows) in both the MarkdownV2 and HTML parsers and wrap them in fenced code blocks so they render with monospace alignment. Also adds a kernel identity instruction telling the LLM to avoid pipe tables in Telegram output.
This commit is contained in:
parent
6e1fab80e2
commit
f54e42b9bd
5 changed files with 64 additions and 2 deletions
|
|
@ -116,7 +116,7 @@ func (cb *ContextBuilder) getIdentity() string {
|
||||||
version := config.FormatVersion()
|
version := config.FormatVersion()
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
`# picoclaw 🦞 (%s)
|
`# picoclaw 🦞 (%s)
|
||||||
|
|
||||||
You are picoclaw, a helpful AI assistant.
|
You are picoclaw, a helpful AI assistant.
|
||||||
|
|
||||||
|
|
@ -134,7 +134,9 @@ Your workspace is at: %s
|
||||||
|
|
||||||
3. **Memory** - When interacting with me if something seems memorable, update %s/memory/MEMORY.md
|
3. **Memory** - When interacting with me if something seems memorable, update %s/memory/MEMORY.md
|
||||||
|
|
||||||
4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.`,
|
4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.
|
||||||
|
|
||||||
|
5. **Telegram output** — Telegram does not support markdown tables. If you need to present tabular data, use a fenced code block or a structured list instead. Markdown pipe tables will be automatically wrapped in code blocks, but it is better to avoid them entirely.`,
|
||||||
version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath)
|
version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,22 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// reTable matches a markdown pipe table (header line + separator line + at least one body row).
|
||||||
|
// The (?m) flag makes ^ match at line boundaries.
|
||||||
|
// Trailing newline(s) are deliberately excluded from the match so that blank lines
|
||||||
|
// between the table and following content are preserved.
|
||||||
|
var reTable = regexp.MustCompile(`(?m)^(\|[^\n]+\|\r?\n\|[-:\|\s]+\|\r?\n(?:\|[^\n]+\|\r?\n?)+)`)
|
||||||
|
|
||||||
|
// wrapTablesInCodeBlocks wraps each pipe table in a fenced code block so that
|
||||||
|
// Telegram renders it with monospace alignment. This must run before any entity
|
||||||
|
// processing so the ``` delimiters are recognized as code blocks.
|
||||||
|
func wrapTablesInCodeBlocks(text string) string {
|
||||||
|
return reTable.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
match = strings.TrimRight(match, "\r\n")
|
||||||
|
return "```\n" + match + "\n```"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// mdV2SpecialChars are all characters that must be escaped in Telegram MarkdownV2
|
// mdV2SpecialChars are all characters that must be escaped in Telegram MarkdownV2
|
||||||
var mdV2SpecialChars = map[rune]bool{
|
var mdV2SpecialChars = map[rune]bool{
|
||||||
'*': true,
|
'*': true,
|
||||||
|
|
@ -89,6 +105,9 @@ var verbatimEntities = map[string]bool{
|
||||||
//
|
//
|
||||||
// Reference: https://core.telegram.org/bots/api#formatting-options
|
// Reference: https://core.telegram.org/bots/api#formatting-options
|
||||||
func markdownToTelegramMarkdownV2(text string) string {
|
func markdownToTelegramMarkdownV2(text string) string {
|
||||||
|
// 0. Wrap pipe tables in fenced code blocks so Telegram renders them legibly.
|
||||||
|
text = wrapTablesInCodeBlocks(text)
|
||||||
|
|
||||||
// 1. Convert Markdown headings → *escaped heading text*
|
// 1. Convert Markdown headings → *escaped heading text*
|
||||||
text = reHeading.ReplaceAllStringFunc(text, func(match string) string {
|
text = reHeading.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
sub := reHeading.FindStringSubmatch(match)
|
sub := reHeading.FindStringSubmatch(match)
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,34 @@ func Test_markdownToTelegramMarkdownV2(t *testing.T) {
|
||||||
input: "<Market Capitalization>",
|
input: "<Market Capitalization>",
|
||||||
expected: "\\<Market Capitalization\\>",
|
expected: "\\<Market Capitalization\\>",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "pipe table is wrapped in code block",
|
||||||
|
input: "| Header 1 | Header 2 |\n" +
|
||||||
|
"|----------|----------|\n" +
|
||||||
|
"| Cell 1 | Cell 2 |\n" +
|
||||||
|
"| Cell 3 | Cell 4 |",
|
||||||
|
expected: "```\n" +
|
||||||
|
"| Header 1 | Header 2 |\n" +
|
||||||
|
"|----------|----------|\n" +
|
||||||
|
"| Cell 1 | Cell 2 |\n" +
|
||||||
|
"| Cell 3 | Cell 4 |\n" +
|
||||||
|
"```",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipe table surrounded by text",
|
||||||
|
input: "Some text\n\n" +
|
||||||
|
"| A | B |\n" +
|
||||||
|
"|---|---|\n" +
|
||||||
|
"| 1 | 2 |\n\n" +
|
||||||
|
"More text",
|
||||||
|
expected: "Some text\n\n" +
|
||||||
|
"```\n" +
|
||||||
|
"| A | B |\n" +
|
||||||
|
"|---|---|\n" +
|
||||||
|
"| 1 | 2 |\n" +
|
||||||
|
"```\n" +
|
||||||
|
"More text",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ func markdownToTelegramHTML(text string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap pipe tables in fenced code blocks so Telegram renders them legibly.
|
||||||
|
text = wrapTablesInCodeBlocks(text)
|
||||||
|
|
||||||
codeBlocks := extractCodeBlocks(text)
|
codeBlocks := extractCodeBlocks(text)
|
||||||
text = codeBlocks.text
|
text = codeBlocks.text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,16 @@ func Test_markdownToTelegramHTML(t *testing.T) {
|
||||||
input: "```json\n{\n \"path\": \"README.md\"\n}\n```",
|
input: "```json\n{\n \"path\": \"README.md\"\n}\n```",
|
||||||
expected: "<pre><code>{\n \"path\": \"README.md\"\n}\n</code></pre>",
|
expected: "<pre><code>{\n \"path\": \"README.md\"\n}\n</code></pre>",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "pipe table is wrapped in code block",
|
||||||
|
input: "| Header 1 | Header 2 |\n" +
|
||||||
|
"|----------|----------|\n" +
|
||||||
|
"| Cell 1 | Cell 2 |",
|
||||||
|
expected: "<pre><code>| Header 1 | Header 2 |\n" +
|
||||||
|
"|----------|----------|\n" +
|
||||||
|
"| Cell 1 | Cell 2 |\n" +
|
||||||
|
"</code></pre>",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue