Skip to content

Commit

Permalink
feat: set up json-schema-to-diagram
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasbueschel committed Nov 29, 2024
0 parents commit a213bb8
Show file tree
Hide file tree
Showing 12 changed files with 544 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.yml]
indent_style = space
indent_size = 2
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: tobiasbueschel
53 changes: 53 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI & Publish
on:
- push
- pull_request

permissions:
contents: read

jobs:
test:
name: Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version:
- 22
- 20
- 18
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test

release:
name: Release
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 22
- name: Install dependencies
run: npm install
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
yarn.lock
.vscode/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) 2024 Tobias Büschel

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
56 changes: 56 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Model } from "@ai-sdk/openai";

export type Options = {
/**
* The path to the file to update.
* @default "./README.md"
*/
readonly filePath?: string;

/**
* The start marker of the diagram in the file.
* @default "<!-- MERMAID_DIAGRAM_START -->"
*/
readonly startMarker?: string;

/**
* The end marker of the diagram in the file.
* @default "<!-- MERMAID_DIAGRAM_END -->"
*/
readonly endMarker?: string;

/**
* The system prompt for the LLM.
*/
readonly systemPrompt?: string;

/**
* The LLM model to use.
* @default openai("gpt-4o")
*/
readonly model?: Model;

/**
* The JSON schema of the tools.
*/
readonly jsonSchema: object;
};

/**
* Generate a mermaid diagram from a JSON schema of tools.
*
* @param options - The options for the diagram generation.
* @returns A promise that resolves when the diagram is successfully updated.
*
* @example
* ```javascript
* import jsonSchemaToDiagram from 'json-schema-to-diagram';
*
* const options = {
* jsonSchema: { /* your JSON schema here *\/ },
* };
*
* await jsonSchemaToDiagram(options);
* ```
*/
export default function jsonSchemaToDiagram(options: Options): Promise<void>;
95 changes: 95 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { readFile, writeFile } from "fs/promises";
import path from "path";
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const DEFAULT_OPTIONS = {
startMarker: "<!-- MERMAID_DIAGRAM_START -->",
endMarker: "<!-- MERMAID_DIAGRAM_END -->",
filePath: "./README.md",
systemPrompt: `
<purpose>You are a helpful assistant that generates mermaid diagrams based on a JSON schema of tools.</purpose>
<instructions>
<instruction>Use a flowchart diagram with left-to-right orientation.</instruction>
<instruction>Level 1 of the flowchart should be the category of tools that are similar, everything branches out from there</instruction>
<instruction>Level 2 of the flowchart should contain individual tools (use bold text for tool names)</instruction>
<instruction>Between level 1 and level 2, there should be a text link description of the tool</instruction>
<instruction>Level 3 of the flowchart should contain the tools' parameters in a single box with <br> between each parameter</instruction>
<instruction>The flowchart should be in the same language as the one used in the file</instruction>
</instructions>`,
model: openai("gpt-4o"),
};

/**
* Generate a mermaid diagram from a JSON schema of tools.
*
* @param {Object} options - The options for the diagram generation.
* @param {string} [options.filePath] - The path to the file to update.
* @param {string} [options.startMarker] - The start marker of the diagram in the file.
* @param {string} [options.endMarker] - The end marker of the diagram in the file.
* @param {string} [options.systemPrompt] - The system prompt for the LLM.
* @param {Model} [options.model] - The LLM model to use.
* @param {Object} options.jsonSchema - The JSON schema of the tools.
*/
export default async function jsonSchemaToDiagram(options) {
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is not set");
}

if (typeof options !== "object" || options === null) {
throw new Error(`Expected a non-null object`);
}

const config = { ...DEFAULT_OPTIONS, ...options };
const { startMarker, endMarker, filePath, systemPrompt, model, jsonSchema } =
config;

const hasAllRequiredOptions =
filePath && startMarker && endMarker && systemPrompt && model && jsonSchema;

// If no filepath is provided, throw an error
if (!hasAllRequiredOptions) {
throw new Error("Missing required options");
}

const fileToUpdatePath = path.join(__dirname, filePath);

