Mermaid это текстовый формат описания диаграмм. В строках задаются тип схемы, узлы и связи. На выходе получается SVG. Формат подходит для API и базы данных. Код можно сгенерировать, проверить, сохранить, открыть повторно и отрендерить на клиенте.
В mermind/views.py добавлен 503, если ответ модели не начинается с валидной головы Mermaid. Без этой проверки запрос завершается успешно, ответ от модели приходит, но диаграмма не строится. В ответе остаются fenced-блоки, Markdown, строки с #, служебный текст и фрагменты до первой строки диаграммы.
В проекте собран полный серверный и клиентский контур. Генерация, очистка ответа, проверка, рендер, повторная правка, сохранение и библиотека.
Как извлекать Mermaid-код из ответа модели. Ответ сначала режется до fenced-блока. Потом проверяется первая строка.
def extract_fenced(text: str) -> str: if not text: return "" m = re.search(r"```(?:mermaid)?\s*([\s\S]*?)```", text, re.IGNORECASE) return (m.group(1).strip() if m else text.strip())def looks_like_mermaid(code: str) -> bool: head = (code or "").strip().lower() return head.startswith(MERMAID_HEADS)
Следующий шаг выполняет sanitize_mermaid. Функция ищет первую валидную голову Mermaid, отбрасывает строки до нее и переводит комментарии из # в %%.
def sanitize_mermaid(raw: str) -> str: s = extract_fenced(raw) lines = (s or "").splitlines() out, started = [], False preface_comments = [] for ln in lines: stripped = ln.strip() if stripped.startswith("```"): break if not started: if stripped.lower().startswith(MERMAID_HEADS): started = True out.append(ln) else: if stripped.startswith("%%") or stripped.startswith("#"): preface_comments.append(_to_mermaid_comment(ln)) continue out.append(_to_mermaid_comment(ln)) cleaned = "\n".join(out).strip() or s.strip() if preface_comments and cleaned: cleaned = f"{cleaned}\n\n" + "\n".join(preface_comments) return cleaned
После этой обработки сервер возвращает только Mermaid-код, пригодный для следующего шага.
Нормализация названий
В диаграммах, которые сохраняются в библиотеку, делаем единую запись брендов и терминов. Модель может вернуть Yandex Pay, Yookassa, Юкасса, Ю Kassa. В сохраненной коллекции это дает разные варианты одного и того же имени.
Для этого в проекте normalize_brand_names.
_BRAND_FIXES = [ (re.compile(r'\bYandex\s*Pay\b', re.IGNORECASE), "YooKassa"), (re.compile(r'\bYandexPay\b', re.IGNORECASE), "YooKassa"), (re.compile(r'\bYo+ka?ssa\b', re.IGNORECASE), "YooKassa"), (re.compile(r'\bЮ\s*Касса\b', re.IGNORECASE), "ЮKassa"), (re.compile(r'\bЮкасса\b', re.IGNORECASE), "ЮKassa"),]
Нормализация нужна для хранения, поиска и повторного открытия диаграмм.
Комментарии и рендер
После серверной очистки в коде остаются комментарии %%. Их можно хранить в базе и показывать в редакторе. Перед рендером они убираются. В useMermaidRender код сначала очищается от комментариев, потом проходит parse, потом идет render.
function stripMermaidCommentsForRender(src: string) { return src.split(/\r?\n/).filter(ln => !ln.trimStart().startsWith("%%")).join("\n");}const renderCode = stripMermaidCommentsForRender(code);try { await mermaid.parse(renderCode);} catch (e) { hostEl.innerHTML = `<div style="padding:12px;text-align:center;color:#b91c1c;background:#fee2e2;border-radius:6px;"> Ошибка в диаграмме: ${(e as Error)?.message || "Syntax error"} </div>`; return;}
В сохраненном коде комментарии остаются. В рендер попадает только код диаграммы.
Пул моделей для генерации
Один model_id не дает стабильного результата. Ответ может прийти без валидной головы, с лишним текстом или в формате, который не проходит looks_like_mermaid.
В проекте после неудачной попытки берется следующая модель из пула.
next_model = pick_next_model_after(used or model)if next_model: out2, used2 = query_openrouter( prompt=user, model_id=next_model, language=lang, system_prompt=sys, temperature=0.3, ) code2 = sanitize_mermaid(_clean_or_template(out2, t)) code2 = normalize_brand_names(code2) if looks_like_mermaid(code2): return t, code2, ["fallback_used"], (used2 or next_model)
Пул собирается отдельно в model_pool.py.
def get_model_pool() -> List[str]: pool = cache.get(POOL_CACHE_KEY) if isinstance(pool, list) and pool: return pool top = get_top_models() or {} code = [m["model_id"] for m in top.get("code_models", [])] text = [m["model_id"] for m in top.get("text_models", [])] pool = _dedup_keep_order([*code, *text]) cache.set(POOL_CACHE_KEY, pool, POOL_TTL_SEC) return pool
Следующая попытка идет по фиксированному порядку пула.
Повторная правка
После первого результата, если задача не решена как надо, можно добавить связь, убрать блок, изменить подпись, поменять структуру.
Для этого используется adjust_mermaid. На вход передается текущий код и короткая инструкция. На выходе приходит новый Mermaid-код после той же цепочки обработки.
@api_view(["POST"])@permission_classes([IsAuthenticated])def adjust_mermaid(request): code = (request.data.get("code") or "").strip() t = request.data.get("type") or "flowchart" instr = (request.data.get("instruction") or "").strip() system = """ Ты модифицируешь Mermaid код. Верни только код Mermaid в fenced-блоке. Комментарии внутри кода только через %%. """.strip() user = f"Инструкция:\n{instr}\n\nТекущий код:\n```mermaid\n{code}\n```" out, used = query_openrouter(...)
На фронте для этого отдельный блок MermindAdjust. Правка выполняется в контексте текущей диаграммы.
Сохранение и библиотека
Для сохранения диаграмм как объектов используется модель Diagram.
class Diagram(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="diagrams") title = models.CharField(max_length=200, blank=True) source_text = models.TextField() type = models.CharField(max_length=20, choices=TYPE_CHOICES, db_index=True) code = models.TextField() model_used = models.CharField(max_length=100, blank=True, default="") warnings = models.JSONField(default=list, blank=True) tags = models.CharField(max_length=200, blank=True)
Сохранение выполняется через save_diagram. Объект либо создается, либо обновляется.
@api_view(["POST"])@permission_classes([IsAuthenticated])def save_diagram(request): data = request.data.copy() data["language"] = "ru" ... if diagram_id: obj = get_object_or_404(Diagram, pk=diagram_id, user=request.user) ser = DiagramSerializer(obj, data=data, partial=True) else: ser = DiagramSerializer(data=data)
На фронте библиотека вызывается через useMermindLibrary. Поддерживаются поиск, фильтр по типу, теги, rename, delete и open.
const fetchList = useCallback(async () => { setLoading(true); try { const data = await api.listDiagrams({ q, type, tags, limit }); setItems(data || []); } finally { setLoading(false); }}, [q, type, tags, limit]);
Итог
В generate_mermaid сервер извлекает Mermaid-код из ответа модели. В normalize.py код очищается и приводится к единому виду. В useMermaidRender код проходит проверку и рендерится без комментариев. В model_pool.py генерация не зависит от одного маршрута. В adjust_mermaid диаграмма правится без повторного ввода исходного запроса. В save_diagram и list_diagrams результат сохраняется в библиотеку.
Такого набора вообщем достаточно для Mermaid как платной AI-функции в проекте. Для примеров в статье использован проект AI-Chat.
ссылка на оригинал статьи https://habr.com/ru/articles/1061160/