From 30577dc0ff9ab0abe83153a843ac0867fa5eb938 Mon Sep 17 00:00:00 2001 From: Adityavardhan Agrawal Date: Thu, 14 Nov 2024 23:18:37 -0500 Subject: [PATCH] basic ingestion using unstructured and k-nearest retrieval works --- .gitignore | 23 +++++ __init__.py | 0 base_embedding_model.py | 9 ++ base_parser.py | 9 ++ base_planner.py | 9 ++ base_vector_store.py | 15 ++++ core.py | 171 -------------------------------------- databridge.py | 106 +++++++++++++++++++++++ databridge_uri.py | 61 ++++++++++++++ document.py | 21 +++++ mongo_vector_store.py | 111 +++++++++++++++++++++++++ openai_embedding_model.py | 23 +++++ requirements.txt | 141 +++++++++++++++++++++++++++++++ sample.pdf | Bin 0 -> 69140 bytes sanity_checks/mongo.py | 72 ++++++++++++++++ simple_example.py | 74 +++++++++++++++++ simple_planner.py | 17 ++++ unstructured_parser.py | 72 ++++++++++++++++ 18 files changed, 763 insertions(+), 171 deletions(-) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 base_embedding_model.py create mode 100644 base_parser.py create mode 100644 base_planner.py create mode 100644 base_vector_store.py delete mode 100644 core.py create mode 100644 databridge.py create mode 100644 databridge_uri.py create mode 100644 document.py create mode 100644 mongo_vector_store.py create mode 100644 openai_embedding_model.py create mode 100644 requirements.txt create mode 100644 sample.pdf create mode 100644 sanity_checks/mongo.py create mode 100644 simple_example.py create mode 100644 simple_planner.py create mode 100644 unstructured_parser.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c51b24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Python-related files +*__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +.env +venv/* +ENV/ +dist/ +build/ +*.egg-info/ +.eggs/ +*.egg +*.pytest_cache/ + + +# Virtual environment +.venv/ +.vscode/ + +*.DS_Store diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/base_embedding_model.py b/base_embedding_model.py new file mode 100644 index 0000000..7ea1070 --- /dev/null +++ b/base_embedding_model.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod +from typing import List, Union + + +class BaseEmbeddingModel(ABC): + @abstractmethod + async def embed(self, text: Union[str, List[str]]) -> List[float]: + """Generate embeddings for input text""" + pass diff --git a/base_parser.py b/base_parser.py new file mode 100644 index 0000000..e69af50 --- /dev/null +++ b/base_parser.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, List + + +class BaseParser(ABC): + @abstractmethod + def parse(self, content: str, metadata: Dict[str, Any]) -> List[str]: + """Parse content into chunks""" + pass diff --git a/base_planner.py b/base_planner.py new file mode 100644 index 0000000..ed2d7ad --- /dev/null +++ b/base_planner.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any + + +class BasePlanner(ABC): + @abstractmethod + def plan_retrieval(self, query: str, **kwargs) -> Dict[str, Any]: + """Create execution plan for retrieval""" + pass diff --git a/base_vector_store.py b/base_vector_store.py new file mode 100644 index 0000000..6d2bcbd --- /dev/null +++ b/base_vector_store.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from typing import List +from document import DocumentChunk + + +class BaseVectorStore(ABC): + @abstractmethod + def store_embeddings(self, chunks: List[DocumentChunk]) -> bool: + """Store document chunks and their embeddings""" + pass + + @abstractmethod + def query_similar(self, query_embedding: List[float], k: int, owner_id: str) -> List[DocumentChunk]: + """Find similar chunks""" + pass diff --git a/core.py b/core.py deleted file mode 100644 index 1053d25..0000000 --- a/core.py +++ /dev/null @@ -1,171 +0,0 @@ -from typing import List, Dict, Any, Optional -from abc import ABC, abstractmethod -from datetime import datetime -import uuid - -# Base Classes and Interfaces - -class Document: - def __init__(self, content: str, metadata: Dict[str, Any], owner_id: str): - self.id = str(uuid.uuid4()) - self.content = content - self.metadata = metadata - self.owner_id = owner_id - self.created_at = datetime.utcnow() - self.chunks: List[DocumentChunk] = [] - -class DocumentChunk: - def __init__(self, content: str, embedding: List[float], doc_id: str): - self.id = str(uuid.uuid4()) - self.content = content - self.embedding = embedding - self.doc_id = doc_id - -class BaseParser(ABC): - @abstractmethod - def parse(self, content: str, metadata: Dict[str, Any]) -> List[str]: - """Parse content into chunks""" - pass - - -class BasePlanner(ABC): - @abstractmethod - def plan_retrieval(self, query: str, **kwargs) -> Dict[str, Any]: - """Create execution plan for retrieval""" - pass - - -class BaseVectorStore(ABC): - @abstractmethod - def store_embeddings(self, chunks: List[DocumentChunk]) -> bool: - """Store document chunks and their embeddings""" - pass - - @abstractmethod - def query_similar(self, query_embedding: List[float], k: int, owner_id: str) -> List[DocumentChunk]: - """Find similar chunks""" - pass - -# Concrete Implementations - -class SimpleParser(BaseParser): - def __init__(self, chunk_size: int = 1000, chunk_overlap: int = 200): - self.chunk_size = chunk_size - self.chunk_overlap = chunk_overlap - - def parse(self, content: str, metadata: Dict[str, Any]) -> List[str]: - # Simple implementation - split by chunk_size - chunks = [] - for i in range(0, len(content), self.chunk_size - self.chunk_overlap): - chunk = content[i:i + self.chunk_size] - if chunk: - chunks.append(chunk) - return chunks - -class SimpleRAGPlanner(BasePlanner): - def __init__(self, k: int = 3): - self.k = k - - def plan_retrieval(self, query: str, **kwargs) -> Dict[str, Any]: - return { - "strategy": "simple_rag", - "k": kwargs.get("k", self.k), - "query": query - } - -# Main DataBridge Class - -class DataBridge: - def __init__( - self, - parser: BaseParser, - planner: BasePlanner, - vector_store: BaseVectorStore, - embedding_model: Any # This would be your chosen embedding model - ): - self.parser = parser - self.planner = planner - self.vector_store = vector_store - self.embedding_model = embedding_model - - async def ingest_document( - self, - content: str, - metadata: Dict[str, Any], - owner_id: str - ) -> Document: - # Create document - doc = Document(content, metadata, owner_id) - - # Parse into chunks - chunk_texts = self.parser.parse(content, metadata) - - # Create embeddings and chunks - for chunk_text in chunk_texts: - embedding = await self.embedding_model.embed(chunk_text) - chunk = DocumentChunk(chunk_text, embedding, doc.id) - doc.chunks.append(chunk) - - # Store in vector store - success = self.vector_store.store_embeddings(doc.chunks) - if not success: - raise Exception("Failed to store embeddings") - - return doc - - async def query( - self, - query: str, - owner_id: str, - **kwargs - ) -> List[Dict[str, Any]]: - # Create plan - plan = self.planner.plan_retrieval(query, **kwargs) - - # Get query embedding - query_embedding = await self.embedding_model.embed(query) - - # Execute plan - chunks = self.vector_store.query_similar( - query_embedding, - k=plan["k"], - owner_id=owner_id - ) - - # Format results - results = [] - for chunk in chunks: - results.append({ - "content": chunk.content, - "doc_id": chunk.doc_id, - "chunk_id": chunk.id, - "score": chunk.score if hasattr(chunk, "score") else None - }) - - return results - -# Example usage -""" -# Initialize components -parser = SimpleParser() -planner = SimpleRAGPlanner() -vector_store = YourVectorStore() # Implement with chosen backend -embedding_model = YourEmbeddingModel() # Implement with chosen model - -# Create DataBridge instance -db = DataBridge(parser, planner, vector_store, embedding_model) - -# Ingest a document -doc = await db.ingest_document( - content="Your document content here", - metadata={"source": "pdf", "title": "Example Doc"}, - owner_id="user123" -) - -# Query the system -results = await db.query( - query="Your query here", - owner_id="user123", - k=5 # optional override -) -""" \ No newline at end of file diff --git a/databridge.py b/databridge.py new file mode 100644 index 0000000..2035249 --- /dev/null +++ b/databridge.py @@ -0,0 +1,106 @@ +from typing import Dict, Any, List +from databridge_uri import DataBridgeURI +from document import Document, DocumentChunk +from mongo_vector_store import MongoDBAtlasVectorStore +from openai_embedding_model import OpenAIEmbeddingModel +from unstructured_parser import UnstructuredAPIParser +from simple_planner import SimpleRAGPlanner + + +class DataBridge: + """ + DataBridge with owner authentication and authorization. + Configured via URI containing owner credentials. + """ + + def __init__(self, uri: str): + # Parse URI and initialize configuration + self.config = DataBridgeURI(uri) + + # Initialize components + self._init_components() + + def _init_components(self): + """Initialize all required components using the URI configuration""" + self.embedding_model = OpenAIEmbeddingModel( + api_key=self.config.openai_api_key, + model_name=self.config.embedding_model + ) + + self.parser = UnstructuredAPIParser( + api_key=self.config.unstructured_api_key, + chunk_size=1000, + chunk_overlap=200 + ) + + self.vector_store = MongoDBAtlasVectorStore( + connection_string=self.config.mongo_uri, + database_name=self.config.db_name, + collection_name=self.config.collection_name + ) + + self.planner = SimpleRAGPlanner(default_k=4) + + async def ingest_document( + self, + content: str, + metadata: Dict[str, Any] + ) -> Document: + """ + Ingest a document using the owner ID from the URI configuration. + """ + # Add owner_id to metadata + metadata['owner_id'] = self.config.owner_id + + # Create document + doc = Document(content, metadata, self.config.owner_id) + + # Parse into chunks + chunk_texts = self.parser.parse(content, metadata) + + # Create embeddings and chunks + for chunk_text in chunk_texts: + embedding = await self.embedding_model.embed(chunk_text) + chunk = DocumentChunk(chunk_text, embedding, doc.id) + chunk.metadata = {'owner_id': self.config.owner_id} + doc.chunks.append(chunk) + + # Store in vector store + success = self.vector_store.store_embeddings(doc.chunks) + if not success: + raise Exception("Failed to store embeddings") + + return doc + + async def query( + self, + query: str, + **kwargs + ) -> List[Dict[str, Any]]: + """ + Query the document store using the owner ID from the URI configuration. + """ + # Create plan + plan = self.planner.plan_retrieval(query, **kwargs) + + # Get query embedding + query_embedding = await self.embedding_model.embed(query) + + # Execute plan + chunks = self.vector_store.query_similar( + query_embedding, + k=plan["k"], + owner_id=self.config.owner_id + ) + + # Format results + results = [] + for chunk in chunks: + results.append({ + "content": chunk.content, + "doc_id": chunk.doc_id, + "chunk_id": chunk.id, + "score": chunk.score if hasattr(chunk, "score") else None + }) + + return results diff --git a/databridge_uri.py b/databridge_uri.py new file mode 100644 index 0000000..2af80d0 --- /dev/null +++ b/databridge_uri.py @@ -0,0 +1,61 @@ +from urllib.parse import urlparse, parse_qs +from typing import Optional, Dict, Any +import os +import jwt +from datetime import datetime, timedelta + + +class DataBridgeURI: + """ + Handles parsing and validation of DataBridge URIs with owner authentication + Format: databridge://:@host/path?params + """ + def __init__(self, uri: str): + self.uri = uri + self._parse_uri() + + def _parse_uri(self): + parsed = urlparse(self.uri) + query_params = parse_qs(parsed.query) + + # Parse authentication info from netloc + auth_parts = parsed.netloc.split('@')[0].split(':') + if len(auth_parts) != 2: + raise ValueError("URI must include owner_id and auth_token") + + self.owner_id = auth_parts[0] + self.auth_token = auth_parts[1] + + # Validate and decode auth token + try: + self._validate_auth_token() + except Exception as e: + raise ValueError(f"Invalid auth token: {str(e)}") + + # Get the original MongoDB URI from environment - use it as is + self.mongo_uri = os.getenv("MONGODB_URI") + if not self.mongo_uri: + raise ValueError("MONGODB_URI environment variable not set") + + # Get configuration from query parameters + self.openai_api_key = query_params.get('openai_key', [os.getenv('OPENAI_API_KEY', '')])[0] + self.unstructured_api_key = query_params.get('unstructured_key', [os.getenv('UNSTRUCTURED_API_KEY', '')])[0] + self.db_name = query_params.get('db', ['brandsyncaidb'])[0] + self.collection_name = query_params.get('collection', ['kb_chunked_embeddings'])[0] + self.embedding_model = query_params.get('embedding_model', ['text-embedding-3-small'])[0] + + # Validate required fields + if not all([self.mongo_uri, self.openai_api_key, self.unstructured_api_key]): + raise ValueError("Missing required configuration in DataBridge URI") + + def _validate_auth_token(self): + """Validate the auth token and extract any additional claims""" + try: + decoded = jwt.decode(self.auth_token, 'your-secret-key', algorithms=['HS256']) + if decoded.get('owner_id') != self.owner_id: + raise ValueError("Token owner_id mismatch") + self.auth_claims = decoded + except jwt.ExpiredSignatureError: + raise ValueError("Auth token has expired") + except jwt.InvalidTokenError: + raise ValueError("Invalid auth token") diff --git a/document.py b/document.py new file mode 100644 index 0000000..4b24257 --- /dev/null +++ b/document.py @@ -0,0 +1,21 @@ +from typing import Dict, Any, List +import uuid +from datetime import datetime + + +class Document: + def __init__(self, content: str, metadata: Dict[str, Any], owner_id: str): + self.id = str(uuid.uuid4()) + self.content = content + self.metadata = metadata + self.owner_id = owner_id + self.created_at = datetime.utcnow() + self.chunks: List[DocumentChunk] = [] + + +class DocumentChunk: + def __init__(self, content: str, embedding: List[float], doc_id: str): + self.id = str(uuid.uuid4()) + self.content = content + self.embedding = embedding + self.doc_id = doc_id diff --git a/mongo_vector_store.py b/mongo_vector_store.py new file mode 100644 index 0000000..319007a --- /dev/null +++ b/mongo_vector_store.py @@ -0,0 +1,111 @@ +from typing import List, Dict, Any +from pymongo import MongoClient +from base_vector_store import BaseVectorStore +from document import DocumentChunk + + +class MongoDBAtlasVectorStore(BaseVectorStore): + def __init__( + self, + connection_string: str, + database_name: str, + collection_name: str = "kb_chunked_embeddings", + index_name: str = "vector_index" + ): + self.client = MongoClient(connection_string) + self.db = self.client[database_name] + self.collection = self.db[collection_name] + self.index_name = index_name + + # Ensure vector search index exists + # self._ensure_index() + + def _ensure_index(self): + """Ensure the vector search index exists""" + try: + # Check if index exists + indexes = self.collection.list_indexes() + index_exists = any(index.get('name') == self.index_name for index in indexes) + + if not index_exists: + # Create the vector search index if it doesn't exist + self.collection.create_index( + [("embedding", "vectorSearch")], + name=self.index_name, + vectorSearchOptions={ + "dimensions": 1536, # For OpenAI embeddings + "similarity": "cosine" + } + ) + except Exception as e: + print(f"Warning: Could not create vector index: {str(e)}") + + def store_embeddings(self, chunks: List[DocumentChunk]) -> bool: + try: + documents = [] + for chunk in chunks: + doc = { + "_id": chunk.id, # Use chunk.id as MongoDB _id + "text": chunk.content, + "embedding": chunk.embedding, + "doc_id": chunk.doc_id, + "owner_id": chunk.metadata.get("owner_id"), + "metadata": chunk.metadata + } + documents.append(doc) + + if documents: + # Use ordered=False to continue even if some inserts fail + result = self.collection.insert_many(documents, ordered=False) + return len(result.inserted_ids) > 0 + return True + + except Exception as e: + print(f"Error storing embeddings: {str(e)}") + return False + + def query_similar( + self, + query_embedding: List[float], + k: int, + owner_id: str, + filters: Dict[str, Any] = None + ) -> List[DocumentChunk]: + """Find similar chunks using MongoDB Atlas Vector Search.""" + base_filter = {"owner_id": owner_id} + if filters: + base_filter.update(filters) + + try: + pipeline = [ + { + "$vectorSearch": { + "index": self.index_name, + "path": "embedding", + "queryVector": query_embedding, + "numCandidates": k * 10, + "limit": k, + "filter": base_filter + } + } + ] + + results = list(self.collection.aggregate(pipeline)) + chunks = [] + + for result in results: + chunk = DocumentChunk( + content=result["text"], + embedding=result["embedding"], + doc_id=result["doc_id"] + ) + chunk.score = result.get("score", 0) + # Add metadata back to chunk + chunk.metadata = result.get("metadata", {}) + chunks.append(chunk) + + return chunks + + except Exception as e: + print(f"Error querying similar documents: {str(e)}") + return [] diff --git a/openai_embedding_model.py b/openai_embedding_model.py new file mode 100644 index 0000000..e3176f2 --- /dev/null +++ b/openai_embedding_model.py @@ -0,0 +1,23 @@ +from typing import List, Union +import openai +from base_embedding_model import BaseEmbeddingModel + + +class OpenAIEmbeddingModel(BaseEmbeddingModel): + def __init__(self, api_key: str, model_name: str = "text-embedding-3-small"): + self.client = openai.Client(api_key=api_key) + self.model_name = model_name + + async def embed(self, text: Union[str, List[str]]) -> List[float]: + if isinstance(text, str): + text = [text] + + response = self.client.embeddings.create( + model=self.model_name, + input=text + ) + + if len(text) == 1: + return response.data[0].embedding + + return [item.embedding for item in response.data] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3e33368 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,141 @@ +aiohappyeyeballs==2.4.3 +aiohttp==3.11.2 +aiosignal==1.3.1 +annotated-types==0.7.0 +antlr4-python3-runtime==4.9.3 +anyio==4.6.2.post1 +attrs==24.2.0 +backoff==2.2.1 +beautifulsoup4==4.12.3 +cachetools==5.5.0 +certifi==2024.8.30 +cffi==1.17.1 +chardet==5.2.0 +charset-normalizer==3.4.0 +click==8.1.7 +coloredlogs==15.0.1 +contourpy==1.3.1 +cryptography==43.0.3 +cycler==0.12.1 +dataclasses-json==0.6.7 +Deprecated==1.2.14 +distro==1.9.0 +dnspython==2.7.0 +effdet==0.4.1 +emoji==2.14.0 +eval_type_backport==0.2.0 +filelock==3.16.1 +filetype==1.2.0 +flatbuffers==24.3.25 +fonttools==4.55.0 +frozenlist==1.5.0 +fsspec==2024.10.0 +google-api-core==2.23.0 +google-auth==2.36.0 +google-cloud-vision==3.8.1 +googleapis-common-protos==1.66.0 +grpcio==1.67.1 +grpcio-status==1.67.1 +h11==0.14.0 +html5lib==1.1 +httpcore==1.0.6 +httpx==0.27.2 +huggingface-hub==0.26.2 +humanfriendly==10.0 +idna==3.10 +iopath==0.1.10 +Jinja2==3.1.4 +jiter==0.7.1 +joblib==1.4.2 +jsonpatch==1.33 +jsonpath-python==1.0.6 +jsonpointer==3.0.0 +jwt==1.3.1 +kiwisolver==1.4.7 +langchain==0.3.7 +langchain-core==0.3.18 +langchain-text-splitters==0.3.2 +langdetect==1.0.9 +langsmith==0.1.143 +layoutparser==0.3.4 +lxml==5.3.0 +MarkupSafe==3.0.2 +marshmallow==3.23.1 +matplotlib==3.9.2 +mpmath==1.3.0 +multidict==6.1.0 +mypy-extensions==1.0.0 +nest-asyncio==1.6.0 +networkx==3.4.2 +nltk==3.9.1 +numpy==1.26.4 +olefile==0.47 +omegaconf==2.3.0 +onnx==1.17.0 +onnxruntime==1.20.0 +openai==1.54.4 +opencv-python==4.10.0.84 +orjson==3.10.11 +packaging==24.2 +pandas==2.2.3 +pdf2image==1.17.0 +pdfminer.six==20231228 +pdfplumber==0.11.4 +pi_heif==0.20.0 +pikepdf==9.4.1 +pillow==11.0.0 +portalocker==2.10.1 +propcache==0.2.0 +proto-plus==1.25.0 +protobuf==5.28.3 +psutil==6.1.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 +pycocotools==2.0.8 +pycparser==2.22 +pydantic==2.9.2 +pydantic_core==2.23.4 +PyJWT==2.9.0 +pymongo==4.10.1 +pyparsing==3.2.0 +pypdf==5.1.0 +pypdfium2==4.30.0 +python-dateutil==2.8.2 +python-dotenv==1.0.1 +python-iso639==2024.10.22 +python-magic==0.4.27 +python-multipart==0.0.17 +python-oxmsg==0.0.1 +pytz==2024.2 +PyYAML==6.0.2 +RapidFuzz==3.10.1 +regex==2024.11.6 +requests==2.32.3 +requests-toolbelt==1.0.0 +rsa==4.9 +safetensors==0.4.5 +scipy==1.14.1 +setuptools==75.5.0 +six==1.16.0 +sniffio==1.3.1 +soupsieve==2.6 +SQLAlchemy==2.0.36 +sympy==1.13.1 +tenacity==9.0.0 +timm==1.0.11 +tokenizers==0.20.3 +torch==2.5.1 +torchvision==0.20.1 +tqdm==4.67.0 +transformers==4.46.2 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +tzdata==2024.2 +unstructured==0.16.5 +unstructured-client==0.27.0 +unstructured-inference==0.8.1 +unstructured.pytesseract==0.3.13 +urllib3==2.2.3 +webencodings==0.5.1 +wrapt==1.16.0 +yarl==1.17.1 diff --git a/sample.pdf b/sample.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b8dc38f20a3f4e34b0851ac0858a23c8304dc9d1 GIT binary patch literal 69140 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 v6NFU{WrQ literal 0 HcmV?d00001 diff --git a/sanity_checks/mongo.py b/sanity_checks/mongo.py new file mode 100644 index 0000000..f536036 --- /dev/null +++ b/sanity_checks/mongo.py @@ -0,0 +1,72 @@ +from pymongo import MongoClient +from dotenv import load_dotenv +import os +import datetime + + +def test_mongo_operations(): + # Load environment variables + load_dotenv() + + # Get MongoDB URI from environment variable + mongo_uri = os.getenv("MONGODB_URI") + if not mongo_uri: + raise ValueError("MONGODB_URI environment variable not set") + + try: + # Connect to MongoDB + client = MongoClient(mongo_uri) + + # Test connection + client.admin.command('ping') + print("✅ Connected successfully to MongoDB") + + # Get database and collection + db = client.brandsyncaidb # Using a test database + collection = db.kb_chunked_embeddings + + # Insert a single document + test_doc = { + "name": "Test Document", + "timestamp": datetime.datetime.now(), + "value": 42 + } + + result = collection.insert_one(test_doc) + print(f"✅ Inserted document with ID: {result.inserted_id}") + + # Insert multiple documents + test_docs = [ + {"name": "Doc 1", "value": 1}, + {"name": "Doc 2", "value": 2}, + {"name": "Doc 3", "value": 3} + ] + + result = collection.insert_many(test_docs) + print(f"✅ Inserted {len(result.inserted_ids)} documents") + + # Retrieve documents + print("\nRetrieving documents:") + for doc in collection.find(): + print(f"Found document: {doc}") + + # Find specific documents + print("\nFinding documents with value >= 2:") + query = {"value": {"$gte": 2}} + for doc in collection.find(query): + print(f"Found document: {doc}") + + # Clean up - delete all test documents + # DON'T DELETE IF It'S BRANDSYNCAI + # result = collection.delete_many({}) + print(f"\n✅ Cleaned up {result.deleted_count} test documents") + + except Exception as e: + print(f"❌ Error: {str(e)}") + finally: + client.close() + print("\n✅ Connection closed") + + +if __name__ == "__main__": + test_mongo_operations() diff --git a/simple_example.py b/simple_example.py new file mode 100644 index 0000000..2ce760b --- /dev/null +++ b/simple_example.py @@ -0,0 +1,74 @@ +from datetime import datetime, timedelta, UTC # Note: using UTC for timezone awareness +import base64 +from databridge import DataBridge +import jwt +import os +from dotenv import load_dotenv + + +def create_databridge_uri() -> str: + """Create DataBridge URI from environment variables""" + load_dotenv() + + # Get credentials from environment + mongo_uri = os.getenv("MONGODB_URI") + openai_key = os.getenv("OPENAI_API_KEY") + unstructured_key = os.getenv("UNSTRUCTURED_API_KEY") + owner_id = os.getenv("DATABRIDGE_OWNER", "admin") + + # Validate required credentials + if not all([mongo_uri, openai_key, unstructured_key]): + raise ValueError("Missing required environment variables") + + # Generate auth token + auth_token = jwt.encode( + { + 'owner_id': owner_id, + 'exp': datetime.now(UTC) + timedelta(days=30) + }, + 'your-secret-key', # In production, use proper secret + algorithm='HS256' + ) + + # For DataBridge URI, use any host identifier (it won't affect MongoDB connection) + uri = ( + f"databridge://{owner_id}:{auth_token}@databridge.local" + f"?openai_key={openai_key}" + f"&unstructured_key={unstructured_key}" + f"&db=brandsyncaidb" + f"&collection=kb_chunked_embeddings" + ) + + return uri + + +async def main(): + # Initialize DataBridge + bridge = DataBridge(create_databridge_uri()) + + # Example: Ingest a PDF document + with open("sample.pdf", "rb") as f: + pdf_content = base64.b64encode(f.read()).decode() + + await bridge.ingest_document( + content=pdf_content, + metadata={ + "content_type": "application/pdf", + "is_base64": True, + "title": "Sample PDF" + } + ) + + # Example: Query documents + results = await bridge.query( + query="What is machine learning?", + k=4 + ) + + for result in results: + print(f"Content: {result['content'][:200]}...") + print(f"Score: {result['score']}\n") + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) diff --git a/simple_planner.py b/simple_planner.py new file mode 100644 index 0000000..df59915 --- /dev/null +++ b/simple_planner.py @@ -0,0 +1,17 @@ +from typing import Dict, Any +from base_planner import BasePlanner + + +class SimpleRAGPlanner(BasePlanner): + def __init__(self, default_k: int = 3): + self.default_k = default_k + + def plan_retrieval(self, query: str, **kwargs) -> Dict[str, Any]: + """Create a simple retrieval plan.""" + return { + "strategy": "simple_retrieval", + "k": kwargs.get("k", self.default_k), + "query": query, + "filters": kwargs.get("filters", {}), + "min_score": kwargs.get("min_score", 0.0) + } diff --git a/unstructured_parser.py b/unstructured_parser.py new file mode 100644 index 0000000..25b43eb --- /dev/null +++ b/unstructured_parser.py @@ -0,0 +1,72 @@ +from typing import Dict, Any, List +from base_parser import BaseParser +from unstructured.partition.auto import partition +from langchain.text_splitter import RecursiveCharacterTextSplitter +import os +import tempfile +import base64 + + +class UnstructuredAPIParser(BaseParser): + def __init__( + self, + api_key: str, + chunk_size: int = 1000, + chunk_overlap: int = 200, + api_url: str = "https://api.unstructuredapp.io" + ): + self.api_key = api_key + self.api_url = api_url + self.chunk_size = chunk_size + self.chunk_overlap = chunk_overlap + self.text_splitter = RecursiveCharacterTextSplitter( + chunk_size=chunk_size, + chunk_overlap=chunk_overlap, + length_function=len, + separators=["\n\n", "\n", ". ", " ", ""] + ) + + def parse(self, content: str, metadata: Dict[str, Any]) -> List[str]: + """Parse content using Unstructured API and split into chunks.""" + try: + # Create temporary file for content + with tempfile.NamedTemporaryFile(delete=False, suffix=self._get_file_extension(metadata)) as temp_file: + if metadata.get("is_base64", False): + temp_file.write(base64.b64decode(content)) + else: + temp_file.write(content.encode('utf-8')) + temp_file_path = temp_file.name + + try: + # Use Unstructured API for parsing + elements = partition( + filename=temp_file_path, + api_key=self.api_key, + api_url=self.api_url, + partition_via_api=True + ) + + # Combine elements and split into chunks + full_text = "\n\n".join(str(element) for element in elements) + chunks = self.text_splitter.split_text(full_text) + + return chunks + finally: + # Clean up temporary file + os.unlink(temp_file_path) + + except Exception as e: + raise Exception(f"Error parsing document: {str(e)}") + + def _get_file_extension(self, metadata: Dict[str, Any]) -> str: + """Get appropriate file extension based on content type.""" + content_type_mapping = { + 'application/pdf': '.pdf', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'image/jpeg': '.jpg', + 'image/png': '.png', + 'text/plain': '.txt', + 'text/html': '.html' + } + return content_type_mapping.get(metadata.get('content_type'), '.txt')