참고소스 수정본
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import asyncio
|
||||
|
||||
import neo4j
|
||||
from neo4j_graphrag.experimental.components.neo4j_reader import Neo4jChunkReader
|
||||
from neo4j_graphrag.experimental.components.types import LexicalGraphConfig, TextChunks
|
||||
|
||||
|
||||
async def main(driver: neo4j.Driver) -> TextChunks:
|
||||
config = LexicalGraphConfig( # only needed to overwrite the default values
|
||||
chunk_node_label="TextPart",
|
||||
)
|
||||
reader = Neo4jChunkReader(driver)
|
||||
result = await reader.run(lexical_graph_config=config)
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with neo4j.GraphDatabase.driver(
|
||||
"bolt://localhost:7687", auth=("neo4j", "password")
|
||||
) as driver:
|
||||
print(asyncio.run(main(driver)))
|
||||
@@ -0,0 +1,66 @@
|
||||
"""This examples shows how to create a custom component
|
||||
that can be added to a Pipeline with:
|
||||
|
||||
c = MyComponent(min_value=0, max_value=10)
|
||||
pipe = Pipeline()
|
||||
pipe.add_component(c, name="my_component")
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from neo4j_graphrag.experimental.pipeline import Component, DataModel
|
||||
from pydantic import BaseModel, validate_call
|
||||
|
||||
|
||||
class ComponentInputModel(BaseModel):
|
||||
"""A class to model the component inputs.
|
||||
This is not required, inputs can also be passed individually.
|
||||
|
||||
Note: can also inherit from DataModel.
|
||||
"""
|
||||
|
||||
text: str
|
||||
|
||||
|
||||
class ComponentResultModel(DataModel):
|
||||
"""A class to model the component outputs.
|
||||
Each component must have such a description of the output,
|
||||
so that the parameter mapping can be validated before the
|
||||
pipeline run starts.
|
||||
"""
|
||||
|
||||
value: int
|
||||
text: str
|
||||
|
||||
|
||||
class MyComponent(Component):
|
||||
"""Multiplies an input text by a random number
|
||||
between `min_value` and `max_value`
|
||||
"""
|
||||
|
||||
def __init__(self, min_value: int, max_value: int) -> None:
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
|
||||
# this decorator is required when a Pydantic model is used in the inputs
|
||||
@validate_call
|
||||
async def run(self, inputs: ComponentInputModel) -> ComponentResultModel:
|
||||
# logic here
|
||||
random_value = random.randint(self.min_value, self.max_value)
|
||||
return ComponentResultModel(
|
||||
value=random_value,
|
||||
text=inputs.text * random_value,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
c = MyComponent(min_value=0, max_value=10)
|
||||
print(
|
||||
asyncio.run(
|
||||
c.run(
|
||||
inputs={"text": "Hello"} # type: ignore
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from neo4j_graphrag.experimental.components.entity_relation_extractor import (
|
||||
EntityRelationExtractor,
|
||||
OnError,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import (
|
||||
DocumentInfo,
|
||||
LexicalGraphConfig,
|
||||
Neo4jGraph,
|
||||
TextChunks,
|
||||
)
|
||||
|
||||
|
||||
class MyExtractor(EntityRelationExtractor):
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
on_error: OnError = OnError.IGNORE,
|
||||
create_lexical_graph: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*args,
|
||||
on_error=on_error,
|
||||
create_lexical_graph=create_lexical_graph,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def run(
|
||||
self,
|
||||
chunks: TextChunks,
|
||||
document_info: Optional[DocumentInfo] = None,
|
||||
lexical_graph_config: Optional[LexicalGraphConfig] = None,
|
||||
**kwargs: Any,
|
||||
) -> Neo4jGraph:
|
||||
# Implement your logic here
|
||||
# you can loop over all text chunks with:
|
||||
for chunk in chunks.chunks:
|
||||
pass
|
||||
return Neo4jGraph(nodes=[], relationships=[])
|
||||
@@ -0,0 +1,33 @@
|
||||
from neo4j_graphrag.experimental.components.entity_relation_extractor import (
|
||||
LLMEntityRelationExtractor,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import (
|
||||
Neo4jGraph,
|
||||
TextChunk,
|
||||
TextChunks,
|
||||
)
|
||||
from neo4j_graphrag.llm import LLMInterface
|
||||
|
||||
|
||||
async def main(llm: LLMInterface) -> Neo4jGraph:
|
||||
"""
|
||||
|
||||
Args:
|
||||
llm (LLMInterface): Any LLM implemented in neo4j_graphrag.llm or from LangChain chat models.
|
||||
"""
|
||||
extractor = LLMEntityRelationExtractor(
|
||||
llm=llm,
|
||||
# optional: customize the prompt used for entity and relation extraction
|
||||
# prompt_template="",
|
||||
# optional: disable the creation of the lexical graph (Document and Chunk nodes)
|
||||
# create_lexical_graph=False,
|
||||
# optional: if an LLM error happens, ignore the chunk and continue process with the next ones
|
||||
# default value is OnError.RAISE which will end the process
|
||||
# on_error=OnError.IGNORE,
|
||||
# optional: tune the max_concurrency parameter to optimize speed
|
||||
# max_concurrency=5,
|
||||
)
|
||||
graph = await extractor.run(
|
||||
chunks=TextChunks(chunks=[TextChunk(text="....", index=0)])
|
||||
)
|
||||
return graph
|
||||
@@ -0,0 +1,33 @@
|
||||
from neo4j_graphrag.experimental.components.entity_relation_extractor import (
|
||||
LLMEntityRelationExtractor,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import (
|
||||
Neo4jGraph,
|
||||
TextChunk,
|
||||
TextChunks,
|
||||
)
|
||||
from neo4j_graphrag.llm import LLMInterface
|
||||
|
||||
|
||||
async def main(llm: LLMInterface) -> Neo4jGraph:
|
||||
"""
|
||||
|
||||
Args:
|
||||
llm (LLMInterface): Any LLM implemented in neo4j_graphrag.llm or from LangChain chat models.
|
||||
"""
|
||||
extractor = LLMEntityRelationExtractor(
|
||||
llm=llm,
|
||||
# optional: customize the prompt used for entity and relation extraction
|
||||
# prompt_template="",
|
||||
# optional: disable the creation of the lexical graph (Document and Chunk nodes)
|
||||
# create_lexical_graph=False,
|
||||
# optional: if an LLM error happens, ignore the chunk and continue process with the next ones
|
||||
# default value is OnError.RAISE which will end the process
|
||||
# on_error=OnError.IGNORE,
|
||||
# optional: tune the max_concurrency parameter to optimize speed
|
||||
# max_concurrency=5,
|
||||
)
|
||||
graph = await extractor.run(
|
||||
chunks=TextChunks(chunks=[TextChunk(text="....", index=0)])
|
||||
)
|
||||
return graph
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Simple example demonstrating structured output with LLMEntityRelationExtractor.
|
||||
|
||||
This example shows how to use structured output for more reliable entity and
|
||||
relationship extraction with automatic schema validation.
|
||||
|
||||
The Neo4jGraph schema is now compatible with both OpenAI and VertexAI structured
|
||||
output APIs, with strict schema validation (additionalProperties: false) and
|
||||
proper required field definitions.
|
||||
|
||||
Prerequisites:
|
||||
- Google Cloud credentials configured for VertexAI
|
||||
- Or OpenAI API key set in OPENAI_API_KEY environment variable
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from neo4j_graphrag.experimental.components.entity_relation_extractor import (
|
||||
LLMEntityRelationExtractor,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import (
|
||||
Neo4jGraph,
|
||||
TextChunk,
|
||||
TextChunks,
|
||||
)
|
||||
from neo4j_graphrag.llm import VertexAILLM
|
||||
|
||||
|
||||
async def main() -> Neo4jGraph:
|
||||
"""
|
||||
Demonstrates entity and relation extraction with structured output.
|
||||
|
||||
With use_structured_output=True:
|
||||
- Uses LLMInterfaceV2 (list of messages)
|
||||
- Passes Neo4jGraph Pydantic model as response_format to invoke()
|
||||
- Ensures response conforms to expected graph structure
|
||||
- Provides automatic type validation
|
||||
- Reduces need for JSON repair and error handling
|
||||
"""
|
||||
load_dotenv()
|
||||
# Initialize LLM - no response_format in constructor!
|
||||
llm = VertexAILLM(model_name="gemini-2.5-flash")
|
||||
|
||||
# llm = OpenAILLM(
|
||||
# model_name="gpt-5-mini",
|
||||
# model_params={"temperature": 0}
|
||||
# )
|
||||
|
||||
# Enable structured output for reliable extraction
|
||||
extractor = LLMEntityRelationExtractor(
|
||||
llm=llm,
|
||||
use_structured_output=True, # This is the key parameter!
|
||||
)
|
||||
|
||||
# Sample text about a person and organization
|
||||
sample_text = """
|
||||
Albert Einstein was a theoretical physicist who developed the theory of relativity.
|
||||
He worked at the Institute for Advanced Study in Princeton from 1933 until his death in 1955.
|
||||
"""
|
||||
|
||||
# Extract entities and relationships
|
||||
graph = await extractor.run(
|
||||
chunks=TextChunks(chunks=[TextChunk(text=sample_text, index=0)])
|
||||
)
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run extraction
|
||||
graph = asyncio.run(main())
|
||||
|
||||
print(graph)
|
||||
@@ -0,0 +1,30 @@
|
||||
from neo4j_graphrag.experimental.components.lexical_graph import (
|
||||
LexicalGraphBuilder,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import (
|
||||
GraphResult,
|
||||
LexicalGraphConfig,
|
||||
TextChunk,
|
||||
TextChunks,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> GraphResult:
|
||||
# optionally, define a LexicalGraphConfig object
|
||||
# shown below with default values
|
||||
config = LexicalGraphConfig(
|
||||
chunk_node_label="Chunk",
|
||||
document_node_label="Document",
|
||||
chunk_to_document_relationship_type="PART_OF_DOCUMENT",
|
||||
next_chunk_relationship_type="NEXT_CHUNK",
|
||||
node_to_chunk_relationship_type="PART_OF_CHUNK",
|
||||
chunk_embedding_property="embeddings",
|
||||
)
|
||||
builder = LexicalGraphBuilder(
|
||||
config=config, # optional
|
||||
)
|
||||
graph_result = await builder.run(
|
||||
text_chunks=TextChunks(chunks=[TextChunk(text="....", index=0)]),
|
||||
# document_info={"path": "example"}, # uncomment to create a "Document" node
|
||||
)
|
||||
return graph_result
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Create a custom data loader to transform content into text."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from fsspec import AbstractFileSystem
|
||||
|
||||
from neo4j_graphrag.experimental.components.data_loader import DataLoader
|
||||
from neo4j_graphrag.experimental.components.types import DocumentInfo, LoadedDocument
|
||||
|
||||
|
||||
class MyLoader(DataLoader):
|
||||
async def run(
|
||||
self,
|
||||
filepath: Union[str, Path],
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
fs: Optional[Union[AbstractFileSystem, str]] = None,
|
||||
) -> LoadedDocument:
|
||||
# Implement logic here; use ``fs`` when reading from non-local storage.
|
||||
_ = fs
|
||||
return LoadedDocument(
|
||||
text="<extracted text>",
|
||||
document_info=DocumentInfo(
|
||||
path=str(filepath),
|
||||
metadata=metadata,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Use the PdfLoader component to extract text from a PDF file."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from neo4j_graphrag.experimental.components.data_loader import PdfLoader
|
||||
|
||||
root_dir = Path(__file__).parents[4]
|
||||
file_path = root_dir / "data" / "Harry Potter and the Chamber of Secrets Summary.pdf"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
loader = PdfLoader()
|
||||
document = await loader.run(filepath=file_path)
|
||||
print(document.text[:200])
|
||||
print(document.document_info)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Use the PdfLoader component to extract text from a remote PDF file."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from neo4j_graphrag.experimental.components.data_loader import PdfLoader
|
||||
|
||||
url = "https://raw.githubusercontent.com/neo4j/neo4j-graphrag-python/c166afc4d5abc56a5686f3da46a97ed7c07da19d/examples/data/Harry%20Potter%20and%20the%20Chamber%20of%20Secrets%20Summary.pdf"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
loader = PdfLoader()
|
||||
document = await loader.run(filepath=url, fs="http")
|
||||
print(document.text[:100])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,161 @@
|
||||
"""This example demonstrates how to use the GraphPruning component."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from neo4j_graphrag.experimental.components.graph_pruning import GraphPruning
|
||||
from neo4j_graphrag.experimental.components.schema import (
|
||||
ConstraintType,
|
||||
GraphConstraintType,
|
||||
GraphSchema,
|
||||
NodeType,
|
||||
Pattern,
|
||||
PropertyType,
|
||||
RelationshipType,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import (
|
||||
Neo4jGraph,
|
||||
Neo4jNode,
|
||||
Neo4jRelationship,
|
||||
)
|
||||
|
||||
graph = Neo4jGraph(
|
||||
nodes=[
|
||||
Neo4jNode(
|
||||
id="Person/John",
|
||||
label="Person",
|
||||
properties={
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"occupation": "employee",
|
||||
},
|
||||
),
|
||||
Neo4jNode(
|
||||
id="Person/Jane",
|
||||
label="Person",
|
||||
properties={
|
||||
"firstName": "Jane",
|
||||
},
|
||||
),
|
||||
Neo4jNode(
|
||||
id="Person/Jack",
|
||||
label="Person",
|
||||
properties={"firstName": "Jack", "lastName": "Dae"},
|
||||
),
|
||||
Neo4jNode(
|
||||
id="Organization/Corp1",
|
||||
label="Organization",
|
||||
properties={"name": "Corp1"},
|
||||
),
|
||||
],
|
||||
relationships=[
|
||||
Neo4jRelationship(
|
||||
start_node_id="Person/John",
|
||||
end_node_id="Person/Jack",
|
||||
type="KNOWS",
|
||||
),
|
||||
Neo4jRelationship(
|
||||
start_node_id="Organization/Corp2",
|
||||
end_node_id="Person/Jack",
|
||||
type="WORKS_FOR",
|
||||
),
|
||||
Neo4jRelationship(
|
||||
start_node_id="Person/John",
|
||||
end_node_id="Person/Jack",
|
||||
type="PARENT_OF",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
schema = GraphSchema(
|
||||
node_types=(
|
||||
NodeType(
|
||||
label="Person",
|
||||
properties=[
|
||||
PropertyType(name="firstName", type="STRING"),
|
||||
PropertyType(name="lastName", type="STRING"),
|
||||
PropertyType(name="age", type="INTEGER"),
|
||||
],
|
||||
additional_properties=False,
|
||||
),
|
||||
NodeType(
|
||||
label="Organization",
|
||||
properties=[
|
||||
PropertyType(name="name", type="STRING"),
|
||||
PropertyType(name="address", type="STRING"),
|
||||
],
|
||||
additional_properties=True,
|
||||
),
|
||||
),
|
||||
relationship_types=(
|
||||
RelationshipType(
|
||||
label="WORKS_FOR",
|
||||
properties=[PropertyType(name="since", type="LOCAL_DATETIME")],
|
||||
additional_properties=True,
|
||||
),
|
||||
RelationshipType(
|
||||
label="KNOWS",
|
||||
),
|
||||
),
|
||||
patterns=(
|
||||
Pattern(source="Person", relationship="KNOWS", target="Person"),
|
||||
Pattern(source="Person", relationship="WORKS_FOR", target="Organization"),
|
||||
),
|
||||
constraints=(
|
||||
ConstraintType(
|
||||
type=GraphConstraintType.EXISTENCE,
|
||||
node_type="Person",
|
||||
property_names=("firstName",),
|
||||
relationship_type=None,
|
||||
),
|
||||
ConstraintType(
|
||||
type=GraphConstraintType.EXISTENCE,
|
||||
node_type="Person",
|
||||
property_names=("lastName",),
|
||||
relationship_type=None,
|
||||
),
|
||||
ConstraintType(
|
||||
type=GraphConstraintType.EXISTENCE,
|
||||
node_type="Organization",
|
||||
property_names=("name",),
|
||||
relationship_type=None,
|
||||
),
|
||||
),
|
||||
additional_node_types=False,
|
||||
additional_relationship_types=False,
|
||||
additional_patterns=False,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
pruner = GraphPruning()
|
||||
res = await pruner.run(graph, schema)
|
||||
print("=" * 20, "FINAL CLEANED GRAPH:", "=" * 20)
|
||||
print(res.graph)
|
||||
print("=" * 20, "PRUNED ITEM:", "=" * 20)
|
||||
print(res.pruning_stats)
|
||||
print("-" * 10, "PRUNED NODES:")
|
||||
for node in res.pruning_stats.pruned_nodes:
|
||||
print(
|
||||
node.item.label,
|
||||
"with properties",
|
||||
node.item.properties,
|
||||
"pruned because",
|
||||
node.pruned_reason,
|
||||
node.metadata,
|
||||
)
|
||||
print("-" * 10, "PRUNED RELATIONSHIPS:")
|
||||
for rel in res.pruning_stats.pruned_relationships:
|
||||
print(rel.item.type, "pruned because", rel.pruned_reason)
|
||||
print("-" * 10, "PRUNED PROPERTIES:")
|
||||
for prop in res.pruning_stats.pruned_properties:
|
||||
print(
|
||||
prop.item,
|
||||
"from node label",
|
||||
prop.label,
|
||||
"pruned because",
|
||||
prop.pruned_reason,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
"""The base EntityResolver class does not enforce
|
||||
a specific signature for the run method, which makes it very flexible.
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
import neo4j
|
||||
from neo4j_graphrag.experimental.components.resolver import EntityResolver
|
||||
from neo4j_graphrag.experimental.components.types import ResolutionStats
|
||||
|
||||
|
||||
class MyEntityResolver(EntityResolver):
|
||||
def __init__(
|
||||
self,
|
||||
driver: neo4j.Driver,
|
||||
filter_query: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(driver, filter_query)
|
||||
|
||||
async def run(self, *args: Any, **kwargs: Any) -> ResolutionStats:
|
||||
# logic here
|
||||
return ResolutionStats(
|
||||
number_of_nodes_to_resolve=0,
|
||||
number_of_created_nodes=0,
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""The FuzzyMatchResolver merges nodes with same label
|
||||
and similar textual properties (by default using the "name" property) based on RapidFuzz
|
||||
for string matching.
|
||||
|
||||
If the resolution is intended to be applied only on some nodes, for instance nodes that
|
||||
belong to a specific document, a "WHERE" query can be added. The only variable in the
|
||||
query scope is "entity".
|
||||
|
||||
WARNING: this process is destructive, initial nodes are deleted and replaced
|
||||
by the resolved ones, but all relationships are kept.
|
||||
See apoc.refactor.mergeNodes documentation for more details.
|
||||
"""
|
||||
|
||||
from neo4j_graphrag.experimental.components.resolver import (
|
||||
FuzzyMatchResolver,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import ResolutionStats
|
||||
|
||||
import neo4j
|
||||
|
||||
|
||||
async def main(driver: neo4j.Driver) -> None:
|
||||
resolver = FuzzyMatchResolver(
|
||||
driver,
|
||||
# let's filter all entities that belong to a certain docId
|
||||
filter_query="WHERE (entity)-[:FROM_CHUNK]->(:Chunk)-[:FROM_DOCUMENT]->(doc:"
|
||||
"Document {id = 'docId'}",
|
||||
# optionally, change the properties used for resolution (default is "name")
|
||||
# resolve_properties=["name", "ssn"],
|
||||
# the similarity threshold (default is 0.8)
|
||||
# similarity_threshold=0.9
|
||||
# and the neo4j database where data is updated
|
||||
# neo4j_database="neo4j",
|
||||
)
|
||||
res: ResolutionStats = await resolver.run()
|
||||
print(res)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""The SinglePropertyExactMatchResolver merge nodes with same label
|
||||
and exact same property value (by default using the "name" property).
|
||||
|
||||
If some nodes need to be excluded from the resolution, for instance nodes
|
||||
created from a previous run, a "WHERE" query can be added. The only variable
|
||||
in the query scope is "entity".
|
||||
|
||||
WARNING: this process is destructive, initial nodes are deleted and replaced
|
||||
by the resolved ones, but all relationships are kept.
|
||||
See apoc.refactor.mergeNodes documentation for more details.
|
||||
"""
|
||||
|
||||
import neo4j
|
||||
from neo4j_graphrag.experimental.components.resolver import (
|
||||
SinglePropertyExactMatchResolver,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import ResolutionStats
|
||||
|
||||
|
||||
async def main(driver: neo4j.Driver) -> None:
|
||||
resolver = SinglePropertyExactMatchResolver(
|
||||
driver,
|
||||
# let's filter out some entities assuming the EntityToExclude label
|
||||
# was manually added to nodes in the db
|
||||
filter_query="WHERE NOT entity:EntityToExclude",
|
||||
# another example: in some cases, we do not want to merge
|
||||
# entities whose name is John Doe because we don't know if it
|
||||
# corresponds to the same real person
|
||||
# filter_query="WHERE entity.name <> 'John Doe'",
|
||||
# optionally, change the property used for resolution (default is "name")
|
||||
# resolve_property="name",
|
||||
# and the neo4j database where data is updated
|
||||
# neo4j_database="neo4j",
|
||||
)
|
||||
res: ResolutionStats = await resolver.run()
|
||||
print(res)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""The SpaCySemanticMatchResolver merges nodes with same label
|
||||
and similar textual properties (by default using the "name" property) based on spaCy
|
||||
embeddings and cosine similarities of embedding vectors.
|
||||
|
||||
If the resolution is intended to be applied only on some nodes, for instance nodes that
|
||||
belong to a specific document, a "WHERE" query can be added. The only variable in the
|
||||
query scope is "entity".
|
||||
|
||||
WARNING: this process is destructive, initial nodes are deleted and replaced
|
||||
by the resolved ones, but all relationships are kept.
|
||||
See apoc.refactor.mergeNodes documentation for more details.
|
||||
"""
|
||||
|
||||
import neo4j
|
||||
from neo4j_graphrag.experimental.components.resolver import (
|
||||
SpaCySemanticMatchResolver,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import ResolutionStats
|
||||
|
||||
|
||||
async def main(driver: neo4j.Driver) -> None:
|
||||
resolver = SpaCySemanticMatchResolver(
|
||||
driver,
|
||||
# let's filter all entities that belong to a certain docId
|
||||
filter_query="WHERE (entity)-[:FROM_CHUNK]->(:Chunk)-[:FROM_DOCUMENT]->(doc:"
|
||||
"Document {id = 'docId'}",
|
||||
# optionally, change the properties used for resolution (default is "name")
|
||||
# resolve_properties=["name", "ssn"],
|
||||
# the similarity threshold (default is 0.8)
|
||||
# similarity_threshold=0.9
|
||||
# the spaCy trained model (default is "en_core_web_lg")
|
||||
# spacy_model="en_core_web_sm"
|
||||
# and the neo4j database where data is updated
|
||||
# neo4j_database="neo4j",
|
||||
)
|
||||
res: ResolutionStats = await resolver.run()
|
||||
print(res)
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) "Neo4j"
|
||||
# Neo4j Sweden AB [https://neo4j.com]
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# #
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from neo4j_graphrag.experimental.components.schema import (
|
||||
SchemaBuilder,
|
||||
NodeType,
|
||||
PropertyType,
|
||||
RelationshipType,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
schema_builder = SchemaBuilder()
|
||||
|
||||
result = await schema_builder.run(
|
||||
node_types=[
|
||||
NodeType(
|
||||
label="Person",
|
||||
properties=[
|
||||
PropertyType(name="name", type="STRING"),
|
||||
PropertyType(name="place_of_birth", type="STRING"),
|
||||
PropertyType(name="date_of_birth", type="DATE"),
|
||||
],
|
||||
),
|
||||
NodeType(
|
||||
label="Organization",
|
||||
properties=[
|
||||
PropertyType(name="name", type="STRING"),
|
||||
PropertyType(name="country", type="STRING"),
|
||||
],
|
||||
),
|
||||
],
|
||||
relationship_types=[
|
||||
RelationshipType(
|
||||
label="WORKED_ON",
|
||||
),
|
||||
RelationshipType(
|
||||
label="WORKED_FOR",
|
||||
),
|
||||
],
|
||||
patterns=[
|
||||
("Person", "WORKED_ON", "Field"),
|
||||
("Person", "WORKED_FOR", "Organization"),
|
||||
],
|
||||
)
|
||||
print(result)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""This example demonstrates how to use the SchemaFromExistingGraphExtractor component
|
||||
to automatically extract a schema from an existing Neo4j database.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pprint import pprint
|
||||
|
||||
import neo4j
|
||||
|
||||
from neo4j_graphrag.experimental.components.schema import (
|
||||
SchemaFromExistingGraphExtractor,
|
||||
GraphSchema,
|
||||
)
|
||||
|
||||
|
||||
URI = "neo4j+s://demo.neo4jlabs.com"
|
||||
AUTH = ("recommendations", "recommendations")
|
||||
DATABASE = "recommendations"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the example."""
|
||||
|
||||
with neo4j.GraphDatabase.driver(
|
||||
URI,
|
||||
auth=AUTH,
|
||||
) as driver:
|
||||
extractor = SchemaFromExistingGraphExtractor(
|
||||
driver,
|
||||
# optional:
|
||||
neo4j_database=DATABASE,
|
||||
additional_patterns=True,
|
||||
additional_node_types=True,
|
||||
additional_relationship_types=True,
|
||||
additional_properties=True,
|
||||
)
|
||||
schema: GraphSchema = await extractor.run()
|
||||
# schema.store_as_json("my_schema.json")
|
||||
pprint(schema.model_dump())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,131 @@
|
||||
"""This example demonstrates how to use the SchemaFromTextExtractor component
|
||||
to automatically extract a schema from text and save it to JSON and YAML files.
|
||||
|
||||
The SchemaFromTextExtractor component uses an LLM to analyze the text and identify entities,
|
||||
relations, and their properties.
|
||||
|
||||
Note: This example requires an OpenAI API key to be set in the .env file.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from neo4j_graphrag.experimental.components.schema import (
|
||||
SchemaFromTextExtractor,
|
||||
GraphSchema,
|
||||
)
|
||||
from neo4j_graphrag.llm import OpenAILLM
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig()
|
||||
logging.getLogger("neo4j_graphrag").setLevel(logging.INFO)
|
||||
|
||||
# Sample text to extract schema from - it's about a company and its employees
|
||||
TEXT = """
|
||||
Acme Corporation was founded in 1985 by John Smith in New York City.
|
||||
The company specializes in manufacturing high-quality widgets and gadgets
|
||||
for the consumer electronics industry.
|
||||
|
||||
Sarah Johnson joined Acme in 2010 as a Senior Engineer and was promoted to
|
||||
Engineering Director in 2015. She oversees a team of 12 engineers working on
|
||||
next-generation products. Sarah holds a PhD in Electrical Engineering from MIT
|
||||
and has filed 5 patents during her time at Acme.
|
||||
|
||||
The company expanded to international markets in 2012, opening offices in London,
|
||||
Tokyo, and Berlin. Each office is managed by a regional director who reports
|
||||
directly to the CEO, Michael Brown, who took over leadership in 2008.
|
||||
|
||||
Acme's most successful product, the SuperWidget X1, was launched in 2018 and
|
||||
has sold over 2 million units worldwide. The product was developed by a team led
|
||||
by Robert Chen, who joined the company in 2016 after working at TechGiant for 8 years.
|
||||
|
||||
The company currently employs 250 people across its 4 locations and had a revenue
|
||||
of $75 million in the last fiscal year. Acme is planning to go public in 2024
|
||||
with an estimated valuation of $500 million.
|
||||
"""
|
||||
|
||||
# Define the file paths for saving the schema
|
||||
root_dir = Path(__file__).parents[4]
|
||||
OUTPUT_DIR = str(root_dir / "data")
|
||||
JSON_FILE_PATH = str(root_dir / "data" / "extracted_schema.json")
|
||||
YAML_FILE_PATH = str(root_dir / "data" / "extracted_schema.yaml")
|
||||
|
||||
|
||||
async def extract_and_save_schema() -> None:
|
||||
"""Extract schema from text and save it to JSON and YAML files."""
|
||||
|
||||
# Define LLM parameters
|
||||
llm_model_params = {
|
||||
"max_completion_tokens": 2000,
|
||||
"response_format": {"type": "json_object"},
|
||||
"temperature": 0, # Lower temperature for more consistent output
|
||||
}
|
||||
|
||||
# Create the LLM instance
|
||||
llm = OpenAILLM(
|
||||
model_name="gpt-5",
|
||||
model_params=llm_model_params,
|
||||
)
|
||||
|
||||
try:
|
||||
# Create a SchemaFromTextExtractor component with the default template
|
||||
schema_extractor = SchemaFromTextExtractor(llm=llm)
|
||||
|
||||
print("Extracting schema from text...")
|
||||
# Extract schema from text
|
||||
inferred_schema = await schema_extractor.run(text=TEXT)
|
||||
|
||||
# Ensure the output directory exists
|
||||
Path(OUTPUT_DIR).mkdir(exist_ok=True)
|
||||
|
||||
print(f"Saving schema to JSON file: {JSON_FILE_PATH}")
|
||||
# Save the schema to JSON file
|
||||
inferred_schema.save(JSON_FILE_PATH, overwrite=True)
|
||||
|
||||
print(f"Saving schema to YAML file: {YAML_FILE_PATH}")
|
||||
# Save the schema to YAML file
|
||||
inferred_schema.save(YAML_FILE_PATH, overwrite=True)
|
||||
|
||||
print("\nExtracted Schema Summary:")
|
||||
print(f"Node types: {list(inferred_schema.node_types)}")
|
||||
print(
|
||||
f"Relationship types: {list(inferred_schema.relationship_types if inferred_schema.relationship_types else [])}"
|
||||
)
|
||||
|
||||
if inferred_schema.patterns:
|
||||
print("\nPatterns:")
|
||||
for entity1, relation, entity2 in inferred_schema.patterns:
|
||||
print(f" {entity1} --[{relation}]--> {entity2}")
|
||||
|
||||
finally:
|
||||
# Close the LLM client
|
||||
if hasattr(llm, "aclose"):
|
||||
await llm.aclose()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the example."""
|
||||
|
||||
# extract schema and save to files
|
||||
await extract_and_save_schema()
|
||||
|
||||
print("\nSchema files have been saved to:")
|
||||
print(f" - JSON: {JSON_FILE_PATH}")
|
||||
print(f" - YAML: {YAML_FILE_PATH}")
|
||||
|
||||
# load schema from files
|
||||
print("\nLoading schemas from saved files:")
|
||||
schema_from_json = GraphSchema.from_file(JSON_FILE_PATH)
|
||||
schema_from_yaml = GraphSchema.from_file(YAML_FILE_PATH)
|
||||
|
||||
print(f"Node types in JSON schema: {list(schema_from_json.node_types)}")
|
||||
print(f"Node types in YAML schema: {list(schema_from_yaml.node_types)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Simple example demonstrating structured output with SchemaFromTextExtractor.
|
||||
|
||||
This example shows how to use structured output for more reliable schema extraction
|
||||
with automatic validation against the GraphSchema Pydantic model.
|
||||
|
||||
The GraphSchema is now compatible with both OpenAI and VertexAI structured output APIs,
|
||||
with strict validation and proper field definitions. With structured output enabled:
|
||||
- Uses LLMInterfaceV2 (list of messages)
|
||||
- Passes GraphSchema Pydantic model as response_format to ainvoke()
|
||||
- Ensures response conforms to expected schema structure
|
||||
- Provides automatic type validation
|
||||
- Reduces need for JSON repair and error handling
|
||||
- Enforces min_length=1 on node properties (nodes must have at least one property)
|
||||
|
||||
Prerequisites:
|
||||
- Google Cloud credentials configured for VertexAI
|
||||
- Or OpenAI API key set in OPENAI_API_KEY environment variable
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from neo4j_graphrag.experimental.components.schema import (
|
||||
SchemaFromTextExtractor,
|
||||
GraphSchema,
|
||||
)
|
||||
from neo4j_graphrag.llm import OpenAILLM
|
||||
|
||||
|
||||
# Sample text to extract schema from
|
||||
SAMPLE_TEXT = """
|
||||
Acme Corporation was founded in 1985 by John Smith in New York City.
|
||||
The company specializes in manufacturing high-quality widgets and gadgets
|
||||
for the consumer electronics industry.
|
||||
|
||||
Sarah Johnson joined Acme in 2010 as a Senior Engineer and was promoted to
|
||||
Engineering Director in 2015. She oversees a team of 12 engineers working on
|
||||
next-generation products. Sarah holds a PhD in Electrical Engineering from MIT
|
||||
and has filed 5 patents during her time at Acme.
|
||||
|
||||
The company expanded to international markets in 2012, opening offices in London,
|
||||
Tokyo, and Berlin. Each office is managed by a regional director who reports
|
||||
directly to the CEO, Michael Brown, who took over leadership in 2008.
|
||||
|
||||
Acme's most successful product, the SuperWidget X1, was launched in 2018 and
|
||||
has sold over 2 million units worldwide. The product was developed by a team led
|
||||
by Robert Chen, who joined the company in 2016 after working at TechGiant for 8 years.
|
||||
"""
|
||||
|
||||
|
||||
def print_schema_summary(schema: GraphSchema, title: str) -> None:
|
||||
"""Print a formatted summary of the extracted schema."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{title}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print(f"\nNode Types ({len(schema.node_types)}):")
|
||||
for node in schema.node_types:
|
||||
props = [f"{p.name} ({p.type})" for p in node.properties]
|
||||
print(f" - {node.label}")
|
||||
if props:
|
||||
print(f" Properties: {', '.join(props)}")
|
||||
if node.description:
|
||||
print(f" Description: {node.description}")
|
||||
|
||||
if schema.relationship_types:
|
||||
print(f"\nRelationship Types ({len(schema.relationship_types)}):")
|
||||
for rel in schema.relationship_types:
|
||||
props = [f"{p.name} ({p.type})" for p in rel.properties]
|
||||
print(f" - {rel.label}")
|
||||
if props:
|
||||
print(f" Properties: {', '.join(props)}")
|
||||
|
||||
if schema.patterns:
|
||||
print(f"\nPatterns ({len(schema.patterns)}):")
|
||||
for source, relationship, target in schema.patterns:
|
||||
print(f" {source} --[{relationship}]--> {target}")
|
||||
|
||||
if schema.constraints:
|
||||
print(f"\nConstraints ({len(schema.constraints)}):")
|
||||
for constraint in schema.constraints:
|
||||
print(
|
||||
f" - {constraint.type} on {constraint.node_type}.{list(constraint.property_names)}"
|
||||
)
|
||||
|
||||
|
||||
async def test_v1_without_structured_output() -> GraphSchema:
|
||||
"""
|
||||
Test V1 approach (default): Prompt-based JSON extraction with manual cleanup.
|
||||
|
||||
With use_structured_output=False (default):
|
||||
- Uses LLMInterface V1 (plain string prompts)
|
||||
- LLM returns JSON string that needs parsing and cleanup
|
||||
- Extensive filtering and validation applied manually
|
||||
- More forgiving of LLM errors
|
||||
- Works with all LLM providers
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Testing V1: Prompt-based JSON extraction (default)")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize LLM with response_format for JSON mode (V1 approach)
|
||||
llm = OpenAILLM(
|
||||
model_name="gpt-5-mini",
|
||||
model_params={
|
||||
"temperature": 0,
|
||||
"response_format": {"type": "json_object"}, # JSON mode for V1
|
||||
},
|
||||
)
|
||||
|
||||
# For VertexAI V1, use:
|
||||
# llm = VertexAILLM(
|
||||
# model_name="gemini-2.5-flash",
|
||||
# model_params={"temperature": 0}
|
||||
# )
|
||||
|
||||
# Create extractor WITHOUT structured output (V1 default)
|
||||
extractor = SchemaFromTextExtractor(
|
||||
llm=llm,
|
||||
use_structured_output=False, # Default, can be omitted
|
||||
)
|
||||
|
||||
# Extract schema
|
||||
schema = await extractor.run(text=SAMPLE_TEXT)
|
||||
|
||||
print_schema_summary(schema, "V1 Result (Prompt-based)")
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
async def test_v2_with_structured_output() -> GraphSchema:
|
||||
"""
|
||||
Test V2 approach: Structured output with GraphSchema validation.
|
||||
|
||||
With use_structured_output=True:
|
||||
- Uses LLMInterfaceV2 (list of messages)
|
||||
- Passes GraphSchema as response_format to ainvoke()
|
||||
- LLM returns properly structured data conforming to GraphSchema
|
||||
- Automatic validation via Pydantic
|
||||
- Less manual cleanup needed
|
||||
- Only works with OpenAI and VertexAI
|
||||
- Enforces min_length=1 on node properties
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Testing V2: Structured output with GraphSchema")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize LLM - NO response_format in constructor for V2!
|
||||
llm = OpenAILLM(model_name="gpt-5-mini", model_params={"temperature": 0})
|
||||
|
||||
# For VertexAI V2, use:
|
||||
# llm = VertexAILLM(
|
||||
# model_name="gemini-2.5-flash",
|
||||
# model_params={"temperature": 0}
|
||||
# )
|
||||
|
||||
# Create extractor WITH structured output (V2)
|
||||
extractor = SchemaFromTextExtractor(
|
||||
llm=llm,
|
||||
use_structured_output=True, # This is the key parameter!
|
||||
)
|
||||
|
||||
# Extract schema
|
||||
schema = await extractor.run(text=SAMPLE_TEXT)
|
||||
|
||||
print_schema_summary(schema, "V2 Result (Structured Output)")
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
async def compare_approaches() -> None:
|
||||
"""Run both approaches and compare results."""
|
||||
load_dotenv()
|
||||
|
||||
# Test V1 (default)
|
||||
schema_v1 = await test_v1_without_structured_output()
|
||||
|
||||
# Test V2 (structured output)
|
||||
schema_v2 = await test_v2_with_structured_output()
|
||||
|
||||
# Comparison
|
||||
print("\n" + "=" * 60)
|
||||
print("COMPARISON")
|
||||
print("=" * 60)
|
||||
print("V1 (Prompt-based):")
|
||||
print(f" - Node types: {len(schema_v1.node_types)}")
|
||||
print(f" - Relationship types: {len(schema_v1.relationship_types)}")
|
||||
print(f" - Patterns: {len(schema_v1.patterns)}")
|
||||
print(
|
||||
f" - Total properties: {sum(len(n.properties) for n in schema_v1.node_types)}"
|
||||
)
|
||||
|
||||
print("\nV2 (Structured Output):")
|
||||
print(f" - Node types: {len(schema_v2.node_types)}")
|
||||
print(f" - Relationship types: {len(schema_v2.relationship_types)}")
|
||||
print(f" - Patterns: {len(schema_v2.patterns)}")
|
||||
print(
|
||||
f" - Total properties: {sum(len(n.properties) for n in schema_v2.node_types)}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run comparison between V1 and V2
|
||||
asyncio.run(compare_approaches())
|
||||
@@ -0,0 +1,14 @@
|
||||
from neo4j_graphrag.experimental.components.text_splitters.base import TextSplitter
|
||||
from neo4j_graphrag.experimental.components.types import TextChunk, TextChunks
|
||||
|
||||
|
||||
class MySplitter(TextSplitter):
|
||||
async def run(self, text: str) -> TextChunks:
|
||||
# your logic here
|
||||
return TextChunks(
|
||||
chunks=[
|
||||
TextChunk(text="", index=0),
|
||||
# optional metadata
|
||||
TextChunk(text="", index=1, metadata={"key": "value"}),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
from neo4j_graphrag.experimental.components.text_splitters.fixed_size_splitter import (
|
||||
FixedSizeSplitter,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import TextChunks
|
||||
|
||||
|
||||
async def main() -> TextChunks:
|
||||
splitter = FixedSizeSplitter(
|
||||
# optionally, configure chunk_size, chunk_overlap, and approximate flag
|
||||
# chunk_size=4000,
|
||||
# chunk_overlap=200,
|
||||
# approximate = False
|
||||
)
|
||||
chunks = await splitter.run(text="text to split")
|
||||
return chunks
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Implement a custom writer to save the results, for instance by using
|
||||
custom Cypher queries.
|
||||
"""
|
||||
|
||||
import neo4j
|
||||
from neo4j_graphrag.experimental.components.kg_writer import KGWriter, KGWriterModel
|
||||
from neo4j_graphrag.experimental.components.types import LexicalGraphConfig, Neo4jGraph
|
||||
from pydantic import validate_call
|
||||
from neo4j_graphrag.utils import driver_config
|
||||
|
||||
|
||||
class MyWriter(KGWriter):
|
||||
def __init__(self, driver: neo4j.Driver) -> None:
|
||||
self.driver = driver_config.override_user_agent(driver)
|
||||
|
||||
@validate_call
|
||||
async def run(
|
||||
self,
|
||||
graph: Neo4jGraph,
|
||||
lexical_graph_config: LexicalGraphConfig = LexicalGraphConfig(),
|
||||
) -> KGWriterModel:
|
||||
try:
|
||||
self.driver.execute_query("my query")
|
||||
return KGWriterModel(status="SUCCESS")
|
||||
except Exception:
|
||||
return KGWriterModel(status="FAILURE")
|
||||
@@ -0,0 +1,19 @@
|
||||
import neo4j
|
||||
from neo4j_graphrag.experimental.components.kg_writer import (
|
||||
KGWriterModel,
|
||||
Neo4jWriter,
|
||||
)
|
||||
from neo4j_graphrag.experimental.components.types import Neo4jGraph
|
||||
|
||||
|
||||
async def main(driver: neo4j.Driver, graph: Neo4jGraph) -> KGWriterModel:
|
||||
writer = Neo4jWriter(
|
||||
driver,
|
||||
# optionally, configure the neo4j database
|
||||
# neo4j_database="neo4j",
|
||||
# you can tune batch_size to
|
||||
# improve speed
|
||||
# batch_size=1000,
|
||||
)
|
||||
result = await writer.run(graph=graph)
|
||||
return result
|
||||
Reference in New Issue
Block a user