Repository: langchain4j/langchain4j-community
Pull request #116: LLMGraphTransformer for Knowledge Graph Construction
Author: login: vga91
Labels: name: enhancement
description: New feature or request
name: P3
description: Medium priority
name: theme: content retriever
description: Issues/PRs related to content retriever
Created: 2025-03-26T10:31:57Z | Merged: 2025-04-28T10:18:26Z

<!--
Thank you so much for your contribution!

Please fill in all the sections below.
Please open the PR as a draft initially. Once it is reviewed and approved, we will ask you to add documentation and examples.
Please note that PRs with breaking changes or without tests will be rejected.

Please note that PRs will be reviewed based on the priority of the issues they address.
We ask for your patience. We are doing our best to review your PR as quickly as possible.
Please refrain from pinging and asking when it will be reviewed. Thank you for understanding!
-->

## Issue
<!-- Please specify the ID of the issue this PR is addressing. For example: "Closes #1234" or "Fixes #1234" -->
Closes https://github.com/langchain4j/langchain4j/issues/2543

Closes `LLMGraphTransformer equivalent` in #111
Closes `Prompt for entity extraction` in #111

## Change
<!-- Please describe the changes you made. -->

EVALUATE: where put LLMGraphTransformer? In neo4j package or another module/package?

- Option to link entities back to the source document
- These knowledge graphs form the foundation for advanced RAG patterns in GraphRAG (see https://graphrag.com)
- Besides data from document loaders also data that already exists in large text fields in databases (semi-structured data) could be analyzed and extracted
This also offers the means to then run additional algorithms on the data e.g. for query focused summarization that compute communities/clusters of topics and summarize them with an LLM

Similar to https://github.com/langchain-ai/langchain-experimental/blob/main/libs/experimental/langchain_experimental/graph_transformers/llm.py

## General checklist
<!-- Please double-check the following points and mark them like this: [X] -->
- [x] There are no breaking changes
- [x] I have added unit and integration tests for my change
- [x] I have manually run all the unit tests in _all_ modules, and they are all green
- [x] I have manually run all integration tests in the module I have added/changed, and they are all green
<!-- Before adding documentation and example(s) (below), please wait until the PR is reviewed and approved. -->
- [ ] I have added/updated the [documentation](https://github.com/langchain4j/langchain4j/tree/main/docs/docs)
- [ ] I have added an example in the [examples repo](https://github.com/langchain4j/langchain4j-examples) (only for "big" features)
- [ ] I have added/updated [Spring Boot starter(s)](https://github.com/langchain4j/langchain4j-spring) (if applicable)


## Checklist for adding new maven module
<!-- Please double-check the following points and mark them like this: [X] -->
- [ ] I have added my new module in the root `pom.xml` and `langchain4j-community-bom/pom.xml`


## Checklist for adding new embedding store integration
<!-- Please double-check the following points and mark them like this: [X] -->
- [ ] I have added a `{NameOfIntegration}EmbeddingStoreIT` that extends from either `EmbeddingStoreIT` or `EmbeddingStoreWithFilteringIT`
- [ ] I have added a `{NameOfIntegration}EmbeddingStoreRemovalIT` that extends from `EmbeddingStoreWithRemovalIT`

## Checklist for changing existing embedding store integration
<!-- Please double-check the following points and mark them like this: [X] -->
- [ ] I have manually verified that the `{NameOfIntegration}EmbeddingStore` works correctly with the data persisted using the latest released version of LangChain4j



Diff:
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/pom.xml b/content-retrievers/langchain4j-community-neo4j-retriever/pom.xml
index 1e5a14fb8..a39e92649 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/pom.xml
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/pom.xml
@@ -12,11 +12,16 @@
     <packaging>jar</packaging>
     <name>LangChain4j :: Community :: Content Retriever :: Neo4j</name>
 
+    <properties>
+        <neo4j.cypher.dsl.version>2023.7.0</neo4j.cypher.dsl.version>
+        <neo4j.version>5.26.0</neo4j.version>
+    </properties>
+
     <dependencies>
 
         <dependency>
             <groupId>dev.langchain4j</groupId>
-            <artifactId>langchain4j-core</artifactId>
+            <artifactId>langchain4j-community-core</artifactId>
             <version>${project.version}</version>
         </dependency>
 
@@ -28,7 +33,7 @@
         <dependency>
             <groupId>org.neo4j.driver</groupId>
             <artifactId>neo4j-java-driver</artifactId>
-            <version>5.26.0</version>
+            <version>${neo4j.version}</version>
         </dependency>
 
         <!-- test dependencies -->
@@ -69,6 +74,20 @@
             <scope>test</scope>
         </dependency>
 
+        <!-- graph transformer dependencies -->
+        <dependency>
+            <groupId>org.neo4j</groupId>
+            <artifactId>neo4j-cypher-dsl-schema-name-support</artifactId>
+            <version>${neo4j.cypher.dsl.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-community-llm-graph-transformer</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/KnowledgeGraphWriter.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/KnowledgeGraphWriter.java
new file mode 100644
index 000000000..70dbe3c44
--- /dev/null
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/KnowledgeGraphWriter.java
@@ -0,0 +1,219 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.content.retriever.neo4j.Neo4jUtils.generateMD5;
+import static dev.langchain4j.community.rag.content.retriever.neo4j.Neo4jUtils.sanitizeOrThrows;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.Experimental;
+import dev.langchain4j.community.data.document.graph.GraphDocument;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Experimental
+public class KnowledgeGraphWriter {
+
+    /* default configs */
+    public static final String DEFAULT_ID_PROP = "id";
+    public static final String DEFAULT_TEXT_PROP = "text";
+    public static final String DEFAULT_LABEL = "__Entity__";
+    public static final String DEFAULT_CONS_NAME = "knowledge_cons";
+    public static final String DEFAULT_REL_TYPE = "HAS_ENTITY";
+
+    final String label;
+    final String relType;
+    final String sanitizedLabel;
+    final String sanitizedRelType;
+    final String constraintName;
+
+    final String idProperty;
+    final String sanitizedIdProperty;
+    final String textProperty;
+    final String sanitizedTextProperty;
+
+    private final Neo4jGraph graph;
+
+    public KnowledgeGraphWriter(
+            Neo4jGraph graph,
+            String idProperty,
+            String label,
+            String textProperty,
+            String relType,
+            String constraintName) {
+        this.graph = ensureNotNull(graph, "graph");
+        this.label = getOrDefault(label, DEFAULT_LABEL);
+        this.relType = getOrDefault(relType, DEFAULT_REL_TYPE);
+        this.idProperty = getOrDefault(idProperty, DEFAULT_ID_PROP);
+        this.textProperty = getOrDefault(textProperty, DEFAULT_TEXT_PROP);
+        this.constraintName = getOrDefault(constraintName, DEFAULT_CONS_NAME);
+
+        /* sanitize labels and property names, to prevent from Cypher Injections */
+        this.sanitizedLabel = sanitizeOrThrows(this.label, "label");
+        this.sanitizedRelType = sanitizeOrThrows(this.relType, "relType");
+        this.sanitizedIdProperty = sanitizeOrThrows(this.idProperty, "idProperty");
+        this.sanitizedTextProperty = sanitizeOrThrows(this.textProperty, "textProperty");
+
+        createConstraint();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private void createConstraint() {
+        // Check and create constraint if not exists
+        final String constraintQuery = String.format(
+                "CREATE CONSTRAINT %s IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                constraintName, sanitizedLabel, sanitizedIdProperty);
+        graph.executeWrite(constraintQuery);
+        graph.refreshSchema();
+    }
+
+    public void addGraphDocuments(List<GraphDocument> graphDocuments, boolean includeSource) {
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            Document source = graphDoc.source();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data", graphDoc.nodes().stream().map(Neo4jUtils::toMap).toList());
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(idProperty)) {
+                    metadata.put(idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(includeSource);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.relationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.sourceNode().id(),
+                            "source_label", rel.sourceNode().type(),
+                            "target", rel.targetNode().id(),
+                            "target_label", rel.targetNode().type(),
+                            "type", rel.type().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery();
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private String getNodeImportQuery(boolean includeSource) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource);
+        final String withDocsRel = includeSource ? String.format("MERGE (d)-[:%s]->(source) ", relType) : "";
+
+        return includeDocsQuery + "UNWIND $data AS row "
+                + String.format("MERGE (source:%1$s {%2$s: row.id}) ", sanitizedLabel, sanitizedIdProperty)
+                + withDocsRel
+                + "WITH source, row "
+                + "SET source:$(row.type) "
+                + "RETURN count(*) as total";
+    }
+
+    private String getIncludeDocsQuery(boolean includeSource) {
+        if (!includeSource) {
+            return "";
+        }
+        return String.format(
+                """
+                        MERGE (d:Document {%1$s: $document.metadata.%1$s})
+                        SET d.%2$s = $document.text
+                        SET d += $document.metadata
+                        WITH d
+                        """,
+                sanitizedIdProperty, sanitizedTextProperty);
+    }
+
+    private String getRelImportQuery() {
+
+        return String.format(
+                """
+                        UNWIND $data AS row
+                        MERGE (source:%1$s {%2$s: row.source})
+                        MERGE (target:%1$s {%2$s: row.target})
+                        WITH source, target, row
+                        MERGE (source)-[rel:$(toString(row.type) + '')]->(target)
+                        RETURN distinct 'done'
+                        """,
+                sanitizedLabel, sanitizedIdProperty);
+    }
+
+    public static class Builder {
+
+        private String label;
+        private String idProperty;
+        private String textProperty;
+        private String relType;
+        private String constraintName;
+        private Neo4jGraph graph;
+
+        /**
+         * @param graph the {@link Neo4jGraph} (required)
+         */
+        public Builder graph(Neo4jGraph graph) {
+            this.graph = graph;
+            return this;
+        }
+
+        /**
+         * @param idProperty the entity id, to be used with {@link #addGraphDocuments(List, boolean)}
+         */
+        public Builder idProperty(String idProperty) {
+            this.idProperty = idProperty;
+            return this;
+        }
+
+        /**
+         * @param textProperty the document text, to be used with {@link #addGraphDocuments(List, boolean)}
+         *                     if the second parameter is true
+         */
+        public Builder textProperty(String textProperty) {
+            this.textProperty = textProperty;
+            return this;
+        }
+
+        /**
+         * @param label the entity label, to be used with {@link #addGraphDocuments(List, boolean)}
+         *              (default: "__Entity__")
+         */
+        public Builder label(String label) {
+            this.label = label;
+            return this;
+        }
+
+        /**
+         * @param relType the entity relationship types, to be used with {@link #addGraphDocuments(List, boolean)}
+         *                (default "HAS_ENTITY")
+         */
+        public Builder relType(String relType) {
+            this.relType = relType;
+            return this;
+        }
+
+        /**
+         * @param constraintName the constraint name (default: "knowledge_cons")
+         */
+        public Builder constraintName(String constraintName) {
+            this.constraintName = constraintName;
+            return this;
+        }
+
+        public KnowledgeGraphWriter build() {
+            return new KnowledgeGraphWriter(graph, idProperty, label, textProperty, relType, constraintName);
+        }
+    }
+}
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jContentRetriever.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jContentRetriever.java
deleted file mode 100644
index f9a9424bb..000000000
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jContentRetriever.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package dev.langchain4j.community.rag.content.retriever.neo4j;
-
-import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.input.PromptTemplate;
-
-/**
- * @deprecated use {@link Neo4jText2CypherRetriever} instead
- */
-@Deprecated(forRemoval = true)
-public class Neo4jContentRetriever extends Neo4jText2CypherRetriever {
-
-    public Neo4jContentRetriever(Neo4jGraph graph, ChatModel chatModel, PromptTemplate promptTemplate) {
-        super(graph, chatModel, promptTemplate, null);
-    }
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    public static class Builder extends Neo4jText2CypherRetriever.Builder<Builder> {
-
-        @Override
-        public Neo4jContentRetriever build() {
-            return new Neo4jContentRetriever(graph, chatModel, promptTemplate);
-        }
-    }
-}
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraph.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraph.java
index 394b97e63..9175258c3 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraph.java
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraph.java
@@ -6,7 +6,9 @@
 import dev.langchain4j.internal.ValidationUtils;
 import java.util.List;
 import java.util.Map;
+import org.neo4j.driver.AuthTokens;
 import org.neo4j.driver.Driver;
+import org.neo4j.driver.GraphDatabase;
 import org.neo4j.driver.Record;
 import org.neo4j.driver.Session;
 import org.neo4j.driver.exceptions.ClientException;
@@ -14,40 +16,6 @@
 
 public class Neo4jGraph implements AutoCloseable {
 
-    public static class Builder {
-        private Driver driver;
-        private Long sample;
-        private Long maxRels;
-
-        /**
-         * @param driver the {@link Driver} (required)
-         */
-        public Builder driver(Driver driver) {
-            this.driver = driver;
-            return this;
-        }
-
-        /**
-         * @param sample number of nodes to sample per label with the `apoc.meta.data` procedure (default: 1000)
-         */
-        public Builder sample(Long sample) {
-            this.sample = sample;
-            return this;
-        }
-
-        /**
-         * @param maxRels the maximum number of relationships to look at per Node Label with the `apoc.meta.data` procedure (default: 100)
-         */
-        public Builder maxRels(Long maxRels) {
-            this.maxRels = maxRels;
-            return this;
-        }
-
-        public Neo4jGraph build() {
-            return new Neo4jGraph(driver, sample, maxRels);
-        }
-    }
-
     private final Driver driver;
     private final Long sample;
     private final Long maxRels;
@@ -74,14 +42,14 @@ public String getSchema() {
         return schema;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    public ResultSummary executeWrite(String queryString) {
+        return executeWrite(queryString, Map.of());
     }
 
-    public ResultSummary executeWrite(String queryString) {
+    public ResultSummary executeWrite(String queryString, Map<String, Object> params) {
 
         try (Session session = this.driver.session()) {
-            return session.executeWrite(tx -> tx.run(queryString).consume());
+            return session.executeWrite(tx -> tx.run(queryString, params).consume());
         } catch (ClientException e) {
             throw new Neo4jException("Error executing query: " + queryString, e);
         }
@@ -106,7 +74,56 @@ public void refreshSchema() {
 
     @Override
     public void close() {
-
         this.driver.close();
     }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        private Driver driver;
+        private Long sample;
+        private Long maxRels;
+
+        /**
+         * @param driver the {@link Driver} (required)
+         */
+        public Builder driver(Driver driver) {
+            this.driver = driver;
+            return this;
+        }
+
+        /**
+         * @param sample number of nodes to sample per label with the `apoc.meta.data` procedure (default: 1000)
+         */
+        public Builder sample(Long sample) {
+            this.sample = sample;
+            return this;
+        }
+
+        /**
+         * @param maxRels the maximum number of relationships to look at per Node Label with the `apoc.meta.data` procedure (default: 100)
+         */
+        public Builder maxRels(Long maxRels) {
+            this.maxRels = maxRels;
+            return this;
+        }
+
+        /**
+         * Creates an instance a {@link Driver}, starting from uri, user and password
+         *
+         * @param uri      the Bolt URI to a Neo4j instance
+         * @param user     the Neo4j instance's username
+         * @param password the Neo4j instance's password
+         */
+        public Builder withBasicAuth(String uri, String user, String password) {
+            this.driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password));
+            return this;
+        }
+
+        public Neo4jGraph build() {
+            return new Neo4jGraph(driver, sample, maxRels);
+        }
+    }
 }
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphSchemaUtils.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphSchemaUtils.java
index 921e59718..129873f0e 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphSchemaUtils.java
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphSchemaUtils.java
@@ -1,23 +1,26 @@
 package dev.langchain4j.community.rag.content.retriever.neo4j;
 
+import dev.langchain4j.Internal;
 import java.util.Map;
 import org.neo4j.driver.Record;
 
-public class Neo4jGraphSchemaUtils {
+@Internal
+class Neo4jGraphSchemaUtils {
+
     static final String SCHEMA_FROM_META_DATA =
             """
-            CALL apoc.meta.data({maxRels: $maxRels, sample: $sample})
-            YIELD label, other, elementType, type, property
-            WITH label, elementType,
-                 apoc.text.join(collect(case when NOT type = "RELATIONSHIP" then property+": "+type else null end),", ") AS properties,
-                 collect(case when type = "RELATIONSHIP" AND elementType = "node" then "(:" + label + ")-[:" + property + "]->(:" + toString(other[0]) + ")" else null end) AS patterns
-            WITH elementType AS type,
-                apoc.text.join(collect(":"+label+" {"+properties+"}"),"\\n") AS entities,
-                apoc.text.join(apoc.coll.flatten(collect(coalesce(patterns,[]))),"\\n") AS patterns
-            RETURN collect(case type when "relationship" then entities end)[0] AS relationships,
-                collect(case type when "node" then entities end)[0] AS nodes,
-                collect(case type when "node" then patterns end)[0] as patterns
-            """;
+                    CALL apoc.meta.data({maxRels: $maxRels, sample: $sample})
+                    YIELD label, other, elementType, type, property
+                    WITH label, elementType,
+                         apoc.text.join(collect(case when NOT type = "RELATIONSHIP" then property+": "+type else null end),", ") AS properties,
+                         collect(case when type = "RELATIONSHIP" AND elementType = "node" then "(:" + label + ")-[:" + property + "]->(:" + toString(other[0]) + ")" else null end) AS patterns
+                    WITH elementType AS type,
+                        apoc.text.join(collect(":"+label+" {"+properties+"}"),"\\n") AS entities,
+                        apoc.text.join(apoc.coll.flatten(collect(coalesce(patterns,[]))),"\\n") AS patterns
+                    RETURN collect(case type when "relationship" then entities end)[0] AS relationships,
+                        collect(case type when "node" then entities end)[0] AS nodes,
+                        collect(case type when "node" then patterns end)[0] as patterns
+                    """;
 
     static String getSchemaFromMetadata(Neo4jGraph graph, Long sample, Long maxRels) {
         final Record record = graph.executeRead(SCHEMA_FROM_META_DATA, Map.of("sample", sample, "maxRels", maxRels))
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetriever.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetriever.java
index 1a3a754b6..27aaaac5e 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetriever.java
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetriever.java
@@ -1,5 +1,6 @@
 package dev.langchain4j.community.rag.content.retriever.neo4j;
 
+import static dev.langchain4j.community.rag.content.retriever.neo4j.Neo4jUtils.getBacktickText;
 import static dev.langchain4j.internal.Utils.getOrDefault;
 import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
 
@@ -11,8 +12,6 @@
 import dev.langchain4j.rag.query.Query;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import org.neo4j.driver.Record;
 import org.neo4j.driver.types.Type;
 import org.neo4j.driver.types.TypeSystem;
@@ -36,7 +35,6 @@ public class Neo4jText2CypherRetriever implements ContentRetriever {
                     The question is: {{question}}
                     """);
 
-    private static final Pattern BACKTICKS_PATTERN = Pattern.compile("```(.*?)```", Pattern.MULTILINE | Pattern.DOTALL);
     private static final Type NODE = TypeSystem.getDefault().NODE();
     private static final Type RELATIONSHIP = TypeSystem.getDefault().RELATIONSHIP();
     private static final Type PATH = TypeSystem.getDefault().PATH();
@@ -49,10 +47,10 @@ public class Neo4jText2CypherRetriever implements ContentRetriever {
     private final List<String> examples;
 
     public Neo4jText2CypherRetriever(
-            Neo4jGraph graph, ChatModel ChatModel, PromptTemplate promptTemplate, List<String> examples) {
+            Neo4jGraph graph, ChatModel chatModel, PromptTemplate promptTemplate, List<String> examples) {
 
         this.graph = ensureNotNull(graph, "graph");
-        this.chatModel = ensureNotNull(ChatModel, "chatModel");
+        this.chatModel = ensureNotNull(chatModel, "chatModel");
         this.promptTemplate = getOrDefault(promptTemplate, DEFAULT_PROMPT_TEMPLATE);
         this.examples = getOrDefault(examples, List.of());
     }
@@ -97,11 +95,7 @@ private String generateCypherQuery(String schema, String question) {
                 Map.of("schema", schema, "question", question, "examples", examplesString);
         Prompt cypherPrompt = promptTemplate.apply(templateVariables);
         String cypherQuery = chatModel.chat(cypherPrompt.text());
-        Matcher matcher = BACKTICKS_PATTERN.matcher(cypherQuery);
-        if (matcher.find()) {
-            return matcher.group(1);
-        }
-        return cypherQuery;
+        return getBacktickText(cypherQuery);
     }
 
     private List<String> executeQuery(String cypherQuery) {
@@ -120,7 +114,7 @@ private List<String> executeQuery(String cypherQuery) {
                 .toList();
     }
 
-    public static class Builder<T extends Builder<T>> {
+    public static class Builder {
 
         protected Neo4jGraph graph;
         protected ChatModel chatModel;
@@ -130,37 +124,33 @@ public static class Builder<T extends Builder<T>> {
         /**
          * @param graph the {@link Neo4jGraph} (required)
          */
-        public T graph(Neo4jGraph graph) {
+        public Builder graph(Neo4jGraph graph) {
             this.graph = graph;
-            return self();
+            return this;
         }
 
         /**
          * @param chatModel the {@link ChatModel} (required)
          */
-        public T chatModel(ChatModel chatModel) {
+        public Builder chatModel(ChatModel chatModel) {
             this.chatModel = chatModel;
-            return self();
+            return this;
         }
 
         /**
          * @param promptTemplate the {@link PromptTemplate} (optional, default is {@link Neo4jText2CypherRetriever#DEFAULT_PROMPT_TEMPLATE})
          */
-        public T promptTemplate(PromptTemplate promptTemplate) {
+        public Builder promptTemplate(PromptTemplate promptTemplate) {
             this.promptTemplate = promptTemplate;
-            return self();
+            return this;
         }
 
         /**
          * @param examples the few-shot examples to improve retrieving (optional, default is "")
          */
-        public T examples(List<String> examples) {
+        public Builder examples(List<String> examples) {
             this.examples = examples;
-            return self();
-        }
-
-        protected T self() {
-            return (T) this;
+            return this;
         }
 
         public Neo4jText2CypherRetriever build() {
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jUtils.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jUtils.java
new file mode 100644
index 000000000..f2b7e5a34
--- /dev/null
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jUtils.java
@@ -0,0 +1,67 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static org.neo4j.cypherdsl.support.schema_name.SchemaNames.sanitize;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.langchain4j.Internal;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Internal
+class Neo4jUtils {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Pattern BACKTICKS_PATTERN = Pattern.compile("```(.*?)```", Pattern.MULTILINE | Pattern.DOTALL);
+
+    /**
+     * Get the text block wrapped by triple backticks
+     */
+    static String getBacktickText(String cypherQuery) {
+        Matcher matcher = BACKTICKS_PATTERN.matcher(cypherQuery);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        return cypherQuery;
+    }
+
+    static String sanitizeOrThrows(String value, String config) {
+        return sanitize(value).orElseThrow(() -> {
+            String invalidSanitizeValue = String.format(
+                    "The value %s, to assign to configuration %s, cannot be safely quoted", value, config);
+            return new RuntimeException(invalidSanitizeValue);
+        });
+    }
+
+    static Map<String, Object> toMap(Object object) {
+        return OBJECT_MAPPER.convertValue(object, new TypeReference<>() {});
+    }
+
+    static String getStringFromListOfMaps(List<Map<String, String>> list) {
+        try {
+            return OBJECT_MAPPER.writeValueAsString(list);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static String generateMD5(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            md.update(input.getBytes());
+            byte[] digest = md.digest();
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("MD5 algorithm not found", e);
+        }
+    }
+}
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterBaseTest.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterBaseTest.java
new file mode 100644
index 000000000..c087a0281
--- /dev/null
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterBaseTest.java
@@ -0,0 +1,417 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter.DEFAULT_ID_PROP;
+import static dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter.DEFAULT_LABEL;
+import static dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter.DEFAULT_REL_TYPE;
+import static dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter.DEFAULT_TEXT_PROP;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import dev.langchain4j.community.data.document.graph.GraphDocument;
+import dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformer;
+import dev.langchain4j.data.document.DefaultDocument;
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.document.Metadata;
+import dev.langchain4j.model.chat.ChatModel;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.neo4j.cypherdsl.support.schema_name.SchemaNames;
+import org.neo4j.driver.Record;
+import org.neo4j.driver.internal.util.Iterables;
+import org.neo4j.driver.internal.value.PathValue;
+import org.neo4j.driver.types.Node;
+import org.neo4j.driver.types.Path;
+import org.neo4j.driver.types.Relationship;
+import org.testcontainers.containers.Neo4jContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+@Testcontainers
+abstract class Neo4jKnowledgeGraphWriterBaseTest {
+
+    private static final String EXAMPLES_PROMPT =
+            """
+                    [
+                       {
+                          "tail":"Microsoft",
+                          "head":"Adam",
+                          "head_type":"Person",
+                          "text":"Adam is a software engineer in Microsoft since 2009, and last year he got an award as the Best Talent",
+                          "relation":"WORKS_FOR",
+                          "tail_type":"Company"
+                       },
+                       {
+                          "tail":"Best Talent",
+                          "head":"Adam",
+                          "head_type":"Person",
+                          "text":"Adam is a software engineer in Microsoft since 2009, and last year he got an award as the Best Talent",
+                          "relation":"HAS_AWARD",
+                          "tail_type":"Award"
+                       },
+                       {
+                          "tail":"Microsoft",
+                          "head":"Microsoft Word",
+                          "head_type":"Product",
+                          "text":"Microsoft is a tech company that provide several products such as Microsoft Word",
+                          "relation":"PRODUCED_BY",
+                          "tail_type":"Company"
+                       },
+                       {
+                          "tail":"lightweight app",
+                          "head":"Microsoft Word",
+                          "head_type":"Product",
+                          "text":"Microsoft Word is a lightweight app that accessible offline",
+                          "relation":"HAS_CHARACTERISTIC",
+                          "tail_type":"Characteristic"
+                       },
+                       {
+                          "tail":"accessible offline",
+                          "head":"Microsoft Word",
+                          "head_type":"Product",
+                          "text":"Microsoft Word is a lightweight app that accessible offline",
+                          "relation":"HAS_CHARACTERISTIC",
+                          "tail_type":"Characteristic"
+                       }
+                    ]
+                    """;
+
+    public static final String ON = "on";
+    public static final String KEY_CAT = "key2";
+    public static final String VALUE_CAT = "value2";
+    public static final String KEY_KEANU = "key33";
+    public static final String VALUE_KEANU = "value3";
+    public static String USERNAME = "neo4j";
+    public static String ADMIN_PASSWORD = "adminPass";
+    private static final String NEO4J_VERSION = System.getProperty("neo4jVersion", "2025.01.0-enterprise");
+
+    public static String CAT_ON_THE_TABLE = "Sylvester the cat is on the table";
+    public static String KEANU_REEVES_ACTED = "Keanu Reeves acted in Matrix";
+    public static String MATCH_AND_RETURN_NODE = "MATCH p=(n)-[]->() RETURN p ORDER BY n.%s";
+    public static String MATCH_WITH_DOCUMENT_RETURN_NODE = "MATCH p=(:Document)-[]->(n)-[]->() RETURN p ORDER BY n.%s";
+    public static String KEANU = "keanu";
+    public static String MATRIX = "matrix";
+    public static String ACTED = "acted";
+    public static String SYLVESTER = "sylvester";
+    public static String TABLE = "table";
+    public static LLMGraphTransformer graphTransformer;
+    public static List<GraphDocument> graphDocs;
+    public static Neo4jGraph neo4jGraph;
+    public static KnowledgeGraphWriter knowledgeGraphWriter;
+
+    public static String CUSTOM_TEXT = "custom  `text";
+    public static String CUSTOM_ID = "custom  ` id";
+    public static final String SANITIZED_CUSTOM_ID =
+            SchemaNames.sanitize(CUSTOM_ID).get();
+    public static String CUSTOM_LABEL = "Label ` to \\ sanitize";
+
+    @Container
+    static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:" + NEO4J_VERSION))
+            .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")
+            .withAdminPassword(ADMIN_PASSWORD)
+            .withPlugins("apoc");
+
+    @BeforeEach
+    void beforeAll() {
+        ChatModel model = getModel();
+
+        graphTransformer = LLMGraphTransformer.builder()
+                .model(model)
+                .examples(EXAMPLES_PROMPT)
+                .build();
+        neo4jGraph = Neo4jGraph.builder()
+                .withBasicAuth(neo4jContainer.getBoltUrl(), USERNAME, ADMIN_PASSWORD)
+                .build();
+        knowledgeGraphWriter = KnowledgeGraphWriter.builder().graph(neo4jGraph).build();
+
+        // given
+        Document docKeanu = new DefaultDocument(KEANU_REEVES_ACTED, Metadata.from(KEY_KEANU, VALUE_KEANU));
+        Document docCat = new DefaultDocument(CAT_ON_THE_TABLE, Metadata.from(KEY_CAT, VALUE_CAT));
+        List<Document> documents = List.of(docCat, docKeanu);
+
+        graphDocs = graphTransformer.transformAll(documents);
+        assertThat(graphDocs.size()).isEqualTo(2);
+    }
+
+    abstract ChatModel getModel();
+
+    @AfterEach
+    void afterEach() {
+        neo4jGraph.executeWrite("MATCH (n) DETACH DELETE n");
+    }
+
+    @AfterAll
+    static void afterAll() {
+        neo4jGraph.close();
+    }
+
+    @Test
+    void testAddGraphDocuments() {
+
+        addGraphDocumentsCommon();
+
+        // retry to check that merge works correctly
+        addGraphDocumentsCommon();
+    }
+
+    @Test
+    void testAddGraphDocumentsWithCustomIdTextAndLabel() {
+
+        knowledgeGraphWriter = KnowledgeGraphWriter.builder()
+                .graph(neo4jGraph)
+                .textProperty(CUSTOM_TEXT)
+                .idProperty(CUSTOM_ID)
+                .label(CUSTOM_LABEL)
+                .build();
+
+        addGraphDocumentsWithCustomIdTextAndLabelCommon();
+
+        // retry to check that merge works correctly
+        addGraphDocumentsWithCustomIdTextAndLabelCommon();
+    }
+
+    @Test
+    void testAddGraphDocumentsWithIncludeSource() {
+        testWithIncludeSourceCommon(DEFAULT_REL_TYPE);
+
+        // retry to check that merge works correctly
+        testWithIncludeSourceCommon(DEFAULT_REL_TYPE);
+    }
+
+    @Test
+    void testAddGraphDocumentsWithIncludeSourceAndCustomRelType() {
+        final String customRelType = "CUSTOM_REL_TYPE";
+        knowledgeGraphWriter = KnowledgeGraphWriter.builder()
+                .graph(neo4jGraph)
+                .relType(customRelType)
+                .build();
+
+        testWithIncludeSourceCommon(customRelType);
+
+        // retry to check that merge works correctly
+        testWithIncludeSourceCommon(customRelType);
+    }
+
+    @Test
+    void testAddGraphDocumentsWithIncludeSourceAndCustomIdTextAndLabel() {
+
+        knowledgeGraphWriter = KnowledgeGraphWriter.builder()
+                .graph(Neo4jKnowledgeGraphWriterBaseTest.neo4jGraph)
+                .textProperty(CUSTOM_TEXT)
+                .idProperty(CUSTOM_ID)
+                .label(CUSTOM_LABEL)
+                .build();
+
+        testWithIncludeSourceAndCustomIdTextAndLabelCommon(DEFAULT_REL_TYPE);
+
+        // retry to check that merge works correctly
+        testWithIncludeSourceAndCustomIdTextAndLabelCommon(DEFAULT_REL_TYPE);
+    }
+
+    @Test
+    void testAddGraphDocumentsWithIncludeSourceAndCustomIdTextLabelRelType() {
+
+        final String customRelType = "CUSTOM_REL_TYPE";
+        knowledgeGraphWriter = KnowledgeGraphWriter.builder()
+                .graph(neo4jGraph)
+                .textProperty(CUSTOM_TEXT)
+                .idProperty(CUSTOM_ID)
+                .label(CUSTOM_LABEL)
+                .relType(customRelType)
+                .build();
+
+        testWithIncludeSourceAndCustomIdTextAndLabelCommon(customRelType);
+
+        // retry to check that merge works correctly
+        testWithIncludeSourceAndCustomIdTextAndLabelCommon(customRelType);
+    }
+
+    private static void addGraphDocumentsCommon() {
+        // when
+        knowledgeGraphWriter.addGraphDocuments(graphDocs, false);
+
+        // then
+        List<Record> records = neo4jGraph.executeRead(MATCH_AND_RETURN_NODE.formatted(DEFAULT_ID_PROP));
+        assertThat(records).hasSize(2);
+        Record record = records.get(0);
+        PathValue p = (PathValue) record.get("p");
+        Path path = p.asPath();
+        Node start = path.start();
+        assertNodeLabels(start, DEFAULT_LABEL);
+        assertNodeProps(start, KEANU, DEFAULT_ID_PROP);
+        Node end = path.end();
+        assertNodeLabels(end, DEFAULT_LABEL);
+        assertNodeProps(end, MATRIX, DEFAULT_ID_PROP);
+        Relationship rel = Iterables.single(path.relationships());
+        assertThat(rel.type()).containsIgnoringCase(ACTED);
+
+        record = records.get(1);
+        p = (PathValue) record.get("p");
+        path = p.asPath();
+        start = path.start();
+        assertNodeLabels(start, DEFAULT_LABEL);
+        assertNodeProps(start, SYLVESTER, DEFAULT_ID_PROP);
+        end = path.end();
+        assertNodeLabels(end, DEFAULT_LABEL);
+        assertNodeProps(end, TABLE, DEFAULT_ID_PROP);
+        rel = Iterables.single(path.relationships());
+        assertThat(rel.type()).containsIgnoringCase(ON);
+    }
+
+    private static void addGraphDocumentsWithCustomIdTextAndLabelCommon() {
+        knowledgeGraphWriter.addGraphDocuments(graphDocs, false);
+
+        List<Record> records = neo4jGraph.executeRead(MATCH_AND_RETURN_NODE.formatted(SANITIZED_CUSTOM_ID));
+        assertThat(records).hasSize(2);
+        Record record = records.get(0);
+        PathValue p = (PathValue) record.get("p");
+        Path path = p.asPath();
+        Node start = path.start();
+        assertNodeLabels(start, CUSTOM_LABEL);
+        assertNodeProps(start, KEANU, CUSTOM_ID);
+        Node end = path.end();
+        assertNodeLabels(end, CUSTOM_LABEL);
+        assertNodeProps(end, MATRIX, CUSTOM_ID);
+        Relationship rel = Iterables.single(path.relationships());
+        assertThat(rel.type()).containsIgnoringCase(ACTED);
+
+        record = records.get(1);
+        p = (PathValue) record.get("p");
+        path = p.asPath();
+        start = path.start();
+        assertNodeLabels(start, CUSTOM_LABEL);
+        assertNodeProps(start, SYLVESTER, CUSTOM_ID);
+        end = path.end();
+        assertNodeLabels(end, CUSTOM_LABEL);
+        assertNodeProps(end, TABLE, CUSTOM_ID);
+        rel = Iterables.single(path.relationships());
+        assertThat(rel.type()).containsIgnoringCase(ON);
+    }
+
+    private static void testWithIncludeSourceCommon(String relType) {
+        // when
+        knowledgeGraphWriter.addGraphDocuments(graphDocs, true);
+
+        // then
+        List<Record> records = neo4jGraph.executeRead(MATCH_WITH_DOCUMENT_RETURN_NODE.formatted(DEFAULT_ID_PROP));
+        assertThat(records).hasSize(2);
+        Record record = records.get(0);
+        PathValue p = (PathValue) record.get("p");
+        Path path = p.asPath();
+        Iterator<Node> iterator = path.nodes().iterator();
+        Node node = iterator.next();
+        assertThat(node.labels()).hasSize(1);
+        assertionsDocument(node, DEFAULT_ID_PROP, DEFAULT_TEXT_PROP, KEANU_REEVES_ACTED, KEY_KEANU, VALUE_KEANU);
+
+        node = iterator.next();
+        assertNodeLabels(node, DEFAULT_LABEL);
+        assertNodeProps(node, KEANU, DEFAULT_ID_PROP);
+
+        node = iterator.next();
+        assertNodeLabels(node, DEFAULT_LABEL);
+        assertNodeProps(node, MATRIX, DEFAULT_ID_PROP);
+        List<Relationship> rels = Iterables.asList(path.relationships());
+        assertThat(rels).hasSize(2);
+        assertThat(rels.get(0).type()).containsIgnoringCase(relType);
+        assertThat(rels.get(1).type()).containsIgnoringCase(ACTED);
+
+        record = records.get(1);
+        p = (PathValue) record.get("p");
+        path = p.asPath();
+        iterator = path.nodes().iterator();
+        node = iterator.next();
+        assertThat(node.labels()).hasSize(1);
+        assertionsDocument(node, DEFAULT_ID_PROP, DEFAULT_TEXT_PROP, CAT_ON_THE_TABLE, KEY_CAT, VALUE_CAT);
+
+        node = iterator.next();
+        assertNodeLabels(node, DEFAULT_LABEL);
+        assertNodeProps(node, SYLVESTER, DEFAULT_ID_PROP);
+
+        node = iterator.next();
+        assertNodeLabels(node, DEFAULT_LABEL);
+        assertNodeProps(node, TABLE, DEFAULT_ID_PROP);
+        rels = Iterables.asList(path.relationships());
+        assertThat(rels).hasSize(2);
+        assertThat(rels.get(0).type()).containsIgnoringCase(relType);
+        assertThat(rels.get(1).type()).containsIgnoringCase(ON);
+    }
+
+    private static void testWithIncludeSourceAndCustomIdTextAndLabelCommon(String relType) {
+        knowledgeGraphWriter.addGraphDocuments(graphDocs, true);
+
+        List<Record> records = neo4jGraph.executeRead(MATCH_WITH_DOCUMENT_RETURN_NODE.formatted(SANITIZED_CUSTOM_ID));
+        assertThat(records).hasSize(2);
+        Record record = records.get(0);
+        PathValue p = (PathValue) record.get("p");
+        Path path = p.asPath();
+        Iterator<Node> iterator = path.nodes().iterator();
+        Node node = iterator.next();
+        assertThat(node.labels()).hasSize(1);
+        assertionsDocument(node, CUSTOM_ID, CUSTOM_TEXT, KEANU_REEVES_ACTED, KEY_KEANU, VALUE_KEANU);
+
+        node = iterator.next();
+        assertNodeLabels(node, CUSTOM_LABEL);
+        assertNodeProps(node, KEANU, CUSTOM_ID);
+
+        node = iterator.next();
+        assertNodeLabels(node, CUSTOM_LABEL);
+        assertNodeProps(node, MATRIX, CUSTOM_ID);
+        List<Relationship> rels = Iterables.asList(path.relationships());
+        assertThat(rels).hasSize(2);
+        assertThat(rels.get(0).type()).containsIgnoringCase(relType);
+        assertThat(rels.get(1).type()).containsIgnoringCase(ACTED);
+
+        record = records.get(1);
+        p = (PathValue) record.get("p");
+        path = p.asPath();
+        iterator = path.nodes().iterator();
+        node = iterator.next();
+        assertThat(node.labels()).hasSize(1);
+        assertionsDocument(node, CUSTOM_ID, CUSTOM_TEXT, CAT_ON_THE_TABLE, KEY_CAT, VALUE_CAT);
+
+        node = iterator.next();
+        assertNodeLabels(node, CUSTOM_LABEL);
+        assertNodeProps(node, SYLVESTER, CUSTOM_ID);
+
+        node = iterator.next();
+        assertNodeLabels(node, CUSTOM_LABEL);
+        assertNodeProps(node, TABLE, CUSTOM_ID);
+        rels = Iterables.asList(path.relationships());
+        assertThat(rels).hasSize(2);
+        assertThat(rels.get(0).type()).containsIgnoringCase(relType);
+        assertThat(rels.get(1).type()).containsIgnoringCase(ON);
+    }
+
+    private static void assertNodeLabels(Node start, String entityLabel) {
+        Iterable<String> labels = start.labels();
+        assertThat(labels).hasSize(2);
+        assertThat(labels).contains(entityLabel);
+    }
+
+    private static void assertNodeProps(Node start, String propRegex, String idProp) {
+        Map<String, Object> map = start.asMap();
+        assertThat(map).hasSize(1);
+        assertThat((String) map.get(idProp)).containsIgnoringCase(propRegex);
+    }
+
+    private static void assertionsDocument(
+            Node start,
+            String idProp,
+            String textProp,
+            String expectedText,
+            String expectedMetaKey,
+            String expectedMetaValue) {
+        Map<String, Object> map = start.asMap();
+        assertThat(map.size()).isEqualTo(3);
+        assertThat(map).containsKey(idProp);
+        Object text = map.get(textProp);
+        assertThat(text).isEqualTo(expectedText);
+
+        final Object actualMetaValue = map.get(expectedMetaKey);
+        assertThat(actualMetaValue).isEqualTo(expectedMetaValue);
+    }
+}
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterIT.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterIT.java
new file mode 100644
index 000000000..cc2f94477
--- /dev/null
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterIT.java
@@ -0,0 +1,23 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
+
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
+class Neo4jKnowledgeGraphWriterIT extends Neo4jKnowledgeGraphWriterBaseTest {
+
+    @Override
+    ChatModel getModel() {
+        return OpenAiChatModel.builder()
+                .baseUrl(System.getenv("OPENAI_BASE_URL"))
+                .apiKey(System.getenv("OPENAI_API_KEY"))
+                .organizationId(System.getenv("OPENAI_ORGANIZATION_ID"))
+                .modelName(GPT_4_O_MINI)
+                .logRequests(true)
+                .logResponses(true)
+                .build();
+    }
+}
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterTest.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterTest.java
new file mode 100644
index 000000000..ea85e5e2f
--- /dev/null
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jKnowledgeGraphWriterTest.java
@@ -0,0 +1,59 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.when;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class Neo4jKnowledgeGraphWriterTest extends Neo4jKnowledgeGraphWriterBaseTest {
+
+    @Mock
+    private static ChatModel model;
+
+    @Override
+    ChatModel getModel() {
+        final String keanuStringResponse =
+                "[{\"head_type\":\"Person\",\"text\":\"Keanu Reeves acted in Matrix\",\"relation\":\"ACTED_IN\",\"tail_type\":\"Movie\",\"tail\":\"Matrix\",\"head\":\"Keanu Reeves\"}]";
+        final ChatResponse chatResponseKeanu = new ChatResponse.Builder()
+                .aiMessage(new AiMessage(keanuStringResponse))
+                .build();
+        final List<ChatMessage> keanuMessages =
+                argThat(arg -> arg != null && arg.toString().toLowerCase().contains("keanu"));
+        when(model.chat(keanuMessages)).thenReturn(chatResponseKeanu);
+
+        final String sylvesterStringResponse =
+                "[{\"tail_type\":\"Location\",\"tail\":\"table\",\"head\":\"Sylvester the cat\",\"head_type\":\"Animal\",\"text\":\"Sylvester the cat is on the table\",\"relation\":\"IS_ON\"}]";
+        final ChatResponse chatResponseSylvester = new ChatResponse.Builder()
+                .aiMessage(new AiMessage(sylvesterStringResponse))
+                .build();
+        final List<ChatMessage> sylvesterMessages =
+                argThat(arg -> arg != null && arg.toString().toLowerCase().contains("sylvester"));
+        when(model.chat(sylvesterMessages)).thenReturn(chatResponseSylvester);
+
+        return model;
+    }
+
+    @Test
+    void testWrongConstraintName() {
+        try {
+            knowledgeGraphWriter = KnowledgeGraphWriter.builder()
+                    .graph(neo4jGraph)
+                    .constraintName("111")
+                    .build();
+            fail("Should fail due to invalid input");
+        } catch (Exception e) {
+            assertThat(e.getMessage()).contains("Error executing query: CREATE CONSTRAINT 111");
+        }
+    }
+}
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverBaseTest.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverBaseTest.java
index f99c32a62..44c8025d4 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverBaseTest.java
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverBaseTest.java
@@ -11,7 +11,7 @@
 import org.testcontainers.containers.Neo4jContainer;
 import org.testcontainers.junit.jupiter.Container;
 
-public class Neo4jText2CypherRetrieverBaseTest {
+class Neo4jText2CypherRetrieverBaseTest {
 
     protected static final String NEO4J_VERSION = System.getProperty("neo4jVersion", "5.26");
 
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4JText2CypherRetrieverIT.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverIT.java
similarity index 98%
rename from content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4JText2CypherRetrieverIT.java
rename to content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverIT.java
index 1a0b26957..e5e493aa6 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4JText2CypherRetrieverIT.java
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverIT.java
@@ -16,7 +16,7 @@
 import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
 
 @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
-class Neo4JText2CypherRetrieverIT extends Neo4jText2CypherRetrieverBaseTest {
+class Neo4jText2CypherRetrieverIT extends Neo4jText2CypherRetrieverBaseTest {
 
     @Test
     void shouldRetrieveContentWhenQueryIsValidAndOpenAiChatModelIsUsed() {
diff --git a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4JText2CypherRetrieverTest.java b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverTest.java
similarity index 98%
rename from content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4JText2CypherRetrieverTest.java
rename to content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverTest.java
index 74194ff25..467aadbc6 100644
--- a/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4JText2CypherRetrieverTest.java
+++ b/content-retrievers/langchain4j-community-neo4j-retriever/src/test/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jText2CypherRetrieverTest.java
@@ -17,7 +17,7 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
-public class Neo4JText2CypherRetrieverTest extends Neo4jText2CypherRetrieverBaseTest {
+class Neo4jText2CypherRetrieverTest extends Neo4jText2CypherRetrieverBaseTest {
 
     private Neo4jText2CypherRetriever retriever;
 
diff --git a/document-transformers/langchain4j-community-llm-graph-transformer/pom.xml b/document-transformers/langchain4j-community-llm-graph-transformer/pom.xml
new file mode 100644
index 000000000..1e9840fe2
--- /dev/null
+++ b/document-transformers/langchain4j-community-llm-graph-transformer/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>dev.langchain4j</groupId>
+        <artifactId>langchain4j-community</artifactId>
+        <version>1.0.0-beta4-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>langchain4j-community-llm-graph-transformer</artifactId>
+    <packaging>jar</packaging>
+    <name>LangChain4j :: Community :: Graph Transformer :: LLM</name>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-community-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <!-- test dependencies -->
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-embeddings-all-minilm-l6-v2-q</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-open-ai</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/document-transformers/langchain4j-community-llm-graph-transformer/src/main/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformer.java b/document-transformers/langchain4j-community-llm-graph-transformer/src/main/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformer.java
new file mode 100644
index 000000000..6b63d0b85
--- /dev/null
+++ b/document-transformers/langchain4j-community-llm-graph-transformer/src/main/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformer.java
@@ -0,0 +1,276 @@
+package dev.langchain4j.community.data.document.transformer.graph;
+
+import static dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformerUtils.getBacktickText;
+import static dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.Experimental;
+import dev.langchain4j.community.data.document.graph.GraphDocument;
+import dev.langchain4j.community.data.document.graph.GraphEdge;
+import dev.langchain4j.community.data.document.graph.GraphNode;
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A llm-based graph transformer which transforms documents into graph-based documents using a LLM.
+ *
+ * @since 1.0.0-beta4
+ */
+@Experimental
+public class LLMGraphTransformer implements GraphTransformer {
+
+    private static final String DEFAULT_NODE_TYPE = "Node";
+    private static final PromptTemplate SYSTEM_TEMPLATE = PromptTemplate.from(
+            """
+                    You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
+                    Your task is to identify entities and relations from a given text and generate output in JSON format.
+                    Each object should have keys: 'head', 'head_type', 'relation', 'tail', and 'tail_type'.
+                    {{nodes}}
+                    {{rels}}
+                    IMPORTANT NOTES:\n- Don't add any explanation or extra text.
+                    {{additional}}
+                    """);
+    private static final PromptTemplate USER_TEMPLATE = PromptTemplate.from(
+            """
+                    Based on the following example, extract entities and relations from the provided text.
+                    {{nodes}}
+                    {{rels}}
+                    Below are a number of examples of text and their extracted entities and relationships.
+                    {{examples}}
+                    {{additional}}
+                    For the following text, extract entities and relations as in the provided example.
+                    Text: {{input}}
+                    """);
+
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String examples;
+    private final String additionalInstructions;
+    private final ChatModel chatModel;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param chatModel              the {@link ChatModel} (required)
+     * @param allowedNodes           Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships   Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt                 The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param maxAttempts            Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel chatModel,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            String examples,
+            Integer maxAttempts) {
+
+        this.chatModel = ensureNotNull(chatModel, "chatModel");
+        this.examples = ensureNotNull(examples, "examples");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public List<ChatMessage> createUnstructuredPrompt(String text) {
+        if (prompt != null && !prompt.isEmpty()) {
+            return prompt;
+        }
+
+        boolean withAllowedNodes = allowedNodes != null && !allowedNodes.isEmpty();
+        boolean withAllowedRels = allowedRelationships != null && !allowedRelationships.isEmpty();
+
+        SystemMessage systemMessage = SYSTEM_TEMPLATE
+                .apply(Map.of(
+                        "nodes",
+                        withAllowedNodes ? "The 'head_type' and 'tail_type' must be one of: " + allowedNodes : "",
+                        "rels",
+                        withAllowedRels ? "The 'relation' must be one of: " + allowedRelationships : "",
+                        "additional",
+                        additionalInstructions))
+                .toSystemMessage();
+        UserMessage userMessage = USER_TEMPLATE
+                .apply(Map.of(
+                        "nodes", withAllowedNodes ? "# ENTITY TYPES:\n" + allowedNodes : "",
+                        "rels", withAllowedRels ? "# RELATION TYPES:\n" + allowedRelationships : "",
+                        "examples", examples,
+                        "additional", additionalInstructions,
+                        "input", text))
+                .toUserMessage();
+
+        return List.of(systemMessage, userMessage);
+    }
+
+    @Override
+    public GraphDocument transform(Document document) {
+
+        String text = document.text();
+        List<ChatMessage> messages = createUnstructuredPrompt(text);
+
+        Set<GraphNode> nodesSet = new HashSet<>();
+        Set<GraphEdge> relationships = new HashSet<>();
+
+        List<Map<String, String>> parsedJson = getJsonResult(messages);
+        if (parsedJson == null || parsedJson.isEmpty()) {
+            return null;
+        }
+
+        for (Map<String, String> rel : parsedJson) {
+            if (!rel.containsKey("head") || !rel.containsKey("tail") || !rel.containsKey("relation")) {
+                continue;
+            }
+
+            GraphNode sourceNode = GraphNode.from(rel.get("head"), rel.getOrDefault("head_type", DEFAULT_NODE_TYPE));
+            GraphNode targetNode = GraphNode.from(rel.get("tail"), rel.getOrDefault("tail_type", DEFAULT_NODE_TYPE));
+
+            nodesSet.add(sourceNode);
+            nodesSet.add(targetNode);
+
+            String relation = rel.get("relation");
+            GraphEdge edge = GraphEdge.from(sourceNode, targetNode, relation);
+            relationships.add(edge);
+        }
+
+        if (nodesSet.isEmpty()) {
+            return null;
+        }
+
+        return new GraphDocument(nodesSet, relationships, document);
+    }
+
+    private List<Map<String, String>> getJsonResult(List<ChatMessage> messages) {
+
+        return RetryUtils.withRetry(
+                () -> {
+                    ChatResponse chat = chatModel.chat(messages);
+                    return parseJson(getBacktickText(chat.aiMessage().text()));
+                },
+                maxAttempts);
+    }
+
+    /**
+     * Builder class for LLMGraphTransformer.
+     */
+    public static class Builder {
+
+        private ChatModel model;
+        private List<String> allowedNodes;
+        private List<String> allowedRelationships;
+        private List<ChatMessage> prompt;
+        private String additionalInstructions = ""; // Default: empty string
+        private String examples; // Default examples
+        private Integer maxAttempts = 1; // Default: 1 attempt
+
+        /**
+         * Sets the required ChatModel.
+         *
+         * @param model the {@link ChatModel} (required)
+         * @return the Builder instance
+         */
+        public Builder model(ChatModel model) {
+            this.model = model;
+            return this;
+        }
+
+        /**
+         * Sets example instructions for the LLM.
+         *
+         * @param examples additional example instructions (required)
+         * @return the Builder instance
+         */
+        public Builder examples(String examples) {
+            this.examples = examples;
+            return this;
+        }
+
+        /**
+         * Sets the allowed node types in the graph.
+         *
+         * @param allowedNodes a list of allowed node types, null or empty allows all types (default: [])
+         * @return the Builder instance
+         */
+        public Builder allowedNodes(List<String> allowedNodes) {
+            this.allowedNodes = allowedNodes;
+            return this;
+        }
+
+        /**
+         * Sets the allowed relationship types in the graph.
+         *
+         * @param allowedRelationships a list of allowed relationship types, null or empty allows all (default: [])
+         * @return the Builder instance
+         */
+        public Builder allowedRelationships(List<String> allowedRelationships) {
+            this.allowedRelationships = allowedRelationships;
+            return this;
+        }
+
+        /**
+         * Sets the chat messages to pass to the LLM.
+         *
+         * @param prompt list of chat messages (optional)
+         * @return the Builder instance
+         */
+        public Builder prompt(List<ChatMessage> prompt) {
+            this.prompt = prompt;
+            return this;
+        }
+
+        /**
+         * Sets additional instructions for the prompt.
+         *
+         * @param additionalInstructions additional text for the prompt (default: '')
+         * @return the Builder instance
+         */
+        public Builder additionalInstructions(String additionalInstructions) {
+            this.additionalInstructions = additionalInstructions;
+            return this;
+        }
+
+        /**
+         * Sets the maximum number of retry attempts.
+         *
+         * @param maxAttempts number of attempts before failing (default: 1)
+         * @return the Builder instance
+         */
+        public Builder maxAttempts(Integer maxAttempts) {
+            this.maxAttempts = maxAttempts;
+            return this;
+        }
+
+        /**
+         * Builds and returns an instance of LLMGraphTransformer.
+         *
+         * @return a new instance of {@link LLMGraphTransformer}
+         * @throws IllegalArgumentException if required parameters are missing
+         */
+        public LLMGraphTransformer build() {
+            return new LLMGraphTransformer(
+                    model, allowedNodes, allowedRelationships, prompt, additionalInstructions, examples, maxAttempts);
+        }
+    }
+}
diff --git a/document-transformers/langchain4j-community-llm-graph-transformer/src/main/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformerUtils.java b/document-transformers/langchain4j-community-llm-graph-transformer/src/main/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformerUtils.java
new file mode 100644
index 000000000..14597cb24
--- /dev/null
+++ b/document-transformers/langchain4j-community-llm-graph-transformer/src/main/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformerUtils.java
@@ -0,0 +1,35 @@
+package dev.langchain4j.community.data.document.transformer.graph;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.langchain4j.Internal;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Internal
+class LLMGraphTransformerUtils {
+
+    private static final Pattern BACKTICKS_PATTERN = Pattern.compile("```(.*?)```", Pattern.MULTILINE | Pattern.DOTALL);
+
+    private LLMGraphTransformerUtils() throws InstantiationException {
+        throw new InstantiationException("Can't instantiate utility class.");
+    }
+
+    /**
+     * Get the text block wrapped by triple backticks
+     */
+    static String getBacktickText(String cypherQuery) {
+        Matcher matcher = BACKTICKS_PATTERN.matcher(cypherQuery);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        return cypherQuery;
+    }
+
+    static <T> T parseJson(String jsonString) throws JsonProcessingException {
+        ObjectMapper objectMapper = new ObjectMapper();
+
+        return objectMapper.readValue(jsonString, new TypeReference<>() {});
+    }
+}
diff --git a/document-transformers/langchain4j-community-llm-graph-transformer/src/test/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformerIT.java b/document-transformers/langchain4j-community-llm-graph-transformer/src/test/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformerIT.java
new file mode 100644
index 000000000..6f3bb7036
--- /dev/null
+++ b/document-transformers/langchain4j-community-llm-graph-transformer/src/test/java/dev/langchain4j/community/data/document/transformer/graph/LLMGraphTransformerIT.java
@@ -0,0 +1,231 @@
+package dev.langchain4j.community.data.document.transformer.graph;
+
+import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+import dev.langchain4j.community.data.document.graph.GraphDocument;
+import dev.langchain4j.community.data.document.graph.GraphEdge;
+import dev.langchain4j.community.data.document.graph.GraphNode;
+import dev.langchain4j.data.document.DefaultDocument;
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.document.Metadata;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
+class LLMGraphTransformerIT {
+
+    private static final String EXAMPLES_PROMPT =
+            """
+                    [
+                       {
+                          "tail":"Microsoft",
+                          "head":"Adam",
+                          "head_type":"Person",
+                          "text":"Adam is a software engineer in Microsoft since 2009, and last year he got an award as the Best Talent",
+                          "relation":"WORKS_FOR",
+                          "tail_type":"Company"
+                       },
+                       {
+                          "tail":"Best Talent",
+                          "head":"Adam",
+                          "head_type":"Person",
+                          "text":"Adam is a software engineer in Microsoft since 2009, and last year he got an award as the Best Talent",
+                          "relation":"HAS_AWARD",
+                          "tail_type":"Award"
+                       },
+                       {
+                          "tail":"Microsoft",
+                          "head":"Microsoft Word",
+                          "head_type":"Product",
+                          "text":"Microsoft is a tech company that provide several products such as Microsoft Word",
+                          "relation":"PRODUCED_BY",
+                          "tail_type":"Company"
+                       },
+                       {
+                          "tail":"lightweight app",
+                          "head":"Microsoft Word",
+                          "head_type":"Product",
+                          "text":"Microsoft Word is a lightweight app that accessible offline",
+                          "relation":"HAS_CHARACTERISTIC",
+                          "tail_type":"Characteristic"
+                       },
+                       {
+                          "tail":"accessible offline",
+                          "head":"Microsoft Word",
+                          "head_type":"Product",
+                          "text":"Microsoft Word is a lightweight app that accessible offline",
+                          "relation":"HAS_CHARACTERISTIC",
+                          "tail_type":"Characteristic"
+                       }
+                    ]
+                    """;
+
+    private static ChatModel model;
+
+    @BeforeAll
+    static void beforeAll() {
+        model = OpenAiChatModel.builder()
+                .baseUrl(System.getenv("OPENAI_BASE_URL"))
+                .apiKey(System.getenv("OPENAI_API_KEY"))
+                .organizationId(System.getenv("OPENAI_ORGANIZATION_ID"))
+                .modelName(GPT_4_O_MINI)
+                .logRequests(true)
+                .logResponses(true)
+                .build();
+    }
+
+    @Test
+    void testAddGraphDocumentsWithMissingModel() {
+        try {
+            LLMGraphTransformer.builder().build();
+            fail();
+        } catch (Exception e) {
+            assertThat(e.getMessage()).contains("chatModel cannot be null");
+        }
+    }
+
+    @Test
+    void testAddGraphDocumentsWithMissingExamples() {
+        try {
+            LLMGraphTransformer.builder().model(model).build();
+            fail();
+        } catch (Exception e) {
+            assertThat(e.getMessage()).contains("examples cannot be null");
+        }
+    }
+
+    @Test
+    void testAddGraphDocumentsWithCustomPrompt() {
+        List<ChatMessage> prompt =
+                List.of(new UserMessage("just return a null value, don't add any explanation or extra text."));
+
+        LLMGraphTransformer transformer = LLMGraphTransformer.builder()
+                .model(model)
+                .examples(EXAMPLES_PROMPT)
+                .prompt(prompt)
+                .build();
+
+        Document doc3 = new DefaultDocument(
+                "Keanu Reeves acted in Matrix. Keanu was born in Beirut", Metadata.from("key3", "value3"));
+        List<GraphDocument> documents = transformer.transformAll(List.of(doc3));
+        assertThat(documents).isEmpty();
+    }
+
+    @Test
+    void testAddGraphDocumentsWithCustomNodesAndRelationshipsSchema() {
+        String cat = "Sylvester the cat";
+        String keanu = "Keanu Reeves";
+        String lino = "Lino Banfi";
+        String goku = "Goku";
+        String hajime = "Hajime Isayama";
+        String levi = "Levi";
+        String table = "table";
+        String matrix = "Matrix";
+        String vac = "Vieni Avanti Cretino";
+        String db = "Dragon Ball";
+        String aot = "Attack On Titan";
+
+        Document docCat = Document.from("%s is on the %s".formatted(cat, table));
+        Document docKeanu = Document.from("%s acted in %s".formatted(keanu, matrix));
+        Document docLino = Document.from("%s acted in %s".formatted(lino, vac));
+        Document docGoku = Document.from("%s acted in %s".formatted(goku, db));
+        Document docHajime = Document.from("%s wrote %s. %s acted in %s".formatted(hajime, aot, levi, aot));
+
+        List<Document> docs = List.of(docCat, docKeanu, docLino, docGoku, docHajime);
+
+        LLMGraphTransformer build2 = LLMGraphTransformer.builder()
+                .model(model)
+                .examples(EXAMPLES_PROMPT)
+                .build();
+        List<GraphDocument> documents2 = build2.transformAll(docs);
+        Stream<String> expectedNodes = Stream.of(cat, keanu, lino, goku, hajime, levi, table, matrix, vac, db, aot);
+        assertThat(documents2).hasSize(5);
+        graphDocsAssertions(documents2, expectedNodes, Stream.of("acted", "acted", "acted", "acted", "wr.", "on"));
+
+        LLMGraphTransformer transformer = LLMGraphTransformer.builder()
+                .model(model)
+                .examples(EXAMPLES_PROMPT)
+                .allowedNodes(List.of("Person"))
+                .allowedRelationships(List.of("Acted_in"))
+                .build();
+
+        List<GraphDocument> documents = transformer.transformAll(docs);
+        System.out.println("documents = " + documents);
+        assertThat(documents).hasSize(4);
+        String[] strings = {keanu, lino, goku, levi, matrix, vac, db, aot};
+        graphDocsAssertions(documents, Stream.of(strings), Stream.of("acted", "acted", "acted", "acted"));
+
+        LLMGraphTransformer build3 = LLMGraphTransformer.builder()
+                .model(model)
+                .examples(EXAMPLES_PROMPT)
+                .allowedNodes(List.of("Person"))
+                .allowedRelationships(List.of("Writes", "Acted_in"))
+                .build();
+
+        List<GraphDocument> documents3 = build3.transformAll(docs);
+        assertThat(documents).hasSize(4);
+        String[] elements3 = {keanu, lino, goku, hajime, levi, matrix, vac, db, aot};
+
+        graphDocsAssertions(documents3, Stream.of(elements3), Stream.of("acted", "acted", "acted", "acted", "wr."));
+    }
+
+    @Test
+    void testAddGraphDocumentsWithDeDuplication() {
+        LLMGraphTransformer transformer = LLMGraphTransformer.builder()
+                .model(model)
+                .examples(EXAMPLES_PROMPT)
+                .build();
+
+        Document doc3 = new DefaultDocument(
+                "Keanu Reeves acted in Matrix. Keanu was born in Beirut", Metadata.from("key3", "value3"));
+
+        List<Document> documents = List.of(doc3);
+        List<GraphDocument> graphDocs = transformer.transformAll(documents);
+
+        assertThat(graphDocs).hasSize(1);
+        Stream<String> expectedNodeElements = Stream.of("matrix", "keanu", "beirut");
+        Stream<String> expectedEdgeElements = Stream.of("acted", "born");
+        graphDocsAssertions(graphDocs, expectedNodeElements, expectedEdgeElements);
+    }
+
+    private static void graphDocsAssertions(
+            List<GraphDocument> documents, Stream<String> expectedNodeElements, Stream<String> expectedEdgeElements) {
+        List<String> actualNodes = getNodeIds(documents);
+        List<String> actualRelationships = getRelationshipIds(documents);
+        entitiesAssertions(expectedNodeElements, actualNodes);
+
+        entitiesAssertions(expectedEdgeElements, actualRelationships);
+    }
+
+    private static void entitiesAssertions(Stream<String> expectedNodeElements, List<String> actualNodes) {
+        List<String> expectedNodes = expectedNodeElements.sorted().toList();
+        assertThat(actualNodes).hasSameSizeAs(expectedNodes);
+        for (int i = 0; i < actualNodes.size(); i++) {
+            assertThat(actualNodes.get(i).toLowerCase()).containsPattern("(?i)" + expectedNodes.get(i));
+        }
+    }
+
+    private static List<String> getNodeIds(List<GraphDocument> documents2) {
+        return documents2.stream()
+                .flatMap(i -> i.nodes().stream().map(GraphNode::id))
+                .sorted()
+                .collect(Collectors.toList());
+    }
+
+    private static List<String> getRelationshipIds(List<GraphDocument> documents2) {
+        return documents2.stream()
+                .flatMap(i -> i.relationships().stream().map(GraphEdge::type))
+                .sorted()
+                .collect(Collectors.toList());
+    }
+}
diff --git a/embedding-stores/langchain4j-community-neo4j/pom.xml b/embedding-stores/langchain4j-community-neo4j/pom.xml
index 7abec8a17..a0fc6e960 100644
--- a/embedding-stores/langchain4j-community-neo4j/pom.xml
+++ b/embedding-stores/langchain4j-community-neo4j/pom.xml
@@ -13,6 +13,7 @@
 
     <properties>
         <neo4j.version>1.2.0</neo4j.version>
+        <neo4j.cypher.dsl.version>2023.7.0</neo4j.cypher.dsl.version>
     </properties>
 
     <dependencies>
@@ -31,7 +32,7 @@
         <dependency>
             <groupId>org.neo4j</groupId>
             <artifactId>neo4j-cypher-dsl-schema-name-support</artifactId>
-            <version>2023.7.0</version>
+            <version>${neo4j.cypher.dsl.version}</version>
         </dependency>
 
         <!-- test dependencies -->
diff --git a/embedding-stores/langchain4j-community-neo4j/src/test/java/dev/langchain4j/community/store/embedding/neo4j/Neo4jEmbeddingStoreBaseTest.java b/embedding-stores/langchain4j-community-neo4j/src/test/java/dev/langchain4j/community/store/embedding/neo4j/Neo4jEmbeddingStoreBaseTest.java
index c9c7f9641..06c32be22 100644
--- a/embedding-stores/langchain4j-community-neo4j/src/test/java/dev/langchain4j/community/store/embedding/neo4j/Neo4jEmbeddingStoreBaseTest.java
+++ b/embedding-stores/langchain4j-community-neo4j/src/test/java/dev/langchain4j/community/store/embedding/neo4j/Neo4jEmbeddingStoreBaseTest.java
@@ -6,7 +6,6 @@
 import dev.langchain4j.store.embedding.EmbeddingStore;
 import dev.langchain4j.store.embedding.EmbeddingStoreIT;
 import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.neo4j.driver.AuthTokens;
 import org.neo4j.driver.Driver;
@@ -51,11 +50,6 @@ static void afterAll() {
         neo4jContainer.stop();
     }
 
-    @AfterEach
-    void afterEach() {
-        session.run("MATCH (n) DETACH DELETE n");
-    }
-
     @Override
     protected EmbeddingStore<TextSegment> embeddingStore() {
         return embeddingStore;
@@ -68,6 +62,10 @@ protected EmbeddingModel embeddingModel() {
 
     @Override
     protected void clearStore() {
+        session.executeWriteWithoutResult(
+                tx -> tx.run("MATCH (n) DETACH DELETE n").consume());
+        session.run("CALL db.awaitIndexes()");
+
         embeddingStore = Neo4jEmbeddingStore.builder()
                 .withBasicAuth(neo4jContainer.getBoltUrl(), USERNAME, ADMIN_PASSWORD)
                 .dimension(384)
diff --git a/langchain4j-community-bom/pom.xml b/langchain4j-community-bom/pom.xml
index b0c6995bf..dac60282c 100644
--- a/langchain4j-community-bom/pom.xml
+++ b/langchain4j-community-bom/pom.xml
@@ -111,6 +111,13 @@
                 <version>${project.version}</version>
             </dependency>
 
+            <!-- document transformer -->
+            <dependency>
+                <groupId>dev.langchain4j</groupId>
+                <artifactId>langchain4j-community-llm-graph-transformer</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
             <!-- web search engines -->
             <dependency>
                 <groupId>dev.langchain4j</groupId>
diff --git a/langchain4j-community-core/pom.xml b/langchain4j-community-core/pom.xml
index 4649681a6..662461681 100644
--- a/langchain4j-community-core/pom.xml
+++ b/langchain4j-community-core/pom.xml
@@ -20,11 +20,18 @@
     </licenses>
 
     <dependencies>
+
         <dependency>
             <groupId>dev.langchain4j</groupId>
             <artifactId>langchain4j-core</artifactId>
             <version>${project.version}</version>
         </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphDocument.java b/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphDocument.java
index 79b66b2bc..76b6e6033 100644
--- a/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphDocument.java
+++ b/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphDocument.java
@@ -3,6 +3,7 @@
 import static dev.langchain4j.internal.Utils.copyIfNotNull;
 import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import dev.langchain4j.Experimental;
 import dev.langchain4j.data.document.Document;
 import java.util.HashSet;
@@ -27,14 +28,17 @@ public GraphDocument(Set<GraphNode> nodes, Set<GraphEdge> relationships, Documen
         this.source = ensureNotNull(source, "source");
     }
 
+    @JsonProperty
     public Set<GraphNode> nodes() {
         return nodes;
     }
 
+    @JsonProperty
     public Set<GraphEdge> relationships() {
         return relationships;
     }
 
+    @JsonProperty
     public Document source() {
         return source;
     }
diff --git a/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphEdge.java b/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphEdge.java
index fd36039fa..b5e29cdc9 100644
--- a/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphEdge.java
+++ b/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphEdge.java
@@ -2,6 +2,7 @@
 
 import static dev.langchain4j.internal.Utils.copyIfNotNull;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import dev.langchain4j.Experimental;
 import java.util.HashMap;
 import java.util.Map;
@@ -27,18 +28,22 @@ public GraphEdge(GraphNode sourceNode, GraphNode targetNode, String type, Map<St
         this.properties = copyIfNotNull(properties);
     }
 
+    @JsonProperty
     public GraphNode sourceNode() {
         return sourceNode;
     }
 
+    @JsonProperty
     public GraphNode targetNode() {
         return targetNode;
     }
 
+    @JsonProperty
     public String type() {
         return type;
     }
 
+    @JsonProperty
     public Map<String, String> properties() {
         return properties;
     }
diff --git a/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphNode.java b/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphNode.java
index dd94e3f87..604a51290 100644
--- a/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphNode.java
+++ b/langchain4j-community-core/src/main/java/dev/langchain4j/community/data/document/graph/GraphNode.java
@@ -3,6 +3,7 @@
 import static dev.langchain4j.internal.Utils.copyIfNotNull;
 import static dev.langchain4j.internal.Utils.getOrDefault;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import dev.langchain4j.Experimental;
 import java.util.HashMap;
 import java.util.Map;
@@ -26,14 +27,17 @@ public GraphNode(String id, String type, Map<String, String> properties) {
         this.properties = copyIfNotNull(properties);
     }
 
+    @JsonProperty
     public String id() {
         return id;
     }
 
+    @JsonProperty
     public String type() {
         return type;
     }
 
+    @JsonProperty
     public Map<String, String> properties() {
         return properties;
     }
@@ -42,6 +46,10 @@ public static GraphNode from(String id, String type, Map<String, String> propert
         return new GraphNode(id, type, properties);
     }
 
+    public static GraphNode from(String id, String type) {
+        return new GraphNode(id, type, new HashMap<>());
+    }
+
     public static GraphNode from(String id) {
         return new GraphNode(id, null, new HashMap<>());
     }
diff --git a/pom.xml b/pom.xml
index 7b26354da..76e3df09d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,9 @@
         <!-- Integration of document parsers -->
         <module>document-parsers/langchain4j-community-document-parser-llamaparse</module>
 
+        <!-- Integration of document transformers -->
+        <module>document-transformers/langchain4j-community-llm-graph-transformer</module>
+
         <!-- Spring Boot Starters -->
         <module>spring-boot-starters</module>
 


Reviews:
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:26:50Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:27:39Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:32:14Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:33:07Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:36:29Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:37:27Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:37:44Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:38:53Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:39:18Z
user: login: jexp
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T08:39:38Z
user: login: Martin7-1
body: @vga91 Thank you!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T12:57:00Z
user: login: vga91
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T13:50:00Z
user: login: Martin7-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T14:13:51Z
user: login: vga91
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T14:46:52Z
user: login: Martin7-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-19T03:40:26Z
user: login: Martin7-1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T12:12:19Z
user: login: vga91
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:58:14Z
user: login: Martin7-1
body: @vga91 Thank you! The changes are all good. But I'm thinking about creating a new `langchain4j-community-core` module and define a new `GraphTransformer` interface.

Besides, I think we can decoupled `LLMGraphTransformer` from Neo4j because it seems that they do not have the necessary relationship.

I will create the new `langchain4j-community-core` module and move `LLMGraphTransformer` to the new module(maybe named `langchain4j-community-llm-graph-transformer`?). Thanks for your work!

WDYT?
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:15:53Z

Review comments:
diff_hunk: @@ -14,6 +20,9 @@
 public class Neo4jGraph implements AutoCloseable {
 
     public static class Builder {
+        private String label;
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraph.java
user: login: jexp
body: I'm not sure that all these things belong directly into Neo4jGraph?

Could we put them into a separate class that is configured and passed via config if needed?

Also if you leave them here, you need to name them more explicitely otherwise you don't know what "label" refers to, or "sanitizedLabel" ...
created_at: 2025-04-17T08:26:50Z
author_association: NONE
diff_hunk: @@ -14,6 +20,9 @@
 public class Neo4jGraph implements AutoCloseable {
 
     public static class Builder {
+        private String label;
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraph.java
user: login: jexp
body: Then that graph (e.g. `KnowledgeGraphWriter`) can also contain the implementation of `addGraphDocuments`
created_at: 2025-04-17T08:27:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: give it a name
created_at: 2025-04-17T08:32:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: just toList() is enough now
created_at: 2025-04-17T08:33:07Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(graph.idProperty)) {
+                    metadata.put(graph.idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(baseEntityLabel, includeSource, graph);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery(baseEntityLabel, graph);
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private static String getNodeImportQuery(boolean baseEntityLabel, boolean includeSource, Neo4jGraph graph) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource, graph);
+        if (baseEntityLabel) {
+            return includeDocsQuery + "UNWIND $data AS row "
+                    + "MERGE (source:%1$s {%2$s: row.id}) ".formatted(graph.sanitizedLabel, graph.sanitizedIdProperty)
+                    + (includeSource ? "MERGE (d)-[:MENTIONS]->(source) " : "")
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: can we use `(source)-[:HAS_ENTITY]->()`
and/or make that rel-type optionally configurable
created_at: 2025-04-17T08:36:29Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(graph.idProperty)) {
+                    metadata.put(graph.idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(baseEntityLabel, includeSource, graph);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery(baseEntityLabel, graph);
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private static String getNodeImportQuery(boolean baseEntityLabel, boolean includeSource, Neo4jGraph graph) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource, graph);
+        if (baseEntityLabel) {
+            return includeDocsQuery + "UNWIND $data AS row "
+                    + "MERGE (source:%1$s {%2$s: row.id}) ".formatted(graph.sanitizedLabel, graph.sanitizedIdProperty)
+                    + (includeSource ? "MERGE (d)-[:MENTIONS]->(source) " : "")
+                    + "WITH source, row "
+                    + "SET source:$(row.type) "
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: why would we set the type on the source? isn't that the entity type?
created_at: 2025-04-17T08:37:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(graph.idProperty)) {
+                    metadata.put(graph.idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(baseEntityLabel, includeSource, graph);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery(baseEntityLabel, graph);
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private static String getNodeImportQuery(boolean baseEntityLabel, boolean includeSource, Neo4jGraph graph) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource, graph);
+        if (baseEntityLabel) {
+            return includeDocsQuery + "UNWIND $data AS row "
+                    + "MERGE (source:%1$s {%2$s: row.id}) ".formatted(graph.sanitizedLabel, graph.sanitizedIdProperty)
+                    + (includeSource ? "MERGE (d)-[:MENTIONS]->(source) " : "")
+                    + "WITH source, row "
+                    + "SET source:$(row.type) "
+                    + "RETURN distinct 'done' AS result";
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: Perhaps better a `count(*) as total`?
created_at: 2025-04-17T08:37:44Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(graph.idProperty)) {
+                    metadata.put(graph.idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(baseEntityLabel, includeSource, graph);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery(baseEntityLabel, graph);
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private static String getNodeImportQuery(boolean baseEntityLabel, boolean includeSource, Neo4jGraph graph) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource, graph);
+        if (baseEntityLabel) {
+            return includeDocsQuery + "UNWIND $data AS row "
+                    + "MERGE (source:%1$s {%2$s: row.id}) ".formatted(graph.sanitizedLabel, graph.sanitizedIdProperty)
+                    + (includeSource ? "MERGE (d)-[:MENTIONS]->(source) " : "")
+                    + "WITH source, row "
+                    + "SET source:$(row.type) "
+                    + "RETURN distinct 'done' AS result";
+        }
+
+        return includeDocsQuery + "UNWIND $data AS row "
+                + "MERGE (node:$(row.type) {%1$s: row.id}) ".formatted(graph.sanitizedIdProperty)
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: we should merge on the baseEntity type
merging on dynamic labels is not fast yet
and set the other type via dynamic label
created_at: 2025-04-17T08:38:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(graph.idProperty)) {
+                    metadata.put(graph.idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(baseEntityLabel, includeSource, graph);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery(baseEntityLabel, graph);
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private static String getNodeImportQuery(boolean baseEntityLabel, boolean includeSource, Neo4jGraph graph) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource, graph);
+        if (baseEntityLabel) {
+            return includeDocsQuery + "UNWIND $data AS row "
+                    + "MERGE (source:%1$s {%2$s: row.id}) ".formatted(graph.sanitizedLabel, graph.sanitizedIdProperty)
+                    + (includeSource ? "MERGE (d)-[:MENTIONS]->(source) " : "")
+                    + "WITH source, row "
+                    + "SET source:$(row.type) "
+                    + "RETURN distinct 'done' AS result";
+        }
+
+        return includeDocsQuery + "UNWIND $data AS row "
+                + "MERGE (node:$(row.type) {%1$s: row.id}) ".formatted(graph.sanitizedIdProperty)
+                + (includeSource ? "MERGE (d)-[:MENTIONS]->(node) " : "")
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: why is this twice here?
created_at: 2025-04-17T08:39:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Neo4jGraphUtils {
+
+    public static void addGraphDocuments(
+            List<GraphDocument> graphDocuments, boolean includeSource, boolean baseEntityLabel, Neo4jGraph graph) {
+        if (baseEntityLabel) {
+            // Check and create constraint if not exists
+            final String constraintQuery = String.format(
+                    "CREATE CONSTRAINT IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                    graph.sanitizedLabel, graph.sanitizedIdProperty);
+            graph.executeWrite(constraintQuery);
+            graph.refreshSchema();
+        }
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .collect(Collectors.toList()));
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(graph.idProperty)) {
+                    metadata.put(graph.idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(baseEntityLabel, includeSource, graph);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery(baseEntityLabel, graph);
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private static String getNodeImportQuery(boolean baseEntityLabel, boolean includeSource, Neo4jGraph graph) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource, graph);
+        if (baseEntityLabel) {
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/Neo4jGraphUtils.java
user: login: jexp
body: I think we should make baseEntity label mandatory for now.
created_at: 2025-04-17T08:39:37Z
author_association: NONE
diff_hunk: @@ -69,6 +69,13 @@
             <scope>test</scope>
         </dependency>
 
+        <!-- graph transformer dependencies -->
+        <dependency>
+            <groupId>org.neo4j</groupId>
+            <artifactId>neo4j-cypher-dsl-schema-name-support</artifactId>
+            <version>2023.7.0</version>
path: content-retrievers/langchain4j-community-neo4j-retriever/pom.xml
user: login: Martin7-1
body: Please use `<properties>` to manage version of dependencies.
created_at: 2025-04-18T12:43:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.sanitizeOrThrows;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class KnowledgeGraphWriter {
+
+    /* default configs */
+    public static final String DEFAULT_ID_PROP = "id";
+    public static final String DEFAULT_TEXT_PROP = "text";
+    public static final String DEFAULT_LABEL = "__Entity__";
+    public static final String DEFAULT_CONS_NAME = "knowledge_cons";
+    public static final String DEFAULT_REL_TYPE = "HAS_ENTITY";
+
+    final String label;
+    final String relType;
+    final String sanitizedLabel;
+    final String sanitizedRelType;
+    final String constraintName;
+
+    final String idProperty;
+    final String sanitizedIdProperty;
+    final String textProperty;
+    final String sanitizedTextProperty;
+
+    private final Neo4jGraph graph;
+
+    public KnowledgeGraphWriter(
+            Neo4jGraph graph,
+            String idProperty,
+            String label,
+            String textProperty,
+            String relType,
+            String constraintName) {
+        this.graph = ensureNotNull(graph, "graph");
+        this.label = getOrDefault(label, DEFAULT_LABEL);
+        this.relType = getOrDefault(relType, DEFAULT_REL_TYPE);
+        this.idProperty = getOrDefault(idProperty, DEFAULT_ID_PROP);
+        this.textProperty = getOrDefault(textProperty, DEFAULT_TEXT_PROP);
+        this.constraintName = getOrDefault(constraintName, DEFAULT_CONS_NAME);
+
+        /* sanitize labels and property names, to prevent from Cypher Injections */
+        this.sanitizedLabel = sanitizeOrThrows(this.label, "label");
+        this.sanitizedRelType = sanitizeOrThrows(this.relType, "relType");
+        this.sanitizedIdProperty = sanitizeOrThrows(this.idProperty, "idProperty");
+        this.sanitizedTextProperty = sanitizeOrThrows(this.textProperty, "textProperty");
+
+        createConstraint();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private void createConstraint() {
+        // Check and create constraint if not exists
+        final String constraintQuery = String.format(
+                "CREATE CONSTRAINT %s IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                constraintName, sanitizedLabel, sanitizedIdProperty);
+        graph.executeWrite(constraintQuery);
+        graph.refreshSchema();
+    }
+
+    public void addGraphDocuments(List<GraphDocument> graphDocuments, boolean includeSource) {
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .toList());
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(idProperty)) {
+                    metadata.put(idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(includeSource);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery();
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private String getNodeImportQuery(boolean includeSource) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource);
+        final String withDocsRel = includeSource ? String.format("MERGE (d)-[:%s]->(source) ", relType) : "";
+
+        return includeDocsQuery + "UNWIND $data AS row "
+                + String.format("MERGE (source:%1$s {%2$s: row.id}) ", sanitizedLabel, sanitizedIdProperty)
+                + withDocsRel
+                + "WITH source, row "
+                + "SET source:$(row.type) "
+                + "RETURN count(*) as total";
+    }
+
+    private String getIncludeDocsQuery(boolean includeSource) {
+        if (!includeSource) {
+            return "";
+        }
+        return """
+                MERGE (d:Document {%1$s: $document.metadata.%1$s})
+                SET d.%2$s = $document.text
+                SET d += $document.metadata
+                WITH d
+                """
+                .formatted(sanitizedIdProperty, sanitizedTextProperty);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/KnowledgeGraphWriter.java
user: login: Martin7-1
body: Same `formatted` problem here.
created_at: 2025-04-18T12:46:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.sanitizeOrThrows;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class KnowledgeGraphWriter {
+
+    /* default configs */
+    public static final String DEFAULT_ID_PROP = "id";
+    public static final String DEFAULT_TEXT_PROP = "text";
+    public static final String DEFAULT_LABEL = "__Entity__";
+    public static final String DEFAULT_CONS_NAME = "knowledge_cons";
+    public static final String DEFAULT_REL_TYPE = "HAS_ENTITY";
+
+    final String label;
+    final String relType;
+    final String sanitizedLabel;
+    final String sanitizedRelType;
+    final String constraintName;
+
+    final String idProperty;
+    final String sanitizedIdProperty;
+    final String textProperty;
+    final String sanitizedTextProperty;
+
+    private final Neo4jGraph graph;
+
+    public KnowledgeGraphWriter(
+            Neo4jGraph graph,
+            String idProperty,
+            String label,
+            String textProperty,
+            String relType,
+            String constraintName) {
+        this.graph = ensureNotNull(graph, "graph");
+        this.label = getOrDefault(label, DEFAULT_LABEL);
+        this.relType = getOrDefault(relType, DEFAULT_REL_TYPE);
+        this.idProperty = getOrDefault(idProperty, DEFAULT_ID_PROP);
+        this.textProperty = getOrDefault(textProperty, DEFAULT_TEXT_PROP);
+        this.constraintName = getOrDefault(constraintName, DEFAULT_CONS_NAME);
+
+        /* sanitize labels and property names, to prevent from Cypher Injections */
+        this.sanitizedLabel = sanitizeOrThrows(this.label, "label");
+        this.sanitizedRelType = sanitizeOrThrows(this.relType, "relType");
+        this.sanitizedIdProperty = sanitizeOrThrows(this.idProperty, "idProperty");
+        this.sanitizedTextProperty = sanitizeOrThrows(this.textProperty, "textProperty");
+
+        createConstraint();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private void createConstraint() {
+        // Check and create constraint if not exists
+        final String constraintQuery = String.format(
+                "CREATE CONSTRAINT %s IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                constraintName, sanitizedLabel, sanitizedIdProperty);
+        graph.executeWrite(constraintQuery);
+        graph.refreshSchema();
+    }
+
+    public void addGraphDocuments(List<GraphDocument> graphDocuments, boolean includeSource) {
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .toList());
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(idProperty)) {
+                    metadata.put(idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(includeSource);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery();
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private String getNodeImportQuery(boolean includeSource) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource);
+        final String withDocsRel = includeSource ? String.format("MERGE (d)-[:%s]->(source) ", relType) : "";
+
+        return includeDocsQuery + "UNWIND $data AS row "
+                + String.format("MERGE (source:%1$s {%2$s: row.id}) ", sanitizedLabel, sanitizedIdProperty)
+                + withDocsRel
+                + "WITH source, row "
+                + "SET source:$(row.type) "
+                + "RETURN count(*) as total";
+    }
+
+    private String getIncludeDocsQuery(boolean includeSource) {
+        if (!includeSource) {
+            return "";
+        }
+        return """
+                MERGE (d:Document {%1$s: $document.metadata.%1$s})
+                SET d.%2$s = $document.text
+                SET d += $document.metadata
+                WITH d
+                """
+                .formatted(sanitizedIdProperty, sanitizedTextProperty);
+    }
+
+    private String getRelImportQuery() {
+
+        return """
+                UNWIND $data AS row
+                MERGE (source:%1$s {%2$s: row.source})
+                MERGE (target:%1$s {%2$s: row.target})
+                WITH source, target, row
+                MERGE (source)-[rel:$(toString(row.type) + '')]->(target)
+                RETURN distinct 'done'
+                """
+                .formatted(sanitizedLabel, sanitizedIdProperty);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/KnowledgeGraphWriter.java
user: login: Martin7-1
body: Same as above.
created_at: 2025-04-18T12:46:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+package dev.langchain4j.community.rag.content.retriever.neo4j;
+
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.generateMD5;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.sanitizeOrThrows;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.community.rag.transformer.GraphDocument;
+import dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils;
+import dev.langchain4j.data.document.Document;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class KnowledgeGraphWriter {
+
+    /* default configs */
+    public static final String DEFAULT_ID_PROP = "id";
+    public static final String DEFAULT_TEXT_PROP = "text";
+    public static final String DEFAULT_LABEL = "__Entity__";
+    public static final String DEFAULT_CONS_NAME = "knowledge_cons";
+    public static final String DEFAULT_REL_TYPE = "HAS_ENTITY";
+
+    final String label;
+    final String relType;
+    final String sanitizedLabel;
+    final String sanitizedRelType;
+    final String constraintName;
+
+    final String idProperty;
+    final String sanitizedIdProperty;
+    final String textProperty;
+    final String sanitizedTextProperty;
+
+    private final Neo4jGraph graph;
+
+    public KnowledgeGraphWriter(
+            Neo4jGraph graph,
+            String idProperty,
+            String label,
+            String textProperty,
+            String relType,
+            String constraintName) {
+        this.graph = ensureNotNull(graph, "graph");
+        this.label = getOrDefault(label, DEFAULT_LABEL);
+        this.relType = getOrDefault(relType, DEFAULT_REL_TYPE);
+        this.idProperty = getOrDefault(idProperty, DEFAULT_ID_PROP);
+        this.textProperty = getOrDefault(textProperty, DEFAULT_TEXT_PROP);
+        this.constraintName = getOrDefault(constraintName, DEFAULT_CONS_NAME);
+
+        /* sanitize labels and property names, to prevent from Cypher Injections */
+        this.sanitizedLabel = sanitizeOrThrows(this.label, "label");
+        this.sanitizedRelType = sanitizeOrThrows(this.relType, "relType");
+        this.sanitizedIdProperty = sanitizeOrThrows(this.idProperty, "idProperty");
+        this.sanitizedTextProperty = sanitizeOrThrows(this.textProperty, "textProperty");
+
+        createConstraint();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private void createConstraint() {
+        // Check and create constraint if not exists
+        final String constraintQuery = String.format(
+                "CREATE CONSTRAINT %s IF NOT EXISTS FOR (b:%s) REQUIRE b.%s IS UNIQUE;",
+                constraintName, sanitizedLabel, sanitizedIdProperty);
+        graph.executeWrite(constraintQuery);
+        graph.refreshSchema();
+    }
+
+    public void addGraphDocuments(List<GraphDocument> graphDocuments, boolean includeSource) {
+
+        for (GraphDocument graphDoc : graphDocuments) {
+            final Document source = graphDoc.getSource();
+
+            // Import nodes
+            Map<String, Object> nodeParams = new HashMap<>();
+            nodeParams.put(
+                    "data",
+                    graphDoc.getNodes().stream()
+                            .map(LLMGraphTransformerUtils::toMap)
+                            .toList());
+
+            if (includeSource) {
+                // create a copyOf metadata, not to update existing graphDoc,
+                // subsequent tests could potentially fail
+                final Map<String, Object> metadata =
+                        new HashMap<>(Map.copyOf(source.metadata().toMap()));
+                if (!metadata.containsKey(idProperty)) {
+                    metadata.put(idProperty, generateMD5(source.text()));
+                }
+                final Map<String, Object> document = Map.of("metadata", metadata, "text", source.text());
+                nodeParams.put("document", document);
+            }
+
+            String nodeImportQuery = getNodeImportQuery(includeSource);
+            graph.executeWrite(nodeImportQuery, nodeParams);
+
+            // Import relationships
+            List<Map<String, String>> relData = graphDoc.getRelationships().stream()
+                    .map(rel -> Map.of(
+                            "source", rel.getSourceNode().getId(),
+                            "source_label", rel.getSourceNode().getType(),
+                            "target", rel.getTargetNode().getId(),
+                            "target_label", rel.getTargetNode().getType(),
+                            "type", rel.getType().replace(" ", "_").toUpperCase()))
+                    .toList();
+
+            String relImportQuery = getRelImportQuery();
+            graph.executeWrite(relImportQuery, Map.of("data", relData));
+        }
+    }
+
+    private String getNodeImportQuery(boolean includeSource) {
+
+        String includeDocsQuery = getIncludeDocsQuery(includeSource);
+        final String withDocsRel = includeSource ? String.format("MERGE (d)-[:%s]->(source) ", relType) : "";
+
+        return includeDocsQuery + "UNWIND $data AS row "
+                + String.format("MERGE (source:%1$s {%2$s: row.id}) ", sanitizedLabel, sanitizedIdProperty)
+                + withDocsRel
+                + "WITH source, row "
+                + "SET source:$(row.type) "
+                + "RETURN count(*) as total";
+    }
+
+    private String getIncludeDocsQuery(boolean includeSource) {
+        if (!includeSource) {
+            return "";
+        }
+        return """
+                MERGE (d:Document {%1$s: $document.metadata.%1$s})
+                SET d.%2$s = $document.text
+                SET d += $document.metadata
+                WITH d
+                """
+                .formatted(sanitizedIdProperty, sanitizedTextProperty);
+    }
+
+    private String getRelImportQuery() {
+
+        return """
+                UNWIND $data AS row
+                MERGE (source:%1$s {%2$s: row.source})
+                MERGE (target:%1$s {%2$s: row.target})
+                WITH source, target, row
+                MERGE (source)-[rel:$(toString(row.type) + '')]->(target)
+                RETURN distinct 'done'
+                """
+                .formatted(sanitizedLabel, sanitizedIdProperty);
+    }
+
+    public static class Builder {
+        private String label;
+        private String idProperty;
+        private String textProperty;
+        private String relType;
+        private String constraintName;
+        private Neo4jGraph graph;
+
+        /**
+         * @param graph the {@link Neo4jGraph} (required)
+         */
+        public Builder graph(Neo4jGraph graph) {
+            this.graph = graph;
+            return this;
+        }
+
+        /**
+         * @param idProperty the entity id, to be used with {@link #addGraphDocuments(List, boolean)}
+         */
+        public Builder idProperty(String idProperty) {
+            this.idProperty = idProperty;
+            return this;
+        }
+
+        /**
+         * @param textProperty the document text, to be used with {@link #addGraphDocuments(List, boolean)}
+         *                     if the second parameter is true
+         */
+        public Builder textProperty(String textProperty) {
+            this.textProperty = textProperty;
+            return this;
+        }
+
+        /**
+         * @param label the entity label, to be used with {@link #addGraphDocuments(List, boolean)}
+         *             (default: "__Entity__")
+         */
+        public Builder label(String label) {
+            this.label = label;
+            return this;
+        }
+
+        /**
+         * @param relType the entity relationship types, to be used with {@link #addGraphDocuments(List, boolean)}
+         *               (default "HAS_ENTITY")
+         */
+        public Builder relType(String relType) {
+            this.relType = relType;
+            return this;
+        }
+
+        /**
+         * @param constraintName the constraint name (default: "knowledge_cons")
+         */
+        public Builder constraintName(String constraintName) {
+            this.constraintName = constraintName;
+            return this;
+        }
+
+        KnowledgeGraphWriter build() {
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/content/retriever/neo4j/KnowledgeGraphWriter.java
user: login: Martin7-1
body: ```suggestion
        public KnowledgeGraphWriter build() {
```
created_at: 2025-04-18T12:46:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: Martin7-1
body: I'm curious why we should provide default examples. Could you please explain? Are there cases where users don't need examples?
created_at: 2025-04-18T12:55:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+package dev.langchain4j.community.rag.transformer;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class LLMGraphTransformerUtils {
+
+    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    public static Map<String, Object> toMap(Object object) {
+        return OBJECT_MAPPER.convertValue(object, new TypeReference<>() {});
+    }
+
+    public static String getStringFromListOfMaps(List<Map<String, String>> list) {
+        try {
+            return OBJECT_MAPPER.writeValueAsString(list);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseJson(String jsonString) throws JsonProcessingException {
+        ObjectMapper objectMapper = new ObjectMapper();
+
+        return objectMapper.readValue(jsonString, new TypeReference<>() {});
+    }
+
+    public static String generateMD5(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            md.update(input.getBytes());
+            byte[] digest = md.digest();
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("MD5 algorithm not found", e);
+        }
+    }
+
+    public static final List<Map<String, String>> EXAMPLES_PROMPT = Arrays.asList(
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformerUtils.java
user: login: Martin7-1
body: Same as above. Why we need default example?
created_at: 2025-04-18T12:56:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: vga91
body: Yeah indeed structured this way it is not very usable, since examples without prompts are not very useful.
We could use an `additionalInstructions`, to add more instructions on the prompt if needed, while keeping the `examples` part fixed.
As done in langchain-ai [here](https://github.com/langchain-ai/langchain-experimental/blob/main/libs/experimental/langchain_experimental/graph_transformers/llm.py#L293-L296).
What do you think?
created_at: 2025-04-18T13:50:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: Martin7-1
body: Good point, I think it's a good way to make it flexible.

BTW, since users may not know the default examples, what about using `ensureNotNull` to enforce users pass examples explicitly.
created_at: 2025-04-18T14:13:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: vga91
body: Sorry, not sure if I understand correctly, 
do you mean `this.examples = ensureNotNull(examples, "examples");`?
created_at: 2025-04-18T14:46:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: Martin7-1
body: Yes. What I mean is that the default examples may cause the user to encounter unexpected result if the user doesn't pass the examples
created_at: 2025-04-19T03:40:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: Martin7-1
body: @vga91 Hi! Any updates?
created_at: 2025-04-22T12:12:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,293 @@
+package dev.langchain4j.community.rag.transformer;
+
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Edge;
+import static dev.langchain4j.community.rag.transformer.GraphDocument.Node;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.EXAMPLES_PROMPT;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.getStringFromListOfMaps;
+import static dev.langchain4j.community.rag.transformer.LLMGraphTransformerUtils.parseJson;
+import static dev.langchain4j.community.rag.transformer.Neo4jUtils.getBacktickText;
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.internal.RetryUtils;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.input.PromptTemplate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LLMGraphTransformer {
+
+    public static final String DEFAULT_NODE_TYPE = "Node";
+    private final List<String> allowedNodes;
+    private final List<String> allowedRelationships;
+    private final List<ChatMessage> prompt;
+    private final String additionalInstructions;
+    private final ChatModel model;
+    private final List<Map<String, String>> examples;
+    private final Integer maxAttempts;
+
+    /**
+     * It allows specifying constraints on the types of nodes and relationships to include in the output graph.
+     * The class supports extracting properties for both nodes and relationships.
+     *
+     * @param model the {@link ChatModel} (required)
+     * @param allowedNodes Specifies which node types are allowed in the graph. If null or empty allows all node types (default: [])
+     * @param allowedRelationships Specifies which relationship types are allowed in the graph. If null or empty allows all relationship types (default: [])
+     * @param prompt The chat messages to pass to the LLM with additional instructions. (optional)
+     * @param additionalInstructions Allows you to add additional instructions to the prompt without having to change the whole prompt (default: '')
+     * @param examples Allows you to add additional instructions to the prompt without having to change the whole prompt (default: {@link LLMGraphTransformerUtils#EXAMPLES_PROMPT})
+     * @param maxAttempts Retry N times the transformation if it fails (default: 1)
+     */
+    public LLMGraphTransformer(
+            ChatModel model,
+            List<String> allowedNodes,
+            List<String> allowedRelationships,
+            List<ChatMessage> prompt,
+            String additionalInstructions,
+            List<Map<String, String>> examples,
+            Integer maxAttempts) {
+
+        this.model = ensureNotNull(model, "model");
+
+        this.allowedNodes = getOrDefault(allowedNodes, List.of());
+        this.allowedRelationships = getOrDefault(allowedRelationships, List.of());
+        this.prompt = prompt;
+
+        this.maxAttempts = getOrDefault(maxAttempts, 1);
+        this.additionalInstructions = getOrDefault(additionalInstructions, "");
+
+        this.examples = getOrDefault(examples, EXAMPLES_PROMPT);
path: content-retrievers/langchain4j-community-neo4j-retriever/src/main/java/dev/langchain4j/community/rag/transformer/LLMGraphTransformer.java
user: login: vga91
body: Hi! Just changed it
created_at: 2025-04-22T13:58:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: Martin7-1
created_at: 2025-04-11T14:33:35Z
body: @vga91 Thank you! Could you please resolve the conflicts?
author_association: COLLABORATOR
user: login: vga91
created_at: 2025-04-16T09:14:46Z
body: @Martin7-1 

The CI fails since the `OpenAiChatModel` implements a `ChatLanguageModel` which doesn't exists anymore, because currently there is a `ChatModel` class instead.
But, even if the `langchain4j-open-ai` version is the same as the latest langchain4j one (`1.0.0-beta4-SNAPSHOT`), 
the `OpenAiChatModel` imported is still the old one instead of the [correct one](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/OpenAiChatModel.java#L46).

Do you know how to solve this? Thanks
author_association: CONTRIBUTOR
user: login: Martin7-1
created_at: 2025-04-18T03:53:21Z
body: > @Martin7-1
> 
> The CI fails since the `OpenAiChatModel` implements a `ChatLanguageModel` which doesn't exists anymore, because currently there is a `ChatModel` class instead. But, even if the `langchain4j-open-ai` version is the same as the latest langchain4j one (`1.0.0-beta4-SNAPSHOT`), the `OpenAiChatModel` imported is still the old one instead of the [correct one](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/OpenAiChatModel.java#L46).
> 
> Do you know how to solve this? Thanks

@vga91 Hi! Sorry for the late replay. The CI failed because the incomplete snapshot replease of the main langchain4j repo. Now it works fine :)
author_association: COLLABORATOR
user: login: vga91
created_at: 2025-04-22T15:40:39Z
body: > @vga91 Thank you! The changes are all good. But I'm thinking about creating a new `langchain4j-community-core` module and define a new `GraphTransformer` interface.
> 
> Besides, I think we can decoupled `LLMGraphTransformer` from Neo4j because it seems that they do not have the necessary relationship.
> 
> I will create the new `langchain4j-community-core` module and move `LLMGraphTransformer` to the new module(maybe named `langchain4j-community-llm-graph-transformer`?). Thanks for your work!
> 
> WDYT?

Yeah it sounds like a great idea!
author_association: CONTRIBUTOR
user: login: vga91
created_at: 2025-04-24T21:28:46Z
body: @Martin7-1 
Some tests fail (despite locally they work correctly).
Maybe they could be fixed by this PR: https://github.com/langchain4j/langchain4j-community/pull/151
author_association: CONTRIBUTOR
user: login: Martin7-1
created_at: 2025-04-27T09:50:51Z
body: @vga91 Hi! I've split `LLMGraphTransformer` into a single module. Could you please check?
author_association: COLLABORATOR
user: login: vga91
created_at: 2025-04-28T10:14:28Z
body: @Martin7-1 Hi, the changes look great to me, thanks!
author_association: CONTRIBUTOR