Logo NascimentoIgor Nascimento / Blog
Agentes

Prompt2Drawio: o modelo escreve o JSON, o código desenha o diagrama nativo do draw.io

Uma frase vira um arquivo .drawio editável, com as formas e cores do próprio aplicativo e layout calculado, usando um modelo local pelo Ollama ou da OpenAI. Uma conversa sobre por que nem Mermaid nem XML gerado pelo modelo servem, sobre colher os estilos de dentro do draw.io para o arquivo parecer nativo, sobre o layout por camadas que é determinístico de propósito, e sobre o que o modelo ainda erra quando o trabalho dele é reduzido ao mínimo.

AgentesIA aplicada14 min de leituraIgor Nascimento

O que ele faz, em uma linha de terminal

O Prompt2Drawio transforma uma frase num arquivo .drawio que abre direto no draw.io, com as formas, as fontes e as cores nativas do próprio aplicativo. Você descreve um diagrama entidade-relacionamento, um diagrama de classes, de sequência, de estados, de atividades ou de casos de uso, e recebe um arquivo editável, com layout calculado, que parece ter sido desenhado à mão dentro da ferramenta. O modelo de linguagem pode ser local, pelo Ollama, ou da OpenAI, à sua escolha por flag.

python prompt2drawio.py \
  "DER de autenticação em dois fatores, com usuário separado da tabela de autenticação" \
  --mode er --model ollama:gpt-oss:20b --direction LR --styles styles.json \
  --style-er-entity er.entity \
  --style-er-edge edge.entityrelation

Num outro texto deste blog conto como montei um agente de Trello com um laço ReAct, e termino dizendo que para fluxos lineares eu preferiria pedir ao modelo um JSON e deixar o código executar. Este projeto é essa preferência posta em prática. Aqui o modelo não chama ferramenta nenhuma. Ele produz uma especificação, e tudo que acontece depois, validação, layout, estilo e XML, é código determinístico. O texto é sobre por que esse desenho serve melhor para diagramas, sobre o que o draw.io exige para um arquivo parecer nativo, e sobre o que o modelo ainda erra mesmo com o trabalho reduzido ao mínimo.

Por que não Mermaid, e por que não deixar o modelo escrever o XML

O caminho mais curto para "texto vira diagrama" é pedir Mermaid ao modelo e renderizar. Funciona para uma imagem. Não funciona para o que eu precisava, que era um arquivo que alguém abre, arrasta uma caixa, muda uma cor e salva. Mermaid gera imagem, não arquivo editável, e a importação para o draw.io perde o estilo. O outro atalho é pedir ao modelo o XML do draw.io diretamente. Esse falha de um jeito pior: o formato é verboso, cada célula tem uma string de estilo com dezenas de chaves, e um atributo mal escapado faz o aplicativo recusar o arquivo inteiro com uma mensagem que não diz onde. O modelo acerta a estrutura e erra os detalhes, e os detalhes são o que importa.

A decisão foi colocar o modelo no único lugar em que ele é insubstituível: entender o que a pessoa quer e devolver isso como dados. Entidades com atributos e tipos. Classes com métodos e visibilidade. Estados com transições. Em JSON, com um esquema que cabe num parágrafo. O resto é problema de geometria e de formato, e geometria e formato são coisas que código faz melhor e sempre igual.

Modelo gera JSON, código gera o resto

  • O esquema pedido é pequeno e o modelo acerta quase sempre
  • Layout é determinístico: a mesma especificação dá o mesmo arquivo
  • Estilos vêm do draw.io, não da memória do modelo
  • Qualquer modelo que escreva JSON serve, inclusive um de 8 bilhões

Modelo gera o XML

  • Um escape errado invalida o arquivo inteiro
  • Posições inventadas, caixas sobrepostas
  • Estilos aproximados, diagrama que "parece" draw.io
  • Só modelos grandes chegam perto, e ainda assim variam

A anatomia: sete esquemas, um layout, um gerador de XML

Cada tipo de diagrama tem o seu esquema JSON, descrito no prompt de forma literal, com o exemplo completo e as regras embaixo. O modelo recebe o esquema e a frase, com temperatura zero e a instrução de responder só JSON. O código tira cercas de markdown se vierem, recorta do primeiro { ao último }, faz o parse e preenche o que faltar com padrões: tipo de atributo vazio vira "text", visibilidade vazia vira "+", relação sem tipo vira associação. O que é inválido é descartado, como uma relação apontando para uma entidade que não existe, e o que é vazio ganha um mínimo, como uma única entidade com um id, para o arquivo nunca sair quebrado.

ModoO que o modelo devolveO que o código faz com isso
erEntidades com atributos (tipo, chave primária, único, nulo) e relações com cardinalidadeUma caixa por entidade com cabeçalho em negrito e uma linha por atributo com as marcações; aresta com a cardinalidade como rótulo
classClasses com atributos, métodos e relações tipadasCaixa com três compartimentos separados por linha; aresta rotulada pelo tipo da relação
sequenceParticipantes e mensagens em ordemUm nó por participante e uma aresta por mensagem, em layout horizontal
stateEstados, transições, estado inicial e finalNós arredondados, marcadores de início e fim, arestas com o evento
activityAtividades com tipo (início, ação, decisão, fim) e arestas com condiçãoLosango para decisão, arredondado para início e fim, retângulo para o resto
usecaseAtores, casos de uso e relações (associação, include, extend)Ator com o estilo de boneco do draw.io, caso de uso em elipse, rótulo da relação na aresta
genericNós com forma simples e arestasPara fluxos e rascunhos, sem semântica

