mcp-agensgraph-data-modeling 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ import argparse
2
+ import asyncio
3
+
4
+ from . import server
5
+ from .utils import process_config
6
+
7
+
8
+ def main():
9
+ """Main entry point for the package."""
10
+ parser = argparse.ArgumentParser(description="AgensGraph Data Modeling MCP Server")
11
+ parser.add_argument(
12
+ "--transport", default=None, help="Transport type (stdio, sse, http)"
13
+ )
14
+ parser.add_argument(
15
+ "--server-host", default=None, help="HTTP host (default: 127.0.0.1)"
16
+ )
17
+ parser.add_argument(
18
+ "--server-port", type=int, default=None, help="HTTP port (default: 8000)"
19
+ )
20
+ parser.add_argument(
21
+ "--server-path", default=None, help="HTTP path (default: /mcp/)"
22
+ )
23
+ parser.add_argument(
24
+ "--allow-origins",
25
+ default=None,
26
+ help="Allow origins for remote servers (comma-separated list)",
27
+ )
28
+ parser.add_argument(
29
+ "--allowed-hosts",
30
+ default=None,
31
+ help="Allowed hosts for DNS rebinding protection on remote servers (comma-separated list)",
32
+ )
33
+ parser.add_argument("--namespace", default=None, help="Tool namespace prefix")
34
+
35
+ args = parser.parse_args()
36
+
37
+ config = process_config(args)
38
+ asyncio.run(server.main(**config))
39
+
40
+
41
+ __all__ = ["main", "server"]
@@ -0,0 +1,619 @@
1
+ import json
2
+ from collections import Counter
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field, ValidationInfo, field_validator
6
+
7
+ from .utils import _quote_identifiers
8
+
9
+ NODE_COLOR_PALETTE = [
10
+ ("#e3f2fd", "#1976d2"), # Light Blue / Blue
11
+ ("#f3e5f5", "#7b1fa2"), # Light Purple / Purple
12
+ ("#e8f5e8", "#388e3c"), # Light Green / Green
13
+ ("#fff3e0", "#f57c00"), # Light Orange / Orange
14
+ ("#fce4ec", "#c2185b"), # Light Pink / Pink
15
+ ("#e0f2f1", "#00695c"), # Light Teal / Teal
16
+ ("#f1f8e9", "#689f38"), # Light Lime / Lime
17
+ ("#fff8e1", "#ffa000"), # Light Amber / Amber
18
+ ("#e8eaf6", "#3f51b5"), # Light Indigo / Indigo
19
+ ("#efebe9", "#5d4037"), # Light Brown / Brown
20
+ ("#fafafa", "#424242"), # Light Grey / Dark Grey
21
+ ("#e1f5fe", "#0277bd"), # Light Cyan / Cyan
22
+ ("#f9fbe7", "#827717"), # Light Yellow-Green / Olive
23
+ ("#fff1f0", "#d32f2f"), # Light Red / Red
24
+ ("#f4e6ff", "#6a1b9a"), # Light Violet / Violet
25
+ ("#e6f7ff", "#1890ff"), # Very Light Blue / Bright Blue
26
+ ]
27
+
28
+
29
+ def _generate_relationship_pattern(
30
+ start_node_label: str, relationship_type: str, end_node_label: str
31
+ ) -> str:
32
+ "Helper function to generate a pattern for a relationship."
33
+ return f"(:{start_node_label})-[:{relationship_type}]->(:{end_node_label})"
34
+
35
+
36
+ class PropertySource(BaseModel):
37
+ "The source of a property."
38
+
39
+ column_name: str | None = Field(
40
+ default=None, description="The column name this property maps to, if known."
41
+ )
42
+ table_name: str | None = Field(
43
+ default=None,
44
+ description="The name of the table this property's column is in, if known. May also be the name of a file.",
45
+ )
46
+ location: str | None = Field(
47
+ default=None,
48
+ description="The location of the property, if known. May be a file path, URL, etc.",
49
+ )
50
+
51
+
52
+ class Property(BaseModel):
53
+ "A AgensGraph Property."
54
+
55
+ name: str = Field(description="The name of the property. Should be in camelCase.")
56
+ type: str = Field(
57
+ default="STRING",
58
+ description="The AgensGraph type of the property. Should be all caps.",
59
+ )
60
+ source: PropertySource | None = Field(
61
+ default=None, description="The source of the property, if known."
62
+ )
63
+ description: str | None = Field(
64
+ default=None, description="The description of the property"
65
+ )
66
+
67
+ @field_validator("type")
68
+ def validate_type(cls, v: str) -> str:
69
+ "Validate the type."
70
+
71
+ return v.upper()
72
+
73
+ @classmethod
74
+ def from_arrows(cls, arrows_property: dict[str, str]) -> "Property":
75
+ "Convert an Arrows Property in dict format to a Property."
76
+
77
+ description = None
78
+
79
+ if "|" in list(arrows_property.values())[0]:
80
+ prop_props = [
81
+ x.strip() for x in list(arrows_property.values())[0].split("|")
82
+ ]
83
+
84
+ prop_type = prop_props[0]
85
+ description = prop_props[1] if prop_props[1].lower() != "key" else None
86
+ else:
87
+ prop_type = list(arrows_property.values())[0]
88
+
89
+ return cls(
90
+ name=list(arrows_property.keys())[0],
91
+ type=prop_type,
92
+ description=description,
93
+ )
94
+
95
+ def to_arrows(self, is_key: bool = False) -> dict[str, Any]:
96
+ "Convert a Property to an Arrows property dictionary. Final JSON string formatting is done at the data model level."
97
+ value = f"{self.type}"
98
+ if self.description:
99
+ value += f" | {self.description}"
100
+ if is_key:
101
+ value += " | KEY"
102
+ return {
103
+ self.name: value,
104
+ }
105
+
106
+
107
+ class Node(BaseModel):
108
+ "A AgensGraph Node."
109
+
110
+ label: str = Field(
111
+ description="The label of the node. Should be in PascalCase.", min_length=1
112
+ )
113
+ key_property: Property = Field(description="The key property of the node")
114
+ properties: list[Property] = Field(
115
+ default_factory=list, description="The properties of the node"
116
+ )
117
+ metadata: dict[str, Any] = Field(
118
+ default_factory=dict,
119
+ description="The metadata of the node. This should only be used when converting data models.",
120
+ )
121
+
122
+ @field_validator("properties")
123
+ def validate_properties(
124
+ cls, properties: list[Property], info: ValidationInfo
125
+ ) -> list[Property]:
126
+ "Validate the properties."
127
+ properties = [p for p in properties if p.name != info.data["key_property"].name]
128
+
129
+ counts = Counter([p.name for p in properties])
130
+ for name, count in counts.items():
131
+ if count > 1:
132
+ raise ValueError(
133
+ f"Property {name} appears {count} times in node {info.data['label']}"
134
+ )
135
+ return properties
136
+
137
+ def add_property(self, prop: Property) -> None:
138
+ "Add a new property to the node."
139
+ if prop.name in [p.name for p in self.properties]:
140
+ raise ValueError(
141
+ f"Property {prop.name} already exists in node {self.label}"
142
+ )
143
+ self.properties.append(prop)
144
+
145
+ def remove_property(self, prop: Property) -> None:
146
+ "Remove a property from the node."
147
+ try:
148
+ self.properties.remove(prop)
149
+ except ValueError:
150
+ pass
151
+
152
+ @property
153
+ def all_properties_dict(self) -> dict[str, str]:
154
+ "Return a dictionary of all properties of the node. {property_name: property_type}"
155
+ props = {p.name: p.type for p in self.properties} if self.properties else {}
156
+ if self.key_property:
157
+ props.update({self.key_property.name: f"{self.key_property.type} | KEY"})
158
+ return props
159
+
160
+ def get_mermaid_config_str(self) -> str:
161
+ "Get the Mermaid configuration string for the node."
162
+ props = [f"<br/>{self.key_property.name}: {self.key_property.type} | KEY"]
163
+ props.extend([f"<br/>{p.name}: {p.type}" for p in self.properties])
164
+ return f'{self.label}["{self.label}{"".join(props)}"]'
165
+
166
+ @classmethod
167
+ def from_arrows(cls, arrows_node_dict: dict[str, Any]) -> "Node":
168
+ "Convert an Arrows Node to a Node."
169
+ props = [
170
+ Property.from_arrows({k: v})
171
+ for k, v in arrows_node_dict["properties"].items()
172
+ if "KEY" not in v.upper()
173
+ ]
174
+ keys = [
175
+ {k: v}
176
+ for k, v in arrows_node_dict["properties"].items()
177
+ if "KEY" in v.upper()
178
+ ]
179
+ key_prop = Property.from_arrows(keys[0]) if keys else None
180
+ metadata = {
181
+ "position": arrows_node_dict["position"],
182
+ "caption": arrows_node_dict["caption"],
183
+ "style": arrows_node_dict["style"],
184
+ }
185
+ return cls(
186
+ label=arrows_node_dict["labels"][0],
187
+ key_property=key_prop,
188
+ properties=props,
189
+ metadata=metadata,
190
+ )
191
+
192
+ def to_arrows(
193
+ self, default_position: dict[str, float] = {"x": 0.0, "y": 0.0}
194
+ ) -> dict[str, Any]:
195
+ "Convert a Node to an Arrows Node dictionary. Final JSON string formatting is done at the data model level."
196
+ props = dict()
197
+ [props.update(p.to_arrows(is_key=False)) for p in self.properties]
198
+ props.update(self.key_property.to_arrows(is_key=True))
199
+ return {
200
+ "id": self.label,
201
+ "labels": [self.label],
202
+ "properties": props,
203
+ "style": self.metadata.get("style", {}),
204
+ "position": self.metadata.get("position", default_position),
205
+ "caption": self.metadata.get("caption", ""),
206
+ }
207
+
208
+ def get_cypher_ingest_query_for_many_records(self) -> str:
209
+ """
210
+ Generate a Cypher query to ingest a list of Node records into a AgensGraph database.
211
+ This query takes a named parameter %(records)s that is a JSONB array of dictionaries.
212
+ Note: For AgensGraph with psycopg, use: cursor.execute(query, {"records": Jsonb(records)})
213
+ where Jsonb is imported from psycopg.types.json
214
+ """
215
+ formatted_props = ", ".join(
216
+ [f"{p.name}: record.{p.name}" for p in self.properties]
217
+ )
218
+ query = f"""UNWIND %(records)s as record
219
+ MERGE (n: {self.label} {{{self.key_property.name}: record.{self.key_property.name}}})
220
+ SET n += {{{formatted_props}}}"""
221
+ return _quote_identifiers(query)
222
+
223
+ def get_cypher_constraint_query(self) -> str:
224
+ """
225
+ Generate a Cypher query to create a UNIQUE constraint on the node's key property.
226
+ This enforces uniqueness on the key property of the node using AgensGraph syntax.
227
+ First creates the VLABEL if it doesn't exist, then creates the constraint.
228
+ """
229
+ # Create VLABEL first, then constraint
230
+ vlabel_query = f"CREATE VLABEL IF NOT EXISTS {self.label}"
231
+ constraint_query = f"CREATE CONSTRAINT {self.label}_constraint ON {self.label} ASSERT {self.key_property.name} IS UNIQUE"
232
+
233
+ # Apply identifier quoting to both queries
234
+ vlabel_query = _quote_identifiers(vlabel_query)
235
+ constraint_query = _quote_identifiers(constraint_query)
236
+
237
+ # Return both queries separated by semicolon
238
+ return f"{vlabel_query}; {constraint_query}"
239
+
240
+
241
+ class Relationship(BaseModel):
242
+ "A AgensGraph Relationship."
243
+
244
+ type: str = Field(
245
+ description="The type of the relationship. Should be in SCREAMING_SNAKE_CASE.",
246
+ min_length=1,
247
+ )
248
+ start_node_label: str = Field(description="The label of the start node")
249
+ end_node_label: str = Field(description="The label of the end node")
250
+ key_property: Property | None = Field(
251
+ default=None, description="The key property of the relationship, if any."
252
+ )
253
+ properties: list[Property] = Field(
254
+ default_factory=list, description="The properties of the relationship, if any."
255
+ )
256
+ metadata: dict[str, Any] = Field(
257
+ default_factory=dict,
258
+ description="The metadata of the relationship. This should only be used when converting data models.",
259
+ )
260
+
261
+ @field_validator("properties")
262
+ def validate_properties(
263
+ cls, properties: list[Property], info: ValidationInfo
264
+ ) -> list[Property]:
265
+ "Validate the properties."
266
+ if info.data.get("key_property"):
267
+ properties = [
268
+ p for p in properties if p.name != info.data["key_property"].name
269
+ ]
270
+
271
+ counts = Counter([p.name for p in properties])
272
+ for name, count in counts.items():
273
+ if count > 1:
274
+ raise ValueError(
275
+ f"Property {name} appears {count} times in relationship {_generate_relationship_pattern(info.data['start_node_label'], info.data['type'], info.data['end_node_label'])}"
276
+ )
277
+ return properties
278
+
279
+ def add_property(self, prop: Property) -> None:
280
+ "Add a new property to the relationship."
281
+ if prop.name in [p.name for p in self.properties]:
282
+ raise ValueError(
283
+ f"Property {prop.name} already exists in relationship {self.pattern}"
284
+ )
285
+ self.properties.append(prop)
286
+
287
+ def remove_property(self, prop: Property) -> None:
288
+ "Remove a property from the relationship."
289
+ try:
290
+ self.properties.remove(prop)
291
+ except ValueError:
292
+ pass
293
+
294
+ @property
295
+ def pattern(self) -> str:
296
+ "Return the pattern of the relationship."
297
+ return _generate_relationship_pattern(
298
+ self.start_node_label, self.type, self.end_node_label
299
+ )
300
+
301
+ @property
302
+ def all_properties_dict(self) -> dict[str, str]:
303
+ "Return a dictionary of all properties of the relationship. {property_name: property_type}"
304
+
305
+ props = {p.name: p.type for p in self.properties} if self.properties else {}
306
+ if self.key_property:
307
+ props.update({self.key_property.name: f"{self.key_property.type} | KEY"})
308
+ return props
309
+
310
+ def get_mermaid_config_str(self) -> str:
311
+ "Get the Mermaid configuration string for the relationship."
312
+ props = (
313
+ [f"<br/>{self.key_property.name}: {self.key_property.type} | KEY"]
314
+ if self.key_property
315
+ else []
316
+ )
317
+ props.extend([f"<br/>{p.name}: {p.type}" for p in self.properties])
318
+ return f"{self.start_node_label} -->|{self.type}{''.join(props)}| {self.end_node_label}"
319
+
320
+ @classmethod
321
+ def from_arrows(
322
+ cls,
323
+ arrows_relationship_dict: dict[str, Any],
324
+ node_id_to_label_map: dict[str, str],
325
+ ) -> "Relationship":
326
+ "Convert an Arrows Relationship to a Relationship."
327
+ props = [
328
+ Property.from_arrows({k: v})
329
+ for k, v in arrows_relationship_dict["properties"].items()
330
+ if "KEY" not in v.upper()
331
+ ]
332
+ keys = [
333
+ {k: v}
334
+ for k, v in arrows_relationship_dict["properties"].items()
335
+ if "KEY" in v.upper()
336
+ ]
337
+ key_prop = Property.from_arrows(keys[0]) if keys else None
338
+ metadata = {
339
+ "style": arrows_relationship_dict["style"],
340
+ }
341
+ return cls(
342
+ type=arrows_relationship_dict["type"],
343
+ start_node_label=node_id_to_label_map[arrows_relationship_dict["fromId"]],
344
+ end_node_label=node_id_to_label_map[arrows_relationship_dict["toId"]],
345
+ key_property=key_prop,
346
+ properties=props,
347
+ metadata=metadata,
348
+ )
349
+
350
+ def to_arrows(self) -> dict[str, Any]:
351
+ "Convert a Relationship to an Arrows Relationship dictionary. Final JSON string formatting is done at the data model level."
352
+ props = dict()
353
+ [props.update(p.to_arrows(is_key=False)) for p in self.properties]
354
+ if self.key_property:
355
+ props.update(self.key_property.to_arrows(is_key=True))
356
+ return {
357
+ "fromId": self.start_node_label,
358
+ "toId": self.end_node_label,
359
+ "type": self.type,
360
+ "properties": props,
361
+ "style": self.metadata.get("style", {}),
362
+ }
363
+
364
+ def get_cypher_ingest_query_for_many_records(
365
+ self, start_node_key_property_name: str, end_node_key_property_name: str
366
+ ) -> str:
367
+ """
368
+ Generate a Cypher query to ingest a list of Relationship records into a AgensGraph database.
369
+ The sourceId and targetId properties are used to match the start and end nodes.
370
+ This query takes a named parameter %(records)s that is a JSONB array of dictionaries.
371
+ Note: For AgensGraph with psycopg, use: cursor.execute(query, {"records": Jsonb(records)})
372
+ where Jsonb is imported from psycopg.types.json
373
+ """
374
+ formatted_props = ", ".join(
375
+ [f"{p.name}: record.{p.name}" for p in self.properties]
376
+ )
377
+ key_prop = (
378
+ f" {{{self.key_property.name}: record.{self.key_property.name}}}"
379
+ if self.key_property
380
+ else ""
381
+ )
382
+
383
+ query = f"""UNWIND %(records)s as record
384
+ MATCH (startNode: {self.start_node_label} {{{start_node_key_property_name}: record.sourceId}})
385
+ MATCH (endNode: {self.end_node_label} {{{end_node_key_property_name}: record.targetId}})
386
+ MERGE (startNode)-[r:{self.type}{key_prop}]->(endNode)"""
387
+ if formatted_props:
388
+ # Relationship properties belong on the relationship (r), not the end node.
389
+ query += f"""
390
+ SET r += {{{formatted_props}}}"""
391
+ return _quote_identifiers(query)
392
+
393
+ def get_cypher_constraint_query(self) -> str | None:
394
+ """
395
+ Generate a Cypher query to create a UNIQUE constraint on the relationship's key property.
396
+ This enforces uniqueness on the key property of the relationship using AgensGraph syntax.
397
+ First creates the ELABEL if it doesn't exist, then creates the constraint.
398
+ """
399
+ if self.key_property:
400
+ # Create ELABEL first, then constraint
401
+ elabel_query = f"CREATE ELABEL IF NOT EXISTS {self.type}"
402
+ constraint_query = f"CREATE CONSTRAINT {self.type}_constraint ON {self.type} ASSERT {self.key_property.name} IS UNIQUE"
403
+
404
+ # Apply identifier quoting to both queries
405
+ elabel_query = _quote_identifiers(elabel_query)
406
+ constraint_query = _quote_identifiers(constraint_query)
407
+
408
+ # Return both queries separated by semicolon
409
+ return f"{elabel_query}; {constraint_query}"
410
+ else:
411
+ return None
412
+
413
+
414
+ class DataModel(BaseModel):
415
+ "A AgensGraph Graph Data Model."
416
+
417
+ nodes: list[Node] = Field(
418
+ default_factory=list, description="The nodes of the data model"
419
+ )
420
+ relationships: list[Relationship] = Field(
421
+ default_factory=list, description="The relationships of the data model"
422
+ )
423
+ metadata: dict[str, Any] = Field(
424
+ default_factory=dict,
425
+ description="The metadata of the data model. This should only be used when converting data models.",
426
+ )
427
+
428
+ @field_validator("nodes")
429
+ def validate_nodes(cls, nodes: list[Node]) -> list[Node]:
430
+ "Validate the nodes."
431
+
432
+ counts = Counter([n.label for n in nodes])
433
+ for label, count in counts.items():
434
+ if count > 1:
435
+ raise ValueError(
436
+ f"Node with label {label} appears {count} times in data model"
437
+ )
438
+ return nodes
439
+
440
+ @field_validator("relationships")
441
+ def validate_relationships(
442
+ cls, relationships: list[Relationship], info: ValidationInfo
443
+ ) -> list[Relationship]:
444
+ "Validate the relationships."
445
+
446
+ # ensure source and target nodes exist
447
+ for relationship in relationships:
448
+ if relationship.start_node_label not in [
449
+ n.label for n in info.data["nodes"]
450
+ ]:
451
+ raise ValueError(
452
+ f"Relationship {relationship.pattern} has a start node that does not exist in data model"
453
+ )
454
+ if relationship.end_node_label not in [n.label for n in info.data["nodes"]]:
455
+ raise ValueError(
456
+ f"Relationship {relationship.pattern} has an end node that does not exist in data model"
457
+ )
458
+
459
+ return relationships
460
+
461
+ @property
462
+ def nodes_dict(self) -> dict[str, Node]:
463
+ "Return a dictionary of the nodes of the data model. {node_label: node_dict}"
464
+ return {n.label: n for n in self.nodes}
465
+
466
+ @property
467
+ def relationships_dict(self) -> dict[str, Relationship]:
468
+ "Return a dictionary of the relationships of the data model. {relationship_pattern: relationship_dict}"
469
+ return {r.pattern: r for r in self.relationships}
470
+
471
+ def add_node(self, node: Node) -> None:
472
+ "Add a new node to the data model."
473
+ if node.label in [n.label for n in self.nodes]:
474
+ raise ValueError(
475
+ f"Node with label {node.label} already exists in data model"
476
+ )
477
+ self.nodes.append(node)
478
+
479
+ def add_relationship(self, relationship: Relationship) -> None:
480
+ "Add a new relationship to the data model."
481
+ if relationship.pattern in [r.pattern for r in self.relationships]:
482
+ raise ValueError(
483
+ f"Relationship {relationship.pattern} already exists in data model"
484
+ )
485
+ self.relationships.append(relationship)
486
+
487
+ def remove_node(self, node_label: str) -> None:
488
+ "Remove a node from the data model."
489
+ try:
490
+ [self.nodes.remove(x) for x in self.nodes if x.label == node_label]
491
+ except ValueError:
492
+ pass
493
+
494
+ def remove_relationship(
495
+ self,
496
+ relationship_type: str,
497
+ relationship_start_node_label: str,
498
+ relationship_end_node_label: str,
499
+ ) -> None:
500
+ "Remove a relationship from the data model."
501
+ pattern = _generate_relationship_pattern(
502
+ relationship_start_node_label,
503
+ relationship_type,
504
+ relationship_end_node_label,
505
+ )
506
+ try:
507
+ [
508
+ self.relationships.remove(x)
509
+ for x in self.relationships
510
+ if x.pattern == pattern
511
+ ]
512
+ except ValueError:
513
+ pass
514
+
515
+ def _generate_mermaid_config_styling_str(self) -> str:
516
+ "Generate the Mermaid configuration string for the data model."
517
+ node_color_config = ""
518
+
519
+ for idx, node in enumerate(self.nodes):
520
+ node_color_config += f"classDef node_{idx}_color fill:{NODE_COLOR_PALETTE[idx % len(NODE_COLOR_PALETTE)][0]},stroke:{NODE_COLOR_PALETTE[idx % len(NODE_COLOR_PALETTE)][1]},stroke-width:3px,color:#000,font-size:12px\nclass {node.label} node_{idx}_color\n\n"
521
+
522
+ return f"""
523
+ %% Styling
524
+ {node_color_config}
525
+ """
526
+
527
+ def get_mermaid_config_str(self) -> str:
528
+ "Get the Mermaid configuration string for the data model."
529
+ mermaid_nodes = [n.get_mermaid_config_str() for n in self.nodes]
530
+ mermaid_relationships = [r.get_mermaid_config_str() for r in self.relationships]
531
+ mermaid_styling = self._generate_mermaid_config_styling_str()
532
+ nodes_formatted = "\n".join(mermaid_nodes)
533
+ relationships_formatted = "\n".join(mermaid_relationships)
534
+ return f"""graph TD
535
+ %% Nodes
536
+ {nodes_formatted}
537
+
538
+ %% Relationships
539
+ {relationships_formatted}
540
+
541
+ {mermaid_styling}
542
+ """
543
+
544
+ @classmethod
545
+ def from_arrows(cls, arrows_data_model_dict: dict[str, Any]) -> "DataModel":
546
+ "Convert an Arrows Data Model to a Data Model."
547
+ nodes = [Node.from_arrows(n) for n in arrows_data_model_dict["nodes"]]
548
+ node_id_to_label_map = {
549
+ n["id"]: n["labels"][0] for n in arrows_data_model_dict["nodes"]
550
+ }
551
+ relationships = [
552
+ Relationship.from_arrows(r, node_id_to_label_map)
553
+ for r in arrows_data_model_dict["relationships"]
554
+ ]
555
+ metadata = {
556
+ "style": arrows_data_model_dict["style"],
557
+ }
558
+ return cls(nodes=nodes, relationships=relationships, metadata=metadata)
559
+
560
+ def to_arrows_dict(self) -> dict[str, Any]:
561
+ "Convert the data model to an Arrows Data Model Python dictionary."
562
+ node_spacing: int = 200
563
+ y_current = 0
564
+ arrows_nodes = []
565
+ for idx, n in enumerate(self.nodes):
566
+ if (idx + 1) % 5 == 0:
567
+ y_current -= 200
568
+ arrows_nodes.append(
569
+ n.to_arrows(
570
+ default_position={"x": node_spacing * (idx % 5), "y": y_current}
571
+ )
572
+ )
573
+ arrows_relationships = [r.to_arrows() for r in self.relationships]
574
+ return {
575
+ "nodes": arrows_nodes,
576
+ "relationships": arrows_relationships,
577
+ "style": self.metadata.get("style", {}),
578
+ }
579
+
580
+ def to_arrows_json_str(self) -> str:
581
+ "Convert the data model to an Arrows Data Model JSON string."
582
+ return json.dumps(self.to_arrows_dict(), indent=2)
583
+
584
+ def get_node_cypher_ingest_query_for_many_records(self, node_label: str) -> str:
585
+ "Generate a Cypher query to ingest a list of Node records into a AgensGraph database."
586
+ node = self.nodes_dict[node_label]
587
+ return node.get_cypher_ingest_query_for_many_records()
588
+
589
+ def get_relationship_cypher_ingest_query_for_many_records(
590
+ self,
591
+ relationship_type: str,
592
+ relationship_start_node_label: str,
593
+ relationship_end_node_label: str,
594
+ ) -> str:
595
+ "Generate a Cypher query to ingest a list of Relationship records into a AgensGraph database."
596
+ pattern = _generate_relationship_pattern(
597
+ relationship_start_node_label,
598
+ relationship_type,
599
+ relationship_end_node_label,
600
+ )
601
+ relationship = self.relationships_dict[pattern]
602
+ start_node = self.nodes_dict[relationship.start_node_label]
603
+ end_node = self.nodes_dict[relationship.end_node_label]
604
+ return relationship.get_cypher_ingest_query_for_many_records(
605
+ start_node.key_property.name, end_node.key_property.name
606
+ )
607
+
608
+ def get_cypher_constraints_query(self) -> list[str]:
609
+ """
610
+ Generate a list of Cypher queries to create constraints on the data model.
611
+ This creates range indexes on the key properties of the nodes and relationships and enforces uniqueness and existence of the key properties.
612
+ """
613
+ node_queries = [n.get_cypher_constraint_query() + ";" for n in self.nodes]
614
+ relationship_queries = [
615
+ r.get_cypher_constraint_query() + ";"
616
+ for r in self.relationships
617
+ if r.key_property is not None
618
+ ]
619
+ return node_queries + relationship_queries
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from .data_model import DataModel
4
+
5
+
6
+ class ExampleDataModelResponse(BaseModel):
7
+ """Response model for the `get_example_data_model` tool."""
8
+
9
+ data_model: DataModel = Field(description="The example graph data model.")
10
+ mermaid_config: str = Field(
11
+ description="The Mermaid visualization configuration for the example graph data model."
12
+ )