Skip to content

Commit 62d0614

Browse files
Merge branch 'STAGING' of https://github.com/neo4j-labs/llm-graph-builder into STAGING
2 parents 527052c + 6998691 commit 62d0614

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+832
-602
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ Allow unauthenticated request : Yes
149149
| VITE_GOOGLE_CLIENT_ID | Optional | | Client ID for Google authentication |
150150
| VITE_LLM_MODELS_PROD | Optional | openai_gpt_4o,openai_gpt_4o_mini,diffbot,gemini_1.5_flash | To Distinguish models based on the Enviornment PROD or DEV
151151
| VITE_LLM_MODELS | Optional | 'diffbot,openai_gpt_3.5,openai_gpt_4o,openai_gpt_4o_mini,gemini_1.5_pro,gemini_1.5_flash,azure_ai_gpt_35,azure_ai_gpt_4o,ollama_llama3,groq_llama3_70b,anthropic_claude_3_5_sonnet' | Supported Models For the application
152+
| VITE_AUTH0_CLIENT_ID | Mandatory if you are enabling Authentication otherwise it is optional | |Okta Oauth Client ID for authentication
153+
| VITE_AUTH0_DOMAIN | Mandatory if you are enabling Authentication otherwise it is optional | | Okta Oauth Cliend Domain
154+
| VITE_SKIP_AUTH | Optional | true | Flag to skip the authentication
152155

153156
## LLMs Supported
154157
1. OpenAI

backend/example.env

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ LLM_MODEL_CONFIG_ollama_llama3="model_name,model_local_url"
4444
YOUTUBE_TRANSCRIPT_PROXY="https://user:pass@domain:port"
4545
EFFECTIVE_SEARCH_RATIO=5
4646
GRAPH_CLEANUP_MODEL="openai_gpt_4o"
47-
CHUNKS_TO_BE_PROCESSED="50"
47+
CHUNKS_TO_BE_CREATED="50"
48+
BEDROCK_EMBEDDING_MODEL="model_name,aws_access_key,aws_secret_key,region_name" #model_name="amazon.titan-embed-text-v1"
49+
LLM_MODEL_CONFIG_bedrock_nova_micro_v1="model_name,aws_access_key,aws_secret_key,region_name" #model_name="amazon.nova-micro-v1:0"
50+
LLM_MODEL_CONFIG_bedrock_nova_lite_v1="model_name,aws_access_key,aws_secret_key,region_name" #model_name="amazon.nova-lite-v1:0"
51+
LLM_MODEL_CONFIG_bedrock_nova_pro_v1="model_name,aws_access_key,aws_secret_key,region_name" #model_name="amazon.nova-pro-v1:0"

backend/score.py

Lines changed: 72 additions & 60 deletions
Large diffs are not rendered by default.

backend/src/create_chunks.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
from src.document_sources.youtube import get_chunks_with_timestamps, get_calculated_timestamps
66
import re
7+
import os
78

89
logging.basicConfig(format="%(asctime)s - %(message)s", level="INFO")
910

