From 218d925113fdc1c0fee84ff6aee5e748b0f97c59 Mon Sep 17 00:00:00 2001 From: ngupta10 Date: Wed, 26 Jun 2024 15:02:54 +0530 Subject: [PATCH 1/3] updated readme and example --- README.md | 341 ++++++++---------- .../transformers/bert_ner_opensourcellm.py | 10 +- .../attn_based_relationship_filter.py | 9 +- .../kg/rel_helperfunctions/triple_to_json.py | 6 +- tests/data/readme_assets/example.pdf | Bin 0 -> 17434 bytes tests/tutorial/__init__.py | 0 tests/tutorial/docker-compose.yaml | 23 ++ tests/tutorial/example_fixed_entities.py | 102 ++++++ tests/tutorial/postgres_utility.py | 199 ++++++++++ 9 files changed, 490 insertions(+), 200 deletions(-) create mode 100644 tests/data/readme_assets/example.pdf create mode 100644 tests/tutorial/__init__.py create mode 100644 tests/tutorial/docker-compose.yaml create mode 100644 tests/tutorial/example_fixed_entities.py create mode 100644 tests/tutorial/postgres_utility.py diff --git a/README.md b/README.md index f4a8f584..ee904184 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,17 @@ The Asynchronous Data Dynamo and Graph Neural Network Catalyst ## Unlock Insights, Asynchronous Scaling, and Forge a Knowledge-Driven Future -πŸš€ **Async at its Core**: Querent thrives in an asynchronous world. With asynchronous processing, we handle multiple data sources seamlessly, eliminating bottlenecks for utmost efficiency. +πŸš€ **Asynchronous Processing**: Querent excels in handling data from multiple sources concurrently with asynchronous processing, eliminating bottlenecks and maximizing efficiency. -πŸ’‘ **Knowledge Graphs Made Easy**: Constructing intricate knowledge graphs is a breeze. Querent's robust architecture simplifies building comprehensive knowledge graphs, enabling you to uncover hidden data relationships. +πŸ’‘ **Effortless Knowledge Graph Construction:**: Querent's robust architecture simplifies building comprehensive knowledge graphs, enabling you to uncover hidden data relationships. -🌐 **Scalability Redefined**: Scaling your data operations is effortless with Querent. We scale horizontally, empowering you to process multiple data streams without breaking a sweat. +🌐 **Seamless Scalability**: Easily scale your data operations with Querent's horizontal scaling capabilities, allowing for the smooth processing of multiple data streams. -πŸ”¬ **GNN Integration**: Querent seamlessly integrates with Graph Neural Networks (GNNs), enabling advanced data analysis, recommendation systems, and predictive modeling. +πŸ” **Data-Driven Insights**: Extract actionable information and make data-informed decisions with ease. -πŸ” **Data-Driven Insights**: Dive deep into data-driven insights with Querent's tools. Extract actionable information and make data-informed decisions with ease. +🧠 **Advanced Language Model Utilization**: Utilize state-of-the-art language models (LLMs) for natural language processing tasks, enabling Querent to tackle complex text-based challenges. -🧠 **Leverage Language Models**: Utilize state-of-the-art language models (LLMs) for text data. Querent empowers natural language processing, tackling complex text-based tasks. - -πŸ“ˆ **Efficient Memory Usage**: Querent is mindful of memory constraints. Our framework uses memory-efficient techniques, ensuring you can handle large datasets economically. +πŸ“ˆ **Memory-Efficient Framework**: Querent is designed to handle large datasets economically, using memory-efficient techniques to ensure optimal performance even under memory constraints. ## Table of Contents @@ -29,72 +27,35 @@ The Asynchronous Data Dynamo and Graph Neural Network Catalyst - [Table of Contents](#table-of-contents) - [Introduction](#introduction) - [Features](#features) - - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - [Usage](#usage) - [Configuration](#configuration) - [Querent: an asynchronous engine for LLMs](#querent-an-asynchronous-engine-for-llms) - - [Ease of Use](#ease-of-use) + - [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Setup DB](#setup-db) + - [Example](#example) - [Contributing](#contributing) - [License](#license) + ## Introduction -Querent is designed to simplify and optimize data collection and processing workflows. Whether you need to scrape web data, ingest files, preprocess text, or create complex knowledge graphs, Querent offers a flexible framework for building and scaling these processes. +Querent is designed to simplify and optimize data collection and processing workflows. Whether you need to ingest files, preprocess text, or create complex knowledge graphs from local data, Querent offers a flexible framework for building and scaling these processes. ## Features -- **Collectors:** Gather data from various sources asynchronously, including web scraping and file collection. +- **Collectors:** Gather local data from file sources asynchronously. - **Ingestors:** Process collected data efficiently with custom transformations and filtering. - **Processors:** Apply asynchronous data processing, including text preprocessing, cleaning, and feature extraction. -- **Engines:** Execute a suite of LLM engines to extract insights from data, leveraging parallel processing for enhanced efficiency. - -- **Storage:** Store processed data in various storage systems, such as databases or cloud storage. - -- **Workflow Management:** Efficiently manage and scale data workflows with task orchestration. - -- **Scalability:** Querent is designed to scale horizontally, handling large volumes of data with ease. - -## Getting Started - -Let's get Querent up and running on your local machine. - -### Prerequisites - -- Python 3.9+ -- Virtual environment (optional but recommended) - -### Installation - -1. Create a virtual environment (recommended): - - ```bash - python -m venv venv - source venv/bin/activate # On Windows, use `venv\Scripts\activate` - ``` -2. Install latest Querent Workflow Orchestrator package: +- **Engines:** Leverage a Language Model (LLM) engine to convert textual data into knowledge triples (Subject, Predicate, Object) based on attention matrix scores. - ```bash - pip install querent - ``` - -3. Install the project dependencies: +- **Storage:** Store processed data in a PostgreSQL storage system. - ```bash - python3 -m spacy download en_core_web_lg - ``` -4. Apt install the project dependencies: - ```bash - sudo apt install tesseract-ocr - sudo apt install libtesseract-dev - sudo apt-get install ffmpeg - sudo apt install antiword - ``` ## Usage @@ -104,17 +65,10 @@ Querent provides a flexible framework that adapts to your specific data collecti 2. **Collecting Data:** Implement collector classes to gather data from chosen sources. Handle errors and edge cases gracefully. -3. **Processing Data:** Create ingestors and processors to clean, transform, and filter collected data. Apply custom logic to meet your requirements. +3. **Processing Data:** Create ingestors and processors to clean, transform, and filter collected data. 4. **Storage:** Choose your storage system (e.g., databases) and configure connections. Store processed data efficiently. -5. **Task Orchestration:** For large tasks, implement a task orchestrator to manage and distribute the workload. - -6. **Scaling:** To handle scalability, consider running multiple instances of collectors and ingestors in parallel. - -7. **Monitoring:** Implement monitoring and logging to track task progress, detect errors, and ensure smooth operation. - -8. **Documentation:** Maintain thorough project documentation to make it easy for others (and yourself) to understand and contribute. ## Configuration @@ -161,139 +115,148 @@ sequenceDiagram ``` -## Ease of Use +## Getting Started + +Let's get Querent up and running on your local machine. + +### Prerequisites + +- Python 3.9+ +- Virtual environment (optional but recommended) + +### Installation + +1. Create a virtual environment (recommended): + + ```bash + python -m venv venv + source venv/bin/activate # On Windows, use `venv\Scripts\activate` + ``` +2. Install latest Querent Workflow Orchestrator package: + + ```bash + pip install querent + ``` + +3. Install the project dependencies: + + ```bash + python3 -m spacy download en_core_web_lg + ``` + +4. Apt install the project dependencies: + ```bash + sudo apt install tesseract-ocr + sudo apt install libtesseract-dev + sudo apt-get install ffmpeg + sudo apt install antiword + ``` +5. Install torch + ``` + pip install torch + ``` +6. Install Docker : Refer to the [official documentation](https://docs.docker.com/engine/install/) + +### Setup DB + +1. **Download the docker compose file** - [Postgres docker compose file.](tests/tutorial/docker-compose.yaml) + +2. **Run Postgres Instance** - Navigate to the directory where the docker compose file is downloaded. Execute the below: + +```bash + docker compose up +``` + +### Example -With Querent, creating scalable workflows with any LLM is just a few lines of code. +1. **Download the example file with fixed entities** - [Example file.](tests/tutorial/example_fixed_entities.py). Then also download the [example pdf](tests/data/readme_assets/example.pdf) and place it in a directory. + +2. **Run the example file** - This script will load the BERT-based embedding model to extract attention weights. The algorithm is designed to identify semantic triples in the data. In the example.py file above, users should modify the script to change the directory where the `example.pdf` file is stored. If running on personal files, modify the fixed entities and their respective types. This will create semantic triples (Subject, Predicate, Object) based on user-provided data. Execute the below: ```python -import pytest -import uuid -from pathlib import Path -import asyncio - -from querent.callback.event_callback_interface import EventCallbackInterface -from querent.common.types.ingested_tokens import IngestedTokens -from querent.common.types.ingested_code import IngestedCode -from querent.common.types.ingested_images import IngestedImages -from querent.common.types.ingested_messages import IngestedMessages -from querent.common.types.querent_event import EventState, EventType -from querent.common.types.querent_queue import QuerentQueue -from querent.core.base_engine import BaseEngine -from querent.querent.querent import Querent -from querent.querent.resource_manager import ResourceManager -from querent.collectors.collector_resolver import CollectorResolver -from querent.common.uri import Uri -from querent.config.collector.collector_config import FSCollectorConfig -from querent.ingestors.ingestor_manager import IngestorFactoryManager - -# Create input and output queues -input_queue = QuerentQueue() -resource_manager = ResourceManager() - - -# Define a simple mock LLM engine for testing -class MockLLMEngine(BaseEngine): - def __init__(self, input_queue: QuerentQueue): - super().__init__(input_queue) - - async def process_tokens(self, data: IngestedTokens): - if data is None or data.is_error(): - # the LLM developer can raise an error here or do something else - # the developers of Querent can customize the behavior of Querent - # to handle the error in a way that is appropriate for the use case - self.set_termination_event() - return - # Set the state of the LLM - # At any given point during the execution of the LLM, the LLM developer - # can set the state of the LLM using the set_state method - # The state of the LLM is stored in the state attribute of the LLM - # The state of the LLM is published to subscribers of the LLM - current_state = EventState(EventType.Graph, 1.0, "anything", "dummy.txt") - await self.set_state(new_state=current_state) - - async def process_code(self, data: IngestedCode): - pass - - async def process_messages(self, data: IngestedMessages): - return super().process_messages(data) - - async def process_images(self, data: IngestedImages): - return super().process_images(data) - - def validate(self): - return True - - -@pytest.mark.asyncio -async def test_example_workflow_with_querent(): - # Initialize some collectors to collect the data - directory_path = "path/to/your/data/directory" - collectors = [ - CollectorResolver().resolve( - Uri("file://" + str(Path(directory_path).resolve())), - FSCollectorConfig(root_path=directory_path, id=str(uuid.uuid4())), - ) - ] - - # Connect to the collector - for collector in collectors: - await collector.connect() - - # Set up the result queue - result_queue = asyncio.Queue() - - # Create the IngestorFactoryManager - ingestor_factory_manager = IngestorFactoryManager( - collectors=collectors, result_queue=result_queue - ) - - # Start the ingest_all_async in a separate task - ingest_task = asyncio.create_task(ingestor_factory_manager.ingest_all_async()) - - ### A Typical Use Case ### - # Create an engine to harness the LLM - llm_mocker = MockLLMEngine(input_queue) - - # Define a callback function to subscribe to state changes - class StateChangeCallback(EventCallbackInterface): - async def handle_event(self, event_type: EventType, event_state: EventState): - print(f"New state: {event_state}") - print(f"New state type: {event_type}") - assert event_state.event_type == EventType.Graph - - # Subscribe to state change events - # This pattern is ideal as we can expose multiple events for each use case of the LLM - llm_mocker.subscribe(EventType.Graph, StateChangeCallback()) - - ## one can also subscribe to other events, e.g. EventType.CHAT_COMPLETION ... - - # Create a Querent instance with a single MockLLM - # here we see the simplicity of the Querent - # massive complexity is hidden in the Querent, - # while being highly configurable, extensible, and scalable - # async architecture helps to scale to multiple querenters - # How async architecture works: - # 1. Querent starts a worker task for each querenter - # 2. Querenter starts a worker task for each worker - # 3. Each worker task runs in a loop, waiting for input data - # 4. When input data is received, the worker task processes the data - # 5. The worker task notifies subscribers of state changes - # 6. The worker task repeats steps 3-5 until termination - querent = Querent( - [llm_mocker], - resource_manager=resource_manager, - ) - # Start the querent - querent_task = asyncio.create_task(querent.start()) - await asyncio.gather(ingest_task, querent_task) - - -if __name__ == "__main__": - asyncio.run(test_example_workflow_with_querent()) + python3 example_fixed_entities.py +``` + + +3. **Example Output** - Two tables are initialized when the above script is run + +- **Metadata table** - + +| id | event_id | subject | subject_type | predicate | object | object_type | sentence | file | doc_source | score | +|----|--------------------------------------|----------|--------------|-----------|--------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|---------| +| 1 | 298b4df3-a2f1-4721-b78d-9099309257c2 | coach | person | athlete | health | method | coach torres, with her innovative approach to student-athlete health and her emphasis on holistic training methods, has significantly influenced the physical and mental preparedness of greenwood's athletes. | /home/user/querent-main/readme_assets/example.pdf | file:///home/user/querent-main/readme_assets | 0.159262 | + + +- **Embedding table** - + +| id | event_id | embeddings | +|----|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| 1 | 298b4df3-a2f1-4721-b78d-9099309257c2 | [-0.00637318,0.0032276064,-0.016642869,0.018911008,-0.004372431,0.035932742,0.010418983,-0.00960234,0.009969827,-0.021499356,...] | + + +## Performing Similarity Search + +Users can perform similarity searches in the embedding table to find relevant documents based on the vector embeddings. Here’s how you can do it: + +1. Convert your query into a vector embedding using the same embedding model used for creating the embeddings in the embedding table. +2. Find similar matches: Perform a similarity search in the embedding table to find the top N similar embeddings. + +3. Retrieve relevant data: Use the `event_id` from the similar embeddings to fetch the corresponding data from the metadata table. + +This approach is highly useful when dealing with thousands of files, as it essentially creates pointers to knowledge, making it easy to retrieve relevant information efficiently. + +## Traversing the Data +Querent allows you to traverse the data using SQL queries, enabling you to explore inward and outward edges from either the subject or object. Here’s how: + +1. Get Outward Edges: Find all relationships where a given entity is the subject. +```sql +SELECT * FROM public.metadata +WHERE subject = 'your_entity'; +``` + +2. Get Inward Edges: Find all relationships where a given entity is the object. +```sql +SELECT * FROM public.metadata +WHERE object = 'your_entity'; +``` +3. Find Shortest Path Based on Score: Use recursive queries to find the shortest path between entities based on the score. +```sql +WITH RECURSIVE Path (id, event_id, subject, object, score, path, depth) AS ( + SELECT id, event_id, subject, object, score, ARRAY[subject, object]::VARCHAR[], 1 + FROM public.metadata + WHERE subject = 'start_entity' + UNION ALL + SELECT m.id, m.event_id, m.subject, m.object, p.score + m.score, p.path || m.object, p.depth + 1 + FROM metadata m + JOIN Path p ON m.subject = p.object + WHERE p.depth < 10 -- Limit depth to prevent infinite recursion + AND NOT (m.object = ANY(p.path)) -- Avoid cycles +) +SELECT * +FROM Path +WHERE 'end_entity' = ANY(path) +ORDER BY score ASC +LIMIT 1; + 1; ``` + +## Additional Benefits + +1. Preparing Factual Data: The extracted triples can be used to prepare factual data for fine-tuning or training large language models (LLMs). + +2. GNN Use Cases: Graph Neural Networks (GNNs) can utilize the relationships and entities extracted to perform downstream tasks such as link prediction, node classification, and more. + +3. AI Use Cases: Enable advanced AI functionalities like cross-document summarization, entity recognition, and trend analysis across a large corpus of documents. + +4. Replacing the Need for a dedicated Graph Database: By using PostgreSQL and the embedded vectors, you can achieve efficient graph traversal and relationship mapping without the overhead of a dedicated graph database. This reduces complexity and cost. + +5. Scalability: This method scales well with the number of documents, making it suitable for large datasets. + +This system not only enhances data retrieval and analysis but also provides a robust foundation for various AI and machine learning applications. + ## Contributing Contributions to Querent are welcome! Please follow our [contribution guidelines](CONTRIBUTING.md) to get started. diff --git a/querent/core/transformers/bert_ner_opensourcellm.py b/querent/core/transformers/bert_ner_opensourcellm.py index 510c84eb..87f1b837 100644 --- a/querent/core/transformers/bert_ner_opensourcellm.py +++ b/querent/core/transformers/bert_ner_opensourcellm.py @@ -1,4 +1,5 @@ import json +import uuid from transformers import AutoConfig, AutoTokenizer import transformers import time @@ -298,7 +299,6 @@ async def process_tokens(self, data: IngestedTokens): try: doc_entity_pairs = [] doc_source = data.doc_source - if not BERTLLM.validate_ingested_tokens(data): self.set_termination_event() return @@ -311,7 +311,7 @@ async def process_tokens(self, data: IngestedTokens): doc_entity_pairs = self._get_entity_pairs(content) if not doc_entity_pairs: return - + doc_entity_pairs = self._process_entity_types(doc_entity_pairs) if not self.entity_context_extractor and not self.predicate_context_extractor: pairs_withattn = self.attn_scores_instance.extract_and_append_attention_weights(doc_entity_pairs) @@ -341,6 +341,7 @@ def _prepare_content(self, data): else: content = clean_text file = data.get_file_path() + return content, file def _get_entity_pairs(self, content): @@ -414,8 +415,9 @@ async def _process_embedding_triples(self, embedding_triples, file, doc_source): for triple in embedding_triples: if self.termination_event.is_set(): return + event_id = str(uuid.uuid4()) - graph_json = json.dumps(TripleToJsonConverter.convert_graphjson(triple)) + graph_json = json.dumps(TripleToJsonConverter.convert_graphjson(triple, event_id=event_id)) if graph_json: current_state = EventState( event_type=EventType.Graph, @@ -436,7 +438,7 @@ async def _process_embedding_triples(self, embedding_triples, file, doc_source): base_weights=[predicate_score, predicate_score, 3], normalize_weights=True # Normalize weights to ensure they sum to 1 ) - vector_json = json.dumps(TripleToJsonConverter.convert_vectorjson(triple=triple, embeddings=final_emb)) + vector_json = json.dumps(TripleToJsonConverter.convert_vectorjson(triple=triple, embeddings=final_emb,event_id=event_id)) if vector_json: current_state = EventState( event_type=EventType.Vector, diff --git a/querent/kg/rel_helperfunctions/attn_based_relationship_filter.py b/querent/kg/rel_helperfunctions/attn_based_relationship_filter.py index f9a2b877..85c1a75f 100644 --- a/querent/kg/rel_helperfunctions/attn_based_relationship_filter.py +++ b/querent/kg/rel_helperfunctions/attn_based_relationship_filter.py @@ -172,9 +172,10 @@ def process_tokens(ner_instance : NER_LLM, extractor, filtered_triples, nlp_mode updated_triples = [] for subject, predicate_metadata, object in filtered_triples: try: - context = predicate_metadata['current_sentence'].replace("\n"," ") + context = predicate_metadata['current_sentence'].replace("\n"," ").lower() head_positions = ner_instance.find_subword_indices(context, predicate_metadata['entity1_nn_chunk']) tail_positions = ner_instance.find_subword_indices(context, predicate_metadata['entity2_nn_chunk']) + if head_positions[0][0] > tail_positions[0][0]: head_entity = {'entity': object, 'noun_chunk':predicate_metadata['entity2_nn_chunk'], 'entity_label':predicate_metadata['entity2_label'] } tail_entity = {'entity': subject, 'noun_chunk':predicate_metadata['entity1_nn_chunk'], 'entity_label':predicate_metadata['entity1_label']} @@ -188,20 +189,18 @@ def process_tokens(ner_instance : NER_LLM, extractor, filtered_triples, nlp_mode attention_matrix = extractor.inference_attention(model_input) token_idx_with_word = ner_instance.tokenize_sentence_with_positions(context) spacy_doc = nlp_model(context) - filter = IndividualFilter(True, 0.02, token_idx_with_word, spacy_doc) - + filter = IndividualFilter(True, 0.01, token_idx_with_word, spacy_doc) + ## HEAD Entity Based Attention Search candidate_paths = perform_search(entity_pair.head_entity['start_idx'], attention_matrix, entity_pair, search_candidates=5, require_contiguous=True, max_relation_length=8, num_initial_tokens=extractor.num_start_tokens()) candidate_paths = remove_duplicates(candidate_paths) filtered_results = filter.filter(candidates=candidate_paths,e_pair=entity_pair) predicate_he, score_he = get_best_relation(filtered_results) - ##TAIL ENTITY Based Attention Search candidate_paths = perform_search(entity_pair.tail_entity['start_idx'], attention_matrix, entity_pair, search_candidates=5, require_contiguous=True, max_relation_length=8, num_initial_tokens=extractor.num_start_tokens()) candidate_paths = remove_duplicates(candidate_paths) filtered_results = filter.filter(candidates=candidate_paths,e_pair=entity_pair) predicate_te, score_te = get_best_relation(filtered_results) - if score_he > score_te and (score_he >= 0.1 or score_te >= 0.1): triple = create_semantic_triple(head_entity=head_entity['noun_chunk'], tail_entity=tail_entity['noun_chunk'], diff --git a/querent/kg/rel_helperfunctions/triple_to_json.py b/querent/kg/rel_helperfunctions/triple_to_json.py index 950d3b4a..f577bfdc 100644 --- a/querent/kg/rel_helperfunctions/triple_to_json.py +++ b/querent/kg/rel_helperfunctions/triple_to_json.py @@ -31,7 +31,7 @@ def _parse_json_str(json_str): raise ValueError(f"Error decoding JSON: {e}") @staticmethod - def convert_graphjson(triple): + def convert_graphjson(triple, event_id = None): try: subject, json_str, object_ = triple predicate_info = TripleToJsonConverter._parse_json_str(json_str) @@ -39,6 +39,7 @@ def convert_graphjson(triple): return {} json_object = { + "event_id": event_id, "subject": TripleToJsonConverter._normalize_text(subject, replace_space=True), "subject_type": TripleToJsonConverter._normalize_text(predicate_info.get("subject_type", "Unlabeled"), replace_space=True), "object": TripleToJsonConverter._normalize_text(object_, replace_space=True), @@ -67,7 +68,7 @@ def dynamic_weighted_average_embeddings(embeddings, base_weights, normalize_weig return weighted_sum @staticmethod - def convert_vectorjson(triple, blob = None, embeddings=None): + def convert_vectorjson(triple, blob = None, embeddings=None, event_id = None): try: subject, json_str, object_ = triple data = TripleToJsonConverter._parse_json_str(json_str) @@ -76,6 +77,7 @@ def convert_vectorjson(triple, blob = None, embeddings=None): id_format = f"{TripleToJsonConverter._normalize_text(subject,replace_space=True)}-{TripleToJsonConverter._normalize_text(data.get('predicate', ''),replace_space=True)}-{TripleToJsonConverter._normalize_text(object_,replace_space=True)}" json_object = { + "event_id": event_id, "id": TripleToJsonConverter._normalize_text(id_format), "embeddings": embeddings.tolist(), "size": len(embeddings.tolist()), diff --git a/tests/data/readme_assets/example.pdf b/tests/data/readme_assets/example.pdf new file mode 100644 index 0000000000000000000000000000000000000000..312fc47981cf6602f4b6464bdcdf97f9e5b59710 GIT binary patch literal 17434 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
  • A5R|0w+aG!{YVw#~xFov)1dz>pFcZ(u3CxL}_ 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 literal 0 HcmV?d00001 diff --git a/tests/tutorial/__init__.py b/tests/tutorial/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tutorial/docker-compose.yaml b/tests/tutorial/docker-compose.yaml new file mode 100644 index 00000000..b1b7382e --- /dev/null +++ b/tests/tutorial/docker-compose.yaml @@ -0,0 +1,23 @@ +version: '3' +services: + postgres: + image: pgvector/pgvector:pg16 + environment: + - POSTGRES_USER=querent + - POSTGRES_PASSWORD=querent + - POSTGRES_DB=querent_test + volumes: + - ./quester/storage/sql/:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + networks: + - querent + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "querent_test"] + interval: 30s + timeout: 60s + retries: 5 + start_period: 80s + +networks: + querent: diff --git a/tests/tutorial/example_fixed_entities.py b/tests/tutorial/example_fixed_entities.py new file mode 100644 index 00000000..4226161d --- /dev/null +++ b/tests/tutorial/example_fixed_entities.py @@ -0,0 +1,102 @@ +import asyncio +from asyncio import Queue +import json +from pathlib import Path +from querent.callback.event_callback_interface import EventCallbackInterface +from querent.collectors.fs.fs_collector import FSCollectorFactory +from querent.common.types.querent_event import EventState, EventType +from querent.config.collector.collector_config import FSCollectorConfig +from querent.common.uri import Uri +from querent.config.core.llm_config import LLM_Config +from querent.ingestors.ingestor_manager import IngestorFactoryManager +import uuid +import numpy as np +from querent.core.transformers.bert_ner_opensourcellm import BERTLLM +from querent.querent.resource_manager import ResourceManager +from querent.querent.querent import Querent +from postgres_utility import DatabaseManager + +async def ingest_all_async(): + db_manager = DatabaseManager( + dbname="querent_test", + user="querent", + password="querent", + host="localhost", + port="5432" + ) + + db_manager.connect_db() + db_manager.create_tables() + directories = ["/home/nishantg/querent-main/querent/tests/data/readme_assets"] + collectors = [ + FSCollectorFactory().resolve( + Uri("file://" + str(Path(directory).resolve())), + FSCollectorConfig(config_source={ + "id": str(uuid.uuid4()), + "root_path": directory, + "name": "Local-config", + "config": {}, + "backend": "localfile", + "uri": "file://", + }), + ) + for directory in directories + ] + + result_queue = asyncio.Queue() + + ingestor_factory_manager = IngestorFactoryManager( + collectors=collectors, result_queue=result_queue + ) + ingest_task = asyncio.create_task(ingestor_factory_manager.ingest_all_async()) + resource_manager = ResourceManager() + bert_llm_config = LLM_Config( + # ner_model_name="English", + rel_model_type = "bert", + rel_model_path = 'bert-base-uncased', + fixed_entities = [ + "university", "greenwood", "liam zheng", "department", "Metroville", + "Emily Stanton", "Coach", "health", "training", "athletes" + ], + sample_entities = [ + "organization", "organization", "person", "department", "city", + "person", "person", "method", "method", "person" + ], + is_confined_search = True + ) + llm_instance = BERTLLM(result_queue, bert_llm_config) + + class StateChangeCallback(EventCallbackInterface): + def handle_event(self, event_type: EventType, event_state: EventState): + if event_state['event_type'] == EventType.Graph: + triple = json.loads(event_state['payload']) + db_manager.insert_metadata( + event_id=triple['event_id'], + subject=triple['subject'], + subject_type=triple['subject_type'], + predicate=triple['predicate'], + object=triple['object'], + object_type=triple['object_type'], + sentence=triple['sentence'], + file=event_state['file'], + doc_source=event_state['doc_source'], + score=triple['score'] +) + elif event_state['event_type'] == EventType.Vector: + triple_v = json.loads(event_state['payload']) + db_manager.insert_embedding( + event_id=triple_v['event_id'], + embeddings=triple_v['embeddings'], + ) + + llm_instance.subscribe(EventType.Graph, StateChangeCallback()) + llm_instance.subscribe(EventType.Vector, StateChangeCallback()) + querent = Querent( + [llm_instance], + resource_manager=resource_manager, + ) + querent_task = asyncio.create_task(querent.start()) + await asyncio.gather(ingest_task, querent_task) + +if __name__ == "__main__": + asyncio.run(ingest_all_async()) diff --git a/tests/tutorial/postgres_utility.py b/tests/tutorial/postgres_utility.py new file mode 100644 index 00000000..7597b587 --- /dev/null +++ b/tests/tutorial/postgres_utility.py @@ -0,0 +1,199 @@ +import psycopg2 +from psycopg2 import sql +from psycopg2.extras import Json + +from querent.kg.rel_helperfunctions.embedding_store import EmbeddingStore +import numpy as np + +class DatabaseManager: + def __init__(self, dbname, user, password, host, port): + self.dbname = dbname + self.user = user + self.password = password + self.host = host + self.port = port + self.connection = None + + def connect_db(self): + try: + self.connection = psycopg2.connect( + dbname=self.dbname, + user=self.user, + password=self.password, + host=self.host, + port=self.port + ) + print("Database connection established") + except Exception as e: + print(f"Error connecting to database: {e}") + + def create_tables(self): + create_metadata_table_query = """ + CREATE TABLE IF NOT EXISTS metadata ( + id SERIAL PRIMARY KEY, + event_id UUID, + subject VARCHAR(255), + subject_type VARCHAR(255), + predicate VARCHAR(255), + object VARCHAR(255), + object_type VARCHAR(255), + sentence TEXT, + file VARCHAR(255), + doc_source VARCHAR(255), + score FLOAT + ); + """ + + create_embedding_table_query = """ + CREATE TABLE IF NOT EXISTS embedding ( + id SERIAL PRIMARY KEY, + event_id UUID, + embeddings VECTOR(384) + ); + """ + + try: + with self.connection.cursor() as cursor: + cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;") # Enable pgvector extension + cursor.execute(create_metadata_table_query) + cursor.execute(create_embedding_table_query) + self.connection.commit() + print("Tables created successfully") + except Exception as e: + print(f"Error creating tables: {e}") + self.connection.rollback() + + def insert_metadata(self, event_id, subject, subject_type, predicate, object, object_type, sentence, file, doc_source, score): + insert_query = """ + INSERT INTO metadata (event_id, subject, subject_type, predicate, object, object_type, sentence, file, doc_source, score) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id; + """ + try: + with self.connection.cursor() as cursor: + cursor.execute(insert_query, (event_id, subject, subject_type, predicate, object, object_type, sentence, file, doc_source, score)) + metadata_id = cursor.fetchone()[0] + self.connection.commit() + return metadata_id + except Exception as e: + print(f"Error inserting metadata: {e}") + self.connection.rollback() + + def insert_embedding(self,event_id, embeddings): + insert_query = """ + INSERT INTO embedding (event_id, embeddings) + VALUES (%s, %s); + """ + try: + with self.connection.cursor() as cursor: + cursor.execute(insert_query, (event_id, embeddings)) + self.connection.commit() + except Exception as e: + print(f"Error inserting embedding: {e}") + self.connection.rollback() + + def close_connection(self): + if self.connection: + self.connection.close() + print("Database connection closed") + + def find_similar_embeddings(self, sentence_embedding, top_k=3, similarity_threshold=0.9): + # print("Senetence embeddi ---", sentence_embedding) + emb = sentence_embedding + query = f""" + SELECT id, 1 - (embeddings <=> '{emb}') AS cosine_similarity + FROM public.embedding + ORDER BY cosine_similarity DESC + LIMIT {top_k}; + """ + try: + with self.connection.cursor() as cursor: + cursor.execute(query, (sentence_embedding, top_k)) + results = cursor.fetchall() + for result in results: + print("Result -----------", result) + filtered_results = [result for result in results if result[1] >= similarity_threshold] + return filtered_results + except Exception as e: + print(f"Error in finding similar embeddings: {e}") + return [] + + def fetch_metadata_by_ids(self, metadata_ids): + print("metafataaaa ids-----", metadata_ids) + + query = """ + SELECT * FROM public.metadata WHERE id IN %s; + """ + try: + with self.connection.cursor() as cursor: + cursor.execute(query, (tuple(metadata_ids),)) + results = cursor.fetchall() + return results + except Exception as e: + print(f"Error fetching metadata: {e}") + return [] + + +# Usage example +if __name__ == "__main__": + db_manager = DatabaseManager( + dbname="querent_test", + user="querent", + password="querent", + host="localhost", + port="5432" + ) + + db_manager.connect_db() + db_manager.create_tables() + + # # Example data insertion + # metadata_id = db_manager.insert_metadata( + # subject='the_environmental_sciences_department', + # subject_type='i_org', + # predicate='have_be_advocate_clean_energy_use', + # object='dr__emily_stanton', + # object_type='i_per', + # sentence='This is an example sentence.', + # file='example_file', + # doc_source='example_source' + # ) + + # db_manager.insert_embedding( + # subject_emb=[0.1, 0.2, 0.3], # Example vectors + # object_emb=[0.4, 0.5, 0.6], + # predicate_emb=[0.7, 0.8, 0.9], + # sentence_emb=[1.0, 1.1, 1.2], + # metadata_id=metadata_id + # ) + # db_manager.update_database_with_averages() + query_1 = "What is gas injection ?" + # query_1 = "What is eagle ford shale porosity and permiability ?" + # query_1 = "What is austin chalk formation ?" + # query_1 = "What type of source rock does austin chalk reservoir have ?" + # query_1 = "What are some of the important characteristics of Gulf of Mexico basin ?" + # query_1 = "Which wells are producing oil ?" + create_emb = EmbeddingStore() + query_1_emb = create_emb.get_embeddings([query_1])[0] +# Find similar embeddings in the database + similar_embeddings = db_manager.find_similar_embeddings(query_1_emb, top_k=10) + # Extract metadata IDs from the results + metadata_ids = [result[0] for result in similar_embeddings] + +# Fetch metadata for these IDs + # metadata_results = db_manager.fetch_metadata_by_ids(metadata_ids) + # print(metadata_results) + # traverser_bfs_results = db_manager.traverser_bfs(metadata_ids=metadata_ids) + # print(traverser_bfs_results) + # print(db_manager.show_detailed_relationship_paths(traverser_bfs_results)) + # print(db_manager.suggest_queries_based_on_edges(traverser_bfs_results)) + + + ## Second Query + # print("2nd Query ---------------------------------------------------") + # user_choice = [27, 29, 171] + # traverser_bfs_results = db_manager.traverser_bfs(metadata_ids=user_choice) + # print(traverser_bfs_results) + # print(db_manager.show_detailed_relationship_paths(traverser_bfs_results)) + # print(db_manager.suggest_queries_based_on_edges(traverser_bfs_results)) + db_manager.close_connection() \ No newline at end of file From ff1ab494c71751b9044b75b2c7d4199188509d37 Mon Sep 17 00:00:00 2001 From: ngupta10 Date: Wed, 26 Jun 2024 15:06:41 +0530 Subject: [PATCH 2/3] updated readme index --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ee904184..42d40a64 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ The Asynchronous Data Dynamo and Graph Neural Network Catalyst - [Installation](#installation) - [Setup DB](#setup-db) - [Example](#example) + - [Perform Similarity Search](#performing-similarity-search) + - [Graph Traversal](#traversing-the-data) + - [Benefits](#additional-benefits) - [Contributing](#contributing) - [License](#license) From f2019ddbb3b1d42be4131b35a15bc70e4b2b1038 Mon Sep 17 00:00:00 2001 From: ngupta10 Date: Wed, 26 Jun 2024 15:43:35 +0530 Subject: [PATCH 3/3] updated assertion --- tests/workflows/test_multiple_collectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/workflows/test_multiple_collectors.py b/tests/workflows/test_multiple_collectors.py index cc8cc003..aa7f242d 100644 --- a/tests/workflows/test_multiple_collectors.py +++ b/tests/workflows/test_multiple_collectors.py @@ -120,7 +120,7 @@ async def test_multiple_collectors_all_async(): ): messages += 1 counter += 1 - assert counter == 112 + assert counter == 94 assert messages > 0