From c4850f64dc7c72c0d33ddd6fcb0b44e9a9d35ab0 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:14:42 +0100 Subject: [PATCH 01/31] feat: Implements pipeline structure for retrievers --- cognee/pipelines/__init__.py | 0 cognee/pipelines/retriever/__init__.py | 0 .../retriever/diffusion_retriever.py | 25 ++++++++++++++++++ cognee/pipelines/retriever/g_retriever.py | 25 ++++++++++++++++++ .../retriever/two_steps_retriever.py | 26 +++++++++++++++++++ examples/python/dynamic_steps_example.py | 17 ++++++------ 6 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 cognee/pipelines/__init__.py create mode 100644 cognee/pipelines/retriever/__init__.py create mode 100644 cognee/pipelines/retriever/diffusion_retriever.py create mode 100644 cognee/pipelines/retriever/g_retriever.py create mode 100644 cognee/pipelines/retriever/two_steps_retriever.py diff --git a/cognee/pipelines/__init__.py b/cognee/pipelines/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cognee/pipelines/retriever/__init__.py b/cognee/pipelines/retriever/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cognee/pipelines/retriever/diffusion_retriever.py b/cognee/pipelines/retriever/diffusion_retriever.py new file mode 100644 index 000000000..a6b79310e --- /dev/null +++ b/cognee/pipelines/retriever/diffusion_retriever.py @@ -0,0 +1,25 @@ +from uuid import UUID +from enum import Enum +from typing import Callable, Dict +from cognee.shared.utils import send_telemetry +from cognee.modules.users.models import User +from cognee.modules.users.methods import get_default_user +from cognee.modules.users.permissions.methods import get_document_ids_for_user + +async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: + if user is None: + user = await get_default_user() + + if user is None: + raise PermissionError("No user found in the system. Please create a user.") + + own_document_ids = await get_document_ids_for_user(user.id) + retrieved_results = await diffusion_retriever(query, user) + + filtered_search_results = [] + + + return retrieved_results + +async def diffusion_retriever(query: str, user, community_filter = []) -> list: + raise(NotImplementedError) \ No newline at end of file diff --git a/cognee/pipelines/retriever/g_retriever.py b/cognee/pipelines/retriever/g_retriever.py new file mode 100644 index 000000000..4b319acd9 --- /dev/null +++ b/cognee/pipelines/retriever/g_retriever.py @@ -0,0 +1,25 @@ +from uuid import UUID +from enum import Enum +from typing import Callable, Dict +from cognee.shared.utils import send_telemetry +from cognee.modules.users.models import User +from cognee.modules.users.methods import get_default_user +from cognee.modules.users.permissions.methods import get_document_ids_for_user + +async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: + if user is None: + user = await get_default_user() + + if user is None: + raise PermissionError("No user found in the system. Please create a user.") + + own_document_ids = await get_document_ids_for_user(user.id) + retrieved_results = await g_retriever(query, user) + + filtered_search_results = [] + + + return retrieved_results + +async def g_retriever(query: str, user, community_filter = []) -> list: + raise(NotImplementedError) \ No newline at end of file diff --git a/cognee/pipelines/retriever/two_steps_retriever.py b/cognee/pipelines/retriever/two_steps_retriever.py new file mode 100644 index 000000000..cb0d80133 --- /dev/null +++ b/cognee/pipelines/retriever/two_steps_retriever.py @@ -0,0 +1,26 @@ +from uuid import UUID +from enum import Enum +from typing import Callable, Dict +from cognee.shared.utils import send_telemetry +from cognee.modules.users.models import User +from cognee.modules.users.methods import get_default_user +from cognee.modules.users.permissions.methods import get_document_ids_for_user + +async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: + if user is None: + user = await get_default_user() + + if user is None: + raise PermissionError("No user found in the system. Please create a user.") + + own_document_ids = await get_document_ids_for_user(user.id) + retrieved_results = await run_two_step_retriever(query, user) + + filtered_search_results = [] + + + return retrieved_results + + +async def run_two_step_retriever(query: str, user, community_filter = []) -> list: + raise(NotImplementedError) \ No newline at end of file diff --git a/examples/python/dynamic_steps_example.py b/examples/python/dynamic_steps_example.py index 309aea82c..11c2f1110 100644 --- a/examples/python/dynamic_steps_example.py +++ b/examples/python/dynamic_steps_example.py @@ -1,6 +1,6 @@ import cognee import asyncio -from cognee.api.v1.search import SearchType +from cognee.pipelines.retriever.two_steps_retriever import two_step_retriever job_position = """0:Senior Data Scientist (Machine Learning) @@ -206,9 +206,8 @@ async def main(enable_steps): print("Knowledge graph created.") # Step 4: Query insights - if enable_steps.get("search_insights"): - search_results = await cognee.search( - SearchType.INSIGHTS, + if enable_steps.get("retriever"): + search_results = await two_step_retriever( {'query': 'Which applicant has the most relevant experience in data science?'} ) print("Search results:") @@ -219,11 +218,11 @@ async def main(enable_steps): if __name__ == '__main__': # Flags to enable/disable steps steps_to_enable = { - "prune_data": True, - "prune_system": True, - "add_text": True, - "cognify": True, - "search_insights": True + "prune_data": False, + "prune_system": False, + "add_text": False, + "cognify": False, + "retriever": True } asyncio.run(main(steps_to_enable)) From f2c0fddeb2b39aa521724133317046365735ff96 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:29:52 +0100 Subject: [PATCH 02/31] feat: Adds graph-data-science to neo4j docker image --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 426b178a7..1e13f1924 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: - 7687:7687 environment: - NEO4J_AUTH=neo4j/pleaseletmein - - NEO4J_PLUGINS=["apoc"] + - NEO4J_PLUGINS=["apoc", "graph-data-science"] networks: - cognee-network From 44ac9b68b41f9889cf904e5f7d3c2148c76ca901 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:39:45 +0100 Subject: [PATCH 03/31] feat: adds get_distances from collection method to LanceDB and PgVector --- .../vector/lancedb/LanceDBAdapter.py | 51 ++++++++++------ .../vector/pgvector/PGVectorAdapter.py | 61 +++++++++++++++++++ .../infrastructure/databases/vector/utils.py | 26 ++++++++ 3 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 cognee/infrastructure/databases/vector/utils.py diff --git a/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py b/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py index 96f026b4f..6cbe45655 100644 --- a/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py +++ b/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py @@ -10,6 +10,7 @@ from cognee.infrastructure.files.storage import LocalStorage from cognee.modules.storage.utils import copy_model, get_own_properties from ..models.ScoredResult import ScoredResult from ..vector_db_interface import VectorDBInterface +from ..utils import normalize_distances from ..embeddings.EmbeddingEngine import EmbeddingEngine class IndexSchema(DataPoint): @@ -141,6 +142,34 @@ class LanceDBAdapter(VectorDBInterface): score = 0, ) for result in results.to_dict("index").values()] + async def get_distances_of_collection( + self, + collection_name: str, + query_text: str = None, + query_vector: List[float] = None, + with_vector: bool = False + ): + if query_text is None and query_vector is None: + raise ValueError("One of query_text or query_vector must be provided!") + + if query_text and not query_vector: + query_vector = (await self.embedding_engine.embed_text([query_text]))[0] + + connection = await self.get_connection() + collection = await connection.open_table(collection_name) + + results = await collection.vector_search(query_vector).to_pandas() + + result_values = list(results.to_dict("index").values()) + + normalized_values = normalize_distances(result_values) + + return [ScoredResult( + id=UUID(result["id"]), + payload=result["payload"], + score=normalized_values[value_index], + ) for value_index, result in enumerate(result_values)] + async def search( self, collection_name: str, @@ -148,6 +177,7 @@ class LanceDBAdapter(VectorDBInterface): query_vector: List[float] = None, limit: int = 5, with_vector: bool = False, + normalized: bool = True ): if query_text is None and query_vector is None: raise ValueError("One of query_text or query_vector must be provided!") @@ -162,26 +192,7 @@ class LanceDBAdapter(VectorDBInterface): result_values = list(results.to_dict("index").values()) - min_value = 100 - max_value = 0 - - for result in result_values: - value = float(result["_distance"]) - if value > max_value: - max_value = value - if value < min_value: - min_value = value - - normalized_values = [] - min_value = min(result["_distance"] for result in result_values) - max_value = max(result["_distance"] for result in result_values) - - if max_value == min_value: - # Avoid division by zero: Assign all normalized values to 0 (or any constant value like 1) - normalized_values = [0 for _ in result_values] - else: - normalized_values = [(result["_distance"] - min_value) / (max_value - min_value) for result in - result_values] + normalized_values = normalize_distances(result_values) return [ScoredResult( id = UUID(result["id"]), diff --git a/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py b/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py index 01691714b..97571a274 100644 --- a/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py +++ b/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py @@ -11,6 +11,7 @@ from cognee.infrastructure.engine import DataPoint from .serialize_data import serialize_data from ..models.ScoredResult import ScoredResult from ..vector_db_interface import VectorDBInterface +from ..utils import normalize_distances from ..embeddings.EmbeddingEngine import EmbeddingEngine from ...relational.sqlalchemy.SqlAlchemyAdapter import SQLAlchemyAdapter from ...relational.ModelBase import Base @@ -22,6 +23,19 @@ class IndexSchema(DataPoint): "index_fields": ["text"] } +def singleton(class_): + # Note: Using this singleton as a decorator to a class removes + # the option to use class methods for that class + instances = {} + + def getinstance(*args, **kwargs): + if class_ not in instances: + instances[class_] = class_(*args, **kwargs) + return instances[class_] + + return getinstance + +@singleton class PGVectorAdapter(SQLAlchemyAdapter, VectorDBInterface): def __init__( @@ -162,6 +176,53 @@ class PGVectorAdapter(SQLAlchemyAdapter, VectorDBInterface): ) for result in results ] + async def get_distances_of_collection( + self, + collection_name: str, + query_text: str = None, + query_vector: List[float] = None, + with_vector: bool = False + )-> List[ScoredResult]: + if query_text is None and query_vector is None: + raise ValueError("One of query_text or query_vector must be provided!") + + if query_text and not query_vector: + query_vector = (await self.embedding_engine.embed_text([query_text]))[0] + + # Get PGVectorDataPoint Table from database + PGVectorDataPoint = await self.get_table(collection_name) + + closest_items = [] + + # Use async session to connect to the database + async with self.get_async_session() as session: + # Find closest vectors to query_vector + closest_items = await session.execute( + select( + PGVectorDataPoint, + PGVectorDataPoint.c.vector.cosine_distance(query_vector).label( + "similarity" + ), + ) + .order_by("similarity") + ) + + vector_list = [] + + # Extract distances and find min/max for normalization + for vector in closest_items: + # TODO: Add normalization of similarity score + vector_list.append(vector) + + # Create and return ScoredResult objects + return [ + ScoredResult( + id = UUID(str(row.id)), + payload = row.payload, + score = row.similarity + ) for row in vector_list + ] + async def search( self, collection_name: str, diff --git a/cognee/infrastructure/databases/vector/utils.py b/cognee/infrastructure/databases/vector/utils.py new file mode 100644 index 000000000..ced161ea3 --- /dev/null +++ b/cognee/infrastructure/databases/vector/utils.py @@ -0,0 +1,26 @@ +from typing import List + + +def normalize_distances(result_values: List[dict]) -> List[float]: + min_value = 100 + max_value = 0 + + for result in result_values: + value = float(result["_distance"]) + if value > max_value: + max_value = value + if value < min_value: + min_value = value + + normalized_values = [] + min_value = min(result["_distance"] for result in result_values) + max_value = max(result["_distance"] for result in result_values) + + if max_value == min_value: + # Avoid division by zero: Assign all normalized values to 0 (or any constant value like 1) + normalized_values = [0 for _ in result_values] + else: + normalized_values = [(result["_distance"] - min_value) / (max_value - min_value) for result in + result_values] + + return normalized_values \ No newline at end of file From d9eec77f18932fd0a2b13795aa2bc2e48b7ea13e Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:40:27 +0100 Subject: [PATCH 04/31] feat: Implements first step of the two step retrieval --- .../retriever/two_steps_retriever.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/cognee/pipelines/retriever/two_steps_retriever.py b/cognee/pipelines/retriever/two_steps_retriever.py index cb0d80133..7a630fab3 100644 --- a/cognee/pipelines/retriever/two_steps_retriever.py +++ b/cognee/pipelines/retriever/two_steps_retriever.py @@ -1,3 +1,4 @@ +import asyncio from uuid import UUID from enum import Enum from typing import Callable, Dict @@ -5,6 +6,9 @@ from cognee.shared.utils import send_telemetry from cognee.modules.users.models import User from cognee.modules.users.methods import get_default_user from cognee.modules.users.permissions.methods import get_document_ids_for_user +from cognee.infrastructure.databases.vector import get_vector_engine +from cognee.infrastructure.databases.graph import get_graph_engine + async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: if user is None: @@ -23,4 +27,32 @@ async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: async def run_two_step_retriever(query: str, user, community_filter = []) -> list: + vector_engine = get_vector_engine() + graph_engine = await get_graph_engine() + + collections = ["Entity_name", "TextSummary_text", 'EntityType_name', 'DocumentChunk_text'] + results = await asyncio.gather( + *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] + ) + + ############################################# This part is a quick fix til we don't fix the vector db inconsistency + results_dict = {} + for collection, results in zip(collections, results): + seen_ids = set() + unique_results = [] + for result in results: + if result.id not in seen_ids: + unique_results.append(result) + seen_ids.add(result.id) + else: + print(f"Duplicate found in collection '{collection}': {result.id}") + results_dict[collection] = unique_results + # :TODO: Due to duplicates and inconsistent vector db state now am collecting + # :TODO: the first appearance of the object but this code should be the solution once the db is fixed. + # results_dict = {collection: result for collection, result in zip(collections, results)} + ############################################## + + print() + + raise(NotImplementedError) \ No newline at end of file From c6e447f28c7da33e48aa6fe2eea3ab855ffb20c5 Mon Sep 17 00:00:00 2001 From: hande-k Date: Wed, 20 Nov 2024 08:47:02 +0100 Subject: [PATCH 05/31] docs: add print statements to the simple example, update README --- README.md | 24 +++++++++++++++++------- examples/python/simple_example.py | 21 +++++++++++++++++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 28d5858a0..8dc8c5c66 100644 --- a/README.md +++ b/README.md @@ -105,37 +105,47 @@ import asyncio from cognee.api.v1.search import SearchType async def main(): - # Reset cognee data + # Create a clean slate for cognee -- reset data and system state + print("Resetting cognee data...") await cognee.prune.prune_data() - # Reset cognee system state await cognee.prune.prune_system(metadata=True) + print("Data reset complete.\n") + # cognee knowledge graph will be created based on this text text = """ Natural language processing (NLP) is an interdisciplinary subfield of computer science and information retrieval. """ - - # Add text to cognee + print("Adding text to cognee:") + print(text.strip()) await cognee.add(text) + print("Text added successfully.\n") # Use LLMs and cognee to create knowledge graph + print("Running cognify to create knowledge graph...") await cognee.cognify() + print("Cognify process complete.\n") - # Search cognee for insights + # Query cognee for insights on the added text + query_text = 'Tell me about NLP' + print(f"Searching cognee for insights with query: '{query_text}'") search_results = await cognee.search( SearchType.INSIGHTS, - "Tell me about NLP", + query_text=query_text, ) - # Display results + # Display search results + print("Search results:") for result_text in search_results: print(result_text) + # Expected output: # natural_language_processing is_a field # natural_language_processing is_subfield_of computer_science # natural_language_processing is_subfield_of information_retrieval asyncio.run(main()) ``` +When you run this script, you will see step-by-step messages in the console that help you trace the execution flow and understand what the script is doing at each stage. A version of this example is here: `examples/python/simple_example.py` ### Create your own memory store diff --git a/examples/python/simple_example.py b/examples/python/simple_example.py index 47940ca6e..e0b212746 100644 --- a/examples/python/simple_example.py +++ b/examples/python/simple_example.py @@ -11,29 +11,42 @@ from cognee.api.v1.search import SearchType async def main(): # Create a clean slate for cognee -- reset data and system state + print("Resetting cognee data...") await cognee.prune.prune_data() await cognee.prune.prune_system(metadata=True) + print("Data reset complete.\n") # cognee knowledge graph will be created based on this text text = """ Natural language processing (NLP) is an interdisciplinary subfield of computer science and information retrieval. """ - + + print("Adding text to cognee:") + print(text.strip()) # Add the text, and make it available for cognify await cognee.add(text) + print("Text added successfully.\n") + + print("Running cognify to create knowledge graph...") # Use LLMs and cognee to create knowledge graph await cognee.cognify() + print("Cognify process complete.\n") + + query_text = 'Tell me about NLP' + print(f"Searching cognee for insights with query: '{query_text}'") # Query cognee for insights on the added text search_results = await cognee.search( - SearchType.INSIGHTS, query_text='Tell me about NLP' + SearchType.INSIGHTS, query_text=query_text ) - - # Display search results + + print("Search results:") + # Display results for result_text in search_results: print(result_text) + if __name__ == '__main__': asyncio.run(main()) From 57783a979a021b676081d8017eecc8d2945dbf3a Mon Sep 17 00:00:00 2001 From: Igor Ilic Date: Wed, 20 Nov 2024 14:03:14 +0100 Subject: [PATCH 06/31] feat: Add support for multiple audio and image formats Added support for multiple audio and image formats with example Feature COG-507 --- .data/multimedia/example.png | Bin 0 -> 10784 bytes .data/multimedia/text_to_speech.mp3 | Bin 0 -> 28173 bytes cognee/tasks/documents/classify_documents.py | 47 +++++++++++++++--- examples/python/multimedia_example.py | 48 +++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 .data/multimedia/example.png create mode 100644 .data/multimedia/text_to_speech.mp3 create mode 100644 examples/python/multimedia_example.py diff --git a/.data/multimedia/example.png b/.data/multimedia/example.png new file mode 100644 index 0000000000000000000000000000000000000000..4d406cafdeb7e6d778ff3fa37d99d42088673bf9 GIT binary patch literal 10784 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 Z|FRx list[Document]: documents = [ - EXTENSION_TO_DOCUMENT_CLASS[data_item.extension](id = data_item.id, title=f"{data_item.name}.{data_item.extension}", raw_data_location=data_item.raw_data_location, name=data_item.name) + EXTENSION_TO_DOCUMENT_CLASS[data_item.extension]( + id=data_item.id, + title=f"{data_item.name}.{data_item.extension}", + raw_data_location=data_item.raw_data_location, + name=data_item.name, + ) for data_item in data_documents ] return documents diff --git a/examples/python/multimedia_example.py b/examples/python/multimedia_example.py new file mode 100644 index 000000000..6c8bc5995 --- /dev/null +++ b/examples/python/multimedia_example.py @@ -0,0 +1,48 @@ +import os +import asyncio +import pathlib + +import cognee +from cognee.api.v1.search import SearchType + +# Prerequisites: +# 1. Copy `.env.template` and rename it to `.env`. +# 2. Add your OpenAI API key to the `.env` file in the `LLM_API_KEY` field: +# LLM_API_KEY = "your_key_here" + + +async def main(): + # Create a clean slate for cognee -- reset data and system state + await cognee.prune.prune_data() + await cognee.prune.prune_system(metadata=True) + + # cognee knowledge graph will be created based on the text + # and description of these files + mp3_file_path = os.path.join( + pathlib.Path(__file__).parent.parent.parent, + ".data/multimedia/text_to_speech.mp3", + ) + png_file_path = os.path.join( + pathlib.Path(__file__).parent.parent.parent, + ".data/multimedia/example.png", + ) + + # Add the files, and make it available for cognify + await cognee.add([mp3_file_path, png_file_path]) + + # Use LLMs and cognee to create knowledge graph + await cognee.cognify() + + # Query cognee for summaries of the data in the multimedia files + search_results = await cognee.search( + SearchType.SUMMARIES, + query_text="What is in the multimedia files?", + ) + + # Display search results + for result_text in search_results: + print(result_text) + + +if __name__ == "__main__": + asyncio.run(main()) From 0101d43b8de29179b5b92a6bfa319af8a144b645 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:13:38 +0100 Subject: [PATCH 07/31] feat: Adds graph node filtering by feature --- .../databases/graph/neo4j_driver/adapter.py | 48 ++++++++++++++++++- .../databases/graph/networkx/adapter.py | 38 ++++++++++++++- .../modules/graph/cognee_graph/CogneeGraph.py | 8 +++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/cognee/infrastructure/databases/graph/neo4j_driver/adapter.py b/cognee/infrastructure/databases/graph/neo4j_driver/adapter.py index 1121a24d5..e6520e4e2 100644 --- a/cognee/infrastructure/databases/graph/neo4j_driver/adapter.py +++ b/cognee/infrastructure/databases/graph/neo4j_driver/adapter.py @@ -2,7 +2,7 @@ import logging import asyncio from textwrap import dedent -from typing import Optional, Any, List, Dict +from typing import Optional, Any, List, Dict, Union from contextlib import asynccontextmanager from uuid import UUID from neo4j import AsyncSession @@ -432,3 +432,49 @@ class Neo4jAdapter(GraphDBInterface): ) for record in result] return (nodes, edges) + + async def get_filtered_graph_data(self, attribute_filters): + """ + Fetches nodes and relationships filtered by specified attribute values. + + Args: + attribute_filters (list of dict): A list of dictionaries where keys are attributes and values are lists of values to filter on. + Example: [{"community": ["1", "2"]}] + + Returns: + tuple: A tuple containing two lists: nodes and edges. + """ + where_clauses = [] + for attribute, values in attribute_filters[0].items(): + values_str = ", ".join(f"'{value}'" if isinstance(value, str) else str(value) for value in values) + where_clauses.append(f"n.{attribute} IN [{values_str}]") + + where_clause = " AND ".join(where_clauses) + + query_nodes = f""" + MATCH (n) + WHERE {where_clause} + RETURN ID(n) AS id, labels(n) AS labels, properties(n) AS properties + """ + result_nodes = await self.query(query_nodes) + + nodes = [( + record["id"], + record["properties"], + ) for record in result_nodes] + + query_edges = f""" + MATCH (n)-[r]->(m) + WHERE {where_clause} AND {where_clause.replace('n.', 'm.')} + RETURN ID(n) AS source, ID(m) AS target, TYPE(r) AS type, properties(r) AS properties + """ + result_edges = await self.query(query_edges) + + edges = [( + record["source"], + record["target"], + record["type"], + record["properties"], + ) for record in result_edges] + + return (nodes, edges) \ No newline at end of file diff --git a/cognee/infrastructure/databases/graph/networkx/adapter.py b/cognee/infrastructure/databases/graph/networkx/adapter.py index a72376082..d249b6336 100644 --- a/cognee/infrastructure/databases/graph/networkx/adapter.py +++ b/cognee/infrastructure/databases/graph/networkx/adapter.py @@ -6,7 +6,7 @@ import json import asyncio import logging from re import A -from typing import Dict, Any, List +from typing import Dict, Any, List, Union from uuid import UUID import aiofiles import aiofiles.os as aiofiles_os @@ -301,3 +301,39 @@ class NetworkXAdapter(GraphDBInterface): logger.info("Graph deleted successfully.") except Exception as error: logger.error("Failed to delete graph: %s", error) + + async def get_filtered_graph_data(self, attribute_filters: List[Dict[str, List[Union[str, int]]]]): + """ + Fetches nodes and relationships filtered by specified attribute values. + + Args: + attribute_filters (list of dict): A list of dictionaries where keys are attributes and values are lists of values to filter on. + Example: [{"community": ["1", "2"]}] + + Returns: + tuple: A tuple containing two lists: + - Nodes: List of tuples (node_id, node_properties). + - Edges: List of tuples (source_id, target_id, relationship_type, edge_properties). + """ + # Create filters for nodes based on the attribute filters + where_clauses = [] + for attribute, values in attribute_filters[0].items(): + where_clauses.append((attribute, values)) + + # Filter nodes + filtered_nodes = [ + (node, data) for node, data in self.graph.nodes(data=True) + if all(data.get(attr) in values for attr, values in where_clauses) + ] + + # Filter edges where both source and target nodes satisfy the filters + filtered_edges = [ + (source, target, data.get('relationship_type', 'UNKNOWN'), data) + for source, target, data in self.graph.edges(data=True) + if ( + all(self.graph.nodes[source].get(attr) in values for attr, values in where_clauses) and + all(self.graph.nodes[target].get(attr) in values for attr, values in where_clauses) + ) + ] + + return filtered_nodes, filtered_edges \ No newline at end of file diff --git a/cognee/modules/graph/cognee_graph/CogneeGraph.py b/cognee/modules/graph/cognee_graph/CogneeGraph.py index d15d93b73..0b752c6cb 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraph.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraph.py @@ -52,13 +52,17 @@ class CogneeGraph(CogneeAbstractGraph): edge_properties_to_project: List[str], directed = True, node_dimension = 1, - edge_dimension = 1) -> None: + edge_dimension = 1, + memory_fragment_filter = List[Dict[str, List[Union[str, int]]]]) -> None: if node_dimension < 1 or edge_dimension < 1: raise ValueError("Dimensions must be positive integers") try: - nodes_data, edges_data = await adapter.get_graph_data() + if len(memory_fragment_filter) == 0: + nodes_data, edges_data = await adapter.get_graph_data() + else: + nodes_data, edges_data = await adapter.get_filtered_graph_data(attribute_filters = memory_fragment_filter) if not nodes_data: raise ValueError("No node data retrieved from the database.") From 9f557b0c5bd4b1aa5c11f4af5af3c51b759df3d1 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:14:36 +0100 Subject: [PATCH 08/31] feat: Extends two steps retriever with graph projection --- .../retriever/two_steps_retriever.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/cognee/pipelines/retriever/two_steps_retriever.py b/cognee/pipelines/retriever/two_steps_retriever.py index 7a630fab3..c681f3e99 100644 --- a/cognee/pipelines/retriever/two_steps_retriever.py +++ b/cognee/pipelines/retriever/two_steps_retriever.py @@ -6,8 +6,11 @@ from cognee.shared.utils import send_telemetry from cognee.modules.users.models import User from cognee.modules.users.methods import get_default_user from cognee.modules.users.permissions.methods import get_document_ids_for_user +from cognee.modules.graph.cognee_graph.CogneeGraph import CogneeGraph from cognee.infrastructure.databases.vector import get_vector_engine from cognee.infrastructure.databases.graph import get_graph_engine +from openai import organization +from sympy.codegen.fnodes import dimension async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: @@ -26,16 +29,7 @@ async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: return retrieved_results -async def run_two_step_retriever(query: str, user, community_filter = []) -> list: - vector_engine = get_vector_engine() - graph_engine = await get_graph_engine() - - collections = ["Entity_name", "TextSummary_text", 'EntityType_name', 'DocumentChunk_text'] - results = await asyncio.gather( - *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] - ) - - ############################################# This part is a quick fix til we don't fix the vector db inconsistency +def delete_duplicated_vector_db_elements(collections, results): #:TODO: This is just for now to fix vector db duplicates results_dict = {} for collection, results in zip(collections, results): seen_ids = set() @@ -47,11 +41,36 @@ async def run_two_step_retriever(query: str, user, community_filter = []) -> lis else: print(f"Duplicate found in collection '{collection}': {result.id}") results_dict[collection] = unique_results - # :TODO: Due to duplicates and inconsistent vector db state now am collecting - # :TODO: the first appearance of the object but this code should be the solution once the db is fixed. + + return results_dict + + +async def run_two_step_retriever(query: str, user, community_filter = []) -> list: + vector_engine = get_vector_engine() + graph_engine = await get_graph_engine() + + collections = ["Entity_name", "TextSummary_text", 'EntityType_name', 'DocumentChunk_text'] + results = await asyncio.gather( + *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] + ) + + ############################################# This part is a quick fix til we don't fix the vector db inconsistency + results_dict = delete_duplicated_vector_db_elements(collections, results)# :TODO: Change when vector db is fixed # results_dict = {collection: result for collection, result in zip(collections, results)} ############################################## + memory_fragment = CogneeGraph() + + await memory_fragment.project_graph_from_db(graph_engine, + node_properties_to_project=['id', + 'community'], + edge_properties_to_project=['id', + 'relationship_name'], + directed=True, + node_dimension=1, + edge_dimension=1, + memory_fragment_filter=[]) + print() From 61ed516d120b9082c3b8736b9749b5dbdc0bb101 Mon Sep 17 00:00:00 2001 From: Igor Ilic Date: Wed, 20 Nov 2024 16:21:29 +0100 Subject: [PATCH 09/31] docs: Add multimedia notebook Added multimedia notebook for cognee Docs COG-507 --- notebooks/cognee_demo.ipynb | 40 +++--- notebooks/cognee_llama_index.ipynb | 25 ++-- notebooks/cognee_multimedia_demo.ipynb | 169 +++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 31 deletions(-) create mode 100644 notebooks/cognee_multimedia_demo.ipynb diff --git a/notebooks/cognee_demo.ipynb b/notebooks/cognee_demo.ipynb index 45f5a618c..33ea91a35 100644 --- a/notebooks/cognee_demo.ipynb +++ b/notebooks/cognee_demo.ipynb @@ -265,7 +265,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "df16431d0f48b006", "metadata": { "ExecuteTime": { @@ -304,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "9086abf3af077ab4", "metadata": { "ExecuteTime": { @@ -349,7 +349,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "a9de0cc07f798b7f", "metadata": { "ExecuteTime": { @@ -393,7 +393,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "185ff1c102d06111", "metadata": { "ExecuteTime": { @@ -437,7 +437,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "d55ce4c58f8efb67", "metadata": { "ExecuteTime": { @@ -479,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "ca4ecc32721ad332", "metadata": { "ExecuteTime": { @@ -529,14 +529,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "bce39dc6", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", - "# # Setting environment variables\n", + "# Setting environment variables\n", "if \"GRAPHISTRY_USERNAME\" not in os.environ: \n", " os.environ[\"GRAPHISTRY_USERNAME\"] = \"\"\n", "\n", @@ -546,24 +546,26 @@ "if \"LLM_API_KEY\" not in os.environ:\n", " os.environ[\"LLM_API_KEY\"] = \"\"\n", "\n", - "os.environ[\"GRAPH_DATABASE_PROVIDER\"]=\"networkx\" # \"neo4j\" or \"networkx\"\n", + "# \"neo4j\" or \"networkx\"\n", + "os.environ[\"GRAPH_DATABASE_PROVIDER\"]=\"networkx\" \n", "# Not needed if using networkx\n", - "#GRAPH_DATABASE_URL=\"\"\n", - "#GRAPH_DATABASE_USERNAME=\"\"\n", - "#GRAPH_DATABASE_PASSWORD=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_URL\"]=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_USERNAME\"]=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_PASSWORD\"]=\"\"\n", "\n", - "os.environ[\"VECTOR_DB_PROVIDER\"]=\"lancedb\" # \"qdrant\", \"weaviate\" or \"lancedb\"\n", - "# Not needed if using \"lancedb\"\n", + "# \"pgvector\", \"qdrant\", \"weaviate\" or \"lancedb\"\n", + "os.environ[\"VECTOR_DB_PROVIDER\"]=\"lancedb\" \n", + "# Not needed if using \"lancedb\" or \"pgvector\"\n", "# os.environ[\"VECTOR_DB_URL\"]=\"\"\n", "# os.environ[\"VECTOR_DB_KEY\"]=\"\"\n", "\n", - "# Database provider\n", - "os.environ[\"DB_PROVIDER\"]=\"sqlite\" # or \"postgres\"\n", + "# Relational Database provider \"sqlite\" or \"postgres\"\n", + "os.environ[\"DB_PROVIDER\"]=\"sqlite\"\n", "\n", "# Database name\n", "os.environ[\"DB_NAME\"]=\"cognee_db\"\n", "\n", - "# Postgres specific parameters (Only if Postgres is run)\n", + "# Postgres specific parameters (Only if Postgres or PGVector is used)\n", "# os.environ[\"DB_HOST\"]=\"127.0.0.1\"\n", "# os.environ[\"DB_PORT\"]=\"5432\"\n", "# os.environ[\"DB_USERNAME\"]=\"cognee\"\n", @@ -620,7 +622,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "7c431fdef4921ae0", "metadata": { "ExecuteTime": { @@ -881,7 +883,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.9.6" } }, "nbformat": 4, diff --git a/notebooks/cognee_llama_index.ipynb b/notebooks/cognee_llama_index.ipynb index 742c2f51c..ec899aaea 100644 --- a/notebooks/cognee_llama_index.ipynb +++ b/notebooks/cognee_llama_index.ipynb @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -71,7 +71,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -90,23 +90,23 @@ "# \"neo4j\" or \"networkx\"\n", "os.environ[\"GRAPH_DATABASE_PROVIDER\"]=\"networkx\" \n", "# Not needed if using networkx\n", - "#GRAPH_DATABASE_URL=\"\"\n", - "#GRAPH_DATABASE_USERNAME=\"\"\n", - "#GRAPH_DATABASE_PASSWORD=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_URL\"]=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_USERNAME\"]=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_PASSWORD\"]=\"\"\n", "\n", - "# \"qdrant\", \"weaviate\" or \"lancedb\"\n", + "# \"pgvector\", \"qdrant\", \"weaviate\" or \"lancedb\"\n", "os.environ[\"VECTOR_DB_PROVIDER\"]=\"lancedb\" \n", - "# Not needed if using \"lancedb\"\n", + "# Not needed if using \"lancedb\" or \"pgvector\"\n", "# os.environ[\"VECTOR_DB_URL\"]=\"\"\n", "# os.environ[\"VECTOR_DB_KEY\"]=\"\"\n", "\n", - "# Database provider\n", - "os.environ[\"DB_PROVIDER\"]=\"sqlite\" # or \"postgres\"\n", + "# Relational Database provider \"sqlite\" or \"postgres\"\n", + "os.environ[\"DB_PROVIDER\"]=\"sqlite\"\n", "\n", "# Database name\n", "os.environ[\"DB_NAME\"]=\"cognee_db\"\n", "\n", - "# Postgres specific parameters (Only if Postgres is run)\n", + "# Postgres specific parameters (Only if Postgres or PGVector is used)\n", "# os.environ[\"DB_HOST\"]=\"127.0.0.1\"\n", "# os.environ[\"DB_PORT\"]=\"5432\"\n", "# os.environ[\"DB_USERNAME\"]=\"cognee\"\n", @@ -130,8 +130,6 @@ "\n", "from cognee.infrastructure.databases.vector.pgvector import create_db_and_tables as create_pgvector_db_and_tables\n", "from cognee.infrastructure.databases.relational import create_db_and_tables as create_relational_db_and_tables\n", - "from cognee.infrastructure.databases.graph import get_graph_engine\n", - "from cognee.shared.utils import render_graph\n", "from cognee.modules.users.models import User\n", "from cognee.modules.users.methods import get_default_user\n", "from cognee.tasks.ingestion.ingest_data_with_metadata import ingest_data_with_metadata\n", @@ -196,6 +194,9 @@ "source": [ "import graphistry\n", "\n", + "from cognee.infrastructure.databases.graph import get_graph_engine\n", + "from cognee.shared.utils import render_graph\n", + "\n", "# Get graph\n", "graphistry.login(username=os.getenv(\"GRAPHISTRY_USERNAME\"), password=os.getenv(\"GRAPHISTRY_PASSWORD\"))\n", "graph_engine = await get_graph_engine()\n", diff --git a/notebooks/cognee_multimedia_demo.ipynb b/notebooks/cognee_multimedia_demo.ipynb new file mode 100644 index 000000000..2d35132f6 --- /dev/null +++ b/notebooks/cognee_multimedia_demo.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cognee GraphRAG with Multimedia files" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Load Data\n", + "\n", + "We will use a few sample multimedia files which we have on GitHub for easy access." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pathlib\n", + "\n", + "# cognee knowledge graph will be created based on the text\n", + "# and description of these files\n", + "mp3_file_path = os.path.join(\n", + " os.path.abspath(''), \"../\",\n", + " \".data/multimedia/text_to_speech.mp3\",\n", + ")\n", + "png_file_path = os.path.join(\n", + " os.path.abspath(''), \"../\",\n", + " \".data/multimedia/example.png\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set environment variables" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Setting environment variables\n", + "if \"GRAPHISTRY_USERNAME\" not in os.environ: \n", + " os.environ[\"GRAPHISTRY_USERNAME\"] = \"\"\n", + "\n", + "if \"GRAPHISTRY_PASSWORD\" not in os.environ: \n", + " os.environ[\"GRAPHISTRY_PASSWORD\"] = \"\"\n", + "\n", + "if \"LLM_API_KEY\" not in os.environ:\n", + " os.environ[\"LLM_API_KEY\"] = \"\"\n", + "\n", + "# \"neo4j\" or \"networkx\"\n", + "os.environ[\"GRAPH_DATABASE_PROVIDER\"]=\"networkx\" \n", + "# Not needed if using networkx\n", + "#os.environ[\"GRAPH_DATABASE_URL\"]=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_USERNAME\"]=\"\"\n", + "#os.environ[\"GRAPH_DATABASE_PASSWORD\"]=\"\"\n", + "\n", + "# \"pgvector\", \"qdrant\", \"weaviate\" or \"lancedb\"\n", + "os.environ[\"VECTOR_DB_PROVIDER\"]=\"lancedb\" \n", + "# Not needed if using \"lancedb\" or \"pgvector\"\n", + "# os.environ[\"VECTOR_DB_URL\"]=\"\"\n", + "# os.environ[\"VECTOR_DB_KEY\"]=\"\"\n", + "\n", + "# Relational Database provider \"sqlite\" or \"postgres\"\n", + "os.environ[\"DB_PROVIDER\"]=\"sqlite\"\n", + "\n", + "# Database name\n", + "os.environ[\"DB_NAME\"]=\"cognee_db\"\n", + "\n", + "# Postgres specific parameters (Only if Postgres or PGVector is used)\n", + "# os.environ[\"DB_HOST\"]=\"127.0.0.1\"\n", + "# os.environ[\"DB_PORT\"]=\"5432\"\n", + "# os.environ[\"DB_USERNAME\"]=\"cognee\"\n", + "# os.environ[\"DB_PASSWORD\"]=\"cognee\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Cognee with multimedia files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import cognee\n", + "\n", + "# Create a clean slate for cognee -- reset data and system state\n", + "await cognee.prune.prune_data()\n", + "await cognee.prune.prune_system(metadata=True)\n", + "\n", + "# Add multimedia files and make them available for cognify\n", + "await cognee.add([mp3_file_path, png_file_path])\n", + "\n", + "# Create knowledge graph with cognee\n", + "await cognee.cognify()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Query Cognee for summaries related to multimedia files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from cognee.api.v1.search import SearchType\n", + "\n", + "# Query cognee for summaries of the data in the multimedia files\n", + "search_results = await cognee.search(\n", + " SearchType.SUMMARIES,\n", + " query_text=\"What is in the multimedia files?\",\n", + ")\n", + "\n", + "# Display search results\n", + "for result_text in search_results:\n", + " print(result_text)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 38ef3d465f411d0d5e9b948879845b0ee5ddab88 Mon Sep 17 00:00:00 2001 From: Igor Ilic Date: Wed, 20 Nov 2024 16:25:23 +0100 Subject: [PATCH 10/31] test: Add github action for multimedia notebook Added github action for multimedia notebook Test COG-507 --- .../test_cognee_multimedia_notebook.yml | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/test_cognee_multimedia_notebook.yml diff --git a/.github/workflows/test_cognee_multimedia_notebook.yml b/.github/workflows/test_cognee_multimedia_notebook.yml new file mode 100644 index 000000000..dd14fa5e6 --- /dev/null +++ b/.github/workflows/test_cognee_multimedia_notebook.yml @@ -0,0 +1,63 @@ +name: test | multimedia notebook + +on: + workflow_dispatch: + pull_request: + branches: + - main + types: [labeled, synchronize] + + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + RUNTIME__LOG_LEVEL: ERROR + +jobs: + get_docs_changes: + name: docs changes + uses: ./.github/workflows/get_docs_changes.yml + + run_notebook_test: + name: test + needs: get_docs_changes + if: needs.get_docs_changes.outputs.changes_outside_docs == 'true' && ${{ github.event.label.name == 'run-checks' }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Check out + uses: actions/checkout@master + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11.x' + + - name: Install Poetry + uses: snok/install-poetry@v1.3.2 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + - name: Install dependencies + run: | + poetry install --no-interaction + poetry add jupyter --no-interaction + + - name: Execute Jupyter Notebook + env: + ENV: 'dev' + LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GRAPHISTRY_USERNAME: ${{ secrets.GRAPHISTRY_USERNAME }} + GRAPHISTRY_PASSWORD: ${{ secrets.GRAPHISTRY_PASSWORD }} + run: | + poetry run jupyter nbconvert \ + --to notebook \ + --execute notebooks/cognee_multimedia_demo.ipynb \ + --output executed_notebook.ipynb \ + --ExecutePreprocessor.timeout=1200 \ No newline at end of file From 980ae2b22c02c59875f9ff71c931677e4e7b2d78 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:32:03 +0100 Subject: [PATCH 11/31] feat: Adds in time edge vector similarity calculation and triplet importances --- .../modules/graph/cognee_graph/CogneeGraph.py | 92 ++++++++++++++++++- .../graph/cognee_graph/CogneeGraphElements.py | 16 +++- .../unit/modules/graph/cognee_graph_test.py | 4 +- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/cognee/modules/graph/cognee_graph/CogneeGraph.py b/cognee/modules/graph/cognee_graph/CogneeGraph.py index 0b752c6cb..158fb9d07 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraph.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraph.py @@ -1,9 +1,12 @@ -from typing import List, Dict, Union +import numpy as np +from typing import List, Dict, Union from cognee.infrastructure.databases.graph.graph_db_interface import GraphDBInterface from cognee.modules.graph.cognee_graph.CogneeGraphElements import Node, Edge from cognee.modules.graph.cognee_graph.CogneeAbstractGraph import CogneeAbstractGraph -from cognee.infrastructure.databases.graph import get_graph_engine +import heapq +from graphistry import edges + class CogneeGraph(CogneeAbstractGraph): """ @@ -39,13 +42,16 @@ class CogneeGraph(CogneeAbstractGraph): def get_node(self, node_id: str) -> Node: return self.nodes.get(node_id, None) - def get_edges(self, node_id: str) -> List[Edge]: + def get_edges_of_node(self, node_id: str) -> List[Edge]: node = self.get_node(node_id) if node: return node.skeleton_edges else: raise ValueError(f"Node with id {node_id} does not exist.") + def get_edges(self)-> List[Edge]: + return edges + async def project_graph_from_db(self, adapter: Union[GraphDBInterface], node_properties_to_project: List[str], @@ -53,7 +59,7 @@ class CogneeGraph(CogneeAbstractGraph): directed = True, node_dimension = 1, edge_dimension = 1, - memory_fragment_filter = List[Dict[str, List[Union[str, int]]]]) -> None: + memory_fragment_filter = []) -> None: if node_dimension < 1 or edge_dimension < 1: raise ValueError("Dimensions must be positive integers") @@ -93,3 +99,81 @@ class CogneeGraph(CogneeAbstractGraph): print(f"Error projecting graph: {e}") except Exception as ex: print(f"Unexpected error: {ex}") + + async def map_vector_distances_to_graph_nodes(self, node_distances) -> None: + for category, scored_results in node_distances.items(): + for scored_result in scored_results: + node_id = str(scored_result.id) + score = scored_result.score + node =self.get_node(node_id) + if node: + node.add_attribute("vector_distance", score) + else: + print(f"Node with id {node_id} not found in the graph.") + + async def map_vector_distances_to_graph_edges(self, vector_engine, query) -> None: # :TODO: When we calculate edge embeddings in vector db change this similarly to node mapping + try: + # Step 1: Generate the query embedding + query_vector = await vector_engine.embed_data([query]) + query_vector = query_vector[0] + if query_vector is None or len(query_vector) == 0: + raise ValueError("Failed to generate query embedding.") + + # Step 2: Collect all unique relationship types + unique_relationship_types = set() + for edge in self.edges: + relationship_type = edge.attributes.get('relationship_type') + if relationship_type: + unique_relationship_types.add(relationship_type) + + # Step 3: Embed all unique relationship types + unique_relationship_types = list(unique_relationship_types) + relationship_type_embeddings = await vector_engine.embed_data(unique_relationship_types) + + # Step 4: Map relationship types to their embeddings and calculate distances + embedding_map = {} + for relationship_type, embedding in zip(unique_relationship_types, relationship_type_embeddings): + edge_vector = np.array(embedding) + + # Calculate cosine similarity + similarity = np.dot(query_vector, edge_vector) / ( + np.linalg.norm(query_vector) * np.linalg.norm(edge_vector) + ) + distance = 1 - similarity + + # Round the distance to 4 decimal places and store it + embedding_map[relationship_type] = round(distance, 4) + + # Step 4: Assign precomputed distances to edges + for edge in self.edges: + relationship_type = edge.attributes.get('relationship_type') + if not relationship_type or relationship_type not in embedding_map: + print(f"Edge {edge} has an unknown or missing relationship type.") + continue + + # Assign the precomputed distance + edge.attributes["vector_distance"] = embedding_map[relationship_type] + + except Exception as ex: + print(f"Error mapping vector distances to edges: {ex}") + + + async def calculate_top_triplet_importances(self, k = int) -> List: + min_heap = [] + for i, edge in enumerate(self.edges): + source_node = self.get_node(edge.node1.id) + target_node = self.get_node(edge.node2.id) + + source_distance = source_node.attributes.get("vector_distance", 0) if source_node else 0 + target_distance = target_node.attributes.get("vector_distance", 0) if target_node else 0 + edge_distance = edge.attributes.get("vector_distance", 0) + + total_distance = source_distance + target_distance + edge_distance + + heapq.heappush(min_heap, (-total_distance, i, edge)) + if len(min_heap) > k: + heapq.heappop(min_heap) + + + return [edge for _, _, edge in sorted(min_heap)] + diff --git a/cognee/modules/graph/cognee_graph/CogneeGraphElements.py b/cognee/modules/graph/cognee_graph/CogneeGraphElements.py index 8235cb24d..cecb0a272 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraphElements.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraphElements.py @@ -1,5 +1,5 @@ import numpy as np -from typing import List, Dict, Optional, Any +from typing import List, Dict, Optional, Any, Union class Node: """ @@ -21,6 +21,7 @@ class Node: raise ValueError("Dimension must be a positive integer") self.id = node_id self.attributes = attributes if attributes is not None else {} + self.attributes["vector_distance"] = float('inf') self.skeleton_neighbours = [] self.skeleton_edges = [] self.status = np.ones(dimension, dtype=int) @@ -55,6 +56,12 @@ class Node: raise ValueError(f"Dimension {dimension} is out of range. Valid range is 0 to {len(self.status) - 1}.") return self.status[dimension] == 1 + def add_attribute(self, key: str, value: Any) -> None: + self.attributes[key] = value + + def get_attribute(self, key: str) -> Union[str, int, float]: + return self.attributes[key] + def __repr__(self) -> str: return f"Node({self.id}, attributes={self.attributes})" @@ -87,6 +94,7 @@ class Edge: self.node1 = node1 self.node2 = node2 self.attributes = attributes if attributes is not None else {} + self.attributes["vector_distance"] = float('inf') self.directed = directed self.status = np.ones(dimension, dtype=int) @@ -95,6 +103,12 @@ class Edge: raise ValueError(f"Dimension {dimension} is out of range. Valid range is 0 to {len(self.status) - 1}.") return self.status[dimension] == 1 + def add_attribute(self, key: str, value: Any) -> None: + self.attributes[key] = value + + def get_attribute(self, key: str, value: Any) -> Union[str, int, float]: + return self.attributes[key] + def __repr__(self) -> str: direction = "->" if self.directed else "--" return f"Edge({self.node1.id} {direction} {self.node2.id}, attributes={self.attributes})" diff --git a/cognee/tests/unit/modules/graph/cognee_graph_test.py b/cognee/tests/unit/modules/graph/cognee_graph_test.py index d05292d75..bad474023 100644 --- a/cognee/tests/unit/modules/graph/cognee_graph_test.py +++ b/cognee/tests/unit/modules/graph/cognee_graph_test.py @@ -77,11 +77,11 @@ def test_get_edges_success(setup_graph): graph.add_node(node2) edge = Edge(node1, node2) graph.add_edge(edge) - assert edge in graph.get_edges("node1") + assert edge in graph.get_edges_of_node("node1") def test_get_edges_nonexistent_node(setup_graph): """Test retrieving edges for a nonexistent node raises an exception.""" graph = setup_graph with pytest.raises(ValueError, match="Node with id nonexistent does not exist."): - graph.get_edges("nonexistent") + graph.get_edges_of_node("nonexistent") From a114d68aeffd63160723c151b2c331664fd23b34 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:33:34 +0100 Subject: [PATCH 12/31] feat: Implements basic global triplet optimizing retrieval --- .../retriever/two_steps_retriever.py | 63 ++++++++++++++++--- examples/python/dynamic_steps_example.py | 48 +++----------- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/cognee/pipelines/retriever/two_steps_retriever.py b/cognee/pipelines/retriever/two_steps_retriever.py index c681f3e99..ff35a0864 100644 --- a/cognee/pipelines/retriever/two_steps_retriever.py +++ b/cognee/pipelines/retriever/two_steps_retriever.py @@ -13,6 +13,46 @@ from openai import organization from sympy.codegen.fnodes import dimension +def format_triplets(edges): + def filter_attributes(obj, attributes): + """Helper function to filter out non-None properties, including nested dicts.""" + print("\n\n\n") + result = {} + for attr in attributes: + value = getattr(obj, attr, None) + if value is not None: + # If the value is a dict, extract relevant keys from it + if isinstance(value, dict): + nested_values = {k: v for k, v in value.items() if k in attributes and v is not None} + result[attr] = nested_values + else: + result[attr] = value + return result + + triplets = [] + for edge in edges: + node1 = edge.node1 + node2 = edge.node2 + edge_attributes = edge.attributes + node1_attributes = node1.attributes + node2_attributes = node2.attributes + + # Filter only non-None properties + node1_info = {key: value for key, value in node1_attributes.items() if value is not None} + node2_info = {key: value for key, value in node2_attributes.items() if value is not None} + edge_info = {key: value for key, value in edge_attributes.items() if value is not None} + + # Create the formatted triplet + triplet = ( + f"Node1: {node1_info}\n" + f"Edge: {edge_info}\n" + f"Node2: {node2_info}\n\n\n" # Add three blank lines for separation + ) + triplets.append(triplet) + + return "".join(triplets) + + async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: if user is None: user = await get_default_user() @@ -25,7 +65,6 @@ async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: filtered_search_results = [] - return retrieved_results @@ -55,7 +94,7 @@ async def run_two_step_retriever(query: str, user, community_filter = []) -> lis ) ############################################# This part is a quick fix til we don't fix the vector db inconsistency - results_dict = delete_duplicated_vector_db_elements(collections, results)# :TODO: Change when vector db is fixed + node_distances = delete_duplicated_vector_db_elements(collections, results)# :TODO: Change when vector db is fixed # results_dict = {collection: result for collection, result in zip(collections, results)} ############################################## @@ -63,15 +102,19 @@ async def run_two_step_retriever(query: str, user, community_filter = []) -> lis await memory_fragment.project_graph_from_db(graph_engine, node_properties_to_project=['id', - 'community'], + 'description', + 'name', + 'type', + 'text'], edge_properties_to_project=['id', - 'relationship_name'], - directed=True, - node_dimension=1, - edge_dimension=1, - memory_fragment_filter=[]) + 'relationship_name']) - print() + await memory_fragment.map_vector_distances_to_graph_nodes(node_distances=node_distances) + + await memory_fragment.map_vector_distances_to_graph_edges(vector_engine, query)# :TODO: This should be coming from vector db + + results = await memory_fragment.calculate_top_triplet_importances(k=5) - raise(NotImplementedError) \ No newline at end of file + print(format_triplets(results)) + print(f'Query was the following:{query}' ) diff --git a/examples/python/dynamic_steps_example.py b/examples/python/dynamic_steps_example.py index 11c2f1110..f4aa0aaf7 100644 --- a/examples/python/dynamic_steps_example.py +++ b/examples/python/dynamic_steps_example.py @@ -2,32 +2,6 @@ import cognee import asyncio from cognee.pipelines.retriever.two_steps_retriever import two_step_retriever -job_position = """0:Senior Data Scientist (Machine Learning) - -Company: TechNova Solutions -Location: San Francisco, CA - -Job Description: - -TechNova Solutions is seeking a Senior Data Scientist specializing in Machine Learning to join our dynamic analytics team. The ideal candidate will have a strong background in developing and deploying machine learning models, working with large datasets, and translating complex data into actionable insights. - -Responsibilities: - -Develop and implement advanced machine learning algorithms and models. -Analyze large, complex datasets to extract meaningful patterns and insights. -Collaborate with cross-functional teams to integrate predictive models into products. -Stay updated with the latest advancements in machine learning and data science. -Mentor junior data scientists and provide technical guidance. -Qualifications: - -Master’s or Ph.D. in Data Science, Computer Science, Statistics, or a related field. -5+ years of experience in data science and machine learning. -Proficient in Python, R, and SQL. -Experience with deep learning frameworks (e.g., TensorFlow, PyTorch). -Strong problem-solving skills and attention to detail. -Candidate CVs -""" - job_1 = """ CV 1: Relevant Name: Dr. Emily Carter @@ -195,7 +169,7 @@ async def main(enable_steps): # Step 2: Add text if enable_steps.get("add_text"): - text_list = [job_position, job_1, job_2, job_3, job_4, job_5] + text_list = [job_1, job_2, job_3, job_4, job_5] for text in text_list: await cognee.add(text) print(f"Added text: {text[:35]}...") @@ -207,22 +181,20 @@ async def main(enable_steps): # Step 4: Query insights if enable_steps.get("retriever"): - search_results = await two_step_retriever( - {'query': 'Which applicant has the most relevant experience in data science?'} - ) - print("Search results:") - for result_text in search_results: - print(result_text) + await two_step_retriever('Who has Phd?') if __name__ == '__main__': # Flags to enable/disable steps + + rebuild_kg = False + retrieve = True steps_to_enable = { - "prune_data": False, - "prune_system": False, - "add_text": False, - "cognify": False, - "retriever": True + "prune_data": rebuild_kg, + "prune_system": rebuild_kg, + "add_text": rebuild_kg, + "cognify": rebuild_kg, + "retriever": retrieve } asyncio.run(main(steps_to_enable)) From 6efe566849d11d96c550438f277babc86f529f29 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:40:56 +0100 Subject: [PATCH 13/31] fix: Adds new obligatory attributes to cognee graph tests --- cognee/tests/unit/modules/graph/cognee_graph_elements_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cognee/tests/unit/modules/graph/cognee_graph_elements_test.py b/cognee/tests/unit/modules/graph/cognee_graph_elements_test.py index d2a1b6c59..a3755a58f 100644 --- a/cognee/tests/unit/modules/graph/cognee_graph_elements_test.py +++ b/cognee/tests/unit/modules/graph/cognee_graph_elements_test.py @@ -8,7 +8,7 @@ def test_node_initialization(): """Test that a Node is initialized correctly.""" node = Node("node1", {"attr1": "value1"}, dimension=2) assert node.id == "node1" - assert node.attributes == {"attr1": "value1"} + assert node.attributes == {"attr1": "value1", 'vector_distance': np.inf} assert len(node.status) == 2 assert np.all(node.status == 1) @@ -95,7 +95,7 @@ def test_edge_initialization(): edge = Edge(node1, node2, {"weight": 10}, directed=False, dimension=2) assert edge.node1 == node1 assert edge.node2 == node2 - assert edge.attributes == {"weight": 10} + assert edge.attributes == {'vector_distance': np.inf,"weight": 10} assert edge.directed is False assert len(edge.status) == 2 assert np.all(edge.status == 1) From b5d9e7a6d2363596ec3da5845c6cfc09c99c94ad Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:03:32 +0100 Subject: [PATCH 14/31] chore: adds return value and sets tue entry point kg generation to true --- cognee/pipelines/retriever/two_steps_retriever.py | 7 +++---- examples/python/dynamic_steps_example.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cognee/pipelines/retriever/two_steps_retriever.py b/cognee/pipelines/retriever/two_steps_retriever.py index ff35a0864..92ef2be2e 100644 --- a/cognee/pipelines/retriever/two_steps_retriever.py +++ b/cognee/pipelines/retriever/two_steps_retriever.py @@ -9,14 +9,12 @@ from cognee.modules.users.permissions.methods import get_document_ids_for_user from cognee.modules.graph.cognee_graph.CogneeGraph import CogneeGraph from cognee.infrastructure.databases.vector import get_vector_engine from cognee.infrastructure.databases.graph import get_graph_engine -from openai import organization -from sympy.codegen.fnodes import dimension def format_triplets(edges): + print("\n\n\n") def filter_attributes(obj, attributes): """Helper function to filter out non-None properties, including nested dicts.""" - print("\n\n\n") result = {} for attr in attributes: value = getattr(obj, attr, None) @@ -115,6 +113,7 @@ async def run_two_step_retriever(query: str, user, community_filter = []) -> lis results = await memory_fragment.calculate_top_triplet_importances(k=5) - print(format_triplets(results)) print(f'Query was the following:{query}' ) + + return results diff --git a/examples/python/dynamic_steps_example.py b/examples/python/dynamic_steps_example.py index f4aa0aaf7..49b41db1c 100644 --- a/examples/python/dynamic_steps_example.py +++ b/examples/python/dynamic_steps_example.py @@ -187,7 +187,7 @@ async def main(enable_steps): if __name__ == '__main__': # Flags to enable/disable steps - rebuild_kg = False + rebuild_kg = True retrieve = True steps_to_enable = { "prune_data": rebuild_kg, From 157d7d217d32432724bd216fc1ceb0a00933366d Mon Sep 17 00:00:00 2001 From: hande-k Date: Thu, 21 Nov 2024 13:57:42 +0100 Subject: [PATCH 15/31] docs: added cognify steps in the print statement and commented example output --- README.md | 42 ++++++++++++++++++++++--------- examples/python/simple_example.py | 18 +++++++++++-- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8dc8c5c66..ed4489fbf 100644 --- a/README.md +++ b/README.md @@ -116,34 +116,52 @@ async def main(): Natural language processing (NLP) is an interdisciplinary subfield of computer science and information retrieval. """ + print("Adding text to cognee:") - print(text.strip()) + print(text.strip()) + # Add the text, and make it available for cognify await cognee.add(text) print("Text added successfully.\n") + + print("Running cognify to create knowledge graph...\n") + print("Cognify process steps:") + print("1. Classifying the document: Determining the type and category of the input text.") + print("2. Checking permissions: Ensuring the user has the necessary rights to process the text.") + print("3. Extracting text chunks: Breaking down the text into sentences or phrases for analysis.") + print("4. Adding data points: Storing the extracted chunks for processing.") + print("5. Generating knowledge graph: Extracting entities and relationships to form a knowledge graph.") + print("6. Summarizing text: Creating concise summaries of the content for quick insights.\n") + # Use LLMs and cognee to create knowledge graph - print("Running cognify to create knowledge graph...") await cognee.cognify() print("Cognify process complete.\n") - # Query cognee for insights on the added text + query_text = 'Tell me about NLP' print(f"Searching cognee for insights with query: '{query_text}'") + # Query cognee for insights on the added text search_results = await cognee.search( - SearchType.INSIGHTS, - query_text=query_text, + SearchType.INSIGHTS, query_text=query_text ) - - # Display search results + print("Search results:") + # Display results for result_text in search_results: print(result_text) - # Expected output: - # natural_language_processing is_a field - # natural_language_processing is_subfield_of computer_science - # natural_language_processing is_subfield_of information_retrieval -asyncio.run(main()) + # Example output: + # ({'id': UUID('bc338a39-64d6-549a-acec-da60846dd90d'), 'updated_at': datetime.datetime(2024, 11, 21, 12, 23, 1, 211808, tzinfo=datetime.timezone.utc), 'name': 'natural language processing', 'description': 'An interdisciplinary subfield of computer science and information retrieval.'}, {'relationship_name': 'is_a_subfield_of', 'source_node_id': UUID('bc338a39-64d6-549a-acec-da60846dd90d'), 'target_node_id': UUID('6218dbab-eb6a-5759-a864-b3419755ffe0'), 'updated_at': datetime.datetime(2024, 11, 21, 12, 23, 15, 473137, tzinfo=datetime.timezone.utc)}, {'id': UUID('6218dbab-eb6a-5759-a864-b3419755ffe0'), 'updated_at': datetime.datetime(2024, 11, 21, 12, 23, 1, 211808, tzinfo=datetime.timezone.utc), 'name': 'computer science', 'description': 'The study of computation and information processing.'}) + # (...) + # + # It represents nodes and relationships in the knowledge graph: + # - The first element is the source node (e.g., 'natural language processing'). + # - The second element is the relationship between nodes (e.g., 'is_a_subfield_of'). + # - The third element is the target node (e.g., 'computer science'). + +if __name__ == '__main__': + asyncio.run(main()) + ``` When you run this script, you will see step-by-step messages in the console that help you trace the execution flow and understand what the script is doing at each stage. A version of this example is here: `examples/python/simple_example.py` diff --git a/examples/python/simple_example.py b/examples/python/simple_example.py index e0b212746..55b07c4c3 100644 --- a/examples/python/simple_example.py +++ b/examples/python/simple_example.py @@ -1,5 +1,4 @@ import asyncio - import cognee from cognee.api.v1.search import SearchType @@ -29,7 +28,15 @@ async def main(): print("Text added successfully.\n") - print("Running cognify to create knowledge graph...") + print("Running cognify to create knowledge graph...\n") + print("Cognify process steps:") + print("1. Classifying the document: Determining the type and category of the input text.") + print("2. Checking permissions: Ensuring the user has the necessary rights to process the text.") + print("3. Extracting text chunks: Breaking down the text into sentences or phrases for analysis.") + print("4. Adding data points: Storing the extracted chunks for processing.") + print("5. Generating knowledge graph: Extracting entities and relationships to form a knowledge graph.") + print("6. Summarizing text: Creating concise summaries of the content for quick insights.\n") + # Use LLMs and cognee to create knowledge graph await cognee.cognify() print("Cognify process complete.\n") @@ -47,6 +54,13 @@ async def main(): for result_text in search_results: print(result_text) + # Example output: + # ({'id': UUID('bc338a39-64d6-549a-acec-da60846dd90d'), 'updated_at': datetime.datetime(2024, 11, 21, 12, 23, 1, 211808, tzinfo=datetime.timezone.utc), 'name': 'natural language processing', 'description': 'An interdisciplinary subfield of computer science and information retrieval.'}, {'relationship_name': 'is_a_subfield_of', 'source_node_id': UUID('bc338a39-64d6-549a-acec-da60846dd90d'), 'target_node_id': UUID('6218dbab-eb6a-5759-a864-b3419755ffe0'), 'updated_at': datetime.datetime(2024, 11, 21, 12, 23, 15, 473137, tzinfo=datetime.timezone.utc)}, {'id': UUID('6218dbab-eb6a-5759-a864-b3419755ffe0'), 'updated_at': datetime.datetime(2024, 11, 21, 12, 23, 1, 211808, tzinfo=datetime.timezone.utc), 'name': 'computer science', 'description': 'The study of computation and information processing.'}) + # (...) + # It represents nodes and relationships in the knowledge graph: + # - The first element is the source node (e.g., 'natural language processing'). + # - The second element is the relationship between nodes (e.g., 'is_a_subfield_of'). + # - The third element is the target node (e.g., 'computer science'). if __name__ == '__main__': asyncio.run(main()) From 9193eca08b0f7462bef727ddc18177ebd3fd7717 Mon Sep 17 00:00:00 2001 From: Igor Ilic Date: Mon, 25 Nov 2024 15:00:02 +0100 Subject: [PATCH 16/31] Trigger GitHub Actions --- .../databases/vector/pgvector/create_db_and_tables.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cognee/infrastructure/databases/vector/pgvector/create_db_and_tables.py b/cognee/infrastructure/databases/vector/pgvector/create_db_and_tables.py index ef27e2889..f40299939 100644 --- a/cognee/infrastructure/databases/vector/pgvector/create_db_and_tables.py +++ b/cognee/infrastructure/databases/vector/pgvector/create_db_and_tables.py @@ -10,5 +10,3 @@ async def create_db_and_tables(): await vector_engine.create_database() async with vector_engine.engine.begin() as connection: await connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector;")) - - From 97dbede4c4377a7ebc6b99c89e80d5721121525b Mon Sep 17 00:00:00 2001 From: Igor Ilic Date: Mon, 25 Nov 2024 15:31:32 +0100 Subject: [PATCH 17/31] test: Add fix for telemetry issue in gh actions Set environment variable of ENV to dev for all jobs in workflows in GH actions --- .github/workflows/test_python_3_10.yml | 1 + .github/workflows/test_python_3_11.yml | 1 + .github/workflows/test_python_3_9.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/test_python_3_10.yml b/.github/workflows/test_python_3_10.yml index 7f762d778..5a4523853 100644 --- a/.github/workflows/test_python_3_10.yml +++ b/.github/workflows/test_python_3_10.yml @@ -13,6 +13,7 @@ concurrency: env: RUNTIME__LOG_LEVEL: ERROR + ENV: 'dev' jobs: get_docs_changes: diff --git a/.github/workflows/test_python_3_11.yml b/.github/workflows/test_python_3_11.yml index b05d901dc..9c79fb0ff 100644 --- a/.github/workflows/test_python_3_11.yml +++ b/.github/workflows/test_python_3_11.yml @@ -13,6 +13,7 @@ concurrency: env: RUNTIME__LOG_LEVEL: ERROR + ENV: 'dev' jobs: get_docs_changes: diff --git a/.github/workflows/test_python_3_9.yml b/.github/workflows/test_python_3_9.yml index 47c5ddc41..9c8456536 100644 --- a/.github/workflows/test_python_3_9.yml +++ b/.github/workflows/test_python_3_9.yml @@ -13,6 +13,7 @@ concurrency: env: RUNTIME__LOG_LEVEL: ERROR + ENV: 'dev' jobs: get_docs_changes: From 66c321f206a10e1191a7cdedc191cbecd22cc0f5 Mon Sep 17 00:00:00 2001 From: Igor Ilic Date: Mon, 25 Nov 2024 17:32:11 +0100 Subject: [PATCH 18/31] fix: Add fix for getting transcription of audio and image from LLMs Enable getting of text from audio and image files from LLMs Fix --- cognee/infrastructure/llm/openai/adapter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cognee/infrastructure/llm/openai/adapter.py b/cognee/infrastructure/llm/openai/adapter.py index 28cdfff4e..1dc9b70f5 100644 --- a/cognee/infrastructure/llm/openai/adapter.py +++ b/cognee/infrastructure/llm/openai/adapter.py @@ -87,6 +87,9 @@ class OpenAIAdapter(LLMInterface): transcription = litellm.transcription( model = self.transcription_model, file = Path(input), + api_key=self.api_key, + api_base=self.endpoint, + api_version=self.api_version, max_retries = 5, ) @@ -112,6 +115,9 @@ class OpenAIAdapter(LLMInterface): }, ], }], + api_key=self.api_key, + api_base=self.endpoint, + api_version=self.api_version, max_tokens = 300, max_retries = 5, ) From a59517409c8e4d29899ecd3d4fafe42b052b8dc5 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:45:48 +0100 Subject: [PATCH 19/31] chore: Fixes some of the issues based on PR review + restructures things --- .../vector/pgvector/PGVectorAdapter.py | 2 - .../modules/graph/cognee_graph/CogneeGraph.py | 6 +-- ...iever.py => brute_force_triplet_search.py} | 42 +++++++++---------- .../retriever/diffusion_retriever.py | 25 ----------- cognee/pipelines/retriever/g_retriever.py | 25 ----------- examples/python/dynamic_steps_example.py | 6 +-- 6 files changed, 26 insertions(+), 80 deletions(-) rename cognee/pipelines/retriever/{two_steps_retriever.py => brute_force_triplet_search.py} (75%) delete mode 100644 cognee/pipelines/retriever/diffusion_retriever.py delete mode 100644 cognee/pipelines/retriever/g_retriever.py diff --git a/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py b/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py index 97571a274..a4cfcf789 100644 --- a/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py +++ b/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py @@ -192,8 +192,6 @@ class PGVectorAdapter(SQLAlchemyAdapter, VectorDBInterface): # Get PGVectorDataPoint Table from database PGVectorDataPoint = await self.get_table(collection_name) - closest_items = [] - # Use async session to connect to the database async with self.get_async_session() as session: # Find closest vectors to query_vector diff --git a/cognee/modules/graph/cognee_graph/CogneeGraph.py b/cognee/modules/graph/cognee_graph/CogneeGraph.py index 158fb9d07..0b5aebce4 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraph.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraph.py @@ -164,9 +164,9 @@ class CogneeGraph(CogneeAbstractGraph): source_node = self.get_node(edge.node1.id) target_node = self.get_node(edge.node2.id) - source_distance = source_node.attributes.get("vector_distance", 0) if source_node else 0 - target_distance = target_node.attributes.get("vector_distance", 0) if target_node else 0 - edge_distance = edge.attributes.get("vector_distance", 0) + source_distance = source_node.attributes.get("vector_distance", 1) if source_node else 1 + target_distance = target_node.attributes.get("vector_distance", 1) if target_node else 1 + edge_distance = edge.attributes.get("vector_distance", 1) total_distance = source_distance + target_distance + edge_distance diff --git a/cognee/pipelines/retriever/two_steps_retriever.py b/cognee/pipelines/retriever/brute_force_triplet_search.py similarity index 75% rename from cognee/pipelines/retriever/two_steps_retriever.py rename to cognee/pipelines/retriever/brute_force_triplet_search.py index 92ef2be2e..6fef6104f 100644 --- a/cognee/pipelines/retriever/two_steps_retriever.py +++ b/cognee/pipelines/retriever/brute_force_triplet_search.py @@ -1,15 +1,11 @@ import asyncio -from uuid import UUID -from enum import Enum -from typing import Callable, Dict -from cognee.shared.utils import send_telemetry +from typing import Dict, List from cognee.modules.users.models import User from cognee.modules.users.methods import get_default_user -from cognee.modules.users.permissions.methods import get_document_ids_for_user from cognee.modules.graph.cognee_graph.CogneeGraph import CogneeGraph from cognee.infrastructure.databases.vector import get_vector_engine from cognee.infrastructure.databases.graph import get_graph_engine - +from cognee.shared.utils import send_telemetry def format_triplets(edges): print("\n\n\n") @@ -44,24 +40,22 @@ def format_triplets(edges): triplet = ( f"Node1: {node1_info}\n" f"Edge: {edge_info}\n" - f"Node2: {node2_info}\n\n\n" # Add three blank lines for separation + f"Node2: {node2_info}\n\n\n" ) triplets.append(triplet) return "".join(triplets) -async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: +async def brute_force_triplet_search(query: str, user: User = None, top_k = 5) -> list: if user is None: user = await get_default_user() if user is None: raise PermissionError("No user found in the system. Please create a user.") - own_document_ids = await get_document_ids_for_user(user.id) - retrieved_results = await run_two_step_retriever(query, user) + retrieved_results = await brute_force_search(query, user, top_k) - filtered_search_results = [] return retrieved_results @@ -82,18 +76,22 @@ def delete_duplicated_vector_db_elements(collections, results): #:TODO: This is return results_dict -async def run_two_step_retriever(query: str, user, community_filter = []) -> list: +async def brute_force_search(query: str, user: User, top_k: int, collections: List[str] = None) -> list: + if collections is None: + collections = ["entity_name", "text_summary_text", "entity_type_name", "document_chunk_text"] + vector_engine = get_vector_engine() graph_engine = await get_graph_engine() - collections = ["Entity_name", "TextSummary_text", 'EntityType_name', 'DocumentChunk_text'] + send_telemetry("cognee.brute_force_triplet_search EXECUTION STARTED", user.id) + results = await asyncio.gather( *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] ) - ############################################# This part is a quick fix til we don't fix the vector db inconsistency - node_distances = delete_duplicated_vector_db_elements(collections, results)# :TODO: Change when vector db is fixed - # results_dict = {collection: result for collection, result in zip(collections, results)} + ############################################# :TODO: Change when vector db does not contain duplicates + node_distances = delete_duplicated_vector_db_elements(collections, results) + # node_distances = {collection: result for collection, result in zip(collections, results)} ############################################## memory_fragment = CogneeGraph() @@ -104,16 +102,16 @@ async def run_two_step_retriever(query: str, user, community_filter = []) -> lis 'name', 'type', 'text'], - edge_properties_to_project=['id', - 'relationship_name']) + edge_properties_to_project=['relationship_name']) await memory_fragment.map_vector_distances_to_graph_nodes(node_distances=node_distances) - await memory_fragment.map_vector_distances_to_graph_edges(vector_engine, query)# :TODO: This should be coming from vector db + #:TODO: Change when vectordb contains edge embeddings + await memory_fragment.map_vector_distances_to_graph_edges(vector_engine, query) - results = await memory_fragment.calculate_top_triplet_importances(k=5) + results = await memory_fragment.calculate_top_triplet_importances(k=top_k) - print(format_triplets(results)) - print(f'Query was the following:{query}' ) + send_telemetry("cognee.brute_force_triplet_search EXECUTION STARTED", user.id) + #:TODO: Once we have Edge pydantic models we should retrieve the exact edge and node objects from graph db return results diff --git a/cognee/pipelines/retriever/diffusion_retriever.py b/cognee/pipelines/retriever/diffusion_retriever.py deleted file mode 100644 index a6b79310e..000000000 --- a/cognee/pipelines/retriever/diffusion_retriever.py +++ /dev/null @@ -1,25 +0,0 @@ -from uuid import UUID -from enum import Enum -from typing import Callable, Dict -from cognee.shared.utils import send_telemetry -from cognee.modules.users.models import User -from cognee.modules.users.methods import get_default_user -from cognee.modules.users.permissions.methods import get_document_ids_for_user - -async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: - if user is None: - user = await get_default_user() - - if user is None: - raise PermissionError("No user found in the system. Please create a user.") - - own_document_ids = await get_document_ids_for_user(user.id) - retrieved_results = await diffusion_retriever(query, user) - - filtered_search_results = [] - - - return retrieved_results - -async def diffusion_retriever(query: str, user, community_filter = []) -> list: - raise(NotImplementedError) \ No newline at end of file diff --git a/cognee/pipelines/retriever/g_retriever.py b/cognee/pipelines/retriever/g_retriever.py deleted file mode 100644 index 4b319acd9..000000000 --- a/cognee/pipelines/retriever/g_retriever.py +++ /dev/null @@ -1,25 +0,0 @@ -from uuid import UUID -from enum import Enum -from typing import Callable, Dict -from cognee.shared.utils import send_telemetry -from cognee.modules.users.models import User -from cognee.modules.users.methods import get_default_user -from cognee.modules.users.permissions.methods import get_document_ids_for_user - -async def two_step_retriever(query: Dict[str, str], user: User = None) -> list: - if user is None: - user = await get_default_user() - - if user is None: - raise PermissionError("No user found in the system. Please create a user.") - - own_document_ids = await get_document_ids_for_user(user.id) - retrieved_results = await g_retriever(query, user) - - filtered_search_results = [] - - - return retrieved_results - -async def g_retriever(query: str, user, community_filter = []) -> list: - raise(NotImplementedError) \ No newline at end of file diff --git a/examples/python/dynamic_steps_example.py b/examples/python/dynamic_steps_example.py index 49b41db1c..6b75310cf 100644 --- a/examples/python/dynamic_steps_example.py +++ b/examples/python/dynamic_steps_example.py @@ -1,6 +1,6 @@ import cognee import asyncio -from cognee.pipelines.retriever.two_steps_retriever import two_step_retriever +from cognee.pipelines.retriever.brute_force_triplet_search import brute_force_triplet_search job_1 = """ CV 1: Relevant @@ -181,13 +181,13 @@ async def main(enable_steps): # Step 4: Query insights if enable_steps.get("retriever"): - await two_step_retriever('Who has Phd?') + await brute_force_triplet_search('Who has Phd?') if __name__ == '__main__': # Flags to enable/disable steps - rebuild_kg = True + rebuild_kg = False retrieve = True steps_to_enable = { "prune_data": rebuild_kg, From 163bdc527cb096f39a9f7e11008f05b6902602bc Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:37:34 +0100 Subject: [PATCH 20/31] chore: fixes PR issues regarding vector normalization and cognee graph --- .../databases/vector/lancedb/LanceDBAdapter.py | 3 +-- cognee/infrastructure/databases/vector/utils.py | 10 ---------- cognee/modules/graph/cognee_graph/CogneeGraph.py | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py b/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py index 6cbe45655..66d960c20 100644 --- a/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py +++ b/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py @@ -146,8 +146,7 @@ class LanceDBAdapter(VectorDBInterface): self, collection_name: str, query_text: str = None, - query_vector: List[float] = None, - with_vector: bool = False + query_vector: List[float] = None ): if query_text is None and query_vector is None: raise ValueError("One of query_text or query_vector must be provided!") diff --git a/cognee/infrastructure/databases/vector/utils.py b/cognee/infrastructure/databases/vector/utils.py index ced161ea3..30cff3f02 100644 --- a/cognee/infrastructure/databases/vector/utils.py +++ b/cognee/infrastructure/databases/vector/utils.py @@ -2,17 +2,7 @@ from typing import List def normalize_distances(result_values: List[dict]) -> List[float]: - min_value = 100 - max_value = 0 - for result in result_values: - value = float(result["_distance"]) - if value > max_value: - max_value = value - if value < min_value: - min_value = value - - normalized_values = [] min_value = min(result["_distance"] for result in result_values) max_value = max(result["_distance"] for result in result_values) diff --git a/cognee/modules/graph/cognee_graph/CogneeGraph.py b/cognee/modules/graph/cognee_graph/CogneeGraph.py index 0b5aebce4..3f0d48a23 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraph.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraph.py @@ -158,7 +158,7 @@ class CogneeGraph(CogneeAbstractGraph): print(f"Error mapping vector distances to edges: {ex}") - async def calculate_top_triplet_importances(self, k = int) -> List: + async def calculate_top_triplet_importances(self, k: int) -> List: min_heap = [] for i, edge in enumerate(self.edges): source_node = self.get_node(edge.node1.id) From c66c43e71786aa37b4f93e3a8f362524d46c922d Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:44:11 +0100 Subject: [PATCH 21/31] chore: places retrievers under modules directory --- cognee/{pipelines => modules/retrieval}/__init__.py | 0 .../retrieval}/brute_force_triplet_search.py | 0 cognee/pipelines/retriever/__init__.py | 0 examples/python/dynamic_steps_example.py | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) rename cognee/{pipelines => modules/retrieval}/__init__.py (100%) rename cognee/{pipelines/retriever => modules/retrieval}/brute_force_triplet_search.py (100%) delete mode 100644 cognee/pipelines/retriever/__init__.py diff --git a/cognee/pipelines/__init__.py b/cognee/modules/retrieval/__init__.py similarity index 100% rename from cognee/pipelines/__init__.py rename to cognee/modules/retrieval/__init__.py diff --git a/cognee/pipelines/retriever/brute_force_triplet_search.py b/cognee/modules/retrieval/brute_force_triplet_search.py similarity index 100% rename from cognee/pipelines/retriever/brute_force_triplet_search.py rename to cognee/modules/retrieval/brute_force_triplet_search.py diff --git a/cognee/pipelines/retriever/__init__.py b/cognee/pipelines/retriever/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/python/dynamic_steps_example.py b/examples/python/dynamic_steps_example.py index 6b75310cf..68bbb7bce 100644 --- a/examples/python/dynamic_steps_example.py +++ b/examples/python/dynamic_steps_example.py @@ -1,6 +1,6 @@ import cognee import asyncio -from cognee.pipelines.retriever.brute_force_triplet_search import brute_force_triplet_search +from cognee.modules.retrieval.brute_force_triplet_search import brute_force_triplet_search job_1 = """ CV 1: Relevant @@ -187,7 +187,7 @@ async def main(enable_steps): if __name__ == '__main__': # Flags to enable/disable steps - rebuild_kg = False + rebuild_kg = True retrieve = True steps_to_enable = { "prune_data": rebuild_kg, From db07179856cbdba1349d3bebf3a70e7f2e16e54b Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:17:57 +0100 Subject: [PATCH 22/31] chore: Adds error handling to brute force triplet search --- .../retrieval/brute_force_triplet_search.py | 85 +++++++++++++------ examples/python/dynamic_steps_example.py | 5 +- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/cognee/modules/retrieval/brute_force_triplet_search.py b/cognee/modules/retrieval/brute_force_triplet_search.py index 6fef6104f..ea7c2cb4d 100644 --- a/cognee/modules/retrieval/brute_force_triplet_search.py +++ b/cognee/modules/retrieval/brute_force_triplet_search.py @@ -1,5 +1,6 @@ import asyncio -from typing import Dict, List +import logging +from typing import List from cognee.modules.users.models import User from cognee.modules.users.methods import get_default_user from cognee.modules.graph.cognee_graph.CogneeGraph import CogneeGraph @@ -76,42 +77,74 @@ def delete_duplicated_vector_db_elements(collections, results): #:TODO: This is return results_dict -async def brute_force_search(query: str, user: User, top_k: int, collections: List[str] = None) -> list: +async def brute_force_search( + query: str, + user: User, + top_k: int, + collections: List[str] = None +) -> list: + """ + Performs a brute force search to retrieve the top triplets from the graph. + + Args: + query (str): The search query. + user (User): The user performing the search. + top_k (int): The number of top results to retrieve. + collections (Optional[List[str]]): List of collections to query. Defaults to predefined collections. + + Returns: + list: The top triplet results. + """ + if not query or not isinstance(query, str): + raise ValueError("The query must be a non-empty string.") + if top_k <= 0: + raise ValueError("top_k must be a positive integer.") + if collections is None: collections = ["entity_name", "text_summary_text", "entity_type_name", "document_chunk_text"] - vector_engine = get_vector_engine() - graph_engine = await get_graph_engine() + try: + vector_engine = get_vector_engine() + graph_engine = await get_graph_engine() + except Exception as e: + logging.error("Failed to initialize engines: %s", e) + raise RuntimeError("Initialization error") from e send_telemetry("cognee.brute_force_triplet_search EXECUTION STARTED", user.id) - results = await asyncio.gather( - *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] - ) + try: + results = await asyncio.gather( + *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] + ) - ############################################# :TODO: Change when vector db does not contain duplicates - node_distances = delete_duplicated_vector_db_elements(collections, results) - # node_distances = {collection: result for collection, result in zip(collections, results)} - ############################################## + ############################################# :TODO: Change when vector db does not contain duplicates + node_distances = delete_duplicated_vector_db_elements(collections, results) + # node_distances = {collection: result for collection, result in zip(collections, results)} + ############################################## - memory_fragment = CogneeGraph() + memory_fragment = CogneeGraph() - await memory_fragment.project_graph_from_db(graph_engine, - node_properties_to_project=['id', - 'description', - 'name', - 'type', - 'text'], - edge_properties_to_project=['relationship_name']) + await memory_fragment.project_graph_from_db(graph_engine, + node_properties_to_project=['id', + 'description', + 'name', + 'type', + 'text'], + edge_properties_to_project=['relationship_name']) - await memory_fragment.map_vector_distances_to_graph_nodes(node_distances=node_distances) + await memory_fragment.map_vector_distances_to_graph_nodes(node_distances=node_distances) - #:TODO: Change when vectordb contains edge embeddings - await memory_fragment.map_vector_distances_to_graph_edges(vector_engine, query) + #:TODO: Change when vectordb contains edge embeddings + await memory_fragment.map_vector_distances_to_graph_edges(vector_engine, query) - results = await memory_fragment.calculate_top_triplet_importances(k=top_k) + results = await memory_fragment.calculate_top_triplet_importances(k=top_k) - send_telemetry("cognee.brute_force_triplet_search EXECUTION STARTED", user.id) + send_telemetry("cognee.brute_force_triplet_search EXECUTION STARTED", user.id) - #:TODO: Once we have Edge pydantic models we should retrieve the exact edge and node objects from graph db - return results + #:TODO: Once we have Edge pydantic models we should retrieve the exact edge and node objects from graph db + return results + + except Exception as e: + logging.error("Error during brute force search for user: %s, query: %s. Error: %s", user.id, query, e) + send_telemetry("cognee.brute_force_triplet_search EXECUTION FAILED", user.id) + raise RuntimeError("An error occurred during brute force search") from e diff --git a/examples/python/dynamic_steps_example.py b/examples/python/dynamic_steps_example.py index 68bbb7bce..ed5c97561 100644 --- a/examples/python/dynamic_steps_example.py +++ b/examples/python/dynamic_steps_example.py @@ -1,6 +1,7 @@ import cognee import asyncio from cognee.modules.retrieval.brute_force_triplet_search import brute_force_triplet_search +from cognee.modules.retrieval.brute_force_triplet_search import format_triplets job_1 = """ CV 1: Relevant @@ -181,8 +182,8 @@ async def main(enable_steps): # Step 4: Query insights if enable_steps.get("retriever"): - await brute_force_triplet_search('Who has Phd?') - + results = await brute_force_triplet_search('Who has the most experience with graphic design?') + print(format_triplets(results)) if __name__ == '__main__': # Flags to enable/disable steps From c9f66145f5b7b67be0c8606d54a4f14b8fe53cbc Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:33:33 +0100 Subject: [PATCH 23/31] feat: checks neo4j test for bruteforce retriever --- cognee/tests/test_neo4j.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cognee/tests/test_neo4j.py b/cognee/tests/test_neo4j.py index 756b29cc4..8eea5b7f7 100644 --- a/cognee/tests/test_neo4j.py +++ b/cognee/tests/test_neo4j.py @@ -4,6 +4,7 @@ import logging import pathlib import cognee from cognee.api.v1.search import SearchType +from cognee.modules.retrieval.brute_force_triplet_search import brute_force_triplet_search logging.basicConfig(level=logging.DEBUG) @@ -61,6 +62,9 @@ async def main(): assert len(history) == 6, "Search history is not correct." + results = await brute_force_triplet_search('Who has the most experience with graphic design?') + assert len(results)>0 + if __name__ == "__main__": import asyncio asyncio.run(main()) From ecdf3d4d54cc04f62be1726568a9f4d442131ae9 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:35:20 +0100 Subject: [PATCH 24/31] fix: Updates neo4j test --- cognee/tests/test_neo4j.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cognee/tests/test_neo4j.py b/cognee/tests/test_neo4j.py index 8eea5b7f7..4efbcc66a 100644 --- a/cognee/tests/test_neo4j.py +++ b/cognee/tests/test_neo4j.py @@ -62,9 +62,6 @@ async def main(): assert len(history) == 6, "Search history is not correct." - results = await brute_force_triplet_search('Who has the most experience with graphic design?') - assert len(results)>0 - if __name__ == "__main__": import asyncio asyncio.run(main()) From 0441e19bc9a294761cce95a6c2aaf2f6ec7c8918 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:42:35 +0100 Subject: [PATCH 25/31] feat: Adds bruteforce retriever test for neo4j --- cognee/tests/test_neo4j.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cognee/tests/test_neo4j.py b/cognee/tests/test_neo4j.py index 60f006c35..87dd789a6 100644 --- a/cognee/tests/test_neo4j.py +++ b/cognee/tests/test_neo4j.py @@ -62,6 +62,9 @@ async def main(): assert len(history) == 6, "Search history is not correct." + results = await brute_force_triplet_search('Who has the most experience with graphic design?') + assert len(results) > 0 + await cognee.prune.prune_data() assert not os.path.isdir(data_directory_path), "Local data files are not deleted" From 4035302dd45e03f8e6f9e318e5ecd98501537e45 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:48:09 +0100 Subject: [PATCH 26/31] feat: Adds tests for pgvector, qdrant and weaviate --- cognee/tests/test_pgvector.py | 4 ++++ cognee/tests/test_qdrant.py | 4 ++++ cognee/tests/test_weaviate.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/cognee/tests/test_pgvector.py b/cognee/tests/test_pgvector.py index bd6584cbc..1a62fa4d4 100644 --- a/cognee/tests/test_pgvector.py +++ b/cognee/tests/test_pgvector.py @@ -3,6 +3,7 @@ import logging import pathlib import cognee from cognee.api.v1.search import SearchType +from cognee.modules.retrieval.brute_force_triplet_search import brute_force_triplet_search logging.basicConfig(level=logging.DEBUG) @@ -89,6 +90,9 @@ async def main(): history = await cognee.get_search_history() assert len(history) == 6, "Search history is not correct." + results = await brute_force_triplet_search('Who has the most experience with graphic design?') + assert len(results) > 0 + await cognee.prune.prune_data() assert not os.path.isdir(data_directory_path), "Local data files are not deleted" diff --git a/cognee/tests/test_qdrant.py b/cognee/tests/test_qdrant.py index 4c2462c3b..e0df0e980 100644 --- a/cognee/tests/test_qdrant.py +++ b/cognee/tests/test_qdrant.py @@ -5,6 +5,7 @@ import logging import pathlib import cognee from cognee.api.v1.search import SearchType +from cognee.modules.retrieval.brute_force_triplet_search import brute_force_triplet_search logging.basicConfig(level=logging.DEBUG) @@ -61,6 +62,9 @@ async def main(): history = await cognee.get_search_history() assert len(history) == 6, "Search history is not correct." + results = await brute_force_triplet_search('Who has the most experience with graphic design?') + assert len(results) > 0 + await cognee.prune.prune_data() assert not os.path.isdir(data_directory_path), "Local data files are not deleted" diff --git a/cognee/tests/test_weaviate.py b/cognee/tests/test_weaviate.py index c352df13e..726a8ebc2 100644 --- a/cognee/tests/test_weaviate.py +++ b/cognee/tests/test_weaviate.py @@ -3,6 +3,7 @@ import logging import pathlib import cognee from cognee.api.v1.search import SearchType +from cognee.modules.retrieval.brute_force_triplet_search import brute_force_triplet_search logging.basicConfig(level=logging.DEBUG) @@ -59,6 +60,9 @@ async def main(): history = await cognee.get_search_history() assert len(history) == 6, "Search history is not correct." + results = await brute_force_triplet_search('Who has the most experience with graphic design?') + assert len(results) > 0 + await cognee.prune.prune_data() assert not os.path.isdir(data_directory_path), "Local data files are not deleted" From 4c9d816f874d5b4aa80335b806ec857a66aa7629 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:05:38 +0100 Subject: [PATCH 27/31] feat: extends bruteforce triplet search for Qdrant db --- .../databases/vector/qdrant/QDrantAdapter.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py b/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py index 1efcd47b3..08c47d005 100644 --- a/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py +++ b/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py @@ -142,6 +142,41 @@ class QDrantAdapter(VectorDBInterface): await client.close() return results + async def get_distances_of_collection( + self, + collection_name: str, + query_text: str = None, + query_vector: List[float] = None, + with_vector: bool = False + ) -> List[ScoredResult]: + + if query_text is None and query_vector is None: + raise ValueError("One of query_text or query_vector must be provided!") + + client = self.get_qdrant_client() + + results = await client.search( + collection_name = collection_name, + query_vector = models.NamedVector( + name = "text", + vector = query_vector if query_vector is not None else (await self.embed_data([query_text]))[0], + ), + with_vectors = with_vector + ) + + await client.close() + + return [ + ScoredResult( + id = UUID(result.id), + payload = { + **result.payload, + "id": UUID(result.id), + }, + score = 1 - result.score, + ) for result in results + ] + async def search( self, collection_name: str, From 98a517dd9f97a9cf87e15e85e8af524dd5a0c7ef Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:20:53 +0100 Subject: [PATCH 28/31] feat: extends brute force triplet search for weaviate db --- .../vector/weaviate_db/WeaviateAdapter.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py b/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py index be356740f..a8aa568a0 100644 --- a/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py +++ b/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py @@ -153,6 +153,36 @@ class WeaviateAdapter(VectorDBInterface): return await future + async def get_distances_of_collection( + self, + collection_name: str, + query_text: str = None, + query_vector: List[float] = None, + with_vector: bool = False + ) -> List[ScoredResult]: + import weaviate.classes as wvc + + if query_text is None and query_vector is None: + raise ValueError("One of query_text or query_vector must be provided!") + + if query_vector is None: + query_vector = (await self.embed_data([query_text]))[0] + + search_result = self.get_collection(collection_name).query.hybrid( + query=None, + vector=query_vector, + include_vector=with_vector, + return_metadata=wvc.query.MetadataQuery(score=True), + ) + + return [ + ScoredResult( + id=UUID(str(result.uuid)), + payload=result.properties, + score=1 - float(result.metadata.score) + ) for result in search_result.objects + ] + async def search( self, collection_name: str, From c30683e20ec2feeddfcb11a77ca76f9d12e44bee Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:29:44 +0100 Subject: [PATCH 29/31] chore: changes query text in tests --- cognee/tests/test_neo4j.py | 2 +- cognee/tests/test_pgvector.py | 2 +- cognee/tests/test_qdrant.py | 2 +- cognee/tests/test_weaviate.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cognee/tests/test_neo4j.py b/cognee/tests/test_neo4j.py index 87dd789a6..92e5b5f05 100644 --- a/cognee/tests/test_neo4j.py +++ b/cognee/tests/test_neo4j.py @@ -62,7 +62,7 @@ async def main(): assert len(history) == 6, "Search history is not correct." - results = await brute_force_triplet_search('Who has the most experience with graphic design?') + results = await brute_force_triplet_search('What is a quantum computer?') assert len(results) > 0 await cognee.prune.prune_data() diff --git a/cognee/tests/test_pgvector.py b/cognee/tests/test_pgvector.py index 1a62fa4d4..3b4fa19c5 100644 --- a/cognee/tests/test_pgvector.py +++ b/cognee/tests/test_pgvector.py @@ -90,7 +90,7 @@ async def main(): history = await cognee.get_search_history() assert len(history) == 6, "Search history is not correct." - results = await brute_force_triplet_search('Who has the most experience with graphic design?') + results = await brute_force_triplet_search('What is a quantum computer?') assert len(results) > 0 await cognee.prune.prune_data() diff --git a/cognee/tests/test_qdrant.py b/cognee/tests/test_qdrant.py index e0df0e980..f32e0b4a4 100644 --- a/cognee/tests/test_qdrant.py +++ b/cognee/tests/test_qdrant.py @@ -62,7 +62,7 @@ async def main(): history = await cognee.get_search_history() assert len(history) == 6, "Search history is not correct." - results = await brute_force_triplet_search('Who has the most experience with graphic design?') + results = await brute_force_triplet_search('What is a quantum computer?') assert len(results) > 0 await cognee.prune.prune_data() diff --git a/cognee/tests/test_weaviate.py b/cognee/tests/test_weaviate.py index 726a8ebc2..43ec30aaf 100644 --- a/cognee/tests/test_weaviate.py +++ b/cognee/tests/test_weaviate.py @@ -60,7 +60,7 @@ async def main(): history = await cognee.get_search_history() assert len(history) == 6, "Search history is not correct." - results = await brute_force_triplet_search('Who has the most experience with graphic design?') + results = await brute_force_triplet_search('What is a quantum computer?') assert len(results) > 0 await cognee.prune.prune_data() From 94dc545fcd904f44083af3bb564a5a8a81a97108 Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:42:35 +0100 Subject: [PATCH 30/31] chore: adds self to cogneegraph edges --- cognee/modules/graph/cognee_graph/CogneeGraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cognee/modules/graph/cognee_graph/CogneeGraph.py b/cognee/modules/graph/cognee_graph/CogneeGraph.py index 3f0d48a23..715b8260e 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraph.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraph.py @@ -50,7 +50,7 @@ class CogneeGraph(CogneeAbstractGraph): raise ValueError(f"Node with id {node_id} does not exist.") def get_edges(self)-> List[Edge]: - return edges + return self.edges async def project_graph_from_db(self, adapter: Union[GraphDBInterface], From 3146ef75c9ceea873efb8de242d9617b6582601c Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:47:26 +0100 Subject: [PATCH 31/31] Fix: renames new vector db and cogneegraph methods --- .../infrastructure/databases/vector/lancedb/LanceDBAdapter.py | 2 +- .../databases/vector/pgvector/PGVectorAdapter.py | 2 +- .../infrastructure/databases/vector/qdrant/QDrantAdapter.py | 2 +- .../databases/vector/weaviate_db/WeaviateAdapter.py | 2 +- cognee/modules/graph/cognee_graph/CogneeGraph.py | 2 +- cognee/modules/retrieval/brute_force_triplet_search.py | 2 +- cognee/tests/unit/modules/graph/cognee_graph_test.py | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py b/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py index 66d960c20..5204c1bad 100644 --- a/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py +++ b/cognee/infrastructure/databases/vector/lancedb/LanceDBAdapter.py @@ -142,7 +142,7 @@ class LanceDBAdapter(VectorDBInterface): score = 0, ) for result in results.to_dict("index").values()] - async def get_distances_of_collection( + async def get_distance_from_collection_elements( self, collection_name: str, query_text: str = None, diff --git a/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py b/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py index a4cfcf789..fd0fd493c 100644 --- a/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py +++ b/cognee/infrastructure/databases/vector/pgvector/PGVectorAdapter.py @@ -176,7 +176,7 @@ class PGVectorAdapter(SQLAlchemyAdapter, VectorDBInterface): ) for result in results ] - async def get_distances_of_collection( + async def get_distance_from_collection_elements( self, collection_name: str, query_text: str = None, diff --git a/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py b/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py index 08c47d005..c340928f4 100644 --- a/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py +++ b/cognee/infrastructure/databases/vector/qdrant/QDrantAdapter.py @@ -142,7 +142,7 @@ class QDrantAdapter(VectorDBInterface): await client.close() return results - async def get_distances_of_collection( + async def get_distance_from_collection_elements( self, collection_name: str, query_text: str = None, diff --git a/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py b/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py index a8aa568a0..c9848e02c 100644 --- a/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py +++ b/cognee/infrastructure/databases/vector/weaviate_db/WeaviateAdapter.py @@ -153,7 +153,7 @@ class WeaviateAdapter(VectorDBInterface): return await future - async def get_distances_of_collection( + async def get_distance_from_collection_elements( self, collection_name: str, query_text: str = None, diff --git a/cognee/modules/graph/cognee_graph/CogneeGraph.py b/cognee/modules/graph/cognee_graph/CogneeGraph.py index 715b8260e..21d095f3d 100644 --- a/cognee/modules/graph/cognee_graph/CogneeGraph.py +++ b/cognee/modules/graph/cognee_graph/CogneeGraph.py @@ -42,7 +42,7 @@ class CogneeGraph(CogneeAbstractGraph): def get_node(self, node_id: str) -> Node: return self.nodes.get(node_id, None) - def get_edges_of_node(self, node_id: str) -> List[Edge]: + def get_edges_from_node(self, node_id: str) -> List[Edge]: node = self.get_node(node_id) if node: return node.skeleton_edges diff --git a/cognee/modules/retrieval/brute_force_triplet_search.py b/cognee/modules/retrieval/brute_force_triplet_search.py index ea7c2cb4d..0a4e9dea5 100644 --- a/cognee/modules/retrieval/brute_force_triplet_search.py +++ b/cognee/modules/retrieval/brute_force_triplet_search.py @@ -114,7 +114,7 @@ async def brute_force_search( try: results = await asyncio.gather( - *[vector_engine.get_distances_of_collection(collection, query_text=query) for collection in collections] + *[vector_engine.get_distance_from_collection_elements(collection, query_text=query) for collection in collections] ) ############################################# :TODO: Change when vector db does not contain duplicates diff --git a/cognee/tests/unit/modules/graph/cognee_graph_test.py b/cognee/tests/unit/modules/graph/cognee_graph_test.py index bad474023..e3b748dab 100644 --- a/cognee/tests/unit/modules/graph/cognee_graph_test.py +++ b/cognee/tests/unit/modules/graph/cognee_graph_test.py @@ -77,11 +77,11 @@ def test_get_edges_success(setup_graph): graph.add_node(node2) edge = Edge(node1, node2) graph.add_edge(edge) - assert edge in graph.get_edges_of_node("node1") + assert edge in graph.get_edges_from_node("node1") def test_get_edges_nonexistent_node(setup_graph): """Test retrieving edges for a nonexistent node raises an exception.""" graph = setup_graph with pytest.raises(ValueError, match="Node with id nonexistent does not exist."): - graph.get_edges_of_node("nonexistent") + graph.get_edges_from_node("nonexistent")