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.
| Modo | O que o modelo devolve | O que o código faz com isso |
|---|---|---|
er | Entidades com atributos (tipo, chave primária, único, nulo) e relações com cardinalidade | Uma caixa por entidade com cabeçalho em negrito e uma linha por atributo com as marcações; aresta com a cardinalidade como rótulo |
class | Classes com atributos, métodos e relações tipadas | Caixa com três compartimentos separados por linha; aresta rotulada pelo tipo da relação |
sequence | Participantes e mensagens em ordem | Um nó por participante e uma aresta por mensagem, em layout horizontal |
state | Estados, transições, estado inicial e final | Nós arredondados, marcadores de início e fim, arestas com o evento |
activity | Atividades com tipo (início, ação, decisão, fim) e arestas com condição | Losango para decisão, arredondado para início e fim, retângulo para o resto |
usecase | Atores, 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 |
generic | Nós com forma simples e arestas | Para 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.
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
- Relações para entidades que não existem. O caso mais comum. O código descarta a relação em vez de criar uma entidade fantasma, porque uma caixa vazia inventada é pior do que uma seta faltando.
- JSON com comentário ou com vírgula sobrando. Modelos menores gostam de explicar. O recorte do primeiro ao último chaveamento resolve o texto em volta, mas não resolve uma vírgula a mais dentro, e aí a execução falha com erro claro. A correção que eu quero é um pedido de reparo ao próprio modelo, com o erro do parser como contexto.
- Diagrama de sequência sem a noção de tempo. Pela natureza do layout, as mensagens viram arestas entre participantes, e a ordem vertical das linhas de vida, que é o que dá sentido a um diagrama de sequência, não existe. O modo funciona para ver quem fala com quem, não para ler o protocolo. É o modo que mais se beneficiaria de um layout próprio.
- Grafos com ciclo e sem raiz. Um diagrama de estados em que todo estado tem transição de entrada não tem camada zero. O código escolhe o primeiro estado como raiz e segue. O resultado abre e é editável, mas a disposição é arbitrária.
- Cruzamento de arestas. O layout ordena os nós dentro de cada camada pelo nome, não para minimizar cruzamentos. Em grafos pequenos não importa; em um DER de quinze entidades aparecem setas atravessando caixas, e a pessoa arrasta no draw.io. Minimizar cruzamentos é um problema conhecido e resolvido na literatura, e é o próximo passo natural do layout.
O que eu quero acrescentar
- Reparo do JSON pelo modelo, uma única tentativa, com a mensagem do parser. Resolve a maior parte das falhas dos modelos pequenos sem laço de agente.
- Layout próprio para sequência, com linhas de vida verticais e mensagens em ordem, que é o que o draw.io já sabe desenhar e só precisa das coordenadas certas.
- Ordenação dentro da camada por baricentro, para reduzir cruzamentos, mantendo o determinismo.
- Um modo de atualização, em que o prompt descreve uma mudança e o programa lê o arquivo existente, aplica a diferença e preserva o que a pessoa arrastou. Hoje cada execução é um arquivo novo, e é por isso que o nome leva um hash.
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
- Uma frase vira um arquivo .drawio editável, com estilos nativos e layout calculado. O modelo pode ser local pelo Ollama ou da OpenAI.
- Nem Mermaid, que gera imagem, nem XML pelo modelo, que quebra no escape. O modelo gera uma especificação JSON pequena; validação, layout, estilo e XML são código.
- Os estilos são colhidos do próprio aplicativo draw.io, 162 na versão que uso, resolvidos por nome com lista de preferências por modo e override por flag.
- O layout por camadas é determinístico e suficiente para grafos pequenos; cruzamentos e diagramas de sequência pedem layouts próprios, que vêm a seguir.
- O que o modelo ainda erra é conteúdo: relação para entidade inexistente, JSON com sobra. O código descarta ou falha com clareza, e o reparo em uma tentativa é o próximo passo.
Igor Nascimento