try {
const data = await readFile(fileToUpdatePath, "utf8");

const {
object: { mermaidDiagramString },
} = await generateObject({
model,
system: systemPrompt,
prompt: `Here is the JSON schema: ${JSON.stringify(jsonSchema)}`,
schema: z.object({
mermaidDiagramString: z
.string()
.describe(
"Contains the created diagram strictly following the mermaid syntax."
),
}),
});

const diagramContent = `\`\`\`mermaid\n${mermaidDiagramString}\n\`\`\`\n`;

const regex = new RegExp(`${startMarker}[\\s\\S]*?${endMarker}`, "g");

if (!regex.test(data)) {
console.error(`Markers not found in ${filePath}.`);
return;
}

const newContent = data.replace(
regex,
`${startMarker}\n${diagramContent}${endMarker}`
);

await writeFile(fileToUpdatePath, newContent, "utf8");
console.log(`Diagram successfully updated in ${filePath}`);
} catch (err) {
console.error(`Error processing ${filePath}:`, err);
}
}
134 changes: 134 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, beforeEach, it, expect, vi, afterEach } from "vitest";
import path from "path";
import { fs, vol } from "memfs";

vi.mock("node:fs");
vi.mock("node:fs/promises");

describe("jsonSchemaToDiagram", () => {
let jsonSchemaToDiagram;
const startMarker = "<!-- MERMAID_DIAGRAM_START -->";
const endMarker = "<!-- MERMAID_DIAGRAM_END -->";

beforeEach(async () => {
vi.restoreAllMocks();
process.env.OPENAI_API_KEY = "test-api-key";
vol.reset();

vol.fromJSON({
"./README.md":
"<!-- MERMAID_DIAGRAM_START -->\nOld content\n<!-- MERMAID_DIAGRAM_END -->",
});

vi.mock("ai", () => ({
__esModule: true,
generateObject: vi.fn().mockResolvedValue({
object: { mermaidDiagramString: "graph TD; A-->B;" },
}),
}));

const module = await import("./index.js");
jsonSchemaToDiagram = module.default;
});

afterEach(() => {
vi.restoreAllMocks();
});

it("throws error if OPENAI_API_KEY is not set", async () => {
delete process.env.OPENAI_API_KEY;
await expect(jsonSchemaToDiagram({})).rejects.toThrow(
"OPENAI_API_KEY is not set"
);
});

it("throws error if options is not an object", async () => {
await expect(jsonSchemaToDiagram(null)).rejects.toThrow(
"Expected a non-null object"
);
});

it("throws error if required options are missing", async () => {
await expect(jsonSchemaToDiagram({})).rejects.toThrow(
"Missing required options"
);
});

it.todo("updates file with generated mermaid diagram", async () => {
const filePath = "./README.md";
const systemPrompt = "test system prompt";
const model = "test model";
const jsonSchema = { tools: [] };
const mermaidDiagramString = "graph TD; A-->B;";

vi.spyOn(fs, "writeFile").mockImplementation(() => {
return Promise.resolve();
});

await jsonSchemaToDiagram({
filePath,
startMarker,
endMarker,
systemPrompt,
model,
jsonSchema,
});

const expectedContent = `${startMarker}\n\`\`\`mermaid\n${mermaidDiagramString}\n\`\`\`\n${endMarker}`;
expect(fs.writeFile).toHaveBeenCalledWith(
path.join(__dirname, filePath),
expectedContent,
"utf8"
);
});

it.todo("logs error if markers are not found in file", async () => {
const filePath = "./README.md";
const systemPrompt = "test system prompt";
const model = "test model";
const jsonSchema = { tools: [] };

vol.fromJSON({
"./README.md": "No markers here",
});

vi.spyOn(console, "error").mockImplementation(() => {});

await jsonSchemaToDiagram({
filePath,
startMarker,
endMarker,
systemPrompt,
model,
jsonSchema,
});

expect(console.error).toHaveBeenCalledWith(
`Markers not found in ${filePath}:`,
expect.any(Error)
);
});

it("logs error if there is an error processing the file", async () => {
const filePath = "./README.md";
const systemPrompt = "test system prompt";
const model = "test model";
const jsonSchema = { tools: [] };

vi.spyOn(console, "error").mockImplementation(() => {});

await jsonSchemaToDiagram({
filePath,
startMarker,
endMarker,
systemPrompt,
model,
jsonSchema,
});

expect(console.error).toHaveBeenCalledWith(
`Error processing ${filePath}:`,
expect.any(Error)
);
});
});
Loading

0 comments on commit a213bb8

Please sign in to comment.