diff --git a/neo4j-example/pom.xml b/neo4j-example/pom.xml index 776f8404..39f45c98 100644 --- a/neo4j-example/pom.xml +++ b/neo4j-example/pom.xml @@ -6,7 +6,7 @@ dev.langchain4j neo4j-example - 1.0.0-beta4 + 1.0.1-beta6 org.springframework.boot spring-boot-starter-parent @@ -48,6 +48,12 @@ ${project.version} + + dev.langchain4j + langchain4j-community-llm-graph-transformer + ${project.version} + + dev.langchain4j langchain4j-embeddings-all-minilm-l6-v2 diff --git a/neo4j-example/src/main/java/KnowledgeGraphWriterExample.java b/neo4j-example/src/main/java/KnowledgeGraphWriterExample.java new file mode 100644 index 00000000..66ecc3d2 --- /dev/null +++ b/neo4j-example/src/main/java/KnowledgeGraphWriterExample.java @@ -0,0 +1,129 @@ +import dev.langchain4j.community.data.document.graph.GraphDocument; +import dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformer; +import dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter; +import dev.langchain4j.community.rag.content.retriever.neo4j.Neo4jGraph; +import dev.langchain4j.data.document.DefaultDocument; +import dev.langchain4j.data.document.Document; +import dev.langchain4j.model.openai.OpenAiChatModel; +import org.testcontainers.containers.Neo4jContainer; + +import java.util.List; + +import static dev.langchain4j.internal.Utils.getOrDefault; +import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI; + +public class KnowledgeGraphWriterExample { + 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 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 final String OPENAI_API_KEY = getOrDefault(System.getenv("OPENAI_API_KEY"), "demo"); + public static final String OPENAI_BASE_URL = "demo".equals(OPENAI_API_KEY) ? "http://langchain4j.dev/demo/openai/v1" : null; + + public static void main(String[] args) { + final OpenAiChatModel model = OpenAiChatModel.builder() + .apiKey(OPENAI_API_KEY) + .baseUrl(OPENAI_BASE_URL) + .modelName(GPT_4_O_MINI) + .build(); + + LLMGraphTransformer graphTransformer = LLMGraphTransformer.builder() + .model(model) + .examples(EXAMPLES_PROMPT) + .build(); + + Document docKeanu = new DefaultDocument(KEANU_REEVES_ACTED); + Document docCat = new DefaultDocument(CAT_ON_THE_TABLE); + List documents = List.of(docCat, docKeanu); + + List graphDocuments = graphTransformer.transformAll(documents); + + try (Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:5.26") + .withAdminPassword("admin1234") + .withLabsPlugins("apoc")) { + neo4jContainer.start(); + Neo4jGraph graph = Neo4jGraph.builder() + .withBasicAuth(neo4jContainer.getBoltUrl(), "neo4j", neo4jContainer.getAdminPassword()) + .build(); + + KnowledgeGraphWriter writer = KnowledgeGraphWriter.builder() + .graph(graph) + .label("Entity") + .relType("MENTIONS") + .idProperty("id") + .textProperty("text") + .build(); + + // `graphDocuments` obtained from LLMGraphTransformer + writer.addGraphDocuments(graphDocuments, true); // set to true to include document source + + /* + The above KnowledgeGraphWriter will add paths like: + (:Document {id: UUID, text: 'Sylvester the cat is on the table'})-[:MENTIONS]->(:Entity:Animal {id: 'Sylvester the cat'})-[:IS_ON]->(:Entity:Object {id: 'table'}) + (Document {id: UUID, text: 'Keanu Reeves acted in Matrix'})-[:MENTIONS]->(:Entity:Person {id: 'Keanu Reeves'})-[:ACTED_IN]->(:Entity:Movie {id: 'Matrix'}) + */ + + KnowledgeGraphWriter writerWithoutDocs = KnowledgeGraphWriter.builder() + .graph(graph) + .label("FooBar") + .relType("MENTIONS") + .idProperty("id") + .textProperty("text") + .build(); + + // `graphDocuments` obtained from LLMGraphTransformer + writerWithoutDocs.addGraphDocuments(graphDocuments, false); // set to true not to include document source + /* + The above KnowledgeGraphWriter will add paths like: + (:FooBar:Animal {id: 'Sylvester the cat'})-[:IS_ON]->(:FooBar:Object {id: 'table'}) + (:FooBar:Person {id: 'Keanu Reeves'})-[:ACTED_IN]->(:FooBar:Movie {id: 'Matrix'}) + */ + + graph.close(); + } + } +} diff --git a/neo4j-example/src/main/java/Neo4jContentRetrieverExample.java b/neo4j-example/src/main/java/Neo4jContentRetrieverExample.java index 808a0f61..c479d520 100644 --- a/neo4j-example/src/main/java/Neo4jContentRetrieverExample.java +++ b/neo4j-example/src/main/java/Neo4jContentRetrieverExample.java @@ -1,5 +1,6 @@ import dev.langchain4j.community.rag.content.retriever.neo4j.Neo4jGraph; import dev.langchain4j.community.rag.content.retriever.neo4j.Neo4jText2CypherRetriever; +import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.rag.content.Content; import dev.langchain4j.rag.query.Query; @@ -33,27 +34,100 @@ public static void main(String[] args) { neo4jContainer.start(); try (Driver driver = GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.none())) { try (Neo4jGraph graph = Neo4jGraph.builder().driver(driver).build()) { - try (Session session = driver.session()) { - session.run("CREATE (book:Book {title: 'Dune'})<-[:WROTE]-(author:Person {name: 'Frank Herbert'})"); - } - // The refreshSchema is needed only if we execute write operation after the `Neo4jGraph` instance, - // in this case `CREATE (book:Book...` - // If CREATE (and in general write operations to the db) are performed externally before Neo4jGraph.builder(), - // the refreshSchema() is not needed - graph.refreshSchema(); + contentRetrieverWithMinimalConfig(driver, graph, chatLanguageModel); - Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder() - .graph(graph) - .chatModel(chatLanguageModel) - .build(); + contentRetrieverWithExamples(graph, chatLanguageModel); - Query query = new Query("Who is the author of the book 'Dune'?"); - - List contents = retriever.retrieve(query); - - System.out.println(contents.get(0).textSegment().text()); // "Frank Herbert" + contentRetrieverWithoutRetries(graph, chatLanguageModel); } } } } + + private static void contentRetrieverWithMinimalConfig(Driver driver, Neo4jGraph graph, ChatModel chatLanguageModel) { + // tag::retrieve-text2cypher[] + try (Session session = driver.session()) { + session.run("CREATE (book:Book {title: 'Dune'})<-[:WROTE]-(author:Person {name: 'Frank Herbert'})"); + } + // The refreshSchema is needed only if we execute write operation after the `Neo4jGraph` instance, + // in this case `CREATE (book:Book...` + // If CREATE (and in general write operations to the db) are performed externally before Neo4jGraph.builder(), + // the refreshSchema() is not needed + graph.refreshSchema(); + + Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder() + .graph(graph) + .chatModel(chatLanguageModel) + .build(); + + Query query = new Query("Who is the author of the book 'Dune'?"); + + List contents = retriever.retrieve(query); + + System.out.println(contents.get(0).textSegment().text()); // "Frank Herbert" + // end::retrieve-text2cypher[] + } + + private static void contentRetrieverWithExamples(Neo4jGraph graph, ChatModel chatLanguageModel) { + // tag::retrieve-text2cypher-examples[] + List examples = List.of( + """ + # Which streamer has the most followers? + MATCH (s:Stream) + RETURN s.name AS streamer + ORDER BY s.followers DESC LIMIT 1 + """, + """ + # How many streamers are from Norway? + MATCH (s:Stream)-[:HAS_LANGUAGE]->(:Language {{name: 'Norwegian'}}) + RETURN count(s) AS streamers + """); + + Neo4jText2CypherRetriever neo4jContentRetriever = Neo4jText2CypherRetriever.builder() + .graph(graph) + .chatModel(chatLanguageModel) + // add the above examples + .examples(examples) + .build(); + + final String textQuery = "Which streamer from Italy has the most followers?"; + Query query = new Query(textQuery); + List contents = neo4jContentRetriever.retrieve(query); + System.out.println(contents.get(0).textSegment().text()); + // output: "The most followed italian streamer" + // end::retrieve-text2cypher-examples[] + } + + private static void contentRetrieverWithoutRetries(Neo4jGraph graph, ChatModel chatLanguageModel) { + Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder() + .graph(graph) + .chatModel(chatLanguageModel) + .maxRetries(0) // disables retry logic + .build(); + + Query query = new Query("Who is the author of the book 'Dune'?"); + + List contents = retriever.retrieve(query); + + System.out.println(contents.get(0).textSegment().text()); // "Frank Herbert" + } + + private static void contentRetrieverWithSamplesAndMaxRels(ChatModel chatLanguageModel, Driver driver) { + // Sample up to 3 example paths from the graph schema + // Explore a maximum of 8 relationships from the start node + try (Neo4jGraph graph = Neo4jGraph.builder().driver(driver).sample(3L).maxRels(8L).build()) { + // tag::retrieve-text2cypher-sample-max-rels[] + Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder() + .graph(graph) + .chatModel(chatLanguageModel) + .build(); + + Query query = new Query("Who is the author of the book 'Dune'?"); + + List contents = retriever.retrieve(query); + + System.out.println(contents.get(0).textSegment().text()); // "Frank Herbert" + // end::retrieve-text2cypher-sample-max-rels[] + } + } } diff --git a/neo4j-example/src/main/java/Neo4jEmbeddingStoreExample.java b/neo4j-example/src/main/java/Neo4jEmbeddingStoreExample.java index 8505485a..311abf3f 100644 --- a/neo4j-example/src/main/java/Neo4jEmbeddingStoreExample.java +++ b/neo4j-example/src/main/java/Neo4jEmbeddingStoreExample.java @@ -29,6 +29,7 @@ public static void main(String[] args) { searchEmbeddingsWithAddAllWithMetadataMaxResultsAndMinScore(); // custom embeddingStore + // tag::custom-embedding-store[] Neo4jEmbeddingStore customEmbeddingStore = Neo4jEmbeddingStore.builder() .withBasicAuth(neo4j.getBoltUrl(), "neo4j", neo4j.getAdminPassword()) .dimension(embeddingModel.dimension()) @@ -38,16 +39,17 @@ public static void main(String[] args) { .idProperty("customId") .textProperty("customText") .build(); + // end::custom-embedding-store[] searchEmbeddingsWithSingleMaxResult(customEmbeddingStore); } } private static void searchEmbeddingsWithSingleMaxResult(EmbeddingStore minimalEmbedding) { - + // tag::add-single-embedding[] TextSegment segment1 = TextSegment.from("I like football."); Embedding embedding1 = embeddingModel.embed(segment1).content(); minimalEmbedding.add(embedding1, segment1); - + // end::add-single-embedding[] TextSegment segment2 = TextSegment.from("The weather is good today."); Embedding embedding2 = embeddingModel.embed(segment2).content(); minimalEmbedding.add(embedding2, segment2); @@ -65,7 +67,7 @@ private static void searchEmbeddingsWithSingleMaxResult(EmbeddingStore