O modo é escolhido por flag, ou inferido da frase por palavras-chave quando você não escolhe: "entidade", "tabela" e "cardinalidade" levam ao DER; "classe" ao diagrama de classes; "ator" e "caso de uso" aos casos de uso. É uma heurística simples, e a frase decide: se ela mistura vocabulário, a flag resolve.

Depois do JSON, tudo vira o mesmo problema: um grafo com nós e arestas. O layout é por camadas. Os nós sem aresta de entrada formam a primeira camada; uma busca em largura coloca cada nó na camada da sua distância até a raiz; dentro de cada camada os nós são ordenados pelo nome, para o resultado ser o mesmo a cada execução. A direção, de cima para baixo ou da esquerda para a direita, pode ser forçada ou escolhida sozinha: se a camada mais larga tem mais nós do que o número de camadas, o grafo é "largo" e vai para a horizontal; senão, vertical. Cada nó tem 240 por 96 pixels e os espaçamentos são fixos, o que é o suficiente para o draw.io abrir com tudo legível e nada sobreposto.

Por fim o XML. Cada nó vira uma célula com posição, tamanho, rótulo e estilo. Cada aresta vira uma célula com origem e destino. O rótulo pode conter HTML, porque é assim que o draw.io quebra linha e põe negrito, e isso exige dois escapes: um para o conteúdo HTML, outro para o atributo XML que o carrega. Foi a parte que mais deu trabalho para acertar, e é a parte que o modelo nunca acertaria sozinho.

O que faz o arquivo parecer nativo: colher os estilos do próprio draw.io

Um retângulo com texto dentro é um retângulo com texto dentro. O que faz uma entidade parecer uma entidade do draw.io é a string de estilo: a forma, a fonte, as cores, as bordas, o comportamento ao redimensionar, tudo numa linha de pares chave-valor que só o aplicativo conhece. Eu poderia escrever essas strings à mão para meia dúzia de casos. Em vez disso, o projeto traz um segundo script que as colhe do próprio aplicativo.

O draw.io de desktop é um aplicativo Electron, e dentro do pacote estão os modelos e as bibliotecas de formas em XML, as mesmas que aparecem no painel lateral. O script extrai o pacote, varre todos os XML, pega o atributo de estilo de cada célula, normaliza a ordem das chaves para deduplicar, e dá a cada estilo um nome legível a partir do que ele é: er.entity, uml.class, edge.entityrelation, shape.ellipse. O resultado é um styles.json com 162 estilos únicos na versão que uso, que o gerador consulta por nome.

A consulta tem três níveis. Se você passa um estilo por flag, ele ganha. Se não, cada modo tem uma lista de preferências, e o gerador procura a primeira chave que contenha os termos, por exemplo "er.entity", depois "entity", depois "table". Se nada bate, há um estilo mínimo embutido para o arquivo abrir de qualquer jeito. E você pode passar a string literal de estilo em vez de uma chave, para o caso em que quer uma cor específica e não quer editar o JSON.

A regra, dita de frente

O modelo escreve o conteúdo. O código decide forma, posição e aparência. O que precisa estar exato, escape, geometria, nome de estilo, nunca passa pelo modelo. E o que precisa ser fiel ao draw.io vem do draw.io, colhido do aplicativo, não lembrado por ninguém. Com isso, trocar o modelo não muda a aparência do diagrama; muda só a qualidade do que está escrito nele.

Local ou nuvem, e o que muda

A chamada ao modelo é a única parte com dependência externa, e ela é pequena de propósito: para o Ollama, uma requisição HTTP à porta local com o prompt e a temperatura; para a OpenAI, a chamada de chat com as mesmas duas mensagens. O resto do programa não sabe qual dos dois respondeu. Com o Ollama, nada do que você descreve sai da máquina, o que importa quando o diagrama é o modelo de dados de um cliente. O tempo limite é generoso, quinze minutos, porque um modelo local de 20 bilhões de parâmetros numa placa doméstica pode levar um bom tempo para escrever um DER com dez entidades e todos os atributos, e eu prefiro esperar a cortar um JSON pela metade.

A diferença de qualidade entre os dois está no conteúdo, não no arquivo. Um modelo grande produz tipos de coluna mais consistentes, não esquece a chave estrangeira, nomeia as relações. Um modelo local de 8 bilhões produz um JSON válido quase sempre, mas às vezes inventa uma relação para uma entidade que não descreveu, e essa relação é descartada em silêncio pelo código. O diagrama abre, e falta uma seta. É o tipo de erro que você vê em um segundo olhando o resultado, e é a razão de o arquivo sair com um hash curto no nome: rodar três vezes e comparar os três lado a lado é mais rápido do que ajustar o prompt.

Onde o modelo ainda tropeça

O que eu quero acrescentar

A receita, para quem quer montar o seu

Defina o esquema JSON de cada coisa que o modelo pode produzir, com exemplo completo no prompt e regras curtas embaixo. Peça só JSON, com temperatura zero, e trate o texto em volta no código. Valide e complete com padrões, descartando o que é inválido em vez de tentar consertar. Deixe tudo que é geometria, formato e aparência no código, e busque a aparência na fonte original, não na memória do modelo. E faça a chamada ao modelo ser uma função de três linhas, para trocar de local para nuvem e de volta sem tocar no resto.

Resumo da conversa