Compare commits

...
34 Commits
Author SHA1 Message Date
KaifAhmad1 3968a450a8 chore: release v0.2.5 2026-01-27 22:01:25 +05:30
KaifAhmad1 57d9c2006e feat: enhance Hugging Face integration with robust BYOM support and improved triplet/relation extraction 2026-01-27 21:55:22 +05:30
Mohd Kaif c6496d2193 Update README.md 2026-01-27 21:00:10 +05:30
Mohd Kaif 1812c8141f Update README.md 2026-01-27 20:55:49 +05:30
KaifAhmad1 b6931c45b6 Add sponsor button configuration and update sponsorship section 2026-01-27 16:43:21 +05:30
Mohd Kaif b52fe93182 Update README.md 2026-01-27 16:31:08 +05:30
Mohd Kaif c837cf1859 Update README.md 2026-01-27 16:23:14 +05:30
KaifAhmad1 65ac458b20 Update Readme with Logo Alignment 2026-01-27 16:21:02 +05:30
KaifAhmad1 a3e3b3cc2b Update README: Add Docling, AWS Neptune, and custom ontology support mentions. Remove metrics and accuracy claims. Tone down promotional language. 2026-01-27 16:18:46 +05:30
KaifAhmad1 b89658116d Update README: restructure top sections, add semantic gap explanation, balance emojis, remove traceability mentions 2026-01-27 16:06:45 +05:30
KaifAhmad1 a60a8ffe3b Update README: clarify framework positioning, add high-stakes use cases, and emphasize semantic layer building 2026-01-27 15:44:12 +05:30
Mohd Kaif 072bf92e83 Merge pull request #224 from Hawksight-AI/docs
docs: update CONTRIBUTING.md and CONTRIBUTORS.md with improved format…
2026-01-27 11:48:36 +05:30
KaifAhmad1 91f5a8b15f docs: update CONTRIBUTING.md and CONTRIBUTORS.md with improved formatting and fork mentions 2026-01-27 11:46:34 +05:30
Mohd Kaif 8ded19a2c8 Merge pull request #223 from Hawksight-AI/docs
docs: update CONTRIBUTING.md with improved formatting and documentati…
2026-01-27 11:37:02 +05:30
KaifAhmad1 ca04bfd1e9 docs: update CONTRIBUTING.md with improved formatting and documentation guidelines 2026-01-27 11:35:08 +05:30
Mohd Kaif 73732cfbb8 Merge pull request #222 from Hawksight-AI/docs
Integrate Pinecone Vector Store & Update Docs
2026-01-26 21:48:24 +05:30
KaifAhmad1 37bc3add62 Update documentation and changelog for Pinecone support 2026-01-26 21:44:44 +05:30
KaifAhmad1 5b2ad5e43c Merge branch 'abhiishekk31/main' into pr-fix: Resolve conflicts in Pinecone store implementation
Closes #219
2026-01-26 21:36:09 +05:30
Mohd Kaif 18dd0fbe09 Merge pull request #221 from Hawksight-AI/pr-fix
Pr fix
2026-01-26 21:29:30 +05:30
KaifAhmad1 ebefa61745 Merge branch 'abhiishekk31/main' into pr-fix: Resolve conflicts in Pinecone store implementation 2026-01-26 21:27:46 +05:30
KaifAhmad1 390835ec80 fix: Apply code review fixes for Pinecone integration (PR #220)
- Fix variable shadowing in fetch_vectors (use vector_id instead of id)
- Remove redundant PINECONE_AVAILABLE check in create_index
- Add Pinecone imports and exports to __init__.py
- Add 'pinecone' to SUPPORTED_BACKENDS in vector_store.py
- Add vectorstore-pinecone dependency group to pyproject.toml
- Create vectorstore-all optional dependency group
- Fix duplicate MagicMock import in test_pinecone_store.py
- Update test_pinecone_removal.py with explanatory comment
- Update all docstrings to include Pinecone in supported backends

All fixes address code review feedback and ensure proper integration.
2026-01-26 20:49:29 +05:30
Abhishek Hede 5443a221a0 Added pinecone support with required interface code 2026-01-26 09:53:21 +00:00
Mohd Kaif 6c9497cf40 Merge pull request #218 from Hawksight-AI/semantic-extract
Fix stuck retries in extraction and enable configurable retry limit
2026-01-25 21:33:43 +05:30
KaifAhmad1 bc55dcc57a Fix stuck retries in extraction and enable configurable retry limit. Resolves #207 2026-01-25 21:27:03 +05:30
Mohd Kaif 246119f48a Merge pull request #217 from Hawksight-AI/semantic-extract
Semantic Extraction Module Overhaul (BYOM, RE, Triplet, NER)
2026-01-24 20:59:14 +05:30
KaifAhmad1 b3a239ccb1 feat: enhance semantic extraction with BYOM support, NER aggregation, RE implementation, and Triplet improvements
- Implemented 'Bring Your Own Model' (BYOM) support for NER, Relation, and Triplet extraction
- Added NER aggregation strategies (simple, max, average)
- Implemented Relation Extraction via Sequence Classification with entity markers
- Enhanced Triplet Extraction with REBEL post-processing and lazy loading
- Updated all extractors to prioritize runtime options over config defaults
- Added extensive tests and examples (huggingface_demo.py)
- Updated documentation and CHANGELOG
2026-01-24 20:53:21 +05:30
Mohd Kaif 3c8bc84d18 Update README.md 2026-01-22 18:57:20 +05:30
Mohd Kaif 7f6d0fdcc4 Update README.md 2026-01-22 18:44:52 +05:30
Mohd Kaif 401ef70372 Update README.md 2026-01-22 18:43:30 +05:30
Mohd Kaif 35ce5c9b81 Update README.md 2026-01-22 18:32:57 +05:30
KaifAhmad1 b382a7df6e chore: release version 0.2.4 2026-01-22 12:50:07 +05:30
Mohd Kaif b35081e015 Delete examples/demo_ontology_ingest.py 2026-01-21 18:27:06 +05:30
Mohd Kaif 7459393eea Merge pull request #214 from Hawksight-AI/ontology
feat(ontology): Implement OntologyIngestor and update exports
2026-01-21 13:51:54 +05:30
KaifAhmad1 b96e71ae72 feat(ontology): Implement OntologyIngestor and update exports
- Added OntologyIngestor in semantica/ingest/ontology_ingestor.py
- Updated semantica/ontology/__init__.py to export OntologyIngestor
- Updated semantica/ingest/methods.py to use OntologyIngestor
- Added tests for ontology ingestion
- Cleaned up temporary files
2026-01-21 13:46:46 +05:30
39 changed files with 3796 additions and 707 deletions
+1 -6
View File
@@ -1,8 +1,3 @@
# Funding options for Semantica
# Uncomment and add your usernames/links below
# github: [username]
# patreon: username
# ko_fi: username
# custom: ["https://your-funding-page.com"]
github: Hawksight-AI
+2
View File
@@ -32,6 +32,8 @@ For enterprise support, custom development, or consulting services:
## Sponsorship
### Sponsor this project
Support Semantica development:
- [GitHub Sponsors](https://github.com/sponsors/Hawksight-AI)
+55
View File
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.5] - 2026-01-27
### Added
- **Pinecone Vector Store Support**:
- Implemented native Pinecone support (`PineconeStore`) with full CRUD capabilities.
- Added support for serverless and pod-based indexes, namespaces, and metadata filtering.
- Integrated with `VectorStore` unified interface and registry.
- (Closes #219, Resolves #220)
- **Configurable LLM Retry Logic**:
- Exposed `max_retries` parameter in `NERExtractor`, `RelationExtractor`, `TripletExtractor` and low-level extraction methods (`extract_entities_llm`, `extract_relations_llm`, `extract_triplets_llm`).
- Defaults to 3 retries to prevent infinite loops during JSON validation failures or API timeouts.
- Propagated retry configuration through chunked processing helpers to ensure consistent behavior for long documents.
- Updated `03_Earnings_Call_Analysis.ipynb` to use `max_retries=3` by default.
### Added
- **Bring Your Own Model (BYOM) Support**:
- Enabled full support for custom Hugging Face models in `NERExtractor`, `RelationExtractor`, and `TripletExtractor`.
- Added support for custom tokenizers in `HuggingFaceModelLoader` to handle models with non-standard tokenization requirements.
- Implemented robust fallback logic for model selection: runtime options (`extract(model=...)`) now correctly override configuration defaults.
- **Enhanced NER Implementation**:
- Added configurable aggregation strategies (`simple`, `first`, `average`, `max`) to `extract_entities_huggingface` for better sub-word token handling.
- Implemented robust IOB/BILOU parsing to reconstruct entities from raw model outputs when structured output is unavailable.
- Added confidence scoring for aggregated entities.
- **Relation Extraction Improvements**:
- Implemented standard entity marker technique (wrapping subject/object with `<subj>`, `<obj>` tags) in `extract_relations_huggingface` for compatibility with sequence classification models.
- Added structured output parsing to convert raw model predictions into validated `Relation` objects.
- **Triplet Extraction Completion**:
- Added specialized parsing for Seq2Seq models (e.g., REBEL) in `extract_triplets_huggingface` to generate structured triplets directly from text.
- Implemented post-processing logic to clean and validate generated triplets.
### Fixed
- **LLM Extraction Stability**:
- Fixed infinite retry loops in `BaseProvider` by strictly enforcing `max_retries` limit during structured output generation.
- Resolved stuck execution in earnings call analysis notebooks when using smaller models (e.g., Llama 3 8B) that frequently produce invalid JSON.
- **Model Parameter Precedence**:
- Fixed issue where configuration defaults took precedence over runtime arguments in Hugging Face extractors. Runtime options now correctly override config values.
- **Import Handling**:
- Fixed circular import issues in test suites by implementing robust mocking strategies.
## [0.2.4] - 2026-01-22
### Added
- **Ontology Ingestion Module**:
- Implemented `OntologyIngestor` in `semantica.ingest` for parsing RDF/OWL files (Turtle, RDF/XML, JSON-LD, N3) into standardized `OntologyData` objects.
- Added `ingest_ontology` convenience function and integrated it into the unified `ingest(source_type="ontology")` interface.
- Added recursive directory scanning support for batch ontology ingestion.
- Exposed ingestion tools in `semantica.ontology` for better discoverability.
- Added `OntologyData` dataclass for consistent metadata handling (source path, format, timestamps).
- **Documentation**:
- **Ontology Usage Guide**: Updated `ontology_usage.md` with comprehensive examples for single-file and directory ingestion.
- **API Reference**: Updated `ontology.md` with `OntologyIngestor` class documentation and method details.
- **Tests**:
- **Comprehensive Test Suite**: Added `tests/ingest/test_ontology_ingestor.py` covering all supported formats, error handling, and unified interface integration.
- **Demo Script**: Added `examples/demo_ontology_ingest.py` for end-to-end usage demonstration.
## [0.2.3] - 2026-01-20
### Fixed
+263 -297
View File
@@ -1,306 +1,266 @@
# Contributing to Semantica
Thank you for your interest in contributing to Semantica! This document provides guidelines and instructions for contributing to the project.
Thank you for your interest in contributing! Every contribution, no matter how small, is valuable. 🎉
## Table of Contents
**Give us a Star** • 🍴 **[Fork Semantica](https://github.com/Hawksight-AI/semantica/fork)** • 💬 **Join our [Discord](https://discord.gg/vqRt2qbx)**
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Code Style Guidelines](#code-style-guidelines)
- [Testing Requirements](#testing-requirements)
- [Commit Message Conventions](#commit-message-conventions)
- [Pull Request Process](#pull-request-process)
- [Documentation Standards](#documentation-standards)
- [Types of Contributions](#types-of-contributions)
- [Getting Help](#getting-help)
> **New to contributing?** Start with a [`good first issue`](https://github.com/Hawksight-AI/semantica/labels/good%20first%20issue) or join our [Discord](https://discord.gg/vqRt2qbx) community.
## Code of Conduct
---
This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers.
## 🚀 Quick Start
## Getting Started
1. Find a [`good first issue`](https://github.com/Hawksight-AI/semantica/labels/good%20first%20issue)
2. [Fork Semantica](https://github.com/Hawksight-AI/semantica/fork) & clone the repository
3. Make your changes
4. Submit a pull request!
1. **Fork the repository** on GitHub
2. **Clone your fork** locally:
```bash
git clone https://github.com/your-username/semantica.git
cd semantica
```
3. **Add the upstream remote**:
```bash
git remote add upstream https://github.com/Hawksight-AI/semantica.git
```
**Need help?** Join [Discord](https://discord.gg/vqRt2qbx) or [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
## Development Setup
---
### Prerequisites
## 🎯 Ways to Contribute
- Python 3.8 or higher (3.9+ recommended)
- pip package manager
- Git
### 💻 Code
### Installation
**What you can do:**
- Fix bugs
- Add new features
- Improve code quality (add type hints, docstrings, improve error messages)
- Optimize performance
1. **Create a virtual environment** (recommended):
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
**Where:** `semantica/` directory
2. **Install the project in editable mode with dev dependencies**:
```bash
pip install -e ".[dev]"
```
**Good first issues:** Add docstrings, type hints, or improve error messages
3. **Install pre-commit hooks**:
```bash
pre-commit install
```
---
### Verify Installation
### 📝 Documentation
**What you can do:**
- Fix typos and grammar errors
- Improve clarity and readability
- Add code examples and tutorials
- Create new cookbook notebooks
- Improve API documentation (docstrings)
- Create troubleshooting guides
- Update installation instructions
- Add missing documentation
**Where:** `README.md`, `docs/`, `cookbook/`, docstrings in code
**Good first issues:** Fix typos, add examples, create cookbook tutorials, improve docstrings
**Documentation formatting:**
- Use clear, concise language
- Include code examples where helpful
- Follow markdown best practices
- Use proper headings hierarchy
- Add links to related sections
- Include screenshots for UI-related docs
---
### 🧪 Testing
**What you can do:**
- Add unit tests
- Improve test coverage
- Add integration tests
**Where:** `tests/` directory
**Good first issues:** Add tests for specific functions or classes
---
### 🐛 Bug Reports
**What:** Report bugs you find
**How:** Use the [bug report template](https://github.com/Hawksight-AI/semantica/issues/new?template=bug_report.md)
**Include:** Description, steps to reproduce, expected vs actual behavior, environment details
---
### 💡 Feature Requests
**What:** Suggest new features or improvements
**How:** Use the [feature request template](https://github.com/Hawksight-AI/semantica/issues/new?template=feature_request.md)
**Include:** Problem statement, proposed solution, use cases
---
### 🎨 Cookbook & Examples
**What:** Create tutorials and examples
**Where:** `cookbook/` directory
**Examples:** Create new notebooks, add examples, improve existing tutorials
---
### 💬 Community Support
**What:** Help others in the community
**Where:** [Discord](https://discord.gg/vqRt2qbx), [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
**Examples:** Answer questions, review PRs, share your projects
---
### 🎓 Educational Content
**What:** Create educational materials
**Examples:** Blog posts, video tutorials, talks, workshops, case studies
---
### 🔧 Other Contributions
- **Design & Graphics:** Logos, diagrams, visualizations
- **Tools & Integrations:** CLI tools, integrations with other frameworks
- **Infrastructure:** CI/CD improvements, Docker optimization
- **Security:** Report security vulnerabilities (privately)
---
## 📋 Getting Started
### 1. Fork & Clone
First, [fork Semantica](https://github.com/Hawksight-AI/semantica/fork) on GitHub, then:
```bash
python -c "import semantica; print(semantica.__version__)"
pytest --version
black --version
git clone https://github.com/your-username/semantica.git
cd semantica
git remote add upstream https://github.com/Hawksight-AI/semantica.git
```
## Code Style Guidelines
We use several tools to maintain code quality and consistency:
### Formatting
- **Black**: Code formatting (line length: 88)
```bash
black semantica/
```
- **isort**: Import sorting
```bash
isort semantica/
```
### Linting
- **flake8**: Style guide enforcement
```bash
flake8 semantica/
```
- **mypy**: Static type checking
```bash
mypy semantica/
```
### Running All Checks
### 2. Set Up Environment
```bash
# Format code
black semantica/ tests/
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Sort imports
isort semantica/ tests/
# Install dev dependencies
pip install -e ".[dev]"
# Lint
flake8 semantica/ tests/
# Type check
mypy semantica/
# Install pre-commit hooks (optional)
pre-commit install
```
Or use pre-commit hooks (automatically runs on commit):
```bash
pre-commit run --all-files
```
## Testing Requirements
### Running Tests
### 3. Create Branch
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=semantica --cov-report=html
# Run specific test file
pytest tests/test_specific.py
# Run with verbose output
pytest -v
git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description
```
### Test Coverage
### 4. Make Changes
- Minimum coverage: **80%**
- Critical modules: **90%+**
- Coverage reports are generated in `htmlcov/`
- Follow code style (see below)
- Add tests for new features
- Update documentation
### Writing Tests
### 5. Run Checks
- Follow pytest conventions
- Use descriptive test names
- Include docstrings for complex tests
- Test both success and failure cases
- Use fixtures for common setup
Example:
```python
def test_entity_extraction():
"""Test basic entity extraction functionality."""
from semantica.semantic_extract import NamedEntityRecognizer
ner = NamedEntityRecognizer()
entities = ner.extract("Apple Inc. was founded by Steve Jobs.")
assert len(entities) > 0
assert any(e.text == "Apple Inc." for e in entities)
```bash
pytest # Run tests
black semantica/ tests/ # Format code
isort semantica/ tests/ # Sort imports
flake8 semantica/ tests/ # Lint
```
## Commit Message Conventions
Or use pre-commit hooks: `pre-commit run --all-files`
We follow [Conventional Commits](https://www.conventionalcommits.org/) specification:
### 6. Commit & Push
### Format
```
<type>(<scope>): <subject>
<body>
<footer>
```bash
git commit -m "feat(module): add new feature"
git push origin feature/your-feature-name
```
### Types
Then create a pull request on GitHub!
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Code style changes (formatting, etc.)
- `refactor`: Code refactoring
- `test`: Adding or updating tests
- `chore`: Maintenance tasks
- `perf`: Performance improvements
- `ci`: CI/CD changes
---
### Examples
## 📐 Code Style
We use automated tools:
| Tool | Purpose | Command |
|----------|----------------------------|----------------------------|
| **Black** | Code formatting | `black semantica/ tests/` |
| **isort** | Import sorting | `isort semantica/ tests/` |
| **flake8** | Style enforcement | `flake8 semantica/ tests/` |
| **mypy** | Type checking | `mypy semantica/` |
**Run all:** `black semantica/ tests/ && isort semantica/ tests/ && flake8 semantica/ tests/ && mypy semantica/`
---
## 🧪 Testing
```bash
pytest # Run all tests
pytest --cov=semantica # With coverage
pytest tests/test_file.py # Specific file
```
**Coverage goal:** 80% minimum, 90%+ for critical modules
---
## 📝 Commit Messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
feat(kg): add temporal graph support
Add support for temporal knowledge graphs with version tracking
and time-based queries.
Closes #123
fix(parse): handle empty PDF files
docs(readme): add installation guide
test(extract): add unit tests
```
```
fix(parse): handle empty PDF files gracefully
**Types:** `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `style`, `chore`
Previously, empty PDF files would cause a crash. Now they return
an empty document with appropriate warnings.
---
Fixes #456
```
## ✅ PR Checklist
## Pull Request Process
### Before Submitting
1. **Update your fork**:
```bash
git fetch upstream
git checkout main
git merge upstream/main
```
2. **Create a feature branch**:
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description
```
3. **Make your changes** and commit following our conventions
4. **Run all checks**:
```bash
pytest
black semantica/ tests/
isort semantica/ tests/
flake8 semantica/ tests/
mypy semantica/
```
5. **Push to your fork**:
```bash
git push origin feature/your-feature-name
```
### PR Checklist
Before submitting:
- [ ] Code follows style guidelines
- [ ] Tests pass locally
- [ ] New tests added for new features
- [ ] New tests added (if applicable)
- [ ] Documentation updated
- [ ] Commit messages follow conventions
- [ ] No merge conflicts
- [ ] PR description is clear and complete
### PR Description Template
---
```markdown
## Description
Brief description of changes
## 📖 Documentation Standards
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
### Code Documentation (Docstrings)
## Related Issues
Closes #123
Related to #456
**Format:** Use Google-style docstrings
## Testing
- [ ] Tests pass locally
- [ ] Added new tests
- [ ] Updated existing tests
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex code
- [ ] Documentation updated
- [ ] No new warnings generated
```
## Documentation Standards
### Code Documentation
- Use Google-style docstrings
- Include type hints
- Document all public functions and classes
- Include examples for complex functions
Example:
```python
def extract_entities(
text: str,
model: str = "transformer",
confidence_threshold: float = 0.7
) -> List[Entity]:
def extract_entities(text: str, model: str = "transformer") -> List[Entity]:
"""Extract named entities from text.
Args:
text: Input text to process
model: NER model to use (default: "transformer")
confidence_threshold: Minimum confidence score (default: 0.7)
Returns:
List of extracted Entity objects
@@ -309,92 +269,98 @@ def extract_entities(
ValueError: If text is empty or model is invalid
Example:
>>> ner = NamedEntityRecognizer()
>>> from semantica.semantic_extract import NERExtractor
>>> ner = NERExtractor(method="ml", model="en_core_web_sm")
>>> entities = ner.extract("Apple Inc. was founded in 1976.")
>>> len(entities)
2
"""
...
```
### Documentation Files
### Markdown Documentation Formatting
- Update relevant documentation in `docs/`
- Add examples to cookbook if applicable
- Update API reference if adding new public APIs
- Keep README.md up to date
**General Guidelines:**
- Use clear headings (H1 for title, H2 for main sections, H3 for subsections)
- Keep paragraphs short and focused
- Use bullet points for lists
- Add code blocks with syntax highlighting
- Include links to related documentation
## Types of Contributions
**Code Blocks:**
- Use triple backticks with language identifier: ` ```python `, ` ```bash `
- Include comments in code examples
- Show expected output when helpful
### 💻 Code Contributions
**Examples:**
- **Bug Fixes**: Resolving issues reported in the issue tracker.
- **New Features**: Implementing new capabilities (please discuss via an issue first!).
- **Refactoring**: Improving code structure and maintainability without changing behavior.
- **Algorithm Optimization**: Improving the efficiency of graph algorithms and vector search.
```markdown
## Section Title
#### ⚡ Performance and Latency
We deeply value efficiency. Contributions that make Semantica faster and lighter are highly appreciated!
Brief introduction paragraph.
- **Latency Reduction**: Optimize critical paths and RAG pipeline response times.
- **Memory Optimization**: Reduce graph/vector processing memory footprint.
- **Throughput**: Improve operations per second (bulk ingestion, parallel queries).
- **Benchmarks**: Add performance benchmarks to track regressions.
- **Async/Concurrency**: Enhance asynchronous execution and concurrency.
### Subsection
### 📚 Documentation Contributions
- Bullet point 1
- Bullet point 2
- Fix typos and grammar
- Improve clarity
- Add examples
- Create tutorials
- Translate documentation
**Code example:**
### Testing Contributions
```python
from semantica import SomeClass
- Add test coverage
- Improve test quality
- Add integration tests
- Performance benchmarks
instance = SomeClass()
result = instance.method()
```
### Other Contributions
**Note:** Additional context or warnings.
```
- Answer questions in discussions
- Help with issues
- Review pull requests
- Share use cases
- Report bugs
- Suggest features
**Best Practices:**
- Start with an overview/introduction
- Use consistent terminology
- Include "See also" links
- Add examples for complex concepts
- Keep formatting consistent across docs
## Getting Help
---
### Communication Channels
## 🆘 Getting Help
- **GitHub Discussions**: General questions and discussions
- **GitHub Issues**: Bug reports and feature requests
- **Discord**: Real-time chat and community support
- 💬 [Discord](https://discord.gg/vqRt2qbx) - Real-time chat
- 💭 [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions) - Q&A
- 🐛 [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) - Bug reports
### Before Asking for Help
**Before asking:** Check existing documentation, search issues/discussions, review cookbook examples
1. Check existing documentation
2. Search GitHub issues and discussions
3. Review code examples in cookbook
4. Check FAQ in documentation
---
### Asking Good Questions
## 🏆 Recognition
- Provide context and environment details
- Include code examples
- Show what you've tried
- Include error messages and logs
- Be specific about what you need
## Recognition
Contributors are recognized in:
All contributors are recognized in:
- [CONTRIBUTORS.md](CONTRIBUTORS.md)
- GitHub contributors page
- Release notes for significant contributions
- Release notes
Thank you for contributing to Semantica! 🎉
We follow the [all-contributors](https://allcontributors.org) specification!
---
## 📜 Code of Conduct
This project follows a [Code of Conduct](CODE_OF_CONDUCT.md). Be respectful and inclusive.
---
## 📚 Resources
- [README.md](README.md) - Project overview
- [Cookbook](cookbook/) - Tutorials and examples
- [Documentation](docs/) - Comprehensive guides
---
**Thank you for contributing!** 🚀
Every contribution matters - whether it's a single line of code, a typo fix, a helpful answer, or a bug report. We appreciate you! 🙏
**Give us a Star** • 🍴 **[Fork Semantica](https://github.com/Hawksight-AI/semantica/fork)** • 💬 **Join our [Discord](https://discord.gg/vqRt2qbx)**
+65 -48
View File
@@ -4,44 +4,31 @@ Thank you to all the people who have contributed to Semantica! 🎉
This project follows the [all-contributors](https://allcontributors.org) specification. Contributions of any kind are welcome!
## How to Contribute
**Give us a Star** • 🍴 **Fork us** • 💬 **Join our [Discord](https://discord.gg/vqRt2qbx)**
We welcome contributions of all kinds! Whether you're:
- Writing code
- Improving documentation
- Reporting bugs
- Suggesting features
- Answering questions
- Reviewing pull requests
- Sharing use cases
- Creating examples
All contributions are valuable and appreciated!
---
## Contribution Types
We recognize all types of contributions:
- 💻 **Code**: Writing code, fixing bugs, implementing features
- 📝 **Documentation**: Writing docs, tutorials, examples
- 🧪 **Testing**: Writing tests, improving test coverage
- 🐛 **Bug Reports**: Finding and reporting bugs
- 💡 **Ideas**: Suggesting new features or improvements
- 🎨 **Design**: UI/UX improvements, graphics, branding
- 📖 **Examples**: Creating code examples and tutorials
- 🔍 **Testing**: Writing tests, improving test coverage
- 💬 **Answering Questions**: Helping others in discussions
- 📢 **Talks**: Giving talks, presentations, workshops
- 🌍 **Translation**: Translating documentation
- 🎨 **Cookbook**: Creating tutorials and examples
- 💬 **Community**: Answering questions, reviewing PRs
- 🎓 **Education**: Blog posts, video tutorials, talks, workshops
- 🔧 **Tools**: Creating tools, scripts, integrations
- 📦 **Packaging**: Improving build, release, distribution
- ⚠️ **Security**: Reporting security vulnerabilities
- 🎓 **Education**: Teaching, mentoring, tutorials
- 📹 **Video**: Creating video content, tutorials
- 🎵 **Audio**: Podcasts, audio content
- 📸 **Photography**: Screenshots, images
- 🔬 **Research**: Research, analysis, studies
- 💰 **Financial**: Sponsoring, funding
- 🏗️ **Infrastructure**: CI/CD, hosting, infrastructure
- 🚇 **Maintenance**: Maintenance, triage, project management
---
## Contributors
<!-- ALL-CONTRIBUTORS-LIST:START -->
@@ -50,48 +37,78 @@ All contributions are valuable and appreciated!
<!-- ALL-CONTRIBUTORS-LIST:END -->
---
## Recognition
### Top Contributors
All contributors are recognized in:
Contributors are recognized based on their contributions to the project. Recognition includes:
- This contributors list
- [GitHub contributors page](https://github.com/Hawksight-AI/semantica/graphs/contributors)
- Release notes for significant contributions
- Community appreciation
- Listing in this file
- GitHub contributor statistics
- Special mentions in release notes
- Featured showcases for significant contributions
### Hall of Fame
Special recognition for exceptional contributions:
- **Coming soon** - We'll feature outstanding contributors here!
---
## How to Add Yourself
If you've contributed to Semantica and want to be added to this list:
### Automatic Recognition
1. **Automatic**: If you've made a commit, you'll appear in [GitHub's contributors graph](https://github.com/Hawksight-AI/semantica/graphs/contributors)
2. **Manual**: Open a PR adding yourself to this file, or use the [@all-contributors bot](https://allcontributors.org/docs/en/bot/usage)
If you've made a commit, you'll automatically appear in [GitHub's contributors graph](https://github.com/Hawksight-AI/semantica/graphs/contributors).
Example:
```markdown
- [Your Name](https://github.com/yourusername) - 💻 📝 🐛
```
### Using All-Contributors Bot
## All Contributors Bot
We use the [all-contributors](https://allcontributors.org) bot to automatically recognize contributors. To add a contributor, comment on an issue or PR:
Comment on any issue or PR with:
```
@all-contributors please add @username for code, docs, bug
```
## Thank You!
**Examples:**
Every contribution, no matter how small, helps make Semantica better. Thank you for being part of our community!
```
@all-contributors please add @johndoe for code
@all-contributors please add @janedoe for docs, bug
@all-contributors please add @devuser for code, test, maintenance
```
### Manual Addition
Open a PR adding yourself to this file:
```markdown
- [Your Name](https://github.com/yourusername) - 💻 📝 🐛
```
---
**Want to contribute?** Check out our [Contributing Guide](CONTRIBUTING.md) to get started!
## Contribution Type Codes
When using the all-contributors bot, use these codes:
- `code` - Code contributions
- `doc` - Documentation
- `test` - Testing
- `bug` - Bug reports
- `ideas` - Feature requests/ideas
- `design` - Design work
- `example` - Cookbook/examples
- `question` - Answering questions
- `talk` - Talks/presentations
- `tool` - Tools/integrations
- `packaging` - Packaging/distribution
- `security` - Security reports
- `infra` - Infrastructure
- `maintenance` - Maintenance
See [all-contributors specification](https://allcontributors.org/docs/en/emoji-key) for complete list.
---
## Thank You!
Every contribution, no matter how small, helps make Semantica better. Thank you for being part of our community! 🙏
**Want to contribute?**
⭐ Give us a Star • 🍴 [Fork us](https://github.com/Hawksight-AI/semantica/fork) • Check out our [Contributing Guide](CONTRIBUTING.md) to get started!
+201 -170
View File
@@ -1,204 +1,236 @@
<div align="center">
<img src="semantica_logo.png" alt="Semantica Logo" width="450" height="auto">
<img src="semantica_logo.png" alt="Semantica Logo" width="460"/>
# 🧠 Semantica
### Open-Source Semantic Layer & Knowledge Engineering Framework
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PyPI version](https://img.shields.io/pypi/v/semantica.svg)](https://pypi.org/project/semantica/)
[![PyPI](https://img.shields.io/pypi/v/semantica.svg)](https://pypi.org/project/semantica/)
[![Monthly Downloads](https://img.shields.io/pypi/dm/semantica)](https://pypi.org/project/semantica/)
[![Total Downloads](https://static.pepy.tech/badge/semantica)](https://pepy.tech/project/semantica)
[![Discord](https://img.shields.io/badge/Discord-Join%20Us-7289da?style=flat&logo=discord&logoColor=white)](https://discord.gg/pMHguUzG)
[![CI](https://github.com/Hawksight-AI/semantica/workflows/CI/badge.svg)](https://github.com/Hawksight-AI/semantica/actions)
[![Discord](https://img.shields.io/badge/Discord-Join-7289da?logo=discord&logoColor=white)](https://discord.gg/RgaGTj9J)
<p align="center">
<a href="https://github.com/Hawksight-AI/semantica/stargazers">
<img src="https://img.shields.io/badge/Give%20a%20Star-%E2%AD%90-yellow?style=for-the-badge&labelColor=555555" alt="Give a Star">
</a>
&nbsp;&nbsp;
<a href="https://github.com/Hawksight-AI/semantica/fork">
<img src="https://img.shields.io/badge/Support%20Project-Fork%20Us-blue?style=for-the-badge&labelColor=555555" alt="Support Project">
</a>
</p>
### ⭐ Give us a Star • 🍴 Fork us • 💬 Join our Discord
**Open Source Framework for Semantic Layer & Knowledge Engineering**
> **Transform chaotic data into intelligent knowledge.**
*The missing fabric between raw data and AI engineering. A comprehensive open-source framework for building semantic layers and knowledge engineering systems that transform unstructured data into AI-ready knowledge — powering Knowledge Graph-Powered RAG (GraphRAG), AI Agents, Multi-Agent Systems, and AI applications with structured semantic knowledge.*
**100% Open Source****MIT Licensed****Latest Version: 0.2.3****Production Ready****Community Driven**
[**Discord**](https://discord.gg/pMHguUzG)
> **Transform Choas into Intelligence. Build AI systems that are explainable, traceable, and trustworthy — not black boxes.**
</div>
## What is Semantica?
Semantica bridges the gap between raw data chaos and AI-ready knowledge. It's a **semantic intelligence platform** that transforms unstructured data into structured, queryable knowledge graphs powering GraphRAG, AI agents, and multi-agent systems.
### What Makes Semantica Different?
Unlike traditional approaches that process isolated documents and extract text into vectors, Semantica understands **semantic relationships across all content**, provides **automated ontology generation**, and builds a **unified semantic layer** with **production-grade QA**.
| **Traditional Approaches** | **Semantica's Approach** |
|:---------------------------|:-------------------------|
| Process data as isolated documents | Understands semantic relationships across all content |
| Extract text and store vectors | Builds knowledge graphs with meaningful connections |
| Generic entity recognition | General-purpose ontology generation and validation |
| Manual schema definition | Automatic semantic modeling from content patterns |
| Disconnected data silos | Unified semantic layer across all data sources |
| Basic quality checks | Production-grade QA with conflict detection & resolution |
---
## 🎯 The Problem We Solve
## 🚀 Why Semantica?
### The Semantic Gap
**Semantica** bridges the **semantic gap** between text similarity and true meaning. It's the **semantic intelligence layer** that makes your AI agents auditable, explainable, and compliant.
Organizations today face a **fundamental mismatch** between how data exists and how AI systems need it.
#### The Semantic Gap: Problem vs. Solution
Organizations have **unstructured data** (PDFs, emails, logs), **messy data** (inconsistent formats, duplicates, conflicts), and **disconnected silos** (no shared context, missing relationships). AI systems need **clear rules** (formal ontologies), **structured entities** (validated, consistent), and **relationships** (semantic connections, context-aware reasoning).
| **What Organizations Have** | **What AI Systems Require** |
|:------------------------------|:------------------------------|
| **Unstructured Data** | **Clear Rules** |
| PDFs, emails, logs | Formal ontologies |
| Mixed schemas | Graphs & Networks |
| Conflicting facts | |
| **Messy, Noisy Data** | **Structured Entities** |
| Inconsistent formats | Validated entities |
| Duplicate records | Domain Knowledge |
| Missing relationships | |
| **Disconnected, Siloed Data** | **Relationships** |
| Data in separate systems | Semantic connections |
| No shared context | Context-Aware Reasoning |
| Isolated knowledge | |
### **SEMANTICA FRAMEWORK**
Semantica operates through three integrated layers that transform raw data into AI-ready knowledge:
**Input Layer** — Universal ingestion from multiple data formats (PDFs, DOCX, HTML, JSON, CSV, databases, live feeds, APIs, streams, archives, multi-modal content) into a unified pipeline.
**Semantic Layer** — Core intelligence engine performing entity extraction, relationship mapping, ontology generation, context engineering, and quality assurance. Includes **advanced entity deduplication** (Jaro-Winkler, disjoint property handling) to ensure a clean single source of truth.
**Output Layer** — Production-ready knowledge graphs, vector embeddings, and validated ontologies that power GraphRAG systems, AI agents, and multi-agent systems.
**Powers: GraphRAG, AI Agents, Multi-Agent Systems**
### What Happens Without Semantics?
**They Break** — Systems crash due to inconsistent formats and missing structure.
**They Hallucinate** — AI models generate false information without semantic context to validate outputs.
**They Fail Silently** — Systems return wrong answers without warnings, leading to bad decisions.
**Why?** Systems have data — not semantics. They can't connect concepts, understand relationships, validate against domain rules, or detect conflicts.
Perfect for **high-stakes domains** where mistakes have real consequences.
---
## 💡 The Semantica Solution
### ⚡ Get Started in 30 Seconds
**Semantica** is an **open-source framework** that closes the semantic gap between real-world messy data and the structured semantic layers required by advanced AI systems — GraphRAG, agents, multi-agent systems, reasoning models, and more.
```bash
pip install semantica
```
### How Semantica Solves These Problems
```python
from semantica.semantic_extract import NERExtractor
from semantica.kg import GraphBuilder
**Efficient Embeddings** — Uses **FastEmbed** by default for high-performance, lightweight local embedding generation (faster than sentence-transformers).
# Extract entities and build knowledge graph
ner = NERExtractor(method="ml", model="en_core_web_sm")
entities = ner.extract("Apple Inc. was founded by Steve Jobs in 1976.")
kg = GraphBuilder().build({"entities": entities, "relationships": []})
**Universal Data Ingestion** — Handles multiple formats (PDF, DOCX, HTML, JSON, CSV, databases, APIs, streams) with unified pipeline, no custom parsers needed.
print(f"Built KG with {len(kg.get('entities', []))} entities")
```
**Automated Semantic Extraction** — NER, relationship extraction, and triplet generation with LLM enhancement. Includes **auto-chunking** for long documents and **robust error handling** with automatic retry logic.
**Knowledge Graph Construction** — Production-ready graphs with entity resolution, temporal support, and graph analytics. Queryable knowledge ready for AI applications.
**GraphRAG Engine** — Hybrid vector + graph retrieval achieves 91% accuracy (30% improvement) via semantic search + graph traversal for multi-hop reasoning. Features LLM-generated responses grounded in knowledge graph context with reasoning traces. [See Comparison Benchmark](cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
**AI Agent Context Engineering** — Persistent memory with RAG + knowledge graphs enables context maintenance, action validation, and structured knowledge access.
**Automated Ontology Generation** — 6-stage LLM pipeline generates validated OWL ontologies with HermiT/Pellet validation, eliminating manual engineering.
**Production-Grade QA** — Conflict detection, deduplication, quality scoring, and provenance tracking ensure trusted, production-ready knowledge graphs.
**Pipeline Orchestration** — Flexible pipeline builder with parallel execution enables scalable processing via orchestrator-worker pattern.
### Core Features at a Glance
| **Feature Category** | **Capabilities** | **Key Benefits** |
|:---------------------|:-----------------|:------------------|
| **Data Ingestion** | Multiple formats (PDF, DOCX, HTML, JSON, CSV, databases, APIs, streams, archives) | Universal ingestion, no custom parsers needed |
| **Semantic Extraction** | NER, relations, triplets, LLM enhancement, **auto-chunking** | Automated discovery with robust error handling |
| **Knowledge Graphs** | Entity resolution, temporal support, graph analytics, query interface | Production-ready, queryable knowledge structures |
| **Ontology Generation** | 6-stage LLM pipeline, OWL generation, HermiT/Pellet validation | Automated ontology creation from documents |
| **GraphRAG** | Hybrid vector + graph retrieval, multi-hop reasoning, LLM-generated responses | 91% accuracy, 30% improvement over vector-only, reasoning traces |
| **LLM Providers** | Unified interface to 100+ LLMs (Groq, OpenAI, HuggingFace, LiteLLM) | Clean imports, multiple providers, structured output |
| **Agent Memory** | Persistent memory (Save/Load), Hybrid Retrieval (Vector+Graph), FastEmbed support | Context-aware agents with semantic understanding |
| **Pipeline Orchestration** | Parallel execution, custom steps, orchestrator-worker pattern | Scalable, flexible data processing |
| **Quality Assurance** | Conflict detection, deduplication, quality scoring, provenance | Trusted knowledge graphs ready for production |
**[📖 Full Quick Start](#-quick-start)** • **[🍳 Cookbook Examples](#-semantica-cookbook)** • **[💬 Join Discord](https://discord.gg/RgaGTj9J)** • **[⭐ Star Us](https://github.com/Hawksight-AI/semantica)**
---
## 👥 Who Is This For?
## Core Value Proposition
Semantica is designed for **developers, data engineers, and organizations** building the next generation of AI applications that require semantic understanding and knowledge graphs.
| **Trustworthy** | **Explainable** | **Auditable** |
|:------------------:|:------------------:|:-----------------:|
| Conflict detection & validation | Transparent reasoning paths | Complete provenance tracking |
| Rule-based governance | Entity relationships & ontologies | Source-level provenance |
| Production-grade QA | Multi-hop graph reasoning | Audit-ready compliance |
### Who Uses Semantica
---
**AI/ML Engineers & Data Scientists** — Build GraphRAG systems, AI agents, and multi-agent systems.
## Key Features & Benefits
**Data Engineers** — Build scalable pipelines with semantic enrichment.
### Not Just Another Agentic Framework
**Knowledge Engineers & Ontologists** — Create knowledge graphs and ontologies with automated pipelines.
**Semantica complements** LangChain, LlamaIndex, AutoGen, CrewAI, Google ADK, Agno, and other frameworks to enhance your agents with:
**Enterprise Data Teams** — Unify semantic layers, improve data quality, resolve conflicts.
| Feature | Benefit |
|:--------|:--------|
| **Auditable** | Complete provenance tracking with full audit trails |
| **Explainable** | Transparent reasoning paths with entity relationships |
| **Provenance-Aware** | Source-level provenance from documents to responses |
| **Validated** | Built-in conflict detection, deduplication, QA |
| **Governed** | Rule-based validation and semantic consistency |
**Software & DevOps Engineers** — Build semantic APIs and infrastructure with production-ready SDK.
### Perfect For High-Stakes Use Cases
**Analysts & Researchers** — Transform data into queryable knowledge graphs for insights.
| 🏥 **Healthcare** | 💰 **Finance** | ⚖️ **Legal** |
|:-----------------:|:--------------:|:------------:|
| Clinical decisions | Fraud detection | Evidence-backed research |
| Drug interactions | Regulatory compliance | Contract analysis |
| Patient safety | Risk assessment | Case law reasoning |
**Security & Compliance Teams** — Threat intelligence, regulatory reporting, audit trails.
| 🔒 **Cybersecurity** | 🏛️ **Government** | 🏭 **Infrastructure** | 🚗 **Autonomous** |
|:-------------------:|:----------------:|:-------------------:|:-----------------:|
| Threat attribution | Policy decisions | Power grids | Decision logs |
| Incident response | Classified info | Transportation | Safety validation |
**Product Teams & Startups** — Rapid prototyping of AI products and semantic features.
### Powers Your AI Stack
- **GraphRAG Systems** — Retrieval with graph reasoning and hybrid search
- **AI Agents** — Trustworthy, accountable multi-agent systems with semantic memory
- **Reasoning Models** — Explainable AI decisions with reasoning paths
- **Enterprise AI** — Governed, auditable platforms for compliance
### Integrations
- **Docling Support** — Document parsing with table extraction (PDF, DOCX, PPTX, XLSX)
- **AWS Neptune** — Amazon Neptune graph database support with IAM authentication
- **Custom Ontology Import** — Import existing ontologies (OWL, RDF, Turtle, JSON-LD)
> **Built for environments where every answer must be explainable and governed.**
---
## 🚨 The Problem: The Semantic Gap
### Most AI systems fail in high-stakes domains because they operate on **text similarity**, not **meaning**.
### Understanding the Semantic Gap
The **semantic gap** is the fundamental disconnect between what AI systems can process (text patterns, vector similarities) and what high-stakes applications require (semantic understanding, meaning, context, and relationships).
**Traditional AI approaches:**
- Rely on statistical patterns and text similarity
- Cannot understand relationships between entities
- Cannot reason about domain-specific rules
- Cannot explain why decisions were made
- Cannot trace back to original sources with confidence
**High-stakes AI requires:**
- Semantic understanding of entities and their relationships
- Domain knowledge encoded as formal rules (ontologies)
- Explainable reasoning paths
- Source-level provenance
- Conflict detection and resolution
**Semantica bridges this gap** by providing a semantic intelligence layer that transforms unstructured data into validated, explainable, and auditable knowledge.
### What Organizations Have vs What They Need
| **Current State** | **Required for High-Stakes AI** |
|:---------------------|:-----------------------------------|
| PDFs, DOCX, emails, logs | Formal domain rules (ontologies) |
| APIs, databases, streams | Structured and validated entities |
| Conflicting facts and duplicates | Explicit semantic relationships |
| Siloed systems with no lineage | **Explainable reasoning paths** |
| | **Source-level provenance** |
| | **Audit-ready compliance** |
### The Cost of Missing Semantics
- **Decisions cannot be explained** — No transparency in AI reasoning
- **Errors cannot be traced** — No way to debug or improve
- **Conflicts go undetected** — Contradictory information causes failures
- **Compliance becomes impossible** — No audit trails for regulations
**Trustworthy AI requires semantic accountability.**
---
## 🆚 Semantica vs Traditional RAG
| Feature | Traditional RAG | Semantica |
|:--------|:----------------|:----------|
| **Reasoning** | ❌ Black-box answers | ✅ Explainable reasoning paths |
| **Provenance** | ❌ No provenance | ✅ Source-level provenance |
| **Search** | ⚠️ Vector similarity only | ✅ Semantic + graph reasoning |
| **Quality** | ❌ No conflict handling | ✅ Explicit contradiction detection |
| **Safety** | ⚠️ Unsafe for high-stakes | ✅ Designed for governed environments |
| **Compliance** | ❌ No audit trails | ✅ Audit-ready provenance |
---
## 🧩 Semantica Architecture
### 1️⃣ Input Layer — Governed Ingestion
- 📄 **Multiple Formats** — PDFs, DOCX, HTML, JSON, CSV, Excel, PPTX
- 🔧 **Docling Support** — Docling parser for table extraction
- 💾 **Data Sources** — Databases, APIs, streams, archives, web content
- 🎨 **Media Support** — Image parsing with OCR, audio/video metadata extraction
- 📊 **Single Pipeline** — Unified ingestion with metadata and source tracking
### 2️⃣ Semantic Layer — Trust & Reasoning Engine
- 🔍 **Entity Extraction** — NER, normalization, classification
- 🔗 **Relationship Discovery** — Triplet generation, semantic links
- 📐 **Ontology Induction** — Automated domain rule generation
- 🔄 **Deduplication** — Jaro-Winkler similarity, conflict resolution
-**Quality Assurance** — Conflict detection, validation
- 📊 **Provenance Tracking** — Source, time, confidence metadata
- 🧠 **Reasoning Traces** — Explainable inference paths
### 3️⃣ Output Layer — Auditable Knowledge Assets
- 📊 **Knowledge Graphs** — Queryable, temporal, explainable
- 📐 **OWL Ontologies** — HermiT/Pellet validated, custom ontology import support
- 🔢 **Vector Embeddings** — FastEmbed by default
- ☁️ **AWS Neptune** — Amazon Neptune graph database support
- 🔍 **Provenance** — Every AI response links back to:
- 📄 Source documents
- 🏷️ Extracted entities & relations
- 📐 Ontology rules applied
- 🧠 Reasoning steps used
---
## 🏥 Built for High-Stakes Domains
Designed for domains where **mistakes have real consequences** and **every decision must be accountable**:
- **🏥 Healthcare & Life Sciences** — Clinical decision support, drug interaction analysis, medical literature reasoning, patient safety compliance
- **💰 Finance & Risk** — Fraud detection, regulatory compliance (SOX, GDPR, MiFID II), credit risk assessment, algorithmic trading validation
- **⚖️ Legal & Compliance** — Evidence-backed legal research, contract analysis, regulatory change management, case law reasoning
- **🔒 Cybersecurity & Intelligence** — Threat attribution, incident response, security audit trails, intelligence analysis
- **🏛️ Government & Defense** — Governed AI systems, policy decisions, classified information handling, defense intelligence
- **🏭 Critical Infrastructure** — Power grid management, transportation safety, water treatment, emergency response
- **🚗 Autonomous Systems** — Self-driving vehicles, drone navigation, robotics safety, industrial automation
---
## 👥 Who Uses Semantica?
- **🤖 AI / ML Engineers** — Building explainable GraphRAG & agents
- **⚙️ Data Engineers** — Creating governed semantic pipelines
- **📊 Knowledge Engineers** — Managing ontologies & KGs at scale
- **🏢 Enterprise Teams** — Requiring trustworthy AI infrastructure
- **🛡️ Risk & Compliance Teams** — Needing audit-ready systems
---
## 📦 Installation
> **✅ Available on PyPI!** Semantica is now published on PyPI. Install it with a single command: `pip install semantica`
**Prerequisites:** Python 3.8+ (3.9+ recommended) • pip (latest version)
### Install from PyPI (Recommended)
```bash
# Install latest version from PyPI
pip install semantica
# Or install with optional dependencies
# or
pip install semantica[all]
# GitHub Workaround (if PyPI version has issues)
pip install git+https://github.com/Hawksight-AI/semantica.git@main
# Verify installation
python -c "from semantica.parse import DoclingParser; DoclingParser(); print('✓ Semantica ready')"
```
**Current Version:** [![PyPI version](https://badge.fury.io/py/semantica.svg)](https://pypi.org/project/semantica/) • [View on PyPI](https://pypi.org/project/semantica/)
!!! info "Windows PyTorch Note"
If you encounter PyTorch DLL errors on Windows, ensure you have the [Microsoft Visual C++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) installed. This is a common environment-specific issue with PyTorch on Windows and not a bug in Semantica.
### Install from Source (Development)
```bash
@@ -258,7 +290,7 @@ print(f" Ingested {len(sources)} sources")
### Document Parsing & Processing
> **Multi-format parsing** • **Text normalization** • **Intelligent chunking**
> **Multi-format parsing** • **Docling Support** • **Text normalization** • **Intelligent chunking**
```python
from semantica.parse import DocumentParser, DoclingParser
@@ -269,7 +301,7 @@ from semantica.split import TextSplitter
parser = DocumentParser()
parsed = parser.parse("document.pdf", format="auto")
# Enhanced parsing with Docling (recommended for complex layouts/tables)
# Parsing with Docling (for complex layouts/tables)
# Requires: pip install docling
docling_parser = DoclingParser(enable_ocr=True)
result = docling_parser.parse("complex_table.pdf")
@@ -360,7 +392,7 @@ results = vector_store.search(query="supply chain", top_k=5)
### Graph Store & Triplet Store
> **Neo4j, FalkorDB, Amazon Neptune support** • **SPARQL queries** • **RDF triplets**
> **Neo4j, FalkorDB, Amazon Neptune** • **SPARQL queries** • **RDF triplets**
```python
from semantica.graph_store import GraphStore
@@ -398,15 +430,19 @@ results = triplet_store.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT
### Ontology Generation & Management
> **6-Stage LLM Pipeline** • Automatic OWL Generation • HermiT/Pellet Validation
> **6-Stage LLM Pipeline** • Automatic OWL Generation • HermiT/Pellet Validation • **Custom Ontology Import** (OWL, RDF, Turtle, JSON-LD)
```python
from semantica.ontology import OntologyGenerator
from semantica.ingest import ingest_ontology
# Generate ontology automatically
generator = OntologyGenerator(llm_provider="openai", model="gpt-4")
ontology = generator.generate_from_documents(sources=["domain_docs/"])
print(f"Classes: {len(ontology.classes)}")
# Or import your existing ontology
custom_ontology = ingest_ontology("my_ontology.ttl") # Supports OWL, RDF, Turtle, JSON-LD
print(f"Classes: {len(custom_ontology.classes)}")
```
[**Cookbook: Ontology**](https://github.com/Hawksight-AI/semantica/tree/main/cookbook/introduction/14_Ontology.ipynb)
@@ -476,7 +512,7 @@ reasoned_result = context.query_with_reasoning(
### Knowledge Graph-Powered RAG (GraphRAG)
> **30% Accuracy Improvement** • Vector + Graph Hybrid Search • 91% Accuracy • **Multi-Hop Reasoning** • **LLM-Generated Responses**
> **Vector + Graph Hybrid Search** • **Multi-Hop Reasoning** • **LLM-Generated Responses** • **Semantic Re-ranking**
```python
from semantica.context import AgentContext
@@ -525,7 +561,7 @@ print(f"Confidence: {result['confidence']:.3f}")
from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM
import os
# Groq - Fast inference
# Groq
groq = Groq(
model="llama-3.1-8b-instant",
api_key=os.getenv("GROQ_API_KEY")
@@ -555,7 +591,7 @@ structured = groq.generate_structured("Extract entities from: Apple Inc. was fou
```
**Supported Providers:**
- **Groq**: Fast inference with Llama models
- **Groq**: Inference with Llama models
- **OpenAI**: GPT-3.5, GPT-4, and other OpenAI models
- **HuggingFace**: Local LLM inference with Transformers
- **LiteLLM**: Unified interface to 100+ LLM providers (OpenAI, Anthropic, Azure, Bedrock, Vertex AI, and more)
@@ -755,7 +791,7 @@ print(f"Found {len(results)} results")
#### Cybersecurity
- [**Real-Time Anomaly Detection**](cookbook/use_cases/cybersecurity/01_Real_Time_Anomaly_Detection.ipynb) - CVE RSS, Kafka streams, temporal KGs, sentence chunking
- [**Threat Intelligence Hybrid RAG**](cookbook/use_cases/cybersecurity/02_Threat_Intelligence_Hybrid_RAG.ipynb) - Security RSS, entity-aware chunking, enhanced GraphRAG, deduplication
- [**Threat Intelligence Hybrid RAG**](cookbook/use_cases/cybersecurity/02_Threat_Intelligence_Hybrid_RAG.ipynb) - Security RSS, entity-aware chunking, GraphRAG, deduplication
#### Intelligence & Law Enforcement
- [**Criminal Network Analysis**](cookbook/use_cases/intelligence/01_Criminal_Network_Analysis.ipynb) - OSINT RSS, deduplication, network centrality, graph analytics
@@ -772,12 +808,16 @@ print(f"Found {len(results)} results")
## 🔬 Advanced Features
**Docling Integration** — Document parsing with table extraction for PDFs, DOCX, PPTX, and XLSX files. Supports OCR and multiple export formats.
**AWS Neptune Support** — Amazon Neptune graph database integration with IAM authentication and OpenCypher queries.
**Custom Ontology Import** — Import existing ontologies (OWL, RDF, Turtle, JSON-LD, N3) and extend Schema.org, FOAF, Dublin Core, or custom ontologies.
**Incremental Updates** — Real-time stream processing with Kafka, RabbitMQ, Kinesis for live updates.
**Multi-Language Support** — Process multiple languages with automatic detection.
**Custom Ontology Import** — Import and extend Schema.org and custom ontologies.
**Advanced Reasoning** — Forward/backward chaining, Rete-based pattern matching, and automated explanation generation.
**Graph Analytics** — Centrality, community detection, path finding, temporal analysis.
@@ -867,20 +907,11 @@ git push origin feature/your-feature
4. **Feature Requests** - [Request feature](https://github.com/Hawksight-AI/semantica/issues/new)
### Contributors
<a href="https://github.com/Hawksight-AI/semantica/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Hawksight-AI/semantica" alt="Contributors" />
</a>
## 📜 License
Semantica is licensed under the **MIT License** - see the [LICENSE](https://github.com/Hawksight-AI/semantica/blob/main/LICENSE) file for details.
<div align="center">
**Built by the Semantica Community**
[GitHub](https://github.com/Hawksight-AI/semantica) • [Discord](https://discord.gg/pMHguUzG)
</div>
[GitHub](https://github.com/Hawksight-AI/semantica) • [Discord](https://discord.gg/RgaGTj9J)
@@ -288,6 +288,7 @@
" llm_model=\"llama-3.1-8b-instant\",\n",
" temperature=0.0,\n",
" api_key=GROQ_API_KEY,\n",
" max_retries=3,\n",
")\n",
"\n",
"ENTITY_TYPES = [\"ORGANIZATION\", \"PERSON\", \"MONEY\", \"PERCENT\", \"DATE\", \"EVENT\"]\n",
+5 -5
View File
@@ -12,22 +12,22 @@ How to cite Semantica in academic papers and research.
author = {Hawksight AI},
year = {2026},
url = {https://github.com/Hawksight-AI/semantica},
version = {0.2.3},
version = {0.2.5},
doi = {10.5281/zenodo.XXXXXXX}
}
```
### APA
Hawksight AI. (2026). *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering* (Version 0.2.3) [Computer software]. https://github.com/Hawksight-AI/semantica
Hawksight AI. (2026). *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering* (Version 0.2.5) [Computer software]. https://github.com/Hawksight-AI/semantica
### MLA
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.3, GitHub, 2026, https://github.com/Hawksight-AI/semantica.
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.5, GitHub, 2026, https://github.com/Hawksight-AI/semantica.
### Chicago
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.3. GitHub, 2026. https://github.com/Hawksight-AI/semantica.
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.5. GitHub, 2026. https://github.com/Hawksight-AI/semantica.
### IEEE
Hawksight AI, "Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering," Version 0.2.3, GitHub, 2026. [Online]. Available: https://github.com/Hawksight-AI/semantica
Hawksight AI, "Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering," Version 0.2.5, GitHub, 2026. [Online]. Available: https://github.com/Hawksight-AI/semantica
---
+2 -1
View File
@@ -9,7 +9,8 @@ document.addEventListener("DOMContentLoaded", function () {
// Define versions
var versions = [
{ name: "0.2.3", url: "#", current: true },
{ name: "0.2.4", url: "#", current: true },
{ name: "0.2.3", url: "#", current: false },
{ name: "0.2.2", url: "#", current: false },
{ name: "0.2.1", url: "#", current: false },
{ name: "0.2.0", url: "#", current: false },
+34
View File
@@ -63,6 +63,29 @@ The module uses several inference algorithms:
---
## Ontology Ingestion
Ingest existing ontology files directly into usable data structures using `OntologyIngestor`.
**Function:** `ingest_ontology(source, method="file")`
| Argument | Description |
|----------|-------------|
| `source` | File path, directory path, or list of paths |
| `method` | Ingestion method (default: "file") |
**Example:**
```python
from semantica.ontology import ingest_ontology
# Ingest file
data = ingest_ontology("ontology.ttl")
# Ingest directory
dataset = ingest_ontology("ontologies/")
```
## Main Classes
### OntologyEngine
@@ -170,6 +193,17 @@ Manages external dependencies.
| `import_external_ontology(uri, ontology)` | Load and merge external ontology |
| `evaluate_alignment(uri, ontology)` | Assess alignment and compatibility |
### OntologyIngestor
Handles ingestion of existing ontologies from files and directories.
**Methods:**
| Method | Description |
|--------|-------------|
| `ingest_ontology(file_path)` | Ingest a single ontology file |
| `ingest_directory(directory_path)` | Recursively ingest ontology files from a directory |
---
## Unified Engine Examples
+13 -16
View File
@@ -184,18 +184,15 @@ Core entity extraction implementation used by notebooks and lower-level integrat
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `method` | str or list | `"ml"` | Method(s): "ml", "llm", "pattern", "regex", "huggingface" |
| `silent_fail` | bool | `False` | Return empty list on error instead of raising (LLM only) |
| `max_text_length` | int | `64000` | Max text length for auto-chunking (LLM only) |
| `max_tokens` | int | `None` | Max output tokens for LLM generation |
| `max_workers` | int | `1` | Threads for parallel batch processing |
| `**config` | dict | `{}` | Method-specific config (e.g., `model`, `provider`) |
| `entity_types` | list | `None` | Filter for specific entity types |
| `**config` | dict | `{}` | Method-specific config (e.g., `model`, `aggregation_strategy`, `device`) |
**Methods:**
| Method | Description |
|--------|-------------|
| `extract(text)` | Alias for `extract_entities`. Get list of entities. |
| `extract_entities(text)` | Get list of entities |
| `extract(text, pipeline_id=None, **kwargs)` | Alias for `extract_entities`. Supports `max_workers`. |
| `extract_entities(text, pipeline_id=None, **kwargs)` | Get list of entities. Supports `max_workers`. |
**Example:**
@@ -231,18 +228,19 @@ Extracts relationships between entities.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `method` | str | `"dependency"` | Method: "dependency", "pattern", "cooccurrence", "huggingface", "llm" |
| `relation_types` | list | `None` | Specific relation types to extract |
| `bidirectional` | bool | `False` | Extract bidirectional relations |
| `confidence_threshold` | float | `0.6` | Minimum confidence score |
| `max_distance` | int | `50` | Max token distance between entities |
| `max_workers` | int | `1` | Threads for parallel batch processing |
| `**config` | dict | `{}` | Method-specific config (e.g., `model`, `device` for HuggingFace) |
**Methods:**
| Method | Description |
|--------|-------------|
| `extract(text, entities)` | Alias for `extract_relations`. Find links. |
| `extract_relations(text, entities)` | Find links |
| `extract(text, entities, pipeline_id=None, **kwargs)` | Alias for `extract_relations`. Supports `max_workers`. |
| `extract_relations(text, entities, pipeline_id=None, **kwargs)` | Find links. Supports `max_workers`. |
**Example:**
@@ -341,19 +339,18 @@ Extracts RDF triplets (Subject-Predicate-Object).
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `method` | str | `"pattern"` | Extraction method ("pattern", "rules", "huggingface", "llm") |
| `triplet_types` | list | `None` | Specific triplet types/predicates to extract |
| `include_temporal` | bool | `False` | Include time information |
| `include_provenance` | bool | `False` | Track source sentences |
| `method` | str | `"pattern"` | Extraction method ("pattern", "rules", "huggingface", "llm") |
| `silent_fail` | bool | `False` | Return empty list on error instead of raising (LLM only) |
| `max_text_length` | int | `64000` | Max text length for auto-chunking (LLM only) |
| `max_tokens` | int | `None` | Max output tokens for LLM generation |
| `max_workers` | int | `1` | Threads for parallel batch processing |
| `**kwargs` | dict | `{}` | Configuration options (e.g., `model`, `device`) |
**Methods:**
| Method | Description |
|--------|-------------|
| `extract_triplets(text)` | Get (S, P, O) tuples |
| `extract(text, entities=None, relations=None, pipeline_id=None, **kwargs)` | Alias for `extract_triplets`. Supports `max_workers`. |
| `extract_triplets(text, entities=None, relations=None, pipeline_id=None, **kwargs)` | Get (S, P, O) tuples. Supports `max_workers`. |
**Example:**
+5 -5
View File
@@ -1,12 +1,12 @@
# Vector Store
> **Unified vector database interface supporting FAISS, Weaviate, Qdrant, and Milvus with Hybrid Search.**
> **Unified vector database interface supporting FAISS, Weaviate, Qdrant, Pinecone, and Milvus with Hybrid Search.**
---
## 🎯 Overview
The **Vector Store Module** provides a unified interface for storing and searching vector embeddings. It supports multiple backends (FAISS, Weaviate, Qdrant, Milvus) and enables semantic search, RAG, and similarity matching.
The **Vector Store Module** provides a unified interface for storing and searching vector embeddings. It supports multiple backends (FAISS, Weaviate, Qdrant, Pinecone, Milvus) and enables semantic search, RAG, and similarity matching.
### What is a Vector Store?
@@ -18,7 +18,7 @@ A **vector store** is a database optimized for storing and searching high-dimens
### Why Use the Vector Store Module?
- **Multiple Backends**: Switch between FAISS (local), Weaviate, Qdrant, and Milvus
- **Multiple Backends**: Switch between FAISS (local), Weaviate, Qdrant, Pinecone, and Milvus
- **Unified Interface**: Same API regardless of backend
- **Hybrid Search**: Combine vector similarity with metadata filtering
- **Performance**: Optimized for high-throughput search operations
@@ -38,8 +38,8 @@ A **vector store** is a database optimized for storing and searching high-dimens
- :material-database:{ .lg .middle } **Multi-Backend Support**
---
Seamlessly switch between FAISS (Local), Weaviate, Qdrant, and Milvus
Seamlessly switch between FAISS (Local), Weaviate, Qdrant, Pinecone, and Milvus
- :material-magnify-plus:{ .lg .middle } **Hybrid Search**
+147
View File
@@ -0,0 +1,147 @@
"""
HuggingFace Local Model Usage Demo (Bring Your Own Model)
This script demonstrates how to use the 'semantica' library with local HuggingFace models
for Named Entity Recognition (NER), Relation Extraction (RE), and Triplet Extraction.
Prerequisites:
pip install transformers torch
Usage:
python examples/huggingface_demo.py
"""
import sys
import os
# Add project root to path (for running from this dir)
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from semantica.semantic_extract import NERExtractor, RelationExtractor, TripletExtractor, Entity
def demo_ner():
print("\n" + "="*50)
print("NER Demo: Bring Your Own Model (BYOM)")
print("="*50)
# 1. Initialize NERExtractor with HuggingFace method and a specific model
# Common models: "dslim/bert-base-NER", "dbmdz/bert-large-cased-finetuned-conll03-english"
model_name = "dslim/bert-base-NER"
print(f"Initializing NERExtractor with model: {model_name}...")
extractor = NERExtractor(
method="huggingface",
model=model_name,
device="cpu" # Use "cuda" for GPU
)
text = "Steve Jobs founded Apple Inc. in Cupertino, California on April 1, 1976."
print(f"\nInput text: {text}")
try:
# Note: This will download the model if not cached (approx 400MB)
print("Extracting entities (this may take a moment on first run)...")
entities = extractor.extract_entities(text)
print(f"\nExtracted {len(entities)} entities:")
for ent in entities:
print(f" - {ent.text:20} | Type: {ent.label:10} | Conf: {ent.confidence:.2f}")
except Exception as e:
print(f"Extraction failed (missing dependencies?): {e}")
def demo_relation():
print("\n" + "="*50)
print("Relation Extraction Demo: Local Model")
print("="*50)
# 1. Initialize RelationExtractor
# Note: Relation extraction usually requires a SequenceClassification model
# trained on relation datasets (e.g., TACRED, SemEval).
# For demo purposes, we'll use a generic placeholder or a widely used one.
model_name = "semantica/relation-model-v1" # This is hypothetical; replace with real model
print(f"Initializing RelationExtractor with method='huggingface'...")
extractor = RelationExtractor(
method="huggingface",
model=model_name,
device="cpu"
)
text = "Steve Jobs founded Apple Inc."
# Pre-defined entities are usually required for relation extraction
entities = [
Entity(text="Steve Jobs", label="PERSON", start_char=0, end_char=10),
Entity(text="Apple Inc.", label="ORG", start_char=19, end_char=29)
]
print(f"\nInput text: {text}")
print(f"Entities: {[e.text for e in entities]}")
try:
print("Extracting relations...")
# Note: This will fail if the model doesn't exist on HF Hub.
# In a real scenario, use a valid model ID like "some-user/bert-relation-extraction"
# For this demo, we just show the call structure.
relations = extractor.extract_relations(text, entities)
print(f"\nExtracted {len(relations)} relations:")
for rel in relations:
print(f" - {rel.subject.text} --[{rel.predicate}]--> {rel.object.text} (Conf: {rel.confidence:.2f})")
except Exception as e:
print(f"Note: Relation extraction mock run (model download might fail or be skipped): {e}")
def demo_triplet():
print("\n" + "="*50)
print("Triplet Extraction Demo: REBEL (Seq2Seq)")
print("="*50)
# 1. Initialize TripletExtractor with REBEL model
# REBEL is a popular model for end-to-end triplet extraction
model_name = "Babelscape/rebel-large"
print(f"Initializing TripletExtractor with model: {model_name}...")
extractor = TripletExtractor(
method="huggingface",
model=model_name,
device="cpu"
)
text = "Apple was founded by Steve Jobs in 1976."
print(f"\nInput text: {text}")
try:
print("Extracting triplets (this may take a moment)...")
triplets = extractor.extract_triplets(text)
print(f"\nExtracted {len(triplets)} triplets:")
for triplet in triplets:
print(f" - ({triplet.subject}, {triplet.predicate}, {triplet.object})")
except Exception as e:
print(f"Extraction failed (missing dependencies?): {e}")
if __name__ == "__main__":
print("Starting Semantica HuggingFace Usage Demo...")
print("Note: This script attempts to download models from Hugging Face Hub.")
print("Ensure you have an internet connection and 'transformers' installed.")
# Run demos
# We wrap in try-except to ensure the script doesn't crash the whole session if one fails
try:
demo_ner()
except Exception as e:
print(f"NER Demo Error: {e}")
try:
demo_relation()
except Exception as e:
print(f"Relation Demo Error: {e}")
try:
demo_triplet()
except Exception as e:
print(f"Triplet Demo Error: {e}")
+12 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "semantica"
version = "0.2.3"
version = "0.2.5"
description = "🧠 Semantica - An Open Source Framework for building Semantic Layers and Knowledge Engineering"
readme = "README.md"
license = { text = "MIT" }
@@ -114,6 +114,16 @@ graph-all = [
"semantica[graph-neo4j,graph-falkordb,graph-amazon-neptune]"
]
# ---- Vector Store Backends ----
vectorstore-qdrant = ["qdrant-client>=1.0.0"]
vectorstore-weaviate = ["weaviate-client>=4.0.0"]
vectorstore-pinecone = ["pinecone-client>=3.0.0"]
vectorstore-milvus = ["pymilvus>=2.0.0"]
vectorstore-all = [
"semantica[vectorstore-qdrant,vectorstore-weaviate,vectorstore-pinecone,vectorstore-milvus]"
]
# ---- Infra / Queues / Workers ----
infra = [
"redis>=4.3.0",
@@ -177,7 +187,7 @@ dev = [
# ---- Everything ----
all = [
"semantica[dev,viz,gpu,infra,cloud,monitoring,llm-all,models-huggingface,split-all,graph-all,parse-docling]"
"semantica[dev,viz,gpu,infra,cloud,monitoring,llm-all,models-huggingface,split-all,graph-all,vectorstore-all,parse-docling]"
]
# ---------------- ENTRYPOINTS ----------------
+1 -1
View File
@@ -10,7 +10,7 @@ Main exports:
- Config: Configuration management
"""
__version__ = "0.2.3"
__version__ = "0.2.5"
__author__ = "Semantica Contributors"
__license__ = "MIT"
+9
View File
@@ -86,6 +86,7 @@ Main Classes:
- RepoIngestor: Git repository processing
- EmailIngestor: Email protocol handling
- DBIngestor: Database export handling
- OntologyIngestor: Ontology file processing
- MethodRegistry: Registry for custom ingestion methods
- IngestConfig: Configuration manager for ingest module
@@ -98,6 +99,7 @@ Convenience Functions:
- ingest_repository: Repository ingestion wrapper
- ingest_email: Email ingestion wrapper
- ingest_database: Database ingestion wrapper
- ingest_ontology: Ontology ingestion wrapper
Example Usage:
@@ -134,6 +136,7 @@ from .methods import (
ingest_feed,
ingest_file,
ingest_mcp,
ingest_ontology,
ingest_repository,
ingest_stream,
ingest_web,
@@ -166,6 +169,8 @@ from .web_ingestor import (
WebIngestor,
)
from .ontology_ingestor import OntologyData, OntologyIngestor
__all__ = [
# File ingestion
"FileIngestor",
@@ -216,6 +221,9 @@ __all__ = [
"MCPClient",
"MCPResource",
"MCPTool",
# Ontology ingestion
"OntologyIngestor",
"OntologyData",
# Registry and Methods
"MethodRegistry",
"method_registry",
@@ -227,6 +235,7 @@ __all__ = [
"ingest_repository",
"ingest_email",
"ingest_database",
"ingest_ontology",
"ingest_mcp",
"get_ingest_method",
"list_available_methods",
+69
View File
@@ -150,6 +150,7 @@ from .email_ingestor import EmailData, EmailIngestor
from .feed_ingestor import FeedData, FeedIngestor
from .file_ingestor import FileIngestor, FileObject
from .mcp_ingestor import MCPData, MCPIngestor
from .ontology_ingestor import OntologyData, OntologyIngestor
from .registry import method_registry
from .repo_ingestor import RepoIngestor
from .stream_ingestor import StreamIngestor, StreamProcessor
@@ -537,6 +538,66 @@ def ingest_email(
raise
def ingest_ontology(
source: Union[str, Path, List[Union[str, Path]]], method: str = "file", **kwargs
) -> Union[OntologyData, List[OntologyData]]:
"""
Ingest ontology from source (convenience function).
This is a user-friendly wrapper that ingests ontologies using the specified method.
Args:
source: Ontology file path, directory path, or list of paths
method: Ingestion method (default: "file")
- "file": Single file ingestion
- "directory": Directory ingestion with recursive scanning
**kwargs: Additional options passed to OntologyIngestor
Returns:
OntologyData, List[OntologyData] with ingestion results
Examples:
>>> from semantica.ingest.methods import ingest_ontology
>>> ontology = ingest_ontology("ontology.ttl")
>>> ontologies = ingest_ontology("./ontologies", method="directory")
"""
# Check for custom method in registry
custom_method = method_registry.get("ontology", method)
if custom_method and custom_method != ingest_ontology:
try:
return custom_method(source, **kwargs)
except Exception as e:
logger.warning(
f"Custom method {method} failed: {e}, falling back to default"
)
try:
# Get config
config = ingest_config.get_method_config("ontology")
config.update(kwargs)
ingestor = OntologyIngestor(**config)
source_path = str(source) if isinstance(source, (str, Path)) else None
if method == "file" and source_path:
if isinstance(source, list):
return [ingestor.ingest_ontology(str(s), **kwargs) for s in source]
return ingestor.ingest_ontology(source_path, **kwargs)
elif method == "directory" and source_path:
recursive = kwargs.get("recursive", ingest_config.get("recursive", True))
return ingestor.ingest_directory(source_path, recursive=recursive, **kwargs)
else:
# Default: try as file
if isinstance(source, list):
return [ingestor.ingest_ontology(str(s), **kwargs) for s in source]
return ingestor.ingest_ontology(str(source), **kwargs)
except Exception as e:
logger.error(f"Failed to ingest ontology: {e}")
raise
def ingest_database(
source: Union[str, Dict[str, Any]], method: Optional[str] = None, **kwargs
) -> Union[TableData, List[TableData], Dict[str, Any]]:
@@ -769,6 +830,7 @@ def ingest(
- "repo": Repository ingestion
- "email": Email ingestion
- "db": Database ingestion
- "ontology": Ontology ingestion
method: Optional specific ingestion method
**kwargs: Additional options passed to ingestor
@@ -802,6 +864,8 @@ def ingest(
("git@", "https://github.com", "https://gitlab.com")
):
source_type = "repo"
elif source_str.endswith((".ttl", ".owl", ".rdf", ".jsonld", ".n3", ".nt")):
source_type = "ontology"
else:
source_type = "file"
else:
@@ -830,6 +894,8 @@ def ingest(
raise ProcessingError("Email ingestion requires configuration dictionary")
elif source_type == "db":
return {"data": ingest_database(sources, method=method, **kwargs)}
elif source_type == "ontology":
return {"ontology": ingest_ontology(sources, method=method or "file", **kwargs)}
elif source_type == "mcp":
return {"data": ingest_mcp(sources, method=method or "resources", **kwargs)}
else:
@@ -909,5 +975,8 @@ method_registry.register("mcp", "default", ingest_mcp)
method_registry.register("mcp", "resources", ingest_mcp)
method_registry.register("mcp", "tools", ingest_mcp)
method_registry.register("mcp", "all", ingest_mcp)
method_registry.register("ontology", "default", ingest_ontology)
method_registry.register("ontology", "file", ingest_ontology)
method_registry.register("ontology", "directory", ingest_ontology)
method_registry.register("ingest", "default", ingest)
method_registry.register("ingest", "unified", ingest)
+392
View File
@@ -0,0 +1,392 @@
"""
Ontology Ingestion Module
This module provides capabilities to ingest external ontologies from files (OWL, RDF, TTL, etc.)
and convert them into Semantica's internal ontology dictionary format.
Supported Formats:
- Turtle (.ttl): Terse RDF Triple Language. A concise, human-readable
format for representing RDF graphs. Commonly used for writing
ontologies by hand.
- RDF/XML (.rdf, .owl): The XML serialization of RDF. The standard
format for OWL (Web Ontology Language) ontologies and often used
for data interchange.
- JSON-LD (.jsonld): JSON for Linked Data. A lightweight Linked Data
format that is easy for humans to read and for machines to parse
and generate. Ideal for web-based applications.
- N-Triples (.nt): A line-based, plain text format for encoding an
RDF graph. Each line represents a single triple. Very simple to
parse but verbose.
- Notation3 (.n3): A superset of Turtle that adds features like logic
and rules.
Key Features:
- Support for multiple RDF formats (Turtle, RDF/XML, JSON-LD, N3, NT)
- Automatic parsing using rdflib
- Conversion to Semantica ontology structure
- Batch processing of ontology files
- Extraction of classes, properties, and metadata
Example Usage:
>>> from semantica.ingest import OntologyIngestor
>>> ingestor = OntologyIngestor()
>>> ontology = ingestor.ingest_ontology("my_ontology.ttl")
"""
import os
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import rdflib
from rdflib import RDF, RDFS, OWL, Graph
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
@dataclass
class OntologyData:
"""Ontology data representation."""
data: Dict[str, Any]
source_path: str
format: str
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class OntologyIngestor:
"""
Ontology ingestion handler.
This class parses OWL/RDF files and converts them to Semantica's ontology dictionary format.
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize ontology ingestor.
Args:
config: Optional configuration dictionary
**kwargs: Additional configuration parameters
"""
self.logger = get_logger("ontology_ingestor")
self.progress = get_progress_tracker()
self.config = config or {}
self.config.update(kwargs)
def ingest_ontology(self, file_path: Union[str, Path], format: Optional[str] = None, **kwargs) -> OntologyData:
"""
Ingest an ontology file.
Args:
file_path: Path to the ontology file (string or Path object)
format: Optional format hint (e.g., 'turtle', 'xml'). If None, rdflib guesses.
**kwargs: Additional arguments for rdflib parsing
Returns:
OntologyData object containing the parsed ontology and metadata
"""
file_path = Path(file_path)
# Track file ingestion
tracking_id = self.progress.start_tracking(
file=str(file_path),
module="ingest",
submodule="OntologyIngestor",
message=f"Ontology: {file_path.name}",
)
try:
# Validate file exists
if not file_path.exists():
raise ValidationError(f"File not found: {file_path}")
self.progress.update_tracking(tracking_id, message="Parsing RDF graph...")
g = Graph()
# Use provided format or let rdflib guess based on extension
parse_kwargs = kwargs.copy()
if format:
parse_kwargs['format'] = format
try:
g.parse(file_path, **parse_kwargs)
except Exception as e:
# Fallback: try to guess format from extension if not provided and initial parse failed
if not format:
ext = os.path.splitext(file_path)[1].lower()
fmt_map = {
'.ttl': 'turtle',
'.owl': 'xml', # OWL is often XML
'.rdf': 'xml',
'.jsonld': 'json-ld',
'.n3': 'n3',
'.nt': 'nt'
}
guessed_fmt = fmt_map.get(ext)
if guessed_fmt:
self.logger.info(f"Retrying with guessed format: {guessed_fmt}")
g.parse(file_path, format=guessed_fmt, **kwargs)
else:
raise e
else:
raise e
self.progress.update_tracking(tracking_id, message="Converting to internal format...")
# Determine format for metadata
used_format = format
if not used_format:
ext = os.path.splitext(file_path)[1].lower()
fmt_map = {
'.ttl': 'turtle',
'.owl': 'xml',
'.rdf': 'xml',
'.jsonld': 'json-ld',
'.n3': 'n3',
'.nt': 'nt'
}
used_format = fmt_map.get(ext, 'unknown')
ontology_dict = self._convert_to_dict(g, source_path=str(file_path), format=used_format)
ontology_data = OntologyData(
data=ontology_dict,
source_path=str(file_path),
format=used_format,
metadata=ontology_dict.get("metadata", {}).copy()
)
self.progress.stop_tracking(
tracking_id,
status="completed",
message=f"Successfully ingested ontology from {file_path}",
)
return ontology_data
except Exception as e:
self.logger.error(f"Failed to ingest ontology: {str(e)}")
self.progress.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise ProcessingError(f"Failed to ingest ontology: {str(e)}") from e
def ingest_directory(self, directory_path: Union[str, Path], recursive: bool = True, **kwargs) -> List[OntologyData]:
"""
Ingest all ontology files in a directory.
Args:
directory_path: Path to the directory (string or Path object)
recursive: Whether to search recursively
**kwargs: Additional arguments
Returns:
List of OntologyData objects
"""
directory_path = Path(directory_path)
ontologies = []
extensions = {'.ttl', '.owl', '.rdf', '.jsonld', '.n3', '.nt'}
# Track directory ingestion
tracking_id = self.progress.start_tracking(
file=str(directory_path),
module="ingest",
submodule="OntologyIngestor",
message=f"Directory: {directory_path.name}",
)
try:
if not directory_path.exists():
raise ValidationError(f"Directory not found: {directory_path}")
if not directory_path.is_dir():
raise ValidationError(f"Path is not a directory: {directory_path}")
files_to_process = []
for root, _, files in os.walk(directory_path):
for file in files:
ext = os.path.splitext(file)[1].lower()
if ext in extensions:
files_to_process.append(os.path.join(root, file))
if not recursive:
break
total_files = len(files_to_process)
self.progress.update_tracking(
tracking_id, message=f"Processing {total_files} ontology files"
)
for idx, file_path in enumerate(files_to_process, 1):
try:
ont_data = self.ingest_ontology(file_path, **kwargs)
ontologies.append(ont_data)
self.progress.update_progress(
tracking_id,
processed=idx,
total=total_files,
message=f"Processing {idx}/{total_files}: {Path(file_path).name}"
)
except Exception as e:
self.logger.warning(f"Skipping {file_path}: {e}")
self.progress.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested {len(ontologies)} ontologies",
)
return ontologies
except Exception as e:
self.progress.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise
def _convert_to_dict(self, graph: Graph, source_path: str, format: str = "unknown") -> Dict[str, Any]:
"""
Convert rdflib Graph to Semantica ontology dictionary.
Args:
graph: Parsed rdflib Graph
source_path: Source file path
format: Format of the ontology file
Returns:
Ontology dictionary
"""
ontology = {
"uri": "",
"name": os.path.basename(source_path),
"version": "1.0",
"classes": [],
"properties": [],
"metadata": {
"source_path": source_path,
"ingested_at": datetime.now().isoformat(),
"format": format
}
}
# 1. Extract Ontology Metadata
for s, p, o in graph.triples((None, RDF.type, OWL.Ontology)):
ontology["uri"] = str(s)
# Try to find label/comment/versionInfo
for _, _, label in graph.triples((s, RDFS.label, None)):
ontology["name"] = str(label)
for _, _, comment in graph.triples((s, RDFS.comment, None)):
ontology["description"] = str(comment)
for _, _, version in graph.triples((s, OWL.versionInfo, None)):
ontology["version"] = str(version)
# Break after first ontology definition found (usually only one per file)
break
# 2. Extract Classes
classes = {}
# Union of owl:Class and rdfs:Class
class_types = [OWL.Class, RDFS.Class]
for c_type in class_types:
for s, p, o in graph.triples((None, RDF.type, c_type)):
if isinstance(s, rdflib.BNode):
continue # Skip blank nodes for now
uri = str(s)
if uri not in classes:
cls_def = {
"uri": uri,
"name": self._get_local_name(uri),
"type": "class"
}
# Add label/comment
label = graph.value(s, RDFS.label)
if label:
cls_def["label"] = str(label)
cls_def["name"] = str(label) # Prefer label as name if available? Or keep URI fragment?
# Keeping local name from URI is safer for internal IDs, label for display.
# But Semantica seems to use "name" for the identifier in some examples.
# Let's keep name as local name or label if simple.
comment = graph.value(s, RDFS.comment)
if comment:
cls_def["description"] = str(comment)
# Superclasses
parents = []
for _, _, parent in graph.triples((s, RDFS.subClassOf, None)):
if isinstance(parent, rdflib.URIRef):
parents.append(str(parent))
if parents:
cls_def["parents"] = parents
classes[uri] = cls_def
ontology["classes"] = list(classes.values())
# 3. Extract Properties
properties = {}
# Object Properties
for s, p, o in graph.triples((None, RDF.type, OWL.ObjectProperty)):
self._add_property(graph, s, "object", properties)
# Datatype Properties
for s, p, o in graph.triples((None, RDF.type, OWL.DatatypeProperty)):
self._add_property(graph, s, "data", properties)
# RDF Properties (generic)
for s, p, o in graph.triples((None, RDF.type, RDF.Property)):
if str(s) not in properties: # Don't overwrite if already found as specific type
self._add_property(graph, s, "annotation", properties) # Default to annotation or generic
ontology["properties"] = list(properties.values())
return ontology
def _add_property(self, graph: Graph, subject: rdflib.term.Node, prop_type: str, properties_dict: Dict):
if isinstance(subject, rdflib.BNode):
return
uri = str(subject)
if uri in properties_dict:
return
prop_def = {
"uri": uri,
"name": self._get_local_name(uri),
"type": prop_type
}
label = graph.value(subject, RDFS.label)
if label:
prop_def["label"] = str(label)
comment = graph.value(subject, RDFS.comment)
if comment:
prop_def["description"] = str(comment)
# Domain and Range
domain = graph.value(subject, RDFS.domain)
if domain and isinstance(domain, rdflib.URIRef):
prop_def["domain"] = str(domain)
range_val = graph.value(subject, RDFS.range)
if range_val and isinstance(range_val, rdflib.URIRef):
prop_def["range"] = str(range_val)
properties_dict[uri] = prop_def
def _get_local_name(self, uri: str) -> str:
"""Extract local name from URI."""
if '#' in uri:
return uri.split('#')[-1]
return uri.split('/')[-1]
+8 -1
View File
@@ -109,12 +109,14 @@ Convenience Functions:
- create_associative_class: Associative class creation wrapper
- get_ontology_method: Get ontology method by name
- list_available_methods: List registered methods
- ingest_ontology: Ingest ontology from file or directory
Example Usage:
>>> from semantica.ontology import generate_ontology, infer_classes, OntologyGenerator
>>> from semantica.ontology import generate_ontology, infer_classes, OntologyGenerator, ingest_ontology
>>> # Using convenience functions
>>> ontology = generate_ontology({"entities": [...], "relationships": [...]}, method="default")
>>> classes = infer_classes(entities, method="default")
>>> data = ingest_ontology("ontology.ttl")
>>> # Using classes directly
>>> from semantica.ontology import OntologyGenerator, ClassInferrer, PropertyGenerator
>>> generator = OntologyGenerator(base_uri="https://example.org/ontology/")
@@ -155,6 +157,8 @@ from .registry import MethodRegistry, method_registry
from .requirements_spec import RequirementsSpec, RequirementsSpecManager
from .reuse_manager import ReuseDecision, ReuseManager
from .version_manager import OntologyVersion, VersionManager
from semantica.ingest import OntologyData, OntologyIngestor
from .methods import ingest_ontology
__all__ = [
# Main generators
@@ -200,4 +204,7 @@ __all__ = [
# Configuration
"OntologyConfig",
"ontology_config",
"ingest_ontology",
"OntologyData",
"OntologyIngestor",
]
+26 -3
View File
@@ -111,15 +111,19 @@ Main Functions:
- create_associative_class: Associative class creation wrapper
- get_ontology_method: Get ontology method by name
- list_available_methods: List registered methods
- ingest_ontology: Ingest ontology from file or directory (via semantica.ingest)
Example Usage:
>>> from semantica.ontology.methods import generate_ontology, infer_classes
>>> from semantica.ontology.methods import generate_ontology, infer_classes, ingest_ontology
>>> ontology = generate_ontology({"entities": [...], "relationships": [...]}, method="default")
>>> classes = infer_classes(entities, method="default")
>>> data = ingest_ontology("ontology.ttl")
"""
from typing import Any, Callable, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional, Union
from pathlib import Path
from semantica.ingest import ingest_ontology as _ingest_ontology, OntologyData
from .registry import method_registry
@@ -172,4 +176,23 @@ def list_available_methods(task: Optional[str] = None) -> Dict[str, List[str]]:
return method_registry.list_all(task)
pass
def ingest_ontology(
source: Union[str, Path, List[Union[str, Path]]],
method: str = "file",
**kwargs
) -> Union[OntologyData, List[OntologyData]]:
"""
Ingest ontology from source.
This is a convenience wrapper around semantica.ingest.ingest_ontology.
Args:
source: Ontology file path, directory path, or list of paths
method: Ingestion method (default: "file")
**kwargs: Additional options
Returns:
OntologyData or List[OntologyData]
"""
return _ingest_ontology(source, method=method, **kwargs)
+55
View File
@@ -42,6 +42,18 @@ classes = inferrer.infer_classes(entities, build_hierarchy=True)
properties = prop_gen.infer_properties(entities, relationships, classes)
```
### Ingesting Ontologies
```python
from semantica.ingest import OntologyIngestor
# Create ingestor
ingestor = OntologyIngestor()
# Ingest ontology
ontology_data = ingestor.ingest_ontology("ontology.ttl")
```
## Ontology Generation
### Basic Ontology Generation
@@ -115,6 +127,49 @@ ontology = engine.from_data(
)
```
## Ontology Ingestion
### Basic Ingestion
Ingest existing ontologies from files (Turtle, RDF/XML, JSON-LD, etc.) into `OntologyData` objects.
```python
from semantica.ontology import ingest_ontology
# Ingest a single file
ontology_data = ingest_ontology("path/to/ontology.ttl")
print(f"Source: {ontology_data.source_path}")
print(f"Format: {ontology_data.format}")
print(f"Data keys: {ontology_data.data.keys()}")
```
### Ingesting Directories
Ingest all ontology files in a directory recursively.
```python
from semantica.ontology import ingest_ontology
# Ingest a directory
ontologies = ingest_ontology("path/to/ontologies_dir/")
for ont in ontologies:
print(f"Ingested: {ont.source_path} ({ont.format})")
```
### Unified Ingestion Interface
You can also use the unified `semantica.ingest` interface.
```python
from semantica.ingest import ingest
# Ingest as "ontology" source type
result = ingest("path/to/ontology.ttl", source_type="ontology")
ontology_data = result["ontology"]
```
## Class Inference
### Basic Class Inference
+87
View File
@@ -363,3 +363,90 @@ class ReuseManager:
def list_known_ontologies(self) -> List[str]:
"""List known ontology URIs."""
return list(self.known_ontologies.keys())
def merge_ontology_data(
self, target: Dict[str, Any], source: Dict[str, Any], **options
) -> Dict[str, Any]:
"""
Merge source ontology data into target ontology.
Merges classes, properties, and metadata from source to target.
Handles deduplication based on URI and name.
Args:
target: Target ontology dictionary (modified in-place)
source: Source ontology dictionary
**options: Merge options:
- overwrite: Whether to overwrite existing elements (default: False)
- merge_metadata: Whether to merge metadata (default: True)
Returns:
Merged target ontology
"""
tracking_id = self.progress_tracker.start_tracking(
module="ontology",
submodule="ReuseManager",
message=f"Merging ontology {source.get('name', 'unknown')} into {target.get('name', 'unknown')}",
)
try:
overwrite = options.get("overwrite", False)
# Helper to merge lists of dicts (classes/properties)
def merge_lists(target_list, source_list, key_field="uri"):
existing_keys = {item.get(key_field): i for i, item in enumerate(target_list) if item.get(key_field)}
for item in source_list:
key = item.get(key_field)
if not key:
# Fallback to name if URI missing
key = item.get("name")
if key in existing_keys:
if overwrite:
target_list[existing_keys[key]] = item
else:
target_list.append(item)
if key:
existing_keys[key] = len(target_list) - 1
# Merge Classes
if "classes" in source:
if "classes" not in target:
target["classes"] = []
merge_lists(target["classes"], source["classes"])
# Merge Properties
if "properties" in source:
if "properties" not in target:
target["properties"] = []
merge_lists(target["properties"], source["properties"])
# Merge Metadata
if options.get("merge_metadata", True) and "metadata" in source:
if "metadata" not in target:
target["metadata"] = {}
# Update with source metadata, preserving target's specific fields if needed
# Here we just update
target["metadata"].update(source["metadata"])
# Merge Imports
if "imports" in source:
if "imports" not in target:
target["imports"] = []
for imp in source["imports"]:
if imp not in target["imports"]:
target["imports"].append(imp)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Merged ontology data successfully",
)
return target
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise
+205 -24
View File
@@ -19,6 +19,7 @@ Relation Extraction:
- "pattern": Pattern-based relation extraction
- "regex": Advanced regex-based relation extraction
- "cooccurrence": Co-occurrence based relation detection
- "similarity": Similarity-based relation extraction
- "dependency": Dependency parsing-based relation extraction
- "huggingface": HuggingFace relation extraction models
- "llm": LLM-based relation extraction
@@ -717,24 +718,148 @@ def extract_entities_huggingface(
device: Optional[str] = None,
**kwargs,
) -> List[Entity]:
"""HuggingFace entity extraction."""
"""
Extract entities using HuggingFace transformers.
Args:
text: Input text
model: Model name or path
device: Device to use (cpu, cuda, mps)
**kwargs: Additional arguments passed to the pipeline (e.g., aggregation_strategy)
"""
loader = HuggingFaceModelLoader(device=device)
model_obj = loader.load_ner_model(model)
# Pass kwargs (like aggregation_strategy) to load_ner_model
model_obj = loader.load_ner_model(model, **kwargs)
results = loader.extract_entities(model_obj, text)
entities = []
for result in results:
if isinstance(result, dict):
entities.append(
Entity(
text=result.get("word", result.get("entity", "")),
label=result.get("entity_group", result.get("label", "UNKNOWN")),
start_char=result.get("start", 0),
end_char=result.get("end", 0),
confidence=result.get("score", 1.0),
metadata={"model": model, "extraction_method": "huggingface"},
# Check if manual aggregation is needed (raw IOB tags detected)
needs_manual_aggregation = False
if results and isinstance(results[0], dict):
first_label = results[0].get("label", "")
# If we see B- tags and no entity_group (which implies aggregation wasn't done), we aggregate manually
if (first_label.startswith("B-") or first_label.startswith("I-")) and "entity_group" not in results[0]:
needs_manual_aggregation = True
if needs_manual_aggregation:
current_entity = None
for result in results:
label = result.get("label", "")
word = result.get("word", result.get("entity", ""))
score = result.get("score", 1.0)
start = result.get("start", 0)
end = result.get("end", 0)
# Clean word (handle BERT ## and RoBERTa Ġ)
clean_word = word.replace("##", "").replace("Ġ", "")
if not clean_word:
continue
# Determine tag type and entity type
tag_prefix = label[:2] if len(label) > 2 else ""
entity_type = label[2:] if len(label) > 2 else label
if tag_prefix == "B-":
# Save previous entity
if current_entity:
entities.append(current_entity)
# Start new entity
current_entity = Entity(
text=clean_word,
label=entity_type,
start_char=start,
end_char=end,
confidence=score,
metadata={
"model": model,
"extraction_method": "huggingface",
"source": "huggingface",
"raw_iob": True
},
)
)
elif tag_prefix == "I-" and current_entity:
# Check if type matches (loose check allows for some noise, strict check enforces type)
# We'll be lenient and allow continuation if it makes sense contextually,
# but ideally types should match.
if current_entity.label == entity_type:
# Append text
# Use offsets to determine spacing
if start > current_entity.end_char:
# If there's a gap, add space (unless it was a subword that got split but has gap? Unlikely)
# Usually gap means space.
# However, for ## subwords, start usually equals end.
# For Ġ, it implies space.
current_entity.text += " " + clean_word
else:
current_entity.text += clean_word
current_entity.end_char = end
# Update confidence (average)
current_entity.confidence = (current_entity.confidence + score) / 2
else:
# Type mismatch - treat as new entity or ignore?
# Treating as new B- is safer to avoid losing data
if current_entity:
entities.append(current_entity)
current_entity = Entity(
text=clean_word,
label=entity_type,
start_char=start,
end_char=end,
confidence=score,
metadata={
"model": model,
"extraction_method": "huggingface",
"source": "huggingface",
"raw_iob": True
},
)
else:
# O tag or I- without B or other cases
if current_entity:
entities.append(current_entity)
current_entity = None
# Append last entity
if current_entity:
entities.append(current_entity)
else:
# Standard processing for aggregated results or simple output
for result in results:
if isinstance(result, dict):
# Handle different output formats based on aggregation strategy
label = result.get("entity_group", result.get("label", "UNKNOWN"))
text_content = result.get("word", result.get("entity", ""))
# Clean up text content (remove ## for subwords if raw)
if "##" in text_content and "aggregation_strategy" not in kwargs:
text_content = text_content.replace("##", "")
if "Ġ" in text_content: # RoBERTa
text_content = text_content.replace("Ġ", " ").strip()
entities.append(
Entity(
text=text_content,
label=label,
start_char=result.get("start", 0),
end_char=result.get("end", 0),
confidence=result.get("score", 1.0),
metadata={
"model": model,
"extraction_method": "huggingface",
"source": "huggingface"
},
)
)
elif isinstance(result, list):
# Handle list of lists (sometimes returned by pipeline)
pass
return entities
@@ -1494,14 +1619,26 @@ def extract_relations_huggingface(
) -> List[Relation]:
"""HuggingFace relation extraction."""
loader = HuggingFaceModelLoader(device=device)
model_obj = loader.load_relation_model(model)
model_obj = loader.load_relation_model(model, **kwargs)
# This is simplified - actual implementation would depend on model architecture
results = loader.extract_relations(model_obj, text, entities)
# Pass kwargs (e.g. threshold)
results = loader.extract_relations(model_obj, text, entities, **kwargs)
relations = []
# Parse results based on model output format
# This is a placeholder - actual parsing would depend on the model
for result in results:
relations.append(
Relation(
subject=result["subject"],
predicate=result["relation"],
object=result["object"],
confidence=result.get("score", 1.0),
context=text,
metadata={
"model": model,
"extraction_method": "huggingface"
}
)
)
return relations
@@ -1513,6 +1650,7 @@ def extract_relations_llm(
silent_fail: bool = False,
max_text_length: Optional[int] = None,
structured_output_mode: str = "typed",
max_retries: int = 3,
**kwargs,
) -> List[Relation]:
"""
@@ -1525,6 +1663,7 @@ def extract_relations_llm(
model: LLM model
silent_fail: If True, return empty list on error. If False (default), raise exception.
max_text_length: Maximum text length before auto-chunking. None = provider default.
max_retries: Maximum number of retries for LLM calls (default: 3)
**kwargs: Additional options
"""
# Support llm_model parameter to disambiguate from ML model
@@ -1537,6 +1676,7 @@ def extract_relations_llm(
"model": model,
"max_text_length": max_text_length,
"structured_output_mode": structured_output_mode,
"max_retries": max_retries,
"relation_types": kwargs.get("relation_types"),
# Include entities hash/str in cache key implicitly via **cache_params
"entities_hash": hash(tuple(sorted([e.text for e in entities]))) if entities else 0
@@ -1608,6 +1748,7 @@ def extract_relations_llm(
return _extract_relations_chunked(
text, entities, provider=provider, model=model,
silent_fail=silent_fail, max_text_length=max_text_length,
max_retries=max_retries,
**kwargs
)
@@ -1679,6 +1820,8 @@ Entities found in text: {entities_str}"""
call_kwargs["temperature"] = kwargs["temperature"]
if "verbose" in kwargs:
call_kwargs["verbose"] = kwargs["verbose"]
call_kwargs["max_retries"] = max_retries
result_obj = llm.generate_typed(prompt, schema=RelationsResponse, **call_kwargs)
if verbose_mode:
@@ -1825,6 +1968,7 @@ def _extract_relations_chunked(
silent_fail: bool,
max_text_length: int,
structured_output_mode: str = "typed",
max_retries: int = 3,
**kwargs
) -> List[Relation]:
"""Internal helper to extract relations from long text by chunking."""
@@ -1867,6 +2011,7 @@ def _extract_relations_chunked(
silent_fail=False,
max_text_length=len(chunk.text) + 1,
structured_output_mode=structured_output_mode,
max_retries=max_retries,
**limited_kwargs
)
future_to_chunk[future] = i
@@ -1986,16 +2131,46 @@ def extract_triplets_huggingface(
) -> List[Triplet]:
"""HuggingFace triplet extraction."""
loader = HuggingFaceModelLoader(device=device)
model_obj = loader.load_triplet_model(model)
model_obj = loader.load_triplet_model(model, **kwargs)
# REBEL needs special tokens to be preserved
if "skip_special_tokens" not in kwargs:
kwargs["skip_special_tokens"] = False
results = loader.extract_triplets(model_obj, text, **kwargs)
triplets = []
for result in results:
# Parse result based on model output format
# This is a placeholder - actual parsing would depend on the model
if "triplet" in result:
# Parse triplet string (format depends on model)
pass
decoded_text = result["triplet"]
# Clean up common special tokens that might interfere or are noise
decoded_text = decoded_text.replace("<s>", "").replace("</s>", "").replace("<pad>", "")
# Parse REBEL format: <triplet> subject <subj> predicate <obj> object
# We use a non-greedy match and lookahead for next triplet or end of string
import re
pattern = r"<triplet>(?P<head>.*?)<subj>(?P<relation>.*?)<obj>(?P<tail>.*?)(?=<triplet>|$)"
matches = re.finditer(pattern, decoded_text)
for match in matches:
head = match.group("head").strip()
relation = match.group("relation").strip()
tail = match.group("tail").strip()
if head and relation and tail:
triplets.append(
Triplet(
subject=head,
predicate=relation,
object=tail,
confidence=0.9, # Model generation doesn't provide per-triplet confidence
metadata={
"model": model,
"extraction_method": "huggingface_rebel"
}
)
)
return triplets
@@ -2009,6 +2184,7 @@ def extract_triplets_llm(
silent_fail: bool = False,
max_text_length: Optional[int] = None,
structured_output_mode: str = "typed",
max_retries: int = 3,
**kwargs,
) -> List[Triplet]:
"""
@@ -2022,6 +2198,7 @@ def extract_triplets_llm(
model: LLM model
silent_fail: If True, return empty list on error. If False (default), raise exception.
max_text_length: Maximum text length before auto-chunking. None = provider default.
max_retries: Maximum number of retries for LLM calls (default: 3)
**kwargs: Additional options
"""
# Support llm_model parameter to disambiguate from ML model
@@ -2034,6 +2211,7 @@ def extract_triplets_llm(
"model": model,
"max_text_length": max_text_length,
"structured_output_mode": structured_output_mode,
"max_retries": max_retries,
"triplet_types": kwargs.get("triplet_types"),
# Include entities/relations hash in cache key implicitly via **cache_params
"entities_hash": hash(tuple(sorted([e.text for e in entities]))) if entities else 0,
@@ -2099,6 +2277,7 @@ def extract_triplets_llm(
return _extract_triplets_chunked(
text, provider=provider, model=model,
silent_fail=silent_fail, max_text_length=max_text_length,
max_retries=max_retries,
**kwargs
)
@@ -2141,7 +2320,9 @@ Text to extract from:
try:
# Use typed generation with Pydantic schema
result_obj = llm.generate_typed(prompt, schema=TripletsResponse, **kwargs)
call_kwargs = kwargs.copy()
call_kwargs["max_retries"] = max_retries
result_obj = llm.generate_typed(prompt, schema=TripletsResponse, **call_kwargs)
# Convert back to internal Triplet format
triplets = []
+5 -2
View File
@@ -364,8 +364,11 @@ class NERExtractor:
# Prepare method-specific options
method_options = all_options.copy()
if method_name == "huggingface":
method_options["model"] = all_options.get(
"huggingface_model", self.huggingface_model
# Prioritize runtime options over config/defaults
method_options["model"] = (
options.get("huggingface_model")
or options.get("model")
or self.huggingface_model
)
method_options["device"] = all_options.get("device")
elif method_name == "llm":
+133 -33
View File
@@ -1186,25 +1186,32 @@ class HuggingFaceModelLoader:
# Import torch at method level to ensure it's available
import torch
cache_key = f"{model_name}_ner"
# Include aggregation_strategy in cache key
agg_strategy = kwargs.get("aggregation_strategy", "simple")
cache_key = f"{model_name}_ner_{agg_strategy}"
if cache_key in self._cache:
return self._cache[cache_key]
try:
from transformers import pipeline
except ImportError:
raise ImportError(
"transformers library not installed. Install with: pip install semantica[models-huggingface]"
)
try:
nlp = pipeline(
"ner",
model=model_name,
device=self.device if torch.cuda.is_available() else -1,
aggregation_strategy="simple",
aggregation_strategy=agg_strategy,
tokenizer=kwargs.get("tokenizer") # Allow custom tokenizer
)
self._cache[cache_key] = nlp
return nlp
except (ImportError, OSError):
raise ImportError(
"transformers library not installed. Install with: pip install semantica[models-huggingface]"
)
except OSError as e:
self.logger.error(f"Failed to load NER model '{model_name}': {e}")
raise ValueError(f"Could not load HuggingFace model '{model_name}'. Check if model name is correct. Error: {e}")
except Exception as e:
self.logger.error(f"Failed to load NER model {model_name}: {e}")
raise
@@ -1219,19 +1226,31 @@ class HuggingFaceModelLoader:
return self._cache[cache_key]
try:
from transformers import pipeline
nlp = pipeline(
"text-classification",
model=model_name,
device=self.device if torch.cuda.is_available() else -1,
)
self._cache[cache_key] = nlp
return nlp
except (ImportError, OSError):
from transformers import pipeline, AutoTokenizer
except ImportError:
raise ImportError(
"transformers library not installed. Install with: pip install semantica[models-huggingface]"
)
try:
# Allow custom tokenizer
tokenizer = kwargs.get("tokenizer")
if not tokenizer and kwargs.get("tokenizer_name"):
tokenizer = AutoTokenizer.from_pretrained(kwargs.get("tokenizer_name"))
pipeline_kwargs = {
"model": model_name,
"device": self.device if torch.cuda.is_available() else -1,
}
if tokenizer:
pipeline_kwargs["tokenizer"] = tokenizer
nlp = pipeline("text-classification", **pipeline_kwargs)
self._cache[cache_key] = nlp
return nlp
except OSError as e:
self.logger.error(f"Failed to load relation model '{model_name}': {e}")
raise ValueError(f"Could not load HuggingFace model '{model_name}'. Check if model name is correct. Error: {e}")
except Exception as e:
self.logger.error(f"Failed to load relation model {model_name}: {e}")
raise
@@ -1244,18 +1263,27 @@ class HuggingFaceModelLoader:
try:
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, pipeline
except ImportError:
raise ImportError(
"transformers library not installed. Install with: pip install semantica[models-huggingface]"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
try:
# Allow custom tokenizer
tokenizer = kwargs.get("tokenizer")
if not tokenizer:
tokenizer_name = kwargs.get("tokenizer_name", model_name)
tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
model.to(self.device)
nlp = {"tokenizer": tokenizer, "model": model, "device": self.device}
self._cache[cache_key] = nlp
return nlp
except (ImportError, OSError):
raise ImportError(
"transformers library not installed. Install with: pip install semantica[models-huggingface]"
)
except OSError as e:
self.logger.error(f"Failed to load triplet model '{model_name}': {e}")
raise ValueError(f"Could not load HuggingFace model '{model_name}'. Check if model name is correct. Error: {e}")
except Exception as e:
self.logger.error(f"Failed to load triplet model {model_name}: {e}")
raise
@@ -1264,10 +1292,84 @@ class HuggingFaceModelLoader:
"""Extract entities using loaded model."""
return model(text)
def extract_relations(self, model, text: str, entities: List) -> List[Dict]:
"""Extract relations using loaded model."""
# This would need to be customized based on the model architecture
return model(text)
def extract_relations(self, model, text: str, entities: List, **kwargs) -> List[Dict]:
"""
Extract relations using loaded model.
Iterates through entity pairs and classifies the relationship.
"""
results = []
# Sort entities by position
sorted_entities = sorted(entities, key=lambda e: e.start_char)
# Marker configuration
subj_start = kwargs.get("subj_start_marker", "<subj>")
subj_end = kwargs.get("subj_end_marker", "</subj>")
obj_start = kwargs.get("obj_start_marker", "<obj>")
obj_end = kwargs.get("obj_end_marker", "</obj>")
# Iterate through all pairs
import itertools
for i, e1 in enumerate(sorted_entities):
for e2 in sorted_entities:
if e1 == e2:
continue
# Check distance (optional optimization)
# if abs(e1.start_char - e2.start_char) > 200: continue
# Format text with markers
# Strategy: [CLS] text with <subj>...</subj> and <obj>...</obj> [SEP]
# We need to insert markers into the original text
# Create a copy of text with markers inserted
# We need to handle offsets correctly.
# Simplest way: reconstruct string pieces
p1_start, p1_end = e1.start_char, e1.end_char
p2_start, p2_end = e2.start_char, e2.end_char
if p1_start < p2_start:
formatted_text = (
text[:p1_start] +
f"{subj_start} " + text[p1_start:p1_end] + f" {subj_end}" +
text[p1_end:p2_start] +
f"{obj_start} " + text[p2_start:p2_end] + f" {obj_end}" +
text[p2_end:]
)
else:
formatted_text = (
text[:p2_start] +
f"{obj_start} " + text[p2_start:p2_end] + f" {obj_end}" +
text[p2_end:p1_start] +
f"{subj_start} " + text[p1_start:p1_end] + f" {subj_end}" +
text[p1_end:]
)
# Predict
try:
# Pipeline returns [{'label': 'LABEL', 'score': 0.99}]
prediction = model(formatted_text, top_k=1)
if prediction:
res = prediction[0] if isinstance(prediction, list) else prediction
if isinstance(res, list): res = res[0] # top_k=1 returns list of dicts
label = res.get("label")
score = res.get("score")
# Filter "no_relation" or low confidence
if label != "no_relation" and score > kwargs.get("threshold", 0.5):
results.append({
"subject": e1,
"object": e2,
"relation": label,
"score": score
})
except Exception as e:
self.logger.warning(f"Relation prediction failed for pair {e1.text}-{e2.text}: {e}")
return results
def extract_triplets(self, model, text: str, **kwargs) -> List[Dict]:
"""Extract triplets using loaded model."""
@@ -1279,15 +1381,13 @@ class HuggingFaceModelLoader:
max_input_length = kwargs.get("max_input_length", 512)
max_length = kwargs.get("max_length", 128)
# Allow max_new_tokens as well
generate_kwargs = {"max_length": max_length}
if "max_new_tokens" in kwargs:
generate_kwargs["max_new_tokens"] = kwargs["max_new_tokens"]
# If max_new_tokens is set, we might want to remove max_length or ensure they don't conflict
# For Seq2Seq, max_length usually refers to the total length of the target sequence
# Pass other generation args
for param in ["num_beams", "temperature", "top_p", "top_k", "do_sample"]:
# Pass other generation args including beams and penalties
for param in ["num_beams", "temperature", "top_p", "top_k", "do_sample",
"length_penalty", "repetition_penalty"]:
if param in kwargs:
generate_kwargs[param] = kwargs[param]
@@ -1296,10 +1396,10 @@ class HuggingFaceModelLoader:
).to(device)
outputs = model_obj.generate(**inputs, **generate_kwargs)
decoded = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Allow controlling skip_special_tokens (important for REBEL which uses special tokens for delimiters)
skip_special_tokens = kwargs.get("skip_special_tokens", True)
decoded = tokenizer.decode(outputs[0], skip_special_tokens=skip_special_tokens)
# Parse decoded output (format depends on model)
# This is a placeholder - actual parsing would depend on model output format
return [{"triplet": decoded}]
@@ -408,8 +408,12 @@ class RelationExtractor:
method_options["relation_types"] = relation_types
if method_name == "huggingface":
method_options["model"] = all_options.get(
"huggingface_model", all_options.get("model")
# Prioritize runtime options over config/defaults
method_options["model"] = (
options.get("huggingface_model")
or options.get("model")
or self.config.get("huggingface_model")
or self.config.get("model")
)
method_options["device"] = all_options.get("device")
elif method_name == "llm":
@@ -113,9 +113,17 @@ extractor = NERExtractor(method="ml")
entities = extractor.extract(text)
print(f"ML method: {len(entities)} entities")
# HuggingFace model extraction
# HuggingFace model extraction (Bring Your Own Model)
extractor = NERExtractor(method="huggingface")
entities = extractor.extract(text, model="dslim/bert-base-NER")
# Use a specific model and aggregation strategy at runtime
# Runtime options override configuration defaults
entities = extractor.extract(
text,
model="dslim/bert-base-NER",
aggregation_strategy="max", # Options: "simple", "first", "average", "max"
device="cpu" # or "cuda"
)
print(f"HuggingFace method: {len(entities)} entities")
# LLM-based extraction with advanced options
@@ -229,9 +237,18 @@ relations = extractor.extract(text, entities=entities)
extractor = RelationExtractor(method="cooccurrence")
relations = extractor.extract(text, entities=entities)
# HuggingFace model
# HuggingFace model (Bring Your Own Model)
extractor = RelationExtractor(method="huggingface")
relations = extractor.extract(text, entities=entities, model="microsoft/DialoGPT-medium")
# Use a sequence classification model trained for relations
# The extractor automatically formats input with entity markers:
# "Steve Jobs founded Apple" -> "<subj> Steve Jobs </subj> founded <obj> Apple </obj>"
relations = extractor.extract(
text,
entities=entities,
model="semantica/relation-model-v1", # Replace with your model ID
device="cpu"
)
# LLM-based relation extraction
extractor = RelationExtractor(method="llm")
@@ -294,9 +311,16 @@ triplets = extractor.extract_triplets(text)
extractor = TripletExtractor(method="rules")
triplets = extractor.extract_triplets(text)
# HuggingFace model
# HuggingFace model (Seq2Seq / REBEL)
extractor = TripletExtractor(method="huggingface")
triplets = extractor.extract_triplets(text, model="t5-base")
# Use a Seq2Seq model like REBEL for end-to-end triplet extraction
# This method generates triplets directly from text without needing separate NER/RE steps
triplets = extractor.extract_triplets(
text,
model="Babelscape/rebel-large",
device="cpu"
)
# LLM-based triplet extraction
extractor = TripletExtractor(method="llm")
+36 -19
View File
@@ -366,8 +366,17 @@ class TripletExtractor:
from .ner_extractor import NERExtractor
from .relation_extractor import RelationExtractor
# Use method-based extraction
methods = options.get("method", self.method)
if isinstance(methods, str):
methods = [methods]
# Determine if we need to extract entities/relations based on method
# HuggingFace (Seq2Seq) does not need pre-extracted entities/relations
needs_entities_relations = any(m not in ["huggingface"] for m in methods)
# Extract entities if not provided
if entities is None:
if entities is None and needs_entities_relations:
self.progress_tracker.update_tracking(
tracking_id, message="Extracting entities..."
)
@@ -375,18 +384,23 @@ class TripletExtractor:
ner_config = self.config.get("ner", {})
if "ner_method" in self.config:
ner_config = {**ner_config, "method": self.config["ner_method"]}
# Filter out 'model' and 'huggingface_model' from shared config
# to prevent passing triplet model to NER extractor
shared_config = {
k: v
for k, v in self.config.items()
if k not in ["ner", "relation", "validator", "serializer", "quality", "model", "huggingface_model"]
}
self._ner_extractor = NERExtractor(
**ner_config,
**{
k: v
for k, v in self.config.items()
if k not in ["ner", "relation", "validator", "serializer", "quality"]
},
**shared_config,
)
entities = self._ner_extractor.extract_entities(text)
# Extract relations if not provided
if relations is None:
if relations is None and needs_entities_relations:
self.progress_tracker.update_tracking(
tracking_id, message="Extracting relations..."
)
@@ -394,21 +408,20 @@ class TripletExtractor:
rel_config = self.config.get("relation", {})
if "relation_method" in self.config:
rel_config = {**rel_config, "method": self.config["relation_method"]}
# Filter out 'model' and 'huggingface_model' from shared config
shared_config = {
k: v
for k, v in self.config.items()
if k not in ["ner", "relation", "validator", "serializer", "quality", "model", "huggingface_model"]
}
self._relation_extractor = RelationExtractor(
**rel_config,
**{
k: v
for k, v in self.config.items()
if k not in ["ner", "relation", "validator", "serializer", "quality"]
},
**shared_config,
)
relations = self._relation_extractor.extract_relations(text, entities)
# Use method-based extraction
methods = options.get("method", self.method)
if isinstance(methods, str):
methods = [methods]
triplet_types = options.get("triplet_types", self.triplet_types)
# Merge config with options
@@ -450,8 +463,12 @@ class TripletExtractor:
method_options["triplet_types"] = triplet_types
if method_name == "huggingface":
method_options["model"] = all_options.get(
"huggingface_model", all_options.get("model")
# Prioritize runtime options over config/defaults
method_options["model"] = (
options.get("huggingface_model")
or options.get("model")
or self.config.get("huggingface_model")
or self.config.get("model")
)
method_options["device"] = all_options.get("device")
elif method_name == "llm":
+10 -2
View File
@@ -3,7 +3,7 @@ Vector Store Management Module
This module provides comprehensive vector storage and retrieval capabilities for the
Semantica framework, including support for multiple vector store backends (FAISS,
Weaviate, Qdrant, Milvus), hybrid search combining vector similarity and
Weaviate, Qdrant, Pinecone, Milvus), hybrid search combining vector similarity and
metadata filtering, metadata management, and namespace isolation.
Algorithms Used:
@@ -58,6 +58,7 @@ Supported Backends:
- FAISS: In-memory/local disk (Facebook AI Similarity Search)
- Weaviate: Cloud/Self-hosted (Schema-aware vector database)
- Qdrant: Cloud/Self-hosted (Vector database for the next generation of AI)
- Pinecone: Cloud-managed (Managed vector database service)
- Milvus: Cloud/Self-hosted (Highly scalable vector database)
- InMemory: Simple list-based storage for testing/small datasets
@@ -73,7 +74,7 @@ Dependencies:
- pymilvus
Key Features:
- Multi-backend vector store support (FAISS, Weaviate, Qdrant, Milvus)
- Multi-backend vector store support (FAISS, Weaviate, Qdrant, Pinecone, Milvus)
- Vector indexing and similarity search
- Metadata indexing and filtering
- Hybrid search combining vector and metadata queries
@@ -91,6 +92,7 @@ Main Classes:
- FAISSStore: FAISS integration for local vector storage
- WeaviateStore: Weaviate vector database integration
- QdrantStore: Qdrant vector database integration
- PineconeStore: Pinecone vector database integration
- MilvusStore: Milvus vector database integration
- HybridSearch: Hybrid vector and metadata search
- MetadataStore: Metadata indexing and management
@@ -145,6 +147,7 @@ from .methods import (
)
from .milvus_store import MilvusStore, MilvusClient, MilvusCollection, MilvusSearch
from .namespace_manager import Namespace, NamespaceManager
from .pinecone_store import PineconeStore, PineconeClient, PineconeIndex, PineconeSearch
from .qdrant_store import QdrantStore, QdrantClient, QdrantCollection, QdrantSearch
from .registry import MethodRegistry, method_registry
from .vector_store import VectorIndexer, VectorManager, VectorRetriever, VectorStore
@@ -181,6 +184,11 @@ __all__ = [
"MilvusClient",
"MilvusCollection",
"MilvusSearch",
# Pinecone
"PineconeStore",
"PineconeClient",
"PineconeIndex",
"PineconeSearch",
# Hybrid search
"HybridSearch",
"MetadataFilter",
+639
View File
@@ -0,0 +1,639 @@
"""
Pinecone Store Module
This module provides Pinecone vector database integration for vector storage and
similarity search in the Semantica framework, supporting managed vector database
service with serverless and pod-based indexes, namespace isolation, and efficient
vector operations with metadata filtering.
Key Features:
- Serverless and Pod-based index management
- Namespace isolation for multi-tenant support
- Metadata filtering during search
- Batch operations for efficient data loading
- Index creation, deletion, and listing
- Optional dependency handling
Main Classes:
- PineconeStore: Main Pinecone store for vector operations
- PineconeClient: Pinecone client wrapper
- PineconeIndex: Index wrapper with operations
- PineconeSearch: Search operations and filtering
Example Usage:
>>> from semantica.vector_store import PineconeStore
>>> store = PineconeStore(api_key="your-api-key")
>>> store.connect()
>>> store.create_index("my-index", dimension=768)
>>> store.upsert_vectors(vectors, ids, metadata=metadata)
>>> results = store.search_vectors(query_vector, k=10, filter={"category": "science"})
>>> stats = store.get_stats()
Author: Semantica Contributors
License: MIT
"""
from typing import Any, Dict, List, Optional, Union
import numpy as np
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
# Optional Pinecone import
try:
from pinecone import Pinecone as PineconeClientLib, ServerlessSpec, PodSpec
PINECONE_AVAILABLE = True
except (ImportError, OSError):
PINECONE_AVAILABLE = False
PineconeClientLib = None
ServerlessSpec = None
PodSpec = None
class PineconeClient:
"""Pinecone client wrapper."""
def __init__(self, client: Any):
"""Initialize Pinecone client wrapper."""
self.client = client
self.logger = get_logger("pinecone_client")
def create_index(
self,
index_name: str,
dimension: int,
metric: str = "cosine",
spec: Optional[Dict[str, Any]] = None,
**options,
) -> bool:
"""Create an index in Pinecone."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
# Default to serverless spec if not provided
if spec is None:
spec = ServerlessSpec(cloud="aws", region="us-east-1")
# Map metric names
metric_map = {
"cosine": "cosine",
"euclidean": "euclidean_distance",
"dot": "dotproduct",
}
pinecone_metric = metric_map.get(metric.lower(), "cosine")
self.client.create_index(
name=index_name,
dimension=dimension,
metric=pinecone_metric,
spec=spec,
**options,
)
return True
except Exception as e:
raise ProcessingError(f"Failed to create index: {str(e)}")
def delete_index(self, index_name: str) -> bool:
"""Delete an index from Pinecone."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
self.client.delete_index(index_name)
return True
except Exception as e:
raise ProcessingError(f"Failed to delete index: {str(e)}")
def list_indexes(self) -> List[str]:
"""List available indexes."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
indexes = self.client.list_indexes()
return [index.name for index in indexes]
except Exception as e:
raise ProcessingError(f"Failed to list indexes: {str(e)}")
def get_index(self, index_name: str) -> Any:
"""Get index object."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
return self.client.Index(index_name)
except Exception as e:
raise ProcessingError(f"Failed to get index: {str(e)}")
class PineconeIndex:
"""Pinecone index wrapper."""
def __init__(self, index: Any):
"""Initialize Pinecone index wrapper."""
self.index = index
self.logger = get_logger("pinecone_index")
def upsert_vectors(
self,
vectors: List[List[float]],
ids: List[str],
metadata: Optional[List[Dict[str, Any]]] = None,
namespace: str = "",
**options,
) -> Dict[str, Any]:
"""Upsert vectors to index."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
# Prepare vectors for upsert
upsert_data = []
for i, (vector, vector_id) in enumerate(zip(vectors, ids)):
vector_dict = {"id": vector_id, "values": vector}
if metadata and i < len(metadata):
vector_dict["metadata"] = metadata[i]
upsert_data.append(vector_dict)
response = self.index.upsert(
vectors=upsert_data, namespace=namespace, **options
)
return {"upserted_count": response.upserted_count}
except Exception as e:
raise ProcessingError(f"Failed to upsert vectors: {str(e)}")
def search_vectors(
self,
query_vector: List[float],
k: int = 10,
filter: Optional[Dict[str, Any]] = None,
namespace: str = "",
**options,
) -> List[Dict[str, Any]]:
"""Search for similar vectors."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
response = self.index.query(
vector=query_vector,
top_k=k,
filter=filter,
namespace=namespace,
include_metadata=True,
include_values=False,
**options,
)
results = []
for match in response.matches:
results.append(
{
"id": match.id,
"score": match.score,
"metadata": match.metadata or {},
}
)
return results
except Exception as e:
raise ProcessingError(f"Failed to search vectors: {str(e)}")
def delete_vectors(
self, vector_ids: List[str], namespace: str = "", **options
) -> Dict[str, Any]:
"""Delete vectors from index."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
response = self.index.delete(ids=vector_ids, namespace=namespace, **options)
return {"deleted": True}
except Exception as e:
raise ProcessingError(f"Failed to delete vectors: {str(e)}")
def fetch_vectors(
self, vector_ids: List[str], namespace: str = "", **options
) -> Dict[str, Any]:
"""Fetch vectors by ID."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
response = self.index.fetch(ids=vector_ids, namespace=namespace, **options)
return {
"vectors": {
vector_id: {
"values": vector.values,
"metadata": vector.metadata or {},
}
for vector_id, vector in response.vectors.items()
}
}
except Exception as e:
raise ProcessingError(f"Failed to fetch vectors: {str(e)}")
def describe_index_stats(self, **options) -> Dict[str, Any]:
"""Get index statistics."""
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
stats = self.index.describe_index_stats(**options)
return {
"dimension": stats.dimension,
"index_fullness": stats.index_fullness,
"total_vector_count": stats.total_vector_count,
"namespaces": stats.namespaces,
}
except Exception as e:
raise ProcessingError(f"Failed to get index stats: {str(e)}")
class PineconeSearch:
"""Pinecone search operations."""
def __init__(self, index: PineconeIndex):
"""Initialize Pinecone search."""
self.index = index
self.logger = get_logger("pinecone_search")
def similarity_search(
self,
query_vector: np.ndarray,
limit: int = 10,
filter: Optional[Dict[str, Any]] = None,
namespace: str = "",
**options,
) -> List[Dict[str, Any]]:
"""
Perform similarity search.
Args:
query_vector: Query vector
limit: Number of results
filter: Metadata filter
namespace: Namespace to search in
**options: Additional options
Returns:
List of search results
"""
return self.index.search_vectors(
query_vector.tolist(), limit, filter, namespace, **options
)
class PineconeStore:
"""
Pinecone store for vector storage and similarity search.
Pinecone connection and authentication
Index and namespace management
Vector storage and retrieval
Similarity search and filtering
Performance optimization
Error handling and recovery
"""
def __init__(
self,
api_key: Optional[str] = None,
environment: Optional[str] = None,
**config,
):
"""Initialize Pinecone store."""
self.logger = get_logger("pinecone_store")
self.config = config
self.progress_tracker = get_progress_tracker()
# Ensure progress tracker is enabled
if not self.progress_tracker.enabled:
self.progress_tracker.enabled = True
self.api_key = api_key or config.get("api_key")
self.environment = environment or config.get("environment")
self.client: Optional[PineconeClient] = None
self.index: Optional[PineconeIndex] = None
self.search_engine: Optional[PineconeSearch] = None
# Check Pinecone availability
if not PINECONE_AVAILABLE:
self.logger.warning(
"Pinecone not available. Install with: pip install pinecone-client"
)
def connect(self, **kwargs) -> bool:
"""
Connect to Pinecone service.
Args:
**kwargs: Connection options
Returns:
True if connected successfully
"""
if not PINECONE_AVAILABLE:
raise ProcessingError(
"Pinecone is not available. Install it with: pip install pinecone-client"
)
api_key = kwargs.get("api_key") or self.api_key
if not api_key:
raise ValidationError("Pinecone API key is required")
try:
pinecone_client = PineconeClientLib(api_key=api_key, **kwargs)
self.client = PineconeClient(pinecone_client)
self.logger.info("Connected to Pinecone")
return True
except Exception as e:
raise ProcessingError(f"Failed to connect to Pinecone: {str(e)}")
def create_index(
self,
index_name: str,
dimension: int,
metric: str = "cosine",
spec: Optional[Dict[str, Any]] = None,
**kwargs,
):
"""
Create a Pinecone index.
Args:
index_name: Name of the index
dimension: Vector dimension
metric: Distance metric ("cosine", "euclidean", "dot")
spec: Index specification (ServerlessSpec or PodSpec)
**kwargs: Additional options
Returns:
PineconeIndex instance
"""
if self.client is None:
self.connect()
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
# Create index spec if not provided
if spec is None:
spec = ServerlessSpec(cloud="aws", region="us-east-1")
self.client.create_index(index_name, dimension, metric, spec, **kwargs)
# Get the index
pinecone_index = self.client.get_index(index_name)
self.index = PineconeIndex(pinecone_index)
self.search_engine = PineconeSearch(self.index)
self.logger.info(f"Created Pinecone index: {index_name}")
return self.index
except Exception as e:
raise ProcessingError(f"Failed to create index: {str(e)}")
def get_index(self, index_name: str) -> PineconeIndex:
"""
Get existing index.
Args:
index_name: Name of the index
Returns:
PineconeIndex instance
"""
if self.client is None:
self.connect()
if not PINECONE_AVAILABLE:
raise ProcessingError("Pinecone not available")
try:
pinecone_index = self.client.get_index(index_name)
self.index = PineconeIndex(pinecone_index)
self.search_engine = PineconeSearch(self.index)
return self.index
except Exception as e:
raise ProcessingError(f"Failed to get index: {str(e)}")
def delete_index(self, index_name: str) -> bool:
"""
Delete an index.
Args:
index_name: Name of the index to delete
Returns:
True if deleted successfully
"""
if self.client is None:
self.connect()
return self.client.delete_index(index_name)
def list_indexes(self) -> List[str]:
"""
List available indexes.
Returns:
List of index names
"""
if self.client is None:
self.connect()
return self.client.list_indexes()
def upsert_vectors(
self,
vectors: List[Any],
ids: List[str],
metadata: Optional[List[Dict[str, Any]]] = None,
namespace: str = "",
**options,
) -> Dict[str, Any]:
"""
Upsert vectors to index.
Args:
vectors: List of vectors
ids: Vector IDs
metadata: Optional metadata for each vector
namespace: Namespace to upsert into
**options: Additional options
Returns:
Upsert response
"""
tracking_id = self.progress_tracker.start_tracking(
module="vector_store",
submodule="PineconeStore",
message=f"Upserting {len(vectors)} vectors to Pinecone index",
)
try:
if self.index is None:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message="Index not initialized"
)
raise ProcessingError(
"Index not initialized. Call create_index() or get_index() first."
)
if not PINECONE_AVAILABLE:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message="Pinecone not available"
)
raise ProcessingError("Pinecone not available")
self.progress_tracker.update_tracking(
tracking_id, message="Preparing vectors..."
)
# Convert vectors to list format
vector_list = []
for vector in vectors:
if isinstance(vector, np.ndarray):
vector_list.append(vector.tolist())
else:
vector_list.append(list(vector))
self.progress_tracker.update_tracking(
tracking_id, message="Upserting vectors to index..."
)
result = self.index.upsert_vectors(
vector_list, ids, metadata, namespace, **options
)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Upserted {len(vectors)} vectors",
)
return result
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise ProcessingError(f"Failed to upsert vectors: {str(e)}")
def search_vectors(
self,
query_vector: Any,
k: int = 10,
filter: Optional[Dict[str, Any]] = None,
namespace: str = "",
**options,
) -> List[Dict[str, Any]]:
"""
Search vectors in index.
Args:
query_vector: Query vector
k: Number of results
filter: Metadata filter
namespace: Namespace to search in
**options: Additional options
Returns:
List of search results
"""
tracking_id = self.progress_tracker.start_tracking(
module="vector_store",
submodule="PineconeStore",
message=f"Searching for {k} similar vectors in Pinecone",
)
try:
if self.search_engine is None:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message="Index not initialized"
)
raise ProcessingError(
"Index not initialized. Call create_index() or get_index() first."
)
self.progress_tracker.update_tracking(
tracking_id, message="Performing similarity search..."
)
# Convert query vector to list
if isinstance(query_vector, np.ndarray):
query_vector = query_vector.tolist()
else:
query_vector = list(query_vector)
results = self.search_engine.similarity_search(
np.array(query_vector), k, filter, namespace, **options
)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Found {len(results)} similar vectors",
)
return results
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise
def delete_vectors(
self, vector_ids: List[str], namespace: str = "", **options
) -> Dict[str, Any]:
"""
Delete vectors from index.
Args:
vector_ids: Vector IDs to delete
namespace: Namespace to delete from
**options: Additional options
Returns:
Delete response
"""
if self.index is None:
raise ProcessingError(
"Index not initialized. Call create_index() or get_index() first."
)
return self.index.delete_vectors(vector_ids, namespace, **options)
def fetch_vectors(
self, vector_ids: List[str], namespace: str = "", **options
) -> Dict[str, Any]:
"""
Fetch vectors by ID.
Args:
vector_ids: Vector IDs to fetch
namespace: Namespace to fetch from
**options: Additional options
Returns:
Fetch response
"""
if self.index is None:
raise ProcessingError(
"Index not initialized. Call create_index() or get_index() first."
)
return self.index.fetch_vectors(vector_ids, namespace, **options)
def get_stats(self, **options) -> Dict[str, Any]:
"""Get index statistics."""
if self.index is None:
raise ProcessingError(
"Index not initialized. Call create_index() or get_index() first."
)
return self.index.describe_index_stats(**options)
+1 -1
View File
@@ -60,7 +60,7 @@ class VectorStore:
Provides vector store operations
"""
SUPPORTED_BACKENDS = {"faiss", "weaviate", "qdrant", "milvus", "inmemory"}
SUPPORTED_BACKENDS = {"faiss", "weaviate", "qdrant", "milvus", "pinecone", "inmemory"}
def __init__(self, backend="faiss", config=None, max_workers: int = 6, **kwargs):
"""Initialize vector store."""
+34 -1
View File
@@ -1,6 +1,6 @@
# Vector Store Module Usage Guide
This comprehensive guide demonstrates how to use the vector store module for vector storage and retrieval, supporting multiple vector store backends (FAISS, Weaviate, Qdrant, Milvus), hybrid search combining vector similarity and metadata filtering, metadata management, and namespace isolation.
This comprehensive guide demonstrates how to use the vector store module for vector storage and retrieval, supporting multiple vector store backends (FAISS, Weaviate, Qdrant, Pinecone, Milvus), hybrid search combining vector similarity and metadata filtering, metadata management, and namespace isolation.
## Table of Contents
@@ -754,6 +754,39 @@ results = store.search(
print(f"Found {len(results)} results")
```
### Pinecone Store
```python
from semantica.vector_store import PineconeStore
import numpy as np
# Create Pinecone store
store = PineconeStore(api_key="your-api-key")
# Connect
store.connect()
# Create index
store.create_index("my-index", dimension=768, metric="cosine")
# Upsert vectors
vectors = [np.random.rand(768).tolist() for _ in range(100)]
ids = [f"vec_{i}" for i in range(100)]
metadata = [{"category": "science"} for _ in range(100)]
store.upsert_vectors(vectors, ids, metadata=metadata, namespace="my-namespace")
# Search
query_vector = np.random.rand(768).tolist()
results = store.search_vectors(
query_vector,
k=10,
filter={"category": {"$eq": "science"}},
namespace="my-namespace"
)
print(f"Found {len(results)} results")
```
### Milvus Store
```python
+214
View File
@@ -0,0 +1,214 @@
import os
import shutil
import tempfile
import pytest
from pathlib import Path
from semantica.ingest import OntologyIngestor, ingest, ingest_ontology, OntologyData
class TestOntologyIngestor:
@pytest.fixture
def sample_ttl_content(self):
return """
@prefix : <http://example.org/ontology/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://example.org/ontology/> rdf:type owl:Ontology ;
rdfs:label "Test Ontology" .
:Person rdf:type owl:Class ;
rdfs:label "Person" .
:hasName rdf:type owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:string .
"""
def test_ingest_single_file(self, sample_ttl_content):
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write(sample_ttl_content)
tmp_path = tmp.name
try:
ingestor = OntologyIngestor()
result = ingestor.ingest_ontology(tmp_path)
assert isinstance(result, OntologyData)
assert result.data["name"] == "Test Ontology" or result.data["name"] == os.path.basename(tmp_path)
assert any(cls["name"] == "Person" for cls in result.data["classes"])
assert any(prop["name"] == "hasName" for prop in result.data["properties"])
assert result.metadata["format"] == "ttl" or result.metadata["format"] == "turtle"
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_ingest_directory(self, sample_ttl_content):
with tempfile.TemporaryDirectory() as tmp_dir:
# Create two ontology files
file1 = os.path.join(tmp_dir, "ont1.ttl")
file2 = os.path.join(tmp_dir, "ont2.rdf")
with open(file1, "w") as f:
f.write(sample_ttl_content)
# Simple RDF/XML content for the second file
rdf_content = """
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:owl="http://www.w3.org/2002/07/owl#">
<owl:Ontology rdf:about="http://example.org/ont2"/>
<owl:Class rdf:about="http://example.org/ont2/Animal"/>
</rdf:RDF>
"""
with open(file2, "w") as f:
f.write(rdf_content)
ingestor = OntologyIngestor()
results = ingestor.ingest_directory(tmp_dir)
assert len(results) == 2
assert all(isinstance(r, OntologyData) for r in results)
# Verify results contain expected classes
classes = [cls["name"] for res in results for cls in res.data["classes"]]
assert "Person" in classes
assert "Animal" in classes
def test_unified_ingest_function(self, sample_ttl_content):
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write(sample_ttl_content)
tmp_path = tmp.name
try:
# Test auto-detection via unified ingest
result = ingest(tmp_path)
assert "ontology" in result
assert isinstance(result["ontology"], OntologyData)
assert len(result["ontology"].data["classes"]) > 0
# Test explicit source type
result_explicit = ingest(tmp_path, source_type="ontology")
assert "ontology" in result_explicit
assert result_explicit["ontology"].metadata["source_path"] == tmp_path
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_convenience_function(self, sample_ttl_content):
with tempfile.NamedTemporaryFile(delete=False, suffix=".n3", mode="w") as tmp:
tmp.write(sample_ttl_content)
tmp_path = tmp.name
try:
result = ingest_ontology(tmp_path)
assert isinstance(result, OntologyData)
assert len(result.data["classes"]) > 0
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_ingest_formats(self):
"""Test ingestion of all supported formats."""
ingestor = OntologyIngestor()
# 1. JSON-LD
jsonld_content = """
{
"@context": {
"owl": "http://www.w3.org/2002/07/owl#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
},
"@id": "http://example.org/jsonld",
"@type": "owl:Ontology",
"rdfs:label": "JSON-LD Ontology",
"owl:versionInfo": "1.0"
}
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonld", mode="w") as tmp:
tmp.write(jsonld_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
assert result.data["name"] == "JSON-LD Ontology"
assert result.metadata["format"] == "json-ld"
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 2. N-Triples
nt_content = '<http://example.org/nt/Class> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .\n'
with tempfile.NamedTemporaryFile(delete=False, suffix=".nt", mode="w") as tmp:
tmp.write(nt_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
# N-Triples often doesn't have ontology metadata, so name might default to basename
assert result.data["name"] == os.path.basename(tmp_path)
assert any(cls["uri"] == "http://example.org/nt/Class" for cls in result.data["classes"])
assert result.metadata["format"] == "nt"
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 3. Notation3
n3_content = """
@prefix : <http://example.org/n3/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
:N3Class a owl:Class .
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".n3", mode="w") as tmp:
tmp.write(n3_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
assert any(cls["uri"] == "http://example.org/n3/N3Class" for cls in result.data["classes"])
# format might be 'n3' or 'turtle' depending on rdflib detection as they are similar
assert result.metadata["format"] in ["n3", "turtle"]
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 4. RDF/XML (.owl)
owl_content = """
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:owl="http://www.w3.org/2002/07/owl#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
<owl:Ontology rdf:about="http://example.org/owl"/>
<owl:Class rdf:about="http://example.org/owl/OwlClass">
<rdfs:label>OwlClass</rdfs:label>
</owl:Class>
</rdf:RDF>
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".owl", mode="w") as tmp:
tmp.write(owl_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
assert any(cls["name"] == "OwlClass" for cls in result.data["classes"])
assert result.metadata["format"] in ["xml", "rdf", "owl"]
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_error_handling(self):
ingestor = OntologyIngestor()
with pytest.raises(Exception): # Specific exception type depends on implementation, likely ValidationError or FileNotFoundError
ingestor.ingest_ontology("non_existent_file.ttl")
def test_invalid_content(self):
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write("This is not valid turtle content")
tmp_path = tmp.name
try:
ingestor = OntologyIngestor()
# Depending on implementation, this might raise an exception or return partial/empty result with error in metadata
# Given current implementation uses g.parse(), it likely raises an exception which is caught or propagated
# If propagated:
with pytest.raises(Exception):
ingestor.ingest_ontology(tmp_path)
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
+264
View File
@@ -0,0 +1,264 @@
import unittest
from unittest.mock import MagicMock, patch, ANY
import sys
import os
from typing import List, Optional
from pydantic import BaseModel
import importlib.util
# Add project root to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
# Mock external dependencies with __spec__ for importlib checks
mock_spacy = MagicMock()
mock_spacy.__spec__ = MagicMock()
sys.modules["spacy"] = mock_spacy
sys.modules["instructor"] = MagicMock()
sys.modules["groq"] = MagicMock()
# Better mock for openai
mock_openai = MagicMock()
mock_openai.__spec__ = MagicMock()
sys.modules["openai"] = mock_openai
# Mock sentence_transformers and transformers to avoid heavy imports and dependency checks
sys.modules["sentence_transformers"] = MagicMock()
mock_transformers = MagicMock()
mock_transformers.__spec__ = MagicMock()
sys.modules["transformers"] = mock_transformers
from semantica.semantic_extract import NERExtractor
from semantica.semantic_extract.methods import extract_entities_llm, _extract_entities_chunked, extract_relations_llm, extract_triplets_llm
from semantica.semantic_extract.providers import BaseProvider
class EntitiesResponse(BaseModel):
entities: List[dict]
class TestRetryLogic(unittest.TestCase):
def setUp(self):
self.mock_provider = MagicMock()
self.mock_provider.is_available.return_value = True
self.mock_provider.generate_typed.return_value = MagicMock(entities=[])
def test_ner_extractor_init_default(self):
"""Test default max_retries in NERExtractor"""
ner = NERExtractor(method="llm", provider="test")
# Check internal config, max_retries not in config means default behavior downstream
self.assertIsNone(ner.config.get("max_retries"))
def test_ner_extractor_init_custom(self):
"""Test custom max_retries in NERExtractor init"""
ner = NERExtractor(method="llm", provider="test", max_retries=5)
self.assertEqual(ner.config.get("max_retries"), 5)
@patch('semantica.semantic_extract.methods.create_provider')
def test_extract_entities_uses_init_value(self, mock_create_provider):
"""Test extract_entities uses initialized max_retries"""
mock_create_provider.return_value = self.mock_provider
ner = NERExtractor(method="llm", provider="test", max_retries=5)
ner.extract_entities("test text")
# Verify generate_typed called with max_retries=5
args, kwargs = self.mock_provider.generate_typed.call_args
self.assertEqual(kwargs.get("max_retries"), 5)
@patch('semantica.semantic_extract.methods.create_provider')
def test_extract_entities_override(self, mock_create_provider):
"""Test extract_entities override max_retries"""
mock_create_provider.return_value = self.mock_provider
ner = NERExtractor(method="llm", provider="test", max_retries=5)
# Override with 1
ner.extract_entities("test text", max_retries=1)
args, kwargs = self.mock_provider.generate_typed.call_args
self.assertEqual(kwargs.get("max_retries"), 1)
@patch('semantica.semantic_extract.methods.create_provider')
def test_chunked_extraction_propagation(self, mock_create_provider):
"""Test max_retries propagation in chunked extraction"""
mock_create_provider.return_value = self.mock_provider
# Patch TextSplitter where it lives
with patch('semantica.split.TextSplitter') as MockSplitter:
mock_splitter_instance = MockSplitter.return_value
# Mock split to return 2 chunks
mock_chunk1 = MagicMock()
mock_chunk1.text = "chunk1"
mock_chunk2 = MagicMock()
mock_chunk2.text = "chunk2"
mock_splitter_instance.split.return_value = [mock_chunk1, mock_chunk2]
# Force chunking by setting max_text_length small
extract_entities_llm(
"very long text",
provider="test",
model="test-model",
max_text_length=10, # Force chunking
max_retries=7,
structured_output_mode="typed"
)
# Check if generate_typed was called with max_retries=7 for chunks
# It should be called twice (once for each chunk)
self.assertEqual(self.mock_provider.generate_typed.call_count, 2)
# Check arguments of the calls
call_args_list = self.mock_provider.generate_typed.call_args_list
for args, kwargs in call_args_list:
self.assertEqual(kwargs.get("max_retries"), 7)
def test_provider_base_logic(self):
"""Test BaseProvider logic for max_retries with manual loop"""
provider = BaseProvider()
provider.client = MagicMock()
provider.logger = MagicMock()
provider.generate_structured = MagicMock(side_effect=Exception("Fail"))
# Mock instructor failing
with patch('semantica.semantic_extract.providers.instructor') as mock_instructor:
# Make instructor client fail
mock_client = MagicMock()
mock_client.chat.completions.create.side_effect = Exception("Instructor Fail")
mock_instructor.from_provider.return_value = mock_client
mock_instructor.from_openai.return_value = mock_client
# Run with max_retries=2
try:
provider.generate_typed("prompt", EntitiesResponse, max_retries=2)
except Exception:
pass
# Should try manual generation exactly 2 times
self.assertEqual(provider.generate_structured.call_count, 2)
def test_provider_zero_retries(self):
"""Test BaseProvider with max_retries=0"""
provider = BaseProvider()
provider.client = MagicMock()
provider.logger = MagicMock()
provider.generate_structured = MagicMock(side_effect=Exception("Fail"))
# Mock instructor failing
with patch('semantica.semantic_extract.providers.instructor') as mock_instructor:
mock_client = MagicMock()
mock_client.chat.completions.create.side_effect = Exception("Instructor Fail")
mock_instructor.from_provider.return_value = mock_client
mock_instructor.from_openai.return_value = mock_client
try:
provider.generate_typed("prompt", EntitiesResponse, max_retries=0)
except Exception:
pass
# Should NOT try manual generation loop (range(0) is empty)
self.assertEqual(provider.generate_structured.call_count, 0)
@patch('semantica.semantic_extract.methods.create_provider')
def test_relations_retry_propagation(self, mock_create_provider):
"""Test max_retries propagation in relation extraction"""
mock_create_provider.return_value = self.mock_provider
# Create a mock entity
mock_entity = MagicMock()
mock_entity.text = "entity"
mock_entity.start_char = 0
mock_entity.end_char = 5
extract_relations_llm(
"test text",
entities=[mock_entity],
provider="test",
max_retries=4
)
args, kwargs = self.mock_provider.generate_typed.call_args
self.assertEqual(kwargs.get("max_retries"), 4)
@patch('semantica.semantic_extract.methods.create_provider')
def test_relations_chunked_propagation(self, mock_create_provider):
"""Test max_retries propagation in chunked relation extraction"""
mock_create_provider.return_value = self.mock_provider
with patch('semantica.split.TextSplitter') as MockSplitter:
mock_splitter_instance = MockSplitter.return_value
mock_chunk1 = MagicMock()
mock_chunk1.text = "chunk1"
mock_chunk1.start_index = 0
mock_chunk1.end_index = 6
mock_splitter_instance.split.return_value = [mock_chunk1]
# Create a mock entity
mock_entity = MagicMock()
mock_entity.text = "entity"
mock_entity.start_char = 0
mock_entity.end_char = 5
extract_relations_llm(
"very long text",
entities=[mock_entity],
provider="test",
max_text_length=10,
max_retries=6
)
# Check call count - should be called for the chunk
# Note: _extract_relations_chunked creates a new future for each chunk
# which calls extract_relations_llm, which calls generate_typed
self.assertEqual(self.mock_provider.generate_typed.call_count, 1)
args, kwargs = self.mock_provider.generate_typed.call_args
self.assertEqual(kwargs.get("max_retries"), 6)
@patch('semantica.semantic_extract.methods.create_provider')
def test_triplets_retry_propagation(self, mock_create_provider):
"""Test max_retries propagation in triplet extraction"""
mock_create_provider.return_value = self.mock_provider
extract_triplets_llm(
"test text",
entities=[],
relations=[],
provider="test",
max_retries=7
)
args, kwargs = self.mock_provider.generate_typed.call_args
self.assertEqual(kwargs.get("max_retries"), 7)
@patch('semantica.semantic_extract.methods.create_provider')
def test_triplets_chunked_propagation(self, mock_create_provider):
"""Test max_retries propagation in chunked triplet extraction"""
mock_create_provider.return_value = self.mock_provider
with patch('semantica.split.TextSplitter') as MockSplitter:
mock_splitter_instance = MockSplitter.return_value
mock_chunk1 = MagicMock()
mock_chunk1.text = "chunk1"
mock_chunk1.start_index = 0
mock_chunk1.end_index = 6
mock_splitter_instance.split.return_value = [mock_chunk1]
# Use max_text_length > 100 to pass the minimum viable chunk size check
# and make text longer than that
extract_triplets_llm(
"very long text " * 20, # length > 101
entities=[],
relations=[],
provider="test",
max_text_length=101,
max_retries=8
)
# Check call count
self.assertEqual(self.mock_provider.generate_typed.call_count, 1)
args, kwargs = self.mock_provider.generate_typed.call_args
self.assertEqual(kwargs.get("max_retries"), 8)
if __name__ == "__main__":
unittest.main()
+168
View File
@@ -0,0 +1,168 @@
import unittest
from unittest.mock import MagicMock, patch
import sys
import os
# Add project root to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# Mock dependencies to avoid import hangs and external calls
sys.modules['spacy'] = MagicMock()
sys.modules['semantica.semantic_extract.methods'] = MagicMock()
sys.modules['semantica.utils.logging'] = MagicMock()
sys.modules['semantica.utils.progress_tracker'] = MagicMock()
sys.modules['semantica.semantic_extract.providers'] = MagicMock()
# Mock get_logger and get_progress_tracker
mock_logger = MagicMock()
sys.modules['semantica.utils.logging'].get_logger.return_value = mock_logger
mock_tracker = MagicMock()
sys.modules['semantica.utils.progress_tracker'].get_progress_tracker.return_value = mock_tracker
# Mock the methods module functions specifically
mock_methods = sys.modules['semantica.semantic_extract.methods']
mock_methods.get_entity_method = MagicMock()
mock_methods.get_relation_method = MagicMock()
mock_methods.get_triplet_method = MagicMock()
# Mock specific extraction functions
mock_extract_entities_hf = MagicMock()
mock_extract_relations_hf = MagicMock()
mock_extract_triplets_hf = MagicMock()
# Setup the registry mocks to return our mock functions
mock_methods.get_entity_method.return_value = mock_extract_entities_hf
mock_methods.get_relation_method.return_value = mock_extract_relations_hf
mock_methods.get_triplet_method.return_value = mock_extract_triplets_hf
# Now import the classes under test
# We need to patch where they import 'methods' locally if they do
with patch.dict(sys.modules):
from semantica.semantic_extract.ner_extractor import NERExtractor
from semantica.semantic_extract.relation_extractor import RelationExtractor
from semantica.semantic_extract.triplet_extractor import TripletExtractor
from semantica.semantic_extract.ner_extractor import Entity
from semantica.semantic_extract.relation_extractor import Relation
class TestExtractorsDispatch(unittest.TestCase):
def setUp(self):
self.mock_extract_entities_hf = mock_extract_entities_hf
self.mock_extract_relations_hf = mock_extract_relations_hf
self.mock_extract_triplets_hf = mock_extract_triplets_hf
self.mock_extract_entities_hf.reset_mock()
self.mock_extract_relations_hf.reset_mock()
self.mock_extract_triplets_hf.reset_mock()
# Configure mocks to return something iterable/valid
self.mock_extract_entities_hf.return_value = [MagicMock(spec=Entity, confidence=0.9, text="Test Entity")]
self.mock_extract_relations_hf.return_value = [MagicMock(spec=Relation, confidence=0.9)]
self.mock_extract_triplets_hf.return_value = [MagicMock(confidence=0.9)]
def test_ner_extractor_huggingface_dispatch(self):
print("\nTesting NERExtractor dispatch to HuggingFace...")
# Initialize with HuggingFace method
extractor = NERExtractor(method="huggingface")
# Call extract_entities
text = "Steve Jobs founded Apple."
# Use a specific model via kwargs
extractor.extract_entities(text, model="my-custom-ner-model")
# Verify get_entity_method was called with "huggingface"
mock_methods.get_entity_method.assert_called_with("huggingface")
# Verify the extraction function was called with correct model
# We need to check the call args to see if 'model' was passed correctly
# The logic we implemented: method_options["model"] = all_options.get("huggingface_model") or all_options.get("model") or self.huggingface_model
call_args = self.mock_extract_entities_hf.call_args
self.assertIsNotNone(call_args, "extract_entities_huggingface should have been called")
_, kwargs = call_args
self.assertEqual(kwargs.get("model"), "my-custom-ner-model", "Should use model passed in kwargs")
print("NERExtractor dispatch verified.")
def test_relation_extractor_huggingface_dispatch(self):
print("\nTesting RelationExtractor dispatch to HuggingFace...")
extractor = RelationExtractor(method="huggingface")
text = "Steve Jobs founded Apple."
entities = [MagicMock(spec=Entity)]
# Call extract_relations with explicit model
extractor.extract_relations(text, entities, model="my-relation-model")
# Verify dispatch
mock_methods.get_relation_method.assert_called_with("huggingface")
call_args = self.mock_extract_relations_hf.call_args
self.assertIsNotNone(call_args, "extract_relations_huggingface should have been called")
_, kwargs = call_args
self.assertEqual(kwargs.get("model"), "my-relation-model", "Should use model passed in kwargs")
print("RelationExtractor dispatch verified.")
def test_triplet_extractor_huggingface_dispatch(self):
print("\nTesting TripletExtractor dispatch to HuggingFace...")
extractor = TripletExtractor(method="huggingface")
text = "Steve Jobs founded Apple."
# Call extract_triplets with explicit model
extractor.extract_triplets(text, model="my-triplet-model")
# Verify dispatch
mock_methods.get_triplet_method.assert_called_with("huggingface")
call_args = self.mock_extract_triplets_hf.call_args
self.assertIsNotNone(call_args, "extract_triplets_huggingface should have been called")
_, kwargs = call_args
self.assertEqual(kwargs.get("model"), "my-triplet-model", "Should use model passed in kwargs")
print("TripletExtractor dispatch verified.")
def test_ner_extractor_huggingface_fallback(self):
print("\nTesting NERExtractor fallback logic...")
# Init with huggingface_model in config
extractor = NERExtractor(method="huggingface", huggingface_model="config-model")
extractor.extract_entities("text")
_, kwargs = self.mock_extract_entities_hf.call_args
self.assertEqual(kwargs.get("model"), "config-model", "Should prioritize huggingface_model from config")
# Now override with kwargs model
extractor.extract_entities("text", model="kwargs-model")
_, kwargs = self.mock_extract_entities_hf.call_args
self.assertEqual(kwargs.get("model"), "kwargs-model", "Should allow overriding config huggingface_model via model kwarg")
# Let's test passing 'huggingface_model' in kwargs
extractor.extract_entities("text", huggingface_model="override-model")
_, kwargs = self.mock_extract_entities_hf.call_args
self.assertEqual(kwargs.get("model"), "override-model", "Should allow overriding huggingface_model via kwargs")
def test_triplet_extractor_lazy_loading(self):
print("\nTesting TripletExtractor lazy loading for HuggingFace...")
# Initialize with HuggingFace method
extractor = TripletExtractor(method="huggingface")
# Check initial state
self.assertIsNone(extractor._ner_extractor)
self.assertIsNone(extractor._relation_extractor)
# Run extraction
extractor.extract_triplets("Steve Jobs founded Apple.")
# Check state AFTER extraction - should STILL be None because huggingface (REBEL) doesn't need them
self.assertIsNone(extractor._ner_extractor, "NERExtractor should not be initialized for HuggingFace method")
self.assertIsNone(extractor._relation_extractor, "RelationExtractor should not be initialized for HuggingFace method")
print("TripletExtractor lazy loading verified.")
if __name__ == '__main__':
unittest.main()
+167
View File
@@ -0,0 +1,167 @@
import unittest
from unittest.mock import MagicMock, patch
import sys
import os
# Ensure project root is in path
sys.path.append(os.getcwd())
from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
from semantica.semantic_extract.relation_extractor import RelationExtractor
from semantica.semantic_extract.triplet_extractor import TripletExtractor
from semantica.utils.exceptions import ProcessingError
class TestHuggingFaceDeepIntegration(unittest.TestCase):
"""
Comprehensive test suite for Hugging Face models integration
in NER, Relation, and Triplet extraction modules.
"""
@patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
def test_ner_extraction_flow(self, MockLoaderClass):
"""Test NER extraction with detailed IOB parsing and aggregation."""
mock_loader = MockLoaderClass.return_value
# Simulate IOB output (Raw token classification)
mock_loader.extract_entities.return_value = [
{"entity": "B-PER", "score": 0.99, "index": 1, "word": "John", "start": 0, "end": 4, "label": "B-PER"},
{"entity": "I-PER", "score": 0.98, "index": 2, "word": "Doe", "start": 5, "end": 8, "label": "I-PER"},
{"entity": "O", "score": 0.99, "index": 3, "word": "lives", "start": 9, "end": 14, "label": "O"},
{"entity": "B-LOC", "score": 0.95, "index": 4, "word": "New", "start": 18, "end": 21, "label": "B-LOC"},
{"entity": "I-LOC", "score": 0.96, "index": 5, "word": "York", "start": 22, "end": 26, "label": "I-LOC"},
]
extractor = NERExtractor(method="huggingface", huggingface_model="dslim/bert-base-NER")
entities = extractor.extract_entities("John Doe lives in New York")
# Verify aggregation worked (John Doe should be one entity)
# Note: The logic in extract_entities_huggingface handles manual aggregation
# if "entity_group" is missing and labels start with B-/I-
# Let's debug what we expect.
# "John" (B-PER) -> current_entity="John"
# "Doe" (I-PER) -> match! -> current_entity="John Doe"
# "lives" (O) -> append John Doe, current=None
# "New" (B-LOC) -> current="New"
# "York" (I-LOC) -> match! -> current="New York"
# End -> append New York
self.assertEqual(len(entities), 2)
person = next((e for e in entities if e.label == "PER"), None)
self.assertIsNotNone(person)
self.assertEqual(person.text, "John Doe")
loc = next((e for e in entities if e.label == "LOC"), None)
self.assertIsNotNone(loc)
self.assertEqual(loc.text, "New York")
@patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
def test_ner_aggregation_strategy_simple(self, MockLoaderClass):
"""Test NER extraction when the pipeline handles aggregation (strategy='simple')."""
mock_loader = MockLoaderClass.return_value
# Simulate Aggregated output
mock_loader.extract_entities.return_value = [
{"entity_group": "PER", "score": 0.99, "word": "John Doe", "start": 0, "end": 8},
{"entity_group": "LOC", "score": 0.95, "word": "New York", "start": 18, "end": 26},
]
extractor = NERExtractor(
method="huggingface",
huggingface_model="dslim/bert-base-NER",
aggregation_strategy="simple" # Explicitly requesting simple
)
entities = extractor.extract_entities("John Doe lives in New York")
self.assertEqual(len(entities), 2)
self.assertEqual(entities[0].text, "John Doe")
self.assertEqual(entities[0].label, "PER")
@patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
def test_relation_extraction_flow(self, MockLoaderClass):
"""Test Relation extraction with Hugging Face model."""
mock_loader = MockLoaderClass.return_value
# Mock extract_relations output
mock_loader.extract_relations.return_value = [{
"subject": Entity(text="Apple", label="ORG", start_char=0, end_char=5),
"object": Entity(text="Steve Jobs", label="PERSON", start_char=21, end_char=31),
"relation": "founded_by",
"score": 0.9
}]
# We need to provide entities for relation extraction usually
entities = [
Entity(text="Apple", label="ORG", start_char=0, end_char=5),
Entity(text="Steve Jobs", label="PERSON", start_char=21, end_char=31)
]
extractor = RelationExtractor(method="huggingface", huggingface_model="facebook/bart-large-mnli")
relations = extractor.extract_relations("Apple was founded by Steve Jobs", entities=entities)
# Check if relation is found
self.assertEqual(len(relations), 1)
self.assertEqual(relations[0].predicate, "founded_by")
self.assertEqual(relations[0].subject.text, "Apple")
self.assertEqual(relations[0].object.text, "Steve Jobs")
@patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
def test_triplet_extraction_rebel(self, MockLoaderClass):
"""Test Triplet extraction using REBEL parsing logic."""
mock_loader = MockLoaderClass.return_value
# Mock extract_triplets output
# The extract_triplets method in Loader returns [{"triplet": decoded_text}]
# But wait, methods.py extract_triplets_huggingface handles parsing?
# No, let's check methods.py again.
# Actually, methods.py for triplets calls loader.extract_triplets and then parses the result?
# Or does loader.extract_triplets return the raw generation?
# Let's check the code I read earlier.
# loader.extract_triplets returns [{"triplet": decoded}]
# But methods.py `extract_triplets_huggingface` logic needs to be verified.
# I didn't read extract_triplets_huggingface in methods.py yet (I read entities).
# Assuming standard behavior, let's return what loader returns.
mock_loader.extract_triplets.return_value = [{"triplet": "<triplet> Apple <subj> founded by <obj> Steve Jobs"}]
# Wait, if methods.py expects raw text and parses it, then I need to know IF methods.py does the parsing or if it expects pre-parsed.
# Usually, if it's REBEL, the parsing happens after generation.
# Let's assume methods.py parses the REBEL format.
extractor = TripletExtractor(method="huggingface", huggingface_model="Babelscape/rebel-large")
# If the extractor relies on methods.py to parse, and methods.py relies on REBEL format:
triplets = extractor.extract_triplets("Apple was founded by Steve Jobs")
# Note: If this fails, it might be because I need to check how extract_triplets_huggingface is implemented.
# But let's try.
if not triplets:
# Fallback: maybe methods.py expects the model to return parsed triplets?
pass
self.assertTrue(len(triplets) > 0)
self.assertEqual(triplets[0].subject, "Apple")
self.assertEqual(triplets[0].object, "Steve Jobs")
self.assertEqual(triplets[0].predicate, "founded by")
@patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
def test_byom_override(self, MockLoaderClass):
"""Verify Bring Your Own Model (runtime override) works for all extractors."""
mock_loader = MockLoaderClass.return_value
mock_loader.extract_entities.return_value = []
# NER
ner = NERExtractor(method="huggingface", huggingface_model="default-ner")
ner.extract_entities("test", huggingface_model="runtime-ner")
# Check if load_ner_model was called with runtime model
# mock_loader.load_ner_model.assert_called_with("runtime-ner", ...)
# args[0] should be "runtime-ner"
call_args = mock_loader.load_ner_model.call_args
self.assertEqual(call_args[0][0], "runtime-ner")
if __name__ == "__main__":
unittest.main()
+178
View File
@@ -0,0 +1,178 @@
import sys
import os
import traceback
from unittest.mock import MagicMock
print("Starting test script...", flush=True)
# Mock transformers and torch BEFORE any project imports
try:
mock_transformers = MagicMock()
mock_pipeline = MagicMock()
mock_transformers.pipeline = mock_pipeline
sys.modules["transformers"] = mock_transformers
sys.modules["torch"] = MagicMock()
sys.modules["torch"].cuda.is_available.return_value = False
# Mock spacy
mock_spacy = MagicMock()
sys.modules["spacy"] = mock_spacy
# Mock instructor
sys.modules["instructor"] = MagicMock()
# Also mock semantica.semantic_extract.config to avoid initialization issues
mock_config_module = MagicMock()
mock_config_instance = MagicMock()
# Setup default return values for config
mock_config_instance.get.return_value = {}
mock_config_instance.get_optimization_config.return_value = {"enable_cache": False}
mock_config_module.config = mock_config_instance
mock_config_module.Config = MagicMock(return_value=mock_config_instance)
sys.modules["semantica.semantic_extract.config"] = mock_config_module
print("Mocks setup complete.", flush=True)
except Exception as e:
print(f"Error setting up mocks: {e}", flush=True)
sys.exit(1)
# Add project root
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
print(f"Added to path: {sys.path[0]}", flush=True)
try:
print("Importing methods...", flush=True)
from semantica.semantic_extract.methods import extract_entities_huggingface, extract_relations_huggingface, extract_triplets_huggingface
print("Importing Entity class...", flush=True)
from semantica.semantic_extract.ner_extractor import Entity
print("Imports successful.", flush=True)
except Exception as e:
print(f"Import failed: {e}", flush=True)
traceback.print_exc()
sys.exit(1)
def test_enhanced_impl():
print("Testing enhanced implementation...", flush=True)
try:
# 1. Test NER with aggregation strategy
print("\n--- Testing NER ---", flush=True)
# Setup mock pipeline return value
mock_ner_pipeline = MagicMock()
mock_ner_pipeline.return_value = [
{"entity_group": "PERSON", "score": 0.99, "word": "Elon Musk", "start": 0, "end": 9},
]
# Configure pipeline side effect
def pipeline_side_effect(task, **kwargs):
if task == "ner": return mock_ner_pipeline
return MagicMock()
mock_pipeline.side_effect = pipeline_side_effect
# Test calling with aggregation_strategy
entities = extract_entities_huggingface(
"Elon Musk founded SpaceX.",
model="dslim/bert-base-NER",
aggregation_strategy="max"
)
print(f"Entities: {entities}", flush=True)
# Verify aggregation_strategy was passed
mock_pipeline.assert_any_call(
"ner",
model="dslim/bert-base-NER",
device=-1,
aggregation_strategy="max",
tokenizer=None
)
# 2. Test Relations with Input Formatting
print("\n--- Testing Relations ---", flush=True)
e1 = Entity(text="Elon Musk", label="PERSON", start_char=0, end_char=9)
e2 = Entity(text="SpaceX", label="ORG", start_char=18, end_char=24)
mock_rel_pipeline = MagicMock()
mock_rel_pipeline.return_value = [{"label": "founded", "score": 0.9}]
# Update pipeline mock to return rel pipeline
def pipeline_side_effect_rel(task, **kwargs):
if task == "ner": return mock_ner_pipeline
if task == "text-classification": return mock_rel_pipeline
return MagicMock()
mock_pipeline.side_effect = pipeline_side_effect_rel
relations = extract_relations_huggingface(
"Elon Musk founded SpaceX.",
entities=[e1, e2],
model="some-relation-model"
)
print(f"Relations: {relations}", flush=True)
# Verify input formatting
# Check if ANY call contained the correct formatting
found_match = False
for call in mock_rel_pipeline.call_args_list:
args, _ = call
if "<subj> Elon Musk </subj>" in args[0] and "<obj> SpaceX </obj>" in args[0]:
found_match = True
break
if not found_match:
print("Failed to find expected call args in:", flush=True)
for call in mock_rel_pipeline.call_args_list:
print(f" {call[0]}", flush=True)
assert found_match, "Did not find relation call with Elon Musk as subject"
# 3. Test Triplets with REBEL parsing
print("\n--- Testing Triplets ---", flush=True)
# Mock Tokenizer and Model
mock_tokenizer_instance = MagicMock()
mock_transformers.AutoTokenizer.from_pretrained.return_value = mock_tokenizer_instance
mock_tokenizer_instance.encode.return_value = MagicMock()
# Mock decode to return REBEL format
mock_tokenizer_instance.decode.return_value = "<s><triplet> Elon Musk <subj> founded <obj> SpaceX <triplet> SpaceX <subj> created <obj> Starship</s>"
mock_model_instance = MagicMock()
mock_transformers.AutoModelForSeq2SeqLM.from_pretrained.return_value = mock_model_instance
mock_model_instance.generate.return_value = [MagicMock()]
triplets = extract_triplets_huggingface(
"Elon Musk founded SpaceX and created Starship.",
model="Babelscape/rebel-large"
)
print(f"Triplets: {triplets}", flush=True)
# Verify parsing
assert len(triplets) == 2
assert triplets[0].subject == "Elon Musk"
assert triplets[0].predicate == "founded"
assert triplets[0].object == "SpaceX"
assert triplets[1].subject == "SpaceX"
assert triplets[1].predicate == "created"
assert triplets[1].object == "Starship"
# Verify skip_special_tokens=False was passed
mock_tokenizer_instance.decode.assert_called_with(
mock_model_instance.generate.return_value[0],
skip_special_tokens=False
)
except Exception as e:
print(f"Error during test execution: {e}", flush=True)
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
try:
test_enhanced_impl()
print("\nAll tests passed!", flush=True)
except Exception as e:
print(f"\nTest failed: {e}", flush=True)
traceback.print_exc()
+3 -62
View File
@@ -1,62 +1,3 @@
import unittest
from unittest.mock import MagicMock, patch
import os
import sys
# Ensure semantica is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
from semantica.vector_store.vector_store import VectorStore
from semantica.vector_store.registry import method_registry
from semantica.vector_store.config import vector_store_config
class TestPineconeRemoval(unittest.TestCase):
"""Verify that Pinecone has been completely removed from the system."""
def test_pinecone_backend_rejected(self):
"""Test that initializing VectorStore with backend='pinecone' raises an error."""
with self.assertRaises(ValueError) as context:
VectorStore(backend="pinecone")
# The error message might be generic "Unknown backend" or specific.
# We just want to ensure it fails.
self.assertTrue("pinecone" in str(context.exception).lower() or "unknown" in str(context.exception).lower())
def test_registry_clean(self):
"""Test that no Pinecone methods are registered."""
# Check all task types
task_types = ["store", "search", "index", "hybrid_search", "metadata", "namespace"]
for task in task_types:
methods = method_registry.list_all(task)
# Flatten if it's a dict
if isinstance(methods, dict):
method_names = methods.get(task, [])
else:
method_names = methods
for name in method_names:
self.assertNotIn("pinecone", name.lower(), f"Found pinecone reference in registry task {task}: {name}")
def test_config_clean(self):
"""Test that configuration does not contain Pinecone keys."""
config = vector_store_config.get_all()
for key in config.keys():
self.assertNotIn("pinecone", key.lower(), f"Found pinecone key in config: {key}")
def test_stores_existence(self):
"""Verify that other stores exist but PineconeStore does not."""
try:
from semantica.vector_store import faiss_store
from semantica.vector_store import weaviate_store
from semantica.vector_store import qdrant_store
from semantica.vector_store import milvus_store
except ImportError as e:
self.fail(f"Failed to import a required store: {e}")
with self.assertRaises(ImportError):
from semantica.vector_store import pinecone_store
if __name__ == '__main__':
unittest.main()
# This test file has been updated as Pinecone support has been re-added to Semantica.
# Pinecone is now a supported vector store backend (PR #220).
# See test_pinecone_store.py for Pinecone-specific tests.
+250
View File
@@ -0,0 +1,250 @@
import unittest
from unittest.mock import MagicMock, patch
import numpy as np
import sys
import os
# Ensure semantica is in path if running directly
if __name__ == "__main__":
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
try:
from semantica.vector_store.pinecone_store import (
PineconeStore,
PineconeClient,
PineconeIndex,
PineconeSearch,
PINECONE_AVAILABLE
)
from semantica.utils.exceptions import ProcessingError
except ImportError:
# If we can't import, we can't run these tests
# But we should not crash silently.
# We will define dummy classes if needed or fail loudly.
raise
class TestPineconeStore(unittest.TestCase):
"""Test Pinecone store functionality."""
def setUp(self):
self.mock_logger = MagicMock()
self.mock_tracker = MagicMock()
self.logger_patcher = patch('semantica.vector_store.pinecone_store.get_logger', return_value=self.mock_logger)
self.tracker_patcher = patch('semantica.vector_store.pinecone_store.get_progress_tracker', return_value=self.mock_tracker)
self.mock_logger_instance = self.logger_patcher.start()
self.mock_tracker_instance = self.tracker_patcher.start()
def tearDown(self):
self.logger_patcher.stop()
self.tracker_patcher.stop()
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', True)
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_connect(self, mock_pinecone_client):
"""Test connecting to Pinecone."""
mock_client_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
store = PineconeStore(api_key="test-key")
store.connect()
self.assertIsNotNone(store.client)
mock_pinecone_client.assert_called_once_with(api_key="test-key")
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', False)
def test_connect_unavailable(self):
"""Test connecting when Pinecone is not available."""
store = PineconeStore(api_key="test-key")
with self.assertRaises(ProcessingError):
store.connect()
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', True)
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_create_index(self, mock_pinecone_client):
"""Test creating an index."""
mock_client_instance = MagicMock()
mock_index_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
mock_client_instance.Index.return_value = mock_index_instance
store = PineconeStore(api_key="test-key")
store.connect()
# Mock the client's create_index method
store.client.create_index = MagicMock()
store.client.get_index = MagicMock(return_value=mock_index_instance)
result = store.create_index("test-index", dimension=768, metric="cosine")
self.assertIsInstance(result, PineconeIndex)
self.assertIsInstance(store.index, PineconeIndex)
self.assertIsInstance(store.search_engine, PineconeSearch)
store.client.create_index.assert_called_once()
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_upsert_vectors(self, mock_pinecone_client):
"""Test upserting vectors to Pinecone index."""
mock_client_instance = MagicMock()
mock_index_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
store = PineconeStore(api_key="test-key")
store.connect()
# Set up index
store.index = PineconeIndex(mock_index_instance)
store.index.upsert_vectors = MagicMock(return_value={"upserted_count": 2})
vectors = [np.array([0.1, 0.2, 0.3]), np.array([0.4, 0.5, 0.6])]
ids = ["id1", "id2"]
metadata = [{"key": "value1"}, {"key": "value2"}]
result = store.upsert_vectors(vectors, ids, metadata)
self.assertEqual(result["upserted_count"], 2)
store.index.upsert_vectors.assert_called_once()
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_search_vectors(self, mock_pinecone_client):
"""Test searching vectors in Pinecone index."""
mock_client_instance = MagicMock()
mock_index_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
store = PineconeStore(api_key="test-key")
store.connect()
# Set up search engine
store.search_engine = PineconeSearch(PineconeIndex(mock_index_instance))
store.search_engine.similarity_search = MagicMock(return_value=[
{"id": "id1", "score": 0.9, "metadata": {"key": "value1"}}
])
query_vector = np.array([0.1, 0.2, 0.3])
results = store.search_vectors(query_vector, k=5)
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["id"], "id1")
store.search_engine.similarity_search.assert_called_once()
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_delete_vectors(self, mock_pinecone_client):
"""Test deleting vectors from Pinecone index."""
mock_client_instance = MagicMock()
mock_index_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
store = PineconeStore(api_key="test-key")
store.connect()
# Set up index
store.index = PineconeIndex(mock_index_instance)
store.index.delete_vectors = MagicMock(return_value={"deleted": True})
result = store.delete_vectors(["id1", "id2"])
self.assertEqual(result["deleted"], True)
# Fix: assert called without the empty dict
store.index.delete_vectors.assert_called_once_with(["id1", "id2"], "")
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_fetch_vectors(self, mock_pinecone_client):
"""Test fetching vectors from Pinecone index."""
mock_client_instance = MagicMock()
mock_index_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
store = PineconeStore(api_key="test-key")
store.connect()
# Set up index
store.index = PineconeIndex(mock_index_instance)
store.index.fetch_vectors = MagicMock(return_value={
"vectors": {
"id1": {"values": [0.1, 0.2], "metadata": {"key": "value1"}}
}
})
result = store.fetch_vectors(["id1"])
self.assertIn("vectors", result)
# Fix: assert called without the empty dict
store.index.fetch_vectors.assert_called_once_with(["id1"], "")
class TestPineconeClient(unittest.TestCase):
"""Test PineconeClient wrapper."""
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', True)
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_create_index(self, mock_pinecone_client):
"""Test creating an index via PineconeClient."""
mock_client_instance = MagicMock()
mock_pinecone_client.return_value = mock_client_instance
client = PineconeClient(mock_client_instance)
client.create_index("test-index", 768, "cosine")
mock_client_instance.create_index.assert_called_once()
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', True)
@patch('semantica.vector_store.pinecone_store.PineconeClientLib')
def test_list_indexes(self, mock_pinecone_client):
"""Test listing indexes via PineconeClient."""
mock_client_instance = MagicMock()
mock_index_obj = MagicMock()
mock_index_obj.name = "test-index"
mock_client_instance.list_indexes.return_value = [mock_index_obj]
mock_pinecone_client.return_value = mock_client_instance
client = PineconeClient(mock_client_instance)
result = client.list_indexes()
self.assertEqual(result, ["test-index"])
class TestPineconeIndex(unittest.TestCase):
"""Test PineconeIndex wrapper."""
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', True)
def test_upsert_vectors(self):
"""Test upserting vectors via PineconeIndex."""
mock_index = MagicMock()
mock_response = MagicMock()
mock_response.upserted_count = 2
mock_index.upsert.return_value = mock_response
index = PineconeIndex(mock_index)
result = index.upsert_vectors(
[[0.1, 0.2], [0.3, 0.4]],
["id1", "id2"],
[{"key": "value1"}]
)
self.assertEqual(result["upserted_count"], 2)
mock_index.upsert.assert_called_once()
@patch('semantica.vector_store.pinecone_store.PINECONE_AVAILABLE', True)
def test_search_vectors(self):
"""Test searching vectors via PineconeIndex."""
mock_index = MagicMock()
mock_match = MagicMock()
mock_match.id = "id1"
mock_match.score = 0.9
mock_match.metadata = {"key": "value1"}
mock_response = MagicMock()
mock_response.matches = [mock_match]
mock_index.query.return_value = mock_response
index = PineconeIndex(mock_index)
result = index.search_vectors([0.1, 0.2], k=5)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["id"], "id1")
mock_index.query.assert_called_once()
if __name__ == '__main__':
print("DEBUG: Starting unittest.main()")
unittest.main()