@@ -25,23 +26,28 @@ def split_file_into_chunks(self):
2526
"""
2627
logging.info("Split file into smaller chunks")
2728
text_splitter = TokenTextSplitter(chunk_size=200, chunk_overlap=20)
29+
chunk_to_be_created = int(os.environ.get('CHUNKS_TO_BE_CREATED', '50'))
2830
if 'page' in self.pages[0].metadata:
2931
chunks = []
3032
for i, document in enumerate(self.pages):
3133
page_number = i + 1
32-
for chunk in text_splitter.split_documents([document]):
33-
chunks.append(Document(page_content=chunk.page_content, metadata={'page_number':page_number}))
34+
if len(chunks) >= chunk_to_be_created:
35+
break
36+
else:
37+
for chunk in text_splitter.split_documents([document]):
38+
chunks.append(Document(page_content=chunk.page_content, metadata={'page_number':page_number}))
3439

3540
elif 'length' in self.pages[0].metadata:
3641
if len(self.pages) == 1 or (len(self.pages) > 1 and self.pages[1].page_content.strip() == ''):
3742
match = re.search(r'(?:v=)([0-9A-Za-z_-]{11})\s*',self.pages[0].metadata['source'])
3843
youtube_id=match.group(1)
3944
chunks_without_time_range = text_splitter.split_documents([self.pages[0]])
40-
chunks = get_calculated_timestamps(chunks_without_time_range, youtube_id)
41-
45+
chunks = get_calculated_timestamps(chunks_without_time_range[:chunk_to_be_created], youtube_id)
4246
else:
43-
chunks_without_time_range = text_splitter.split_documents(self.pages)
44-
chunks = get_chunks_with_timestamps(chunks_without_time_range)
47+
chunks_without_time_range = text_splitter.split_documents(self.pages)
48+
chunks = get_chunks_with_timestamps(chunks_without_time_range[:chunk_to_be_created])
4549
else:
4650
chunks = text_splitter.split_documents(self.pages)
51+
52+
chunks = chunks[:chunk_to_be_created]
4753
return chunks

backend/src/graphDB_dataAccess.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,4 +535,30 @@ def update_node_relationship_count(self,document_name):
535535
"nodeCount" : nodeCount,
536536
"relationshipCount" : relationshipCount
537537
}
538-
return response
538+
return response
539+
540+
def get_nodelabels_relationships(self):
541+
node_query = """
542+
CALL db.labels() YIELD label
543+
WITH label
544+
WHERE NOT label IN ['Document', 'Chunk', '_Bloom_Perspective_', '__Community__', '__Entity__']
545+
CALL apoc.cypher.run("MATCH (n:`" + label + "`) RETURN count(n) AS count",{}) YIELD value
546+
WHERE value.count > 0
547+
RETURN label order by label
548+
"""
549+
550+
relation_query = """
551+
CALL db.relationshipTypes() yield relationshipType
552+
WHERE NOT relationshipType IN ['PART_OF', 'NEXT_CHUNK', 'HAS_ENTITY', '_Bloom_Perspective_','FIRST_CHUNK','SIMILAR','IN_COMMUNITY','PARENT_COMMUNITY']
553+
return relationshipType order by relationshipType
554+
"""
555+
556+
try:
557+
node_result = self.execute_query(node_query)
558+
node_labels = [record["label"] for record in node_result]
559+
relationship_result = self.execute_query(relation_query)
560+
relationship_types = [record["relationshipType"] for record in relationship_result]
561+
return node_labels,relationship_types
562+
except Exception as e:
563+
print(f"Error in getting node labels/relationship types from db: {e}")
564+
return []

backend/src/llm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def get_llm(model: str):
8989
)
9090

9191
llm = ChatBedrock(
92-
client=bedrock_client, model_id=model_name, model_kwargs=dict(temperature=0)
92+
client=bedrock_client,region_name=region_name, model_id=model_name, model_kwargs=dict(temperature=0)
9393
)
9494

9595
elif "ollama" in model:

backend/src/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,6 @@ async def processing_source(uri, userName, password, database, model, file_name,
361361

362362
logging.info('Update the status as Processing')
363363
update_graph_chunk_processed = int(os.environ.get('UPDATE_GRAPH_CHUNKS_PROCESSED'))
364-
chunk_to_be_processed = int(os.environ.get('CHUNKS_TO_BE_PROCESSED', '50'))
365364
# selected_chunks = []
366365
is_cancelled_status = False
367366
job_status = "Completed"
@@ -676,7 +675,7 @@ def get_labels_and_relationtypes(graph):
676675
query = """
677676
RETURN collect {
678677
CALL db.labels() yield label
679-
WHERE NOT label IN ['Chunk','_Bloom_Perspective_', '__Community__', '__Entity__']
678+
WHERE NOT label IN ['Document','Chunk','_Bloom_Perspective_', '__Community__', '__Entity__']
680679
return label order by label limit 100 } as labels,
681680
collect {
682681
CALL db.relationshipTypes() yield relationshipType as type

backend/src/post_processing.py

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from langchain_core.prompts import ChatPromptTemplate
99
from src.shared.constants import GRAPH_CLEANUP_PROMPT
1010
from src.llm import get_llm
11-
from src.main import get_labels_and_relationtypes
11+
from src.graphDB_dataAccess import graphDBdataAccess
12+
import time
1213

1314
DROP_INDEX_QUERY = "DROP INDEX entities IF EXISTS;"
1415
LABELS_QUERY = "CALL db.labels()"
@@ -195,50 +196,35 @@ def update_embeddings(rows, graph):
195196
return graph.query(query,params={'rows':rows})
196197

197198
def graph_schema_consolidation(graph):
198-
nodes_and_relations = get_labels_and_relationtypes(graph)
199-
logging.info(f"nodes_and_relations in existing graph : {nodes_and_relations}")
200-
node_labels = []
201-
relation_labels = []
202-
203-
node_labels.extend(nodes_and_relations[0]['labels'])
204-
relation_labels.extend(nodes_and_relations[0]['relationshipTypes'])
205-
199+
graphDb_data_Access = graphDBdataAccess(graph)
200+
node_labels,relation_labels = graphDb_data_Access.get_nodelabels_relationships()
206201
parser = JsonOutputParser()
207-
prompt = ChatPromptTemplate(messages=[("system",GRAPH_CLEANUP_PROMPT),("human", "{input}")],
208-
partial_variables={"format_instructions": parser.get_format_instructions()})
209-
210-
graph_cleanup_model = os.getenv("GRAPH_CLEANUP_MODEL",'openai_gpt_4o')
202+
prompt = ChatPromptTemplate(
203+
messages=[("system", GRAPH_CLEANUP_PROMPT), ("human", "{input}")],
204+
partial_variables={"format_instructions": parser.get_format_instructions()}
205+
)
206+
graph_cleanup_model = os.getenv("GRAPH_CLEANUP_MODEL", 'openai_gpt_4o')
211207
llm, _ = get_llm(graph_cleanup_model)
212208
chain = prompt | llm | parser
213-
nodes_dict = chain.invoke({'input':node_labels})
214-
relation_dict = chain.invoke({'input':relation_labels})
215-
216-
node_match = {}
217-
relation_match = {}
218-
for new_label , values in nodes_dict.items() :
219-
for old_label in values:
220-
if new_label != old_label:
221-
node_match[old_label]=new_label
222-
223-
for new_label , values in relation_dict.items() :
224-
for old_label in values:
225-
if new_label != old_label:
226-
relation_match[old_label]=new_label
227209

228-
logging.info(f"updated node labels : {node_match}")
229-
logging.info(f"updated relationship labels : {relation_match}")
230-
231-
# Update node labels in graph
232-
for old_label, new_label in node_match.items():
233-
query = f"""
234-
MATCH (n:`{old_label}`)
235-
SET n:`{new_label}`
236-
REMOVE n:`{old_label}`
237-
"""
238-
graph.query(query)
210+
nodes_relations_input = {'nodes': node_labels, 'relationships': relation_labels}
211+
mappings = chain.invoke({'input': nodes_relations_input})
212+
node_mapping = {old: new for new, old_list in mappings['nodes'].items() for old in old_list if new != old}
213+
relation_mapping = {old: new for new, old_list in mappings['relationships'].items() for old in old_list if new != old}
214+
215+
logging.info(f"Node Labels: Total = {len(node_labels)}, Reduced to = {len(set(node_mapping.values()))} (from {len(node_mapping)})")
216+
logging.info(f"Relationship Types: Total = {len(relation_labels)}, Reduced to = {len(set(relation_mapping.values()))} (from {len(relation_mapping)})")
217+
218+
if node_mapping:
219+
for old_label, new_label in node_mapping.items():
220+
query = f"""
221+
MATCH (n:`{old_label}`)
222+
SET n:`{new_label}`
223+
REMOVE n:`{old_label}`
224+
"""
225+
graph.query(query)
239226

240-
# Update relation types in graph
241-
for old_label, new_label in relation_match.items():
227+
for old_label, new_label in relation_mapping.items():
242228
query = f"""
243229
MATCH (n)-[r:`{old_label}`]->(m)
244230
CREATE (n)-[r2:`{new_label}`]->(m)

backend/src/shared/common_fn.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
import os
1212
from pathlib import Path
1313
from urllib.parse import urlparse
14-
14+
import boto3
15+
from langchain_community.embeddings import BedrockEmbeddings
1516

1617
def check_url_source(source_type, yt_url:str=None, wiki_query:str=None):
1718
language=''
@@ -77,6 +78,10 @@ def load_embedding_model(embedding_model_name: str):
7778
)
7879
dimension = 768
7980
logging.info(f"Embedding: Using Vertex AI Embeddings , Dimension:{dimension}")
81+
elif embedding_model_name == "titan":
82+
embeddings = get_bedrock_embeddings()
83+
dimension = 1536
84+
logging.info(f"Embedding: Using bedrock titan Embeddings , Dimension:{dimension}")
8085
else:
8186
embeddings = HuggingFaceEmbeddings(
8287
model_name="all-MiniLM-L6-v2"#, cache_folder="/embedding_model"
@@ -134,4 +139,38 @@ def last_url_segment(url):
134139
parsed_url = urlparse(url)
135140
path = parsed_url.path.strip("/") # Remove leading and trailing slashes
136141
last_url_segment = path.split("/")[-1] if path else parsed_url.netloc.split(".")[0]
137-
return last_url_segment
142+
return last_url_segment
143+
144+
def get_bedrock_embeddings():
145+
"""
146+
Creates and returns a BedrockEmbeddings object using the specified model name.
147+
Args:
148+
model (str): The name of the model to use for embeddings.
149+
Returns:
150+
BedrockEmbeddings: An instance of the BedrockEmbeddings class.
151+
"""
152+
try:
153+
env_value = os.getenv("BEDROCK_EMBEDDING_MODEL")
154+
if not env_value:
155+
raise ValueError("Environment variable 'BEDROCK_EMBEDDING_MODEL' is not set.")
156+
try:
157+
model_name, aws_access_key, aws_secret_key, region_name = env_value.split(",")
158+
except ValueError:
159+
raise ValueError(
160+
"Environment variable 'BEDROCK_EMBEDDING_MODEL' is improperly formatted. "
161+
"Expected format: 'model_name,aws_access_key,aws_secret_key,region_name'."
162+
)
163+
bedrock_client = boto3.client(
164+
service_name="bedrock-runtime",
165+
region_name=region_name.strip(),
166+
aws_access_key_id=aws_access_key.strip(),
167+
aws_secret_access_key=aws_secret_key.strip(),
168+
)
169+
bedrock_embeddings = BedrockEmbeddings(
170+
model_id=model_name.strip(),
171+
client=bedrock_client
172+
)
173+
return bedrock_embeddings
174+
except Exception as e:
175+
print(f"An unexpected error occurred: {e}")
176+
raise

backend/src/shared/constants.py

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -831,27 +831,62 @@
831831
DELETE_ENTITIES_AND_START_FROM_BEGINNING = "delete_entities_and_start_from_beginning"
832832
START_FROM_LAST_PROCESSED_POSITION = "start_from_last_processed_position"
833833

834-
GRAPH_CLEANUP_PROMPT = """Please consolidate the following list of types into a smaller set of more general, semantically
835-
related types. The consolidated types must be drawn from the original list; do not introduce new types.
836-
Return a JSON object representing the mapping of original types to consolidated types. Every key is the consolidated type
837-
and value is list of the original types that were merged into the consolidated type. Prioritize using the most generic and
838-
repeated term when merging. If a type doesn't merge with any other type, it should still be included in the output,
839-
mapped to itself.
840-
841-
**Input:** A list of strings representing the types to be consolidated. These types may represent either node
842-
labels or relationship labels Your algorithm should do appropriate groupings based on semantic similarity.
843-
844-
Example 1:
845-
Input:
846-
[ "Person", "Human", "People", "Company", "Organization", "Product"]
847-
Output :
848-
[Person": ["Person", "Human", "People"], Organization": ["Company", "Organization"], Product": ["Product"]]
849-
850-
Example 2:
851-
Input :
852-
["CREATED_FOR", "CREATED_TO", "CREATED", "PLACE", "LOCATION", "VENUE"]
834+
GRAPH_CLEANUP_PROMPT = """
835+
You are tasked with organizing a list of types into semantic categories based on their meanings, including synonyms or morphological similarities. The input will include two separate lists: one for **Node Labels** and one for **Relationship Types**. Follow these rules strictly:
836+
### 1. Input Format
837+
The input will include two keys:
838+
- `nodes`: A list of node labels.
839+
- `relationships`: A list of relationship types.
840+
### 2. Grouping Rules
841+
- Group similar items into **semantic categories** based on their meaning or morphological similarities.
842+
- The name of each category must be chosen from the types in the input list (node labels or relationship types). **Do not create or infer new names for categories**.
843+
- Items that cannot be grouped must remain in their own category.
844+
### 3. Naming Rules
845+
- The category name must reflect the grouped items and must be an existing type in the input list.
846+
- Use a widely applicable type as the category name.
847+
- **Do not introduce new names or types** under any circumstances.
848+
### 4. Output Rules
849+
- Return the output as a JSON object with two keys:
850+
- `nodes`: A dictionary where each key represents a category name for nodes, and its value is a list of original node labels in that category.
851+
- `relationships`: A dictionary where each key represents a category name for relationships, and its value is a list of original relationship types in that category.
852+
- Every key and value must come from the provided input lists.
853+
### 5. Examples
854+
#### Example 1:
855+
Input:
856+
{{
857+
"nodes": ["Person", "Human", "People", "Company", "Organization", "Product"],
858+
"relationships": ["CREATED_FOR", "CREATED_TO", "CREATED", "PUBLISHED","PUBLISHED_BY", "PUBLISHED_IN", "PUBLISHED_ON"]
859+
}}
860+
Output in JSON:
861+
{{
862+
"nodes": {{
863+
"Person": ["Person", "Human", "People"],
864+
"Organization": ["Company", "Organization"],
865+
"Product": ["Product"]
866+
}},
867+
"relationships": {{
868+
"CREATED": ["CREATED_FOR", "CREATED_TO", "CREATED"],
869+
"PUBLISHED": ["PUBLISHED_BY", "PUBLISHED_IN", "PUBLISHED_ON"]
870+
}}
871+
}}
872+
#### Example 2: Avoid redundant or incorrect grouping
873+
Input:
874+
{{
875+
"nodes": ["Process", "Process_Step", "Step", "Procedure", "Method", "Natural Process", "Step"],
876+
"relationships": ["USED_FOR", "USED_BY", "USED_WITH", "USED_IN"]
877+
}}
853878
Output:
854-
["CREATED": ["CREATED_FOR", "CREATED_TO", "CREATED"],"PLACE": ["PLACE", "LOCATION", "VENUE"]]
879+
{{
880+
"nodes": {{
881+
"Process": ["Process", "Process_Step", "Step", "Procedure", "Method", "Natural Process"]
882+
}},
883+
"relationships": {{
884+
"USED": ["USED_FOR", "USED_BY", "USED_WITH", "USED_IN"]
885+
}}
886+
}}
887+
### 6. Key Rule
888+
If any item cannot be grouped, it must remain in its own category using its original name. Do not repeat values or create incorrect mappings.
889+
Use these rules to group and name categories accurately without introducing errors or new types.
855890
"""
856891

857892
ADDITIONAL_INSTRUCTIONS = """Your goal is to identify and categorize entities while ensuring that specific data

0 commit comments

Comments
 (0)