A Graph Model DSL

Say hello to the Graph Model Domain Specific Language (GMDSL), created with the help of Github’s Copilot.

I was ready to iterate on the implementation of BrainExpanded, especially the backend graph data model. I wanted to explore the use of Neo4j as my graph store and also start getting ready for a live deployment to real alpha-testing users. As I was learning Cypher (Neo4j’s graph langauge), it occured to me that I had to iterate on many aspects of the end-to-end, before I could finalize the graph data model for the graph store.

  • Graph database data model. I wanted something simpler than Cypher when it comes to describing the graph nodes and edges.
  • OpenAPI for the Web Service. This is the part of the BrainExpanded API that allows client applications to access the graph and ensures the appropriate authorization.
  • C# classes to represent the data model. This is the implementation of the above contract but also additional, application-specific logic that requires queries against the graph store.
  • Python representation of the data model. To make it easier to implement the AI agents that operate over the BrainExpanded memory, I implemented a data model in Python to represent the entries in the graph store. However, I later decided that perhaps the AI agents should not access the database directly and, instead, also go through the Web API layer. So the OpenAPI contract might be enough.
  • As the OpenAPI changes, the Swift classes representing the data model will also change.

I also realized that the node types and relationships in the graph store will evolve differently for different users so the OpenAPI contract will have to evolve as well or be appropriately generic to support such an evolution. Stay tuned on this topic.

So what do software engineers do in such a case? They build a tool to automate things. Well, at least this is what I decided to do. I have been using Github’s Copilot a lot for BrainExpanded but for this little project, Copilot wrote 90-95% of the code with me supervising the iteration process as we were adding more features.

The result is a very simple DSL that helps me describe a simple graph data model. The tool then generates the Cypher, OpenAPI, and C# classes. The Python and Swift classes will be generated from the OpenAPI using existing tools.

Given that this is a generic tool and not tightly coupled with BrainExpanded, I am opening up the repository to everyone in case there is interest in adding more plugins or evolving its functionality. You can find it at https://github.com/savasp/gmdsl.

Here’s a simple example of a graph data model description:

namespace Example

// Import some core types for properties
import GM.Core

// New types can be declared for properties
type Address {
    street: String
    city: String
    state: String
    zipcode: Integer
    country: String
}

// Declare some graph nodes

node Person {
    firstName: String
    lastName: String
    dateOfBirth: DateTime
    placeOfBirth: Location
}

node Company {
    name: String
    address: Address
}

// Department graph node
node Department {
    name: String
}

// Declare some edges. As per Neo4j's data model, edges may have
// properties

edge PartOf(Department -> Company)

edge Friend(Person <-> Person) {
    metOn: DateTime
}

edge Works(Person -> Company) {
    role: String
}

I can now generate Cypher schema declarations, C# classes, and the OpenAPI contract.

Here’s the generated Cypher schema (with the generated comments removed). Over time, I will add the necessary logic to the cypher plugin to generate additional statements.

CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.firstName IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.lastName IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.dateOfBirth IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.placeOfBirth IS NOT NULL;

CREATE CONSTRAINT IF NOT EXISTS FOR (n:Company) REQUIRE n.name IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Company) REQUIRE n.address IS NOT NULL;

CREATE CONSTRAINT IF NOT EXISTS FOR (n:Department) REQUIRE n.name IS NOT NULL;

CREATE CONSTRAINT IF NOT EXISTS FOR ()-[r:Friend]-() REQUIRE r.metOn IS NOT NULL;

CREATE CONSTRAINT IF NOT EXISTS FOR ()-[r:Works]-() REQUIRE r.role IS NOT NULL;

Here are two of the classes generated by the C# plugin. Relationships are represented as objects. This plugin has a bug. It assumes that nodes have IDs. I am working on a fix which will indeed require all nodes and relationships to have IDs since that’s necessary for my use case.

namespace Example
{
    public class Example.Person
    {
        public string firstName { get; set; }
        public string lastName { get; set; }
        public System.DateTime dateOfBirth { get; set; }
        public None placeOfBirth { get; set; }
    }

    public class Example.Friend
    {
        public string SourceId { get; set; }
        public string TargetId { get; set; }
        public System.DateTime metOn { get; set; }
    }
}

And here’s an example of a generated OpenAPI contract (only parts of it are shown). Both graph nodes and relationships are represented as HTTP resources with the usual HTTP verbs.

paths:
  /people:
    post:
      summary: Create Person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/person'
      responses:
        '201':
          description: Created
  /people/{node-id}:
    get:
      summary: Get Person by ID
      responses:
        '200':
          description: OK
    put:
      summary: Update Person by ID
      ...
    delete:
      summary: Delete Person by ID
      ...
  /companies:
    post:
      ...
  /companies/{node-id}:
    get:
      ...
    put:
    ...
  ...
  /people/{node-id}/friends:
    post:
      summary: Create Friend from Person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/friend'
          ...
  /people/{node-id}/friends/{relationship-id}:
    get:
      ...
    put:
      ...
    ...
  ...
components:
  schemas:
    person:
      type: object
      properties:
        firstName:
          type: string
        lastName:
          type: string
        dateOfBirth:
          type: string
        placeOfBirth:
          type: string
    ...
    friend:
      type: object
      properties:
        metOn:
          type: string

This was a fun few hours-long distraction from the main work on BrainExpanded. I enjoyed it and learned even more about how to use Github’s Copilot as a coding companion. How do the kids call it these days? “Vibe coding”? I don’t like the term but here we are 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *