import re from yuxi.channel.extensions.slack.constants import ( SLACK_AGENT_PROMPT_RULES, SLACK_MENTION_PATTERN, SLACK_TEXT_LIMIT, ) _LINK_PATTERN = re.compile(r"\[([^\]]*)\]\(([^)]*)\)") _BOLD_PATTERN = re.compile(r"\*\*(.+?)\*\*") _ITALIC_PATTERN = re.compile(r"(?]+> """, re.VERBOSE, ) def _find_safe_split_position(text: str, pos: int) -> int: for match in _FORMAT_TOKEN_PATTERN.finditer(text): start, end = match.start(), match.end() if start < pos < end: before_len = pos - start after_len = end - pos return start if before_len <= after_len else end return pos def markdown_to_slack(text: str) -> str: text = _LINK_PATTERN.sub(r"<\2|\1>", text) text = _BOLD_PATTERN.sub(r"*\1*", text) text = _ITALIC_PATTERN.sub(r"_\1_", text) text = _STRIKETHROUGH_PATTERN.sub(r"~\1~", text) text = _HEADING_PATTERN.sub(r"*\1*", text) lines = text.split("\n") result: list[str] = [] table_buffer: list[str] = [] in_table = False for line in lines: stripped = line.strip() if stripped.startswith("|") and stripped.endswith("|"): if not in_table: in_table = True table_buffer = [] table_buffer.append(stripped) continue else: if in_table: in_table = False result.extend(_convert_table_rows(table_buffer)) table_buffer = [] result.append(line) if in_table and table_buffer: result.extend(_convert_table_rows(table_buffer)) return "\n".join(result) def _convert_table_rows(rows: list[str]) -> list[str]: if len(rows) < 2: return rows def _parse_cells(row: str) -> list[str]: return [cell.strip() for cell in row.strip("|").split("|")] header = _parse_cells(rows[0]) separator = _parse_cells(rows[1]) body_rows = rows[2:] is_sep = all(re.fullmatch(r"[-:]{3,}", cell) or re.fullmatch(r":[-:]+:", cell) for cell in separator) if not is_sep: all_rows = [_parse_cells(r) for r in rows] else: all_rows = [_parse_cells(r) for r in body_rows] if not header: return [", ".join(cells) for cells in all_rows] bullet_lines: list[str] = [] for cells in all_rows: pairs = [f"{header[i]}: {cells[i]}" if i < len(cells) else f"{header[i]}: " for i in range(len(header))] bullet_lines.append("• " + ", ".join(pairs)) return bullet_lines def chunk_text(text: str, limit: int = SLACK_TEXT_LIMIT) -> list[str]: if not text: return [] if len(text) <= limit: return [text] chunks: list[str] = [] paragraphs = text.split("\n\n") for para in paragraphs: if not para: continue if len(para) <= limit: chunks.append(para) else: chunks.extend(_chunk_single_paragraph(para, limit)) merged: list[str] = [] for chunk in chunks: if not merged: merged.append(chunk) continue last = merged[-1] candidate = last + "\n\n" + chunk if len(candidate) <= limit: merged[-1] = candidate else: merged.append(chunk) return merged def _chunk_single_paragraph(text: str, limit: int) -> list[str]: sentences = re.split(r"(?<=[.?!])\s+", text) chunks: list[str] = [] current = "" for sentence in sentences: if not sentence: continue if len(sentence) > limit: if current: chunks.append(current.rstrip()) current = "" chunks.extend(_chunk_by_words(sentence, limit)) else: candidate = (current + " " + sentence).strip() if current else sentence if len(candidate) <= limit: current = candidate else: chunks.append(current.rstrip()) current = sentence if current: chunks.append(current.rstrip()) return chunks def _chunk_by_words(text: str, limit: int) -> list[str]: words = text.split(" ") chunks: list[str] = [] current = "" for word in words: if not word: continue if not current: current = word continue candidate = current + " " + word if len(candidate) <= limit: current = candidate else: chunks.append(current) current = word if current: chunks.append(current) safe_chunks: list[str] = [] for chunk in chunks: if len(chunk) <= limit: safe_chunks.append(chunk) else: split_pos = _find_safe_split_position(chunk, limit) if split_pos == 0 or split_pos == limit: safe_chunks.append(chunk) else: safe_chunks.append(chunk[:split_pos]) safe_chunks.append(chunk[split_pos:]) return safe_chunks def strip_mentions(text: str) -> str: return re.sub(SLACK_MENTION_PATTERN, "", text).strip() def extract_mentions(text: str) -> list[str]: return re.findall(SLACK_MENTION_PATTERN, text) def get_agent_prompt_rules() -> list[str]: return SLACK_AGENT_PROMPT_RULES